@heyamiko/amiko-cli 0.9.0-beta.7 → 0.9.1

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
@@ -18484,6 +18484,104 @@ async function requireApproval(args) {
18484
18484
  process.exit(2);
18485
18485
  }
18486
18486
 
18487
+ // src/lib/amiko-web.ts
18488
+ var DEFAULT_PLATFORM_URL = "https://platform.heyamiko.com";
18489
+ function baseUrl(auth) {
18490
+ const url = auth.platformUrl || DEFAULT_PLATFORM_URL;
18491
+ return url.replace(/\/$/, "");
18492
+ }
18493
+ function authHeader(auth) {
18494
+ if (auth.twinToken)
18495
+ return `Bearer ${auth.twinToken}`;
18496
+ throw new Error("No twin token found. Place an .amiko.json file with amikoTwinToken in the current directory or any parent.");
18497
+ }
18498
+ async function amikoWebFetch(auth, path, opts = {}) {
18499
+ const url = new URL(`${baseUrl(auth)}${path}`);
18500
+ if (opts.query) {
18501
+ for (const [k, v] of Object.entries(opts.query)) {
18502
+ if (v !== undefined && v !== null)
18503
+ url.searchParams.set(k, String(v));
18504
+ }
18505
+ }
18506
+ const headers = {
18507
+ Authorization: authHeader(auth),
18508
+ ...opts.headers
18509
+ };
18510
+ let body;
18511
+ if (opts.multipart) {
18512
+ body = opts.multipart;
18513
+ } else if (opts.rawBody) {
18514
+ body = opts.rawBody;
18515
+ if (opts.rawContentType)
18516
+ headers["Content-Type"] = opts.rawContentType;
18517
+ } else if (opts.body) {
18518
+ body = JSON.stringify(opts.body);
18519
+ headers["Content-Type"] = "application/json";
18520
+ }
18521
+ const controller = new AbortController;
18522
+ const timeoutMs = opts.timeoutMs ?? 30000;
18523
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
18524
+ let res;
18525
+ try {
18526
+ res = await fetch(url, {
18527
+ method: opts.method ?? "GET",
18528
+ headers,
18529
+ body,
18530
+ signal: controller.signal
18531
+ });
18532
+ } finally {
18533
+ clearTimeout(timer);
18534
+ }
18535
+ if (!res.ok) {
18536
+ const text = await res.text().catch(() => "");
18537
+ const err = new Error(formatError(res.status, text, path));
18538
+ err.status = res.status;
18539
+ err.body = text;
18540
+ throw err;
18541
+ }
18542
+ const ctype = res.headers.get("content-type") || "";
18543
+ if (ctype.includes("application/json")) {
18544
+ return await res.json();
18545
+ }
18546
+ return await res.text();
18547
+ }
18548
+ function formatError(status, body, path) {
18549
+ const prefix = `${status} ${path}`;
18550
+ if (status === 401) {
18551
+ return `${prefix}: unauthorized. The stored token is invalid or expired; re-link this device to Amiko.`;
18552
+ }
18553
+ if (status === 403) {
18554
+ return `${prefix}: forbidden. The current auth context does not have access to this resource.`;
18555
+ }
18556
+ if (!body)
18557
+ return prefix;
18558
+ const snippet = body.length > 500 ? `${body.slice(0, 500)}...` : body;
18559
+ return `${prefix}: ${snippet}`;
18560
+ }
18561
+
18562
+ // src/lib/wallet-default.ts
18563
+ var cache = null;
18564
+ async function loadWallets(auth) {
18565
+ if (cache)
18566
+ return cache;
18567
+ const data = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets`);
18568
+ cache = data.wallets ?? [];
18569
+ return cache;
18570
+ }
18571
+ async function resolveWalletAddress(auth, explicit, preferredChain = "solana") {
18572
+ if (explicit)
18573
+ return explicit;
18574
+ const wallets = await loadWallets(auth);
18575
+ const match = wallets.find((w) => w.status === "active" && w.chain === preferredChain);
18576
+ if (match)
18577
+ return match.wallet_address;
18578
+ const anyActive = wallets.find((w) => w.status === "active");
18579
+ if (anyActive && !explicit) {
18580
+ throw new Error(`No active ${preferredChain} wallet found. Run \`amiko wallets create --chain ${preferredChain}\` or pass --wallet <address> explicitly.`);
18581
+ }
18582
+ throw new Error(`No wallets found for this agent. Run \`amiko wallets create --chain ${preferredChain}\` first.`);
18583
+ }
18584
+
18487
18585
  // src/lib/across.ts
