@heyamiko/amiko-cli 0.14.0-beta.12 → 0.14.0-beta.14

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @heyamiko/amiko-cli (v0.14.0-beta.0)
1
+ # @heyamiko/amiko-cli (v0.14.0-beta.14)
2
2
 
3
3
  Manage wallets, credits, swaps, MPP marketplace services, and your Amiko twin (identity, documents, voice, avatar, friends, feed) from the terminal. Works for both human users and AI agents running on OpenClaw.
4
4
 
@@ -131,6 +131,10 @@ amiko create video "animate cover" --first-frame https://.../cover.png --yes
131
131
  amiko create video "dance clip" --model dreamina-seedance-2-0-fast-260128 \
132
132
  --reference-image https://.../ref1.png --reference-audio https://.../beat.mp3 --generate-audio --yes
133
133
  amiko create tts "Hello world" --voice 21m00Tcm4TlvDq8ikWAM
134
+ amiko create tts "Hello world" --provider minimax --model speech-2.8-turbo
135
+ amiko create tts "Hello" --provider minimax --voice English_expressive_narrator --yes
136
+ amiko voice clone ./me.mp3 --provider minimax
137
+ amiko voice clone ./me.mp3 --provider elevenlabs
134
138
  amiko create music "lo-fi chill beat" --duration 30000
135
139
  amiko create music --lyrics "..." --instrumental # instrumental
136
140
  amiko create sfx "thunder and heavy rain" --duration 5
