@heyamiko/amiko-cli 0.7.10 → 0.8.0

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/index.js CHANGED
@@ -71810,12 +71810,14 @@ function resetConfig() {
71810
71810
  return true;
71811
71811
  }
71812
71812
  function getPrivyToken(config) {
71813
- return config.privyToken || config.creatorToken;
71813
+ return config.privyToken;
71814
71814
  }
71815
71815
  function normalizeConfig(config) {
71816
71816
  const normalized = { ...config };
71817
- if (!normalized.privyToken && normalized.creatorToken) {
71818
- normalized.privyToken = normalized.creatorToken;
71817
+ const raw = normalized;
71818
+ if (!normalized.privyToken && typeof raw.creatorToken === "string") {
71819
+ normalized.privyToken = raw.creatorToken;
71820
+ delete raw.creatorToken;
71819
71821
  }
71820
71822
  if (LEGACY_SITE_BASES.has(normalized.siteBase)) {
71821
71823
  normalized.siteBase = DEFAULTS.siteBase;
@@ -71996,6 +71998,13 @@ function success(text) {
71996
71998
  function warn(text) {
71997
71999
  return source_default.yellow("⚠ ") + text;
71998
72000
  }
72001
+ function renderOutput(data, human, opts = {}) {
72002
+ if (opts.json) {
72003
+ console.log(JSON.stringify(data, null, 2));
72004
+ return;
72005
+ }
72006
+ human(data);
72007
+ }
71999
72008
 
72000
72009
  // src/onboarding.ts
72001
72010
  var SKIP_COMMANDS = new Set([
@@ -72984,10 +72993,7 @@ function authHeaders() {
72984
72993
  }
72985
72994
  async function get(path) {
72986
72995
  const res = await fetch(`${mppBase()}${path}`, { headers: authHeaders() });
72987
- if (!res.ok) {
72988
- const text = await res.text().catch(() => "");
72989
- throw new Error(`${res.status} ${res.statusText}: ${text}`);
72990
- }
72996
+ await assertOk(res);
72991
72997
  return res.json();
72992
72998
  }
72993
72999
  async function bootstrapPrivyAccount(token, base = loadConfig().billingBase) {
@@ -72997,10 +73003,7 @@ async function bootstrapPrivyAccount(token, base = loadConfig().billingBase) {
72997
73003
  "Content-Type": "application/json"
72998
73004
  }
72999
73005
  });
73000
- if (!res.ok) {
73001
- const text = await res.text().catch(() => "");
73002
- throw new Error(`${res.status} ${res.statusText}: ${text}`);
73003
- }
73006
+ await assertOk(res);
73004
73007
  return res.json();
73005
73008
  }
73006
73009
  async function createConnectedKeyTopupCheckout(input) {
@@ -73017,15 +73020,18 @@ async function createConnectedKeyTopupCheckout(input) {
73017
73020
  cancelUrl: `${siteBase}/?topup=cancelled`
73018
73021
  })
73019
73022
  });
73020
- if (!res.ok) {
73021
- const text = await res.text().catch(() => "");
73022
- throw new Error(`${res.status} ${res.statusText}: ${text}`);
73023
- }
73023
+ await assertOk(res);
73024
73024
  return res.json();
73025
73025
  }
73026
73026
  function trimBase(base) {
73027
73027
  return base.replace(/\/$/, "");
73028
73028
  }
73029
+ async function assertOk(res) {
73030
+ if (!res.ok) {
73031
+ const text = await res.text().catch(() => "");
73032
+ throw new Error(`${res.status} ${res.statusText}: ${text}`);
73033
+ }
73034
+ }
73029
73035
 
73030
73036
  // src/lib/tempo.ts
73031
73037
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
@@ -73321,7 +73327,6 @@ var VALID_KEYS = [
73321
73327
  "wallet",
73322
73328
  "apiKey",
73323
73329
  "privyToken",
73324
- "creatorToken",
73325
73330
  "userId",
73326
73331
  "twinId",
73327
73332
  "twinToken",
@@ -73475,7 +73480,6 @@ import { execFileSync as execFileSync4 } from "node:child_process";
73475
73480
  function buildVerifiedAuthConfig(token, bootstrap) {
73476
73481
  return {
73477
73482
  privyToken: token,
73478
- creatorToken: undefined,
73479
73483
  userId: bootstrap.userId,
73480
73484
  connectedKeyId: bootstrap.connectedKey?.id,
73481
73485
  connectedKeyName: bootstrap.connectedKey?.name,
@@ -73690,7 +73694,6 @@ Run "amiko login" again to re-authenticate, or "amiko logout" to sign out.
73690
73694
  }
73691
73695
  saveConfig({
73692
73696
  privyToken: undefined,
73693
- creatorToken: undefined,
73694
73697
  userId: undefined,
73695
73698
  connectedKeyId: undefined,
73696
73699
  connectedKeyName: undefined,
@@ -74166,7 +74169,8 @@ function registerBridgeCommand(program2) {
74166
74169
  params.originChainId = resolveChainId(opts.from);
74167
74170
  if (opts.to)
74168
74171
  params.destinationChainId = resolveChainId(opts.to);
74169
- const routes = await getAvailableRoutes(params);
74172
+ const routesData = await getAvailableRoutes(params);
74173
+ const routes = Array.isArray(routesData) ? routesData : [];
74170
74174
  const usdcRoutes = routes.filter((r19) => r19.originTokenSymbol === "USDC" || r19.destinationTokenSymbol === "USDC");
74171
74175
  spinner.stop();
74172
74176
  console.log(heading("USDC Bridge Routes"));
@@ -74407,6 +74411,9 @@ Tip: you can also generate a connect link from your Amiko settings`));
74407
74411
  });
74408
74412
  }
74409
74413
 
74414
+ // src/commands/credits.ts
74415
+ import readline from "node:readline";
74416
+
74410
74417
  // src/lib/mpp-topup.ts
74411
74418
  async function mppCreditsTopup(params) {
74412
74419
  const config2 = loadConfig();
@@ -74468,6 +74475,21 @@ async function mppCreditsTopup(params) {
74468
74475
  }
74469
74476
 
74470
74477
  // src/commands/credits.ts
74478
+ function promptYesNo(question) {
74479
+ if (!process.stdin.isTTY) {
74480
+ return Promise.resolve(false);
74481
+ }
74482
+ const rl = readline.createInterface({
74483
+ input: process.stdin,
74484
+ output: process.stdout
74485
+ });
74486
+ return new Promise((resolve2) => {
74487
+ rl.question(question, (answer) => {
74488
+ rl.close();
74489
+ resolve2(/^y(es)?$/i.test(answer.trim()));
74490
+ });
74491
+ });
74492
+ }
74471
74493
  function registerCreditsCommand(program2) {
74472
74494
  const credits = program2.command("credits").description("Manage Amiko credit balance and top-ups");
74473
74495
  credits.command("balance").description("Show credit balance and agent wallet balances").action(async () => {
@@ -74574,12 +74596,41 @@ function registerCreditsCommand(program2) {
74574
74596
  process.exit(1);
74575
74597
  }
74576
74598
  });
74577
- credits.command("topup <amount>").description("Top up credits by spending tokens from your agent wallet").option("--token <TOKEN>", "Token to use for top-up").option("--chain <CHAIN>", "Chain to use for top-up").option("--wallet <ADDR>", "Wallet address to use").option("--method <METHOD>", "Payment method: auto, crossmint, mpp", "auto").option("--yes", "Skip confirmation prompt").action(async (amountStr, options) => {
74578
- const creditsAmount = parseInt(amountStr.replace(/,/g, ""), 10);
74579
- if (isNaN(creditsAmount) || creditsAmount <= 0) {
74580
- console.error(error(`Invalid amount: ${amountStr}`));
74599
+ credits.command("topup [amount]").description("Top up credits by spending tokens from your agent wallet. <amount> is in credits (10,000 = $1.00). Use --usd <n> to specify dollars, or --spend <n> to specify a token amount (e.g. 1 SOL worth).").option("--token <TOKEN>", "Token to use for top-up").option("--chain <CHAIN>", "Chain to use for top-up").option("--wallet <ADDR>", "Wallet address to use").option("--method <METHOD>", "Payment method: auto, crossmint, mpp", "auto").option("--usd <amount>", "Amount in USD instead of credits (e.g. --usd 1 for $1 = 10,000 credits)").option("--spend <amount>", "Spend this many units of --token and convert to credits at current price (e.g. --spend 1 --token SOL for '1 SOL worth of credits')").option("--yes", "Skip the interactive Proceed? prompt (summary still prints)").action(async (amountStr, options) => {
74600
+ const inputs = [amountStr, options.usd, options.spend].filter((v8) => v8 != null && v8 !== "");
74601
+ if (inputs.length !== 1) {
74602
+ console.error(error("Specify exactly one of: <amount> (credits), --usd <dollars>, or --spend <token amount>."));
74581
74603
  process.exit(1);
74582
74604
  }
74605
+ let presetCredits = null;
74606
+ let presetUsd = null;
74607
+ let presetSpend = null;
74608
+ if (options.usd != null) {
74609
+ const usd2 = parseFloat(String(options.usd).replace(/,/g, ""));
74610
+ if (!isFinite(usd2) || usd2 <= 0) {
74611
+ console.error(error(`Invalid --usd value: ${options.usd}`));
74612
+ process.exit(1);
74613
+ }
74614
+ presetUsd = usd2;
74615
+ } else if (options.spend != null) {
74616
+ const spend = parseFloat(String(options.spend).replace(/,/g, ""));
74617
+ if (!isFinite(spend) || spend <= 0) {
74618
+ console.error(error(`Invalid --spend value: ${options.spend}`));
74619
+ process.exit(1);
74620
+ }
74621
+ if (!options.token) {
74622
+ console.error(error("--spend requires --token (e.g. --spend 1 --token SOL)"));
74623
+ process.exit(1);
74624
+ }
74625
+ presetSpend = spend;
74626
+ } else {
74627
+ const credits2 = parseInt(String(amountStr).replace(/,/g, ""), 10);
74628
+ if (isNaN(credits2) || credits2 <= 0) {
74629
+ console.error(error(`Invalid amount: ${amountStr}`));
74630
+ process.exit(1);
74631
+ }
74632
+ presetCredits = credits2;
74633
+ }
74583
74634
  const spinner = ora("Resolving auth...").start();
74584
74635
  try {
74585
74636
  const auth = resolveAuth(loadConfig());
@@ -74605,39 +74656,11 @@ function registerCreditsCommand(program2) {
74605
74656
  "Content-Type": "application/json"
74606
74657
  };
74607
74658
  spinner.stop();
74608
- if (options.method === "mpp" && auth.userId && auth.connectedKeyId) {
74609
- const spinner2 = ora("Topping up credits via MPP...").start();
74610
- const mppResult = await mppCreditsTopup({
74611
- userId: auth.userId,
74612
- keyId: auth.connectedKeyId,
74613
- amount: creditsAmount / 1e4
74614
- });
74615
- spinner2.stop();
74616
- if (!mppResult.success) {
74617
- console.error(error(mppResult.error || "MPP top-up failed"));
74618
- process.exit(1);
74619
- }
74620
- console.log(heading("Top-up Complete (MPP)"));
74621
- console.log("");
74622
- if (mppResult.credits?.balance)
74623
- console.log(label("Balance", mppResult.credits.balance));
74624
- console.log(success("Credits topped up via MPP."));
74625
- return;
74626
- }
74627
- const agentWallet = auth.agentWallets?.find((w5) => w5.source === "agent");
74628
- const walletAddr = options.wallet || agentWallet?.address || auth.agentWallets?.[0]?.address;
74629
- const walletChain = options.chain || auth.agentWallets?.find((w5) => w5.address === walletAddr)?.chain || "solana";
74630
74659
  const token = options.token || "AMIKO";
74631
- if (!walletAddr) {
74632
- console.error(error("No wallet found. Specify --wallet <address>"));
74633
- process.exit(1);
74634
- }
74635
- const usdAmount = creditsAmount / 1e4;
74636
- let tokenAmount;
74637
- let pricePerToken = null;
74638
74660
  const stablecoins = ["usdc", "usdt", "usdc.e", "usdt0"];
74639
- if (stablecoins.includes(token.toLowerCase())) {
74640
- tokenAmount = usdAmount;
74661
+ const isStable = stablecoins.includes(token.toLowerCase());
74662
+ let pricePerToken = null;
74663
+ if (isStable) {
74641
74664
  pricePerToken = 1;
74642
74665
  } else {
74643
74666
  const spinner2 = ora(`Fetching ${token} price...`).start();
@@ -74663,7 +74686,6 @@ function registerCreditsCommand(program2) {
74663
74686
  }
74664
74687
  const jupData = await jupRes.json();
74665
74688
  pricePerToken = parseFloat(jupData.outAmount) / 1e6;
74666
- tokenAmount = usdAmount / pricePerToken;
74667
74689
  spinner2.stop();
74668
74690
  } catch {
74669
74691
  spinner2.stop();
@@ -74671,6 +74693,62 @@ function registerCreditsCommand(program2) {
74671
74693
  process.exit(1);
74672
74694
  }
74673
74695
  }
74696
+ let creditsAmount;
74697
+ let usdAmount;
74698
+ let tokenAmount;
74699
+ if (presetSpend != null) {
74700
+ tokenAmount = presetSpend;
74701
+ usdAmount = tokenAmount * pricePerToken;
74702
+ creditsAmount = Math.round(usdAmount * 1e4);
74703
+ } else if (presetUsd != null) {
74704
+ usdAmount = presetUsd;
74705
+ creditsAmount = Math.round(usdAmount * 1e4);
74706
+ tokenAmount = usdAmount / pricePerToken;
74707
+ } else {
74708
+ creditsAmount = presetCredits;
74709
+ usdAmount = creditsAmount / 1e4;
74710
+ tokenAmount = usdAmount / pricePerToken;
74711
+ }
74712
+ if (options.method === "mpp" && auth.userId && auth.connectedKeyId) {
74713
+ if (!options.yes) {
74714
+ console.log(heading("Top-up Summary (MPP)"));
74715
+ console.log("");
74716
+ console.log(label("Credits", creditsAmount.toLocaleString()));
74717
+ console.log(label("USD", `$${usdAmount.toFixed(2)}`));
74718
+ console.log(dim("Charged via MPP connected key."));
74719
+ console.log("");
74720
+ const ok = await promptYesNo("Proceed? [y/N] ");
74721
+ if (!ok) {
74722
+ console.log(dim("Top-up cancelled."));
74723
+ process.exit(0);
74724
+ }
74725
+ console.log("");
74726
+ }
74727
+ const spinner2 = ora("Topping up credits via MPP...").start();
74728
+ const mppResult = await mppCreditsTopup({
74729
+ userId: auth.userId,
74730
+ keyId: auth.connectedKeyId,
74731
+ amount: usdAmount
74732
+ });
74733
+ spinner2.stop();
74734
+ if (!mppResult.success) {
74735
+ console.error(error(mppResult.error || "MPP top-up failed"));
74736
+ process.exit(1);
74737
+ }
74738
+ console.log(heading("Top-up Complete (MPP)"));
74739
+ console.log("");
74740
+ if (mppResult.credits?.balance)
74741
+ console.log(label("Balance", String(mppResult.credits.balance)));
74742
+ console.log(success("Credits topped up via MPP."));
74743
+ return;
74744
+ }
74745
+ const agentWallet = auth.agentWallets?.find((w5) => w5.source === "agent");
74746
+ const walletAddr = options.wallet || agentWallet?.address || auth.agentWallets?.[0]?.address;
74747
+ const walletChain = options.chain || auth.agentWallets?.find((w5) => w5.address === walletAddr)?.chain || "solana";
74748
+ if (!walletAddr) {
74749
+ console.error(error("No wallet found. Specify --wallet <address>"));
74750
+ process.exit(1);
74751
+ }
74674
74752
  const shortWallet = walletAddr.length > 12 ? `${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}` : walletAddr;
74675
74753
  console.log(heading("Top-up Summary"));
74676
74754
  console.log("");
@@ -74682,6 +74760,14 @@ function registerCreditsCommand(program2) {
74682
74760
  console.log(label("From", `wallet ${shortWallet} (${walletChain})`));
74683
74761
  console.log(dim("Tokens will be transferred from your agent wallet to the treasury."));
74684
74762
  console.log("");
74763
+ if (!options.yes) {
74764
+ const confirmed = await promptYesNo("Proceed? [y/N] ");
74765
+ if (!confirmed) {
74766
+ console.log(dim("Top-up cancelled."));
74767
+ process.exit(0);
74768
+ }
74769
+ console.log("");
74770
+ }
74685
74771
  if (["base", "ethereum", "tempo"].includes(walletChain)) {
74686
74772
  console.error(error("EVM topup not supported yet. Use a Solana wallet with --token AMIKO, USDC, USDT, or SOL."));
74687
74773
  process.exit(1);
@@ -74922,8 +75008,8 @@ function registerSwapCommand(program2) {
74922
75008
  console.log("");
74923
75009
  const isInteractive2 = process.stdin.isTTY && !opts.yes;
74924
75010
  if (isInteractive2) {
74925
- const readline = await import("readline");
74926
- const rl = readline.createInterface({
75011
+ const readline2 = await import("readline");
75012
+ const rl = readline2.createInterface({
74927
75013
  input: process.stdin,
74928
75014
  output: process.stdout
74929
75015
  });
@@ -75005,6 +75091,8 @@ function registerSwapCommand(program2) {
75005
75091
 
75006
75092
  // src/commands/update.ts
75007
75093
  import { execFileSync as execFileSync6 } from "node:child_process";
75094
+ import fs from "node:fs";
75095
+ import path from "node:path";
75008
75096
  function detectPackageManager() {
75009
75097
  try {
75010
75098
  execFileSync6("bun", ["--version"], { stdio: "ignore" });
@@ -75013,6 +75101,43 @@ function detectPackageManager() {
75013
75101
  return "npm";
75014
75102
  }
75015
75103
  }
75104
+ var DATA_CLI = "/data/.amiko-cli";
75105
+ var NPM_GLOBAL_CANDIDATES = [
75106
+ "/usr/local/lib/node_modules/@heyamiko/amiko-cli",
75107
+ "/usr/lib/node_modules/@heyamiko/amiko-cli"
75108
+ ];
75109
+ var BIN_TARGET = "/usr/local/bin/amiko";
75110
+ var TWIN_SKILL_DIRS = [
75111
+ "/data/.openclaw/skills/amiko-cli",
75112
+ "/data/.openclaw/workspace/skills/amiko-cli"
75113
+ ];
75114
+ function syncTwinInstall() {
75115
+ if (!fs.existsSync(DATA_CLI))
75116
+ return false;
75117
+ const src = NPM_GLOBAL_CANDIDATES.find((p13) => fs.existsSync(p13));
75118
+ if (!src)
75119
+ return false;
75120
+ fs.rmSync(DATA_CLI, { recursive: true, force: true });
75121
+ execFileSync6("cp", ["-rL", src, DATA_CLI]);
75122
+ try {
75123
+ fs.unlinkSync(BIN_TARGET);
75124
+ } catch {}
75125
+ fs.symlinkSync(path.join(DATA_CLI, "dist/index.js"), BIN_TARGET);
75126
+ try {
75127
+ fs.chmodSync(BIN_TARGET, 493);
75128
+ } catch {}
75129
+ const skillSrc = path.join(DATA_CLI, "skills/SKILL.md");
75130
+ if (fs.existsSync(skillSrc)) {
75131
+ for (const dir of TWIN_SKILL_DIRS) {
75132
+ const parent = path.dirname(dir);
75133
+ if (!fs.existsSync(parent))
75134
+ continue;
75135
+ fs.mkdirSync(dir, { recursive: true });
75136
+ fs.copyFileSync(skillSrc, path.join(dir, "SKILL.md"));
75137
+ }
75138
+ }
75139
+ return true;
75140
+ }
75016
75141
  function registerUpdateCommand(program2) {
75017
75142
  program2.command("update").description("Update amiko-cli to the latest version").action(async () => {
75018
75143
  const pm = detectPackageManager();
@@ -75034,6 +75159,17 @@ function registerUpdateCommand(program2) {
75034
75159
  });
75035
75160
  }
75036
75161
  spinner.stop();
75162
+ let syncedTwin = false;
75163
+ try {
75164
+ const twinSpinner = ora("Refreshing twin install...").start();
75165
+ syncedTwin = syncTwinInstall();
75166
+ twinSpinner.stop();
75167
+ if (syncedTwin) {
75168
+ console.log(dim("Refreshed /data/.amiko-cli, symlink, and skill files."));
75169
+ }
75170
+ } catch (err) {
75171
+ console.log(dim(`Twin sync failed: ${err instanceof Error ? err.message : String(err)}. Container restart will apply the update.`));
75172
+ }
75037
75173
  let newVersion = "latest";
75038
75174
  try {
75039
75175
  newVersion = execFileSync6("amiko", ["--version"], {
@@ -75056,10 +75192,123 @@ function registerUpdateCommand(program2) {
75056
75192
  });
75057
75193
  }
75058
75194
 
75195
+ // src/lib/balance.ts
75196
+ async function fetchCreditsBalance(auth) {
75197
+ const billingToken = auth.platformKey || auth.twinToken || auth.privyToken;
75198
+ if (!billingToken)
75199
+ return null;
75200
+ const base2 = (auth.billingBase || "https://billing.heyamiko.com").replace(/\/$/, "");
75201
+ try {
75202
+ const res = await fetch(`${base2}/api/credits`, {
75203
+ headers: { Authorization: `Bearer ${billingToken}` }
75204
+ });
75205
+ if (!res.ok)
75206
+ return null;
75207
+ const data = await res.json();
75208
+ const raw = data.displayBalance ?? Number(data.credits?.balance ?? 0);
75209
+ return typeof raw === "number" && isFinite(raw) ? Math.round(raw) : null;
75210
+ } catch {
75211
+ return null;
75212
+ }
75213
+ }
75214
+ async function printBalanceRemaining(auth) {
75215
+ const balance = await fetchCreditsBalance(auth);
75216
+ if (balance == null) {
75217
+ console.log(dim("Balance: (could not fetch)"));
75218
+ return;
75219
+ }
75220
+ const usd2 = (balance / 1e4).toFixed(2);
75221
+ console.log(label("Balance", `${balance.toLocaleString()} credits (~$${usd2})`));
75222
+ }
75223
+
75224
+ // src/lib/errors.ts
75225
+ async function reportHttpError(res, label2) {
75226
+ const text = await res.text().catch(() => "");
75227
+ console.error(error(`${label2} failed: ${res.status} ${res.statusText || ""}`.trim()));
75228
+ if (!text)
75229
+ return;
75230
+ let pretty = text;
75231
+ try {
75232
+ const parsed = JSON.parse(text);
75233
+ pretty = parsed?.error ?? parsed?.message ?? parsed?.detail ?? JSON.stringify(parsed, null, 2);
75234
+ } catch {}
75235
+ console.error(dim("Response body:"));
75236
+ console.error(String(pretty).slice(0, 2000));
75237
+ }
75238
+
75239
+ // src/lib/confirm.ts
75240
+ import readline2 from "node:readline";
75241
+ function promptYesNo2(question) {
75242
+ const rl = readline2.createInterface({
75243
+ input: process.stdin,
75244
+ output: process.stdout
75245
+ });
75246
+ return new Promise((resolve2) => {
75247
+ rl.question(question, (answer) => {
75248
+ rl.close();
75249
+ resolve2(/^y(es)?$/i.test(answer.trim()));
75250
+ });
75251
+ });
75252
+ }
75253
+ async function confirmDestructive(args) {
75254
+ if (args.yes)
75255
+ return;
75256
+ if (process.stdin.isTTY) {
75257
+ console.log("");
75258
+ console.log(warn(args.action));
75259
+ if (args.detail)
75260
+ console.log(dim(args.detail));
75261
+ const ok = await promptYesNo2("Proceed? [y/N] ");
75262
+ if (!ok) {
75263
+ console.log(dim("Cancelled."));
75264
+ process.exit(0);
75265
+ }
75266
+ return;
75267
+ }
75268
+ console.error(error(`Refusing to run without --yes (non-interactive shell).`));
75269
+ console.error("");
75270
+ console.error(`Action: ${args.action}`);
75271
+ if (args.detail)
75272
+ console.error(dim(args.detail));
75273
+ console.error("");
75274
+ console.error(dim("Re-run with --yes to confirm:"));
75275
+ console.error(dim(` ${args.commandExample} --yes`));
75276
+ process.exit(2);
75277
+ }
75278
+ async function requireApproval(args) {
75279
+ if (args.yes)
75280
+ return;
75281
+ if (process.stdin.isTTY) {
75282
+ console.log("");
75283
+ console.log(`About to run: ${args.summary}`);
75284
+ console.log(`Cost: ${args.cost}`);
75285
+ const ok = await promptYesNo2("Proceed? [y/N] ");
75286
+ if (!ok) {
75287
+ console.log(dim("Cancelled."));
75288
+ process.exit(0);
75289
+ }
75290
+ return;
75291
+ }
75292
+ console.error(error(`Refusing to run without --yes (non-interactive shell detected).`));
75293
+ console.error("");
75294
+ console.error(`This command will cost ${args.cost}.`);
75295
+ console.error(`Action: ${args.summary}`);
75296
+ console.error("");
75297
+ console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
75298
+ console.error(dim(` ${args.commandExample} --yes`));
75299
+ process.exit(2);
75300
+ }
75301
+
75059
75302
  // src/commands/search.ts
75060
75303
  var TREASURY = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
75061
75304
  function registerSearchCommand(program2) {
75062
- program2.command("search <query>").description("X Search — pays 1 AMIKO token per search").option("--raw", "Output raw JSON").option("--token <token>", "Payment token (AMIKO, USDC, SOL)", "AMIKO").action(async (query, opts) => {
75305
+ program2.command("search <query>").description("X Search — pays 1 AMIKO token per search").option("--raw", "Output raw JSON").option("--token <token>", "Payment token (AMIKO, USDC, SOL)", "AMIKO").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
75306
+ await requireApproval({
75307
+ cost: `1 ${opts.token || "AMIKO"}`,
75308
+ summary: `X/Twitter search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
75309
+ yes: opts.yes,
75310
+ commandExample: `amiko search ${JSON.stringify(query)}${opts.token && opts.token !== "AMIKO" ? ` --token ${opts.token}` : ""}`
75311
+ });
75063
75312
  const spinner = ora("X Search...").start();
75064
75313
  try {
75065
75314
  const config2 = loadConfig();
@@ -75125,8 +75374,7 @@ function registerSearchCommand(program2) {
75125
75374
  });
75126
75375
  spinner.stop();
75127
75376
  if (!res.ok) {
75128
- const text = await res.text().catch(() => "");
75129
- console.error(error(`Search failed: ${res.status} ${text.slice(0, 200)}`));
75377
+ await reportHttpError(res, "Search");
75130
75378
  const explorer = chain === "solana" ? `https://solscan.io/tx/${txHash}` : `https://explorer.tempo.xyz/tx/${txHash}`;
75131
75379
  console.log(dim(`Payment tx: ${explorer}`));
75132
75380
  process.exit(1);
@@ -75155,6 +75403,7 @@ function registerSearchCommand(program2) {
75155
75403
  }
75156
75404
  }
75157
75405
  console.log(success(`Paid ${amount2} ${token}`) + dim(` tx: ${txHash.slice(0, 16)}...`));
75406
+ await printBalanceRemaining(auth);
75158
75407
  } catch (err) {
75159
75408
  spinner.stop();
75160
75409
  console.error(error(err instanceof Error ? err.message : "Search failed"));
@@ -75166,8 +75415,41 @@ function registerSearchCommand(program2) {
75166
75415
  // src/commands/image.ts
75167
75416
  var TREASURY2 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
75168
75417
  function registerImageCommand(program2) {
75169
- program2.command("image <prompt>").alias("img").description("Generate an image — costs 5 AMIKO").option("--size <size>", "Image size: 1024x1024, 1024x1792, 1792x1024", "1024x1024").option("--quality <q>", "Quality: auto, low, medium, high (gpt-image-1) or standard, hd (dall-e-3)", "auto").option("--model <model>", "Model: gpt-image-1 or dall-e-3", "gpt-image-1").option("--background <bg>", "Background: transparent, opaque, auto (gpt-image-1 only)").option("--format <fmt>", "Output: png, webp, jpeg (gpt-image-1 only)", "png").option("--raw", "Output raw JSON").action(async (prompt2, opts) => {
75170
- const spinner = ora("Generating image...").start();
75418
+ program2.command("image <prompt>").alias("img").description("Generate an image — costs 5 AMIKO").option("--size <size>", "Image size: 1024x1024, 1024x1792, 1792x1024", "1024x1024").option("--quality <q>", "Quality: auto, low, medium, high (gpt-image-1) or standard, hd (dall-e-3)", "auto").option("--model <model>", "Model: gpt-image-1 or dall-e-3", "gpt-image-1").option("--background <bg>", "Background: transparent, opaque, auto (gpt-image-1 only)").option("--format <fmt>", "Output: png, webp, jpeg (gpt-image-1 only)", "png").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt2, opts) => {
75419
+ await requireApproval({
75420
+ cost: "5 AMIKO",
75421
+ summary: `Generate image: ${prompt2.slice(0, 80)}${prompt2.length > 80 ? "…" : ""}`,
75422
+ yes: opts.yes,
75423
+ commandExample: `amiko image ${JSON.stringify(prompt2)}`
75424
+ });
75425
+ const isTTY = process.stderr.isTTY;
75426
+ const spinner = isTTY ? ora("Generating image...").start() : null;
75427
+ let progressTimer = null;
75428
+ const started = Date.now();
75429
+ const setStatus = (text) => {
75430
+ if (spinner) {
75431
+ spinner.text = text;
75432
+ } else {
75433
+ console.error(text);
75434
+ }
75435
+ };
75436
+ const startProgress = (text) => {
75437
+ setStatus(text);
75438
+ if (!spinner && !progressTimer) {
75439
+ progressTimer = setInterval(() => {
75440
+ const elapsed = Math.round((Date.now() - started) / 1000);
75441
+ console.error(` …still working (${elapsed}s elapsed)`);
75442
+ }, 3000);
75443
+ }
75444
+ };
75445
+ const stopProgress = () => {
75446
+ if (progressTimer) {
75447
+ clearInterval(progressTimer);
75448
+ progressTimer = null;
75449
+ }
75450
+ if (spinner)
75451
+ spinner.stop();
75452
+ };
75171
75453
  try {
75172
75454
  const config2 = loadConfig();
75173
75455
  const auth = resolveAuth(config2);
@@ -75175,18 +75457,18 @@ function registerImageCommand(program2) {
75175
75457
  const billingBase = auth.billingBase || "https://billing.heyamiko.com";
75176
75458
  const billingToken = auth.platformKey || auth.twinToken || auth.privyToken;
75177
75459
  if (!billingToken) {
75178
- spinner.stop();
75460
+ stopProgress();
75179
75461
  console.error(error("Not authenticated. Check .amiko.json credentials."));
75180
75462
  process.exit(1);
75181
75463
  }
75182
75464
  const allWallets = auth.agentWallets || config2.agentWallets || [];
75183
75465
  const solanaWallet = allWallets.find((w5) => w5.chain === "solana")?.address;
75184
75466
  if (!solanaWallet) {
75185
- spinner.stop();
75467
+ stopProgress();
75186
75468
  console.error(error("No Solana wallet for AMIKO payment."));
75187
75469
  process.exit(1);
75188
75470
  }
75189
- spinner.text = "Paying 5 AMIKO...";
75471
+ setStatus("Paying 5 AMIKO...");
75190
75472
  const transferRes = await fetch(`${billingBase}/api/crossmint/wallets/${encodeURIComponent(solanaWallet)}/transfer`, {
75191
75473
  method: "POST",
75192
75474
  headers: {
@@ -75201,7 +75483,7 @@ function registerImageCommand(program2) {
75201
75483
  })
75202
75484
  });
75203
75485
  if (!transferRes.ok) {
75204
- spinner.stop();
75486
+ stopProgress();
75205
75487
  const err = await transferRes.json().catch(() => ({ error: "Transfer failed" }));
75206
75488
  console.error(error(err.error || `Payment failed: ${transferRes.status}`));
75207
75489
  process.exit(1);
@@ -75209,11 +75491,11 @@ function registerImageCommand(program2) {
75209
75491
  const transferData = await transferRes.json();
75210
75492
  const txHash = transferData.txHash;
75211
75493
  if (!txHash) {
75212
- spinner.stop();
75494
+ stopProgress();
75213
75495
  console.error(error("Payment sent but no tx hash returned."));
75214
75496
  process.exit(1);
75215
75497
  }
75216
- spinner.text = "Generating image...";
75498
+ startProgress("Generating image...");
75217
75499
  const res = await fetch(`${mppBase2}/v1/ai/openai/images`, {
75218
75500
  method: "POST",
75219
75501
  headers: {
@@ -75229,10 +75511,9 @@ function registerImageCommand(program2) {
75229
75511
  ...opts.model === "gpt-image-1" && { output_format: opts.format || "png" }
75230
75512
  })
75231
75513
  });
75232
- spinner.stop();
75514
+ stopProgress();
75233
75515
  if (!res.ok) {
75234
- const text = await res.text().catch(() => "");
75235
- console.error(error(`Image generation failed: ${res.status} ${text.slice(0, 300)}`));
75516
+ await reportHttpError(res, "Image generation");
75236
75517
  console.log(dim(`Payment tx: https://solscan.io/tx/${txHash}`));
75237
75518
  process.exit(1);
75238
75519
  }
@@ -75261,8 +75542,9 @@ function registerImageCommand(program2) {
75261
75542
  }
75262
75543
  console.log("");
75263
75544
  console.log(success("Paid 5 AMIKO") + dim(` tx: ${txHash.slice(0, 16)}...`));
75545
+ await printBalanceRemaining(auth);
75264
75546
  } catch (err) {
75265
- spinner.stop();
75547
+ stopProgress();
75266
75548
  console.error(error(err instanceof Error ? err.message : "Image generation failed"));
75267
75549
  process.exit(1);
75268
75550
  }
@@ -75291,7 +75573,13 @@ async function payAmiko(billingBase, billingToken, solanaWallet, amount2) {
75291
75573
  }
75292
75574
  function registerAmazonCommand(program2) {
75293
75575
  const cmd = program2.command("amazon").description("Search and buy products on Amazon — paid with AMIKO");
75294
- cmd.command("search <query>").description("Search Amazon products — 1 AMIKO").option("--limit <n>", "Number of results", "5").option("--raw", "Output raw JSON").action(async (query, opts) => {
75576
+ cmd.command("search <query>").description("Search Amazon products — 1 AMIKO").option("--limit <n>", "Number of results", "5").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
75577
+ await requireApproval({
75578
+ cost: "1 AMIKO",
75579
+ summary: `Amazon product search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
75580
+ yes: opts.yes,
75581
+ commandExample: `amiko amazon search ${JSON.stringify(query)}`
75582
+ });
75295
75583
  const spinner = ora("Searching Amazon...").start();
75296
75584
  try {
75297
75585
  const config2 = loadConfig();
@@ -75322,8 +75610,7 @@ function registerAmazonCommand(program2) {
75322
75610
  });
75323
75611
  spinner.stop();
75324
75612
  if (!res.ok) {
75325
- const text = await res.text().catch(() => "");
75326
- console.error(error(`Search failed: ${res.status} ${text.slice(0, 200)}`));
75613
+ await reportHttpError(res, "Amazon search");
75327
75614
  process.exit(1);
75328
75615
  }
75329
75616
  const data = await res.json();
@@ -75353,6 +75640,7 @@ function registerAmazonCommand(program2) {
75353
75640
  console.log(dim(" No results found."));
75354
75641
  }
75355
75642
  console.log(success("Paid 1 AMIKO") + dim(` tx: ${txHash.slice(0, 16)}...`));
75643
+ await printBalanceRemaining(auth);
75356
75644
  } catch (err) {
75357
75645
  spinner.stop();
75358
75646
  console.error(error(err instanceof Error ? err.message : "Search failed"));
@@ -75385,8 +75673,7 @@ function registerAmazonCommand(program2) {
75385
75673
  });
75386
75674
  spinner.stop();
75387
75675
  if (!res.ok) {
75388
- const text = await res.text().catch(() => "");
75389
- console.error(error(`Quote failed: ${res.status} ${text.slice(0, 300)}`));
75676
+ await reportHttpError(res, "Amazon quote");
75390
75677
  process.exit(1);
75391
75678
  }
75392
75679
  const data = await res.json();
@@ -75433,7 +75720,7 @@ function usdToAmiko(usd2, pricePerAmiko) {
75433
75720
  const amount2 = usd2 / pricePerAmiko;
75434
75721
  return Math.ceil(amount2 * 100) / 100 + "";
75435
75722
  }
75436
- async function payAndCall(mppBase2, method, path, billingBase, billingToken, solanaWallet, amountOrAuto, body, onProgress, token = "AMIKO") {
75723
+ async function payAndCall(mppBase2, method, path2, billingBase, billingToken, solanaWallet, amountOrAuto, body, onProgress, token = "AMIKO") {
75437
75724
  let amount2 = amountOrAuto;
75438
75725
  if (amount2 === "auto") {
75439
75726
  onProgress?.("Fetching price...");
@@ -75441,7 +75728,7 @@ async function payAndCall(mppBase2, method, path, billingBase, billingToken, sol
75441
75728
  const specRes = await fetch(`${mppBase2}/openapi.json`);
75442
75729
  if (specRes.ok) {
75443
75730
  const spec = await specRes.json();
75444
- const pathSpec = spec.paths?.[path];
75731
+ const pathSpec = spec.paths?.[path2];
75445
75732
  const methodSpec = pathSpec?.[method.toLowerCase()];
75446
75733
  const usdPrice = parseFloat(methodSpec?.["x-payment-info"]?.price || "0");
75447
75734
  if (usdPrice > 0) {
@@ -75488,13 +75775,19 @@ async function payAndCall(mppBase2, method, path, billingBase, billingToken, sol
75488
75775
  if (body && method !== "GET") {
75489
75776
  fetchOpts.body = JSON.stringify(body);
75490
75777
  }
75491
- const url2 = path.startsWith("http") ? path : `${mppBase2}${path}`;
75778
+ const url2 = path2.startsWith("http") ? path2 : `${mppBase2}${path2}`;
75492
75779
  const res = await fetch(url2, fetchOpts);
75493
75780
  return { res, txHash, amikoSpent: amount2 };
75494
75781
  }
75495
75782
  function registerServicesCommand(program2) {
75496
75783
  const svc = program2.command("service").alias("svc").description("Call any MPP service — pays with AMIKO tokens");
75497
- svc.command("tts <voiceId> <text>").description("Text-to-speech — 1 AMIKO").option("--raw", "Output raw response").action(async (voiceId, text, opts) => {
75784
+ svc.command("tts <voiceId> <text>").description("Text-to-speech — 1 AMIKO").option("--raw", "Output raw response").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (voiceId, text, opts) => {
75785
+ await requireApproval({
75786
+ cost: "1 AMIKO",
75787
+ summary: `Text-to-speech (voice ${voiceId}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
75788
+ yes: opts.yes,
75789
+ commandExample: `amiko service tts ${voiceId} ${JSON.stringify(text)}`
75790
+ });
75498
75791
  await callService({
75499
75792
  name: "Text-to-Speech",
75500
75793
  method: "POST",
@@ -75504,7 +75797,13 @@ function registerServicesCommand(program2) {
75504
75797
  opts
75505
75798
  });
75506
75799
  });
75507
- svc.command("stt <audioUrl>").description("Speech-to-text — 1 AMIKO").option("--raw", "Output raw response").action(async (audioUrl, opts) => {
75800
+ svc.command("stt <audioUrl>").description("Speech-to-text — 1 AMIKO").option("--raw", "Output raw response").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (audioUrl, opts) => {
75801
+ await requireApproval({
75802
+ cost: "1 AMIKO",
75803
+ summary: `Speech-to-text on ${audioUrl}`,
75804
+ yes: opts.yes,
75805
+ commandExample: `amiko service stt ${JSON.stringify(audioUrl)}`
75806
+ });
75508
75807
  await callService({
75509
75808
  name: "Speech-to-Text",
75510
75809
  method: "POST",
@@ -75532,7 +75831,13 @@ function registerServicesCommand(program2) {
75532
75831
  console.log(JSON.stringify(data, null, 2));
75533
75832
  }
75534
75833
  });
75535
- svc.command("chat <message>").description("AI chat — 1 AMIKO").option("--model <model>", "Model to use").option("--raw", "Output raw response").action(async (message, opts) => {
75834
+ svc.command("chat <message>").description("AI chat — 1 AMIKO").option("--model <model>", "Model to use").option("--raw", "Output raw response").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (message, opts) => {
75835
+ await requireApproval({
75836
+ cost: "1 AMIKO",
75837
+ summary: `AI chat${opts.model ? ` (model ${opts.model})` : ""}: "${message.slice(0, 60)}${message.length > 60 ? "…" : ""}"`,
75838
+ yes: opts.yes,
75839
+ commandExample: `amiko service chat ${JSON.stringify(message)}${opts.model ? ` --model ${opts.model}` : ""}`
75840
+ });
75536
75841
  const body = { message };
75537
75842
  if (opts.model)
75538
75843
  body.model = opts.model;
@@ -75545,7 +75850,14 @@ function registerServicesCommand(program2) {
75545
75850
  opts
75546
75851
  });
75547
75852
  });
75548
- svc.command("call <method> <path> [body]").description("Call any MPP endpoint — pays in AMIKO or USDC on Solana").option("--amiko <amount>", "AMIKO tokens to pay (default: auto-price from USD)").option("--usdc <amount>", "Pay in USDC instead of AMIKO").option("--raw", "Output raw response").action(async (method, path, bodyStr, opts) => {
75853
+ svc.command("call <method> <path> [body]").description("Call any MPP endpoint — pays in AMIKO or USDC on Solana").option("--amiko <amount>", "AMIKO tokens to pay (default: auto-price from USD)").option("--usdc <amount>", "Pay in USDC instead of AMIKO").option("--raw", "Output raw response").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (method, path2, bodyStr, opts) => {
75854
+ const costLabel = opts.usdc ? `${opts.usdc} USDC` : opts.amiko ? `${opts.amiko} AMIKO` : "auto-priced AMIKO (check /v1/ list for endpoint price first)";
75855
+ await requireApproval({
75856
+ cost: costLabel,
75857
+ summary: `${method.toUpperCase()} ${path2}`,
75858
+ yes: opts.yes,
75859
+ commandExample: `amiko service call ${method.toUpperCase()} ${path2}${bodyStr ? ` ${JSON.stringify(bodyStr)}` : ""}${opts.amiko ? ` --amiko ${opts.amiko}` : ""}${opts.usdc ? ` --usdc ${opts.usdc}` : ""}`
75860
+ });
75549
75861
  let body;
75550
75862
  if (bodyStr) {
75551
75863
  try {
@@ -75555,9 +75867,9 @@ function registerServicesCommand(program2) {
75555
75867
  }
75556
75868
  }
75557
75869
  await callService({
75558
- name: `${method.toUpperCase()} ${path}`,
75870
+ name: `${method.toUpperCase()} ${path2}`,
75559
75871
  method: method.toUpperCase(),
75560
- path,
75872
+ path: path2,
75561
75873
  body,
75562
75874
  amiko: opts.amiko || (opts.usdc ? undefined : "auto"),
75563
75875
  usdc: opts.usdc,
@@ -75575,11 +75887,11 @@ function registerServicesCommand(program2) {
75575
75887
  console.log(heading(`MPP Services
75576
75888
  `));
75577
75889
  const paths = spec.paths || {};
75578
- for (const [path, methods] of Object.entries(paths)) {
75890
+ for (const [path2, methods] of Object.entries(paths)) {
75579
75891
  for (const [method, op] of Object.entries(methods)) {
75580
75892
  const price = op["x-payment-info"]?.price;
75581
75893
  const priceLabel = price === "0" || !price ? source_default.green("FREE") : source_default.yellow(`$${parseFloat(price).toFixed(2)}`);
75582
- console.log(` ${source_default.bold(method.toUpperCase().padEnd(6))} ${path.padEnd(40)} ${priceLabel}`);
75894
+ console.log(` ${source_default.bold(method.toUpperCase().padEnd(6))} ${path2.padEnd(40)} ${priceLabel}`);
75583
75895
  if (op.summary)
75584
75896
  console.log(dim(` ${op.summary}`));
75585
75897
  }
@@ -75615,11 +75927,9 @@ async function callService(params) {
75615
75927
  const { res, txHash, amikoSpent } = await payAndCall(mppBase2, params.method, params.path, billingBase, billingToken, solanaWallet, token === "USDC" ? amount2 : amount2, params.body, (msg) => {
75616
75928
  spinner.text = msg;
75617
75929
  });
75618
- if (params.usdc) {}
75619
75930
  spinner.stop();
75620
75931
  if (!res.ok) {
75621
- const text = await res.text().catch(() => "");
75622
- console.error(error(`${params.name} failed: ${res.status} ${text.slice(0, 300)}`));
75932
+ await reportHttpError(res, params.name);
75623
75933
  console.log(dim(`Payment tx: https://solscan.io/tx/${txHash}`));
75624
75934
  process.exit(1);
75625
75935
  }
@@ -75641,6 +75951,7 @@ async function callService(params) {
75641
75951
  }
75642
75952
  console.log("");
75643
75953
  console.log(success(`Paid ${amikoSpent} ${token}`) + dim(` tx: ${txHash.slice(0, 16)}...`));
75954
+ await printBalanceRemaining(auth);
75644
75955
  } catch (err) {
75645
75956
  spinner.stop();
75646
75957
  console.error(error(err instanceof Error ? err.message : "Service call failed"));
@@ -75648,6 +75959,904 @@ async function callService(params) {
75648
75959
  }
75649
75960
  }
75650
75961
 
75962
+ // src/lib/amiko-web.ts
75963
+ var DEFAULT_PLATFORM_URL = "https://platform.heyamiko.com";
75964
+ function baseUrl(auth) {
75965
+ const url2 = auth.platformUrl || DEFAULT_PLATFORM_URL;
75966
+ return url2.replace(/\/$/, "");
75967
+ }
75968
+ function authHeader(auth) {
75969
+ if (auth.privyToken)
75970
+ return `Bearer am-${auth.privyToken}`;
75971
+ if (auth.twinToken)
75972
+ return `Bearer ${auth.twinToken}`;
75973
+ throw new Error("No amiko-web auth available. Run `amiko login` to link your account.");
75974
+ }
75975
+ async function amikoWebFetch(auth, path2, opts = {}) {
75976
+ const url2 = new URL(`${baseUrl(auth)}${path2}`);
75977
+ if (opts.query) {
75978
+ for (const [k6, v8] of Object.entries(opts.query)) {
75979
+ if (v8 !== undefined && v8 !== null)
75980
+ url2.searchParams.set(k6, String(v8));
75981
+ }
75982
+ }
75983
+ const headers = {
75984
+ Authorization: authHeader(auth),
75985
+ ...opts.headers
75986
+ };
75987
+ let body;
75988
+ if (opts.multipart) {
75989
+ body = opts.multipart;
75990
+ } else if (opts.rawBody) {
75991
+ body = opts.rawBody;
75992
+ if (opts.rawContentType)
75993
+ headers["Content-Type"] = opts.rawContentType;
75994
+ } else if (opts.body) {
75995
+ body = JSON.stringify(opts.body);
75996
+ headers["Content-Type"] = "application/json";
75997
+ }
75998
+ const controller = new AbortController;
75999
+ const timeoutMs = opts.timeoutMs ?? 30000;
76000
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
76001
+ let res;
76002
+ try {
76003
+ res = await fetch(url2, {
76004
+ method: opts.method ?? "GET",
76005
+ headers,
76006
+ body,
76007
+ signal: controller.signal
76008
+ });
76009
+ } finally {
76010
+ clearTimeout(timer);
76011
+ }
76012
+ if (!res.ok) {
76013
+ const text = await res.text().catch(() => "");
76014
+ const err = new Error(formatError2(res.status, text, path2));
76015
+ err.status = res.status;
76016
+ err.body = text;
76017
+ throw err;
76018
+ }
76019
+ const ctype = res.headers.get("content-type") || "";
76020
+ if (ctype.includes("application/json")) {
76021
+ return await res.json();
76022
+ }
76023
+ return await res.text();
76024
+ }
76025
+ function formatError2(status, body, path2) {
76026
+ const prefix = `${status} ${path2}`;
76027
+ if (status === 401) {
76028
+ return `${prefix}: unauthorized. Run \`amiko connect\` to refresh your session.`;
76029
+ }
76030
+ if (status === 403) {
76031
+ return `${prefix}: forbidden. The twin token does not have access to this resource.`;
76032
+ }
76033
+ if (!body)
76034
+ return prefix;
76035
+ const snippet = body.length > 500 ? `${body.slice(0, 500)}...` : body;
76036
+ return `${prefix}: ${snippet}`;
76037
+ }
76038
+
76039
+ // src/lib/twin-context.ts
76040
+ class TwinResolutionError extends Error {
76041
+ constructor(message) {
76042
+ super(message);
76043
+ }
76044
+ }
76045
+ function resolveTwinId(args) {
76046
+ const { flag, config: config2, auth } = args;
76047
+ if (flag)
76048
+ return flag;
76049
+ if (auth.twinId)
76050
+ return auth.twinId;
76051
+ if (config2.twinId)
76052
+ return config2.twinId;
76053
+ throw new TwinResolutionError("No twin configured. Pass --twin <id> or provision one via .amiko.json.");
76054
+ }
76055
+
76056
+ // src/commands/accounts.ts
76057
+ function registerAccountsCommand(program2) {
76058
+ program2.command("accounts").description("Show resolved identity (platform, user, twin, wallets)").option("--json", "Output as JSON").action(async (opts) => {
76059
+ const config2 = loadConfig();
76060
+ const auth = resolveAuth(config2);
76061
+ const data = {
76062
+ mode: auth.mode,
76063
+ platformUrl: auth.platformUrl ?? config2.platformUrl ?? config2.siteBase,
76064
+ userId: auth.userId ?? null,
76065
+ activeTwinId: auth.twinId ?? config2.twinId ?? null,
76066
+ wallets: auth.agentWallets ?? [],
76067
+ hasTwinToken: Boolean(auth.twinToken),
76068
+ hasPlatformKey: Boolean(auth.platformKey)
76069
+ };
76070
+ renderOutput(data, (d13) => {
76071
+ console.log(heading("Amiko Account"));
76072
+ console.log(label("Mode", d13.mode));
76073
+ if (d13.platformUrl)
76074
+ console.log(label("Platform", d13.platformUrl));
76075
+ if (d13.userId)
76076
+ console.log(label("User ID", d13.userId));
76077
+ if (d13.activeTwinId)
76078
+ console.log(label("Active twin", d13.activeTwinId));
76079
+ console.log(label("Wallets", String(d13.wallets.length)));
76080
+ if (d13.mode === "none") {
76081
+ console.log("");
76082
+ console.log(error("No credentials found. Provision auth via .amiko.json or an API key."));
76083
+ }
76084
+ }, { json: opts.json });
76085
+ });
76086
+ 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) => {
76087
+ const config2 = loadConfig();
76088
+ const auth = resolveAuth(config2);
76089
+ let twinId;
76090
+ try {
76091
+ twinId = resolveTwinId({ flag: opts.twin, config: config2, auth });
76092
+ } catch (e8) {
76093
+ if (e8 instanceof TwinResolutionError) {
76094
+ console.error(error(e8.message));
76095
+ process.exit(1);
76096
+ }
76097
+ throw e8;
76098
+ }
76099
+ const spinner = opts.json ? null : ora("Fetching twin info...").start();
76100
+ try {
76101
+ const twin = await amikoWebFetch(auth, `/api/agents/${twinId}`);
76102
+ spinner?.stop();
76103
+ renderOutput(twin, (t17) => {
76104
+ console.log(success(`${t17.name}`));
76105
+ console.log(label("ID", t17.id));
76106
+ if (t17.description)
76107
+ console.log(label("Description", t17.description));
76108
+ console.log(label("Public", t17.is_public ? "yes" : "no"));
76109
+ if (t17.voice_status)
76110
+ console.log(label("Voice", t17.voice_status));
76111
+ if (t17.avatar_url)
76112
+ console.log(label("Avatar", t17.avatar_url));
76113
+ if (t17.shipped_at)
76114
+ console.log(label("Shipped", t17.shipped_at));
76115
+ if (t17.updated_at)
76116
+ console.log(dim(`Updated ${t17.updated_at}`));
76117
+ }, { json: opts.json });
76118
+ } catch (e8) {
76119
+ spinner?.stop();
76120
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76121
+ process.exit(1);
76122
+ }
76123
+ });
76124
+ }
76125
+
76126
+ // src/commands/twin.ts
76127
+ function parseBool(raw, flag) {
76128
+ const v8 = raw.toLowerCase();
76129
+ if (v8 === "true" || v8 === "1" || v8 === "yes")
76130
+ return true;
76131
+ if (v8 === "false" || v8 === "0" || v8 === "no")
76132
+ return false;
76133
+ throw new Error(`${flag} must be true or false, got "${raw}"`);
76134
+ }
76135
+ function registerTwinCommand(program2) {
76136
+ program2.command("twin:update").description("Update twin name, description, or public visibility").option("--name <name>", "Display name").option("--description <text>", "Description").option("--public <bool>", "Public visibility (true/false)").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation when changing visibility").option("--json", "Output as JSON").action(async (opts) => {
76137
+ if (opts.name === undefined && opts.description === undefined && opts.public === undefined) {
76138
+ console.error(error("Pass at least one of --name, --description, --public"));
76139
+ process.exit(1);
76140
+ }
76141
+ const config2 = loadConfig();
76142
+ const auth = resolveAuth(config2);
76143
+ let twinId;
76144
+ try {
76145
+ twinId = resolveTwinId({ flag: opts.twin, config: config2, auth });
76146
+ } catch (e8) {
76147
+ if (e8 instanceof TwinResolutionError) {
76148
+ console.error(error(e8.message));
76149
+ process.exit(1);
76150
+ }
76151
+ throw e8;
76152
+ }
76153
+ const body = {};
76154
+ if (opts.name !== undefined)
76155
+ body.name = opts.name;
76156
+ if (opts.description !== undefined)
76157
+ body.description = opts.description;
76158
+ if (opts.public !== undefined) {
76159
+ let next;
76160
+ try {
76161
+ next = parseBool(opts.public, "--public");
76162
+ } catch (e8) {
76163
+ console.error(error(e8.message));
76164
+ process.exit(1);
76165
+ }
76166
+ body.is_public = next;
76167
+ await confirmDestructive({
76168
+ action: `Set twin visibility to ${next ? "PUBLIC" : "PRIVATE"}`,
76169
+ detail: next ? "Anyone on amiko.com will be able to find and chat with this twin." : "The twin will no longer be discoverable.",
76170
+ yes: opts.yes,
76171
+ commandExample: `amiko twin:update --public ${next}`
76172
+ });
76173
+ }
76174
+ const spinner = opts.json ? null : ora("Updating twin...").start();
76175
+ try {
76176
+ const updated = await amikoWebFetch(auth, `/api/agents/${twinId}`, { method: "PATCH", body });
76177
+ spinner?.stop();
76178
+ renderOutput(updated, (u10) => {
76179
+ console.log(success("Twin updated"));
76180
+ if (u10.name)
76181
+ console.log(label("Name", String(u10.name)));
76182
+ if (u10.description !== undefined)
76183
+ console.log(label("Description", String(u10.description ?? "")));
76184
+ if (u10.is_public !== undefined)
76185
+ console.log(label("Public", u10.is_public ? "yes" : "no"));
76186
+ }, { json: opts.json });
76187
+ } catch (e8) {
76188
+ spinner?.stop();
76189
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76190
+ process.exit(1);
76191
+ }
76192
+ });
76193
+ }
76194
+
76195
+ // src/lib/file-input.ts
76196
+ import { readFile } from "node:fs/promises";
76197
+ import { basename, extname } from "node:path";
76198
+ var MAX_BYTES = 100 * 1024 * 1024;
76199
+ var FETCH_TIMEOUT_MS = 30000;
76200
+ async function resolveFileInput(src, opts = {}) {
76201
+ if (src === "-" || opts.stdin) {
76202
+ const buffer4 = await readStdin();
76203
+ const name2 = opts.hintName ?? "stdin";
76204
+ const mime2 = opts.hintMime ?? detectMime(buffer4, name2);
76205
+ return { buffer: buffer4, name: name2, mime: mime2, size: buffer4.length };
76206
+ }
76207
+ if (/^https?:\/\//i.test(src)) {
76208
+ const buffer4 = await fetchRemote(src);
76209
+ const urlPath = new URL(src).pathname;
76210
+ const name2 = opts.hintName ?? (basename(urlPath) || "download");
76211
+ const mime2 = opts.hintMime ?? detectMime(buffer4, name2);
76212
+ return { buffer: buffer4, name: name2, mime: mime2, size: buffer4.length };
76213
+ }
76214
+ const buffer3 = await readFile(src);
76215
+ if (buffer3.length > MAX_BYTES) {
76216
+ throw new Error(`File too large: ${buffer3.length} bytes exceeds ${MAX_BYTES} byte cap.`);
76217
+ }
76218
+ const name = opts.hintName ?? basename(src);
76219
+ const mime = opts.hintMime ?? detectMime(buffer3, name);
76220
+ return { buffer: buffer3, name, mime, size: buffer3.length };
76221
+ }
76222
+ async function readStdin() {
76223
+ const chunks = [];
76224
+ let total = 0;
76225
+ for await (const chunk of process.stdin) {
76226
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
76227
+ total += buf.length;
76228
+ if (total > MAX_BYTES) {
76229
+ throw new Error(`stdin exceeded ${MAX_BYTES} byte cap.`);
76230
+ }
76231
+ chunks.push(buf);
76232
+ }
76233
+ return Buffer.concat(chunks);
76234
+ }
76235
+ async function fetchRemote(url2) {
76236
+ const controller = new AbortController;
76237
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
76238
+ try {
76239
+ const res = await fetch(url2, { signal: controller.signal });
76240
+ if (!res.ok) {
76241
+ throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
76242
+ }
76243
+ const len = Number(res.headers.get("content-length") || 0);
76244
+ if (len > MAX_BYTES) {
76245
+ throw new Error(`Remote file too large: ${len} bytes exceeds ${MAX_BYTES} byte cap.`);
76246
+ }
76247
+ const buf = Buffer.from(await res.arrayBuffer());
76248
+ if (buf.length > MAX_BYTES) {
76249
+ throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${MAX_BYTES} byte cap.`);
76250
+ }
76251
+ return buf;
76252
+ } finally {
76253
+ clearTimeout(timer);
76254
+ }
76255
+ }
76256
+ var MAGIC = [
76257
+ [Buffer.from([37, 80, 68, 70]), "application/pdf"],
76258
+ [Buffer.from([137, 80, 78, 71]), "image/png"],
76259
+ [Buffer.from([255, 216, 255]), "image/jpeg"],
76260
+ [Buffer.from([71, 73, 70, 56]), "image/gif"],
76261
+ [Buffer.from([82, 73, 70, 70]), "audio/wav"],
76262
+ [Buffer.from([73, 68, 51]), "audio/mpeg"],
76263
+ [Buffer.from([255, 251]), "audio/mpeg"],
76264
+ [Buffer.from([80, 75, 3, 4]), "application/zip"]
76265
+ ];
76266
+ var EXT_MIME = {
76267
+ ".pdf": "application/pdf",
76268
+ ".png": "image/png",
76269
+ ".jpg": "image/jpeg",
76270
+ ".jpeg": "image/jpeg",
76271
+ ".gif": "image/gif",
76272
+ ".webp": "image/webp",
76273
+ ".txt": "text/plain",
76274
+ ".md": "text/markdown",
76275
+ ".json": "application/json",
76276
+ ".wav": "audio/wav",
76277
+ ".mp3": "audio/mpeg",
76278
+ ".m4a": "audio/mp4",
76279
+ ".ogg": "audio/ogg",
76280
+ ".zip": "application/zip"
76281
+ };
76282
+ function detectMime(buffer3, name) {
76283
+ for (const [sig, mime] of MAGIC) {
76284
+ if (buffer3.length >= sig.length && buffer3.subarray(0, sig.length).equals(sig)) {
76285
+ return mime;
76286
+ }
76287
+ }
76288
+ const ext = extname(name).toLowerCase();
76289
+ return EXT_MIME[ext] ?? "application/octet-stream";
76290
+ }
76291
+
76292
+ // src/commands/docs.ts
76293
+ function twinFromFlag(opts) {
76294
+ const config2 = loadConfig();
76295
+ const auth = resolveAuth(config2);
76296
+ try {
76297
+ return { twinId: resolveTwinId({ flag: opts.twin, config: config2, auth }), auth };
76298
+ } catch (e8) {
76299
+ if (e8 instanceof TwinResolutionError) {
76300
+ console.error(error(e8.message));
76301
+ process.exit(1);
76302
+ }
76303
+ throw e8;
76304
+ }
76305
+ }
76306
+ function fmtBytes(n20) {
76307
+ if (!n20 || n20 <= 0)
76308
+ return "-";
76309
+ const units = ["B", "KB", "MB", "GB"];
76310
+ let v8 = n20;
76311
+ let i15 = 0;
76312
+ while (v8 >= 1024 && i15 < units.length - 1) {
76313
+ v8 /= 1024;
76314
+ i15++;
76315
+ }
76316
+ return `${v8.toFixed(v8 >= 10 ? 0 : 1)} ${units[i15]}`;
76317
+ }
76318
+ function registerDocsCommand(program2) {
76319
+ program2.command("docs").description("List uploaded documents for a twin").option("--twin <idOrName>", "Target twin id or name").option("--limit <n>", "Max results", "50").option("--json", "Output as JSON").action(async (opts) => {
76320
+ const { twinId, auth } = twinFromFlag(opts);
76321
+ const spinner = opts.json ? null : ora("Loading documents...").start();
76322
+ try {
76323
+ const data = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, { query: { limit: opts.limit ?? "50" } });
76324
+ spinner?.stop();
76325
+ renderOutput(data, (d13) => {
76326
+ if (!d13.docs || d13.docs.length === 0) {
76327
+ console.log(dim("No documents."));
76328
+ return;
76329
+ }
76330
+ console.log(heading(`Documents (${d13.total ?? d13.docs.length})`));
76331
+ const rows = [
76332
+ [
76333
+ dim("ID"),
76334
+ dim("NAME"),
76335
+ dim("SIZE"),
76336
+ dim("STATUS"),
76337
+ dim("CREATED")
76338
+ ],
76339
+ ...d13.docs.map((doc2) => [
76340
+ doc2.id.slice(0, 8),
76341
+ doc2.title || doc2.filename,
76342
+ fmtBytes(doc2.file_size),
76343
+ doc2.status || "-",
76344
+ doc2.created_at?.slice(0, 10) || "-"
76345
+ ])
76346
+ ];
76347
+ console.log(table(rows));
76348
+ }, { json: opts.json });
76349
+ } catch (e8) {
76350
+ spinner?.stop();
76351
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76352
+ process.exit(1);
76353
+ }
76354
+ });
76355
+ program2.command("docs:upload <source>").description("Upload a document (path | https URL | - for stdin)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--title <title>", "Document title (defaults to filename)").option("--description <text>", "Document description").option("--doc-type <type>", "Document type (e.g. 'other', 'memory')", "other").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
76356
+ const { twinId, auth } = twinFromFlag(opts);
76357
+ const spinner = opts.json ? null : ora("Reading input...").start();
76358
+ try {
76359
+ const file2 = await resolveFileInput(source, {
76360
+ stdin: opts.stdin,
76361
+ hintName: opts.name
76362
+ });
76363
+ if (spinner)
76364
+ spinner.text = `Uploading ${file2.name}...`;
76365
+ const form2 = new FormData;
76366
+ form2.set("file", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
76367
+ const upload = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/upload`, {
76368
+ method: "POST",
76369
+ multipart: form2
76370
+ });
76371
+ if (!upload.url || !upload.filename || !upload.fileType) {
76372
+ throw new Error(`Upload response missing required fields: ${JSON.stringify(upload)}`);
76373
+ }
76374
+ if (spinner)
76375
+ spinner.text = `Registering ${upload.filename}...`;
76376
+ const title = opts.title || upload.filename.replace(/\.[^.]+$/, "");
76377
+ const doc2 = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, {
76378
+ method: "POST",
76379
+ body: {
76380
+ filename: upload.filename,
76381
+ fileUrl: upload.url,
76382
+ fileType: upload.fileType,
76383
+ title,
76384
+ description: opts.description ?? undefined,
76385
+ doc_type: opts.docType ?? "other"
76386
+ }
76387
+ });
76388
+ spinner?.stop();
76389
+ renderOutput(doc2, (d13) => {
76390
+ console.log(success(`Uploaded ${d13.filename}`));
76391
+ console.log(label("Doc ID", d13.id));
76392
+ console.log(label("Title", d13.title || "-"));
76393
+ if (upload.fileSize)
76394
+ console.log(label("Size", fmtBytes(upload.fileSize)));
76395
+ if (upload.fileType)
76396
+ console.log(label("Type", upload.fileType));
76397
+ if (upload.url)
76398
+ console.log(label("URL", upload.url));
76399
+ }, { json: opts.json });
76400
+ } catch (e8) {
76401
+ spinner?.stop();
76402
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76403
+ process.exit(1);
76404
+ }
76405
+ });
76406
+ program2.command("docs:delete <docId>").description("Delete a document").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (docId, opts) => {
76407
+ const { twinId, auth } = twinFromFlag(opts);
76408
+ await confirmDestructive({
76409
+ action: `Delete document ${docId}`,
76410
+ detail: "This will remove the document and its embeddings from the twin's RAG.",
76411
+ yes: opts.yes,
76412
+ commandExample: `amiko docs:delete ${docId}`
76413
+ });
76414
+ const spinner = opts.json ? null : ora("Deleting document...").start();
76415
+ try {
76416
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "DELETE" });
76417
+ spinner?.stop();
76418
+ renderOutput(result, () => console.log(success(`Deleted ${docId}`)), { json: opts.json });
76419
+ } catch (e8) {
76420
+ spinner?.stop();
76421
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76422
+ process.exit(1);
76423
+ }
76424
+ });
76425
+ program2.command("docs:presign <source>").description("Get a presigned upload URL for a document (no upload)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename used in the request").option("--json", "Output as JSON").action(async (source, opts) => {
76426
+ const { twinId, auth } = twinFromFlag(opts);
76427
+ const filename = opts.name ?? (source === "-" ? "stdin" : source.split("/").pop() ?? source);
76428
+ try {
76429
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/presigned-url`, { method: "POST", body: { filename } });
76430
+ renderOutput(result, (r19) => console.log(JSON.stringify(r19, null, 2)), { json: opts.json });
76431
+ } catch (e8) {
76432
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76433
+ process.exit(1);
76434
+ }
76435
+ });
76436
+ }
76437
+
76438
+ // src/commands/voice.ts
76439
+ function twinFromFlag2(opts) {
76440
+ const config2 = loadConfig();
76441
+ const auth = resolveAuth(config2);
76442
+ try {
76443
+ return { twinId: resolveTwinId({ flag: opts.twin, config: config2, auth }), auth };
76444
+ } catch (e8) {
76445
+ if (e8 instanceof TwinResolutionError) {
76446
+ console.error(error(e8.message));
76447
+ process.exit(1);
76448
+ }
76449
+ throw e8;
76450
+ }
76451
+ }
76452
+ function registerVoiceCommand(program2) {
76453
+ program2.command("voice: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) => {
76454
+ if (opts.description.trim().length < 20) {
76455
+ console.error(error("--description must be at least 20 characters"));
76456
+ process.exit(1);
76457
+ }
76458
+ const { twinId, auth } = twinFromFlag2(opts);
76459
+ const spinner = opts.json ? null : ora("Generating voice previews...").start();
76460
+ try {
76461
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/design`, {
76462
+ method: "POST",
76463
+ body: { voiceDescription: opts.description.trim() },
76464
+ timeoutMs: 120000
76465
+ });
76466
+ spinner?.stop();
76467
+ renderOutput(result, (r19) => {
76468
+ console.log(heading(`Voice Previews (${r19.previews?.length ?? 0})`));
76469
+ for (const [i15, p13] of (r19.previews ?? []).entries()) {
76470
+ console.log(label(`#${i15 + 1} id`, p13.generated_voice_id ?? "?"));
76471
+ }
76472
+ console.log("");
76473
+ console.log(dim("Pick one with: amiko voice:create --sample <id>"));
76474
+ }, { json: opts.json });
76475
+ } catch (e8) {
76476
+ spinner?.stop();
76477
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76478
+ process.exit(1);
76479
+ }
76480
+ });
76481
+ program2.command("voice: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) => {
76482
+ const { twinId, auth } = twinFromFlag2(opts);
76483
+ const spinner = opts.json ? null : ora("Creating voice...").start();
76484
+ try {
76485
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/create`, {
76486
+ method: "POST",
76487
+ body: {
76488
+ generatedVoiceId: opts.sample,
76489
+ voiceDescription: opts.description
76490
+ },
76491
+ timeoutMs: 120000
76492
+ });
76493
+ spinner?.stop();
76494
+ renderOutput(result, (r19) => {
76495
+ console.log(success("Voice created"));
76496
+ if (r19.voice_id)
76497
+ console.log(label("Voice ID", String(r19.voice_id)));
76498
+ if (r19.voice_status)
76499
+ console.log(label("Status", String(r19.voice_status)));
76500
+ }, { json: opts.json });
76501
+ } catch (e8) {
76502
+ spinner?.stop();
76503
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76504
+ process.exit(1);
76505
+ }
76506
+ });
76507
+ program2.command("voice: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) => {
76508
+ const { twinId, auth } = twinFromFlag2(opts);
76509
+ const spinner = opts.json ? null : ora("Reading audio...").start();
76510
+ try {
76511
+ const file2 = await resolveFileInput(source, {
76512
+ stdin: opts.stdin,
76513
+ hintName: opts.name,
76514
+ hintMime: "audio/mpeg"
76515
+ });
76516
+ if (spinner)
76517
+ spinner.text = `Cloning voice from ${file2.name}...`;
76518
+ const form2 = new FormData;
76519
+ form2.set("audio", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
76520
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/clone`, { method: "POST", multipart: form2, timeoutMs: 180000 });
76521
+ spinner?.stop();
76522
+ renderOutput(result, (r19) => {
76523
+ console.log(success("Voice cloned"));
76524
+ if (r19.voice_id)
76525
+ console.log(label("Voice ID", String(r19.voice_id)));
76526
+ if (r19.voice_status)
76527
+ console.log(label("Status", String(r19.voice_status)));
76528
+ }, { json: opts.json });
76529
+ } catch (e8) {
76530
+ spinner?.stop();
76531
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76532
+ process.exit(1);
76533
+ }
76534
+ });
76535
+ program2.command("voice:reset").description("Reset the twin's voice to default").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (opts) => {
76536
+ const { twinId, auth } = twinFromFlag2(opts);
76537
+ await confirmDestructive({
76538
+ action: "Reset voice",
76539
+ detail: "This removes the custom voice. You can redesign or clone a new one later.",
76540
+ yes: opts.yes,
76541
+ commandExample: "amiko voice:reset"
76542
+ });
76543
+ const spinner = opts.json ? null : ora("Resetting voice...").start();
76544
+ try {
76545
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/reset`, { method: "POST" });
76546
+ spinner?.stop();
76547
+ renderOutput(result, () => console.log(success("Voice reset")), { json: opts.json });
76548
+ } catch (e8) {
76549
+ spinner?.stop();
76550
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76551
+ process.exit(1);
76552
+ }
76553
+ });
76554
+ }
76555
+
76556
+ // src/commands/avatar.ts
76557
+ function registerAvatarCommand(program2) {
76558
+ program2.command("avatar:update").description("Update twin avatar image (path | https URL | - for stdin)").requiredOption("--file <source>", "Image source").option("--twin <idOrName>", "Target twin id or name").option("--stdin", "Read from stdin (file must be -)").option("--name <filename>", "Override filename").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (opts) => {
76559
+ const config2 = loadConfig();
76560
+ const auth = resolveAuth(config2);
76561
+ let twinId;
76562
+ try {
76563
+ twinId = resolveTwinId({ flag: opts.twin, config: config2, auth });
76564
+ } catch (e8) {
76565
+ if (e8 instanceof TwinResolutionError) {
76566
+ console.error(error(e8.message));
76567
+ process.exit(1);
76568
+ }
76569
+ throw e8;
76570
+ }
76571
+ await confirmDestructive({
76572
+ action: "Replace twin avatar",
76573
+ detail: "The twin's public avatar will change immediately.",
76574
+ yes: opts.yes,
76575
+ commandExample: `amiko avatar:update --file ${opts.file}`
76576
+ });
76577
+ const spinner = opts.json ? null : ora("Reading image...").start();
76578
+ try {
76579
+ const file2 = await resolveFileInput(opts.file, {
76580
+ stdin: opts.stdin,
76581
+ hintName: opts.name
76582
+ });
76583
+ if (!file2.mime.startsWith("image/")) {
76584
+ spinner?.stop();
76585
+ console.error(error(`File is not an image (mime: ${file2.mime})`));
76586
+ process.exit(1);
76587
+ }
76588
+ if (file2.size > 5 * 1024 * 1024) {
76589
+ spinner?.stop();
76590
+ console.error(error(`Image too large: ${file2.size} bytes (max 5 MB)`));
76591
+ process.exit(1);
76592
+ }
76593
+ if (spinner)
76594
+ spinner.text = `Uploading avatar (${file2.name})...`;
76595
+ const form2 = new FormData;
76596
+ form2.set("image", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
76597
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/avatar`, {
76598
+ method: "POST",
76599
+ multipart: form2
76600
+ });
76601
+ spinner?.stop();
76602
+ renderOutput(result, (r19) => {
76603
+ console.log(success("Avatar updated"));
76604
+ if (r19.avatar_url)
76605
+ console.log(label("URL", r19.avatar_url));
76606
+ }, { json: opts.json });
76607
+ } catch (e8) {
76608
+ spinner?.stop();
76609
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76610
+ process.exit(1);
76611
+ }
76612
+ });
76613
+ }
76614
+
76615
+ // src/commands/friends.ts
76616
+ function registerFriendsCommand(program2) {
76617
+ program2.command("friends").description("List friends").option("--type <user|twin>", "Filter by friend type").option("--json", "Output as JSON").action(async (opts) => {
76618
+ const auth = resolveAuth(loadConfig());
76619
+ const spinner = opts.json ? null : ora("Loading friends...").start();
76620
+ try {
76621
+ const data = await amikoWebFetch(auth, "/api/friends", { query: { type: opts.type } });
76622
+ spinner?.stop();
76623
+ renderOutput(data, (d13) => {
76624
+ const items = d13.items ?? [];
76625
+ if (items.length === 0) {
76626
+ console.log(dim("No friends."));
76627
+ return;
76628
+ }
76629
+ console.log(heading(`Friends (${items.length})`));
76630
+ const rows = [
76631
+ [dim("ID"), dim("NAME"), dim("HANDLE"), dim("TYPE"), dim("STATUS")],
76632
+ ...items.map((f13) => [
76633
+ f13.id.slice(0, 8),
76634
+ f13.friend.name ?? "-",
76635
+ f13.friend.handle ?? "-",
76636
+ f13.friend.type,
76637
+ f13.status
76638
+ ])
76639
+ ];
76640
+ console.log(table(rows));
76641
+ }, { json: opts.json });
76642
+ } catch (e8) {
76643
+ spinner?.stop();
76644
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76645
+ process.exit(1);
76646
+ }
76647
+ });
76648
+ program2.command("friends:add").description("Send a friend request to a user by id").requiredOption("--id <userId>", "Target user id").option("--json", "Output as JSON").action(async (opts) => {
76649
+ const auth = resolveAuth(loadConfig());
76650
+ const spinner = opts.json ? null : ora("Sending friend request...").start();
76651
+ try {
76652
+ const data = await amikoWebFetch(auth, "/api/friends", {
76653
+ method: "POST",
76654
+ body: { friend_id: opts.id }
76655
+ });
76656
+ spinner?.stop();
76657
+ renderOutput(data, (d13) => {
76658
+ console.log(success(d13.auto_accepted ? "Auto-accepted" : "Request sent"));
76659
+ if (d13.friendship_id)
76660
+ console.log(label("Friendship ID", d13.friendship_id));
76661
+ }, { json: opts.json });
76662
+ } catch (e8) {
76663
+ spinner?.stop();
76664
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76665
+ process.exit(1);
76666
+ }
76667
+ });
76668
+ program2.command("friends:accept <friendshipId>").description("Accept a pending friend request").option("--json", "Output as JSON").action(async (friendshipId, opts) => {
76669
+ const auth = resolveAuth(loadConfig());
76670
+ const spinner = opts.json ? null : ora("Accepting...").start();
76671
+ try {
76672
+ const data = await amikoWebFetch(auth, `/api/friends/${friendshipId}`, { method: "PATCH", body: { action: "accept" } });
76673
+ spinner?.stop();
76674
+ renderOutput(data, () => console.log(success(`Accepted ${friendshipId}`)), { json: opts.json });
76675
+ } catch (e8) {
76676
+ spinner?.stop();
76677
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76678
+ process.exit(1);
76679
+ }
76680
+ });
76681
+ program2.command("friends:remove <friendshipId>").description("Remove a friend").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (friendshipId, opts) => {
76682
+ await confirmDestructive({
76683
+ action: `Remove friend ${friendshipId}`,
76684
+ yes: opts.yes,
76685
+ commandExample: `amiko friends:remove ${friendshipId}`
76686
+ });
76687
+ const auth = resolveAuth(loadConfig());
76688
+ const spinner = opts.json ? null : ora("Removing...").start();
76689
+ try {
76690
+ const data = await amikoWebFetch(auth, `/api/friends/${friendshipId}`, { method: "DELETE" });
76691
+ spinner?.stop();
76692
+ renderOutput(data, () => console.log(success(`Removed ${friendshipId}`)), { json: opts.json });
76693
+ } catch (e8) {
76694
+ spinner?.stop();
76695
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76696
+ process.exit(1);
76697
+ }
76698
+ });
76699
+ }
76700
+
76701
+ // src/commands/feed.ts
76702
+ function registerFeedCommand(program2) {
76703
+ program2.command("feed").description("Get feed posts").option("--type <for_you|friends>", "Feed type", "friends").option("--hashtag <tag>", "Filter by hashtag").option("--limit <n>", "Max results (default 10, max 100)").option("--cursor <id>", "Pagination cursor").option("--json", "Output as JSON").action(async (opts) => {
76704
+ const auth = resolveAuth(loadConfig());
76705
+ const spinner = opts.json ? null : ora("Loading feed...").start();
76706
+ try {
76707
+ const data = await amikoWebFetch(auth, "/api/feed", {
76708
+ query: {
76709
+ type: opts.type,
76710
+ hashtag: opts.hashtag,
76711
+ limit: opts.limit,
76712
+ cursor: opts.cursor
76713
+ }
76714
+ });
76715
+ spinner?.stop();
76716
+ const posts = data.items ?? data.posts ?? [];
76717
+ renderOutput({ ...data, posts }, (d13) => {
76718
+ if (posts.length === 0) {
76719
+ console.log(dim("No posts."));
76720
+ return;
76721
+ }
76722
+ console.log(heading(`Feed (${opts.type ?? "friends"}) — ${posts.length} posts`));
76723
+ for (const p13 of posts) {
76724
+ const author = p13.twin?.name ?? p13.user?.name ?? p13.user?.handle ?? "unknown";
76725
+ console.log("");
76726
+ console.log(label(author, p13.created_at?.slice(0, 16) ?? ""));
76727
+ console.log(p13.content);
76728
+ if (p13._count) {
76729
+ console.log(dim(`${p13._count.likes ?? 0} likes · ${p13._count.comments ?? 0} comments · ${p13._count.bookmarks ?? 0} bookmarks`));
76730
+ }
76731
+ console.log(dim(`id: ${p13.id}`));
76732
+ }
76733
+ if (d13.nextCursor) {
76734
+ console.log("");
76735
+ console.log(dim(`Next: --cursor ${d13.nextCursor}`));
76736
+ }
76737
+ }, { json: opts.json });
76738
+ } catch (e8) {
76739
+ spinner?.stop();
76740
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76741
+ process.exit(1);
76742
+ }
76743
+ });
76744
+ program2.command("post:comment").description("Comment on a post").requiredOption("--id <postId>", "Target post id").requiredOption("--comment <text>", "Comment body").option("--twin <idOrName>", "Post as this twin (agent mode)").option("--json", "Output as JSON").action(async (opts) => {
76745
+ const config2 = loadConfig();
76746
+ const auth = resolveAuth(config2);
76747
+ let twinId;
76748
+ if (opts.twin || auth.twinId) {
76749
+ try {
76750
+ twinId = resolveTwinId({ flag: opts.twin, config: config2, auth });
76751
+ } catch (e8) {
76752
+ if (e8 instanceof TwinResolutionError) {
76753
+ twinId = undefined;
76754
+ } else {
76755
+ throw e8;
76756
+ }
76757
+ }
76758
+ }
76759
+ const spinner = opts.json ? null : ora("Posting comment...").start();
76760
+ try {
76761
+ const body = { comment: opts.comment };
76762
+ if (twinId)
76763
+ body.twin_id = twinId;
76764
+ const data = await amikoWebFetch(auth, `/api/posts/${opts.id}/comments`, { method: "POST", body });
76765
+ spinner?.stop();
76766
+ renderOutput(data, (d13) => {
76767
+ console.log(success("Comment posted"));
76768
+ if (d13.id)
76769
+ console.log(label("Comment ID", String(d13.id)));
76770
+ }, { json: opts.json });
76771
+ } catch (e8) {
76772
+ spinner?.stop();
76773
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76774
+ process.exit(1);
76775
+ }
76776
+ });
76777
+ }
76778
+
76779
+ // src/commands/composio.ts
76780
+ function twinFromFlag3(opts) {
76781
+ const config2 = loadConfig();
76782
+ const auth = resolveAuth(config2);
76783
+ try {
76784
+ return { twinId: resolveTwinId({ flag: opts.twin, config: config2, auth }), auth };
76785
+ } catch (e8) {
76786
+ if (e8 instanceof TwinResolutionError) {
76787
+ console.error(error(e8.message));
76788
+ process.exit(1);
76789
+ }
76790
+ throw e8;
76791
+ }
76792
+ }
76793
+ function registerComposioCommand(program2) {
76794
+ program2.command("composio:connect").description("Start Composio OAuth for an app (gmail, github, ...)").requiredOption("--app <slug>", "App name (e.g. gmail)").option("--twin <idOrName>", "Target twin id or name").option("--redirect <url>", "Override OAuth redirect URL").option("--force", "Force reconnect if already connected").option("--json", "Output as JSON").action(async (opts) => {
76795
+ const { twinId, auth } = twinFromFlag3(opts);
76796
+ const spinner = opts.json ? null : ora(`Connecting ${opts.app}...`).start();
76797
+ try {
76798
+ const data = await amikoWebFetch(auth, `/api/agents/${twinId}/composio/connect`, {
76799
+ method: "POST",
76800
+ body: {
76801
+ appName: opts.app,
76802
+ redirectUrl: opts.redirect,
76803
+ forceReconnect: opts.force ?? true
76804
+ }
76805
+ });
76806
+ spinner?.stop();
76807
+ renderOutput(data, (d13) => {
76808
+ if (d13.alreadyConnected) {
76809
+ console.log(success(`${opts.app} is already connected`));
76810
+ if (d13.connectionId)
76811
+ console.log(label("Connection ID", d13.connectionId));
76812
+ return;
76813
+ }
76814
+ console.log(success(`OAuth URL ready for ${opts.app}`));
76815
+ if (d13.redirectUrl) {
76816
+ console.log("");
76817
+ console.log(d13.redirectUrl);
76818
+ console.log("");
76819
+ console.log(dim("Open this URL in a browser to complete the connection."));
76820
+ }
76821
+ if (d13.connectionId)
76822
+ console.log(label("Connection ID", d13.connectionId));
76823
+ }, { json: opts.json });
76824
+ } catch (e8) {
76825
+ spinner?.stop();
76826
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76827
+ process.exit(1);
76828
+ }
76829
+ });
76830
+ program2.command("composio:disconnect").description("Disconnect a Composio app for the twin").requiredOption("--app <slug>", "App name (e.g. gmail)").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (opts) => {
76831
+ const { twinId, auth } = twinFromFlag3(opts);
76832
+ await confirmDestructive({
76833
+ action: `Disconnect ${opts.app}`,
76834
+ detail: "The twin will lose access to the app's tools until reconnected.",
76835
+ yes: opts.yes,
76836
+ commandExample: `amiko composio:disconnect --app ${opts.app}`
76837
+ });
76838
+ const spinner = opts.json ? null : ora("Finding connection...").start();
76839
+ try {
76840
+ const connections = await amikoWebFetch(auth, `/api/agents/${twinId}/composio/connections`);
76841
+ const match = connections.details?.find((d13) => d13.appName?.toLowerCase() === opts.app.toLowerCase());
76842
+ if (!match?.id) {
76843
+ spinner?.stop();
76844
+ console.error(error(`No active connection for "${opts.app}". Connected: ${connections.connections?.join(", ") || "none"}`));
76845
+ process.exit(1);
76846
+ }
76847
+ if (spinner)
76848
+ spinner.text = `Disconnecting ${opts.app}...`;
76849
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/composio/connections`, { method: "DELETE", body: { connectionId: match.id } });
76850
+ spinner?.stop();
76851
+ renderOutput(result, () => console.log(success(`Disconnected ${opts.app}`)), { json: opts.json });
76852
+ } catch (e8) {
76853
+ spinner?.stop();
76854
+ console.error(error(e8 instanceof Error ? e8.message : String(e8)));
76855
+ process.exit(1);
76856
+ }
76857
+ });
76858
+ }
76859
+
75651
76860
  // src/index.ts
75652
76861
  var require2 = createRequire2(import.meta.url);
75653
76862
  var { version: PKG_VERSION } = require2("../package.json");
@@ -75669,4 +76878,12 @@ registerConfigCommand(program2);
75669
76878
  registerServicesCommand(program2);
75670
76879
  registerDiscoveryCommand(program2);
75671
76880
  registerUpdateCommand(program2);
76881
+ registerAccountsCommand(program2);
76882
+ registerTwinCommand(program2);
76883
+ registerDocsCommand(program2);
76884
+ registerVoiceCommand(program2);
76885
+ registerAvatarCommand(program2);
76886
+ registerFriendsCommand(program2);
76887
+ registerFeedCommand(program2);
76888
+ registerComposioCommand(program2);
75672
76889
  program2.parse();