18488
18586
  function getBridgeBase() {
18489
18587
  const config = loadConfig();
@@ -18635,7 +18733,7 @@ var DEFAULT_ORIGIN = CHAINS.solana;
18635
18733
  var DEFAULT_DESTINATION = CHAINS.tempo;
18636
18734
  function registerBridgeCommand(program2) {
18637
18735
  const cmd = program2.command("bridge").description("Bridge USDC across chains via Across Protocol");
18638
- cmd.command("quote").description("Get a bridge quote for USDC transfer").argument("<amount>", "Amount of USDC to bridge (e.g., 10, 5.50)").option("--from <chain>", "Origin chain (default: solana)", "solana").option("--to <chain>", "Destination chain (default: tempo)", "tempo").option("--recipient <address>", "Recipient address on destination chain").option("--depositor <address>", "Depositor address on origin chain").action(async (amountStr, opts) => {
18736
+ cmd.command("quote").description("Get a bridge quote for USDC transfer").argument("<amount>", "Amount of USDC to bridge (e.g., 10, 5.50)").option("--from <chain>", "Origin chain (default: solana)", "solana").option("--to <chain>", "Destination chain (default: tempo)", "tempo").option("--recipient <address>", "Recipient address on destination chain (required for Solana ↔ EVM)").option("--depositor <address>", "Depositor address on origin chain (defaults to your wallet on that chain)").action(async (amountStr, opts) => {
18639
18737
  const spinner = ora("Fetching bridge quote...").start();
18640
18738
  try {
18641
18739
  const originChainId = resolveChainId(opts.from);
@@ -18643,10 +18741,14 @@ function registerBridgeCommand(program2) {
18643
18741
  const inputToken = getUsdcAddress(originChainId);
18644
18742
  const outputToken = getUsdcAddress(destChainId);
18645
18743
  const amount = parseUsdcAmount(amountStr);
18646
- const depositor = opts.depositor;
18647
- if (!depositor) {
18744
+ const preferredChain = originChainId === CHAINS.solana ? "solana" : "base";
18745
+ let depositor;
18746
+ try {
18747
+ const auth = requireAuth();
18748
+ depositor = await resolveWalletAddress(auth, opts.depositor, preferredChain);
18749
+ } catch (e) {
18648
18750
  spinner.stop();
18649
- console.error(error("No depositor address. Provide --depositor <address>."));
18751
+ console.error(error(e instanceof Error ? e.message : String(e)));
18650
18752
  process.exit(1);
18651
18753
  }
18652
18754
  const isCrossEcosystem = originChainId === CHAINS.solana !== (destChainId === CHAINS.solana);
@@ -18695,7 +18797,7 @@ function registerBridgeCommand(program2) {
18695
18797
  if (quote.swapTx) {
18696
18798
  console.log("");
18697
18799
  console.log(dim('Transaction ready. Run "amiko wallets bridge send" to execute.'));
18698
- console.log(dim(` amiko wallets bridge send ${amountStr} --from ${opts.from} --to ${opts.to}`));
18800
+ console.log(dim(` amiko wallets bridge send ${amountStr} --from ${opts.from} --to ${opts.to}${opts.depositor ? ` --depositor ${opts.depositor}` : ""}${opts.recipient ? ` --recipient ${opts.recipient}` : ""}`));
18699
18801
  }
18700
18802
  } catch (err) {
18701
18803
  spinner.stop();
@@ -18703,7 +18805,7 @@ function registerBridgeCommand(program2) {
18703
18805
  process.exit(1);
18704
18806
  }
18705
18807
  });
18706
- cmd.command("send").description("Execute a USDC bridge transfer via Across").argument("<amount>", "Amount of USDC to bridge (e.g., 10, 5.50)").option("--from <chain>", "Origin chain (default: solana)", "solana").option("--to <chain>", "Destination chain (default: tempo)", "tempo").option("--recipient <address>", "Recipient address on destination chain").option("--depositor <address>", "Depositor address on origin chain").option("--raw", "Output raw transaction JSON for manual signing").option("--yes", "Skip confirmation (only after the user has approved)").action(async (amountStr, opts) => {
18808
+ cmd.command("send").description("Execute a USDC bridge transfer via Across").argument("<amount>", "Amount of USDC to bridge (e.g., 10, 5.50)").option("--from <chain>", "Origin chain (default: solana)", "solana").option("--to <chain>", "Destination chain (default: tempo)", "tempo").option("--recipient <address>", "Recipient address on destination chain (required for Solana ↔ EVM)").option("--depositor <address>", "Depositor address on origin chain (defaults to your wallet on that chain)").option("--raw", "Output raw transaction JSON for manual signing").option("--yes", "Skip confirmation (only after the user has approved)").action(async (amountStr, opts) => {
18707
18809
  const spinner = ora("Preparing bridge transaction...").start();
18708
18810
  try {
18709
18811
  const originChainId = resolveChainId(opts.from);
@@ -18711,10 +18813,14 @@ function registerBridgeCommand(program2) {
18711
18813
  const inputToken = getUsdcAddress(originChainId);
18712
18814
  const outputToken = getUsdcAddress(destChainId);
18713
18815
  const amount = parseUsdcAmount(amountStr);
18714
- const depositor = opts.depositor;
18715
- if (!depositor) {
18816
+ const preferredChain = originChainId === CHAINS.solana ? "solana" : "base";
18817
+ let depositor;
18818
+ try {
18819
+ const auth = requireAuth();
18820
+ depositor = await resolveWalletAddress(auth, opts.depositor, preferredChain);
18821
+ } catch (e) {
18716
18822
  spinner.stop();
18717
- console.error(error("No depositor address. Provide --depositor <address>."));
18823
+ console.error(error(e instanceof Error ? e.message : String(e)));
18718
18824
  process.exit(1);
18719
18825
  }
18720
18826
  const isCrossEcosystem = originChainId === CHAINS.solana !== (destChainId === CHAINS.solana);
@@ -18974,81 +19080,6 @@ function registerBridgeCommand(program2) {
18974
19080
  // src/commands/credits.ts
18975
19081
  import readline2 from "node:readline";
18976
19082
 
18977
- // src/lib/amiko-web.ts
18978
- var DEFAULT_PLATFORM_URL = "https://platform.heyamiko.com";
18979
- function baseUrl(auth) {
18980
- const url = auth.platformUrl || DEFAULT_PLATFORM_URL;
18981
- return url.replace(/\/$/, "");
18982
- }
18983
- function authHeader(auth) {
18984
- if (auth.twinToken)
18985
- return `Bearer ${auth.twinToken}`;
18986
- throw new Error("No twin token found. Place an .amiko.json file with amikoTwinToken in the current directory or any parent.");
18987
- }
18988
- async function amikoWebFetch(auth, path, opts = {}) {
18989
- const url = new URL(`${baseUrl(auth)}${path}`);
18990
- if (opts.query) {
18991
- for (const [k, v] of Object.entries(opts.query)) {
18992
- if (v !== undefined && v !== null)
18993
- url.searchParams.set(k, String(v));
18994
- }
18995
- }
18996
- const headers = {
18997
- Authorization: authHeader(auth),
18998
- ...opts.headers
18999
- };
19000
- let body;
19001
- if (opts.multipart) {
19002
- body = opts.multipart;
19003
- } else if (opts.rawBody) {
19004
- body = opts.rawBody;
19005
- if (opts.rawContentType)
19006
- headers["Content-Type"] = opts.rawContentType;
19007
- } else if (opts.body) {
19008
- body = JSON.stringify(opts.body);
19009
- headers["Content-Type"] = "application/json";
19010
- }
19011
- const controller = new AbortController;
19012
- const timeoutMs = opts.timeoutMs ?? 30000;
19013
- const timer = setTimeout(() => controller.abort(), timeoutMs);
19014
- let res;
19015
- try {
19016
- res = await fetch(url, {
19017
- method: opts.method ?? "GET",
19018
- headers,
19019
- body,
19020
- signal: controller.signal
19021
- });
19022
- } finally {
19023
- clearTimeout(timer);
19024
- }
19025
- if (!res.ok) {
19026
- const text = await res.text().catch(() => "");
19027
- const err = new Error(formatError(res.status, text, path));
19028
- err.status = res.status;
19029
- err.body = text;
19030
- throw err;
19031
- }
19032
- const ctype = res.headers.get("content-type") || "";
19033
- if (ctype.includes("application/json")) {
19034
- return await res.json();
19035
- }
19036
- return await res.text();
19037
- }
19038
- function formatError(status, body, path) {
19039
- const prefix = `${status} ${path}`;
19040
- if (status === 401) {
19041
- return `${prefix}: unauthorized. The stored token is invalid or expired; re-link this device to Amiko.`;
19042
- }
19043
- if (status === 403) {
19044
- return `${prefix}: forbidden. The current auth context does not have access to this resource.`;
19045
- }
19046
- if (!body)
19047
- return prefix;
19048
- const snippet = body.length > 500 ? `${body.slice(0, 500)}...` : body;
19049
- return `${prefix}: ${snippet}`;
19050
- }
19051
-
19052
19083
  // src/lib/balance.ts
19053
19084
  async function fetchCreditsBalance(auth) {
19054
19085
  if (!auth.twinToken)
@@ -19295,6 +19326,7 @@ function registerCreditsCommand(program2) {
19295
19326
  if (mppResult.credits?.balance)
19296
19327
  console.log(label("Balance", String(mppResult.credits.balance)));
19297
19328
  console.log(success("Credits topped up via MPP."));
19329
+ console.log(dim("Balance may take a moment to reflect on-chain confirmation — re-run `amiko credits balance` if it looks stale."));
19298
19330
  return;
19299
19331
  }
19300
19332
  console.log(heading("Top-up Summary"));
@@ -19339,6 +19371,7 @@ function registerCreditsCommand(program2) {
19339
19371
  }
19340
19372
  console.log("");
19341
19373
  console.log(success("Credits topped up."));
19374
+ console.log(dim("Balance may take a moment to reflect on-chain confirmation — re-run `amiko credits balance` if it looks stale."));
19342
19375
  } catch (e) {
19343
19376
  spinner.stop();
19344
19377
  console.error(error(e instanceof Error ? e.message : "Top-up failed"));
@@ -19453,14 +19486,15 @@ function registerSwapCommand(program2) {
19453
19486
  console.log(label("Route", dim(route)));
19454
19487
  }
19455
19488
  console.log("");
19456
- console.log(dim(`To execute: amiko wallets swap send ${amountStr} ${fromToken} ${toToken}`));
19489
+ console.log(dim(`To execute: amiko wallets swap send ${amountStr} ${fromToken} ${toToken}` + `
19490
+ (uses your default Solana wallet; pass --wallet <address> to override)`));
19457
19491
  } catch (err) {
19458
19492
  spinner.stop();
19459
19493
  console.error(error(err instanceof Error ? err.message : "Failed to get quote"));
19460
19494
  process.exit(1);
19461
19495
  }
19462
19496
  });
19463
- cmd.command("send").description("Execute a token swap on Solana via Crossmint wallet").argument("<amount>", "Amount to swap (e.g., 5, 0.1)").argument("<from>", "Input token (sol, usdc, usdt, amiko, bonk, or mint address)").argument("<to>", "Output token (sol, usdc, usdt, amiko, bonk, or mint address)").option("--wallet <address>", "Wallet address (uses config wallet by default)").option("--slippage <bps>", "Slippage tolerance in basis points", "50").option("--yes", "Skip confirmation").action(async (amountStr, fromToken, toToken, opts) => {
19497
+ cmd.command("send").description("Execute a token swap on Solana via Crossmint wallet").argument("<amount>", "Amount to swap (e.g., 5, 0.1)").argument("<from>", "Input token (sol, usdc, usdt, amiko, bonk, or mint address)").argument("<to>", "Output token (sol, usdc, usdt, amiko, bonk, or mint address)").option("--wallet <address>", "Solana wallet to swap from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if none)").option("--slippage <bps>", "Slippage tolerance in basis points", "50").option("--yes", "Skip confirmation").action(async (amountStr, fromToken, toToken, opts) => {
19464
19498
  try {
19465
19499
  let auth;
19466
19500
  try {
@@ -19469,9 +19503,11 @@ function registerSwapCommand(program2) {
19469
19503
  console.error(error(e instanceof Error ? e.message : "auth failed"));
19470
19504
  process.exit(1);
19471
19505
  }
19472
- const wallet = opts.wallet;
19473
- if (!wallet) {
19474
- console.error(error("No Solana wallet specified. Provide --wallet <address>."));
19506
+ let wallet;
19507
+ try {
19508
+ wallet = await resolveWalletAddress(auth, opts.wallet, "solana");
19509
+ } catch (e) {
19510
+ console.error(error(e instanceof Error ? e.message : String(e)));
19475
19511
  process.exit(1);
19476
19512
  }
19477
19513
  const input = resolveToken(fromToken);
@@ -19748,12 +19784,12 @@ async function reportHttpError(res, label2) {
19748
19784
  // src/commands/search.ts
19749
19785
  var TREASURY = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
19750
19786
  function registerSearchCommand(program2) {
19751
- 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("--wallet <address>", "Solana wallet address to pay from (required)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
19787
+ 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("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if you don't have one)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
19752
19788
  await requireApproval({
19753
19789
  cost: `1 ${opts.token || "AMIKO"}`,
19754
19790
  summary: `X/Twitter search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
19755
19791
  yes: opts.yes,
19756
- commandExample: `amiko market search ${JSON.stringify(query)}${opts.token && opts.token !== "AMIKO" ? ` --token ${opts.token}` : ""}`
19792
+ commandExample: `amiko markets search ${JSON.stringify(query)}${opts.token && opts.token !== "AMIKO" ? ` --token ${opts.token}` : ""}`
19757
19793
  });
19758
19794
  const spinner = ora("X Search...").start();
19759
19795
  try {
@@ -19770,10 +19806,12 @@ function registerSearchCommand(program2) {
19770
19806
  const token = opts.token || "AMIKO";
19771
19807
  const chain = "solana";
19772
19808
  const amount2 = token === "AMIKO" ? "1" : "0.01";
19773
- const wallet = opts.wallet;
19774
- if (!wallet) {
19809
+ let wallet;
19810
+ try {
19811
+ wallet = await resolveWalletAddress(auth, opts.wallet, "solana");
19812
+ } catch (e) {
19775
19813
  spinner.stop();
19776
- console.error(error("Provide --wallet <address> to pay from."));
19814
+ console.error(error(e instanceof Error ? e.message : String(e)));
19777
19815
  process.exit(1);
19778
19816
  }
19779
19817
  spinner.text = `Paying ${amount2} ${token} (${chain})...`;
@@ -19801,7 +19839,8 @@ function registerSearchCommand(program2) {
19801
19839
  "Content-Type": "application/json",
19802
19840
  "X-Payment-Tx": txHash,
19803
19841
  "X-Payment-Amount": amount2,
19804
- "X-Payment-Token": token
19842
+ "X-Payment-Token": token,
19843
+ "X-Twin-Id": auth.twinId
19805
19844
  }
19806
19845
  });
19807
19846
  spinner.stop();
@@ -19847,12 +19886,12 @@ function registerSearchCommand(program2) {
19847
19886
  // src/commands/image.ts
19848
19887
  var TREASURY2 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
19849
19888
  function registerImageCommand(program2) {
19850
- 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("--wallet <address>", "Solana wallet address to pay from (required)").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
19889
+ 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("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if you don't have one)").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
19851
19890
  await requireApproval({
19852
19891
  cost: "5 AMIKO",
19853
19892
  summary: `Generate image: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
19854
19893
  yes: opts.yes,
19855
- commandExample: `amiko market image ${JSON.stringify(prompt)}`
19894
+ commandExample: `amiko markets image ${JSON.stringify(prompt)}`
19856
19895
  });
19857
19896
  const isTTY = process.stderr.isTTY;
19858
19897
  const spinner = isTTY ? ora("Generating image...").start() : null;
@@ -19893,10 +19932,12 @@ function registerImageCommand(program2) {
19893
19932
  process.exit(1);
19894
19933
  }
19895
19934
  const mppBase2 = (config2.apiBase || "https://mpp.amikomarkets.com").replace(/\/$/, "");
19896
- const wallet = opts.wallet;
19897
- if (!wallet) {
19935
+ let wallet;
19936
+ try {
19937
+ wallet = await resolveWalletAddress(auth, opts.wallet, "solana");
19938
+ } catch (e) {
19898
19939
  stopProgress();
19899
- console.error(error("Provide --wallet <address> to pay from."));
19940
+ console.error(error(e instanceof Error ? e.message : String(e)));
19900
19941
  process.exit(1);
19901
19942
  }
19902
19943
  setStatus("Paying 5 AMIKO...");
@@ -19922,7 +19963,8 @@ function registerImageCommand(program2) {
19922
19963
  method: "POST",
19923
19964
  headers: {
19924
19965
  "Content-Type": "application/json",
19925
- "X-Payment-Tx": txHash
19966
+ "X-Payment-Tx": txHash,
19967
+ "X-Twin-Id": auth.twinId
19926
19968
  },
19927
19969
  body: JSON.stringify({
19928
19970
  prompt,
@@ -19956,7 +19998,7 @@ function registerImageCommand(program2) {
19956
19998
  console.log(label("URL", img.url));
19957
19999
  } else if (img.b64_json) {
19958
20000
  console.log(label("Format", "base64 (inline image data)"));
19959
- console.log(dim(` ${img.b64_json.length.toLocaleString()} chars — save with: amiko market image "..." --raw | jq -r '.images[0].b64_json' | base64 -d > image.png`));
20001
+ console.log(dim(` ${img.b64_json.length.toLocaleString()} chars — save with: amiko markets image "..." --raw | jq -r '.images[0].b64_json' | base64 -d > image.png`));
19960
20002
  }
19961
20003
  if (img.revised_prompt && img.revised_prompt !== prompt) {
19962
20004
  console.log(label("Revised", dim(img.revised_prompt)));
@@ -19986,12 +20028,12 @@ async function payAmiko(auth, wallet, amount2) {
19986
20028
  }
19987
20029
  function registerAmazonCommand(program2) {
19988
20030
  const cmd = program2.command("amazon").description("Search and buy products on Amazon — paid with AMIKO");
19989
- cmd.command("search <query>").description("Search Amazon products — 1 AMIKO").option("--limit <n>", "Number of results", "5").option("--wallet <address>", "Solana wallet address to pay from (required)").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
20031
+ cmd.command("search <query>").description("Search Amazon products — 1 AMIKO").option("--limit <n>", "Number of results", "5").option("--wallet <address>", "Wallet to pay from (defaults to your Solana wallet — run `amiko wallets create --chain solana` if you don't have one)").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
19990
20032
  await requireApproval({
19991
20033
  cost: "1 AMIKO",
19992
20034
  summary: `Amazon product search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
19993
20035
  yes: opts.yes,
19994
- commandExample: `amiko market amazon search ${JSON.stringify(query)}`
20036
+ commandExample: `amiko markets amazon search ${JSON.stringify(query)}`
19995
20037
  });
19996
20038
  const spinner = ora("Searching Amazon...").start();
19997
20039
  try {
@@ -20005,10 +20047,12 @@ function registerAmazonCommand(program2) {
20005
20047
  process.exit(1);
20006
20048
  }
20007
20049
  const mppBase2 = (config2.apiBase || "https://mpp.amikomarkets.com").replace(/\/$/, "");
20008
- const wallet = opts.wallet;
20009
- if (!wallet) {
20050
+ let wallet;
20051
+ try {
20052
+ wallet = await resolveWalletAddress(auth, opts.wallet, "solana");
20053
+ } catch (e) {
20010
20054
  spinner.stop();
20011
- console.error(error("Provide --wallet <address>."));
20055
+ console.error(error(e instanceof Error ? e.message : String(e)));
20012
20056
  process.exit(1);
20013
20057
  }
20014
20058
  spinner.text = "Paying 1 AMIKO...";
@@ -20018,7 +20062,7 @@ function registerAmazonCommand(program2) {
20018
20062
  q: `site:amazon.com ${query}`
20019
20063
  });
20020
20064
  const res = await fetch(`${mppBase2}/v1/search/google_shopping?${params}`, {
20021
- headers: { "X-Payment-Tx": txHash }
20065
+ headers: { "X-Payment-Tx": txHash, "X-Twin-Id": auth.twinId }
20022
20066
  });
20023
20067
  spinner.stop();
20024
20068
  if (!res.ok) {
@@ -20166,7 +20210,8 @@ async function payAndCall(mppBase2, method, path2, auth, wallet, amountOrAuto, b
20166
20210
  throw new Error("Payment sent but no tx hash returned.");
20167
20211
  const headers = {
20168
20212
  "Content-Type": "application/json",
20169
- "X-Payment-Tx": txHash
20213
+ "X-Payment-Tx": txHash,
20214
+ "X-Twin-Id": auth.twinId
20170
20215
  };
20171
20216
  const fetchOpts = { method, headers };
20172
20217
  if (body && method !== "GET") {
@@ -20178,12 +20223,12 @@ async function payAndCall(mppBase2, method, path2, auth, wallet, amountOrAuto, b
20178
20223
  }
20179
20224
  function registerServicesCommand(program2) {
20180
20225
  const svc = program2.command("service").alias("svc").description("Call any MPP service — pays with AMIKO tokens");
20181
- svc.command("tts <voiceId> <text>").description("Text-to-speech — 1 AMIKO").option("--raw", "Output raw response").option("--wallet <address>", "Solana wallet address to pay from (required)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (voiceId, text, opts) => {
20226
+ 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) => {
20182
20227
  await requireApproval({
20183
20228
  cost: "1 AMIKO",
20184
20229
  summary: `Text-to-speech (voice ${voiceId}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
20185
20230
  yes: opts.yes,
20186
- commandExample: `amiko market service tts ${voiceId} ${JSON.stringify(text)}`
20231
+ commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}`
20187
20232
  });
20188
20233
  await callService({
20189
20234
  name: "Text-to-Speech",
@@ -20194,12 +20239,12 @@ function registerServicesCommand(program2) {
20194
20239
  opts
20195
20240
  });
20196
20241
  });
20197
- svc.command("stt <audioUrl>").description("Speech-to-text — 1 AMIKO").option("--raw", "Output raw response").option("--wallet <address>", "Solana wallet address to pay from (required)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (audioUrl, opts) => {
20242
+ svc.command("stt <audioUrl>").description("Speech-to-text — 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 (audioUrl, opts) => {
20198
20243
  await requireApproval({
20199
20244
  cost: "1 AMIKO",
20200
20245
  summary: `Speech-to-text on ${audioUrl}`,
20201
20246
  yes: opts.yes,
20202
- commandExample: `amiko market service stt ${JSON.stringify(audioUrl)}`
20247
+ commandExample: `amiko markets service stt ${JSON.stringify(audioUrl)}`
20203
20248
  });
20204
20249
  await callService({
20205
20250
  name: "Speech-to-Text",
@@ -20228,12 +20273,12 @@ function registerServicesCommand(program2) {
20228
20273
  console.log(JSON.stringify(data, null, 2));
20229
20274
  }
20230
20275
  });
20231
- svc.command("chat <message>").description("AI chat — 1 AMIKO").option("--model <model>", "Model to use").option("--raw", "Output raw response").option("--wallet <address>", "Solana wallet address to pay from (required)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (message, opts) => {
20276
+ svc.command("chat <message>").description("AI chat — 1 AMIKO").option("--model <model>", "Model to use").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 (message, opts) => {
20232
20277
  await requireApproval({
20233
20278
  cost: "1 AMIKO",
20234
20279
  summary: `AI chat${opts.model ? ` (model ${opts.model})` : ""}: "${message.slice(0, 60)}${message.length > 60 ? "…" : ""}"`,
20235
20280
  yes: opts.yes,
20236
- commandExample: `amiko market service chat ${JSON.stringify(message)}${opts.model ? ` --model ${opts.model}` : ""}`
20281
+ commandExample: `amiko markets service chat ${JSON.stringify(message)}${opts.model ? ` --model ${opts.model}` : ""}`
20237
20282
  });
20238
20283
  const body = { message };
20239
20284
  if (opts.model)
@@ -20247,13 +20292,13 @@ function registerServicesCommand(program2) {
20247
20292
  opts
20248
20293
  });
20249
20294
  });
20250
- 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("--wallet <address>", "Solana wallet address to pay from (required)").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (method, path2, bodyStr, opts) => {
20295
+ 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("--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 (method, path2, bodyStr, opts) => {
20251
20296
  const costLabel = opts.usdc ? `${opts.usdc} USDC` : opts.amiko ? `${opts.amiko} AMIKO` : "auto-priced AMIKO (check /v1/ list for endpoint price first)";
20252
20297
  await requireApproval({
20253
20298
  cost: costLabel,
20254
20299
  summary: `${method.toUpperCase()} ${path2}`,
20255
20300
  yes: opts.yes,
20256
- commandExample: `amiko market service call ${method.toUpperCase()} ${path2}${bodyStr ? ` ${JSON.stringify(bodyStr)}` : ""}${opts.amiko ? ` --amiko ${opts.amiko}` : ""}${opts.usdc ? ` --usdc ${opts.usdc}` : ""}`
20301
+ commandExample: `amiko markets service call ${method.toUpperCase()} ${path2}${bodyStr ? ` ${JSON.stringify(bodyStr)}` : ""}${opts.amiko ? ` --amiko ${opts.amiko}` : ""}${opts.usdc ? ` --usdc ${opts.usdc}` : ""}`
20257
20302
  });
20258
20303
  let body;
20259
20304
  if (bodyStr) {
@@ -20312,10 +20357,12 @@ async function callService(params) {
20312
20357
  process.exit(1);
20313
20358
  }
20314
20359
  const mppBase2 = (config2.apiBase || "https://mpp.amikomarkets.com").replace(/\/$/, "");
20315
- const wallet = params.opts.wallet;
20316
- if (!wallet) {
20360
+ let wallet;
20361
+ try {
20362
+ wallet = await resolveWalletAddress(auth, params.opts.wallet, "solana");
20363
+ } catch (e) {
20317
20364
  spinner.stop();
20318
- console.error(error("Provide --wallet <address>."));
20365
+ console.error(error(e instanceof Error ? e.message : String(e)));
20319
20366
  process.exit(1);
20320
20367
  }
20321
20368
  const token = params.usdc ? "USDC" : "AMIKO";
@@ -20605,6 +20652,18 @@ function detectMime(buffer, name) {
20605
20652
  }
20606
20653
 
20607
20654
  // src/commands/docs.ts
20655
+ function fmtBytes(n) {
20656
+ if (!n || n <= 0)
20657
+ return "-";
20658
+ const units = ["B", "KB", "MB", "GB"];
20659
+ let v = n;
20660
+ let i = 0;
20661
+ while (v >= 1024 && i < units.length - 1) {
20662
+ v /= 1024;
20663
+ i++;
20664
+ }
20665
+ return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
20666
+ }
20608
20667
  function twinFromFlag(opts) {
20609
20668
  const config2 = loadConfig();
20610
20669
  const auth = resolveAuth();
@@ -20618,18 +20677,6 @@ function twinFromFlag(opts) {
20618
20677
  throw e;
20619
20678
  }
20620
20679
  }
20621
- function fmtBytes(n) {
20622
- if (!n || n <= 0)
20623
- return "-";
20624
- const units = ["B", "KB", "MB", "GB"];
20625
- let v = n;
20626
- let i = 0;
20627
- while (v >= 1024 && i < units.length - 1) {
20628
- v /= 1024;
20629
- i++;
20630
- }
20631
- return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
20632
- }
20633
20680
  function registerDocsCommand(program2) {
20634
20681
  program2.command("list").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) => {
20635
20682
  const { twinId, auth } = twinFromFlag(opts);
@@ -20647,15 +20694,15 @@ function registerDocsCommand(program2) {
20647
20694
  [
20648
20695
  dim("ID"),
20649
20696
  dim("NAME"),
20650
- dim("SIZE"),
20697
+ dim("TYPE"),
20651
20698
  dim("STATUS"),
20652
20699
  dim("CREATED")
20653
20700
  ],
20654
20701
  ...d.docs.map((doc2) => [
20655
- doc2.id.slice(0, 8),
20702
+ doc2.id,
20656
20703
  doc2.title || doc2.filename,
20657
- fmtBytes(doc2.file_size),
20658
- doc2.status || "-",
20704
+ doc2.file_type || "-",
20705
+ doc2.is_processed ? "processed" : doc2.is_parsed ? "parsed" : "pending",
20659
20706
  doc2.created_at?.slice(0, 10) || "-"
20660
20707
  ])
20661
20708
  ];
@@ -20936,20 +20983,20 @@ function registerFriendsCommand(program2) {
20936
20983
  const data = await amikoWebFetch(auth, "/api/friends", { query: { type: opts.type } });
20937
20984
  spinner?.stop();
20938
20985
  renderOutput(data, (d) => {
20939
- const items = d.items ?? [];
20986
+ const items = d.friends ?? [];
20940
20987
  if (items.length === 0) {
20941
20988
  console.log(dim("No friends."));
20942
20989
  return;
20943
20990
  }
20944
20991
  console.log(heading(`Friends (${items.length})`));
20945
20992
  const rows = [
20946
- [dim("ID"), dim("NAME"), dim("HANDLE"), dim("TYPE"), dim("STATUS")],
20993
+ [dim("ID"), dim("NAME"), dim("HANDLE"), dim("TYPE"), dim("SINCE")],
20947
20994
  ...items.map((f) => [
20948
- f.id.slice(0, 8),
20995
+ f.friendship_id,
20949
20996
  f.friend.name ?? "-",
20950
20997
  f.friend.handle ?? "-",
20951
20998
  f.friend.type,
20952
- f.status
20999
+ f.since?.slice(0, 10) ?? "-"
20953
21000
  ])
20954
21001
  ];
20955
21002
  console.log(table(rows));
@@ -20993,6 +21040,375 @@ function registerFriendsCommand(program2) {
20993
21040
  process.exit(1);
20994
21041
  }
20995
21042
  });
21043
+ program2.command("requests").description("List pending friend requests (incoming and outgoing)").option("--direction <incoming|outgoing|all>", "Filter direction", "all").option("--json", "Output as JSON").action(async (opts) => {
21044
+ const auth = resolveAuth();
21045
+ const spinner = opts.json ? null : ora("Loading requests...").start();
21046
+ try {
21047
+ const data = await amikoWebFetch(auth, "/api/friends/requests");
21048
+ spinner?.stop();
21049
+ renderOutput(data, (d) => {
21050
+ const dir = opts.direction;
21051
+ const showIn = dir === "all" || dir === "incoming";
21052
+ const showOut = dir === "all" || dir === "outgoing";
21053
+ const incoming = d.incoming ?? [];
21054
+ const outgoing = d.outgoing ?? [];
21055
+ if (showIn) {
21056
+ console.log(heading(`Incoming (${incoming.length})`));
21057
+ if (incoming.length === 0) {
21058
+ console.log(dim(" none"));
21059
+ } else {
21060
+ const rows = [
21061
+ [dim("FRIENDSHIP"), dim("FROM"), dim("HANDLE"), dim("SENT")],
21062
+ ...incoming.map((r) => [
21063
+ r.friendship_id,
21064
+ r.user.name ?? "-",
21065
+ r.user.handle ?? "-",
21066
+ r.sent_at?.slice(0, 10) ?? "-"
21067
+ ])
21068
+ ];
21069
+ console.log(table(rows));
21070
+ console.log(dim(`
21071
+ Accept with: amiko friends accept <friendshipId>`));
21072
+ }
21073
+ }
21074
+ if (showOut) {
21075
+ if (showIn)
21076
+ console.log();
21077
+ console.log(heading(`Outgoing (${outgoing.length})`));
21078
+ if (outgoing.length === 0) {
21079
+ console.log(dim(" none"));
21080
+ } else {
21081
+ const rows = [
21082
+ [dim("FRIENDSHIP"), dim("TO"), dim("HANDLE"), dim("SENT")],
21083
+ ...outgoing.map((r) => [
21084
+ r.friendship_id,
21085
+ r.user.name ?? "-",
21086
+ r.user.handle ?? "-",
21087
+ r.sent_at?.slice(0, 10) ?? "-"
21088
+ ])
21089
+ ];
21090
+ console.log(table(rows));
21091
+ }
21092
+ }
21093
+ }, { json: opts.json });
21094
+ } catch (e) {
21095
+ spinner?.stop();
21096
+ console.error(error(e instanceof Error ? e.message : String(e)));
21097
+ process.exit(1);
21098
+ }
21099
+ });
21100
+ const reports = program2.command("reports").description("Friend matching reports");
21101
+ reports.command("list").description("List friend matching reports").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (1-50)", "20").option("--json", "Output as JSON").action(async (opts) => {
21102
+ const auth = resolveAuth();
21103
+ const spinner = opts.json ? null : ora("Loading reports...").start();
21104
+ try {
21105
+ const data = await amikoWebFetch(auth, "/api/friends/reports", {
21106
+ query: { page: opts.page, limit: opts.limit }
21107
+ });
21108
+ spinner?.stop();
21109
+ renderOutput(data, (d) => {
21110
+ const items = d.items ?? [];
21111
+ if (items.length === 0) {
21112
+ console.log(dim("No reports."));
21113
+ return;
21114
+ }
21115
+ console.log(heading(`Reports (${items.length}/${d.total ?? items.length})`));
21116
+ const rows = [
21117
+ [
21118
+ dim("ID"),
21119
+ dim("TYPE"),
21120
+ dim("STATUS"),
21121
+ dim("SCORE"),
21122
+ dim("WITH"),
21123
+ dim("CREATED")
21124
+ ],
21125
+ ...items.map((r) => {
21126
+ const other = r.respondent ?? r.initiator;
21127
+ return [
21128
+ r.id,
21129
+ r.report_type ?? "-",
21130
+ r.status ?? "-",
21131
+ r.score_preview != null ? String(r.score_preview) : "-",
21132
+ other?.name ?? other?.handle ?? "-",
21133
+ r.created_at?.slice(0, 10) ?? "-"
21134
+ ];
21135
+ })
21136
+ ];
21137
+ console.log(table(rows));
21138
+ }, { json: opts.json });
21139
+ } catch (e) {
21140
+ spinner?.stop();
21141
+ console.error(error(e instanceof Error ? e.message : String(e)));
21142
+ process.exit(1);
21143
+ }
21144
+ });
21145
+ reports.command("view <id>").description("View a friend matching report").option("--json", "Output as JSON").action(async (id, opts) => {
21146
+ const auth = resolveAuth();
21147
+ const spinner = opts.json ? null : ora("Loading report...").start();
21148
+ try {
21149
+ const data = await amikoWebFetch(auth, `/api/friends/reports/${encodeURIComponent(id)}`);
21150
+ spinner?.stop();
21151
+ renderOutput(data, (d) => {
21152
+ const r = d.report;
21153
+ console.log(heading(`Report ${r.id}`));
21154
+ if (r.report_type)
21155
+ console.log(label("Type", r.report_type));
21156
+ if (r.status)
21157
+ console.log(label("Status", r.status));
21158
+ const other = r.respondent ?? r.initiator;
21159
+ if (other)
21160
+ console.log(label("With", other.name ?? other.handle ?? other.id));
21161
+ if (r.created_at)
21162
+ console.log(label("Created", r.created_at.slice(0, 10)));
21163
+ if (r.teaser_text) {
21164
+ console.log();
21165
+ console.log(r.teaser_text);
21166
+ }
21167
+ if (r.report_json) {
21168
+ console.log();
21169
+ console.log(heading("Report"));
21170
+ console.log(typeof r.report_json === "string" ? r.report_json : JSON.stringify(r.report_json, null, 2));
21171
+ }
21172
+ }, { json: opts.json });
21173
+ } catch (e) {
21174
+ spinner?.stop();
21175
+ console.error(error(e instanceof Error ? e.message : String(e)));
21176
+ process.exit(1);
21177
+ }
21178
+ });
21179
+ reports.command("pending").description("List pending-consent report requests (awaiting my consent + awaiting theirs)").option("--limit <n>", "Page size (1-50)", "50").option("--json", "Output as JSON").action(async (opts) => {
21180
+ const auth = resolveAuth();
21181
+ const me = auth.userId;
21182
+ if (!me && !opts.json) {
21183
+ console.error(error("Current user id unknown — cannot split requests. Run `amiko accounts` to check."));
21184
+ process.exit(1);
21185
+ }
21186
+ const spinner = opts.json ? null : ora("Loading...").start();
21187
+ try {
21188
+ const data = await amikoWebFetch(auth, "/api/friends/reports", {
21189
+ query: { page: "1", limit: opts.limit }
21190
+ });
21191
+ spinner?.stop();
21192
+ const items = (data.items ?? []).filter((r) => r.status === "pending_consent");
21193
+ const incoming = items.filter((r) => r.respondent_id === me);
21194
+ const outgoing = items.filter((r) => r.initiator_id === me);
21195
+ renderOutput({ incoming, outgoing }, () => {
21196
+ console.log(heading(`Awaiting my consent (${incoming.length})`));
21197
+ if (incoming.length === 0) {
21198
+ console.log(dim(" none"));
21199
+ } else {
21200
+ const rows = [
21201
+ [
21202
+ dim("REPORT ID"),
21203
+ dim("TYPE"),
21204
+ dim("FROM"),
21205
+ dim("HANDLE"),
21206
+ dim("SENT")
21207
+ ],
21208
+ ...incoming.map((r) => [
21209
+ r.id,
21210
+ r.report_type ?? "-",
21211
+ r.initiator?.name ?? "-",
21212
+ r.initiator?.handle ?? "-",
21213
+ r.created_at?.slice(0, 10) ?? "-"
21214
+ ])
21215
+ ];
21216
+ console.log(table(rows));
21217
+ console.log(dim(`
21218
+ Consent with: amiko friends reports consent <reportId>`));
21219
+ }
21220
+ console.log();
21221
+ console.log(heading(`Awaiting their consent (${outgoing.length})`));
21222
+ if (outgoing.length === 0) {
21223
+ console.log(dim(" none"));
21224
+ } else {
21225
+ const rows = [
21226
+ [
21227
+ dim("REPORT ID"),
21228
+ dim("TYPE"),
21229
+ dim("TO"),
21230
+ dim("HANDLE"),
21231
+ dim("SENT")
21232
+ ],
21233
+ ...outgoing.map((r) => [
21234
+ r.id,
21235
+ r.report_type ?? "-",
21236
+ r.respondent?.name ?? "-",
21237
+ r.respondent?.handle ?? "-",
21238
+ r.created_at?.slice(0, 10) ?? "-"
21239
+ ])
21240
+ ];
21241
+ console.log(table(rows));
21242
+ console.log(dim(`
21243
+ Cancel with: amiko friends reports cancel <reportId>`));
21244
+ }
21245
+ }, { json: opts.json });
21246
+ } catch (e) {
21247
+ spinner?.stop();
21248
+ console.error(error(e instanceof Error ? e.message : String(e)));
21249
+ process.exit(1);
21250
+ }
21251
+ });
21252
+ reports.command("request").description("Request a friend matching report (notifies the other user to consent)").requiredOption("--id <userId>", "Target user id (must be an accepted friend)").requiredOption("--type <friend|romantic|career>", "Report type").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (opts) => {
21253
+ const allowed = ["friend", "romantic", "career"];
21254
+ if (!allowed.includes(opts.type)) {
21255
+ console.error(error(`--type must be one of: ${allowed.join(", ")}`));
21256
+ process.exit(1);
21257
+ }
21258
+ await confirmDestructive({
21259
+ action: `Request a ${opts.type} matching report with user ${opts.id} (they will be notified)`,
21260
+ yes: opts.yes,
21261
+ commandExample: `amiko friends reports request --id ${opts.id} --type ${opts.type}`
21262
+ });
21263
+ const auth = resolveAuth();
21264
+ const spinner = opts.json ? null : ora("Requesting report...").start();
21265
+ try {
21266
+ const data = await amikoWebFetch(auth, "/api/friends/reports", {
21267
+ method: "POST",
21268
+ body: { friend_id: opts.id, report_type: opts.type }
21269
+ });
21270
+ spinner?.stop();
21271
+ renderOutput(data, (d) => {
21272
+ if (d.report) {
21273
+ console.log(success(`Report requested (${d.report.report_type})`));
21274
+ console.log(label("Report ID", d.report.id));
21275
+ console.log(label("Status", d.report.status));
21276
+ console.log(dim(`
21277
+ The other user will be notified and must consent before the report is generated.`));
21278
+ }
21279
+ }, { json: opts.json });
21280
+ } catch (e) {
21281
+ spinner?.stop();
21282
+ console.error(error(e instanceof Error ? e.message : String(e)));
21283
+ process.exit(1);
21284
+ }
21285
+ });
21286
+ for (const action of ["consent", "cancel", "retry"]) {
21287
+ const desc = {
21288
+ consent: "Consent to a report request (respondent only)",
21289
+ cancel: "Cancel a pending report",
21290
+ retry: "Retry a failed report generation"
21291
+ };
21292
+ reports.command(`${action} <id>`).description(desc[action]).option("--json", "Output as JSON").action(async (id, opts) => {
21293
+ const auth = resolveAuth();
21294
+ const spinner = opts.json ? null : ora(`${action}...`).start();
21295
+ try {
21296
+ const data = await amikoWebFetch(auth, `/api/friends/reports/${encodeURIComponent(id)}`, { method: "PATCH", body: { action } });
21297
+ spinner?.stop();
21298
+ renderOutput(data, (d) => {
21299
+ console.log(success(`${action} ok`));
21300
+ if (d.status)
21301
+ console.log(label("Status", d.status));
21302
+ }, { json: opts.json });
21303
+ } catch (e) {
21304
+ spinner?.stop();
21305
+ console.error(error(e instanceof Error ? e.message : String(e)));
21306
+ process.exit(1);
21307
+ }
21308
+ });
21309
+ }
21310
+ program2.command("find").description("Find matches for a custom relationship description (LLM-generated, on-demand)").requiredOption("--relationship <text>", 'Relationship description, e.g. "cofounder who is good at design"').option("--rationale <text>", "Optional extra context for the matcher").option("--limit <n>", "Max results (1-20)", "10").option("--json", "Output as JSON").action(async (opts) => {
21311
+ const limitNum = Math.max(1, Math.min(20, Number(opts.limit) || 10));
21312
+ const auth = resolveAuth();
21313
+ const spinner = opts.json ? null : ora("Finding matches (may take ~10s)...").start();
21314
+ try {
21315
+ const data = await amikoWebFetch(auth, "/api/user/personality-profile/matches", {
21316
+ method: "POST",
21317
+ body: {
21318
+ relationship_type: opts.relationship,
21319
+ rationale: opts.rationale,
21320
+ limit: limitNum
21321
+ }
21322
+ });
21323
+ spinner?.stop();
21324
+ renderOutput(data, (d) => {
21325
+ const matches = d.matches ?? [];
21326
+ if (matches.length === 0) {
21327
+ console.log(dim("No matches."));
21328
+ if (d.no_match_reason)
21329
+ console.log(dim(d.no_match_reason));
21330
+ return;
21331
+ }
21332
+ console.log(heading(`Matches for "${opts.relationship}" (${matches.length})`));
21333
+ const rows = [
21334
+ [
21335
+ dim("USER"),
21336
+ dim("NAME"),
21337
+ dim("SCORE"),
21338
+ dim("STATUS"),
21339
+ dim("PAIRING")
21340
+ ],
21341
+ ...matches.map((m) => [
21342
+ m.user_id,
21343
+ m.display_name ?? "-",
21344
+ m.score.toFixed(3),
21345
+ m.friendship_status ?? "none",
21346
+ m.pairing_label ?? "-"
21347
+ ])
21348
+ ];
21349
+ console.log(table(rows));
21350
+ for (const m of matches) {
21351
+ if (m.compatibility_highlights?.length) {
21352
+ console.log();
21353
+ console.log(label(m.display_name ?? m.user_id, m.compatibility_highlights.join("; ")));
21354
+ }
21355
+ }
21356
+ }, { json: opts.json });
21357
+ } catch (e) {
21358
+ spinner?.stop();
21359
+ console.error(error(e instanceof Error ? e.message : String(e)));
21360
+ process.exit(1);
21361
+ }
21362
+ });
21363
+ program2.command("matches").description("List friend matches").option("--limit <n>", "Max results (1-50)", "10").option("--offset <n>", "Offset", "0").option("--relationship-type <type>", "Filter by relationship type").option("--dimension <dimension>", "Filter by match dimension").option("--json", "Output as JSON").action(async (opts) => {
21364
+ const auth = resolveAuth();
21365
+ const spinner = opts.json ? null : ora("Loading matches...").start();
21366
+ try {
21367
+ const data = await amikoWebFetch(auth, "/api/friends/matches", {
21368
+ query: {
21369
+ limit: opts.limit,
21370
+ offset: opts.offset,
21371
+ relationship_type: opts.relationshipType,
21372
+ dimension: opts.dimension
21373
+ }
21374
+ });
21375
+ spinner?.stop();
21376
+ renderOutput(data, (d) => {
21377
+ const items = d.items ?? [];
21378
+ if (items.length === 0) {
21379
+ console.log(dim("No matches."));
21380
+ return;
21381
+ }
21382
+ console.log(heading(`Matches (${items.length}/${d.total ?? items.length})`));
21383
+ const rows = [
21384
+ [
21385
+ dim("USER"),
21386
+ dim("NAME"),
21387
+ dim("REL"),
21388
+ dim("DIM"),
21389
+ dim("SCORE"),
21390
+ dim("STATUS")
21391
+ ],
21392
+ ...items.map((m) => [
21393
+ m.matched_user_id,
21394
+ m.display_name ?? m.handle ?? "-",
21395
+ m.relationship_type ?? "-",
21396
+ m.match_dimension ?? "-",
21397
+ m.score != null ? String(m.score) : "-",
21398
+ m.friendship_status ?? "-"
21399
+ ])
21400
+ ];
21401
+ console.log(table(rows));
21402
+ if (d.has_more)
21403
+ console.log(dim(`
21404
+ (more available)`));
21405
+ }, { json: opts.json });
21406
+ } catch (e) {
21407
+ spinner?.stop();
21408
+ console.error(error(e instanceof Error ? e.message : String(e)));
21409
+ process.exit(1);
21410
+ }
21411
+ });
20996
21412
  program2.command("remove <friendshipId>").description("Remove a friend").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (friendshipId, opts) => {
20997
21413
  await confirmDestructive({
20998
21414
  action: `Remove friend ${friendshipId}`,
@@ -21013,6 +21429,85 @@ function registerFriendsCommand(program2) {
21013
21429
  });
21014
21430
  }
21015
21431
 
21432
+ // src/commands/users.ts
21433
+ function registerUsersCommand(program2) {
21434
+ program2.command("search <query>").description("Search for users by name or handle").option("--limit <n>", "Max results (1-20)", "10").option("--cursor <cursor>", "Pagination cursor").option("--json", "Output as JSON").action(async (query, opts) => {
21435
+ const auth = resolveAuth();
21436
+ const spinner = opts.json ? null : ora("Searching users...").start();
21437
+ try {
21438
+ const data = await amikoWebFetch(auth, "/api/search", {
21439
+ query: {
21440
+ q: query,
21441
+ type: "people",
21442
+ limit: opts.limit,
21443
+ cursor: opts.cursor
21444
+ }
21445
+ });
21446
+ spinner?.stop();
21447
+ renderOutput(data, (d) => {
21448
+ const items = d.users ?? [];
21449
+ if (items.length === 0) {
21450
+ console.log(dim("No users found."));
21451
+ return;
21452
+ }
21453
+ console.log(heading(`Users (${items.length})`));
21454
+ const rows = [
21455
+ [dim("ID"), dim("NAME"), dim("HANDLE")],
21456
+ ...items.map((u) => [
21457
+ u.id,
21458
+ u.name ?? "-",
21459
+ u.handle ?? "-"
21460
+ ])
21461
+ ];
21462
+ console.log(table(rows));
21463
+ if (d.hasMore && d.nextCursor) {
21464
+ console.log(dim(`
21465
+ Next cursor: ${d.nextCursor}`));
21466
+ }
21467
+ }, { json: opts.json });
21468
+ } catch (e) {
21469
+ spinner?.stop();
21470
+ console.error(error(e instanceof Error ? e.message : String(e)));
21471
+ process.exit(1);
21472
+ }
21473
+ });
21474
+ program2.command("profile <handle>").description("View a user's public profile").option("--json", "Output as JSON").action(async (handle, opts) => {
21475
+ const auth = resolveAuth();
21476
+ const spinner = opts.json ? null : ora("Loading profile...").start();
21477
+ try {
21478
+ const data = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`);
21479
+ spinner?.stop();
21480
+ renderOutput(data, (d) => {
21481
+ const u = d.user;
21482
+ console.log(heading(u.name ?? u.handle ?? u.id));
21483
+ console.log(label("ID", u.id));
21484
+ if (u.handle)
21485
+ console.log(label("Handle", u.handle));
21486
+ if (u.created_at)
21487
+ console.log(label("Joined", u.created_at.slice(0, 10)));
21488
+ if (u.last_seen_at)
21489
+ console.log(label("Last seen", u.last_seen_at.slice(0, 10)));
21490
+ if (u.twins && u.twins.length > 0) {
21491
+ console.log(heading(`
21492
+ Twins (${u.twins.length})`));
21493
+ for (const t of u.twins) {
21494
+ console.log(` ${t.name ?? "-"} ${dim(t.handle ?? t.id.slice(0, 8))}`);
21495
+ }
21496
+ }
21497
+ if (Array.isArray(u.mutual_friends) && u.mutual_friends.length > 0) {
21498
+ console.log(label("Mutual friends", String(u.mutual_friends.length)));
21499
+ } else if (typeof u.mutual_friends === "number") {
21500
+ console.log(label("Mutual friends", String(u.mutual_friends)));
21501
+ }
21502
+ }, { json: opts.json });
21503
+ } catch (e) {
21504
+ spinner?.stop();
21505
+ console.error(error(e instanceof Error ? e.message : String(e)));
21506
+ process.exit(1);
21507
+ }
21508
+ });
21509
+ }
21510
+
21016
21511
  // src/commands/feed.ts
21017
21512
  function registerFeedCommand(program2) {
21018
21513
  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) => {
@@ -21250,6 +21745,109 @@ function registerWalletsBalanceCommand(program2) {
21250
21745
  }
21251
21746
  });
21252
21747
  }
21748
+ function registerWalletsCreateCommand(program2) {
21749
+ program2.command("create").description("Create a new wallet for the twin on solana or base").requiredOption("--chain <chain>", "Chain to create the wallet on (solana | base)").option("--custodian <custodian>", "Custodian (crossmint | amiko). Defaults to crossmint; amiko is solana-only.", "crossmint").option("--json", "Output raw JSON").action(async (opts) => {
21750
+ const chain = opts.chain.toLowerCase();
21751
+ if (!["solana", "base"].includes(chain)) {
21752
+ console.error(error(`Unsupported chain '${opts.chain}'. Use solana or base.`));
21753
+ process.exit(1);
21754
+ }
21755
+ let auth;
21756
+ try {
21757
+ auth = requireAuth();
21758
+ } catch (e) {
21759
+ console.error(error(e instanceof Error ? e.message : String(e)));
21760
+ process.exit(1);
21761
+ }
21762
+ const spinner = opts.json ? null : ora(`Creating ${chain} wallet...`).start();
21763
+ try {
21764
+ const wallet = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets`, { method: "POST", body: { chain, custodian: opts.custodian } });
21765
+ spinner?.stop();
21766
+ if (opts.json) {
21767
+ console.log(JSON.stringify(wallet, null, 2));
21768
+ return;
21769
+ }
21770
+ console.log(success(`${chain} wallet created`));
21771
+ console.log("");
21772
+ console.log(label("Chain", `${wallet.chain} (${wallet.chain_type})`));
21773
+ if (wallet.name)
21774
+ console.log(label("Name", wallet.name));
21775
+ console.log(label("Address", wallet.wallet_address));
21776
+ console.log(label("Custodian", wallet.custodian ?? "-"));
21777
+ console.log("");
21778
+ console.log(dim(`Tip: paid commands (swap, markets search, image, amazon, services) will now use this wallet by default.`));
21779
+ } catch (e) {
21780
+ spinner?.stop();
21781
+ const msg = e instanceof Error ? e.message : "Failed to create wallet";
21782
+ if (/409|already/i.test(msg)) {
21783
+ console.error(error(`An active ${chain} wallet already exists. Run \`amiko wallets list\` to see it.`));
21784
+ } else {
21785
+ console.error(error(msg));
21786
+ }
21787
+ process.exit(1);
21788
+ }
21789
+ });
21790
+ }
21791
+ function registerWalletsTransferCommand(program2) {
21792
+ program2.command("transfer").description("Transfer tokens from a twin wallet to another address").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Amount to send (e.g., 0.5)").requiredOption("--token <symbol>", "Token symbol (sol, usdc, usdt, amiko, eth) or mint/contract address").option("--wallet <address>", "Source wallet (defaults to your Solana wallet)").option("--chain <chain>", "Preferred chain when resolving default wallet (solana | base)", "solana").option("--yes", "Skip confirmation").option("--json", "Output raw JSON").action(async (opts) => {
21793
+ let auth;
21794
+ try {
21795
+ auth = requireAuth();
21796
+ } catch (e) {
21797
+ console.error(error(e instanceof Error ? e.message : String(e)));
21798
+ process.exit(1);
21799
+ }
21800
+ const chain = opts.chain.toLowerCase();
21801
+ if (chain !== "solana" && chain !== "base") {
21802
+ console.error(error(`Unsupported chain '${opts.chain}'. Use solana or base.`));
21803
+ process.exit(1);
21804
+ }
21805
+ let wallet;
21806
+ try {
21807
+ wallet = await resolveWalletAddress(auth, opts.wallet, chain);
21808
+ } catch (e) {
21809
+ console.error(error(e instanceof Error ? e.message : String(e)));
21810
+ process.exit(1);
21811
+ }
21812
+ if (!opts.json) {
21813
+ console.log(heading("Transfer"));
21814
+ console.log("");
21815
+ console.log(label("From", wallet));
21816
+ console.log(label("To", opts.to));
21817
+ console.log(label("Amount", `${opts.amount} ${opts.token.toUpperCase()}`));
21818
+ console.log("");
21819
+ }
21820
+ await requireApproval({
21821
+ cost: `${chain} gas`,
21822
+ summary: `transfer ${opts.amount} ${opts.token.toUpperCase()} from ${truncAddr(wallet)} to ${truncAddr(opts.to)}`,
21823
+ yes: opts.yes,
21824
+ commandExample: `amiko wallets transfer --to ${opts.to} --amount ${opts.amount} --token ${opts.token}`
21825
+ });
21826
+ const spinner = opts.json ? null : ora("Sending transfer...").start();
21827
+ try {
21828
+ const data = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(wallet)}/transfer`, {
21829
+ method: "POST",
21830
+ body: { to: opts.to, amount: opts.amount, token: opts.token }
21831
+ });
21832
+ spinner?.stop();
21833
+ if (opts.json) {
21834
+ console.log(JSON.stringify(data, null, 2));
21835
+ return;
21836
+ }
21837
+ console.log(success("Transfer submitted"));
21838
+ console.log("");
21839
+ console.log(label("Status", data.crossmint.status));
21840
+ if (data.txHash)
21841
+ console.log(label("Tx", data.txHash));
21842
+ if (data.transaction?.id)
21843
+ console.log(label("Transaction", data.transaction.id));
21844
+ } catch (e) {
21845
+ spinner?.stop();
21846
+ console.error(error(e instanceof Error ? e.message : "Failed to transfer"));
21847
+ process.exit(1);
21848
+ }
21849
+ });
21850
+ }
21253
21851
  function registerWalletsListCommand(program2) {
21254
21852
  program2.command("list").alias("ls").description("List wallets for the authenticated twin").option("--json", "Output raw JSON").action(async (opts) => {
21255
21853
  let auth;
@@ -21304,26 +21902,29 @@ if (didOnboard)
21304
21902
  process.exit(0);
21305
21903
  var program2 = new Command;
21306
21904
  program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace services").version(PKG_VERSION);
21307
- var market = program2.command("market").description("MPP marketplace — discover and call paid AMIKO services");
21905
+ var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
21308
21906
  var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
21309
21907
  var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
21310
21908
  var docs = program2.command("docs").description("Manage twin RAG documents — list, upload, delete, presign");
21311
21909
  var voice = program2.command("voice").description("Manage twin voice — design, create, clone, reset");
21312
21910
  var avatar = program2.command("avatar").description("Manage twin avatar image");
21313
- var friends = program2.command("friends").description("Manage friendships — list, add, accept, remove");
21911
+ var friends = program2.command("friends").description("Manage friendships — list, requests, add, accept, remove, reports, matches");
21912
+ var users = program2.command("users").description("Search users and view public profiles");
21314
21913
  var post = program2.command("post").description("Create posts and comments on the feed");
21315
21914
  var composio = program2.command("composio").description("Manage Composio app connections");
21316
21915
  registerCreditsCommand(program2);
21317
- registerSearchCommand(market);
21318
- registerImageCommand(market);
21319
- registerAmazonCommand(market);
21916
+ registerSearchCommand(markets);
21917
+ registerImageCommand(markets);
21918
+ registerAmazonCommand(markets);
21919
+ registerWalletsCreateCommand(wallets);
21320
21920
  registerWalletsListCommand(wallets);
21321
21921
  registerWalletsBalanceCommand(wallets);
21922
+ registerWalletsTransferCommand(wallets);
21322
21923
  registerSwapCommand(wallets);
21323
21924
  registerBridgeCommand(wallets);
21324
21925
  registerConfigCommand(program2);
21325
- registerServicesCommand(market);
21326
- registerDiscoveryCommand(market);
21926
+ registerServicesCommand(markets);
21927
+ registerDiscoveryCommand(markets);
21327
21928
  registerUpdateCommand(program2);
21328
21929
  registerAccountsCommand(program2);
21329
21930
  registerTwinCommand(twin);
@@ -21331,6 +21932,7 @@ registerDocsCommand(docs);
21331
21932
  registerVoiceCommand(voice);
21332
21933
  registerAvatarCommand(avatar);
21333
21934
  registerFriendsCommand(friends);
21935
+ registerUsersCommand(users);
21334
21936
  registerFeedCommand(program2);
21335
21937
  registerPostCommand(post);
21336
21938
  registerComposioCommand(composio);