@tangle-network/tcloud 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -248,6 +248,7 @@ var PrivateRouter = class {
248
248
 
249
249
  // src/client.ts
250
250
  var ROTATING_MARKER = "__tcloudRotating";
251
+ var DIRECT_CLI_BRIDGE_MARKER = "__tcloudDirectCliBridge";
251
252
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
252
253
  var SDK_VERSION = "0.4.0";
253
254
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -297,6 +298,43 @@ var DEFAULT_RETRY = {
297
298
  };
298
299
  var DEFAULT_TIMEOUT_MS = 6e4;
299
300
  var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
301
+ var PROTECTED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
302
+ "model",
303
+ "messages",
304
+ "temperature",
305
+ "max_tokens",
306
+ "maxTokens",
307
+ "stream",
308
+ "stop",
309
+ "top_p",
310
+ "topP",
311
+ "frequency_penalty",
312
+ "frequencyPenalty",
313
+ "presence_penalty",
314
+ "presencePenalty",
315
+ "response_format",
316
+ "responseFormat",
317
+ "tools",
318
+ "tool_choice",
319
+ "toolChoice",
320
+ "gateway",
321
+ "bridge",
322
+ "agent_profile",
323
+ "agentProfile",
324
+ "session_id",
325
+ "sessionId"
326
+ ]);
327
+ function sanitizeProviderOptions(providerOptions) {
328
+ if (!providerOptions) return {};
329
+ for (const key of Object.keys(providerOptions)) {
330
+ if (PROTECTED_PROVIDER_OPTION_KEYS.has(key)) {
331
+ throw new Error(
332
+ `providerOptions cannot override protected chat field "${key}"; use the typed ChatOptions field instead`
333
+ );
334
+ }
335
+ }
336
+ return providerOptions;
337
+ }
300
338
  var TCloudClient = class _TCloudClient {
301
339
  baseURL;
302
340
  platformURL;
@@ -337,13 +375,21 @@ var TCloudClient = class _TCloudClient {
337
375
  * const reply = await client.ask('explain X', 'claude-code/sonnet')
338
376
  * ```
339
377
  *
340
- * For session-resumable agentic dispatches (file edits, multi-turn
341
- * coding), use the router-mediated `tcloud.bridge({...})` API instead
342
- * (or POST to cli-bridge directly with `session_id` in the body).
378
+ * For session-resumable agentic dispatches, use `client.bridge(...)` on
379
+ * the returned direct client. `resume` is serialized to cli-bridge's
380
+ * `session_id` body field and the model wire format stays
381
+ * `<harness>/<model>` without the router-only `bridge/` prefix.
343
382
  */
344
383
  static fromCliBridge(opts) {
345
384
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
346
- return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
385
+ const client = new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
386
+ Object.defineProperty(client, DIRECT_CLI_BRIDGE_MARKER, {
387
+ value: true,
388
+ enumerable: false,
389
+ configurable: false,
390
+ writable: false
391
+ });
392
+ return client;
347
393
  }
348
394
  /**
349
395
  * Build a client that rotates which operator serves each call. Mirrors
@@ -355,7 +401,7 @@ var TCloudClient = class _TCloudClient {
355
401
  *
356
402
  * ```ts
357
403
  * const tcloud = TCloudClient.rotating({
358
- * apiKey: process.env.TCLOUD_API_KEY,
404
+ * apiKey: process.env.TANGLE_API_KEY,
359
405
  * routing: { strategy: 'min-exposure' },
360
406
  * })
361
407
  * await tcloud.ask('hello')
@@ -396,7 +442,7 @@ var TCloudClient = class _TCloudClient {
396
442
  constructor(config = {}) {
397
443
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
398
444
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
399
- this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
445
+ this.apiKey = config.apiKey || process.env.TANGLE_API_KEY || process.env.TCLOUD_API_KEY;
400
446
  this.model = config.model || "gpt-4o-mini";
401
447
  this.privacy = config.privacy;
402
448
  this.limits = config.limits;
@@ -605,8 +651,8 @@ var TCloudClient = class _TCloudClient {
605
651
  delete headers["Authorization"];
606
652
  }
607
653
  }
608
- if (bridge) {
609
- headers["X-Bridge-Unlock"] = bridge.unlock;
654
+ if (bridge && !this._isDirectCliBridge()) {
655
+ headers["X-Bridge-Unlock"] = bridge.unlock ?? "";
610
656
  if (bridge.resume) headers["X-Resume"] = bridge.resume;
611
657
  if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
612
658
  if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
@@ -619,13 +665,24 @@ var TCloudClient = class _TCloudClient {
619
665
  */
620
666
  _effectiveModel(options) {
621
667
  if (options.bridge) {
668
+ if (this._isDirectCliBridge()) {
669
+ return options.bridge.model ? `${options.bridge.harness}/${options.bridge.model}` : options.bridge.harness;
670
+ }
622
671
  return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
623
672
  }
624
673
  return options.model || this.model;
625
674
  }
626
675
  /** Build the chat completions request body */
627
676
  _chatBody(options, stream) {
677
+ const providerOptions = sanitizeProviderOptions(options.providerOptions);
678
+ const sandboxBody = {};
679
+ if (options.sandbox?.agentProfile) sandboxBody.agent_profile = options.sandbox.agentProfile;
680
+ if (options.sandbox?.sessionId) sandboxBody.session_id = options.sandbox.sessionId;
681
+ if (this._isDirectCliBridge() && options.bridge?.resume && !sandboxBody.session_id) {
682
+ sandboxBody.session_id = options.bridge.resume;
683
+ }
628
684
  return JSON.stringify({
685
+ ...providerOptions,
629
686
  model: this._effectiveModel(options),
630
687
  messages: options.messages,
631
688
  temperature: options.temperature,
@@ -639,7 +696,7 @@ var TCloudClient = class _TCloudClient {
639
696
  tools: options.tools,
640
697
  tool_choice: options.toolChoice,
641
698
  ...options.gateway ? { gateway: options.gateway } : {},
642
- ...options.providerOptions
699
+ ...sandboxBody
643
700
  });
644
701
  }
645
702
  /** Chat completion (non-streaming) */
@@ -720,7 +777,13 @@ var TCloudClient = class _TCloudClient {
720
777
  "TCloudClient.rotating() cannot dispatch sandbox-harness sessions.\nSandbox sessions bind to a single operator; rotation is meaningful only for\nstateless calls. Use TCloudClient.shielded() + AgentProfile.confidential.tee\nfor privacy-preserving sandbox execution instead."
721
778
  );
722
779
  }
723
- return new BridgeSession(this, cfg);
780
+ if (!this._isDirectCliBridge() && cfg.unlock == null) {
781
+ throw new Error("Bridge unlock is required for router-mediated bridge sessions");
782
+ }
783
+ return new BridgeSession(this, cfg, this._isDirectCliBridge());
784
+ }
785
+ _isDirectCliBridge() {
786
+ return this[DIRECT_CLI_BRIDGE_MARKER] === true;
724
787
  }
725
788
  /**
726
789
  * Rotation stats — populated only on clients created via
@@ -886,6 +949,49 @@ var TCloudClient = class _TCloudClient {
886
949
  })
887
950
  });
888
951
  }
952
+ /**
953
+ * Edit / inpaint / variate an existing image with a text prompt.
954
+ * Sibling to `imageGenerate`; routes to `/v1/images/edits` via
955
+ * multipart/form-data per the OpenAI spec.
956
+ *
957
+ * Reference image attachments may be passed as `Blob`, `ArrayBuffer`,
958
+ * or `{data: base64, mediaType, filename?}`. Multi-image composition
959
+ * (e.g. gpt-image-2 with two reference frames + a prompt that fuses
960
+ * them) is supported by passing an array; for legacy models only the
961
+ * first image is honored upstream.
962
+ */
963
+ async imagesEdit(options) {
964
+ const formData = new FormData();
965
+ formData.append("prompt", options.prompt);
966
+ formData.append("model", options.model || "gpt-image-2");
967
+ if (options.n != null) formData.append("n", String(options.n));
968
+ if (options.size) formData.append("size", options.size);
969
+ if (options.quality) formData.append("quality", options.quality);
970
+ if (options.response_format) formData.append("response_format", options.response_format);
971
+ if (options.mask) formData.append("mask", toEditBlob(options.mask, "mask.png"), "mask.png");
972
+ const images = Array.isArray(options.image) ? options.image : [options.image];
973
+ if (images.length === 0) {
974
+ throw new TCloudError(400, "imagesEdit requires at least one image attachment");
975
+ }
976
+ images.forEach((img, idx) => {
977
+ const blob = toEditBlob(img, `image-${idx + 1}.png`);
978
+ formData.append("image[]", blob, blobFilename(img, `image-${idx + 1}.png`));
979
+ });
980
+ const headers = { ...this.headers };
981
+ delete headers["Content-Type"];
982
+ this.checkLimits();
983
+ const res = await proxiedFetch(this.privacy, `${this.baseURL}/images/edits`, {
984
+ method: "POST",
985
+ headers,
986
+ body: formData
987
+ }, false);
988
+ if (!res.ok) {
989
+ const err = await res.json().catch(() => ({ error: res.statusText }));
990
+ throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
991
+ }
992
+ this._requestCount++;
993
+ return res.json();
994
+ }
889
995
  /** Rerank documents by relevance to a query */
890
996
  async rerank(options) {
891
997
  return this._request(`${this.baseURL}/rerank`, {
@@ -977,7 +1083,7 @@ var TCloudClient = class _TCloudClient {
977
1083
  }
978
1084
  /** Get video generation status */
979
1085
  async videoStatus(id) {
980
- return this._fetch(`${this.baseURL}/video?id=${id}`);
1086
+ return this._fetch(`${this.baseURL}/video/${encodeURIComponent(id)}`);
981
1087
  }
982
1088
  /** Generate an avatar video (lip-synced talking head from audio + face image).
983
1089
  * Returns 202 with a job_id for async polling via avatarJobStatus(). */
@@ -1237,12 +1343,14 @@ var ALL_TIERS = [
1237
1343
  { name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
1238
1344
  ];
1239
1345
  var BridgeSession = class _BridgeSession {
1240
- constructor(client, cfg) {
1346
+ constructor(client, cfg, direct = false) {
1241
1347
  this.client = client;
1242
1348
  this.cfg = cfg;
1349
+ this.direct = direct;
1243
1350
  }
1244
1351
  client;
1245
1352
  cfg;
1353
+ direct;
1246
1354
  /** Full chat completion (non-streaming). */
1247
1355
  async chat(options) {
1248
1356
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1276,15 +1384,16 @@ var BridgeSession = class _BridgeSession {
1276
1384
  }
1277
1385
  /** Clone with a new resume id — same harness, different logical conversation. */
1278
1386
  withResume(resume) {
1279
- return new _BridgeSession(this.client, { ...this.cfg, resume });
1387
+ return new _BridgeSession(this.client, { ...this.cfg, resume }, this.direct);
1280
1388
  }
1281
1389
  /** Clone with a different model inside the same harness. */
1282
1390
  withModel(model) {
1283
- return new _BridgeSession(this.client, { ...this.cfg, model });
1391
+ return new _BridgeSession(this.client, { ...this.cfg, model }, this.direct);
1284
1392
  }
1285
1393
  /** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
1286
1394
  get model() {
1287
- return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
1395
+ const prefix = this.direct ? "" : "bridge/";
1396
+ return this.cfg.model ? `${prefix}${this.cfg.harness}/${this.cfg.model}` : `${prefix}${this.cfg.harness}`;
1288
1397
  }
1289
1398
  /** The resume id currently bound to this session, if any. */
1290
1399
  get resume() {
@@ -1306,6 +1415,32 @@ function selectTiers(all, n) {
1306
1415
  function formatPrice(pricePerToken) {
1307
1416
  return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
1308
1417
  }
1418
+ function toEditBlob(input, defaultFilename) {
1419
+ if (input instanceof Blob) return input;
1420
+ if (input instanceof ArrayBuffer) return new Blob([input], { type: "image/png" });
1421
+ const binary = base64ToUint8Array(input.data);
1422
+ const copy = new Uint8Array(binary.byteLength);
1423
+ copy.set(binary);
1424
+ return new Blob([copy.buffer], { type: input.mediaType || "image/png" });
1425
+ void defaultFilename;
1426
+ }
1427
+ function blobFilename(input, fallback) {
1428
+ if (typeof input === "object" && !(input instanceof Blob) && !(input instanceof ArrayBuffer)) {
1429
+ if (input.filename) return input.filename;
1430
+ }
1431
+ return fallback;
1432
+ }
1433
+ function base64ToUint8Array(b64) {
1434
+ const m = /^data:[^;,]*(?:;[^,]*)?,(.+)$/.exec(b64);
1435
+ const raw = m ? m[1] : b64;
1436
+ const bin = typeof atob === "function" ? atob(raw) : (
1437
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1438
+ globalThis.Buffer.from(raw, "base64").toString("binary")
1439
+ );
1440
+ const out = new Uint8Array(bin.length);
1441
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1442
+ return out;
1443
+ }
1309
1444
  var TCloudError = class extends Error {
1310
1445
  constructor(status, message) {
1311
1446
  super(message);
@@ -1704,7 +1839,14 @@ function buildSandboxCreateOptions(options) {
1704
1839
  memoryMB: options.memoryMb,
1705
1840
  diskGB: options.diskGb
1706
1841
  } : void 0,
1707
- backend: options.backend ? { type: options.backend } : void 0,
1842
+ // backend: merge `backend` (type string) with `agentProfile` (carried
1843
+ // as `backend.profile` on the underlying SDK). Either may be set
1844
+ // independently; setting both produces `{ type, profile }`. Setting
1845
+ // neither leaves `backend` undefined so the SDK applies its default.
1846
+ backend: options.backend || options.agentProfile ? {
1847
+ ...options.backend ? { type: options.backend } : {},
1848
+ ...options.agentProfile ? { profile: options.agentProfile } : {}
1849
+ } : void 0,
1708
1850
  confidential: options.tee ? {
1709
1851
  tee: options.tee,
1710
1852
  sealed: options.sealed || void 0,
@@ -1804,7 +1946,7 @@ var TCloud = class _TCloud extends TCloudClient {
1804
1946
  *
1805
1947
  * ```ts
1806
1948
  * const client = TCloud.rotating({
1807
- * apiKey: process.env.TCLOUD_API_KEY,
1949
+ * apiKey: process.env.TANGLE_API_KEY,
1808
1950
  * routing: { strategy: 'min-exposure' },
1809
1951
  * })
1810
1952
  * const stats = client.getRotationStats()
@@ -1822,6 +1964,23 @@ var TCloud = class _TCloud extends TCloudClient {
1822
1964
  * ```
1823
1965
  */
1824
1966
  static generateWallet = generateWallet;
1967
+ /**
1968
+ * Create a Sandbox SDK client using this TCloud client's API key by default.
1969
+ *
1970
+ * ```ts
1971
+ * const tcloud = new TCloud({ apiKey })
1972
+ * const sandbox = await tcloud.sandbox().create({ name: 'runner' })
1973
+ * ```
1974
+ */
1975
+ sandbox(config = {}) {
1976
+ const apiKey = config.apiKey ?? this.apiKey;
1977
+ if (!apiKey) throw new Error("TCloud.sandbox() requires an apiKey");
1978
+ return new TCloudSandbox({ ...config, apiKey });
1979
+ }
1980
+ /** Create a standalone Sandbox SDK client. */
1981
+ static sandbox(config) {
1982
+ return new TCloudSandbox(config);
1983
+ }
1825
1984
  };
1826
1985
 
1827
1986
  // src/cli.ts
@@ -1838,8 +1997,14 @@ function ensureDir() {
1838
1997
  }
1839
1998
  function loadConfig() {
1840
1999
  ensureDir();
1841
- if (fs.existsSync(CONFIG_FILE)) return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
1842
- return { apiUrl: "https://router.tangle.tools", defaultModel: "gpt-4o-mini", chainId: 3799 };
2000
+ const fileConfig = fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) : { apiUrl: "https://router.tangle.tools", defaultModel: "gpt-4o-mini", chainId: 3799 };
2001
+ return {
2002
+ ...fileConfig,
2003
+ ...process.env.TANGLE_ROUTER_URL ? { apiUrl: process.env.TANGLE_ROUTER_URL.replace(/\/v1\/?$/, "") } : {},
2004
+ ...process.env.TCLOUD_API_URL ? { apiUrl: process.env.TCLOUD_API_URL.replace(/\/v1\/?$/, "") } : {},
2005
+ ...process.env.TANGLE_API_KEY ? { apiKey: process.env.TANGLE_API_KEY } : {},
2006
+ ...process.env.TCLOUD_API_KEY ? { apiKey: process.env.TCLOUD_API_KEY } : {}
2007
+ };
1843
2008
  }
1844
2009
  function saveConfig(c) {
1845
2010
  ensureDir();
@@ -1895,6 +2060,15 @@ function packageVersion() {
1895
2060
  }
1896
2061
  return "0.0.0";
1897
2062
  }
2063
+ function printJson(value) {
2064
+ console.log(JSON.stringify(value, null, 2));
2065
+ }
2066
+ function firstImageResult(resp) {
2067
+ return resp.data?.[0]?.url ?? resp.data?.[0]?.b64_json;
2068
+ }
2069
+ function videoResultUrl(resp) {
2070
+ return resp.url ?? resp.video_url ?? resp.id;
2071
+ }
1898
2072
  var program = new import_commander.Command();
1899
2073
  program.name("tcloud").description("Tangle AI Cloud CLI").version(packageVersion());
1900
2074
  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) => {
@@ -2156,6 +2330,111 @@ program.command("models").description("List available models").option("-s, --sea
2156
2330
  console.error("Error:", e.message);
2157
2331
  }
2158
2332
  });
2333
+ 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) => {
2334
+ const client = getClient();
2335
+ try {
2336
+ const resp = await client.imageGenerate({
2337
+ prompt: opts.prompt,
2338
+ model: opts.model,
2339
+ size: opts.size,
2340
+ quality: opts.quality,
2341
+ n: optionalNumber(opts.count),
2342
+ response_format: opts.responseFormat
2343
+ });
2344
+ if (opts.json) {
2345
+ printJson(resp);
2346
+ return;
2347
+ }
2348
+ const result = firstImageResult(resp);
2349
+ if (result) console.log(result);
2350
+ else printJson(resp);
2351
+ } catch (e) {
2352
+ console.error("Error:", e.message);
2353
+ process.exit(1);
2354
+ }
2355
+ });
2356
+ 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) => {
2357
+ const client = getClient();
2358
+ try {
2359
+ const resp = await client.videoGenerate({
2360
+ prompt: opts.prompt,
2361
+ model: opts.model,
2362
+ provider: opts.provider,
2363
+ duration: optionalNumber(opts.duration),
2364
+ resolution: opts.resolution,
2365
+ aspect_ratio: opts.aspectRatio,
2366
+ size: opts.size,
2367
+ image_url: opts.imageUrl,
2368
+ generate_audio: opts.generateAudio,
2369
+ seed: optionalNumber(opts.seed),
2370
+ callback_url: opts.callbackUrl
2371
+ });
2372
+ if (opts.json) {
2373
+ printJson(resp);
2374
+ return;
2375
+ }
2376
+ const result = videoResultUrl(resp);
2377
+ if (result) console.log(result);
2378
+ else printJson(resp);
2379
+ } catch (e) {
2380
+ console.error("Error:", e.message);
2381
+ process.exit(1);
2382
+ }
2383
+ });
2384
+ 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) => {
2385
+ const client = getClient();
2386
+ try {
2387
+ const audio = await client.speech({
2388
+ input: opts.input,
2389
+ model: opts.model,
2390
+ voice: opts.voice
2391
+ });
2392
+ fs.writeFileSync(opts.output, Buffer.from(audio));
2393
+ const result = { output: opts.output, bytes: audio.byteLength };
2394
+ if (opts.json) printJson(result);
2395
+ else console.log(opts.output);
2396
+ } catch (e) {
2397
+ console.error("Error:", e.message);
2398
+ process.exit(1);
2399
+ }
2400
+ });
2401
+ 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) => {
2402
+ const client = getClient();
2403
+ try {
2404
+ const data = fs.readFileSync(file);
2405
+ const blob = new Blob([data]);
2406
+ const resp = await client.transcribe(blob, {
2407
+ model: opts.model,
2408
+ language: opts.language,
2409
+ prompt: opts.prompt
2410
+ });
2411
+ if (opts.json) printJson(resp);
2412
+ else console.log(resp.text);
2413
+ } catch (e) {
2414
+ console.error("Error:", e.message);
2415
+ process.exit(1);
2416
+ }
2417
+ });
2418
+ 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) => {
2419
+ const client = getClient();
2420
+ try {
2421
+ const resp = await client.avatarGenerate({
2422
+ audio_url: opts.audioUrl,
2423
+ image_url: opts.imageUrl,
2424
+ avatar_id: opts.avatarId,
2425
+ duration_seconds: optionalNumber(opts.duration),
2426
+ output_format: opts.outputFormat
2427
+ });
2428
+ if (opts.json) {
2429
+ printJson(resp);
2430
+ return;
2431
+ }
2432
+ console.log(resp.result?.video_url ?? resp.job_id);
2433
+ } catch (e) {
2434
+ console.error("Error:", e.message);
2435
+ process.exit(1);
2436
+ }
2437
+ });
2159
2438
  program.command("operators").description("List active operators").action(async () => {
2160
2439
  const client = getClient();
2161
2440
  try {
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TCloud
4
- } from "./chunk-F6OVKJG3.js";
4
+ } from "./chunk-27OTTDMZ.js";
5
5
  import {
6
6
  TCloudSandbox
7
- } from "./chunk-DBIT227N.js";
7
+ } from "./chunk-DRCOPW7D.js";
8
8
  import {
9
9
  generateWallet
10
- } from "./chunk-4ZUVGKVH.js";
11
- import "./chunk-M5K3EFNP.js";
10
+ } from "./chunk-YWN4JOCW.js";
11
+ import "./chunk-CVWEKCQ3.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { Command } from "commander";
@@ -24,8 +24,14 @@ function ensureDir() {
24
24
  }
25
25
  function loadConfig() {
26
26
  ensureDir();
27
- if (fs.existsSync(CONFIG_FILE)) return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
28
- return { apiUrl: "https://router.tangle.tools", defaultModel: "gpt-4o-mini", chainId: 3799 };
27
+ const fileConfig = fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) : { apiUrl: "https://router.tangle.tools", defaultModel: "gpt-4o-mini", chainId: 3799 };
28
+ return {
29
+ ...fileConfig,
30
+ ...process.env.TANGLE_ROUTER_URL ? { apiUrl: process.env.TANGLE_ROUTER_URL.replace(/\/v1\/?$/, "") } : {},
31
+ ...process.env.TCLOUD_API_URL ? { apiUrl: process.env.TCLOUD_API_URL.replace(/\/v1\/?$/, "") } : {},
32
+ ...process.env.TANGLE_API_KEY ? { apiKey: process.env.TANGLE_API_KEY } : {},
33
+ ...process.env.TCLOUD_API_KEY ? { apiKey: process.env.TCLOUD_API_KEY } : {}
34
+ };
29
35
  }
30
36
  function saveConfig(c) {
31
37
  ensureDir();
@@ -81,6 +87,15 @@ function packageVersion() {
81
87
  }
82
88
  return "0.0.0";
83
89
  }
90
+ function printJson(value) {
91
+ console.log(JSON.stringify(value, null, 2));
92
+ }
93
+ function firstImageResult(resp) {
94
+ return resp.data?.[0]?.url ?? resp.data?.[0]?.b64_json;
95
+ }
96
+ function videoResultUrl(resp) {
97
+ return resp.url ?? resp.video_url ?? resp.id;
98
+ }
84
99
  var program = new Command();
85
100
  program.name("tcloud").description("Tangle AI Cloud CLI").version(packageVersion());
86
101
  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) => {
@@ -342,6 +357,111 @@ program.command("models").description("List available models").option("-s, --sea
342
357
  console.error("Error:", e.message);
343
358
  }
344
359
  });
