@heyamiko/amiko-cli 0.12.0-beta.8 → 0.14.0-beta.10

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
@@ -27031,12 +27031,19 @@ async function confirmDestructive(args) {
27031
27031
  process.exit(2);
27032
27032
  }
27033
27033
  async function requireApproval(args) {
27034
+ if (args.cost === undefined === (args.free === undefined)) {
27035
+ throw new Error("requireApproval: set exactly one of `cost` or `free`.");
27036
+ }
27034
27037
  if (args.yes)
27035
27038
  return;
27036
27039
  if (process.stdin.isTTY) {
27037
27040
  console.log("");
27038
27041
  console.log(`About to run: ${args.summary}`);
27039
- console.log(`Cost: ${args.cost}`);
27042
+ if (args.cost !== undefined) {
27043
+ console.log(`Cost: ${args.cost}`);
27044
+ } else {
27045
+ console.log(`Free — but this is a real outward action other people will see (${args.free}).`);
27046
+ }
27040
27047
  const ok = await promptYesNo("Proceed? [y/N] ");
27041
27048
  if (!ok) {
27042
27049
  console.log(dim("Cancelled."));
@@ -27046,10 +27053,17 @@ async function requireApproval(args) {
27046
27053
  }
27047
27054
  console.error(error(`Refusing to run without --yes (non-interactive shell detected).`));
27048
27055
  console.error("");
27049
- console.error(`This command will cost ${args.cost}.`);
27050
- console.error(`Action: ${args.summary}`);
27051
- console.error("");
27052
- console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
27056
+ if (args.cost !== undefined) {
27057
+ console.error(`This command will cost ${args.cost}.`);
27058
+ console.error(`Action: ${args.summary}`);
27059
+ console.error("");
27060
+ console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
27061
+ } else {
27062
+ console.error(`This action is FREE — do not quote a price or say "cost: 0". It is outward-facing: ${args.free}.`);
27063
+ console.error(`Action: ${args.summary}`);
27064
+ console.error("");
27065
+ console.error(dim(`Ask the owner to confirm the action itself (what will be sent and who sees it), then re-run with --yes:`));
27066
+ }
27053
27067
  console.error(dim(` ${args.commandExample} --yes`));
27054
27068
  process.exit(2);
27055
27069
  }
@@ -28306,15 +28320,60 @@ async function reportHttpError(res, label2) {
28306
28320
  console.error(String(pretty).slice(0, 2000));
28307
28321
  }
28308
28322
 
28309
- // src/commands/search.ts
28323
+ // src/lib/treasury-payment.ts
28310
28324
  var TREASURY = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
28325
+ var PAYMENT_TOKENS = ["AMIKO", "USDC", "USDT"];
28326
+ function normalizePaymentToken(raw) {
28327
+ const sym = (raw ?? "AMIKO").trim().toUpperCase();
28328
+ if (!PAYMENT_TOKENS.includes(sym)) {
28329
+ throw new Error(`Unsupported payment token "${raw}". Supported: ${PAYMENT_TOKENS.join(", ")}.`);
28330
+ }
28331
+ return sym;
28332
+ }
28333
+ async function quoteTokenAmount(auth, token, usd2) {
28334
+ if (token === "USDC" || token === "USDT") {
28335
+ return { amount: usd2.toFixed(6), priceUsd: 1 };
28336
+ }
28337
+ const priceUsd = await getTokenUsdPrice(auth, "AMIKO");
28338
+ if (!priceUsd || priceUsd <= 0) {
28339
+ throw new Error("Could not fetch AMIKO/USD price from the Amiko platform. Try again in a moment, or pay with --token USDT/USDC.");
28340
+ }
28341
+ const amount2 = (usd2 / priceUsd * 1.01).toFixed(6);
28342
+ return { amount: amount2, priceUsd };
28343
+ }
28344
+ async function payTreasury(auth, args) {
28345
+ const transferData = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(args.wallet)}/transfer`, {
28346
+ method: "POST",
28347
+ body: { to: TREASURY, amount: args.amount, token: args.token, chain: "solana" }
28348
+ });
28349
+ if (!transferData.txHash)
28350
+ throw new Error("Payment sent but no tx hash returned.");
28351
+ return transferData.txHash;
28352
+ }
28353
+
28354
+ // src/commands/search.ts
28355
+ var SEARCH_USD = 0.01;
28311
28356
  function registerSearchCommand(program2) {
28312
- 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) => {
28357
+ program2.command("search <query>").description("X Search — $0.01 per search, paid in AMIKO, USDC, or USDT").option("--raw", "Output raw JSON").option("--token <token>", "Payment token (AMIKO, USDC, USDT)", "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) => {
28358
+ let token;
28359
+ try {
28360
+ token = normalizePaymentToken(opts.token);
28361
+ } catch (e5) {
28362
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28363
+ process.exit(1);
28364
+ }
28365
+ let quote;
28366
+ try {
28367
+ quote = await quoteTokenAmount(resolveAuth(), token, SEARCH_USD);
28368
+ } catch (e5) {
28369
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28370
+ process.exit(1);
28371
+ }
28313
28372
  await requireApproval({
28314
- cost: `1 ${opts.token || "AMIKO"}`,
28373
+ cost: `${quote.amount} ${token} (≈ $${SEARCH_USD.toFixed(2)})`,
28315
28374
  summary: `X/Twitter search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
28316
28375
  yes: opts.yes,
28317
- commandExample: `amiko markets search ${JSON.stringify(query)}${opts.token && opts.token !== "AMIKO" ? ` --token ${opts.token}` : ""}`
28376
+ commandExample: `amiko markets search ${JSON.stringify(query)}${token !== "AMIKO" ? ` --token ${token}` : ""}`
28318
28377
  });
28319
28378
  const spinner = ora("X Search...").start();
