@heyamiko/amiko-cli 0.14.0-beta.1 → 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 +53 -6
- package/dist/index.js +850 -73
- package/package.json +1 -1
- package/skills/SKILL.md +46 -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
|
}
|
|
@@ -28459,7 +28473,7 @@ function computeImageUsd(model, quality, size3, n) {
|
|
|
28459
28473
|
return Number((pricing[q][tier] * clamped * IMAGE_MARKUP).toFixed(6));
|
|
28460
28474
|
}
|
|
28461
28475
|
function registerImageCommand(program2) {
|
|
28462
|
-
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) => {
|
|
28463
28477
|
if (!SUPPORTED_MODELS.includes(opts.model)) {
|
|
28464
28478
|
console.error(error(`Unknown model "${opts.model}". Supported: ${SUPPORTED_MODELS.join(", ")}.`));
|
|
28465
28479
|
process.exit(1);
|
|
@@ -28633,6 +28647,15 @@ var ETA = {
|
|
|
28633
28647
|
function statusOf(err) {
|
|
28634
28648
|
return err?.status;
|
|
28635
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
|
+
}
|
|
28636
28659
|
function printCompleted(final, title) {
|
|
28637
28660
|
console.log(heading(`${title}
|
|
28638
28661
|
`));
|
|
@@ -28644,7 +28667,9 @@ function printCompleted(final, title) {
|
|
|
28644
28667
|
console.log(label("Type", final.assetMime));
|
|
28645
28668
|
console.log("");
|
|
28646
28669
|
if (final.charged) {
|
|
28647
|
-
|
|
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)}…`) : ""));
|
|
28648
28673
|
} else {
|
|
28649
28674
|
console.log(dim("Not charged (settlement pending or waived)."));
|
|
28650
28675
|
}
|
|
@@ -28713,9 +28738,12 @@ async function submitCreateJob(opts) {
|
|
|
28713
28738
|
console.log(label("Status", "PENDING — generating in the background"));
|
|
28714
28739
|
console.log(label("Check result", `amiko create status ${jobId}`));
|
|
28715
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
|
+
}
|
|
28716
28744
|
}
|
|
28717
28745
|
function registerCreateCommand(create2) {
|
|
28718
|
-
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) => {
|
|
28719
28747
|
await submitCreateJob({
|
|
28720
28748
|
mode: "IMAGE",
|
|
28721
28749
|
summary: `Create image: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
|
|
@@ -28731,12 +28759,19 @@ function registerCreateCommand(create2) {
|
|
|
28731
28759
|
}
|
|
28732
28760
|
});
|
|
28733
28761
|
});
|
|
28734
|
-
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) => {
|
|
28735
28763
|
const seconds = Number(opts.seconds);
|
|
28736
28764
|
if (seconds !== 6 && seconds !== 10) {
|
|
28737
28765
|
console.error(error("--seconds must be 6 or 10."));
|
|
28738
28766
|
process.exit(1);
|
|
28739
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;
|
|
28740
28775
|
await submitCreateJob({
|
|
28741
28776
|
mode: "VIDEO",
|
|
28742
28777
|
summary: `Create video: ${prompt.slice(0, 80)}${prompt.length > 80 ? "…" : ""}`,
|
|
@@ -28756,6 +28791,19 @@ function registerCreateCommand(create2) {
|
|
|
28756
28791
|
seconds,
|
|
28757
28792
|
...opts.aspect ? { aspectRatio: opts.aspect } : {},
|
|
28758
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 } : {},
|
|
28759
28807
|
...opts.token ? { preferredToken: opts.token } : {}
|
|
28760
28808
|
}
|
|
28761
28809
|
});
|
|
@@ -28776,7 +28824,7 @@ function registerCreateCommand(create2) {
|
|
|
28776
28824
|
}
|
|
28777
28825
|
});
|
|
28778
28826
|
});
|
|
28779
|
-
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("--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) => {
|
|
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) => {
|
|
28780
28828
|
if (!prompt && !opts.lyrics) {
|
|
28781
28829
|
console.error(error("Provide a prompt or --lyrics."));
|
|
28782
28830
|
process.exit(1);
|
|
@@ -28900,41 +28948,42 @@ import { basename, extname } from "node:path";
|
|
|
28900
28948
|
var MAX_BYTES = 100 * 1024 * 1024;
|
|
28901
28949
|
var FETCH_TIMEOUT_MS = 30000;
|
|
28902
28950
|
async function resolveFileInput(src, opts = {}) {
|
|
28951
|
+
const maxBytes = opts.maxBytes ?? MAX_BYTES;
|
|
28903
28952
|
if (src === "-" || opts.stdin) {
|
|
28904
|
-
const buffer2 = await readStdin();
|
|
28953
|
+
const buffer2 = await readStdin(maxBytes);
|
|
28905
28954
|
const name2 = opts.hintName ?? "stdin";
|
|
28906
28955
|
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28907
28956
|
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28908
28957
|
}
|
|
28909
28958
|
if (/^https?:\/\//i.test(src)) {
|
|
28910
|
-
const buffer2 = await fetchRemote(src);
|
|
28959
|
+
const buffer2 = await fetchRemote(src, maxBytes);
|
|
28911
28960
|
const urlPath = new URL(src).pathname;
|
|
28912
28961
|
const name2 = opts.hintName ?? (basename(urlPath) || "download");
|
|
28913
28962
|
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28914
28963
|
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28915
28964
|
}
|
|
28916
28965
|
const buffer = await readFile(src);
|
|
28917
|
-
if (buffer.length >
|
|
28918
|
-
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.`);
|
|
28919
28968
|
}
|
|
28920
28969
|
const name = opts.hintName ?? basename(src);
|
|
28921
28970
|
const mime = opts.hintMime ?? detectMime(buffer, name);
|
|
28922
28971
|
return { buffer, name, mime, size: buffer.length };
|
|
28923
28972
|
}
|
|
28924
|
-
async function readStdin() {
|
|
28973
|
+
async function readStdin(maxBytes) {
|
|
28925
28974
|
const chunks = [];
|
|
28926
28975
|
let total = 0;
|
|
28927
28976
|
for await (const chunk of process.stdin) {
|
|
28928
28977
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28929
28978
|
total += buf.length;
|
|
28930
|
-
if (total >
|
|
28931
|
-
throw new Error(`stdin exceeded ${
|
|
28979
|
+
if (total > maxBytes) {
|
|
28980
|
+
throw new Error(`stdin exceeded ${maxBytes} byte cap.`);
|
|
28932
28981
|
}
|
|
28933
28982
|
chunks.push(buf);
|
|
28934
28983
|
}
|
|
28935
28984
|
return Buffer.concat(chunks);
|
|
28936
28985
|
}
|
|
28937
|
-
async function fetchRemote(url2) {
|
|
28986
|
+
async function fetchRemote(url2, maxBytes) {
|
|
28938
28987
|
const controller = new AbortController;
|
|
28939
28988
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
28940
28989
|
try {
|
|
@@ -28943,12 +28992,12 @@ async function fetchRemote(url2) {
|
|
|
28943
28992
|
throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
|
|
28944
28993
|
}
|
|
28945
28994
|
const len = Number(res.headers.get("content-length") || 0);
|
|
28946
|
-
if (len >
|
|
28947
|
-
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.`);
|
|
28948
28997
|
}
|
|
28949
28998
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
28950
|
-
if (buf.length >
|
|
28951
|
-
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.`);
|
|
28952
29001
|
}
|
|
28953
29002
|
return buf;
|
|
28954
29003
|
} finally {
|
|
@@ -28979,6 +29028,13 @@ var EXT_MIME = {
|
|
|
28979
29028
|
".mp3": "audio/mpeg",
|
|
28980
29029
|
".m4a": "audio/mp4",
|
|
28981
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",
|
|
28982
29038
|
".zip": "application/zip"
|
|
28983
29039
|
};
|
|
28984
29040
|
function detectMime(buffer, name) {
|
|
@@ -28991,12 +29047,15 @@ function detectMime(buffer, name) {
|
|
|
28991
29047
|
return EXT_MIME[ext] ?? "application/octet-stream";
|
|
28992
29048
|
}
|
|
28993
29049
|
|
|
28994
|
-
// src/
|
|
29050
|
+
// src/lib/conversations.ts
|
|
28995
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;
|
|
28996
29052
|
var CUID_RE = /^c[a-z0-9]{20,}$/i;
|
|
28997
29053
|
function looksLikeConversationId(t) {
|
|
28998
29054
|
return UUID_RE.test(t) || CUID_RE.test(t);
|
|
28999
29055
|
}
|
|
29056
|
+
function normalizeUserId(id) {
|
|
29057
|
+
return id.replace(/^did:privy:/, "");
|
|
29058
|
+
}
|
|
29000
29059
|
function authOrExit() {
|
|
29001
29060
|
try {
|
|
29002
29061
|
return requireAuth();
|
|
@@ -29008,9 +29067,531 @@ function authOrExit() {
|
|
|
29008
29067
|
function convLabel(conv, selfUserId) {
|
|
29009
29068
|
if (conv.title)
|
|
29010
29069
|
return conv.title;
|
|
29011
|
-
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);
|
|
29012
29072
|
return others.length ? others.join(", ") : "(conversation)";
|
|
29013
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
|
|
29014
29595
|
async function findOrCreateDm(auth, userId) {
|
|
29015
29596
|
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
29016
29597
|
method: "POST",
|
|
@@ -29026,30 +29607,91 @@ async function findOrCreateDm(auth, userId) {
|
|
|
29026
29607
|
throw new Error("Could not open a conversation with that user.");
|
|
29027
29608
|
return id;
|
|
29028
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
|
+
}
|
|
29029
29641
|
async function resolveConversation(auth, target) {
|
|
29030
29642
|
const t = target.trim();
|
|
29031
|
-
if (
|
|
29032
|
-
|
|
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
|
+
});
|
|
29033
29651
|
if (t.startsWith("@")) {
|
|
29034
29652
|
const handle = t.slice(1);
|
|
29035
|
-
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
29653
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
29654
|
+
timeoutMs: 15000
|
|
29655
|
+
});
|
|
29036
29656
|
if (!prof.user?.id)
|
|
29037
29657
|
throw new Error(`No user @${handle}.`);
|
|
29038
|
-
return
|
|
29658
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
29039
29659
|
}
|
|
29040
|
-
const
|
|
29041
|
-
|
|
29042
|
-
|
|
29043
|
-
|
|
29044
|
-
|
|
29045
|
-
|
|
29046
|
-
|
|
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);
|
|
29047
29684
|
}
|
|
29048
|
-
|
|
29049
|
-
|
|
29050
|
-
|
|
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}`);
|
|
29051
29692
|
}
|
|
29052
|
-
|
|
29693
|
+
const p = resolved[0];
|
|
29694
|
+
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
29053
29695
|
}
|
|
29054
29696
|
var HTTPS_RE = /^https:\/\//i;
|
|
29055
29697
|
var CHAT_IMAGE_MIME = new Set([
|
|
@@ -29136,11 +29778,20 @@ function registerChatCommand(chat) {
|
|
|
29136
29778
|
console.log(dim(` ${unread.trim()}`));
|
|
29137
29779
|
}
|
|
29138
29780
|
});
|
|
29139
|
-
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) => {
|
|
29140
29782
|
const auth = authOrExit();
|
|
29141
29783
|
let convId;
|
|
29142
29784
|
try {
|
|
29143
|
-
|
|
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
|
+
}
|
|
29144
29795
|
} catch (e5) {
|
|
29145
29796
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
29146
29797
|
process.exit(1);
|
|
@@ -29171,26 +29822,33 @@ function registerChatCommand(chat) {
|
|
|
29171
29822
|
console.log(dim(` ${when}`));
|
|
29172
29823
|
}
|
|
29173
29824
|
});
|
|
29174
|
-
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) => {
|
|
29175
29826
|
const auth = authOrExit();
|
|
29176
29827
|
if (opts.image && opts.audio) {
|
|
29177
29828
|
console.error(error("Attach either --image or --audio, not both (one media block per message)."));
|
|
29178
29829
|
process.exit(1);
|
|
29179
29830
|
}
|
|
29180
|
-
let
|
|
29831
|
+
let resolved;
|
|
29181
29832
|
try {
|
|
29182
|
-
|
|
29833
|
+
resolved = await resolveConversation(auth, target);
|
|
29183
29834
|
} catch (e5) {
|
|
29184
29835
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
29185
29836
|
process.exit(1);
|
|
29186
29837
|
}
|
|
29187
29838
|
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
|
|
29188
29839
|
await requireApproval({
|
|
29189
|
-
|
|
29190
|
-
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 ? "…" : ""}`,
|
|
29191
29842
|
yes: opts.yes,
|
|
29192
29843
|
commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
|
|
29193
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
|
+
}
|
|
29194
29852
|
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
29195
29853
|
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
29196
29854
|
method: "POST",
|
|
@@ -29227,6 +29885,8 @@ function registerChatCommand(chat) {
|
|
|
29227
29885
|
const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : "Message sent";
|
|
29228
29886
|
console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
|
|
29229
29887
|
});
|
|
29888
|
+
const group = chat.command("group").description("Group chats — create, list, info, rename, add/remove members, leave");
|
|
29889
|
+
registerChatGroupCommand(group);
|
|
29230
29890
|
}
|
|
29231
29891
|
|
|
29232
29892
|
// src/commands/card.ts
|
|
@@ -29444,7 +30104,7 @@ function registerCardCommand(card) {
|
|
|
29444
30104
|
// src/commands/amazon.ts
|
|
29445
30105
|
var AMAZON_SEARCH_USD = 0.01;
|
|
29446
30106
|
function registerAmazonCommand(program2) {
|
|
29447
|
-
const cmd = program2.command("amazon").description("Search and buy products on Amazon —
|
|
30107
|
+
const cmd = program2.command("amazon").description("Search and buy products on Amazon — pay in AMIKO, USDC, or USDT");
|
|
29448
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) => {
|
|
29449
30109
|
let payToken;
|
|
29450
30110
|
try {
|
|
@@ -29661,7 +30321,7 @@ async function fetchWithTimeout2(url2, init = {}, timeoutMs) {
|
|
|
29661
30321
|
}
|
|
29662
30322
|
}
|
|
29663
30323
|
function registerServicesCommand(program2) {
|
|
29664
|
-
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");
|
|
29665
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) => {
|
|
29666
30326
|
await requireApproval({
|
|
29667
30327
|
cost: "1 AMIKO",
|
|
@@ -29997,6 +30657,18 @@ function registerTwinCommand(program2) {
|
|
|
29997
30657
|
// src/commands/drive.ts
|
|
29998
30658
|
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
29999
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
|
+
}
|
|
30000
30672
|
function fmtBytes(n) {
|
|
30001
30673
|
if (!n || n <= 0)
|
|
30002
30674
|
return "-";
|
|
@@ -30009,7 +30681,7 @@ function fmtBytes(n) {
|
|
|
30009
30681
|
}
|
|
30010
30682
|
return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
|
|
30011
30683
|
}
|
|
30012
|
-
function
|
|
30684
|
+
function statusOf3(doc2) {
|
|
30013
30685
|
const meta3 = doc2.metadata?.status;
|
|
30014
30686
|
if (meta3)
|
|
30015
30687
|
return meta3;
|
|
@@ -30040,6 +30712,7 @@ function renderDocTable(docs) {
|
|
|
30040
30712
|
dim("TYPE"),
|
|
30041
30713
|
dim("FOLDER"),
|
|
30042
30714
|
dim("STATUS"),
|
|
30715
|
+
dim("PUBLIC"),
|
|
30043
30716
|
dim("CREATED")
|
|
30044
30717
|
],
|
|
30045
30718
|
...docs.map((doc2) => [
|
|
@@ -30047,7 +30720,8 @@ function renderDocTable(docs) {
|
|
|
30047
30720
|
doc2.title || doc2.filename,
|
|
30048
30721
|
doc2.file_type || "-",
|
|
30049
30722
|
doc2.folder_id || dim("root"),
|
|
30050
|
-
|
|
30723
|
+
statusOf3(doc2),
|
|
30724
|
+
doc2.is_public ? "yes" : dim("-"),
|
|
30051
30725
|
doc2.created_at?.slice(0, 10) || "-"
|
|
30052
30726
|
])
|
|
30053
30727
|
];
|
|
@@ -30120,26 +30794,56 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
30120
30794
|
try {
|
|
30121
30795
|
const file2 = await resolveFileInput(source, {
|
|
30122
30796
|
stdin: opts.stdin,
|
|
30123
|
-
hintName: opts.name
|
|
30797
|
+
hintName: opts.name,
|
|
30798
|
+
maxBytes: MAX_DRIVE_UPLOAD_BYTES
|
|
30124
30799
|
});
|
|
30125
30800
|
if (spinner)
|
|
30126
|
-
spinner.text = `Uploading ${file2.name}...`;
|
|
30127
|
-
|
|
30128
|
-
|
|
30129
|
-
|
|
30130
|
-
|
|
30131
|
-
|
|
30132
|
-
|
|
30133
|
-
|
|
30134
|
-
|
|
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;
|
|
30135
30839
|
}
|
|
30136
30840
|
if (spinner)
|
|
30137
|
-
spinner.text = `Registering ${
|
|
30138
|
-
const title = opts.title ||
|
|
30841
|
+
spinner.text = `Registering ${uploadedName}...`;
|
|
30842
|
+
const title = opts.title || uploadedName.replace(/\.[^.]+$/, "");
|
|
30139
30843
|
const body = {
|
|
30140
|
-
filename:
|
|
30141
|
-
fileUrl:
|
|
30142
|
-
fileType:
|
|
30844
|
+
filename: uploadedName,
|
|
30845
|
+
fileUrl: uploadedUrl,
|
|
30846
|
+
fileType: uploadedType,
|
|
30143
30847
|
title,
|
|
30144
30848
|
doc_type: opts.docType ?? "other"
|
|
30145
30849
|
};
|
|
@@ -30155,10 +30859,8 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
30155
30859
|
console.log(label("Title", d.title || "-"));
|
|
30156
30860
|
if (d.folder_id)
|
|
30157
30861
|
console.log(label("Folder", d.folder_id));
|
|
30158
|
-
|
|
30159
|
-
|
|
30160
|
-
if (upload.fileType)
|
|
30161
|
-
console.log(label("Type", upload.fileType));
|
|
30862
|
+
console.log(label("Size", fmtBytes(file2.size)));
|
|
30863
|
+
console.log(label("Type", uploadedType));
|
|
30162
30864
|
if (d.job_id) {
|
|
30163
30865
|
console.log(label("RAG job", d.job_id));
|
|
30164
30866
|
console.log(dim("Processing runs in the background. Use `amiko drive status <id>` to check."));
|
|
@@ -30236,9 +30938,15 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
30236
30938
|
console.log(label("Folder", d.doc.folder_id));
|
|
30237
30939
|
if (d.doc.description)
|
|
30238
30940
|
console.log(label("Description", d.doc.description));
|
|
30239
|
-
console.log(label("Status",
|
|
30941
|
+
console.log(label("Status", statusOf3(d.doc)));
|
|
30240
30942
|
if (d.doc.metadata?.error)
|
|
30241
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
|
+
}
|
|
30242
30950
|
console.log(label("Signed URL", d.signed_url));
|
|
30243
30951
|
if (opts.content && d.doc.content) {
|
|
30244
30952
|
console.log(heading(`
|
|
@@ -30300,6 +31008,40 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
|
|
|
30300
31008
|
process.exit(1);
|
|
30301
31009
|
}
|
|
30302
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
|
+
});
|
|
30303
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) => {
|
|
30304
31046
|
const { twinId, auth } = twinFromFlag(opts);
|
|
30305
31047
|
try {
|
|
@@ -30341,11 +31083,12 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
|
|
|
30341
31083
|
}
|
|
30342
31084
|
console.log(heading(`Folders (${d.folders.length})`));
|
|
30343
31085
|
const rows = [
|
|
30344
|
-
[dim("ID"), dim("NAME"), dim("PARENT"), dim("CREATED")],
|
|
31086
|
+
[dim("ID"), dim("NAME"), dim("PARENT"), dim("PUBLIC"), dim("CREATED")],
|
|
30345
31087
|
...d.folders.map((f) => [
|
|
30346
31088
|
f.id,
|
|
30347
31089
|
f.name,
|
|
30348
31090
|
f.parent_id || dim("root"),
|
|
31091
|
+
f.is_public ? "yes" : dim("-"),
|
|
30349
31092
|
f.created_at?.slice(0, 10) || "-"
|
|
30350
31093
|
])
|
|
30351
31094
|
];
|
|
@@ -30394,6 +31137,40 @@ Parsed content available (${d.doc.content.length} chars). Pass --content to prin
|
|
|
30394
31137
|
process.exit(1);
|
|
30395
31138
|
}
|
|
30396
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
|
+
});
|
|
30397
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) => {
|
|
30398
31175
|
const { twinId, auth } = twinFromFlag(opts);
|
|
30399
31176
|
await confirmDestructive({
|
|
@@ -32227,10 +33004,10 @@ var didOnboard = await runOnboarding(process.argv);
|
|
|
32227
33004
|
if (didOnboard)
|
|
32228
33005
|
process.exit(0);
|
|
32229
33006
|
var program2 = new Command;
|
|
32230
|
-
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);
|
|
32231
33008
|
var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
|
|
32232
33009
|
var create2 = program2.command("create").description("Create Studio — generate images, video, speech, music, and SFX (charged on success)");
|
|
32233
|
-
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");
|
|
32234
33011
|
var card = program2.command("card").description("Twin Cards (work/play/love) — generate, view, download, mint");
|
|
32235
33012
|
var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
|
|
32236
33013
|
var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
|