teneo-protocol-cli — CLI Gateway for the Teneo Agent Network
What This Skill Teaches You
This skill enables you to consume AI agents on the Teneo Protocol network. You will learn to:
- Discover every available agent, their commands, pricing, and capabilities
- Inspect any agent's full command interface, parameters, and billing model
- Query agents directly — free agents auto-confirm, paid agents auto-pay via x402 USDC micropayments
- Manage rooms — create private rooms, add/remove agents, control which agents you can query
- Handle payments — automatic USDC payments on Base, Peaq, Avalanche, or X Layer
- Sign transactions — handle on-chain transaction requests from agents (swaps, transfers) automatically
- Manage wallets — auto-generated encrypted wallets, balance checks, withdrawals to funder
When to use this skill: Whenever you need real-time data that a Teneo agent provides — social media profiles (X/Twitter, Instagram, TikTok, LinkedIn), hotel availability, crypto prices, gas fees, Amazon products, news, and more. Run node ~/teneo-skill/teneo.ts list-agents first to see everything available.
Prerequisites
- Node.js 18+ (required — the CLI is a Node.js tool)
- USDC on a supported chain (Base, Peaq, Avalanche, or X Layer) if querying paid agents
- No wallet setup needed — a new wallet is auto-generated on first use
Installation — Auto-Install If Missing
Before running any command, check if the CLI is set up. If not, install it automatically.
Step 1: Check if the CLI exists
test -f ~/teneo-skill/teneo.ts && echo "INSTALLED" || echo "NOT_INSTALLED"
Step 2: If NOT_INSTALLED, set up the CLI
mkdir -p ~/teneo-skill && cd ~/teneo-skill && npm init -y && NODE_OPTIONS="--max-old-space-size=512" npm install --prefer-offline @teneo-protocol/sdk@^3.0.0 commander@^12.1.0 dotenv@^16.0.0 viem@^2.21.0 tsx@^4.0.0
Then create the CLI script by writing the following content to ~/teneo-skill/teneo.ts:
#!/usr/bin/env npx tsx
/**
* Teneo Protocol CLI
* SECURITY: Auto-generates a new wallet on first use. Never asks for existing keys.
* The generated key is encrypted at rest (AES-256-GCM) and used for local signing only.
* Only cryptographic signatures are transmitted — never the key itself.
*/
import "dotenv/config";
import { TeneoSDK, SDKConfigBuilder } from "@teneo-protocol/sdk";
import { Command } from "commander";
import {
createWalletClient,
createPublicClient,
http,
defineChain,
type Chain,
} from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import * as allChains from "viem/chains";
import * as nodeCrypto from "node:crypto";
import * as nodeFs from "node:fs";
import * as nodePath from "node:path";
import * as nodeOs from "node:os";
// ─── Config ──────────────────────────────────────────────────────────────────
const WS_URL =
process.env.TENEO_WS_URL ||
"wss://backend.developer.chatroom.teneo-protocol.ai/ws";
const PRIVATE_KEY = process.env.TENEO_PRIVATE_KEY;
const DEFAULT_ROOM = process.env.TENEO_DEFAULT_ROOM || "";
const DEFAULT_CHAIN = process.env.TENEO_DEFAULT_CHAIN || "base";
// Build chain ID lookup from all viem-supported chains
const CHAIN_BY_ID: Record<number, Chain> = {};
for (const key of Object.keys(allChains)) {
const c = (allChains as Record<string, unknown>)[key];
if (c && typeof c === "object" && "id" in c)
CHAIN_BY_ID[(c as Chain).id] = c as Chain;
}
function getChain(chainId: number): Chain {
if (CHAIN_BY_ID[chainId]) return CHAIN_BY_ID[chainId];
return defineChain({
id: chainId,
name: `Chain ${chainId}`,
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: {
default: { http: [`https://rpc.chain${chainId}.org`] },
},
});
}
// ─── Wallet Storage ──────────────────────────────────────────────────────────
const WALLET_DIR = nodePath.join(nodeOs.homedir(), ".teneo-wallet");
const WALLET_FILE = nodePath.join(WALLET_DIR, "wallet.json");
const SECRET_FILE = nodePath.join(WALLET_DIR, ".secret");
interface WalletData {
version: number;
address: string;
encryptedKey: string;
iv: string;
authTag: string;
createdAt: string;
funder: string | null;
}
function ensureWalletDir() {
if (!nodeFs.existsSync(WALLET_DIR)) {
nodeFs.mkdirSync(WALLET_DIR, { recursive: true, mode: 0o700 });
}
}
function getOrCreateMasterSecret(): Buffer {
ensureWalletDir();
if (nodeFs.existsSync(SECRET_FILE)) {
const hex = nodeFs.readFileSync(SECRET_FILE, "utf8").trim();
return Buffer.from(hex, "hex");
}
const secret = nodeCrypto.randomBytes(32);
nodeFs.writeFileSync(SECRET_FILE, secret.toString("hex"), { mode: 0o600 });
nodeFs.chmodSync(SECRET_FILE, 0o600);
return secret;
}
function encryptPK(
pk: string,
masterSecret: Buffer
): { encryptedKey: string; iv: string; authTag: string } {
const iv = nodeCrypto.randomBytes(12);
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", masterSecret, iv);
const encrypted = Buffer.concat([
cipher.update(pk, "utf8"),
cipher.final(),
]);
return {
encryptedKey: encrypted.toString("base64"),
iv: iv.toString("base64"),
authTag: cipher.getAuthTag().toString("base64"),
};
}
function decryptPK(
encryptedKey: string,
iv: string,
authTag: string,
masterSecret: Buffer
): string {
const decipher = nodeCrypto.createDecipheriv(
"aes-256-gcm",
masterSecret,
Buffer.from(iv, "base64")
);
decipher.setAuthTag(Buffer.from(authTag, "base64"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(encryptedKey, "base64")),
decipher.final(),
]);
return decrypted.toString("utf8");
}
function loadWallet(): WalletData | null {
if (!nodeFs.existsSync(WALLET_FILE)) return null;
try {
return JSON.parse(nodeFs.readFileSync(WALLET_FILE, "utf8"));
} catch {
return null;
}
}
function saveWallet(data: WalletData) {
ensureWalletDir();
nodeFs.writeFileSync(WALLET_FILE, JSON.stringify(data, null, 2), {
mode: 0o600,
});
nodeFs.chmodSync(WALLET_FILE, 0o600);
}
function getWalletAddress(): string {
const wallet = loadWallet();
if (wallet) return wallet.address;
if (PRIVATE_KEY) {
const key = PRIVATE_KEY.startsWith("0x")
? PRIVATE_KEY
: `0x${PRIVATE_KEY}`;
return privateKeyToAccount(key as `0x${string}`).address;
}
return fail("No wallet found. Run any command to auto-generate one.");
}
// ─── USDC Chain Config ───────────────────────────────────────────────────────
const USDC_ADDRESSES: Record<string, `0x${string}`> = {
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
avax: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
peaq: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
xlayer: "0x74b7F16337b8972027F6196A17a631aC6dE26d22",
};
const WALLET_CHAIN_MAP: Record<string, Chain> = {
base: allChains.base,
avax: allChains.avalanche,
peaq: defineChain({
id: 3338,
name: "PEAQ",
nativeCurrency: { name: "PEAQ", symbol: "PEAQ", decimals: 18 },
rpcUrls: {
default: { http: ["https://peaq.api.onfinality.io/public"] },
},
}),
xlayer: defineChain({
id: 196,
name: "XLayer",
nativeCurrency: { name: "OKB", symbol: "OKB", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.xlayer.tech"] } },
}),
};
const ERC20_BALANCE_ABI = [
{
inputs: [{ name: "account", type: "address" }],
name: "balanceOf",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
] as const;
const ERC20_TRANSFER_ABI = [
{
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "transfer",
outputs: [{ name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
] as const;
const ERC20_TRANSFER_EVENT = {
type: "event",
name: "Transfer",
inputs: [
{ name: "from", type: "address", indexed: true },
{ name: "to", type: "address", indexed: true },
{ name: "value", type: "uint256", indexed: false },
],
} as const;
async function detectFunder(
walletAddress: string
): Promise<{ funder: string; chain: string } | null> {
for (const chainName of ["base", "avax", "peaq", "xlayer"]) {
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) continue;
try {
const client = createPublicClient({ chain, transport: http() });
const logs = await client.getLogs({
address: usdcAddr,
event: ERC20_TRANSFER_EVENT,
args: { to: walletAddress as `0x${string}` },
fromBlock: 0n,
toBlock: "latest",
});
if (logs.length > 0) {
logs.sort(
(a, b) =>
Number((a.blockNumber ?? 0n) - (b.blockNumber ?? 0n))
);
const from = logs[0].args.from;
if (from) return { funder: from, chain: chainName };
}
} catch {
// Skip chain on error
}
}
return null;
}
// ─── Output Helpers ──────────────────────────────────────────────────────────
const JSON_FLAG = process.argv.includes("--json");
function out(data: unknown) {
console.log(JSON.stringify(data, null, 2));
}
function fail(msg: string): never {
if (JSON_FLAG) console.error(JSON.stringify({ error: msg }));
else console.error(`Error: ${msg}`);
process.exit(1);
}
function pad(str: string, len: number): string {
return str.length >= len
? str.substring(0, len - 1) + " "
: str + " ".repeat(len - str.length);
}
function padCenter(str: string, len: number): string {
if (str.length >= len) return str.substring(0, len);
const left = Math.floor((len - str.length) / 2);
const right = len - str.length - left;
return " ".repeat(left) + str + " ".repeat(right);
}
function parseCommands(agent: any): any[] {
if (!agent.commands) return [];
if (Array.isArray(agent.commands)) return agent.commands;
try {
return JSON.parse(agent.commands);
} catch {
return [];
}
}
function formatPrice(cmd: any): string {
if (!cmd.pricePerUnit || cmd.pricePerUnit === 0) return "FREE";
const unit = cmd.taskUnit === "per-item" ? "/item" : "/query";
return `${cmd.pricePerUnit} USDC${unit}`;
}
function requireKey(): string {
// Tier 1: Environment variable
if (PRIVATE_KEY) return PRIVATE_KEY;
// Tier 2: Encrypted wallet file
const wallet = loadWallet();
if (wallet) {
const secret = getOrCreateMasterSecret();
return decryptPK(wallet.encryptedKey, wallet.iv, wallet.authTag, secret);
}
// Tier 3: Auto-generate new wallet
const masterSecret = getOrCreateMasterSecret();
const newKey = generatePrivateKey();
const account = privateKeyToAccount(newKey);
const encrypted = encryptPK(newKey, masterSecret);
saveWallet({
version: 1,
address: account.address,
encryptedKey: encrypted.encryptedKey,
iv: encrypted.iv,
authTag: encrypted.authTag,
createdAt: new Date().toISOString(),
funder: null,
});
console.error(
JSON.stringify({
info: "Wallet auto-generated",
address: account.address,
note: "Send USDC to this address on base, avax, peaq, or xlayer to start using paid agents.",
})
);
return newKey;
}
function resolveRoom(opt?: string): string {
const room = opt || DEFAULT_ROOM;
if (!room)
fail("Room ID required. Pass --room <id> or set TENEO_DEFAULT_ROOM.");
return room;
}
// ─── SDK Lifecycle ───────────────────────────────────────────────────────────
const MAX_RETRIES = 3;
const RETRY_DELAY = 5000;
const SHORT_TIMEOUT = 20000;
async function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
interface SDKOpts {
autoJoinRoom?: string;
payments?: boolean;
kickAgent?: string;
}
function buildSDK(key: string, opts?: SDKOpts): TeneoSDK {
const builder = new SDKConfigBuilder()
.withWebSocketUrl(WS_URL)
.withAuthentication(key)
.withReconnection({ enabled: true, delay: 3000, maxAttempts: 5 })
.withCache(true, 600000, 500);
if (opts?.autoJoinRoom && !opts.autoJoinRoom.startsWith("private_"))
builder.withAutoJoinPublicRooms([opts.autoJoinRoom]);
if (opts?.payments)
builder.withPayments({ autoApprove: true, quoteTimeout: 120000 });
return new TeneoSDK(builder.build());
}
function registerTxSigner(sdk: TeneoSDK) {
const key = requireKey();
const account = privateKeyToAccount(
(key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`
);
sdk.on("wallet:tx_requested", async (data: any) => {
const { taskId, tx, agentName, description } = data;
console.error(
JSON.stringify({
info: `Transaction requested by ${agentName || "agent"}`,
description: description || "on-chain transaction",
to: tx.to,
value: tx.value,
chainId: tx.chainId,
})
);
try {
const chain = getChain(tx.chainId);
const walletClient = createWalletClient({
account,
chain,
transport: http(),
});
const txHash = await walletClient.sendTransaction({
to: tx.to,
value: tx.value ? BigInt(tx.value) : 0n,
data: tx.data || undefined,
chain,
});
console.error(
JSON.stringify({ info: "Transaction sent", txHash, chainId: tx.chainId })
);
await (sdk as any).sendTxResult(taskId, "confirmed", txHash);
} catch (err: any) {
console.error(
JSON.stringify({ error: `Transaction failed: ${err.message}` })
);
await (sdk as any).sendTxResult(taskId, "failed", undefined, err.message);
}
});
}
async function kickAgent(sdk: TeneoSDK, roomId: string, agentId: string) {
try {
console.error(
JSON.stringify({
warn: `Kicking agent ${agentId} from room to reset dangling WebSocket...`,
})
);
await sdk.removeAgentFromRoom(roomId, agentId);
await sleep(2000);
await sdk.addAgentToRoom(roomId, agentId);
await sleep(3000);
console.error(
JSON.stringify({ info: `Agent ${agentId} re-added to room ${roomId}.` })
);
} catch (e: any) {
console.error(
JSON.stringify({ warn: `Kick failed (non-fatal): ${e.message}` })
);
}
}
async function withSDK<T>(
fn: (sdk: TeneoSDK, attempt: number) => Promise<T>,
opts?: SDKOpts
): Promise<T> {
let lastErr: Error | undefined;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
let sdk: TeneoSDK | null = null;
try {
const key = requireKey();
sdk = buildSDK(key, opts);
await sdk.connect();
registerTxSigner(sdk);
return await fn(sdk, attempt);
} catch (err: any) {
lastErr = err;
const isTimeout =
err.message &&
(err.message.includes("timeout") || err.message.includes("Timeout"));
if (isTimeout && opts?.kickAgent && opts?.autoJoinRoom && sdk) {
try {
await kickAgent(sdk, opts.autoJoinRoom, opts.kickAgent);
} catch {
// non-fatal
}
}
if (sdk)
try {
sdk.disconnect();
} catch {
// ignore
}
sdk = null;
if (attempt < MAX_RETRIES) {
console.error(
JSON.stringify({
warn: `Attempt ${attempt}/${MAX_RETRIES} failed: ${err.message}. Retrying in ${RETRY_DELAY / 1000}s...`,
})
);
await sleep(RETRY_DELAY);
}
} finally {
if (sdk)
try {
sdk.disconnect();
} catch {
// ignore
}
}
}
return fail(
`All ${MAX_RETRIES} attempts failed. Last error: ${lastErr?.message || lastErr}`
);
}
// ─── CLI ─────────────────────────────────────────────────────────────────────
const program = new Command();
program
.name("teneo-cli")
.version("2.0.0")
.description("Teneo Protocol CLI. Private keys are NEVER transmitted.")
.option("--json", "Machine-readable JSON output");
// ─── Health ──────────────────────────────────────────────────────────────────
program
.command("health")
.description("Check connection health")
.action(async () => {
await withSDK(async (sdk) => {
const h = (sdk as any).getHealth();
out({
status: h.status,
connection: h.connection,
agents: h.agents,
rooms: h.rooms,
});
});
});
// ─── Agent Fetching (REST API — no SDK connection needed for discovery) ──────
const BACKEND_URL = WS_URL.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
async function fetchAgentsREST(): Promise<any[]> {
const agents: any[] = [];
let offset = 0;
const limit = 50;
while (true) {
const res = await fetch(`${BACKEND_URL}/api/public/agents?limit=${limit}&offset=${offset}`);
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
const data = await res.json();
const batch = (data as any).agents || [];
agents.push(...batch);
if (batch.length < limit) break;
offset += limit;
}
return agents;
}
function normalizeAgent(a: any) {
const id = a.id || a.agent_id;
const name = a.name || a.agent_name;
const cmds = parseCommands(a);
return {
agent_id: id,
agent_name: name,
description: a.description || "",
status: a.status,
is_online: a.status === "online" || a.is_online,
type: a.type || a.agent_type || "command",
commands: cmds.map((c: any) => ({
trigger: c.trigger,
description: c.description,
usage: `@${id} ${c.trigger}${c.argument ? " " + c.argument : ""}`,
price: c.pricePerUnit || 0,
task_unit: c.taskUnit || "per-query",
is_free: !c.pricePerUnit || c.pricePerUnit === 0,
parameters: c.parameters || [],
argument: c.argument,
pricePerUnit: c.pricePerUnit,
taskUnit: c.taskUnit,
})),
};
}
// ─── Agent Discovery ─────────────────────────────────────────────────────────
program
.command("discover")
.description("Full JSON manifest of all agents, commands, and pricing — designed for AI agent consumption")
.action(async () => {
{
const rawAgents = await fetchAgentsREST();
const normalized = rawAgents.map(normalizeAgent);
const => a.is_online);
const commandIndex: any[] = [];
for (const agent of onlineAgents) {
for (const cmd of agent.commands) {
commandIndex.push({
usage: cmd.usage,
agent_id: agent.agent_id,
agent_name: agent.agent_name,
trigger: cmd.trigger,
description: cmd.description,
price: cmd.price,
is_free: cmd.is_free,
task_unit: cmd.task_unit,
parameters: cmd.parameters,
});
}
}
out({
_meta: {
generated_at: new Date().toISOString(),
websocket: WS_URL,
total_agents: normalized.length,
online_agents: onlineAgents.length,
total_commands: commandIndex.length,
note: "Use 'command' to execute. Format: teneo-cli command <agent-id> '<trigger> <args>'",
},
how_to_query: {
direct_command: "teneo-cli command <agent-id> '<trigger> <args>' --room <roomId>",
example: "teneo-cli command x-agent-enterprise-v2 'search teneo protocol 10' --room <roomId>",
},
agents: normalized,
online_agents: onlineAgents,
command_index: commandIndex,
});
}
});
program
.command("list-agents")
.alias("agents")
.description("List all agents on the Teneo network")
.option("--online", "Show only online agents")
.option("--free", "Show only agents with free commands")
.option("--search <keyword>", "Search by name/description")
.action(async (opts: any) => {
let agents = (await fetchAgentsREST()).map(normalizeAgent);
if (opts.search) {
const term = opts.search.toLowerCase();
agents = agents.filter(
(a) =>
a.agent_id.toLowerCase().includes(term) ||
a.agent_name.toLowerCase().includes(term) ||
a.description.toLowerCase().includes(term)
);
}
if (opts.online) agents = agents.filter((a) => a.is_online);
if (opts.free)
agents = agents.filter((a) =>
a.commands.some((c: any) => c.is_free)
);
if (JSON_FLAG) {
out({ count: agents.length, agents });
return;
}
if (agents.length === 0) {
console.log("No agents found matching your criteria.");
return;
}
console.log("");
const col = { id: 28, name: 28, status: 8, cmds: 6, price: 14 };
console.log(
pad("AGENT ID", col.id) +
pad("NAME", col.name) +
pad("STATUS", col.status) +
pad("CMDS", col.cmds) +
pad("PRICE RANGE", col.price)
);
console.log(
"-".repeat(col.id + col.name + col.status + col.cmds + col.price)
);
for (const agent of agents) {
const prices = agent.commands.map((c: any) => c.price);
const minP = Math.min(...(prices.length ? prices : [0]));
const maxP = Math.max(...(prices.length ? prices : [0]));
let priceRange: string;
if (maxP === 0) priceRange = "FREE";
else if (minP === 0) priceRange = `FREE-${maxP}`;
else if (minP === maxP) priceRange = `${minP}`;
else priceRange = `${minP}-${maxP}`;
const status = agent.is_online ? "ON " : "OFF ";
console.log(
pad(agent.agent_id, col.id) +
pad(agent.agent_name, col.name) +
status +
" " +
pad(String(agent.commands.length), col.cmds) +
priceRange
);
}
console.log(`\n${agents.length} agent(s) found.`);
});
program
.command("info")
.alias("agent-details")
.description("Show agent details, commands, and pricing")
.argument("<agentId>")
.action(async (agentId: string) => {
const rawAgents = await fetchAgentsREST();
const allNormalized = rawAgents.map(normalizeAgent);
const agent = allNormalized.find((a) => a.agent_id === agentId);
if (!agent) {
const similar = allNormalized
.filter(
(a) =>
a.agent_id.includes(agentId) ||
a.agent_name.toLowerCase().includes(agentId.toLowerCase())
)
.slice(0, 5);
if (JSON_FLAG) {
out({
error: "not_found",
agent_id: agentId,
suggestions: similar.map((a) => a.agent_id),
});
} else {
console.error(`Agent "${agentId}" not found.`);
if (similar.length > 0) {
console.log("\nDid you mean:");
similar.forEach((a) =>
console.log(` ${a.agent_id} (${a.agent_name})`)
);
}
}
process.exit(1);
}
if (JSON_FLAG) {
out(agent);
return;
}
// Pretty output
console.log(
"\n========================================================"
);
console.log(` ${padCenter(agent.agent_name, 54)}`);
console.log(
"========================================================"
);
console.log(` ID: ${agent.agent_id}`);
console.log(` Type: ${agent.type}`);
console.log(` Status: ${agent.is_online ? "ONLINE" : "OFFLINE"}`);
if (agent.description)
console.log(` Description: ${agent.description}`);
if (agent.commands.length > 0) {
console.log(`\n COMMANDS (${agent.commands.length}):`);
console.log(" " + "-".repeat(60));
for (const cmd of agent.commands) {
const price = formatPrice(cmd);
console.log(`\n ${cmd.usage}`);
if (cmd.description) console.log(` ${cmd.description}`);
console.log(` Price: ${price}`);
if (cmd.parameters && cmd.parameters.length > 0) {
console.log(" Parameters:");
for (const p of cmd.parameters) {
const req = p.required ? "required" : "optional";
console.log(
` ${p.name} (${p.type}, ${req}) - ${p.description}`
);
}
}
}
}
console.log(
`\n QUERY THIS AGENT:`
);
console.log(
` teneo-cli command ${agent.agent_id} "${agent.commands[0]?.trigger || "help"}" --room <roomId>\n`
);
});
// ─── Agent Commands ──────────────────────────────────────────────────────────
program
.command("command")
.description(
"Direct command to agent (use internal agent ID, not display name)"
)
.argument("<agent>", "Internal agent ID (e.g. x-agent-enterprise-v2)")
.argument("<cmd>", "Command string: {trigger} {argument}")
.option("--room <roomId>")
.option("--timeout <ms>", "Response timeout", "120000")
.option("--chain <chain>")
.action(async (agent: string, cmd: string, opts: any) => {
const room = resolveRoom(opts.room);
await withSDK(
async (sdk, attempt) => {
const r = await (sdk as any).sendDirectCommand(
{
agent,
command: cmd,
room,
...(opts.chain ? { network: opts.chain } : {}),
},
true
);
if (!r || (!r.humanized && !r.raw)) {
await sleep(4000);
out({
status: "sent",
note: "Command sent with payment. Response may arrive asynchronously.",
});
} else {
out({ humanized: r.humanized, raw: r.raw, metadata: r.metadata });
}
},
{ autoJoinRoom: room, payments: true, kickAgent: agent }
);
});
program
.command("quote")
.description("Request price quote (no execution)")
.argument("<message>")
.option("--room <roomId>")
.option("--chain <chain>")
.action(async (message: string, opts: any) => {
const room = resolveRoom(opts.room);
await withSDK(
async (sdk) => {
const q = await (sdk as any).requestQuote(
message,
room,
opts.chain || DEFAULT_CHAIN
);
out({
taskId: q.taskId,
agentId: q.agentId,
agentName: q.agentName,
command: q.command,
pricing: q.pricing,
expiresAt: q.expiresAt,
network: opts.chain || DEFAULT_CHAIN,
});
},
{ autoJoinRoom: room, payments: true }
);
});
program
.command("confirm")
.description("Confirm quoted task with payment")
.argument("<taskId>")
.option("--room <roomId>")
.option("--timeout <ms>", "Response timeout", "120000")
.action(async (taskId: string, opts: any) => {
const room = resolveRoom(opts.room);
await withSDK(
async (sdk) => {
const r = await (sdk as any).confirmQuote(taskId, {
waitForResponse: true,
timeout: parseInt(opts.timeout),
});
if (r && (r.humanized || r.raw)) {
out({ humanized: r.humanized, raw: r.raw, metadata: r.metadata });
} else {
await sleep(4000);
out({
status: "confirmed",
note: "Payment sent. Agent response may arrive asynchronously.",
});
}
},
{ autoJoinRoom: room, payments: true }
);
});
// ─── Room Management ─────────────────────────────────────────────────────────
program
.command("rooms")
.description("List all rooms")
.action(async () => {
await withSDK(async (sdk) => {
const rooms = await (sdk as any).listRooms();
out({
count: rooms.length,
rooms: rooms.map((r: any) => ({
id: r.id,
name: r.name,
is_public: r.is_public,
is_owner: r.is_owner,
description: r.description,
})),
});
});
});
program
.command("room-agents")
.description("List agents in room")
.argument("<roomId>")
.action(async (roomId: string) => {
await withSDK(async (sdk) => {
const agents = await sdk.listRoomAgents(roomId);
out({
roomId,
count: agents.length,
agents: agents.map((a: any) => ({
id: a.agent_id,
name: a.agent_name,
status: a.status,
})),
});
});
});
program
.command("create-room")
.description("Create room")
.argument("<name>")
.option("--description <desc>")
.option("--public", "Make room public", false)
.action(async (name: string, opts: any) => {
await withSDK(async (sdk) => {
const r = await sdk.createRoom({
name,
description: opts.description,
isPublic: opts.public,
});
out({
status: "created",
room: { id: r.id, name: r.name, is_public: (r as any).is_public },
});
});
});
program
.command("update-room")
.description("Update room")
.argument("<roomId>")
.option("--name <name>")
.option("--description <desc>")
.action(async (roomId: string, opts: any) => {
await withSDK(async (sdk) => {
const updates: Record<string, string> = {};
if (opts.name) updates.name = opts.name;
if (opts.description) updates.description = opts.description;
out({
status: "updated",
room: await (sdk as any).updateRoom(roomId, updates),
});
});
});
program
.command("delete-room")
.description("Delete room")
.argument("<roomId>")
.action(async (roomId: string) => {
await withSDK(async (sdk) => {
await (sdk as any).deleteRoom(roomId);
out({ status: "deleted", roomId });
});
});
program
.command("add-agent")
.description("Add agent to room")
.argument("<roomId>")
.argument("<agentId>")
.action(async (roomId: string, agentId: string) => {
await withSDK(async (sdk) => {
await sdk.addAgentToRoom(roomId, agentId);
out({ status: "added", roomId, agentId });
});
});
program
.command("remove-agent")
.description("Remove agent from room")
.argument("<roomId>")
.argument("<agentId>")
.action(async (roomId: string, agentId: string) => {
await withSDK(async (sdk) => {
await sdk.removeAgentFromRoom(roomId, agentId);
out({ status: "removed", roomId, agentId });
});
});
program
.command("owned-rooms")
.description("List rooms you own")
.action(async () => {
await withSDK(async (sdk) => {
const rooms = (sdk as any).getOwnedRooms();
out({
count: rooms.length,
rooms: rooms.map((r: any) => ({
id: r.id,
name: r.name,
is_public: r.is_public,
})),
});
});
});
program
.command("shared-rooms")
.description("List rooms shared with you")
.action(async () => {
await withSDK(async (sdk) => {
const rooms = (sdk as any).getSharedRooms();
out({
count: rooms.length,
rooms: rooms.map((r: any) => ({
id: r.id,
name: r.name,
is_public: r.is_public,
})),
});
});
});
program
.command("subscribe")
.description("Subscribe to public room")
.argument("<roomId>")
.action(async (roomId: string) => {
await withSDK(async (sdk) => {
await (sdk as any).subscribeToPublicRoom(roomId);
out({ status: "subscribed", roomId });
});
});
program
.command("unsubscribe")
.description("Unsubscribe from room")
.argument("<roomId>")
.action(async (roomId: string) => {
await withSDK(async (sdk) => {
await (sdk as any).unsubscribeFromPublicRoom(roomId);
out({ status: "unsubscribed", roomId });
});
});
// ─── Wallet Management ───────────────────────────────────────────────────────
program
.command("wallet-init")
.description("Generate a new wallet (auto-called on first use)")
.action(async () => {
const existing = loadWallet();
if (existing) {
out({
status: "exists",
address: existing.address,
createdAt: existing.createdAt,
});
return;
}
if (PRIVATE_KEY) {
out({
status: "env_var_set",
note: "Private key found in environment. No wallet file needed.",
});
return;
}
requireKey();
const wallet = loadWallet();
out({
status: "created",
address: wallet!.address,
createdAt: wallet!.createdAt,
note: "Send USDC to this address on base, avax, peaq, or xlayer to start using paid agents.",
});
});
program
.command("wallet-address")
.description("Show wallet public address")
.action(async () => {
const wallet = loadWallet();
if (wallet) {
out({ address: wallet.address, createdAt: wallet.createdAt });
} else if (PRIVATE_KEY) {
const key = PRIVATE_KEY.startsWith("0x")
? PRIVATE_KEY
: `0x${PRIVATE_KEY}`;
out({
address: privateKeyToAccount(key as `0x${string}`).address,
source: "environment_variable",
});
} else {
requireKey();
const w = loadWallet();
out({ address: w!.address, createdAt: w!.createdAt });
}
});
program
.command("wallet-export-key")
.description("Export private key (DANGEROUS)")
.action(async () => {
const wallet = loadWallet();
if (!wallet) {
fail(
PRIVATE_KEY
? "No wallet file found. Key is in an environment variable."
: "No wallet found. Run wallet-init first."
);
}
const secret = getOrCreateMasterSecret();
const key = decryptPK(
wallet.encryptedKey,
wallet.iv,
wallet.authTag,
secret
);
console.error(
JSON.stringify({
warning:
"PRIVATE KEY EXPORTED. Never share this. Never paste into websites. Never commit to git.",
})
);
out({ address: wallet.address, privateKey: key });
});
program
.command("wallet-balance")
.description("Check USDC balance on supported chains")
.option("--chain <chain>", "Specific chain (base|avax|peaq|xlayer)")
.action(async (opts: any) => {
const address = getWalletAddress();
const chainsToCheck = opts.chain
? [opts.chain]
: ["base", "avax", "peaq", "xlayer"];
const results: Record<string, any> = {};
for (const chainName of chainsToCheck) {
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) {
results[chainName] = { error: `Unknown chain: ${chainName}` };
continue;
}
try {
const client = createPublicClient({ chain, transport: http() });
const balance = await client.readContract({
address: usdcAddr,
abi: ERC20_BALANCE_ABI,
functionName: "balanceOf",
args: [address as `0x${string}`],
});
results[chainName] = {
usdc: (Number(balance) / 1e6).toFixed(6),
raw: balance.toString(),
};
} catch (err: any) {
results[chainName] = { error: err.message };
}
}
out({ address, balances: results });
});
program
.command("wallet-withdraw")
.description("Withdraw USDC back to original funder ONLY")
.argument("<amount>", "Amount in USDC")
.argument("<chain>", "Chain (base|avax|peaq|xlayer)")
.action(async (amountStr: string, chainName: string) => {
const wallet = loadWallet();
if (!wallet) fail("No wallet file found.");
let destination = wallet.funder;
if (!destination) {
console.error(
JSON.stringify({
info: "No funder locked yet. Scanning chains for incoming USDC transfers...",
})
);
const result = await detectFunder(wallet.address);
if (!result)
fail("No incoming USDC transfers found. Cannot determine funder address.");
wallet.funder = result.funder;
saveWallet(wallet);
destination = result.funder;
console.error(
JSON.stringify({
info: `Funder auto-detected and locked: ${destination} (${result.chain})`,
})
);
}
const amount = parseFloat(amountStr);
if (isNaN(amount) || amount <= 0) fail("Invalid amount.");
const rawAmount = BigInt(Math.round(amount * 1e6));
const chain = WALLET_CHAIN_MAP[chainName];
const usdcAddr = USDC_ADDRESSES[chainName];
if (!chain || !usdcAddr) fail(`Unknown chain: ${chainName}`);
const secret = getOrCreateMasterSecret();
const pk = decryptPK(
wallet.encryptedKey,
wallet.iv,
wallet.authTag,
secret
);
const account = privateKeyToAccount(
(pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`
);
const wc = createWalletClient({ account, chain, transport: http() });
const txHash = await wc.writeContract({
address: usdcAddr,
abi: ERC20_TRANSFER_ABI,
functionName: "transfer",
args: [destination as `0x${string}`, rawAmount],
});
out({
status: "sent",
txHash,
amount: amountStr,
chain: chainName,
destination,
note: "Funds returned to original funder address.",
});
});
program
.command("wallet-detect-funder")
.description(
"Detect and lock the first address that sent USDC to this wallet"
)
.action(async () => {
const wallet = loadWallet();
if (!wallet) fail("No wallet file found. Run wallet-init first.");
if (wallet.funder) {
out({
funder: wallet.funder,
locked: true,
note: "Funder already locked. Cannot be changed.",
});
return;
}
console.error(
JSON.stringify({
info: "Scanning all chains for incoming USDC transfers...",
})
);
const result = await detectFunder(wallet.address);
if (!result) {
out({
funder: null,
note: "No incoming USDC transfers found yet. Send USDC to this wallet first.",
});
return;
}
wallet.funder = result.funder;
saveWallet(wallet);
out({
funder: result.funder,
chain: result.chain,
locked: true,
note: "Funder detected and permanently locked. Withdrawals will only go to this address.",
});
});
// ─── Metadata Export ─────────────────────────────────────────────────────────
if (process.argv.includes("--dump-commands")) {
const commands = program.commands.map((cmd) => ({
name: cmd.name(),
description: cmd.description(),
arguments: cmd.registeredArguments.map((a) => ({
name: a.name(),
description: a.description,
required: a.required,
})),
options: cmd.options.map((o) => ({
flags: o.flags,
description: o.description,
defaultValue: o.defaultValue,
})),
}));
console.log(
JSON.stringify(
{
name: program.name(),
version: program.version(),
description: program.description(),
commands,
},
null,
2,
),
);
process.exit(0);
}
// ─── Parse ───────────────────────────────────────────────────────────────────
program.parseAsync(process.argv).catch((err) => fail(err.message || String(err)));
Step 3: Verify installation
node ~/teneo-skill/teneo.ts health
How to run all commands
All commands in this skill are run as:
node ~/teneo-skill/teneo.ts <command> [options]
Authentication
The CLI creates and manages its own wallet automatically. Three tiers of key resolution (in priority order):
1. Environment variable (highest priority)
export TENEO_PRIVATE_KEY=4a8b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a2e
node ~/teneo-skill/teneo.ts command "hotel-finder" "search vienna" --room <roomId>
2. Encrypted wallet file (auto-generated)
On first use, the CLI auto-generates a new wallet and stores it encrypted (AES-256-GCM) at ~/.teneo-wallet/wallet.json. No setup needed — just run any command.
The auto-generated key serves two purposes:
- Authentication — signs the WebSocket handshake to prove identity on Teneo
- Payment — signs x402 USDC transactions to pay agents
3. .env file (auto-loaded from current directory)
echo "TENEO_PRIVATE_KE
…(truncated)