28320
28379
  try {
@@ -28328,9 +28387,7 @@ function registerSearchCommand(program2) {
28328
28387
  process.exit(1);
28329
28388
  }
28330
28389
  const mppBase2 = (config2.apiBase || "https://mpp.amikomarkets.com").replace(/\/$/, "");
28331
- const token = opts.token || "AMIKO";
28332
- const chain = "solana";
28333
- const amount2 = token === "AMIKO" ? "1" : "0.01";
28390
+ const amount2 = quote.amount;
28334
28391
  let wallet;
28335
28392
  try {
28336
28393
  wallet = await resolveWalletAddress(auth, opts.wallet, "solana");
@@ -28339,19 +28396,10 @@ function registerSearchCommand(program2) {
28339
28396
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28340
28397
  process.exit(1);
28341
28398
  }
28342
- spinner.text = `Paying ${amount2} ${token} (${chain})...`;
28399
+ spinner.text = `Paying ${amount2} ${token} (solana)...`;
28343
28400
  let txHash;
28344
28401
  try {
28345
- const transferData = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(wallet)}/transfer`, {
28346
- method: "POST",
28347
- body: { to: TREASURY, amount: amount2, token, chain }
28348
- });
28349
- if (!transferData.txHash) {
28350
- spinner.stop();
28351
- console.error(error("Payment sent but no tx hash returned."));
28352
- process.exit(1);
28353
- }
28354
- txHash = transferData.txHash;
28402
+ txHash = await payTreasury(auth, { wallet, amount: amount2, token });
28355
28403
  } catch (e5) {
28356
28404
  spinner.stop();
28357
28405
  console.error(error(e5 instanceof Error ? e5.message : `${token} payment failed`));
@@ -28408,7 +28456,6 @@ function registerSearchCommand(program2) {
28408
28456
  }
28409
28457
 
28410
28458
  // src/commands/image.ts
28411
- var TREASURY2 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
28412
28459
  var IMAGE_MARKUP = 1.3;
28413
28460
  var IMAGE_PRICING = {
28414
28461
  "gpt-image-2": { low: { sq: 0.006, lg: 0.005 }, medium: { sq: 0.053, lg: 0.041 }, high: { sq: 0.211, lg: 0.165 } },
@@ -28426,7 +28473,7 @@ function computeImageUsd(model, quality, size3, n) {
28426
28473
  return Number((pricing[q][tier] * clamped * IMAGE_MARKUP).toFixed(6));
28427
28474
  }
28428
28475
  function registerImageCommand(program2) {
28429
- program2.command("image <prompt>").alias("img").description("Generate an image — pays in AMIKO at OpenAI pass-through pricing × 1.30 (cost varies by model/quality/size)").option("--size <size>", "Image size: 1024x1024, 1024x1536, 1536x1024", "1024x1024").option("--quality <q>", "Quality: low, medium, high, auto", "high").option("--model <model>", `Model: ${SUPPORTED_MODELS.join(", ")}`, DEFAULT_IMAGE_MODEL).option("--n <count>", "Number of images (1–4)", "1").option("--background <bg>", "Background: transparent, opaque, auto").option("--format <fmt>", "Output: png, webp, jpeg", "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) => {
28476
+ program2.command("image <prompt>").alias("img").description("Generate an image — pay in AMIKO, USDC, or USDT (cost varies by model/quality/size; exact amount quoted before charging)").option("--size <size>", "Image size: 1024x1024, 1024x1536, 1536x1024", "1024x1024").option("--quality <q>", "Quality: low, medium, high, auto", "high").option("--model <model>", `Model: ${SUPPORTED_MODELS.join(", ")}`, DEFAULT_IMAGE_MODEL).option("--n <count>", "Number of images (1–4)", "1").option("--background <bg>", "Background: transparent, opaque, auto").option("--format <fmt>", "Output: png, webp, jpeg", "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("--token <token>", "Payment token (AMIKO, USDC, USDT)", "AMIKO").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28430
28477
  if (!SUPPORTED_MODELS.includes(opts.model)) {
28431
28478
  console.error(error(`Unknown model "${opts.model}". Supported: ${SUPPORTED_MODELS.join(", ")}.`));
28432
28479
  process.exit(1);
@@ -28461,14 +28508,23 @@ function registerImageCommand(program2) {
28461
28508
  console.error(error(e5 instanceof Error ? e5.message : "auth failed"));
28462
28509
  process.exit(1);
28463
28510
  }
28464
- const amikoUsd = await getTokenUsdPrice(auth, "AMIKO");
28465
- if (!amikoUsd || amikoUsd <= 0) {
28466
- console.error(error("Could not fetch AMIKO/USD price from the Amiko platform. Try again in a moment, or top up via `amiko credits topup` and use credit auto-pay instead."));
28511
+ let payToken;
28512
+ try {
28513
+ payToken = normalizePaymentToken(opts.token);
28514
+ } catch (e5) {
28515
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28516
+ process.exit(1);
28517
+ }
28518
+ let quote;
28519
+ try {
28520
+ quote = await quoteTokenAmount(auth, payToken, usdCost);
28521
+ } catch (e5) {
28522
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28467
28523
  process.exit(1);
28468
28524
  }
28469
- const amikoAmount = (usdCost / amikoUsd).toFixed(6);
28525
+ const payAmount = quote.amount;
28470
28526
  await requireApproval({
28471
- cost: `${amikoAmount} AMIKO (≈ $${usdCost.toFixed(4)} @ $${amikoUsd.toFixed(6)}/AMIKO, ${opts.model} ${opts.quality} ${opts.size}${n > 1 ? ` ×${n}` : ""})`,
28527
+ cost: `${payAmount} ${payToken} (≈ $${usdCost.toFixed(4)} @ $${quote.priceUsd.toFixed(6)}/${payToken}, ${opts.model} ${opts.quality} ${opts.size}${n > 1 ? ` ×${n}` : ""})`,
28472
28528
  summary: `Generate image: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
28473
28529
  yes: opts.yes,
28474
28530
  commandExample: `amiko markets image ${JSON.stringify(prompt)}`
@@ -28512,19 +28568,10 @@ function registerImageCommand(program2) {
28512
28568
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
28513
28569
  process.exit(1);
28514
28570
  }
28515
- setStatus(`Paying ${amikoAmount} AMIKO...`);
28571
+ setStatus(`Paying ${payAmount} ${payToken}...`);
28516
28572
  let txHash;
28517
28573
  try {
28518
- const transferData = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(wallet)}/transfer`, {
28519
- method: "POST",
28520
- body: { to: TREASURY2, amount: amikoAmount, token: "AMIKO", chain: "solana" }
28521
- });
28522
- if (!transferData.txHash) {
28523
- stopProgress();
28524
- console.error(error("Payment sent but no tx hash returned."));
28525
- process.exit(1);
28526
- }
28527
- txHash = transferData.txHash;
28574
+ txHash = await payTreasury(auth, { wallet, amount: payAmount, token: payToken });
28528
28575
  } catch (e5) {
28529
28576
  stopProgress();
28530
28577
  console.error(error(e5 instanceof Error ? e5.message : "Payment failed"));
@@ -28536,6 +28583,7 @@ function registerImageCommand(program2) {
28536
28583
  headers: {
28537
28584
  "Content-Type": "application/json",
28538
28585
  "X-Payment-Tx": txHash,
28586
+ "X-Payment-Token": payToken,
28539
28587
  "X-Twin-Id": auth.twinId
28540
28588
  },
28541
28589
  body: JSON.stringify({
@@ -28578,7 +28626,7 @@ function registerImageCommand(program2) {
28578
28626
  }
28579
28627
  }
28580
28628
  console.log("");
28581
- console.log(success(`Paid ${amikoAmount} AMIKO (≈ $${usdCost.toFixed(4)})`) + dim(` tx: ${txHash.slice(0, 16)}...`));
28629
+ console.log(success(`Paid ${payAmount} ${payToken} (≈ $${usdCost.toFixed(4)})`) + dim(` tx: ${txHash.slice(0, 16)}...`));
28582
28630
  await printBalanceRemaining(auth);
28583
28631
  } catch (err) {
28584
28632
  stopProgress();
@@ -28599,6 +28647,15 @@ var ETA = {
28599
28647
  function statusOf(err) {
28600
28648
  return err?.status;
28601
28649
  }
28650
+ function collectOption(value, previous) {
28651
+ return [...previous ?? [], value];
28652
+ }
28653
+ function assertMaxUrls(resourceName, urls, max) {
28654
+ if (urls.length > max) {
28655
+ console.error(error(`At most ${max} ${resourceName} allowed (got ${urls.length}).`));
28656
+ process.exit(1);
28657
+ }
28658
+ }
28602
28659
  function printCompleted(final, title) {
28603
28660
  console.log(heading(`${title}
28604
28661
  `));
@@ -28610,7 +28667,9 @@ function printCompleted(final, title) {
28610
28667
  console.log(label("Type", final.assetMime));
28611
28668
  console.log("");
28612
28669
  if (final.charged) {
28613
- console.log(success(`Charged ${final.costAmiko ?? "?"} AMIKO`) + (final.chargeTx ? dim(` tx: ${final.chargeTx.slice(0, 16)}…`) : ""));
28670
+ const amt = final.chargeAmount ?? final.costAmiko ?? "?";
28671
+ const tok = final.chargeToken ?? "AMIKO";
28672
+ console.log(success(`Charged ${amt} ${tok}`) + (final.chargeTx ? dim(` tx: ${final.chargeTx.slice(0, 16)}…`) : ""));
28614
28673
  } else {
28615
28674
  console.log(dim("Not charged (settlement pending or waived)."));
28616
28675
  }
@@ -28679,9 +28738,12 @@ async function submitCreateJob(opts) {
28679
28738
  console.log(label("Status", "PENDING — generating in the background"));
28680
28739
  console.log(label("Check result", `amiko create status ${jobId}`));
28681
28740
  console.log(dim(`Usually ready in ${ETA[opts.mode]} — charged only on success. (Also: amiko create media)`));
28741
+ if (opts.mode === "VIDEO") {
28742
+ console.log(dim("Async video: HTTP 202 / PENDING means the job was accepted — not a failure. Poll status after ~2–9 min."));
28743
+ }
28682
28744
  }
28683
28745
  function registerCreateCommand(create2) {
28684
- create2.command("image <prompt>").description("Generate an image (Create Studio)").option("--model <model>", "Image model", "gpt-image-2").option("--aspect <ratio>", "Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4", "1:1").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28746
+ create2.command("image <prompt>").description("Generate an image (Create Studio)").option("--model <model>", "Image model: nano-banana-2 (default), nano-banana[-pro|-lite] (Google), gpt-image-2/-1.5/-1/-1-mini (OpenAI), grok-imagine-image-quality (xAI), image-01 (MiniMax)", "nano-banana-2").option("--aspect <ratio>", "Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4", "1:1").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28685
28747
  await submitCreateJob({
28686
28748
  mode: "IMAGE",
28687
28749
  summary: `Create image: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
@@ -28697,12 +28759,19 @@ function registerCreateCommand(create2) {
28697
28759
  }
28698
28760
  });
28699
28761
  });
28700
- create2.command("video <prompt>").description("Generate a video (Create Studio)").option("--model <model>", "Video model", "MiniMax-Hailuo-2.3-Fast").option("--resolution <res>", "512P, 720P, 768P, 1080P", "768P").option("--seconds <n>", "Clip length: 6 or 10", "6").option("--aspect <ratio>", "Aspect ratio (provider-dependent)").option("--first-frame <url>", "Image-to-video: first frame URL/data URI").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28762
+ create2.command("video <prompt>").description("Generate a video (Create Studio)").option("--model <model>", "Video model: MiniMax-Hailuo-2.3-Fast (default; I2V — needs --first-frame), MiniMax-Hailuo-02 / MiniMax-Hailuo-2.3 (T2V+I2V), grok-imagine-video (xAI), dreamina-seedance-* (BytePlus). ONLY these — kling and other ids are not supported", "MiniMax-Hailuo-2.3-Fast").option("--resolution <res>", "512P, 720P, 768P, 1080P", "768P").option("--seconds <n>", "Clip length: 6 or 10", "6").option("--aspect <ratio>", "Aspect ratio (provider-dependent)").option("--first-frame <url>", "Image-to-video: first frame URL/data URI").option("--last-frame <url>", "First-and-last-frame video: ending frame URL/data URI").option("--reference-image <url>", "Seedance multimodal: reference image URL (repeatable, max 9)", collectOption, []).option("--reference-video <url>", "Seedance multimodal: reference video URL (repeatable, max 3)", collectOption, []).option("--reference-audio <url>", "Seedance multimodal: reference audio URL (repeatable, max 3)", collectOption, []).option("--subject-reference <url>", "MiniMax S2V: subject/character reference image URL").option("--generate-audio", "Seedance: generate audio in the output video").option("--no-generate-audio", "Seedance: output silent video").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28701
28763
  const seconds = Number(opts.seconds);
28702
28764
  if (seconds !== 6 && seconds !== 10) {
28703
28765
  console.error(error("--seconds must be 6 or 10."));
28704
28766
  process.exit(1);
28705
28767
  }