@@ -178,8 +182,20 @@ Reference flags accept URLs or data URIs (not local paths). Typical I2V flow: `c
178
182
 
179
183
  | Flag | Default | Description |
180
184
  |------|---------|-------------|
181
- | `--model <model>` | `eleven_multilingual_v2` | TTS model id |
182
- | `--voice <voiceId>` | `21m00Tcm4TlvDq8ikWAM` | Voice id |
185
+ | `--provider <name>` | inferred / `elevenlabs` | `minimax` or `elevenlabs` — sets model/voice defaults when those flags are omitted |
186
+ | `--model <model>` | EL: `eleven_multilingual_v2`; MiniMax: `speech-2.8-turbo` | MiniMax: `speech-2.8-turbo\|hd`, `speech-2.6-turbo\|hd`. ElevenLabs: `eleven_*` |
187
+ | `--voice <voiceId>` | EL default hash; MiniMax: `English_expressive_narrator` | MiniMax system or cloned `voice_id`; ElevenLabs voice id |
188
+
189
+ Examples: `amiko create tts "Hi" --provider minimax --yes` · `amiko create tts "Hi" --provider minimax --model speech-2.8-turbo --voice <clonedId> --yes`
190
+
191
+ **`amiko voice clone <source>`** — audio path, https URL, or `-` (stdin)
192
+
193
+ | Flag | Default | Description |
194
+ |------|---------|-------------|
195
+ | `--provider <name>` | `minimax` | `minimax` (mpp rapid clone + ownership registry) or `elevenlabs` (legacy IVC) |
196
+ | `--voice-id <id>` | auto | **MiniMax only** — optional custom voice_id (ignored for elevenlabs) |
197
+ | `--model <model>` | `speech-2.8-turbo` | **MiniMax only** — preview speech model (ignored for elevenlabs) |
198
+ | `--preview-text <text>` | provider default | **MiniMax only** — preview line; empty string skips preview (ignored for elevenlabs) |
183
199
 
184
200
  **`amiko create music [prompt]`** — pass a prompt and/or `--lyrics`
185
201
 
@@ -345,8 +361,11 @@ amiko markets service call POST /v1/sfx '{"text":"Thunder with heavy rain","dura
345
361
  # Music generation ($0.10) — returns { url, format, service }
346
362
  amiko markets service call POST /v1/music '{"prompt":"Lo-fi chill beat","music_length_ms":30000}'
347
363
 
348
- # Text-to-speech (1 AMIKO) — returns { url, format, service }
364
+ # Text-to-speech — ElevenLabs voice id (default)
349
365
  amiko markets service tts 21m00Tcm4TlvDq8ikWAM "Hello world"
366
+ # MiniMax system or cloned voice_id
367
+ amiko markets service tts English_expressive_narrator "Hello" --provider minimax --yes
368
+ amiko markets service tts <clonedId> "Hello" --provider minimax --model speech-2.8-turbo --yes
350
369
 
351
370
  # Composition plan ($0.02)
352
371
  amiko markets service call POST /v1/music/plan '{"prompt":"Epic orchestral piece"}'
@@ -564,6 +583,16 @@ npm publish
564
583
 
565
584
  ## Changelog
566
585
 
586
+ ### 0.14.0-beta.14
587
+
588
+ - `credits balance` prefers `AMIKO_API_KEY` (`amk_`) → `billing.heyamiko.com/api/credits` directly; falls back to twin-token platform proxy. Override base with `AMIKO_BILLING_URL`.
589
+
590
+ ### 0.14.0-beta.13
591
+
592
+ - **Create Studio TTS + voice clone: MiniMax.** `amiko create tts` accepts `--provider minimax|elevenlabs` (infers from `speech-*` / `eleven_*` models when omitted), with provider-specific defaults (`speech-2.8-turbo` + `English_expressive_narrator` vs `eleven_multilingual_v2` + Rachel). Quote body includes `voiceId`. Cloned-voice examples require `--provider minimax` when using a MiniMax `voice_id`.
593
+ - **`amiko voice clone`** defaults to MiniMax (mpp rapid clone + ownership registry); `--provider elevenlabs` keeps legacy IVC. MiniMax-only flags: `--voice-id`, `--model`, `--preview-text`.
594
+ - **`amiko markets service tts`** gains `--provider minimax|elevenlabs` and `--model` for MiniMax speech routing on `/v1/tts/:voiceId`.
595
+
567
596
  ### 0.14.0-beta.7
568
597
 
569
598
  - **`amiko chat group invite <group>` — share a group's invite/join link + QR.** New read-only subcommand that fetches `GET /api/conversations/{id}/invite` (twin token) and prints the join link plus a scannable QR image URL. Admins-only: SKILL.md instructs the agent to confirm the asker holds `admin`/`owner` (via `amiko chat group info`) before sharing, and the endpoint is admin-gated server-side (403 → admin tip). Fails loudly if the response is missing its link/QR instead of printing a placeholder. Requires the companion amiko-web change (adds `qrUrl` to the invite response + a public `GET /api/conversations/join/[token]/qr` PNG route).
package/dist/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- }
864
+ } else {}
865
865
  };
866
866
  }
867
867
  return this;
@@ -3596,7 +3596,7 @@ function getHumanReadableErrorMessage(code, context = {}) {
3596
3596
  function getErrorMessage(code, context = {}) {
3597
3597
  if (true) {
3598
3598
  return getHumanReadableErrorMessage(code, context);
3599
- }
3599
+ } else {}
3600
3600
  }
3601
3601
  function isSolanaError(e, code) {
3602
3602
  const isSolanaError2 = e instanceof Error && e.name === "SolanaError";
@@ -19992,7 +19992,7 @@ function finalize(ctx, schema) {
19992
19992
  result.$schema = "http://json-schema.org/draft-07/schema#";
19993
19993
  } else if (ctx.target === "draft-04") {
19994
19994
  result.$schema = "http://json-schema.org/draft-04/schema#";
19995
- } else if (ctx.target === "openapi-3.0") {}
19995
+ } else if (ctx.target === "openapi-3.0") {} else {}
19996
19996
  if (ctx.external?.uri) {
19997
19997
  const id = ctx.external.registry.get(schema)?.id;
19998
19998
  if (!id)
@@ -20257,7 +20257,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
20257
20257
  if (val === undefined) {
20258
20258
  if (ctx.unrepresentable === "throw") {
20259
20259
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
20260
- }
20260
+ } else {}
20261
20261
  } else if (typeof val === "bigint") {
20262
20262
  if (ctx.unrepresentable === "throw") {
20263
20263
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -23655,27 +23655,50 @@ function readFromFile(path) {
23655
23655
  twinId: asString(parsed.amikoTwinId) ?? asString(parsed.AMIKO_TWIN_ID),
23656
23656
  userId: asString(parsed.amikoUserId) ?? asString(parsed.AMIKO_USER_ID),
23657
23657
  platformUrl: asString(parsed.amikoPlatformUrl) ?? asString(parsed.AMIKO_PLATFORM_URL),
23658
- apiBase: asString(parsed.amikoApiBase) ?? asString(parsed.AMIKO_API_BASE) ?? asString(parsed.apiBase)
23658
+ apiBase: asString(parsed.amikoApiBase) ?? asString(parsed.AMIKO_API_BASE) ?? asString(parsed.apiBase),
23659
+ billingApiKey: asString(parsed.amikoApiKey) ?? asString(parsed.AMIKO_API_KEY) ?? asString(parsed.billingApiKey),
23660
+ billingUrl: asString(parsed.amikoBillingUrl) ?? asString(parsed.AMIKO_BILLING_URL) ?? asString(parsed.billingUrl)
23659
23661
  };
23660
23662
  } catch {
23661
23663
  return null;
23662
23664
  }
23663
23665
  }
23666
+ function readRoutingIdsFromEnv() {
23667
+ return {
23668
+ agentId: asString(process.env.AMIKO_AGENT_ID),
23669
+ sessionId: asString(process.env.AMIKO_SESSION_ID)
23670
+ };
23671
+ }
23664
23672
  function readFromEnv() {
23665
23673
  return {
23666
23674
  twinToken: asString(process.env.AMIKO_TWIN_TOKEN) ?? asString(process.env.AMIKO_USER_TOKEN),
23667
23675
  twinId: asString(process.env.AMIKO_TWIN_ID),
23668
23676
  userId: asString(process.env.AMIKO_USER_ID),
23669
23677
  platformUrl: asString(process.env.AMIKO_PLATFORM_URL),
23670
- apiBase: asString(process.env.AMIKO_API_BASE)
23678
+ apiBase: asString(process.env.AMIKO_API_BASE),
23679
+ billingApiKey: asString(process.env.AMIKO_API_KEY),
23680
+ billingUrl: asString(process.env.AMIKO_BILLING_URL),
23681
+ ...readRoutingIdsFromEnv()
23682
+ };
23683
+ }
23684
+ function resolveFileAuth(path) {
23685
+ const fromFile = readFromFile(path);
23686
+ if (!fromFile)
23687
+ return null;
23688
+ const fromEnv = readFromEnv();
23689
+ return {
23690
+ ...fromFile,
23691
+ ...readRoutingIdsFromEnv(),
23692
+ billingApiKey: fromEnv.billingApiKey ?? fromFile.billingApiKey,
23693
+ billingUrl: fromEnv.billingUrl ?? fromFile.billingUrl
23671
23694
  };
23672
23695
  }
23673
23696
  function resolveAuth() {
23674
23697
  const filePath = findAmikoJsonPath();
23675
23698
  if (filePath) {
23676
- const fromFile = readFromFile(filePath);
23677
- if (fromFile)
23678
- return fromFile;
23699
+ const merged = resolveFileAuth(filePath);
23700
+ if (merged)
23701
+ return merged;
23679
23702
  }
23680
23703
  return readFromEnv();
23681
23704
  }
@@ -25405,7 +25428,31 @@ function registerBridgeCommand(program2) {
25405
25428
  import readline2 from "node:readline";
25406
25429
 
25407
25430
  // src/lib/balance.ts
25408
- async function fetchCreditsBalance(auth) {
25431
+ var DEFAULT_BILLING_URL = "https://billing.heyamiko.com";
25432
+ function billingBase(auth) {
25433
+ const url = auth.billingUrl || DEFAULT_BILLING_URL;
25434
+ return url.replace(/\/$/, "");
25435
+ }
25436
+ async function fetchCreditsBalanceViaAmk(auth) {
25437
+ const apiKey = auth.billingApiKey;
25438
+ if (!apiKey || !apiKey.startsWith("amk_"))
25439
+ return null;
25440
+ try {
25441
+ const res = await fetch(`${billingBase(auth)}/api/credits`, {
25442
+ method: "GET",
25443
+ headers: { Authorization: `Bearer ${apiKey}` },
25444
+ signal: AbortSignal.timeout(15000)
25445
+ });
25446
+ if (!res.ok)
25447
+ return null;
25448
+ const data = await res.json();
25449
+ const raw = data.displayBalance;
25450
+ return typeof raw === "number" && isFinite(raw) ? Math.round(raw) : null;
25451
+ } catch {
25452
+ return null;
25453
+ }
25454
+ }
25455
+ async function fetchCreditsBalanceViaPlatformProxy(auth) {
25409
25456
  if (!auth.twinToken)
25410
25457
  return null;
25411
25458
  try {
@@ -25416,6 +25463,12 @@ async function fetchCreditsBalance(auth) {
25416
25463
  return null;
25417
25464
  }
25418
25465
  }
25466
+ async function fetchCreditsBalance(auth) {
25467
+ const viaAmk = await fetchCreditsBalanceViaAmk(auth);
25468
+ if (viaAmk != null)
25469
+ return viaAmk;
25470
+ return fetchCreditsBalanceViaPlatformProxy(auth);
25471
+ }
25419
25472
  async function printBalanceRemaining(auth) {
25420
25473
  const balance = await fetchCreditsBalance(auth);
25421
25474
  if (balance == null) {
@@ -26442,6 +26495,13 @@ var ETA = {
26442
26495
  function statusOf(err) {
26443
26496
  return err?.status;
26444
26497
  }
26498
+ function autoApproveLimit() {
26499
+ const raw = process.env.AMIKO_AUTO_APPROVE_LIMIT;
26500
+ if (typeof raw !== "string" || raw.trim() === "")
26501
+ return;
26502
+ const limit = Number(raw);
26503
+ return Number.isFinite(limit) ? limit : undefined;
26504
+ }
26445
26505
  function collectOption(value, previous) {
26446
26506
  return [...previous ?? [], value];
26447
26507
  }
@@ -26451,7 +26511,7 @@ function assertMaxUrls(resourceName, urls, max) {
26451
26511
  process.exit(1);
26452
26512
  }
26453
26513
  }
26454
- function printCompleted(final, title) {
26514
+ function printCompleted(final, title, deliveredToThisSession = false) {
26455
26515
  console.log(heading(`${title}