360
+ 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) => {
361
+ const client = getClient();
362
+ try {
363
+ const resp = await client.imageGenerate({
364
+ prompt: opts.prompt,
365
+ model: opts.model,
366
+ size: opts.size,
367
+ quality: opts.quality,
368
+ n: optionalNumber(opts.count),
369
+ response_format: opts.responseFormat
370
+ });
371
+ if (opts.json) {
372
+ printJson(resp);
373
+ return;
374
+ }
375
+ const result = firstImageResult(resp);
376
+ if (result) console.log(result);
377
+ else printJson(resp);
378
+ } catch (e) {
379
+ console.error("Error:", e.message);
380
+ process.exit(1);
381
+ }
382
+ });
383
+ 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) => {
384
+ const client = getClient();
385
+ try {
386
+ const resp = await client.videoGenerate({
387
+ prompt: opts.prompt,
388
+ model: opts.model,
389
+ provider: opts.provider,
390
+ duration: optionalNumber(opts.duration),
391
+ resolution: opts.resolution,
392
+ aspect_ratio: opts.aspectRatio,
393
+ size: opts.size,
394
+ image_url: opts.imageUrl,
395
+ generate_audio: opts.generateAudio,
396
+ seed: optionalNumber(opts.seed),
397
+ callback_url: opts.callbackUrl
398
+ });
399
+ if (opts.json) {
400
+ printJson(resp);
401
+ return;
402
+ }
403
+ const result = videoResultUrl(resp);
404
+ if (result) console.log(result);
405
+ else printJson(resp);
406
+ } catch (e) {
407
+ console.error("Error:", e.message);
408
+ process.exit(1);
409
+ }
410
+ });
411
+ 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) => {
412
+ const client = getClient();
413
+ try {
414
+ const audio = await client.speech({
415
+ input: opts.input,
416
+ model: opts.model,
417
+ voice: opts.voice
418
+ });
419
+ fs.writeFileSync(opts.output, Buffer.from(audio));
420
+ const result = { output: opts.output, bytes: audio.byteLength };
421
+ if (opts.json) printJson(result);
422
+ else console.log(opts.output);
423
+ } catch (e) {
424
+ console.error("Error:", e.message);
425
+ process.exit(1);
426
+ }
427
+ });
428
+ 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) => {
429
+ const client = getClient();
430
+ try {
431
+ const data = fs.readFileSync(file);
432
+ const blob = new Blob([data]);
433
+ const resp = await client.transcribe(blob, {
434
+ model: opts.model,
435
+ language: opts.language,
436
+ prompt: opts.prompt
437
+ });
438
+ if (opts.json) printJson(resp);
439
+ else console.log(resp.text);
440
+ } catch (e) {
441
+ console.error("Error:", e.message);
442
+ process.exit(1);
443
+ }
444
+ });
445
+ 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) => {
446
+ const client = getClient();
447
+ try {
448
+ const resp = await client.avatarGenerate({
449
+ audio_url: opts.audioUrl,
450
+ image_url: opts.imageUrl,
451
+ avatar_id: opts.avatarId,
452
+ duration_seconds: optionalNumber(opts.duration),
453
+ output_format: opts.outputFormat
454
+ });
455
+ if (opts.json) {
456
+ printJson(resp);
457
+ return;
458
+ }
459
+ console.log(resp.result?.video_url ?? resp.job_id);
460
+ } catch (e) {
461
+ console.error("Error:", e.message);
462
+ process.exit(1);
463
+ }
464
+ });
345
465
  program.command("operators").description("List active operators").action(async () => {
346
466
  const client = getClient();
347
467
  try {