28768
+ const referenceImageUrls = opts.referenceImage ?? [];
28769
+ const referenceVideoUrls = opts.referenceVideo ?? [];
28770
+ const referenceAudioUrls = opts.referenceAudio ?? [];
28771
+ assertMaxUrls("--reference-image", referenceImageUrls, 9);
28772
+ assertMaxUrls("--reference-video", referenceVideoUrls, 3);
28773
+ assertMaxUrls("--reference-audio", referenceAudioUrls, 3);
28774
+ const generateAudio = opts.generateAudio;
28706
28775
  await submitCreateJob({
28707
28776
  mode: "VIDEO",
28708
28777
  summary: `Create video: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
@@ -28722,6 +28791,19 @@ function registerCreateCommand(create2) {
28722
28791
  seconds,
28723
28792
  ...opts.aspect ? { aspectRatio: opts.aspect } : {},
28724
28793
  ...opts.firstFrame ? { firstFrameImage: opts.firstFrame } : {},
28794
+ ...opts.lastFrame ? { lastFrameImage: opts.lastFrame } : {},
28795
+ ...referenceImageUrls.length ? { referenceImageUrls } : {},
28796
+ ...referenceVideoUrls.length ? { referenceVideoUrls } : {},
28797
+ ...referenceAudioUrls.length ? { referenceAudioUrls } : {},
28798
+ ...opts.subjectReference ? {
28799
+ subjectReference: [
28800
+ {
28801
+ type: "character",
28802
+ image: [opts.subjectReference]
28803
+ }
28804
+ ]
28805
+ } : {},
28806
+ ...generateAudio !== undefined ? { generateAudio } : {},
28725
28807
  ...opts.token ? { preferredToken: opts.token } : {}
28726
28808
  }
28727
28809
  });
@@ -28742,7 +28824,7 @@ function registerCreateCommand(create2) {
28742
28824
  }
28743
28825
  });
28744
28826
  });
28745
- create2.command("music [prompt]").description("Generate music (Create Studio)").option("--model <model>", "music_v1, music-2.6, music-cover", "music-2.6").option("--lyrics <lyrics>", "Lyrics (optional)").option("--duration <ms>", "Length in ms").option("--instrumental", "Instrumental (no vocals)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28827
+ create2.command("music [prompt]").description("Generate music (Create Studio)").option("--model <model>", "music_v1, music-2.6[-free], music-cover[-free]", "music-2.6").option("--lyrics <lyrics>", "Lyrics (optional)").option("--duration <ms>", "Length in ms").option("--instrumental", "Instrumental (no vocals)").option("--lyrics-optimizer", "Auto-write lyrics from the prompt (music_v1 only; on by default when no --lyrics/--instrumental)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (prompt, opts) => {
28746
28828
  if (!prompt && !opts.lyrics) {
28747
28829
  console.error(error("Provide a prompt or --lyrics."));
28748
28830
  process.exit(1);
@@ -28760,6 +28842,7 @@ function registerCreateCommand(create2) {
28760
28842
  ...opts.lyrics ? { lyrics: opts.lyrics } : {},
28761
28843
  ...opts.duration ? { durationMs: Number(opts.duration) } : {},
28762
28844
  ...opts.instrumental ? { isInstrumental: true } : {},
28845
+ ...opts.lyricsOptimizer ? { lyricsOptimizer: true } : {},
28763
28846
  ...opts.token ? { preferredToken: opts.token } : {}
28764
28847
  }
28765
28848
  });
@@ -28865,41 +28948,42 @@ import { basename, extname } from "node:path";
28865
28948
  var MAX_BYTES = 100 * 1024 * 1024;
28866
28949
  var FETCH_TIMEOUT_MS = 30000;
28867
28950
  async function resolveFileInput(src, opts = {}) {
28951
+ const maxBytes = opts.maxBytes ?? MAX_BYTES;
28868
28952
  if (src === "-" || opts.stdin) {
28869
- const buffer2 = await readStdin();
28953
+ const buffer2 = await readStdin(maxBytes);
28870
28954
  const name2 = opts.hintName ?? "stdin";
28871
28955
  const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
28872
28956
  return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
28873
28957
  }
28874
28958
  if (/^https?:\/\//i.test(src)) {
28875
- const buffer2 = await fetchRemote(src);
28959
+ const buffer2 = await fetchRemote(src, maxBytes);
28876
28960
  const urlPath = new URL(src).pathname;
28877
28961
  const name2 = opts.hintName ?? (basename(urlPath) || "download");
28878
28962
  const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
28879
28963
  return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
28880
28964
  }
28881
28965
  const buffer = await readFile(src);
28882
- if (buffer.length > MAX_BYTES) {
28883
- throw new Error(`File too large: ${buffer.length} bytes exceeds ${MAX_BYTES} byte cap.`);
28966
+ if (buffer.length > maxBytes) {
28967
+ throw new Error(`File too large: ${buffer.length} bytes exceeds ${maxBytes} byte cap.`);
28884
28968
  }
28885
28969
  const name = opts.hintName ?? basename(src);
28886
28970
  const mime = opts.hintMime ?? detectMime(buffer, name);
28887
28971
  return { buffer, name, mime, size: buffer.length };
28888
28972
  }
28889
- async function readStdin() {
28973
+ async function readStdin(maxBytes) {
28890
28974
  const chunks = [];
28891
28975
  let total = 0;
28892
28976
  for await (const chunk of process.stdin) {
28893
28977
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28894
28978
  total += buf.length;
28895
- if (total > MAX_BYTES) {
28896
- throw new Error(`stdin exceeded ${MAX_BYTES} byte cap.`);
28979
+ if (total > maxBytes) {
28980
+ throw new Error(`stdin exceeded ${maxBytes} byte cap.`);
28897
28981
  }
28898
28982
  chunks.push(buf);
28899
28983
  }
28900
28984
  return Buffer.concat(chunks);
28901
28985
  }
28902
- async function fetchRemote(url2) {
28986
+ async function fetchRemote(url2, maxBytes) {
28903
28987
  const controller = new AbortController;
28904
28988
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
28905
28989
  try {
@@ -28908,12 +28992,12 @@ async function fetchRemote(url2) {
28908
28992
  throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
28909
28993
  }
28910
28994
  const len = Number(res.headers.get("content-length") || 0);
28911
- if (len > MAX_BYTES) {
28912
- throw new Error(`Remote file too large: ${len} bytes exceeds ${MAX_BYTES} byte cap.`);
28995
+ if (len > maxBytes) {
28996
+ throw new Error(`Remote file too large: ${len} bytes exceeds ${maxBytes} byte cap.`);
28913
28997
  }
28914
28998
  const buf = Buffer.from(await res.arrayBuffer());
28915
- if (buf.length > MAX_BYTES) {
28916
- throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${MAX_BYTES} byte cap.`);
28999
+ if (buf.length > maxBytes) {
29000
+ throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${maxBytes} byte cap.`);
28917
29001
  }
28918
29002
  return buf;
28919
29003
  } finally {
@@ -28944,6 +29028,13 @@ var EXT_MIME = {
28944
29028
  ".mp3": "audio/mpeg",
28945
29029
  ".m4a": "audio/mp4",
28946
29030
  ".ogg": "audio/ogg",
29031
+ ".aac": "audio/aac",
29032
+ ".flac": "audio/flac",
29033
+ ".mp4": "video/mp4",
29034
+ ".m4v": "video/x-m4v",
29035
+ ".webm": "video/webm",
29036
+ ".mov": "video/quicktime",
29037
+ ".mkv": "video/x-matroska",
28947
29038
  ".zip": "application/zip"
28948
29039
  };
28949
29040
  function detectMime(buffer, name) {
@@ -28956,12 +29047,15 @@ function detectMime(buffer, name) {
28956
29047
  return EXT_MIME[ext] ?? "application/octet-stream";
28957
29048
  }
28958
29049
 
28959
- // src/commands/chat.ts
29050
+ // src/lib/conversations.ts
28960
29051
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28961
29052
  var CUID_RE = /^c[a-z0-9]{20,}$/i;
28962
29053
  function looksLikeConversationId(t) {
28963
29054
  return UUID_RE.test(t) || CUID_RE.test(t);
28964
29055
  }
29056
+ function normalizeUserId(id) {
29057
+ return id.replace(/^did:privy:/, "");
29058
+ }
28965
29059
  function authOrExit() {
28966
29060
  try {
28967
29061
  return requireAuth();
@@ -28973,9 +29067,531 @@ function authOrExit() {
28973
29067
  function convLabel(conv, selfUserId) {
28974
29068
  if (conv.title)
28975
29069
  return conv.title;
28976
- const others = (conv.participants ?? []).filter((p) => !(p.participant_type === "user" && p.participant_id === selfUserId)).map((p) => p.user?.name ?? p.twin?.name ?? p.participant_id.slice(0, 8)).filter((n) => !!n);
29070
+ const self = selfUserId ? normalizeUserId(selfUserId) : undefined;
29071
+ const others = (conv.participants ?? []).filter((p) => !(p.participant_type === "user" && normalizeUserId(p.participant_id) === self)).map((p) => p.user?.name ?? p.twin?.name ?? p.participant_id.slice(0, 8)).filter((n) => !!n);
28977
29072
  return others.length ? others.join(", ") : "(conversation)";
28978
29073
  }
29074
+ function candidateList(users) {
29075
+ return users.map((u) => `${u.name ?? "?"} (@${u.handle ?? u.id.slice(0, 8)})`).join(", ");
29076
+ }
29077
+ async function resolveMembers(auth, tokens) {
29078
+ const resolved = [];
29079
+ const failures = [];
29080
+ const seen = new Set;
29081
+ let friendsCache = null;
29082
+ const getFriends = async () => {
29083
+ if (friendsCache)
29084
+ return friendsCache;
29085
+ const data = await amikoWebFetch(auth, "/api/friends", {
29086
+ query: { type: "user" },
29087
+ timeoutMs: 15000
29088
+ });
29089
+ friendsCache = (data.friends ?? []).map((f) => f.friend).filter((f) => !!f && f.type === "user");
29090
+ return friendsCache;
29091
+ };
29092
+ const push = (m) => {
29093
+ if (seen.has(m.userId))
29094
+ return;
29095
+ seen.add(m.userId);
29096
+ resolved.push(m);
29097
+ };
29098
+ for (const raw of tokens) {
29099
+ const token = raw.trim();
29100
+ if (!token)
29101
+ continue;
29102
+ const asId = normalizeUserId(token);
29103
+ if (looksLikeConversationId(asId)) {
29104
+ push({ token, userId: asId, source: "id" });
29105
+ continue;
29106
+ }
29107
+ if (token.startsWith("@")) {
29108
+ const handle = token.slice(1);
29109
+ try {
29110
+ const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
29111
+ timeoutMs: 15000
29112
+ });
29113
+ if (!prof.user?.id)
29114
+ throw new Error(`No user @${handle}.`);
29115
+ push({
29116
+ token,
29117
+ userId: prof.user.id,
29118
+ name: prof.user.name ?? undefined,
29119
+ handle: prof.user.handle ?? handle,
29120
+ source: "handle"
29121
+ });
29122
+ } catch (e5) {
29123
+ failures.push({
29124
+ token,
29125
+ reason: e5 instanceof Error ? e5.message : `No user @${handle}.`
29126
+ });
29127
+ }
29128
+ continue;
29129
+ }
29130
+ try {
29131
+ const friends = await getFriends();
29132
+ const lower = token.toLowerCase();
29133
+ const exact = friends.filter((f) => f.name?.toLowerCase() === lower || f.handle?.toLowerCase() === lower);
29134
+ const matches = exact.length ? exact : friends.filter((f) => f.name?.toLowerCase().includes(lower) || f.handle?.toLowerCase().includes(lower));
29135
+ if (matches.length === 1) {
29136
+ const f = matches[0];
29137
+ push({
29138
+ token,
29139
+ userId: f.id,
29140
+ name: f.name,
29141
+ handle: f.handle,
29142
+ source: "friend"
29143
+ });
29144
+ continue;
29145
+ }
29146
+ if (matches.length > 1) {
29147
+ failures.push({
29148
+ token,
29149
+ reason: `ambiguous — matches: ${candidateList(matches)}. Use an exact @handle or a user id.`
29150
+ });
29151
+ continue;
29152
+ }
29153
+ const res = await amikoWebFetch(auth, "/api/search", {
29154
+ query: { q: token, type: "people", limit: 8 },
29155
+ timeoutMs: 15000
29156
+ });
29157
+ const users = res.users ?? [];
29158
+ if (users.length === 1) {
29159
+ const u = users[0];
29160
+ push({
29161
+ token,
29162
+ userId: u.id,
29163
+ name: u.name,
29164
+ handle: u.handle,
29165
+ source: "search"
29166
+ });
29167
+ } else if (users.length > 1) {
29168
+ failures.push({
29169
+ token,
29170
+ reason: `ambiguous — matches: ${candidateList(users)}. Use an exact @handle or a user id.`
29171
+ });
29172
+ } else {
29173
+ failures.push({
29174
+ token,
29175
+ reason: `no friend or user matches. Use an exact @handle, or an id from \`amiko friends list --json\`.`
29176
+ });
29177
+ }
29178
+ } catch (e5) {
29179
+ failures.push({
29180
+ token,
29181
+ reason: e5 instanceof Error ? e5.message : "lookup failed"
29182
+ });
29183
+ }
29184
+ }
29185
+ return { resolved, failures };
29186
+ }
29187
+ async function fetchConversationById(auth, id) {
29188
+ const data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(id)}`, { timeoutMs: 20000 });
29189
+ if (!data.conversation) {
29190
+ throw new Error(`Conversation ${id} not found or you are not a participant.`);
29191
+ }
29192
+ return data.conversation;
29193
+ }
29194
+ async function listGroups(auth, limit = 100) {
29195
+ const data = await amikoWebFetch(auth, "/api/conversations", { query: { limit }, timeoutMs: 20000 });
29196
+ return (data.conversations ?? []).filter((c) => c.conversation_type === "group");
29197
+ }
29198
+ async function findGroupIdByExactTitle(auth, title) {
29199
+ const lower = title.trim().toLowerCase();
29200
+ if (!lower)
29201
+ return null;
29202
+ const groups = await listGroups(auth);
29203
+ const matches = groups.filter((g) => g.title?.toLowerCase() === lower);
29204
+ return matches.length === 1 ? matches[0].id : null;
29205
+ }
29206
+ async function resolveGroup(auth, target) {
29207
+ const t = target.trim();
29208
+ if (!t) {
29209
+ throw new Error("No group given. Run `amiko chat group list` and pass a group id or title.");
29210
+ }
29211
+ if (looksLikeConversationId(t)) {
29212
+ const conv = await fetchConversationById(auth, t);
29213
+ if (conv.conversation_type !== "group") {
29214
+ throw new Error(`Conversation ${t} is not a group chat (type: ${conv.conversation_type}).`);
29215
+ }
29216
+ return conv;
29217
+ }
29218
+ const groups = await listGroups(auth);
29219
+ const lower = t.toLowerCase();
29220
+ const exact = groups.filter((g) => g.title?.toLowerCase() === lower);
29221
+ const matches = exact.length ? exact : groups.filter((g) => g.title?.toLowerCase().includes(lower));
29222
+ if (matches.length === 0) {
29223
+ throw new Error(`No group matching "${t}". Run \`amiko chat group list\` and use the group's id or exact title.`);
29224
+ }
29225
+ if (matches.length > 1) {
29226
+ const names = matches.map((g) => `"${g.title}" (${g.id})`).join(", ");
29227
+ throw new Error(`"${t}" is ambiguous — matches: ${names}. Use the group's id.`);
29228
+ }
29229
+ return fetchConversationById(auth, matches[0].id);
29230
+ }
29231
+
29232
+ // src/commands/chat-group.ts
29233
+ var collect = (v, prev) => [...prev, v];
29234
+ function statusOf2(e5) {
29235
+ return e5 && typeof e5 === "object" && "status" in e5 ? e5.status : undefined;
29236
+ }
29237
+ var ADMIN_TIP = "Only group admins can do this — check roles with `amiko chat group info`.";
29238
+ function memberLabel(m) {
29239
+ const name = m.name ?? m.token;
29240
+ return m.handle ? `${name} (@${m.handle})` : name;
29241
+ }
29242
+ function memberFlags(tokens) {
29243
+ return tokens.map((t) => `--member ${JSON.stringify(t)}`).join(" ");
29244
+ }
29245
+ function exitWithError(e5, fallback2) {
29246
+ console.error(error(e5 instanceof Error ? e5.message : fallback2));
29247
+ process.exit(1);
29248
+ }
29249
+ async function resolveMembersOrExit(auth, tokens) {
29250
+ if (tokens.length === 0) {
29251
+ console.error(error("At least one --member is required."));
29252
+ process.exit(1);
29253
+ }
29254
+ let resolution;
29255
+ try {
29256
+ resolution = await resolveMembers(auth, tokens);
29257
+ } catch (e5) {
29258
+ exitWithError(e5, "Could not resolve members");
29259
+ }
29260
+ const { resolved, failures } = resolution;
29261
+ if (failures.length > 0) {
29262
+ console.error(error("Could not resolve every member — nothing was changed."));
29263
+ for (const m of resolved) {
29264
+ console.error(dim(` resolved: ${memberLabel(m)} → ${m.userId}`));
29265
+ }
29266
+ for (const f of failures) {
29267
+ console.error(error(` ${JSON.stringify(f.token)}: ${f.reason}`));
29268
+ }
29269
+ process.exit(1);
29270
+ }
29271
+ return resolved;
29272
+ }
29273
+ async function resolveGroupOrExit(auth, target) {
29274
+ try {
29275
+ return await resolveGroup(auth, target);
29276
+ } catch (e5) {
29277
+ exitWithError(e5, "Could not resolve group");
29278
+ }
29279
+ }
29280
+ function activeParticipantOf(conv, userId) {
29281
+ return (conv.participants ?? []).find((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === userId && !p.left_at);
29282
+ }
29283
+ function participantName(p) {
29284
+ return p.user?.name ?? p.twin?.name ?? p.user?.handle ?? p.participant_id.slice(0, 8);
29285
+ }
29286
+ function reportMemberResults(conversationId, results, raw) {
29287
+ if (raw) {
29288
+ console.log(JSON.stringify({
29289
+ conversation_id: conversationId,
29290
+ results: results.map((r) => ({
29291
+ user_id: r.member.userId,
29292
+ member: memberLabel(r.member),
29293
+ outcome: r.outcome,
29294
+ ok: r.ok
29295
+ }))
29296
+ }, null, 2));
29297
+ return;
29298
+ }
29299
+ for (const r of results) {
29300
+ const line = `${memberLabel(r.member)} — ${r.outcome}`;
29301
+ console.log(r.ok ? success(line) : error(line));
29302
+ }
29303
+ }
29304
+ function registerChatGroupCommand(group) {
29305
+ group.command("create <name>").description("Create a group chat with the given name and members").option("--member <who>", "Member to add (name, @handle, or user id); repeat per member", collect, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (name, opts) => {
29306
+ const auth = authOrExit();
29307
+ const members = await resolveMembersOrExit(auth, opts.member);
29308
+ await requireApproval({
29309
+ free: "creates a group chat that real people are added to",
29310
+ summary: `Create group "${name}" with ${members.map(memberLabel).join(", ")}`,
29311
+ yes: opts.yes,
29312
+ commandExample: `amiko chat group create ${JSON.stringify(name)} ${memberFlags(opts.member)}`
29313
+ });
29314
+ let data;
29315
+ try {
29316
+ data = await amikoWebFetch(auth, "/api/conversations", {
29317
+ method: "POST",
29318
+ body: {
29319
+ participant_ids: members.map((m) => m.userId),
29320
+ participant_types: members.map(() => "user"),
29321
+ conversation_type: "group",
29322
+ title: name
29323
+ },
29324
+ timeoutMs: 20000
29325
+ });
29326
+ } catch (e5) {
29327
+ exitWithError(e5, "Failed to create group");
29328
+ }
29329
+ if (opts.raw) {
29330
+ console.log(JSON.stringify(data, null, 2));
29331
+ return;
29332
+ }
29333
+ if (!data.conversation?.id) {
29334
+ console.error(error("Group creation returned no conversation id."));
29335
+ process.exit(1);
29336
+ }
29337
+ console.log(success(`Group created: ${name}`));
29338
+ console.log(label("ID", data.conversation.id));
29339
+ console.log(label("Members", `${members.map(memberLabel).join(", ")} + you (admin)`));
29340
+ if (data.reused_existing) {
29341
+ console.log(dim("Note: an existing matching group was reused."));
29342
+ }
29343
+ });
29344
+ group.command("list").description("List your group chats").option("--limit <n>", "Max conversations to scan", "100").option("--raw", "Output raw JSON").action(async (opts) => {
29345
+ const auth = authOrExit();
29346
+ let groups;
29347
+ try {
29348
+ const data = await amikoWebFetch(auth, "/api/conversations", { query: { limit: opts.limit }, timeoutMs: 20000 });
29349
+ groups = (data.conversations ?? []).filter((c) => c.conversation_type === "group");
29350
+ } catch (e5) {
29351
+ exitWithError(e5, "Failed to list groups");
29352
+ }
29353
+ if (opts.raw) {
29354
+ console.log(JSON.stringify({ groups }, null, 2));
29355
+ return;
29356
+ }
29357
+ if (groups.length === 0) {
29358
+ console.log(dim("No group chats."));
29359
+ return;
29360
+ }
29361
+ console.log(heading(`Groups (${groups.length})
29362
+ `));
29363
+ const rows = [
29364
+ [dim("TITLE"), dim("ID"), dim("MEMBERS"), dim("UNREAD"), dim("LAST")],
29365
+ ...groups.map((g) => [
29366
+ g.title ?? "(untitled)",
29367
+ g.id,
29368
+ String(g.participants?.length ?? "-"),
29369
+ g.unread_count ? String(g.unread_count) : "-",
29370
+ g.last_message?.content?.slice(0, 40) ?? "-"
29371
+ ])
29372
+ ];
29373
+ console.log(table(rows));
29374
+ });
29375
+ group.command("info <group>").description("Show a group's members and roles (group id or title)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29376
+ const auth = authOrExit();
29377
+ const conv = await resolveGroupOrExit(auth, target);
29378
+ if (opts.raw) {
29379
+ console.log(JSON.stringify({ conversation: conv }, null, 2));
29380
+ return;
29381
+ }
29382
+ const selfId = auth.userId ? normalizeUserId(auth.userId) : undefined;
29383
+ const active = (conv.participants ?? []).filter((p) => !p.left_at);
29384
+ console.log(heading(conv.title ?? "(untitled group)"));
29385
+ console.log(label("ID", conv.id));
29386
+ console.log(label("Members", String(active.length)));
29387
+ console.log("");
29388
+ const rows = [
29389
+ [dim("NAME"), dim("HANDLE"), dim("USER ID"), dim("TYPE"), dim("ROLE")],
29390
+ ...active.map((p) => {
29391
+ const isSelf = p.participant_type === "user" && normalizeUserId(p.participant_id) === selfId;
29392
+ return [
29393
+ participantName(p) + (isSelf ? " (you)" : ""),
29394
+ p.user?.handle ? `@${p.user.handle}` : "-",
29395
+ p.participant_id,
29396
+ p.participant_type,
29397
+ p.role ?? "member"
29398
+ ];
29399
+ })
29400
+ ];
29401
+ console.log(table(rows));
29402
+ });
29403
+ group.command("invite <group>").description("Show a group's invite/join link + QR image URL (admins only)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29404
+ const auth = authOrExit();
29405
+ const conv = await resolveGroupOrExit(auth, target);
29406
+ let data;
29407
+ try {
29408
+ data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/invite`, { timeoutMs: 20000 });
29409
+ } catch (e5) {
29410
+ if (statusOf2(e5) === 403) {
29411
+ console.error(error(`Can't view the invite link. ${ADMIN_TIP}`));
29412
+ process.exit(1);
29413
+ }
29414
+ exitWithError(e5, "Failed to load invite link");
29415
+ }
29416
+ if (!data.url || !data.qrUrl) {
29417
+ exitWithError(null, "The invite link response was missing its link or QR URL — nothing to share.");
29418
+ }
29419
+ if (opts.raw) {
29420
+ console.log(JSON.stringify(data, null, 2));
29421
+ return;
29422
+ }
29423
+ console.log(heading(conv.title ?? "(untitled group)"));
29424
+ console.log(label("Invite link", data.url));
29425
+ console.log(label("QR image", data.qrUrl));
29426
+ console.log(dim("Share with group admins only — anyone who opens it can join."));
29427
+ });
29428
+ group.command("rename <group> <newTitle>").description("Rename a group (admins only)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, newTitle, opts) => {
29429
+ const auth = authOrExit();
29430
+ const conv = await resolveGroupOrExit(auth, target);
29431
+ await confirmDestructive({
29432
+ action: `Rename group "${conv.title ?? conv.id}" to "${newTitle}"`,
29433
+ yes: opts.yes,
29434
+ commandExample: `amiko chat group rename ${JSON.stringify(target)} ${JSON.stringify(newTitle)}`
29435
+ });
29436
+ let data;
29437
+ try {
29438
+ data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}`, { method: "PATCH", body: { title: newTitle }, timeoutMs: 20000 });
29439
+ } catch (e5) {
29440
+ if (statusOf2(e5) === 403) {
29441
+ console.error(error(`Rename failed. ${ADMIN_TIP}`));
29442
+ process.exit(1);
29443
+ }
29444
+ exitWithError(e5, "Rename failed");
29445
+ }
29446
+ if (opts.raw) {
29447
+ console.log(JSON.stringify(data, null, 2));
29448
+ return;
29449
+ }
29450
+ console.log(success(`Group renamed to: ${newTitle}`) + dim(` (${conv.id})`));
29451
+ });
29452
+ group.command("add <group>").description("Add members to a group (admins only)").option("--member <who>", "Member to add (name, @handle, or user id); repeat per member", collect, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29453
+ const auth = authOrExit();
29454
+ const members = await resolveMembersOrExit(auth, opts.member);
29455
+ const conv = await resolveGroupOrExit(auth, target);
29456
+ await requireApproval({
29457
+ free: "adds real people to the group",
29458
+ summary: `Add ${members.map(memberLabel).join(", ")} to group "${conv.title ?? conv.id}"`,
29459
+ yes: opts.yes,
29460
+ commandExample: `amiko chat group add ${JSON.stringify(target)} ${memberFlags(opts.member)}`
29461
+ });
29462
+ const results = [];
29463
+ let hardFailure = false;
29464
+ for (const m of members) {
29465
+ if (activeParticipantOf(conv, m.userId)) {
29466
+ results.push({ member: m, outcome: "already a member", ok: true });
29467
+ continue;
29468
+ }
29469
+ try {
29470
+ await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/participants`, {
29471
+ method: "POST",
29472
+ body: { participant_id: m.userId, participant_type: "user" },
29473
+ timeoutMs: 20000
29474
+ });
29475
+ results.push({ member: m, outcome: "added", ok: true });
29476
+ } catch (e5) {
29477
+ const status = statusOf2(e5);
29478
+ if (status === 409) {
29479
+ results.push({ member: m, outcome: "already a member", ok: true });
29480
+ } else if (status === 403) {
29481
+ if (results.length)
29482
+ reportMemberResults(conv.id, results, opts.raw);
29483
+ console.error(error(`Adding members failed. ${ADMIN_TIP}`));
29484
+ process.exit(1);
29485
+ } else if (status === 404) {
29486
+ results.push({ member: m, outcome: "user not found", ok: false });
29487
+ hardFailure = true;
29488
+ } else {
29489
+ results.push({
29490
+ member: m,
29491
+ outcome: e5 instanceof Error ? e5.message : "failed",
29492
+ ok: false
29493
+ });
29494
+ hardFailure = true;
29495
+ }
29496
+ }
29497
+ }
29498
+ reportMemberResults(conv.id, results, opts.raw);
29499
+ if (hardFailure)
29500
+ process.exit(1);
29501
+ });
29502
+ group.command("remove <group>").description("Remove members from a group (admins only; anyone can remove themselves)").option("--member <who>", "Member to remove (name, @handle, or user id); repeat per member", collect, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29503
+ const auth = authOrExit();
29504
+ const members = await resolveMembersOrExit(auth, opts.member);
29505
+ const conv = await resolveGroupOrExit(auth, target);
29506
+ await confirmDestructive({
29507
+ action: `Remove ${members.map(memberLabel).join(", ")} from group "${conv.title ?? conv.id}"`,
29508
+ yes: opts.yes,
29509
+ commandExample: `amiko chat group remove ${JSON.stringify(target)} ${memberFlags(opts.member)}`
29510
+ });
29511
+ const results = [];
29512
+ let hardFailure = false;
29513
+ for (const m of members) {
29514
+ const row = activeParticipantOf(conv, m.userId);
29515
+ if (!row?.id) {
29516
+ results.push({ member: m, outcome: "not a member", ok: true });
29517
+ continue;
29518
+ }
29519
+ try {
29520
+ await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/participants/${encodeURIComponent(row.id)}`, { method: "DELETE", timeoutMs: 20000 });
29521
+ results.push({ member: m, outcome: "removed", ok: true });
29522
+ } catch (e5) {
29523
+ if (statusOf2(e5) === 403) {
29524
+ if (results.length)
29525
+ reportMemberResults(conv.id, results, opts.raw);
29526
+ console.error(error(`Removing members failed. ${ADMIN_TIP}`));
29527
+ process.exit(1);
29528
+ }
29529
+ results.push({
29530
+ member: m,
29531
+ outcome: e5 instanceof Error ? e5.message : "failed",
29532
+ ok: false
29533
+ });
29534
+ hardFailure = true;
29535
+ }
29536
+ }
29537
+ reportMemberResults(conv.id, results, opts.raw);
29538
+ if (hardFailure)
29539
+ process.exit(1);
29540
+ });
29541
+ group.command("leave <group>").alias("delete").description("Leave a group (hides it for you only — other members keep the conversation)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29542
+ const auth = authOrExit();
29543
+ const conv = await resolveGroupOrExit(auth, target);
29544
+ await confirmDestructive({
29545
+ action: `Leave group "${conv.title ?? conv.id}" — this removes/hides the conversation for YOU only; other members keep it and its history.`,
29546
+ yes: opts.yes,
29547
+ commandExample: `amiko chat group leave ${JSON.stringify(target)}`
29548
+ });
29549
+ let data;
29550
+ try {
29551
+ data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}`, { method: "DELETE", timeoutMs: 20000 });
29552
+ } catch (e5) {
29553
+ exitWithError(e5, "Leave failed");
29554
+ }
29555
+ if (opts.raw) {
29556
+ console.log(JSON.stringify(data, null, 2));
29557
+ return;
29558
+ }
29559
+ console.log(success(`Left group "${conv.title ?? conv.id}"`) + dim(" (hidden for you; other members keep it)"));
29560
+ });
29561
+ group.command("promote <group>").description("Promote a member to group admin (admins only)").requiredOption("--member <who>", "Member to promote (name, @handle, or user id)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
29562
+ const auth = authOrExit();
29563
+ const [member] = await resolveMembersOrExit(auth, [opts.member]);
29564
+ const conv = await resolveGroupOrExit(auth, target);
29565
+ const row = activeParticipantOf(conv, member.userId);
29566
+ if (!row?.id) {
29567
+ console.error(error(`${memberLabel(member)} is not a member of "${conv.title ?? conv.id}".`));
29568
+ process.exit(1);
29569
+ }
29570
+ await confirmDestructive({
29571
+ action: `Promote ${memberLabel(member)} to admin in "${conv.title ?? conv.id}"`,
29572
+ detail: "There is no demote — this cannot be undone from the CLI.",
29573
+ yes: opts.yes,
29574
+ commandExample: `amiko chat group promote ${JSON.stringify(target)} --member ${JSON.stringify(opts.member)}`
29575
+ });
29576
+ let data;
29577
+ try {
29578
+ data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/participants/${encodeURIComponent(row.id)}`, { method: "PATCH", body: { role: "admin" }, timeoutMs: 20000 });
29579
+ } catch (e5) {
29580
+ if (statusOf2(e5) === 403) {
29581
+ console.error(error(`Promote failed. ${ADMIN_TIP}`));
29582
+ process.exit(1);
29583
+ }
29584
+ exitWithError(e5, "Promote failed");
29585
+ }
29586
+ if (opts.raw) {
29587
+ console.log(JSON.stringify(data, null, 2));
29588
+ return;
29589
+ }
29590
+ console.log(success(`${memberLabel(member)} is now an admin.`));
29591
+ });
29592
+ }
29593
+
29594
+ // src/commands/chat.ts
28979
29595
  async function findOrCreateDm(auth, userId) {
28980
29596
  const data = await amikoWebFetch(auth, "/api/conversations", {
28981
29597
  method: "POST",
@@ -28991,30 +29607,91 @@ async function findOrCreateDm(auth, userId) {
28991
29607
  throw new Error("Could not open a conversation with that user.");
28992
29608
  return id;
28993
29609
  }
29610
+ async function fetchUserById(auth, id) {
29611
+ try {
29612
+ const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
29613
+ return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
29614
+ } catch (e5) {
29615
+ if (e5.status === 404)
29616
+ return null;
29617
+ throw e5;
29618
+ }
29619
+ }
29620
+ async function findDmByUserId(auth, userId) {
29621
+ const want = normalizeUserId(userId);
29622
+ const limit = 100;
29623
+ for (let offset = 0;offset < 1e4; offset += limit) {
29624
+ const data = await amikoWebFetch(auth, "/api/conversations", {
29625
+ query: { limit, offset },
29626
+ timeoutMs: 20000
29627
+ });
29628
+ const page = data.conversations ?? [];
29629
+ const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
29630
+ if (dm)
29631
+ return dm.id;
29632
+ if (page.length < limit || data.pagination?.has_more === false)
29633
+ return null;
29634
+ }
29635
+ return null;
29636
+ }
29637
+ function personLabel(u) {
29638
+ const name = u.name ?? u.id;
29639
+ return u.handle ? `${name} (@${u.handle})` : name;
29640
+ }
28994
29641
  async function resolveConversation(auth, target) {
28995
29642
  const t = target.trim();
28996
- if (looksLikeConversationId(t))
28997
- return t;
29643
+ if (!t) {
29644
+ throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
29645
+ }
29646
+ const person = (u) => ({
29647
+ kind: "user",
29648
+ userId: u.id,
29649
+ label: `DM with ${personLabel(u)}`
29650
+ });
28998
29651
  if (t.startsWith("@")) {
28999
29652
  const handle = t.slice(1);
29000
- const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, { timeoutMs: 15000 });
29653
+ const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
29654
+ timeoutMs: 15000
29655
+ });
29001
29656
  if (!prof.user?.id)
29002
29657
  throw new Error(`No user @${handle}.`);
29003
- return findOrCreateDm(auth, prof.user.id);
29658
+ return person({ ...prof.user, id: prof.user.id });
29004
29659
  }
29005
- const res = await amikoWebFetch(auth, "/api/search", {
29006
- query: { q: t, type: "people", limit: 8 },
29007
- timeoutMs: 15000
29008
- });
29009
- const users = res.users ?? [];
29010
- if (users.length === 0) {
29011
- throw new Error(`No user matching "${t}". Use an exact @handle, or a conversation id from \`amiko chat list\`.`);
29660
+ const idish = normalizeUserId(t);
29661
+ if (looksLikeConversationId(idish)) {
29662
+ if (idish === t) {
29663
+ try {
29664
+ const conv = await fetchConversationById(auth, t);
29665
+ return {
29666
+ kind: "conversation",
29667
+ id: conv.id,
29668
+ label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`
29669
+ };
29670
+ } catch (e5) {
29671
+ const status = e5.status;
29672
+ if (status === 403) {
29673
+ throw new Error(`Conversation ${t} exists but you are not a participant.`);
29674
+ }
29675
+ if (status !== 404)
29676
+ throw e5;
29677
+ }
29678
+ }
29679
+ const user = await fetchUserById(auth, idish);
29680
+ if (!user) {
29681
+ throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
29682
+ }
29683
+ return person(user);
29012
29684
  }
29013
- if (users.length > 1) {
29014
- const names = users.map((u) => `${u.name ?? "?"} (@${u.handle ?? u.id.slice(0, 8)})`).join(", ");
29015
- throw new Error(`"${t}" is ambiguous — matches: ${names}. Use an exact @handle or a conversation id.`);
29685
+ const groupId = await findGroupIdByExactTitle(auth, t);
29686
+ if (groupId)
29687
+ return { kind: "conversation", id: groupId, label: `group "${t}"` };
29688
+ const { resolved, failures } = await resolveMembers(auth, [t]);
29689
+ if (failures.length || resolved.length !== 1) {
29690
+ const reason = failures[0]?.reason ?? "could not be resolved";
29691
+ throw new Error(`"${t}" ${reason}`);
29016
29692
  }
29017
- return findOrCreateDm(auth, users[0].id);
29693
+ const p = resolved[0];
29694
+ return person({ id: p.userId, name: p.name, handle: p.handle });
29018
29695
  }
29019
29696
  var HTTPS_RE = /^https:\/\//i;
29020
29697
  var CHAT_IMAGE_MIME = new Set([
@@ -29101,11 +29778,20 @@ function registerChatCommand(chat) {
29101
29778
  console.log(dim(` ${unread.trim()}`));
29102
29779
  }
29103
29780
  });
29104
- chat.command("read <target>").description("Read recent messages in a conversation (conversation id, @handle, or name)").option("--limit <n>", "Max messages", "20").option("--raw", "Output raw JSON").action(async (target, opts) => {
29781
+ chat.command("read <target>").description("Read recent messages in a conversation (conversation id, user id, @handle, or name)").option("--limit <n>", "Max messages", "20").option("--raw", "Output raw JSON").action(async (target, opts) => {
29105
29782
  const auth = authOrExit();
29106
29783
  let convId;
29107
29784
  try {
29108
- convId = await resolveConversation(auth, target);
29785
+ const resolved = await resolveConversation(auth, target);
29786
+ if (resolved.kind === "user") {
29787
+ const existing = await findDmByUserId(auth, resolved.userId);
29788
+ if (!existing) {
29789
+ throw new Error(`No ${resolved.label} yet — start one with \`amiko chat send\`.`);
29790
+ }
29791
+ convId = existing;
29792
+ } else {
29793
+ convId = resolved.id;
29794
+ }
29109
29795
  } catch (e5) {
29110
29796
  console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
29111
29797
  process.exit(1);
@@ -29136,26 +29822,33 @@ function registerChatCommand(chat) {
29136
29822
  console.log(dim(` ${when}`));
29137
29823
  }
29138
29824
  });