26456
26516
  `));
26457
26517
  if (final.prompt)
@@ -26460,6 +26520,9 @@ function printCompleted(final, title) {
26460
26520
  console.log(label("URL", final.assetUrl));
26461
26521
  if (final.assetMime)
26462
26522
  console.log(label("Type", final.assetMime));
26523
+ if (deliveredToThisSession) {
26524
+ console.log(success("Already delivered to the user in this chat automatically. Do NOT send this file again and do NOT paste the URL — you would duplicate it."));
26525
+ }
26463
26526
  console.log("");
26464
26527
  if (final.charged) {
26465
26528
  const amt = final.chargeAmount ?? final.costAmiko ?? "?";
@@ -26488,6 +26551,7 @@ async function submitCreateJob(opts) {
26488
26551
  console.error(dim("--token is ignored with --pay credits (credits come from your Amiko account, not a wallet token)."));
26489
26552
  }
26490
26553
  let costLine = payCredits ? "reserved from your Amiko account credits, captured on success (released on failure)" : "charged from your twin wallet on success (nothing on failure)";
26554
+ let quotedCostAmiko;
26491
26555
  try {
26492
26556
  const q = await amikoWebFetch(auth, "/api/create/quote", {
26493
26557
  method: "POST",
@@ -26497,11 +26561,29 @@ async function submitCreateJob(opts) {
26497
26561
  if (q.cost_usd != null) {
26498
26562
  costLine = payCredits ? `≈ ${Math.ceil(q.cost_usd * 1e4)} Amiko credits (≈ $${q.cost_usd.toFixed(4)}) — reserved on submit, captured only on success` : `${q.cost_amiko ?? "?"} AMIKO (≈ $${q.cost_usd.toFixed(4)}) — charged only on success`;
26499
26563
  }
26564
+ if (typeof q.cost_amiko === "string" && q.cost_amiko.trim() !== "") {
26565
+ const n = Number(q.cost_amiko);
26566
+ if (Number.isFinite(n))
26567
+ quotedCostAmiko = n;
26568
+ }
26500
26569
  } catch {}
26570
+ const preferredToken = typeof opts.jobBody.preferredToken === "string" ? opts.jobBody.preferredToken.toUpperCase() : undefined;
26571
+ const chargeIsAmiko = !payCredits && (preferredToken === undefined || preferredToken === "AMIKO");
26572
+ const effectiveChargeToken = preferredToken ?? "AMIKO";
26573
+ let yes = opts.yes;
26574
+ let autoApproved = false;
26575
+ if (!yes) {
26576
+ const limit = autoApproveLimit();
26577
+ if (limit !== undefined && quotedCostAmiko !== undefined && quotedCostAmiko > 0 && chargeIsAmiko && quotedCostAmiko <= limit) {
26578
+ yes = true;
26579
+ autoApproved = true;
26580
+ console.log(dim(`Auto-approved (cost ${quotedCostAmiko} ≤ limit ${limit} AMIKO)`));
26581
+ }
26582
+ }
26501
26583
  await requireApproval({
26502
26584
  cost: costLine,
26503
26585
  summary: opts.summary,
26504
- yes: opts.yes,
26586
+ yes,
26505
26587
  commandExample: `amiko create ${opts.mode.toLowerCase()} ...`
26506
26588
  });
26507
26589
  const isTTY = process.stderr.isTTY;
@@ -26511,16 +26593,21 @@ async function submitCreateJob(opts) {
26511
26593
  spinner.stop();
26512
26594
  };
26513
26595
  let jobId;
26596
+ let routedAgentId;
26597
+ let routedSessionId;
26514
26598
  try {
26515
26599
  const submit = await amikoWebFetch(auth, "/api/create/jobs", {
26516
26600
  method: "POST",
26517
26601
  body: {
26518
26602
  ...opts.jobBody,
26519
26603
  twinId: auth.twinId,
26604
+ ...autoApproved ? { preferredToken: effectiveChargeToken, strictToken: true } : {},
26520
26605
  ...payCredits ? {
26521
26606
  paymentMethod: { kind: "account_credits" },
26522
26607
  requestId: randomUUID()
26523
- } : {}
26608
+ } : {},
26609
+ ...auth.agentId ? { agentId: auth.agentId } : {},
26610
+ ...auth.sessionId ? { sessionId: auth.sessionId } : {}
26524
26611
  },
26525
26612
  timeoutMs: 30000
26526
26613
  });
@@ -26530,6 +26617,8 @@ async function submitCreateJob(opts) {
26530
26617
  process.exit(1);
26531
26618
  }
26532
26619
  jobId = submit.jobId;
26620
+ routedAgentId = submit.agentId;
26621
+ routedSessionId = submit.sessionId;
26533
26622
  } catch (e5) {
26534
26623
  stop();
26535
26624
  const webErr = e5;
@@ -26550,19 +26639,24 @@ async function submitCreateJob(opts) {
26550
26639
  }
26551
26640
  process.exit(1);
26552
26641
  }
26642
+ const sessionRouted = !!(routedAgentId && routedSessionId);
26553
26643
  stop();
26554
26644
  if (opts.raw) {
26555
26645
  console.log(JSON.stringify({
26556
26646
  jobId,
26557
26647
  status: "PENDING",
26558
26648
  check: `amiko create status ${jobId}`,
26559
- eta: ETA[opts.mode]
26649
+ eta: ETA[opts.mode],
26650
+ ...sessionRouted ? { sessionRouted: true, deliveredToSession: routedSessionId } : {}
26560
26651
  }, null, 2));
26561
26652
  return;
26562
26653
  }
26563
26654
  console.log(success(`Submitted — job ${jobId}`));
26564
26655
  console.log(label("Status", "PENDING — generating in the background"));
26565
26656
  console.log(label("Check result", `amiko create status ${jobId}`));
26657
+ if (sessionRouted) {
26658
+ console.log(dim("A generating-placeholder is already showing in the chat and the finished media will be delivered there automatically — do not send the file or URL yourself."));
26659
+ }
26566
26660
  console.log(dim(`Usually ready in ${ETA[opts.mode]} — charged only on success. (Also: amiko create media)`));
26567
26661
  if (opts.mode === "VIDEO") {
26568
26662
  console.log(dim("Async video: HTTP 202 / PENDING means the job was accepted — not a failure. Poll status after ~2–9 min."));
@@ -26610,7 +26704,8 @@ function registerCreateCommand(create2) {
26610
26704
  mode: "VIDEO",
26611
26705
  model,
26612
26706
  size: opts.resolution,
26613
- seconds
26707
+ seconds,
26708
+ ...referenceVideoUrls.length ? { referenceVideoUrls } : {}
26614
26709
  },
26615
26710
  jobBody: {
26616
26711
  mode: "VIDEO",
@@ -26637,19 +26732,54 @@ function registerCreateCommand(create2) {
26637
26732
  }
26638
26733
  });
26639
26734
  });
26640
- create2.command("tts <text>").description("Generate speech from text (Create Studio)").option("--model <model>", "TTS model id", "eleven_multilingual_v2").option("--voice <voiceId>", "Voice id", "21m00Tcm4TlvDq8ikWAM").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: wallet (twin wallet, default) or credits (Amiko account credits — unified accounts only)", "wallet").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
26735
+ const DEFAULT_EL_TTS_MODEL = "eleven_multilingual_v2";
26736
+ const DEFAULT_EL_TTS_VOICE = "21m00Tcm4TlvDq8ikWAM";
26737
+ const DEFAULT_MM_TTS_MODEL = "speech-2.8-turbo";
26738
+ const DEFAULT_MM_TTS_VOICE = "English_expressive_narrator";
26739
+ const MINIMAX_TTS_MODELS = new Set([
26740
+ "speech-2.8-turbo",
26741
+ "speech-2.8-hd",
26742
+ "speech-2.6-turbo",
26743
+ "speech-2.6-hd"
26744
+ ]);
26745
+ create2.command("tts <text>").description("Generate speech from text (Create Studio)").option("--provider <name>", "TTS provider: minimax | elevenlabs (sets defaults when --model/--voice omitted)").option("--model <model>", `TTS model id (MiniMax: speech-2.8-turbo|hd, speech-2.6-turbo|hd; ElevenLabs: eleven_*)`).option("--voice <voiceId>", "Voice id (MiniMax system e.g. English_expressive_narrator, or a cloned voice_id; ElevenLabs voice hash)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: wallet (twin wallet, default) or credits (Amiko account credits — unified accounts only)", "wallet").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
26746
+ const providerRaw = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
26747
+ if (providerRaw && providerRaw !== "minimax" && providerRaw !== "elevenlabs") {
26748
+ console.error(error("--provider must be minimax or elevenlabs"));
26749
+ process.exit(1);
26750
+ }
26751
+ let model = typeof opts.model === "string" && opts.model.trim().length > 0 ? opts.model.trim() : undefined;
26752
+ let voice = typeof opts.voice === "string" && opts.voice.trim().length > 0 ? opts.voice.trim() : undefined;
26753
+ let provider = providerRaw === "minimax" || providerRaw === "elevenlabs" ? providerRaw : model && MINIMAX_TTS_MODELS.has(model) ? "minimax" : model && model.startsWith("eleven") ? "elevenlabs" : "elevenlabs";
26754
+ if (!model) {
26755
+ model = provider === "minimax" ? DEFAULT_MM_TTS_MODEL : DEFAULT_EL_TTS_MODEL;
26756
+ }
26757
+ if (!voice) {
26758
+ voice = provider === "minimax" ? DEFAULT_MM_TTS_VOICE : DEFAULT_EL_TTS_VOICE;
26759
+ }
26760
+ if (MINIMAX_TTS_MODELS.has(model) && providerRaw !== "elevenlabs") {
26761
+ provider = "minimax";
26762
+ }
26763
+ if (provider === "minimax" && !MINIMAX_TTS_MODELS.has(model)) {
26764
+ console.error(error(`--provider minimax requires a MiniMax speech model (${[...MINIMAX_TTS_MODELS].join(", ")})`));
26765
+ process.exit(1);
26766
+ }
26767
+ if (provider === "elevenlabs" && !model.startsWith("eleven")) {
26768
+ console.error(error("--provider elevenlabs requires an eleven_* model"));
26769
+ process.exit(1);
26770
+ }
26641
26771
  await submitCreateJob({
26642
26772
  mode: "TTS",
26643
- summary: `Create speech: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
26773
+ summary: `Create speech (${provider}/${model}): ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
26644
26774
  raw: opts.raw,
26645
26775
  pay: opts.pay,
26646
26776
  yes: opts.yes,
26647
- quoteBody: { mode: "TTS", model: opts.model, prompt: text },
26777
+ quoteBody: { mode: "TTS", model, prompt: text, voiceId: voice },
26648
26778
  jobBody: {
26649
26779
  mode: "TTS",
26650
26780
  prompt: text,
26651
- model: opts.model,
26652
- voiceId: opts.voice,
26781
+ model,
26782
+ voiceId: voice,
26653
26783
  ...opts.token ? { preferredToken: opts.token } : {}
26654
26784
  }
26655
26785
  });
@@ -26713,8 +26843,9 @@ function registerCreateCommand(create2) {
26713
26843
  }
26714
26844
  process.exit(1);
26715
26845
  }
26846
+ const deliveredToThisSession = !!(p.agentId && p.sessionId && auth.sessionId && p.sessionId === auth.sessionId);
26716
26847
  if (opts.raw) {
26717
- console.log(JSON.stringify(p, null, 2));
26848
+ console.log(JSON.stringify(deliveredToThisSession ? { ...p, sessionRouted: true, deliveredToSession: p.sessionId } : p, null, 2));
26718
26849
  return;
26719
26850
  }
26720
26851
  if (p.status === "FAILED") {
@@ -26726,7 +26857,7 @@ function registerCreateCommand(create2) {
26726
26857
  console.log(dim(`Job id: ${jobId}`));
26727
26858
  return;
26728
26859
  }
26729
- printCompleted(p, "Job complete");
26860
+ printCompleted(p, "Job complete", deliveredToThisSession);
26730
26861
  });
26731
26862
  create2.command("media").description("List your recent Create Studio generations").option("--limit <n>", "How many to show (max 50)", "20").option("--service <type>", "Filter by type: image, video, tts, music, sfx").option("--raw", "Output raw JSON").action(async (opts) => {
26732
26863
  let auth;
@@ -28311,18 +28442,32 @@ async function fetchWithTimeout2(url2, init = {}, timeoutMs) {
28311
28442
  }
28312
28443
  function registerServicesCommand(program2) {
28313
28444
  const svc = program2.command("service").alias("svc").description("Call any MPP service — pay in AMIKO, USDC, or USDT");
28314
- svc.command("tts <voiceId> <text>").description("Text-to-speech — 1 AMIKO").option("--raw", "Output raw response").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if none)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (voiceId, text, opts) => {
28445
+ svc.command("tts <voiceId> <text>").description("Text-to-speech via mpp /v1/tts (pay-per-call)").option("--provider <name>", "elevenlabs (default) | minimax").option("--model <model>", "MiniMax speech-* model when provider=minimax").option("--raw", "Output raw response").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if none)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (voiceId, text, opts) => {
28446
+ const provider = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
28447
+ const body = { text };
28448
+ if (provider === "minimax") {
28449
+ body.provider = "minimax";
28450
+ if (opts.model)
28451
+ body.model_id = opts.model;
28452
+ else
28453
+ body.model_id = "speech-2.8-turbo";
28454
+ } else if (provider && provider !== "elevenlabs") {
28455
+ console.error(error("--provider must be minimax or elevenlabs"));
28456
+ process.exit(1);
28457
+ }
28458
+ const providerArgs = provider ? ` --provider ${provider}` : "";
28459
+ const modelArgs = provider === "minimax" ? ` --model ${JSON.stringify(typeof opts.model === "string" && opts.model ? opts.model : "speech-2.8-turbo")}` : "";
28315
28460
  await requireApproval({
28316
- cost: "1 AMIKO",
28317
- summary: `Text-to-speech (voice ${voiceId}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
28461
+ cost: "variable (see mpp pricing)",
28462
+ summary: `Text-to-speech (voice ${voiceId}${provider ? `, ${provider}` : ""}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
28318
28463
  yes: opts.yes,
28319
- commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}`
28464
+ commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}${providerArgs}${modelArgs}`
28320
28465
  });
28321
28466
  await callService({
28322
28467
  name: "Text-to-Speech",
28323
28468
  method: "POST",
28324
28469
  path: `/v1/tts/${encodeURIComponent(voiceId)}`,
28325
- body: { text },
28470
+ body,
28326
28471
  amiko: "1",
28327
28472
  opts
28328
28473
  });
@@ -28534,7 +28679,7 @@ function registerAccountsCommand(program2) {
28534
28679
  }
28535
28680
  }, { json: opts.json });
28536
28681
  });
28537
- program2.command("info").description("Get twin info (name, description, voice, avatar)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
28682
+ program2.command("info").description("Get twin info (name, description, voice ids, avatar)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
28538
28683
  const config2 = loadConfig();
28539
28684
  const auth = resolveAuth();
28540
28685
  let twinId;
@@ -28558,13 +28703,24 @@ function registerAccountsCommand(program2) {
28558
28703
  console.log(label("Description", t.description));
28559
28704
  console.log(label("Public", t.is_public ? "yes" : "no"));
28560
28705
  if (t.voice_status)
28561
- console.log(label("Voice", t.voice_status));
28706
+ console.log(label("Voice status", t.voice_status));
28707
+ if (t.voice_id)
28708
+ console.log(label("ElevenLabs voice_id", t.voice_id));
28709
+ else
28710
+ console.log(label("ElevenLabs voice_id", dim("(none)")));
28711
+ if (t.minimax_voice_id)
28712
+ console.log(label("MiniMax voice_id", t.minimax_voice_id));
28713
+ else
28714
+ console.log(label("MiniMax voice_id", dim("(none — clone with amiko voice clone)")));
28562
28715
  if (t.avatar_url)
28563
28716
  console.log(label("Avatar", t.avatar_url));
28564
28717
  if (t.shipped_at)
28565
28718
  console.log(label("Shipped", t.shipped_at));
28566
28719
  if (t.updated_at)
28567
28720
  console.log(dim(`Updated ${t.updated_at}`));
28721
+ console.log("");
28722
+ console.log(dim('MiniMax TTS: amiko create tts "…" --provider minimax --voice <MiniMax voice_id> --yes'));
28723
+ console.log(dim('ElevenLabs TTS: amiko create tts "…" --provider elevenlabs --voice <ElevenLabs voice_id> --yes'));
28568
28724
  }, { json: opts.json });
28569
28725
  } catch (e5) {
28570
28726
  spinner?.stop();
@@ -29201,8 +29357,18 @@ function twinFromFlag2(opts) {
29201
29357
  throw e5;
29202
29358
  }
29203
29359
  }
29360
+ function parseProvider(raw, fallback2) {
29361
+ if (raw == null || raw.trim() === "")
29362
+ return fallback2;
29363
+ const p = raw.trim().toLowerCase();
29364
+ if (p !== "minimax" && p !== "elevenlabs") {
29365
+ console.error(error("--provider must be minimax or elevenlabs"));
29366
+ process.exit(1);
29367
+ }
29368
+ return p;
29369
+ }
29204
29370
  function registerVoiceCommand(program2) {
29205
- program2.command("design").description("Design a voice from a text description").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
29371
+ program2.command("design").description("Design a voice from a text description (ElevenLabs)").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
29206
29372
  if (opts.description.trim().length < 20) {
29207
29373
  console.error(error("--description must be at least 20 characters"));
29208
29374
  process.exit(1);
@@ -29230,7 +29396,7 @@ function registerVoiceCommand(program2) {
29230
29396
  process.exit(1);
29231
29397
  }
29232
29398
  });
29233
- program2.command("create").description("Create a voice from a previously designed preview").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
29399
+ program2.command("create").description("Create a voice from a previously designed preview (ElevenLabs)").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
29234
29400
  const { twinId, auth } = twinFromFlag2(opts);
29235
29401
  const spinner = opts.json ? null : ora("Creating voice...").start();
29236
29402
  try {
@@ -29256,7 +29422,8 @@ function registerVoiceCommand(program2) {
29256
29422
  process.exit(1);
29257
29423
  }
29258
29424
  });
29259
- program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
29425
+ program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -). Default provider: minimax").option("--provider <name>", "minimax (default, mpp rapid clone) | elevenlabs (platform IVC)", "minimax").option("--voice-id <id>", "MiniMax: optional custom voice_id (8–256 chars)").option("--model <model>", "MiniMax preview TTS model", "speech-2.8-turbo").option("--preview-text <text>", "MiniMax: preview line (empty string skips preview)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
29426
+ const provider = parseProvider(opts.provider, "minimax");
29260
29427
  const { twinId, auth } = twinFromFlag2(opts);
29261
29428
  const spinner = opts.json ? null : ora("Reading audio...").start();
29262
29429
  try {
@@ -29266,17 +29433,30 @@ function registerVoiceCommand(program2) {
29266
29433
  hintMime: "audio/mpeg"
29267
29434
  });
29268
29435
  if (spinner)
29269
- spinner.text = `Cloning voice from ${file2.name}...`;
29436
+ spinner.text = `Cloning voice (${provider}) from ${file2.name}...`;
29270
29437
  const form = new FormData;
29271
29438
  form.set("audio", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
29439
+ form.set("provider", provider);
29440
+ if (provider === "minimax") {
29441
+ if (opts.voiceId)
29442
+ form.set("voice_id", opts.voiceId);
29443
+ if (opts.model)
29444
+ form.set("model_id", opts.model);
29445
+ if (opts.previewText !== undefined)
29446
+ form.set("text", opts.previewText);
29447
+ }
29272
29448
  const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/clone`, { method: "POST", multipart: form, timeoutMs: 180000 });
