@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
JavaScript
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