29139
- chat.command("send <target> <message>").description("Send a message as yourself (the owner) to a conversation (id, @handle, or name)").option("--image <pathOrUrl>", "Attach an image: local file (jpg/png/webp/gif, ≤10MB) or an Amiko URL (e.g. from `amiko create`)").option("--audio <pathOrUrl>", "Attach audio as a voice note: local file (≤25MB) or an Amiko URL").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
29825
+ chat.command("send <target> <message>").description("Send a message as yourself (the owner) to a conversation or person (conversation id, user id, @handle, or name — DMs are opened automatically)").option("--image <pathOrUrl>", "Attach an image: local file (jpg/png/webp/gif, ≤10MB) or an Amiko URL (e.g. from `amiko create`)").option("--audio <pathOrUrl>", "Attach audio as a voice note: local file (≤25MB) or an Amiko URL").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
29140
29826
  const auth = authOrExit();
29141
29827
  if (opts.image && opts.audio) {
29142
29828
  console.error(error("Attach either --image or --audio, not both (one media block per message)."));
29143
29829
  process.exit(1);
29144
29830
  }
29145
- let convId;
29831
+ let resolved;
29146
29832
  try {
29147
- convId = await resolveConversation(auth, target);
29833
+ resolved = await resolveConversation(auth, target);
29148
29834
  } catch (e5) {
29149
29835
  console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
29150
29836
  process.exit(1);
29151
29837
  }
