@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/README.md +65 -11
- package/dist/index.js +968 -150
- package/package.json +1 -1
- package/skills/SKILL.md +48 -10
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
|
-
|
|
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
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
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/
|
|
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 —
|
|
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:
|
|
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)}${
|
|
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
|
|
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} (
|
|
28399
|
+
spinner.text = `Paying ${amount2} ${token} (solana)...`;
|
|
28343
28400
|
let txHash;
|
|
28344
28401
|
try {
|
|
28345
|
-
|
|
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 —
|
|
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
|
-
|
|
28465
|
-
|
|
28466
|
-
|
|
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
|
|
28525
|
+
const payAmount = quote.amount;
|
|
28470
28526
|
await requireApproval({
|
|
28471
|
-
cost: `${
|
|
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 ${
|
|
28571
|
+
setStatus(`Paying ${payAmount} ${payToken}...`);
|
|
28516
28572
|
let txHash;
|
|
28517
28573
|
try {
|
|
28518
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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 >
|
|
28883
|
-
throw new Error(`File too large: ${buffer.length} bytes exceeds ${
|
|
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 >
|
|
28896
|
-
throw new Error(`stdin exceeded ${
|
|
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 >
|
|
28912
|
-
throw new Error(`Remote file too large: ${len} bytes exceeds ${
|
|
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 >
|
|
28916
|
-
throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${
|
|
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/
|
|
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
|
|
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 (
|
|
28997
|
-
|
|
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)}`, {
|
|
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
|
|
29658
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
29004
29659
|
}
|
|
29005
|
-
const
|
|
29006
|
-
|
|
29007
|
-
|
|
29008
|
-
|
|
29009
|
-
|
|
29010
|
-
|
|
29011
|
-
|
|
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
|
-
|
|
29014
|
-
|
|
29015
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
29831
|
+
let resolved;
|
|
29146
29832
|
try {
|
|
29147
|
-
|
|
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
|
-
|
|
29155
|
-
summary: `Send to ${
|
|
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
|
|
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 —
|
|
29422
|
-
cmd.command("search <query>").description("Search Amazon products —
|
|
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:
|
|
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 =
|
|
29450
|
-
const txHash = await
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30087
|
-
|
|
30088
|
-
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
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 ${
|
|
30097
|
-
const title = opts.title ||
|
|
30841
|
+
spinner.text = `Registering ${uploadedName}...`;
|
|
30842
|
+
const title = opts.title || uploadedName.replace(/\.[^.]+$/, "");
|
|
30098
30843
|
const body = {
|
|
30099
|
-
filename:
|
|
30100
|
-
fileUrl:
|
|
30101
|
-
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
|
-
|
|
30118
|
-
|
|
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",
|
|
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 —
|
|
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)");
|