UNPKG

@tangle-network/tcloud

Version:

TypeScript SDK and CLI for Tangle Router, Sandbox, model routing, and agent service calls

680 lines (679 loc) • 28.6 kB
#!/usr/bin/env node const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs"); const require_version = require("./version-B9IFgil-.cjs"); const require_shielded = require("./shielded.cjs"); const require_sandbox = require("./sandbox.cjs"); const require_index = require("./index.cjs"); let commander = require("commander"); let fs = require("fs"); fs = require_rolldown_runtime.__toESM(fs, 1); let path = require("path"); path = require_rolldown_runtime.__toESM(path, 1); let readline = require("readline"); readline = require_rolldown_runtime.__toESM(readline, 1); let child_process = require("child_process"); //#region src/cli.ts /** * tcloud CLI — Tangle AI Cloud command-line interface * * Uses the same TCloudClient as the SDK. Adds terminal UI, config file * management, device flow auth, and interactive chat mode. */ const CONFIG_DIR = path.join(process.env.HOME || "~", ".tcloud"); const CONFIG_FILE = path.join(CONFIG_DIR, "config.json"); const WALLETS_FILE = path.join(CONFIG_DIR, "wallets.json"); function ensureDir() { if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 }); } function loadConfig() { ensureDir(); return { ...fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) : { apiUrl: "https://router.tangle.tools", defaultModel: "gpt-4o-mini", chainId: 3799 }, ...process.env.TANGLE_ROUTER_URL ? { apiUrl: process.env.TANGLE_ROUTER_URL.replace(/\/v1\/?$/, "") } : {}, ...process.env.TCLOUD_API_URL ? { apiUrl: process.env.TCLOUD_API_URL.replace(/\/v1\/?$/, "") } : {}, ...process.env.TCLOUD_API_KEY ? { apiKey: process.env.TCLOUD_API_KEY } : {}, ...process.env.TANGLE_API_KEY ? { apiKey: process.env.TANGLE_API_KEY } : {} }; } function saveConfig(c) { ensureDir(); fs.writeFileSync(CONFIG_FILE, JSON.stringify(c, null, 2), { mode: 384 }); } function loadWallets() { ensureDir(); if (fs.existsSync(WALLETS_FILE)) return JSON.parse(fs.readFileSync(WALLETS_FILE, "utf-8")); return []; } function saveWallets(w) { ensureDir(); fs.writeFileSync(WALLETS_FILE, JSON.stringify(w, null, 2), { mode: 384 }); } function getClient(opts) { const config = loadConfig(); if (opts?.private) { const wallets = loadWallets(); if (wallets.length === 0) { console.error("No shielded wallets. Run: tcloud wallet generate"); process.exit(1); } return require_index.TCloud.shielded({ baseURL: `${config.apiUrl}/v1`, wallet: wallets[0] }); } return new require_index.TCloud({ baseURL: `${config.apiUrl}/v1`, apiKey: config.apiKey, model: config.defaultModel }); } function requireApiKey(config) { if (!config.apiKey) { console.error("No API key. Run: tcloud login"); process.exit(1); } return config.apiKey; } function optionalNumber(value) { if (value == null) return void 0; const parsed = Number(value); if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got ${String(value)}`); return parsed; } function teeType(value) { if (!value) return void 0; return value.toLowerCase(); } function printJson(value) { console.log(JSON.stringify(value, null, 2)); } function firstImageResult(resp) { return resp.data?.[0]?.url ?? resp.data?.[0]?.b64_json; } function videoResultUrl(resp) { return resp.url ?? resp.video_url ?? resp.id; } const program = new commander.Command(); program.name("tcloud").description("Tangle AI Cloud CLI").version(require_version.packageVersion()); program.command("config").description("View or update configuration").option("--api-url <url>", "API base URL").option("--sandbox-url <url>", "Sandbox API base URL").option("--api-key <key>", "API key").option("--model <model>", "Default model").option("--chain <id>", "Chain ID").action((opts) => { const c = loadConfig(); if (opts.apiUrl) c.apiUrl = opts.apiUrl; if (opts.sandboxUrl) c.sandboxUrl = opts.sandboxUrl; if (opts.apiKey) c.apiKey = opts.apiKey; if (opts.model) c.defaultModel = opts.model; if (opts.chain) c.chainId = parseInt(opts.chain); saveConfig(c); console.log(JSON.stringify(c, null, 2)); }); const auth = program.command("auth").description("Authentication"); function openBrowser(url) { if (process.env.NO_BROWSER || process.env.CI) return false; if (!process.stdout.isTTY) return false; try { const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; (0, child_process.spawn)(cmd, [url], { stdio: "ignore", detached: true }).unref(); return true; } catch { return false; } } const SPINNER_FRAMES = [ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" ]; async function runDeviceFlow(mode) { const config = loadConfig(); const initRes = await fetch(`${config.apiUrl}/api/auth/device`, { method: "POST" }); if (!initRes.ok) { console.error(`\n ✗ Auth server returned ${initRes.status}.`); process.exit(1); } const d = await initRes.json(); const urlWithCode = `${d.verification_url}?user_code=${encodeURIComponent(d.user_code)}`; const opened = openBrowser(urlWithCode); console.log(`\n ${mode === "signup" ? "Creating your Tangle account" : "Signing you in"}\n`); console.log(` ${opened ? "Browser opened:" : "Open this URL:"}`); console.log(` ${urlWithCode}\n`); console.log(` Verification code: ${d.user_code}\n`); const deadline = Date.now() + (d.expires_in || 600) * 1e3; const interval = Math.max(2, d.interval || 5) * 1e3; const spinnerEnabled = process.stdout.isTTY && !process.env.CI; let frame = 0; function tickSpinner() { if (!spinnerEnabled) return; const remaining = Math.max(0, Math.round((deadline - Date.now()) / 1e3)); process.stdout.write(`\r ${SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]} waiting for browser confirmation (${remaining}s remaining) `); } const spinnerTimer = spinnerEnabled ? setInterval(tickSpinner, 100) : null; try { while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, interval)); const t = await (await fetch(`${config.apiUrl}/api/auth/device/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_code: d.device_code }) })).json(); if (t.access_token) { config.apiKey = t.access_token; saveConfig(config); if (spinnerTimer) { clearInterval(spinnerTimer); process.stdout.write("\r" + " ".repeat(80) + "\r"); } console.log(` ✓ Authenticated`); console.log(` ✓ API key saved to ${CONFIG_FILE}\n`); console.log(` Try it:`); console.log(` tcloud whoami`); console.log(` tcloud chat "hello world"\n`); return; } if (t.error === "expired_token") { if (spinnerTimer) { clearInterval(spinnerTimer); process.stdout.write("\r" + " ".repeat(80) + "\r"); } console.error(` ✗ Code expired. Run 'tcloud auth ${mode}' again.`); process.exit(1); } } if (spinnerTimer) { clearInterval(spinnerTimer); process.stdout.write("\r" + " ".repeat(80) + "\r"); } console.error(` ✗ Timed out after ${Math.round((d.expires_in || 600) / 60)} minutes.`); process.exit(1); } finally { if (spinnerTimer) clearInterval(spinnerTimer); } } auth.command("signup").description("Create an account via browser (device flow)").action(() => runDeviceFlow("signup")); auth.command("login").description("Log in via browser (device flow)").action(() => runDeviceFlow("login")); auth.command("logout").description("Remove stored credentials").action(() => { const c = loadConfig(); delete c.apiKey; saveConfig(c); console.log(` ✓ Logged out. Config kept at ${CONFIG_FILE}.`); }); auth.command("set-key").description("Set API key directly").argument("<key>").action((key) => { const c = loadConfig(); c.apiKey = key; saveConfig(c); console.log(" ✓ API key saved."); }); auth.command("status").description("Show auth status").action(() => { const c = loadConfig(); console.log(c.apiKey ? ` ✓ Authenticated: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}` : " ✗ Not authenticated. Run: tcloud auth signup"); const w = loadWallets(); if (w.length) console.log(` Shielded wallets: ${w.length}`); }); auth.command("whoami").description("Show logged-in account details").action(async () => { const c = loadConfig(); if (!c.apiKey) { console.log(" ✗ Not authenticated. Run: tcloud auth signup"); return; } try { const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, { headers: { Authorization: `Bearer ${c.apiKey}` } }); if (!res.ok) { console.log(` ✗ Auth check failed (${res.status}). Your key may be revoked — run: tcloud auth login`); return; } const me = await res.json(); const user = me.user ?? {}; const sub = me.subscription; console.log(` Email: ${user.email ?? "n/a"}`); console.log(` User: ${user.name ?? user.id ?? "n/a"}`); console.log(` Plan: ${sub?.plan ?? "free"}`); console.log(` Balance: $${Number(me.balance ?? 0).toFixed(4)}`); console.log(` Key: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}`); console.log(` API: ${c.apiUrl}`); } catch (e) { console.log(` ✗ ${e.message ?? e}`); } }); program.command("signup").description("Create an account via browser (alias for `auth signup`)").action(() => runDeviceFlow("signup")); program.command("login").description("Log in via browser (alias for `auth login`)").action(() => runDeviceFlow("login")); program.command("logout").description("Remove stored credentials (alias for `auth logout`)").action(() => { const c = loadConfig(); delete c.apiKey; saveConfig(c); console.log(` ✓ Logged out.`); }); program.command("whoami").description("Show logged-in account (alias for `auth whoami`)").action(async () => { const c = loadConfig(); if (!c.apiKey) { console.log(" ✗ Not authenticated. Run: tcloud signup"); return; } try { const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, { headers: { Authorization: `Bearer ${c.apiKey}` } }); if (!res.ok) { console.log(` ✗ Auth failed (${res.status}). Run: tcloud login`); return; } const me = await res.json(); const user = me.user ?? {}; const sub = me.subscription; console.log(` ${user.email ?? user.name ?? user.id} · $${Number(me.balance ?? 0).toFixed(4)} · ${sub?.plan ?? "free"}`); } catch (e) { console.log(` ✗ ${e.message ?? e}`); } }); const wallet = program.command("wallet").description("Shielded wallet management"); wallet.command("generate").description("Generate ephemeral wallet").option("-l, --label <name>").action((opts) => { const w = require_shielded.generateWallet(); const wallets = loadWallets(); wallets.push({ ...w, label: opts.label, createdAt: (/* @__PURE__ */ new Date()).toISOString() }); saveWallets(wallets); console.log(`Wallet generated:`); console.log(` Address: ${w.address}`); console.log(` Commitment: ${w.commitment}`); console.log(` Saved to: ${WALLETS_FILE}`); console.log(`\nFund with: tcloud credits fund`); }); wallet.command("list").description("List wallets").action(() => { const wallets = loadWallets(); if (!wallets.length) { console.log("No wallets. Run: tcloud wallet generate"); return; } wallets.forEach((w, i) => console.log(` [${i}] ${(w.label || "default").padEnd(15)} ${w.commitment.slice(0, 20)}...`)); }); program.command("decide").description("Ask decision questions about an input (OpenAI Decisions wire)").argument("<input>", "The input to evaluate").option("-m, --model <model>", "Decision model", "gpt-6-luna").option("-p, --predicate <instructions...>", "Predicate question(s): how likely is this true?").option("-c, --choice <name=options...>", "Choice question: name then 2+ comma-separated options").action(async (input, opts) => { const client = getClient(); const questions = []; for (const instructions of opts.predicate ?? []) questions.push({ type: "predicate", instructions }); if (opts.choice) { const [name, options] = opts.choice; questions.push({ type: "choice", name, instructions: `Choose the best option for: ${name}`, choices: String(options).split(",").map((v) => ({ value: v.trim() })) }); } if (questions.length === 0) { console.error("Ask at least one question: --predicate \"...\" and/or --choice name=a,b"); process.exit(1); } const { decision, receipts } = await client.decisions.create({ model: opts.model, input, questions }); for (const answer of decision.answers) { const name = answer.name ? `${answer.name}: ` : ""; if (answer.type === "predicate") console.log(`${name}${(answer.probability * 100).toFixed(1)}% likely`); else if (answer.type === "choice") console.log(`${name}${String(answer.choice)} (${(answer.confidence * 100).toFixed(0)}% confident)`); else if (answer.type === "score") console.log(`${name}${answer.score} (confidence ${answer.confidence})`); else console.log(`${name}refused`); } if (receipts.costUsd !== null) console.error(`cost: $${receipts.costUsd.toFixed(6)} (${decision.usage.input_tokens} input tokens)`); }); program.command("systemone").description("Evaluate System One decision questions (the fleet wire)").argument("<state>", "The state to evaluate").option("-m, --model <model>", "System One row", "openai/gpt-6-luna").option("-n, --noul <name=instructions...>", "Noul question(s): name then instructions").action(async (state, opts) => { const client = getClient(); const questions = {}; for (const spec of opts.noul ?? []) { const eq = spec.indexOf("="); if (eq <= 0) { console.error("--noul expects name=instructions"); process.exit(1); } questions[spec.slice(0, eq)] = { type: "noul", instructions: spec.slice(eq + 1) }; } if (Object.keys(questions).length === 0) { console.error("Ask at least one question: --noul name=instructions"); process.exit(1); } const { result, receipts } = await client.systemone({ model: opts.model, state, questions }); for (const [name, answer] of Object.entries(result.answers)) if (answer.type === "noul") console.log(`${name}: ${(answer.noul * 100).toFixed(1)}% likely`); else if (answer.type === "choice") console.log(`${name}: ${answer.choice} (${(answer.confidence * 100).toFixed(0)}% confident)`); else console.log(`${name}: ${answer.score} (confidence ${answer.confidence})`); if (receipts.costUsd !== null) console.error(`cost: $${receipts.costUsd.toFixed(6)} (${result.usage.input_tokens} input tokens)`); }); program.command("key-info").description("This key's own scope: allow-list, budgets, rate limit, expiry").action(async () => { const info = await getClient().keyInfo(); console.log(JSON.stringify(info, null, 2)); }); program.command("chat").description("Chat with a model").argument("[message]", "Message (or interactive if omitted)").option("-m, --model <model>", "Model").option("--private", "Use shielded credits (anonymous)").option("--stream", "Stream output", true).action(async (message, opts) => { const client = getClient({ private: opts.private }); const model = opts.model || loadConfig().defaultModel; if (!message) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(`tcloud chat — ${model}${opts.private ? " (private)" : ""}\nCtrl+C to exit.\n`); const ask = () => rl.question("> ", async (input) => { if (!input.trim()) { ask(); return; } try { for await (const chunk of client.askStream(input.trim(), { model })) process.stdout.write(chunk); process.stdout.write("\n\n"); } catch (e) { console.error("Error:", e.message); } ask(); }); ask(); return; } try { if (opts.stream) { for await (const chunk of client.askStream(message, { model })) process.stdout.write(chunk); process.stdout.write("\n"); } else { const completion = await client.askFull(message, { model }); const text = completion.choices[0]?.message?.content || ""; const usedModel = completion.model || model; const usage = completion.usage; process.stdout.write(`[${usedModel}] ${text}\n`); if (usage) { const cost = usage.total_tokens * 1e-6; process.stdout.write(` \u21B3 ${usage.total_tokens} tokens \u00B7 $${cost.toFixed(6)}\n`); } } } catch (e) { console.error("Error:", e.message); if (e.status === 402) console.error("Add credits: tcloud credits fund"); } }); program.command("models").description("List available models").option("-s, --search <query>", "Search").action(async (opts) => { const client = getClient(); try { let models = await client.models(); if (opts.search) { const q = opts.search.toLowerCase(); models = models.filter((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q)); } console.log(`${models.length} models:`); models.slice(0, 30).forEach((m) => console.log(` ${m.id.padEnd(40)} ${m.name || ""}`)); if (models.length > 30) console.log(` ... +${models.length - 30} more`); } catch (e) { console.error("Error:", e.message); } }); program.command("search").description("Search the web").argument("<query>", "Search query").option("--provider <provider>", "Search provider: perplexity, exa, you, parallel, tavily, brave").option("-n, --max-results <n>", "Maximum results").option("--recency <period>", "day, week, month, or year").option("--include-domain <domain...>", "Restrict to domains").option("--exclude-domain <domain...>", "Exclude domains").option("--json", "Print raw JSON response").action(async (query, opts) => { const client = getClient(); try { const resp = await client.search({ query, provider: opts.provider, maxResults: optionalNumber(opts.maxResults), searchRecency: opts.recency, includeDomains: opts.includeDomain, excludeDomains: opts.excludeDomain }); if (opts.json) { printJson(resp); return; } console.log(`${resp.provider} results for "${resp.query}":`); resp.data.forEach((hit, index) => { console.log(`${index + 1}. ${hit.title}`); console.log(` ${hit.url}`); if (hit.snippet) console.log(` ${hit.snippet}`); }); if (resp.usage?.billed_cost != null) console.log(` \u21B3 $${resp.usage.billed_cost.toFixed(6)} billed`); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("mcp").description("Run a Model Context Protocol (stdio) server exposing Tangle tools (web_search, deep_research). Mount with: { \"command\": [\"tcloud\", \"mcp\"] }").action(async () => { const { runMcpServer } = await Promise.resolve().then(() => require("./mcp.cjs")); await runMcpServer(getClient()); }); program.command("image-generate").description("Generate an image").requiredOption("-p, --prompt <prompt>", "Prompt").option("-m, --model <model>", "Image model").option("--size <size>", "Output size").option("--quality <quality>", "Output quality").option("-n, --count <n>", "Number of images").option("--response-format <format>", "url or b64_json").option("--json", "Print raw JSON response").action(async (opts) => { const client = getClient(); try { const resp = await client.imageGenerate({ prompt: opts.prompt, model: opts.model, size: opts.size, quality: opts.quality, n: optionalNumber(opts.count), response_format: opts.responseFormat }); if (opts.json) { printJson(resp); return; } const result = firstImageResult(resp); if (result) console.log(result); else printJson(resp); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("video-generate").description("Generate a video").requiredOption("-p, --prompt <prompt>", "Prompt").option("-m, --model <model>", "Video model/provider").option("--provider <provider>", "Video provider, e.g. kling or runway").option("--duration <seconds>", "Duration in seconds").option("--resolution <resolution>", "Output resolution, e.g. 720p or 1080p").option("--aspect-ratio <ratio>", "Output aspect ratio, e.g. 16:9 or 9:16").option("--size <size>", "Exact output size, e.g. 1280x720").option("--image-url <url>", "Reference image URL").option("--generate-audio", "Generate audio when the model supports it").option("--seed <seed>", "Deterministic seed").option("--callback-url <url>", "Webhook callback URL").option("--json", "Print raw JSON response").action(async (opts) => { const client = getClient(); try { const resp = await client.videoGenerate({ prompt: opts.prompt, model: opts.model, provider: opts.provider, duration: optionalNumber(opts.duration), resolution: opts.resolution, aspect_ratio: opts.aspectRatio, size: opts.size, image_url: opts.imageUrl, generate_audio: opts.generateAudio, seed: optionalNumber(opts.seed), callback_url: opts.callbackUrl }); if (opts.json) { printJson(resp); return; } const result = videoResultUrl(resp); if (result) console.log(result); else printJson(resp); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("speech").description("Generate speech audio").requiredOption("-i, --input <text>", "Input text").option("-m, --model <model>", "Speech model").option("--voice <voice>", "Voice").option("-o, --output <file>", "Output file", "speech.mp3").option("--json", "Print JSON metadata").action(async (opts) => { const client = getClient(); try { const audio = await client.speech({ input: opts.input, model: opts.model, voice: opts.voice }); fs.writeFileSync(opts.output, Buffer.from(audio)); const result = { output: opts.output, bytes: audio.byteLength }; if (opts.json) printJson(result); else console.log(opts.output); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("transcribe").description("Transcribe an audio file").argument("<file>", "Audio file").option("-m, --model <model>", "Transcription model").option("--language <language>", "Language hint").option("--prompt <prompt>", "Prompt hint").option("--json", "Print raw JSON response").action(async (file, opts) => { const client = getClient(); try { const blob = new Blob([new Uint8Array(fs.readFileSync(file))]); const resp = await client.transcribe(blob, { model: opts.model, language: opts.language, prompt: opts.prompt }); if (opts.json) printJson(resp); else console.log(resp.text); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("avatar-generate").description("Generate an avatar video").requiredOption("--audio-url <url>", "Narration audio URL").option("--image-url <url>", "Face image URL").option("--avatar-id <id>", "Preset avatar ID").option("--duration <seconds>", "Target duration in seconds").option("--output-format <format>", "Output format").option("--json", "Print raw JSON response").action(async (opts) => { const client = getClient(); try { const resp = await client.avatarGenerate({ audio_url: opts.audioUrl, image_url: opts.imageUrl, avatar_id: opts.avatarId, duration_seconds: optionalNumber(opts.duration), output_format: opts.outputFormat }); if (opts.json) { printJson(resp); return; } console.log(resp.result?.video_url ?? resp.job_id); } catch (e) { console.error("Error:", e.message); process.exit(1); } }); program.command("operators").description("List active operators").action(async () => { const client = getClient(); try { const { operators, stats } = await client.operators(); console.log(`${stats.activeOperators} operators, ${stats.totalModels} models:\n`); operators.forEach((o) => console.log(` ${o.slug.padEnd(20)} ${o.status.padEnd(10)} ${String(o.models.length).padEnd(3)} models ${o.reputationScore}% rep ${o.avgLatencyMs}ms`)); } catch (e) { console.error("Error:", e.message); } }); program.command("sandbox").description("Sandbox workflows").command("create").description("Create a sandbox").option("--name <name>", "Sandbox name").option("--image <image>", "Sandbox image or environment").option("--environment <environment>", "Sandbox environment").option("--ssh", "Enable SSH").option("--cpu <cores>", "CPU cores").option("--memory <mb>", "Memory in MB").option("--disk <gb>", "Disk in GB").option("--git-url <url>", "Git repository URL").option("--git-ref <ref>", "Git ref").option("--backend <type>", "Agent backend").option("--tee <type>", "Require a TEE backend: any, tdx, nitro, sev-snp, phala-dstack, gcp, azure").option("--sealed", "Require sealed secret support").option("--attestation-nonce <hex|auto>", "Attach a caller challenge nonce").option("--verify", "Verify returned attestation evidence (default when --tee is set)").option("--allow-unverified-hardware", "Allow structural attestation checks before vendor-root verification is available").option("--sandbox-url <url>", "Sandbox API base URL").option("--json", "Print JSON").action(async (opts) => { try { const config = loadConfig(); const result = await new require_sandbox.TCloudSandbox({ apiKey: requireApiKey(config), baseUrl: opts.sandboxUrl ?? config.sandboxUrl }).create({ name: opts.name, image: opts.image, environment: opts.environment, ssh: Boolean(opts.ssh), cpu: optionalNumber(opts.cpu), memoryMb: optionalNumber(opts.memory), diskGb: optionalNumber(opts.disk), gitUrl: opts.gitUrl, gitRef: opts.gitRef, backend: opts.backend, tee: teeType(opts.tee), sealed: Boolean(opts.sealed), attestationNonce: opts.attestationNonce, verify: Boolean(opts.verify || opts.tee), attestationPolicy: { allowUnverifiedHardware: Boolean(opts.allowUnverifiedHardware) } }); if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; } const box = result.sandbox; console.log(`Sandbox created: ${box.id ?? "unknown"}`); if (box.status) console.log(`Status: ${box.status}`); if (opts.tee) { const status = result.attestationStatus; console.log(`TEE: ${opts.tee}`); console.log(`Attestation: ${status.verified ? "verified" : status.evidenceReturned ? "unverified" : "not returned"}`); console.log(`Nonce bound: ${status.nonceBound ? "yes" : "no"}`); if (result.attestationNonce) console.log(`Attestation nonce: ${result.attestationNonce}`); } } catch (e) { console.error("Error:", e.message); process.exit(1); } }); const credits = program.command("credits").description("Credit management"); credits.command("balance").description("Check balance").action(async () => { const client = getClient(); try { const data = await client.credits(); console.log(`Balance: $${data.balance.toFixed(4)}`); if (data.transactions.length) { console.log("\nRecent transactions:"); data.transactions.slice(0, 5).forEach((t) => console.log(` ${t.amount > 0 ? "+" : ""}$${Math.abs(t.amount).toFixed(4).padEnd(10)} ${t.description}`)); } } catch (e) { console.error("Error:", e.message); } }); credits.command("add").description("Add credits").argument("<amount>").action(async (amount) => { const client = getClient(); try { const data = await client.addCredits(parseFloat(amount)); if (data.url) { console.log(`Checkout URL: ${data.url}`); console.log("Complete payment to add credits."); } } catch (e) { console.error("Error:", e.message); } }); credits.command("fund").description("Fund shielded credits from pool").action(() => { console.log("Shielded credit funding requires integration with the VAnchor pool."); console.log("See: https://docs.tangle.tools/privacy/funding"); }); const keys = program.command("keys").description("API key management"); keys.command("create").description("Create API key").argument("<name>").action(async (name) => { const config = loadConfig(); try { const data = await (await fetch(`${config.apiUrl}/api/keys`, { method: "POST", headers: { "Content-Type": "application/json", ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {} }, body: JSON.stringify({ name }) })).json(); if (data.key) console.log(`Key created: ${data.key}\nSave this — shown once only.`); else console.error("Error:", data.error); } catch (e) { console.error("Error:", e.message); } }); keys.command("list").description("List API keys").action(async () => { const config = loadConfig(); try { const data = await (await fetch(`${config.apiUrl}/api/keys`, { headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {} })).json(); if (data.keys?.length) data.keys.forEach((k) => console.log(` ${k.id.slice(0, 8)} ${k.name.padEnd(20)} ${k.keyPrefix}`)); else console.log("No keys."); } catch (e) { console.error("Error:", e.message); } }); program.parse(); //#endregion