29152
29838
  const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
29153
29839
  await requireApproval({
29154
- cost: "no charge — sends a message as you (the owner)",
29155
- summary: `Send to ${target}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
29840
+ free: "sends a message as you (the owner) — the recipients see it",
29841
+ summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
29156
29842
  yes: opts.yes,
29157
29843
  commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
29158
29844
  });
29845
+ let convId;
29846
+ try {
29847
+ convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
29848
+ } catch (e5) {
29849
+ console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
29850
+ process.exit(1);
29851
+ }
29159
29852
  const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
29160
29853
  const post = (body) => amikoWebFetch(auth, messagesPath, {
29161
29854
  method: "POST",
@@ -29192,6 +29885,8 @@ function registerChatCommand(chat) {
29192
29885
  const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : "Message sent";
29193
29886
  console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
29194
29887
  });
29888
+ const group = chat.command("group").description("Group chats — create, list, info, rename, add/remove members, leave");
29889
+ registerChatGroupCommand(group);
29195
29890
  }
29196
29891
 
29197
29892
  // src/commands/card.ts
@@ -29407,24 +30102,29 @@ function registerCardCommand(card) {
29407
30102
  }
29408
30103
 
29409
30104
  // src/commands/amazon.ts
29410
- var TREASURY3 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
29411
- async function payAmiko(auth, wallet, amount2) {
29412
- const data = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(wallet)}/transfer`, {
29413
- method: "POST",
29414
- body: { to: TREASURY3, amount: amount2, token: "AMIKO", chain: "solana" }
29415
- });
29416
- if (!data.txHash)
29417
- throw new Error("No tx hash returned.");
29418
- return data.txHash;
29419
- }
30105
+ var AMAZON_SEARCH_USD = 0.01;
29420
30106
  function registerAmazonCommand(program2) {
29421
- const cmd = program2.command("amazon").description("Search and buy products on Amazon — paid with AMIKO");
29422
- 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) => {
30107
+ const cmd = program2.command("amazon").description("Search and buy products on Amazon — pay in AMIKO, USDC, or USDT");
30108
+ cmd.command("search <query>").description("Search Amazon products — $0.01, paid in AMIKO, USDC, or USDT").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("--token <token>", "Payment token (AMIKO, USDC, USDT)", "AMIKO").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (query, opts) => {
30109
+ let payToken;
30110
+ try {
30111
+ payToken = normalizePaymentToken(opts.token);
30112
+ } catch (e5) {
30113
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30114
+ process.exit(1);
30115
+ }
30116
+ let quote;
30117
+ try {
30118
+ quote = await quoteTokenAmount(resolveAuth(), payToken, AMAZON_SEARCH_USD);
30119
+ } catch (e5) {
30120
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30121
+ process.exit(1);
30122
+ }
29423
30123
  await requireApproval({
29424
- cost: "1 AMIKO",
30124
+ cost: `${quote.amount} ${payToken} (≈ $${AMAZON_SEARCH_USD.toFixed(2)})`,
29425
30125
  summary: `Amazon product search: ${query.slice(0, 80)}${query.length > 80 ? "…" : ""}`,
29426
30126
  yes: opts.yes,
29427
- commandExample: `amiko markets amazon search ${JSON.stringify(query)}`
30127
+ commandExample: `amiko markets amazon search ${JSON.stringify(query)}${payToken !== "AMIKO" ? ` --token ${payToken}` : ""}`
29428
30128
  });
29429
30129
  const spinner = ora("Searching Amazon...").start();
29430
30130
  try {
@@ -29446,14 +30146,14 @@ function registerAmazonCommand(program2) {
29446
30146
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29447
30147
  process.exit(1);
29448
30148
  }
29449
- spinner.text = "Paying 1 AMIKO...";
29450
- const txHash = await payAmiko(auth, wallet, "1");
30149
+ spinner.text = `Paying ${quote.amount} ${payToken}...`;
30150
+ const txHash = await payTreasury(auth, { wallet, amount: quote.amount, token: payToken });
29451
30151
  spinner.text = "Searching Amazon...";
29452
30152
  const params = new URLSearchParams({
29453
30153
  q: `site:amazon.com ${query}`
29454
30154
  });
29455
30155
  const res = await fetch(`${mppBase2}/v1/search/google_shopping?${params}`, {
29456
- headers: { "X-Payment-Tx": txHash, "X-Twin-Id": auth.twinId }
30156
+ headers: { "X-Payment-Tx": txHash, "X-Payment-Token": payToken, "X-Twin-Id": auth.twinId }
29457
30157
  });
29458
30158
  spinner.stop();
29459
30159
  if (!res.ok) {
@@ -29550,52 +30250,52 @@ function registerAmazonCommand(program2) {
29550
30250
  }
29551
30251
 
29552
30252
  // src/commands/services.ts
29553
- var TREASURY4 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
29554
30253
  function usdToAmiko(usd2, pricePerAmiko) {
29555
30254
  if (pricePerAmiko <= 0)
29556
30255
  return "1";
29557
30256
  const amount2 = usd2 / pricePerAmiko;
29558
30257
  return Math.ceil(amount2 * 100) / 100 + "";
29559
30258
  }
29560
- async function payAndCall(mppBase2, method, path2, auth, wallet, amountOrAuto, body, onProgress, token = "AMIKO") {
30259
+ async function payAndCall(mppBase2, method, path2, auth, wallet, amountOrAuto, body, onProgress, rawToken = "AMIKO") {
30260
+ const token = normalizePaymentToken(rawToken);
29561
30261
  if (!path2.startsWith("/")) {
29562
30262
  throw new Error(`path must start with "/" — got: ${path2}`);
29563
30263
  }
29564
30264
  let amount2 = amountOrAuto;
29565
30265
  if (amount2 === "auto") {
29566
30266
  onProgress?.("Fetching price...");
30267
+ let usdPrice = 0;
29567
30268
  try {
29568
30269
  const specRes = await fetchWithTimeout2(`${mppBase2}/openapi.json`, {}, 15000);
29569
30270
  if (specRes.ok) {
29570
30271
  const spec = await specRes.json();
29571
30272
  const pathSpec = spec.paths?.[path2];
29572
30273
  const methodSpec = pathSpec?.[method.toLowerCase()];
29573
- const usdPrice = parseFloat(methodSpec?.["x-payment-info"]?.price || "0");
29574
- if (usdPrice > 0) {
29575
- const amikoPrice = await getTokenUsdPrice(auth, "AMIKO") ?? 0;
29576
- amount2 = usdToAmiko(usdPrice, amikoPrice);
29577
- onProgress?.(`Price: $${usdPrice.toFixed(4)} = ${amount2} AMIKO`);
29578
- } else {
29579
- amount2 = "1";
29580
- }
29581
- } else {
29582
- amount2 = "1";
30274
+ usdPrice = parseFloat(methodSpec?.["x-payment-info"]?.price || "0");
29583
30275
  }
29584
30276
  } catch {
30277
+ usdPrice = 0;
30278
+ }
30279
+ if (token === "USDC" || token === "USDT") {
30280
+ if (!(usdPrice > 0)) {
30281
+ throw new Error(`Could not auto-price ${path2} in USD — pass an explicit amount (e.g. --${token.toLowerCase()} 0.01).`);
30282
+ }
30283
+ amount2 = usdPrice.toFixed(6);
30284
+ onProgress?.(`Price: $${usdPrice.toFixed(4)} = ${amount2} ${token}`);
30285
+ } else if (usdPrice > 0) {
30286
+ const amikoPrice = await getTokenUsdPrice(auth, "AMIKO") ?? 0;
30287
+ amount2 = usdToAmiko(usdPrice, amikoPrice);
30288
+ onProgress?.(`Price: $${usdPrice.toFixed(4)} = ${amount2} AMIKO`);
30289
+ } else {
29585
30290
  amount2 = "1";
29586
30291
  }
29587
30292
  }
29588
30293
  onProgress?.(`Paying ${amount2} ${token}...`);
29589
- const transferData = await amikoWebFetch(auth, `/api/agents/${encodeURIComponent(auth.twinId)}/wallets/${encodeURIComponent(wallet)}/transfer`, {
29590
- method: "POST",
29591
- body: { to: TREASURY4, amount: amount2, token, chain: "solana" }
29592
- });
29593
- const txHash = transferData.txHash;
29594
- if (!txHash)
29595
- throw new Error("Payment sent but no tx hash returned.");
30294
+ const txHash = await payTreasury(auth, { wallet, amount: amount2, token });
29596
30295
  const headers = {
29597
30296
  "Content-Type": "application/json",
29598
30297
  "X-Payment-Tx": txHash,
30298
+ "X-Payment-Token": token,
29599
30299
  "X-Twin-Id": auth.twinId
29600
30300
  };
29601
30301
  const fetchOpts = { method, headers };
@@ -29621,7 +30321,7 @@ async function fetchWithTimeout2(url2, init = {}, timeoutMs) {
29621
30321
  }
29622
30322
  }
29623
30323
  function registerServicesCommand(program2) {
29624
- const svc = program2.command("service").alias("svc").description("Call any MPP service — pays with AMIKO tokens");
30324
+ const svc = program2.command("service").alias("svc").description("Call any MPP service — pay in AMIKO, USDC, or USDT");
29625
30325
  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) => {
29626
30326
  await requireApproval({
29627
30327
  cost: "1 AMIKO",
@@ -29691,13 +30391,13 @@ function registerServicesCommand(program2) {
29691
30391
  opts
29692
30392
  });
29693
30393
  });
29694
- 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) => {
29695
- const costLabel = opts.usdc ? `${opts.usdc} USDC` : opts.amiko ? `${opts.amiko} AMIKO` : "auto-priced AMIKO (check /v1/ list for endpoint price first)";
30394
+ svc.command("call <method> <path> [body]").description("Call any MPP endpoint — pays in AMIKO, USDC, or USDT on Solana").option("--amiko <amount>", "AMIKO tokens to pay (default: auto-price from USD)").option("--usdc <amount>", 'Pay in USDC instead of AMIKO ("auto" to price from USD)').option("--usdt <amount>", 'Pay in USDT instead of AMIKO ("auto" to price from USD)').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) => {
30395
+ const costLabel = opts.usdt ? `${opts.usdt} USDT` : opts.usdc ? `${opts.usdc} USDC` : opts.amiko ? `${opts.amiko} AMIKO` : "auto-priced AMIKO (check /v1/ list for endpoint price first)";
29696
30396
  await requireApproval({
29697
30397
  cost: costLabel,
29698
30398
  summary: `${method.toUpperCase()} ${path2}`,
29699
30399
  yes: opts.yes,
29700
- commandExample: `amiko markets service call ${method.toUpperCase()} ${path2}${bodyStr ? ` ${JSON.stringify(bodyStr)}` : ""}${opts.amiko ? ` --amiko ${opts.amiko}` : ""}${opts.usdc ? ` --usdc ${opts.usdc}` : ""}`
30400
+ commandExample: `amiko markets service call ${method.toUpperCase()} ${path2}${bodyStr ? ` ${JSON.stringify(bodyStr)}` : ""}${opts.amiko ? ` --amiko ${opts.amiko}` : ""}${opts.usdc ? ` --usdc ${opts.usdc}` : ""}${opts.usdt ? ` --usdt ${opts.usdt}` : ""}`
29701
30401
  });
29702
30402
  let body;
29703
30403
  if (bodyStr) {
@@ -29712,8 +30412,9 @@ function registerServicesCommand(program2) {
29712
30412
  method: method.toUpperCase(),
29713
30413
  path: path2,
29714
30414
  body,
29715
- amiko: opts.amiko || (opts.usdc ? undefined : "auto"),
30415
+ amiko: opts.amiko || (opts.usdc || opts.usdt ? undefined : "auto"),
29716
30416
  usdc: opts.usdc,
30417
+ usdt: opts.usdt,
29717
30418
  opts
29718
30419
  });
29719
30420
  });
@@ -29764,8 +30465,8 @@ async function callService(params) {
29764
30465
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29765
30466
  process.exit(1);
29766
30467
  }
29767
- const token = params.usdc ? "USDC" : "AMIKO";
29768
- const amount2 = params.usdc || params.amiko || "auto";
30468
+ const token = params.usdt ? "USDT" : params.usdc ? "USDC" : "AMIKO";
30469
+ const amount2 = params.usdt || params.usdc || params.amiko || "auto";
29769
30470
  const { res, txHash, amikoSpent } = await payAndCall(mppBase2, params.method, params.path, auth, wallet, amount2, params.body, (msg) => {
29770
30471
  spinner.text = msg;
29771
30472
  }, token);
@@ -29956,6 +30657,18 @@ function registerTwinCommand(program2) {
29956
30657
  // src/commands/drive.ts
29957
30658
  import { writeFile as writeFile2 } from "node:fs/promises";
29958
30659
  import { basename as basename2, resolve as resolvePath } from "node:path";
30660
+ var DIRECT_UPLOAD_MAX_BYTES = 4 * 1024 * 1024;
30661
+ var MAX_DRIVE_UPLOAD_BYTES = 200 * 1024 * 1024;
30662
+ var PRESIGNED_PUT_TIMEOUT_MS = 10 * 60 * 1000;
30663
+ function shareBase(auth) {
30664
+ return (auth.platformUrl ?? "https://platform.heyamiko.com").replace(/\/$/, "");
30665
+ }
30666
+ function fileShareUrl(auth, slug) {
30667
+ return `${shareBase(auth)}/d/${slug}`;
30668
+ }
30669
+ function folderShareUrl(auth, slug) {
30670
+ return `${shareBase(auth)}/d/f/${slug}`;
30671
+ }
29959
30672
  function fmtBytes(n) {
29960
30673
  if (!n || n <= 0)
29961
30674
  return "-";
@@ -29968,7 +30681,7 @@ function fmtBytes(n) {
29968
30681
  }
29969
30682
  return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
29970
30683
  }
29971
- function statusOf2(doc2) {
30684
+ function statusOf3(doc2) {
29972
30685
  const meta3 = doc2.metadata?.status;
29973
30686
  if (meta3)
29974
30687
  return meta3;
@@ -29999,6 +30712,7 @@ function renderDocTable(docs) {
29999
30712
  dim("TYPE"),
30000
30713
  dim("FOLDER"),
30001
30714
  dim("STATUS"),
30715
+ dim("PUBLIC"),
30002
30716
  dim("CREATED")
30003
30717
  ],
30004
30718
  ...docs.map((doc2) => [
@@ -30006,7 +30720,8 @@ function renderDocTable(docs) {
30006
30720
  doc2.title || doc2.filename,
30007
30721
  doc2.file_type || "-",
30008
30722
  doc2.folder_id || dim("root"),
30009
- statusOf2(doc2),
30723
+ statusOf3(doc2),
30724
+ doc2.is_public ? "yes" : dim("-"),
30010
30725
  doc2.created_at?.slice(0, 10) || "-"
30011
30726
  ])
30012
30727
  ];
@@ -30079,26 +30794,56 @@ Search hits filename, title, description, and parsed RAG content (once processed
30079
30794
  try {
30080
30795
  const file2 = await resolveFileInput(source, {
30081
30796
  stdin: opts.stdin,
30082
- hintName: opts.name
30797
+ hintName: opts.name,
30798
+ maxBytes: MAX_DRIVE_UPLOAD_BYTES
30083
30799
  });
30084
30800
  if (spinner)
30085
- spinner.text = `Uploading ${file2.name}...`;
30086
- const form = new FormData;
30087
- form.set("file", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
30088
- const upload = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/upload`, {
30089
- method: "POST",
30090
- multipart: form
30091
- });
30092
- if (!upload.url || !upload.filename || !upload.fileType) {
30093
- throw new Error(`Upload response missing required fields: ${JSON.stringify(upload)}`);
30801
+ spinner.text = `Uploading ${file2.name} (${fmtBytes(file2.size)})...`;
30802
+ let uploadedUrl;
30803
+ let uploadedName;
30804
+ let uploadedType;
30805
+ if (file2.size > DIRECT_UPLOAD_MAX_BYTES) {
30806
+ const pres = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/presigned-url`, {
30807
+ method: "POST",
30808
+ body: { filename: file2.name }
30809
+ });
30810
+ if (!pres.uploadUrl || !pres.publicUrl) {
30811
+ throw new Error(`Presigned-URL response missing fields: ${JSON.stringify(pres)}`);
30812
+ }
30813
+ const putRes = await fetch(pres.uploadUrl, {
30814
+ method: "PUT",
30815
+ headers: { "Content-Type": file2.mime },
30816
+ body: new Uint8Array(file2.buffer),
30817
+ signal: AbortSignal.timeout(PRESIGNED_PUT_TIMEOUT_MS)
30818
+ });
30819
+ if (!putRes.ok) {
30820
+ const detail = await putRes.text().catch(() => "");
30821
+ throw new Error(`Direct storage upload failed: ${putRes.status} ${putRes.statusText}${detail ? ` — ${detail.slice(0, 200)}` : ""}`);
30822
+ }
30823
+ uploadedUrl = pres.publicUrl;
30824
+ uploadedName = file2.name;
30825
+ uploadedType = file2.mime;
30826
+ } else {
30827
+ const form = new FormData;
30828
+ form.set("file", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
30829
+ const upload = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/upload`, {
30830
+ method: "POST",
30831
+ multipart: form
30832
+ });
30833
+ if (!upload.url || !upload.filename || !upload.fileType) {
30834
+ throw new Error(`Upload response missing required fields: ${JSON.stringify(upload)}`);
30835
+ }
30836
+ uploadedUrl = upload.url;
30837
+ uploadedName = upload.filename;
30838
+ uploadedType = upload.fileType;
30094
30839
  }
30095
30840
  if (spinner)
30096
- spinner.text = `Registering ${upload.filename}...`;
30097
- const title = opts.title || upload.filename.replace(/\.[^.]+$/, "");
30841
+ spinner.text = `Registering ${uploadedName}...`;
30842
+ const title = opts.title || uploadedName.replace(/\.[^.]+$/, "");
30098
30843
  const body = {
30099
- filename: upload.filename,
30100
- fileUrl: upload.url,
30101
- fileType: upload.fileType,
30844
+ filename: uploadedName,
30845
+ fileUrl: uploadedUrl,
30846
+ fileType: uploadedType,
30102
30847
  title,
30103
30848
  doc_type: opts.docType ?? "other"
30104
30849
  };
@@ -30114,10 +30859,8 @@ Search hits filename, title, description, and parsed RAG content (once processed
30114
30859
  console.log(label("Title", d.title || "-"));
30115
30860
  if (d.folder_id)
30116
30861
  console.log(label("Folder", d.folder_id));
30117
- if (upload.fileSize)
30118
- console.log(label("Size", fmtBytes(upload.fileSize)));
30119
- if (upload.fileType)
30120
- console.log(label("Type", upload.fileType));
30862
+ console.log(label("Size", fmtBytes(file2.size)));
30863
+ console.log(label("Type", uploadedType));
30121
30864
  if (d.job_id) {
30122
30865
  console.log(label("RAG job", d.job_id));
30123
30866
  console.log(dim("Processing runs in the background. Use `amiko drive status <id>` to check."));
@@ -30195,9 +30938,15 @@ Search hits filename, title, description, and parsed RAG content (once processed
30195
30938
  console.log(label("Folder", d.doc.folder_id));
30196
30939
  if (d.doc.description)
30197
30940
  console.log(label("Description", d.doc.description));
30198
- console.log(label("Status", statusOf2(d.doc)));
30941
+ console.log(label("Status", statusOf3(d.doc)));
30199
30942
  if (d.doc.metadata?.error)
30200
30943
  console.log(label("Error", d.doc.metadata.error));
30944
+ if (d.doc.is_public && d.doc.public_slug) {
30945
+ console.log(label("Public", "yes"));
30946
+ console.log(label("Share URL", fileShareUrl(auth, d.doc.public_slug)));
30947
+ } else {
30948
+ console.log(label("Public", "no"));
30949
+ }
30201
30950
  console.log(label("Signed URL", d.signed_url));
30202
30951
  if (opts.content && d.doc.content) {
30203
30952
  console.log(heading(`
@@ -30259,6 +31008,40 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
30259
31008
  process.exit(1);
30260
31009
  }
30261
31010
  });
31011
+ program2.command("share <docId>").description("Make a file public and print its share link").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (docId, opts) => {
31012
+ const { twinId, auth } = twinFromFlag(opts);
31013
+ await confirmDestructive({
31014
+ action: `Make file ${docId} PUBLIC`,
31015
+ detail: "Anyone with the link can view and download it — no Amiko login required. The link stays stable across unshare/re-share.",
31016
+ yes: opts.yes,
31017
+ commandExample: `amiko drive share ${docId}`
31018
+ });
31019
+ try {
31020
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "PATCH", body: { isPublic: true } });
31021
+ renderOutput(result, (r) => {
31022
+ console.log(success(`${r.doc.title || r.doc.filename} is now public`));
31023
+ if (r.doc.public_slug)
31024
+ console.log(label("Share URL", fileShareUrl(auth, r.doc.public_slug)));
31025
+ console.log(dim("Revoke anytime: amiko drive unshare " + docId));
31026
+ }, { json: opts.json });
31027
+ } catch (e5) {
31028
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
31029
+ process.exit(1);
31030
+ }
31031
+ });
31032
+ program2.command("unshare <docId>").description("Revoke a file's public share link").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (docId, opts) => {
31033
+ const { twinId, auth } = twinFromFlag(opts);
31034
+ try {
31035
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "PATCH", body: { isPublic: false } });
31036
+ renderOutput(result, (r) => {
31037
+ console.log(success(`${r.doc.title || r.doc.filename} is private again`));
31038
+ console.log(dim("The link no longer works. Re-sharing later restores the SAME link. Note: the file stays reachable if a parent folder is still shared."));
31039
+ }, { json: opts.json });
31040
+ } catch (e5) {
31041
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
31042
+ process.exit(1);
31043
+ }
31044
+ });
30262
31045
  program2.command("presign <name>").description("Get a presigned upload URL (browser-direct uploads; no upload performed)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (name, opts) => {
30263
31046
  const { twinId, auth } = twinFromFlag(opts);
30264
31047
  try {
@@ -30300,11 +31083,12 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
30300
31083
  }
30301
31084
  console.log(heading(`Folders (${d.folders.length})`));
30302
31085
  const rows = [
30303
- [dim("ID"), dim("NAME"), dim("PARENT"), dim("CREATED")],
31086
+ [dim("ID"), dim("NAME"), dim("PARENT"), dim("PUBLIC"), dim("CREATED")],
30304
31087
  ...d.folders.map((f) => [
30305
31088
  f.id,
30306
31089
  f.name,
30307
31090
  f.parent_id || dim("root"),
31091
+ f.is_public ? "yes" : dim("-"),
30308
31092
  f.created_at?.slice(0, 10) || "-"
30309
31093
  ])
30310
31094
  ];
@@ -30353,6 +31137,40 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
30353
31137
  process.exit(1);
30354
31138
  }