29273
29449
  spinner?.stop();
29274
29450
  renderOutput(result, (r) => {
29275
- console.log(success("Voice cloned"));
29451
+ console.log(success(`Voice cloned (${String(r.provider ?? provider)})`));
29276
29452
  if (r.voice_id)
29277
29453
  console.log(label("Voice ID", String(r.voice_id)));
29278
29454
  if (r.voice_status)
29279
29455
  console.log(label("Status", String(r.voice_status)));
29456
+ if (r.demo_audio)
29457
+ console.log(label("Demo", String(r.demo_audio)));
29458
+ console.log("");
29459
+ console.log(dim(`Synthesize: amiko create tts "Hello" --provider ${provider} --voice ${String(r.voice_id ?? "<id>")} --yes`));
29280
29460
  }, { json: opts.json });
29281
29461
  } catch (e5) {
29282
29462
  spinner?.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.14.0-beta.12",
3
+ "version": "0.14.0-beta.14",
4
4
  "description": "Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace agents",
5
5
  "type": "module",
6
6
  "bin": {
package/skills/SKILL.md CHANGED
@@ -35,6 +35,8 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
35
35
  | "share the group's invite link" (asked by an admin) | shell → `amiko chat group info "<group>"` (confirm they're admin) → `amiko chat group invite "<group>"` |
36
36
  | "react to the latest message from Ava about the team with a heart" | shell → `amiko chat read "Ava"` (find that message, copy its `id`) → `amiko chat react <messageId> ❤️ --yes` |
37
37
  | "what can amiko do?" | shell → `amiko --help` |
38
+ | "what's my MiniMax / ElevenLabs voice id?" | shell → `amiko info` |
39
+ | "speak with my cloned MiniMax voice" | shell → `amiko info`, quote the cost, obtain explicit approval, then run `amiko create tts "…" --provider minimax --voice <minimax_voice_id> --yes` |
38
40
 
39
41
  The CLI is installed globally and is pre-authenticated when you're inside your workspace folder. Never suggest `amiko login` or `amiko connect` — they don't exist.
40
42
 
@@ -96,6 +98,21 @@ All paid markets commands auto-select the twin's active Solana wallet (see `--wa
96
98
 
97
99
  **Prefer `amiko create` for anything it supports.** For media generation — image, video, speech (TTS), music, SFX — use `amiko create` (async, charge-on-success, never loses money on a timeout), NOT `markets`. Reach for `markets` **only for what `create` does not cover**: X/Twitter search, Amazon, AI chat, speech-to-text/transcription, and arbitrary MPP endpoints via `markets service call`.
98
100
 
101
+ ### Voice IDs — never guess
102
+
103
+ This twin has **two** optional TTS ids. They are **not** the same:
104
+
105
+ | Field | Provider | How to get it | How to use |
106
+ |---|---|---|---|
107
+ | `voice_id` | ElevenLabs | shown by `amiko info` | quote cost + approval, then `amiko create tts "…" --provider elevenlabs --voice <voice_id> --yes` |
108
+ | `minimax_voice_id` | MiniMax | shown by `amiko info` | quote cost + approval, then `amiko create tts "…" --provider minimax --voice <minimax_voice_id> --yes` |
109
+
110
+ - **Before any TTS or clone follow-up**, run `amiko info` (or `amiko info --json`) and read the printed ids. Do **not** invent a `voice_id`, ask the owner to paste one you already have on the twin, or claim MiniMax TTS is blocked because you "don't know" the id.
111
+ - Paid voice ops still need the Critical Rules gate: quote live cost, get explicit approval, only then append `--yes`.
112
+ - If MiniMax voice_id is `(none)`, quote the clone cost, obtain explicit approval, then run `amiko voice clone <audio> --provider minimax --yes` and `amiko info` again.
113
+ - System MiniMax voices (no clone) also work: e.g. `--voice English_expressive_narrator --provider minimax` (same approval gate before `--yes`).
114
+ - `markets service tts <voiceId> "…" --provider minimax --yes` needs the same MiniMax id and approval gate.
115
+
99
116
  **Payment token:** `markets search`, `markets image`, and `markets amazon search` accept `--token <AMIKO|USDC|USDT>` (default AMIKO); `markets service call` uses `--usdc`/`--usdt` (pass `auto` to price from USD). Stables are pinned at $1.00 — the amount equals the endpoint's USD price — so there is no forced swap through AMIKO. When the owner asks to pay in USDT/USDC, pass the flag; do NOT swap first.
100
117
 
101
118
  ## Create Studio — behavior notes