30355
31139
  });
31140
+ folder.command("share <folderId>").description("Make a folder public (shares its ENTIRE subtree) and print the link").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (folderId, opts) => {
31141
+ const { twinId, auth } = twinFromFlag(opts);
31142
+ await confirmDestructive({
31143
+ action: `Make folder ${folderId} PUBLIC`,
31144
+ detail: "Anyone with the link can browse and download EVERY file and subfolder inside it — no Amiko login required. Files added later are exposed too.",
31145
+ yes: opts.yes,
31146
+ commandExample: `amiko drive folder share ${folderId}`
31147
+ });
31148
+ try {
31149
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders/${folderId}`, { method: "PATCH", body: { isPublic: true } });
31150
+ renderOutput(result, (r) => {
31151
+ console.log(success(`Folder "${r.folder.name}" is now public`));
31152
+ if (r.folder.public_slug)
31153
+ console.log(label("Share URL", folderShareUrl(auth, r.folder.public_slug)));
31154
+ console.log(dim("Revoke anytime: amiko drive folder unshare " + folderId));
31155
+ }, { json: opts.json });
31156
+ } catch (e5) {
31157
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
31158
+ process.exit(1);
31159
+ }
31160
+ });
31161
+ folder.command("unshare <folderId>").description("Revoke a folder's public share link (instantly unshares the subtree)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (folderId, opts) => {
31162
+ const { twinId, auth } = twinFromFlag(opts);
31163
+ try {
31164
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders/${folderId}`, { method: "PATCH", body: { isPublic: false } });
31165
+ renderOutput(result, (r) => {
31166
+ console.log(success(`Folder "${r.folder.name}" is private again`));
31167
+ console.log(dim("The link no longer works and the whole subtree is unshared (unless an ancestor folder or individual file is still public). Re-sharing restores the SAME link."));
31168
+ }, { json: opts.json });
31169
+ } catch (e5) {
31170
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
31171
+ process.exit(1);
31172
+ }
31173
+ });
30356
31174
  folder.command("delete <folderId>").description("Delete a folder (use --force to delete non-empty folders + their contents)").option("--twin <idOrName>", "Target twin id or name").option("--force", "Recursively delete subfolders and files in the subtree").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (folderId, opts) => {
30357
31175
  const { twinId, auth } = twinFromFlag(opts);
30358
31176
  await confirmDestructive({
@@ -32186,10 +33004,10 @@ var didOnboard = await runOnboarding(process.argv);
32186
33004
  if (didOnboard)
32187
33005
  process.exit(0);
32188
33006
  var program2 = new Command;
32189
- program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace services").version(PKG_VERSION);
33007
+ program2.name("amiko").description("Amiko CLI — Create Studio media generation, chats, drive, credits & wallets, social graph, and paid MPP marketplace services").version(PKG_VERSION);
32190
33008
  var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
32191
33009
  var create2 = program2.command("create").description("Create Studio — generate images, video, speech, music, and SFX (charged on success)");
32192
- var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send");
33010
+ var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send, manage groups");
32193
33011
  var card = program2.command("card").description("Twin Cards (work/play/love) — generate, view, download, mint");
32194
33012
  var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
32195
33013
  var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");