@heyamiko/amiko-cli 0.14.0-beta.11 → 0.14.0-beta.13
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 +29 -4
- package/dist/index.js +314 -28
- package/package.json +1 -1
- package/skills/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @heyamiko/amiko-cli (v0.14.0-beta.
|
|
1
|
+
# @heyamiko/amiko-cli (v0.14.0-beta.13)
|
|
2
2
|
|
|
3
3
|
Manage wallets, credits, swaps, MPP marketplace services, and your Amiko twin (identity, documents, voice, avatar, friends, feed) from the terminal. Works for both human users and AI agents running on OpenClaw.
|
|
4
4
|
|
|
@@ -131,6 +131,10 @@ amiko create video "animate cover" --first-frame https://.../cover.png --yes
|
|
|
131
131
|
amiko create video "dance clip" --model dreamina-seedance-2-0-fast-260128 \
|
|
132
132
|
--reference-image https://.../ref1.png --reference-audio https://.../beat.mp3 --generate-audio --yes
|
|
133
133
|
amiko create tts "Hello world" --voice 21m00Tcm4TlvDq8ikWAM
|
|
134
|
+
amiko create tts "Hello world" --provider minimax --model speech-2.8-turbo
|
|
135
|
+
amiko create tts "Hello" --provider minimax --voice English_expressive_narrator --yes
|
|
136
|
+
amiko voice clone ./me.mp3 --provider minimax
|
|
137
|
+
amiko voice clone ./me.mp3 --provider elevenlabs
|
|
134
138
|
amiko create music "lo-fi chill beat" --duration 30000
|
|
135
139
|
amiko create music --lyrics "..." --instrumental # instrumental
|
|
136
140
|
amiko create sfx "thunder and heavy rain" --duration 5
|
|
@@ -178,8 +182,20 @@ Reference flags accept URLs or data URIs (not local paths). Typical I2V flow: `c
|
|
|
178
182
|
|
|
179
183
|
| Flag | Default | Description |
|
|
180
184
|
|------|---------|-------------|
|
|
181
|
-
| `--
|
|
182
|
-
| `--
|
|
185
|
+
| `--provider <name>` | inferred / `elevenlabs` | `minimax` or `elevenlabs` — sets model/voice defaults when those flags are omitted |
|
|
186
|
+
| `--model <model>` | EL: `eleven_multilingual_v2`; MiniMax: `speech-2.8-turbo` | MiniMax: `speech-2.8-turbo\|hd`, `speech-2.6-turbo\|hd`. ElevenLabs: `eleven_*` |
|
|
187
|
+
| `--voice <voiceId>` | EL default hash; MiniMax: `English_expressive_narrator` | MiniMax system or cloned `voice_id`; ElevenLabs voice id |
|
|
188
|
+
|
|
189
|
+
Examples: `amiko create tts "Hi" --provider minimax --yes` · `amiko create tts "Hi" --provider minimax --model speech-2.8-turbo --voice <clonedId> --yes`
|
|
190
|
+
|
|
191
|
+
**`amiko voice clone <source>`** — audio path, https URL, or `-` (stdin)
|
|
192
|
+
|
|
193
|
+
| Flag | Default | Description |
|
|
194
|
+
|------|---------|-------------|
|
|
195
|
+
| `--provider <name>` | `minimax` | `minimax` (mpp rapid clone + ownership registry) or `elevenlabs` (legacy IVC) |
|
|
196
|
+
| `--voice-id <id>` | auto | **MiniMax only** — optional custom voice_id (ignored for elevenlabs) |
|
|
197
|
+
| `--model <model>` | `speech-2.8-turbo` | **MiniMax only** — preview speech model (ignored for elevenlabs) |
|
|
198
|
+
| `--preview-text <text>` | provider default | **MiniMax only** — preview line; empty string skips preview (ignored for elevenlabs) |
|
|
183
199
|
|
|
184
200
|
**`amiko create music [prompt]`** — pass a prompt and/or `--lyrics`
|
|
185
201
|
|
|
@@ -345,8 +361,11 @@ amiko markets service call POST /v1/sfx '{"text":"Thunder with heavy rain","dura
|
|
|
345
361
|
# Music generation ($0.10) — returns { url, format, service }
|
|
346
362
|
amiko markets service call POST /v1/music '{"prompt":"Lo-fi chill beat","music_length_ms":30000}'
|
|
347
363
|
|
|
348
|
-
# Text-to-speech
|
|
364
|
+
# Text-to-speech — ElevenLabs voice id (default)
|
|
349
365
|
amiko markets service tts 21m00Tcm4TlvDq8ikWAM "Hello world"
|
|
366
|
+
# MiniMax system or cloned voice_id
|
|
367
|
+
amiko markets service tts English_expressive_narrator "Hello" --provider minimax --yes
|
|
368
|
+
amiko markets service tts <clonedId> "Hello" --provider minimax --model speech-2.8-turbo --yes
|
|
350
369
|
|
|
351
370
|
# Composition plan ($0.02)
|
|
352
371
|
amiko markets service call POST /v1/music/plan '{"prompt":"Epic orchestral piece"}'
|
|
@@ -564,6 +583,12 @@ npm publish
|
|
|
564
583
|
|
|
565
584
|
## Changelog
|
|
566
585
|
|
|
586
|
+
### 0.14.0-beta.13
|
|
587
|
+
|
|
588
|
+
- **Create Studio TTS + voice clone: MiniMax.** `amiko create tts` accepts `--provider minimax|elevenlabs` (infers from `speech-*` / `eleven_*` models when omitted), with provider-specific defaults (`speech-2.8-turbo` + `English_expressive_narrator` vs `eleven_multilingual_v2` + Rachel). Quote body includes `voiceId`. Cloned-voice examples require `--provider minimax` when using a MiniMax `voice_id`.
|
|
589
|
+
- **`amiko voice clone`** defaults to MiniMax (mpp rapid clone + ownership registry); `--provider elevenlabs` keeps legacy IVC. MiniMax-only flags: `--voice-id`, `--model`, `--preview-text`.
|
|
590
|
+
- **`amiko markets service tts`** gains `--provider minimax|elevenlabs` and `--model` for MiniMax speech routing on `/v1/tts/:voiceId`.
|
|
591
|
+
|
|
567
592
|
### 0.14.0-beta.7
|
|
568
593
|
|
|
569
594
|
- **`amiko chat group invite <group>` — share a group's invite/join link + QR.** New read-only subcommand that fetches `GET /api/conversations/{id}/invite` (twin token) and prints the join link plus a scannable QR image URL. Admins-only: SKILL.md instructs the agent to confirm the asker holds `admin`/`owner` (via `amiko chat group info`) before sharing, and the endpoint is admin-gated server-side (403 → admin tip). Fails loudly if the response is missing its link/QR instead of printing a placeholder. Requires the companion amiko-web change (adds `qrUrl` to the invite response + a public `GET /api/conversations/join/[token]/qr` PNG route).
|
package/dist/index.js
CHANGED
|
@@ -23661,21 +23661,34 @@ function readFromFile(path) {
|
|
|
23661
23661
|
return null;
|
|
23662
23662
|
}
|
|
23663
23663
|
}
|
|
23664
|
+
function readRoutingIdsFromEnv() {
|
|
23665
|
+
return {
|
|
23666
|
+
agentId: asString(process.env.AMIKO_AGENT_ID),
|
|
23667
|
+
sessionId: asString(process.env.AMIKO_SESSION_ID)
|
|
23668
|
+
};
|
|
23669
|
+
}
|
|
23664
23670
|
function readFromEnv() {
|
|
23665
23671
|
return {
|
|
23666
23672
|
twinToken: asString(process.env.AMIKO_TWIN_TOKEN) ?? asString(process.env.AMIKO_USER_TOKEN),
|
|
23667
23673
|
twinId: asString(process.env.AMIKO_TWIN_ID),
|
|
23668
23674
|
userId: asString(process.env.AMIKO_USER_ID),
|
|
23669
23675
|
platformUrl: asString(process.env.AMIKO_PLATFORM_URL),
|
|
23670
|
-
apiBase: asString(process.env.AMIKO_API_BASE)
|
|
23676
|
+
apiBase: asString(process.env.AMIKO_API_BASE),
|
|
23677
|
+
...readRoutingIdsFromEnv()
|
|
23671
23678
|
};
|
|
23672
23679
|
}
|
|
23680
|
+
function resolveFileAuth(path) {
|
|
23681
|
+
const fromFile = readFromFile(path);
|
|
23682
|
+
if (!fromFile)
|
|
23683
|
+
return null;
|
|
23684
|
+
return { ...fromFile, ...readRoutingIdsFromEnv() };
|
|
23685
|
+
}
|
|
23673
23686
|
function resolveAuth() {
|
|
23674
23687
|
const filePath = findAmikoJsonPath();
|
|
23675
23688
|
if (filePath) {
|
|
23676
|
-
const
|
|
23677
|
-
if (
|
|
23678
|
-
return
|
|
23689
|
+
const merged = resolveFileAuth(filePath);
|
|
23690
|
+
if (merged)
|
|
23691
|
+
return merged;
|
|
23679
23692
|
}
|
|
23680
23693
|
return readFromEnv();
|
|
23681
23694
|
}
|
|
@@ -26442,6 +26455,13 @@ var ETA = {
|
|
|
26442
26455
|
function statusOf(err) {
|
|
26443
26456
|
return err?.status;
|
|
26444
26457
|
}
|
|
26458
|
+
function autoApproveLimit() {
|
|
26459
|
+
const raw = process.env.AMIKO_AUTO_APPROVE_LIMIT;
|
|
26460
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
26461
|
+
return;
|
|
26462
|
+
const limit = Number(raw);
|
|
26463
|
+
return Number.isFinite(limit) ? limit : undefined;
|
|
26464
|
+
}
|
|
26445
26465
|
function collectOption(value, previous) {
|
|
26446
26466
|
return [...previous ?? [], value];
|
|
26447
26467
|
}
|
|
@@ -26451,7 +26471,7 @@ function assertMaxUrls(resourceName, urls, max) {
|
|
|
26451
26471
|
process.exit(1);
|
|
26452
26472
|
}
|
|
26453
26473
|
}
|
|
26454
|
-
function printCompleted(final, title) {
|
|
26474
|
+
function printCompleted(final, title, deliveredToThisSession = false) {
|
|
26455
26475
|
console.log(heading(`${title}
|
|
26456
26476
|
`));
|
|
26457
26477
|
if (final.prompt)
|
|
@@ -26460,6 +26480,9 @@ function printCompleted(final, title) {
|
|
|
26460
26480
|
console.log(label("URL", final.assetUrl));
|
|
26461
26481
|
if (final.assetMime)
|
|
26462
26482
|
console.log(label("Type", final.assetMime));
|
|
26483
|
+
if (deliveredToThisSession) {
|
|
26484
|
+
console.log(success("Already delivered to the user in this chat automatically. Do NOT send this file again and do NOT paste the URL — you would duplicate it."));
|
|
26485
|
+
}
|
|
26463
26486
|
console.log("");
|
|
26464
26487
|
if (final.charged) {
|
|
26465
26488
|
const amt = final.chargeAmount ?? final.costAmiko ?? "?";
|
|
@@ -26488,6 +26511,7 @@ async function submitCreateJob(opts) {
|
|
|
26488
26511
|
console.error(dim("--token is ignored with --pay credits (credits come from your Amiko account, not a wallet token)."));
|
|
26489
26512
|
}
|
|
26490
26513
|
let costLine = payCredits ? "reserved from your Amiko account credits, captured on success (released on failure)" : "charged from your twin wallet on success (nothing on failure)";
|
|
26514
|
+
let quotedCostAmiko;
|
|
26491
26515
|
try {
|
|
26492
26516
|
const q = await amikoWebFetch(auth, "/api/create/quote", {
|
|
26493
26517
|
method: "POST",
|
|
@@ -26497,11 +26521,29 @@ async function submitCreateJob(opts) {
|
|
|
26497
26521
|
if (q.cost_usd != null) {
|
|
26498
26522
|
costLine = payCredits ? `≈ ${Math.ceil(q.cost_usd * 1e4)} Amiko credits (≈ $${q.cost_usd.toFixed(4)}) — reserved on submit, captured only on success` : `${q.cost_amiko ?? "?"} AMIKO (≈ $${q.cost_usd.toFixed(4)}) — charged only on success`;
|
|
26499
26523
|
}
|
|
26524
|
+
if (typeof q.cost_amiko === "string" && q.cost_amiko.trim() !== "") {
|
|
26525
|
+
const n = Number(q.cost_amiko);
|
|
26526
|
+
if (Number.isFinite(n))
|
|
26527
|
+
quotedCostAmiko = n;
|
|
26528
|
+
}
|
|
26500
26529
|
} catch {}
|
|
26530
|
+
const preferredToken = typeof opts.jobBody.preferredToken === "string" ? opts.jobBody.preferredToken.toUpperCase() : undefined;
|
|
26531
|
+
const chargeIsAmiko = !payCredits && (preferredToken === undefined || preferredToken === "AMIKO");
|
|
26532
|
+
const effectiveChargeToken = preferredToken ?? "AMIKO";
|
|
26533
|
+
let yes = opts.yes;
|
|
26534
|
+
let autoApproved = false;
|
|
26535
|
+
if (!yes) {
|
|
26536
|
+
const limit = autoApproveLimit();
|
|
26537
|
+
if (limit !== undefined && quotedCostAmiko !== undefined && quotedCostAmiko > 0 && chargeIsAmiko && quotedCostAmiko <= limit) {
|
|
26538
|
+
yes = true;
|
|
26539
|
+
autoApproved = true;
|
|
26540
|
+
console.log(dim(`Auto-approved (cost ${quotedCostAmiko} ≤ limit ${limit} AMIKO)`));
|
|
26541
|
+
}
|
|
26542
|
+
}
|
|
26501
26543
|
await requireApproval({
|
|
26502
26544
|
cost: costLine,
|
|
26503
26545
|
summary: opts.summary,
|
|
26504
|
-
yes
|
|
26546
|
+
yes,
|
|
26505
26547
|
commandExample: `amiko create ${opts.mode.toLowerCase()} ...`
|
|
26506
26548
|
});
|
|
26507
26549
|
const isTTY = process.stderr.isTTY;
|
|
@@ -26511,16 +26553,21 @@ async function submitCreateJob(opts) {
|
|
|
26511
26553
|
spinner.stop();
|
|
26512
26554
|
};
|
|
26513
26555
|
let jobId;
|
|
26556
|
+
let routedAgentId;
|
|
26557
|
+
let routedSessionId;
|
|
26514
26558
|
try {
|
|
26515
26559
|
const submit = await amikoWebFetch(auth, "/api/create/jobs", {
|
|
26516
26560
|
method: "POST",
|
|
26517
26561
|
body: {
|
|
26518
26562
|
...opts.jobBody,
|
|
26519
26563
|
twinId: auth.twinId,
|
|
26564
|
+
...autoApproved ? { preferredToken: effectiveChargeToken, strictToken: true } : {},
|
|
26520
26565
|
...payCredits ? {
|
|
26521
26566
|
paymentMethod: { kind: "account_credits" },
|
|
26522
26567
|
requestId: randomUUID()
|
|
26523
|
-
} : {}
|
|
26568
|
+
} : {},
|
|
26569
|
+
...auth.agentId ? { agentId: auth.agentId } : {},
|
|
26570
|
+
...auth.sessionId ? { sessionId: auth.sessionId } : {}
|
|
26524
26571
|
},
|
|
26525
26572
|
timeoutMs: 30000
|
|
26526
26573
|
});
|
|
@@ -26530,6 +26577,8 @@ async function submitCreateJob(opts) {
|
|
|
26530
26577
|
process.exit(1);
|
|
26531
26578
|
}
|
|
26532
26579
|
jobId = submit.jobId;
|
|
26580
|
+
routedAgentId = submit.agentId;
|
|
26581
|
+
routedSessionId = submit.sessionId;
|
|
26533
26582
|
} catch (e5) {
|
|
26534
26583
|
stop();
|
|
26535
26584
|
const webErr = e5;
|
|
@@ -26550,19 +26599,24 @@ async function submitCreateJob(opts) {
|
|
|
26550
26599
|
}
|
|
26551
26600
|
process.exit(1);
|
|
26552
26601
|
}
|
|
26602
|
+
const sessionRouted = !!(routedAgentId && routedSessionId);
|
|
26553
26603
|
stop();
|
|
26554
26604
|
if (opts.raw) {
|
|
26555
26605
|
console.log(JSON.stringify({
|
|
26556
26606
|
jobId,
|
|
26557
26607
|
status: "PENDING",
|
|
26558
26608
|
check: `amiko create status ${jobId}`,
|
|
26559
|
-
eta: ETA[opts.mode]
|
|
26609
|
+
eta: ETA[opts.mode],
|
|
26610
|
+
...sessionRouted ? { sessionRouted: true, deliveredToSession: routedSessionId } : {}
|
|
26560
26611
|
}, null, 2));
|
|
26561
26612
|
return;
|
|
26562
26613
|
}
|
|
26563
26614
|
console.log(success(`Submitted — job ${jobId}`));
|
|
26564
26615
|
console.log(label("Status", "PENDING — generating in the background"));
|
|
26565
26616
|
console.log(label("Check result", `amiko create status ${jobId}`));
|
|
26617
|
+
if (sessionRouted) {
|
|
26618
|
+
console.log(dim("A generating-placeholder is already showing in the chat and the finished media will be delivered there automatically — do not send the file or URL yourself."));
|
|
26619
|
+
}
|
|
26566
26620
|
console.log(dim(`Usually ready in ${ETA[opts.mode]} — charged only on success. (Also: amiko create media)`));
|
|
26567
26621
|
if (opts.mode === "VIDEO") {
|
|
26568
26622
|
console.log(dim("Async video: HTTP 202 / PENDING means the job was accepted — not a failure. Poll status after ~2–9 min."));
|
|
@@ -26610,7 +26664,8 @@ function registerCreateCommand(create2) {
|
|
|
26610
26664
|
mode: "VIDEO",
|
|
26611
26665
|
model,
|
|
26612
26666
|
size: opts.resolution,
|
|
26613
|
-
seconds
|
|
26667
|
+
seconds,
|
|
26668
|
+
...referenceVideoUrls.length ? { referenceVideoUrls } : {}
|
|
26614
26669
|
},
|
|
26615
26670
|
jobBody: {
|
|
26616
26671
|
mode: "VIDEO",
|
|
@@ -26637,19 +26692,54 @@ function registerCreateCommand(create2) {
|
|
|
26637
26692
|
}
|
|
26638
26693
|
});
|
|
26639
26694
|
});
|
|
26640
|
-
|
|
26695
|
+
const DEFAULT_EL_TTS_MODEL = "eleven_multilingual_v2";
|
|
26696
|
+
const DEFAULT_EL_TTS_VOICE = "21m00Tcm4TlvDq8ikWAM";
|
|
26697
|
+
const DEFAULT_MM_TTS_MODEL = "speech-2.8-turbo";
|
|
26698
|
+
const DEFAULT_MM_TTS_VOICE = "English_expressive_narrator";
|
|
26699
|
+
const MINIMAX_TTS_MODELS = new Set([
|
|
26700
|
+
"speech-2.8-turbo",
|
|
26701
|
+
"speech-2.8-hd",
|
|
26702
|
+
"speech-2.6-turbo",
|
|
26703
|
+
"speech-2.6-hd"
|
|
26704
|
+
]);
|
|
26705
|
+
create2.command("tts <text>").description("Generate speech from text (Create Studio)").option("--provider <name>", "TTS provider: minimax | elevenlabs (sets defaults when --model/--voice omitted)").option("--model <model>", `TTS model id (MiniMax: speech-2.8-turbo|hd, speech-2.6-turbo|hd; ElevenLabs: eleven_*)`).option("--voice <voiceId>", "Voice id (MiniMax system e.g. English_expressive_narrator, or a cloned voice_id; ElevenLabs voice hash)").option("--token <symbol>", "Preferred charge token: AMIKO, USDC, USDT, SOL").option("--pay <method>", "Payment method: wallet (twin wallet, default) or credits (Amiko account credits — unified accounts only)", "wallet").option("--raw", "Output raw JSON").option("--yes", "Skip the pre-spend confirmation (required in non-interactive shells)").action(async (text, opts) => {
|
|
26706
|
+
const providerRaw = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
|
|
26707
|
+
if (providerRaw && providerRaw !== "minimax" && providerRaw !== "elevenlabs") {
|
|
26708
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
26709
|
+
process.exit(1);
|
|
26710
|
+
}
|
|
26711
|
+
let model = typeof opts.model === "string" && opts.model.trim().length > 0 ? opts.model.trim() : undefined;
|
|
26712
|
+
let voice = typeof opts.voice === "string" && opts.voice.trim().length > 0 ? opts.voice.trim() : undefined;
|
|
26713
|
+
let provider = providerRaw === "minimax" || providerRaw === "elevenlabs" ? providerRaw : model && MINIMAX_TTS_MODELS.has(model) ? "minimax" : model && model.startsWith("eleven") ? "elevenlabs" : "elevenlabs";
|
|
26714
|
+
if (!model) {
|
|
26715
|
+
model = provider === "minimax" ? DEFAULT_MM_TTS_MODEL : DEFAULT_EL_TTS_MODEL;
|
|
26716
|
+
}
|
|
26717
|
+
if (!voice) {
|
|
26718
|
+
voice = provider === "minimax" ? DEFAULT_MM_TTS_VOICE : DEFAULT_EL_TTS_VOICE;
|
|
26719
|
+
}
|
|
26720
|
+
if (MINIMAX_TTS_MODELS.has(model) && providerRaw !== "elevenlabs") {
|
|
26721
|
+
provider = "minimax";
|
|
26722
|
+
}
|
|
26723
|
+
if (provider === "minimax" && !MINIMAX_TTS_MODELS.has(model)) {
|
|
26724
|
+
console.error(error(`--provider minimax requires a MiniMax speech model (${[...MINIMAX_TTS_MODELS].join(", ")})`));
|
|
26725
|
+
process.exit(1);
|
|
26726
|
+
}
|
|
26727
|
+
if (provider === "elevenlabs" && !model.startsWith("eleven")) {
|
|
26728
|
+
console.error(error("--provider elevenlabs requires an eleven_* model"));
|
|
26729
|
+
process.exit(1);
|
|
26730
|
+
}
|
|
26641
26731
|
await submitCreateJob({
|
|
26642
26732
|
mode: "TTS",
|
|
26643
|
-
summary: `Create speech: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
|
|
26733
|
+
summary: `Create speech (${provider}/${model}): ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
|
|
26644
26734
|
raw: opts.raw,
|
|
26645
26735
|
pay: opts.pay,
|
|
26646
26736
|
yes: opts.yes,
|
|
26647
|
-
quoteBody: { mode: "TTS", model
|
|
26737
|
+
quoteBody: { mode: "TTS", model, prompt: text, voiceId: voice },
|
|
26648
26738
|
jobBody: {
|
|
26649
26739
|
mode: "TTS",
|
|
26650
26740
|
prompt: text,
|
|
26651
|
-
model
|
|
26652
|
-
voiceId:
|
|
26741
|
+
model,
|
|
26742
|
+
voiceId: voice,
|
|
26653
26743
|
...opts.token ? { preferredToken: opts.token } : {}
|
|
26654
26744
|
}
|
|
26655
26745
|
});
|
|
@@ -26713,8 +26803,9 @@ function registerCreateCommand(create2) {
|
|
|
26713
26803
|
}
|
|
26714
26804
|
process.exit(1);
|
|
26715
26805
|
}
|
|
26806
|
+
const deliveredToThisSession = !!(p.agentId && p.sessionId && auth.sessionId && p.sessionId === auth.sessionId);
|
|
26716
26807
|
if (opts.raw) {
|
|
26717
|
-
console.log(JSON.stringify(p, null, 2));
|
|
26808
|
+
console.log(JSON.stringify(deliveredToThisSession ? { ...p, sessionRouted: true, deliveredToSession: p.sessionId } : p, null, 2));
|
|
26718
26809
|
return;
|
|
26719
26810
|
}
|
|
26720
26811
|
if (p.status === "FAILED") {
|
|
@@ -26726,7 +26817,7 @@ function registerCreateCommand(create2) {
|
|
|
26726
26817
|
console.log(dim(`Job id: ${jobId}`));
|
|
26727
26818
|
return;
|
|
26728
26819
|
}
|
|
26729
|
-
printCompleted(p, "Job complete");
|
|
26820
|
+
printCompleted(p, "Job complete", deliveredToThisSession);
|
|
26730
26821
|
});
|
|
26731
26822
|
create2.command("media").description("List your recent Create Studio generations").option("--limit <n>", "How many to show (max 50)", "20").option("--service <type>", "Filter by type: image, video, tts, music, sfx").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
26732
26823
|
let auth;
|
|
@@ -27061,6 +27152,53 @@ async function resolveGroup(auth, target) {
|
|
|
27061
27152
|
return fetchConversationById(auth, matches[0].id);
|
|
27062
27153
|
}
|
|
27063
27154
|
|
|
27155
|
+
// src/lib/reactions.ts
|
|
27156
|
+
var EMOJI_MAX = 32;
|
|
27157
|
+
function validateEmoji(raw) {
|
|
27158
|
+
const emoji3 = (raw ?? "").trim();
|
|
27159
|
+
if (!emoji3)
|
|
27160
|
+
throw new Error("No emoji given. Pass the emoji to react with, e.g. ❤️.");
|
|
27161
|
+
if (emoji3.length > EMOJI_MAX) {
|
|
27162
|
+
throw new Error(`Emoji is too long (max ${EMOJI_MAX} characters).`);
|
|
27163
|
+
}
|
|
27164
|
+
return emoji3;
|
|
27165
|
+
}
|
|
27166
|
+
async function fetchMessage(auth, messageId) {
|
|
27167
|
+
const data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}`, { timeoutMs: 20000 });
|
|
27168
|
+
if (!data.message?.id) {
|
|
27169
|
+
throw new Error(`Message ${messageId} not found.`);
|
|
27170
|
+
}
|
|
27171
|
+
return { ...data.message, reactions: data.message.reactions ?? [] };
|
|
27172
|
+
}
|
|
27173
|
+
function ownReaction(reactions, userId) {
|
|
27174
|
+
if (!userId)
|
|
27175
|
+
return null;
|
|
27176
|
+
const me = normalizeUserId(userId);
|
|
27177
|
+
const mine = reactions.find((r) => normalizeUserId(r.user_id) === me);
|
|
27178
|
+
return mine?.emoji ?? null;
|
|
27179
|
+
}
|
|
27180
|
+
function previewText(msg, max = 60) {
|
|
27181
|
+
const content = (msg.content ?? "").trim();
|
|
27182
|
+
if (content) {
|
|
27183
|
+
return content.length > max ? `${content.slice(0, max)}…` : content;
|
|
27184
|
+
}
|
|
27185
|
+
if (msg.message_type === "voice")
|
|
27186
|
+
return "[voice message]";
|
|
27187
|
+
if (msg.metadata?.images?.length)
|
|
27188
|
+
return "[image]";
|
|
27189
|
+
return `[${msg.message_type || "message"}]`;
|
|
27190
|
+
}
|
|
27191
|
+
function groupReactions(reactions) {
|
|
27192
|
+
const counts = new Map;
|
|
27193
|
+
for (const r of reactions) {
|
|
27194
|
+
counts.set(r.emoji, (counts.get(r.emoji) ?? 0) + 1);
|
|
27195
|
+
}
|
|
27196
|
+
return Array.from(counts.entries());
|
|
27197
|
+
}
|
|
27198
|
+
function summarizeReactions(reactions) {
|
|
27199
|
+
return groupReactions(reactions).map(([emoji3, count]) => `${emoji3}×${count}`).join(" ");
|
|
27200
|
+
}
|
|
27201
|
+
|
|
27064
27202
|
// src/commands/chat-group.ts
|
|
27065
27203
|
var collect = (v, prev) => [...prev, v];
|
|
27066
27204
|
function statusOf2(e5) {
|
|
@@ -27650,8 +27788,11 @@ function registerChatCommand(chat) {
|
|
|
27650
27788
|
const who = m.sender?.name ?? m.name ?? (m.sender_id ? m.sender_id.slice(0, 8) : "?");
|
|
27651
27789
|
const when = m.created_at ? m.created_at.slice(0, 19).replace("T", " ") : "";
|
|
27652
27790
|
console.log(label(who, m.content));
|
|
27653
|
-
|
|
27654
|
-
|
|
27791
|
+
const meta3 = [when, `id ${m.id}`].filter(Boolean).join(" ");
|
|
27792
|
+
console.log(dim(` ${meta3}`));
|
|
27793
|
+
if (m.reactions?.length) {
|
|
27794
|
+
console.log(dim(` reactions: ${summarizeReactions(m.reactions)}`));
|
|
27795
|
+
}
|
|
27655
27796
|
}
|
|
27656
27797
|
});
|
|
27657
27798
|
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) => {
|
|
@@ -27717,6 +27858,113 @@ function registerChatCommand(chat) {
|
|
|
27717
27858
|
const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : "Message sent";
|
|
27718
27859
|
console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
|
|
27719
27860
|
});
|
|
27861
|
+
chat.command("react <messageId> <emoji>").description("React to a message as yourself (the owner). Get the message id from `amiko chat read`. One reaction per message — a new emoji replaces your previous one.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, emojiArg, opts) => {
|
|
27862
|
+
const auth = authOrExit();
|
|
27863
|
+
let emoji3;
|
|
27864
|
+
try {
|
|
27865
|
+
emoji3 = validateEmoji(emojiArg);
|
|
27866
|
+
} catch (e5) {
|
|
27867
|
+
console.error(error(e5 instanceof Error ? e5.message : "Invalid emoji"));
|
|
27868
|
+
process.exit(1);
|
|
27869
|
+
}
|
|
27870
|
+
let msg;
|
|
27871
|
+
try {
|
|
27872
|
+
msg = await fetchMessage(auth, messageId);
|
|
27873
|
+
} catch (e5) {
|
|
27874
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
27875
|
+
process.exit(1);
|
|
27876
|
+
}
|
|
27877
|
+
if (ownReaction(msg.reactions, auth.userId) === emoji3) {
|
|
27878
|
+
if (opts.raw) {
|
|
27879
|
+
console.log(JSON.stringify({ success: true, action: "unchanged", reactions: msg.reactions }, null, 2));
|
|
27880
|
+
return;
|
|
27881
|
+
}
|
|
27882
|
+
console.log(success(`Already reacted with ${emoji3}`));
|
|
27883
|
+
return;
|
|
27884
|
+
}
|
|
27885
|
+
await requireApproval({
|
|
27886
|
+
free: "adds your reaction — people in the chat see it",
|
|
27887
|
+
summary: `React ${emoji3} to "${previewText(msg)}"`,
|
|
27888
|
+
yes: opts.yes,
|
|
27889
|
+
commandExample: `amiko chat react ${JSON.stringify(messageId)} ${JSON.stringify(emoji3)}`
|
|
27890
|
+
});
|
|
27891
|
+
let data;
|
|
27892
|
+
try {
|
|
27893
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", body: { emoji: emoji3 }, timeoutMs: 20000 });
|
|
27894
|
+
} catch (e5) {
|
|
27895
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to react"));
|
|
27896
|
+
process.exit(1);
|
|
27897
|
+
}
|
|
27898
|
+
if (opts.raw) {
|
|
27899
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27900
|
+
return;
|
|
27901
|
+
}
|
|
27902
|
+
const what = data.action === "removed" ? `Removed your ${emoji3} reaction` : data.action === "updated" ? `Reaction changed to ${emoji3}` : `Reacted ${emoji3}`;
|
|
27903
|
+
console.log(success(what));
|
|
27904
|
+
});
|
|
27905
|
+
chat.command("unreact <messageId>").description("Remove your (the owner's) reaction from a message. Get the message id from `amiko chat read`.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
27906
|
+
const auth = authOrExit();
|
|
27907
|
+
let msg;
|
|
27908
|
+
try {
|
|
27909
|
+
msg = await fetchMessage(auth, messageId);
|
|
27910
|
+
} catch (e5) {
|
|
27911
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
27912
|
+
process.exit(1);
|
|
27913
|
+
}
|
|
27914
|
+
const mine = ownReaction(msg.reactions, auth.userId);
|
|
27915
|
+
if (!mine) {
|
|
27916
|
+
if (opts.raw) {
|
|
27917
|
+
console.log(JSON.stringify({ success: true, action: "none", reactions: msg.reactions }, null, 2));
|
|
27918
|
+
return;
|
|
27919
|
+
}
|
|
27920
|
+
console.log(dim("You have no reaction on that message."));
|
|
27921
|
+
return;
|
|
27922
|
+
}
|
|
27923
|
+
await requireApproval({
|
|
27924
|
+
free: "removes your reaction from the chat",
|
|
27925
|
+
summary: `Remove your ${mine} reaction from "${previewText(msg)}"`,
|
|
27926
|
+
yes: opts.yes,
|
|
27927
|
+
commandExample: `amiko chat unreact ${JSON.stringify(messageId)}`
|
|
27928
|
+
});
|
|
27929
|
+
let data;
|
|
27930
|
+
try {
|
|
27931
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", body: { emoji: mine }, timeoutMs: 20000 });
|
|
27932
|
+
} catch (e5) {
|
|
27933
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to remove reaction"));
|
|
27934
|
+
process.exit(1);
|
|
27935
|
+
}
|
|
27936
|
+
if (opts.raw) {
|
|
27937
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27938
|
+
return;
|
|
27939
|
+
}
|
|
27940
|
+
const what = data.action === "removed" ? `Removed your ${mine} reaction` : data.action === "updated" ? `Reaction changed to ${mine}` : `Reacted ${mine}`;
|
|
27941
|
+
console.log(success(what));
|
|
27942
|
+
});
|
|
27943
|
+
chat.command("reactions <messageId>").description("Show the reactions on a message. Get the message id from `amiko chat read`.").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
27944
|
+
const auth = authOrExit();
|
|
27945
|
+
let msg;
|
|
27946
|
+
try {
|
|
27947
|
+
msg = await fetchMessage(auth, messageId);
|
|
27948
|
+
} catch (e5) {
|
|
27949
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
27950
|
+
process.exit(1);
|
|
27951
|
+
}
|
|
27952
|
+
if (opts.raw) {
|
|
27953
|
+
console.log(JSON.stringify({ reactions: msg.reactions }, null, 2));
|
|
27954
|
+
return;
|
|
27955
|
+
}
|
|
27956
|
+
if (msg.reactions.length === 0) {
|
|
27957
|
+
console.log(dim("No reactions yet."));
|
|
27958
|
+
return;
|
|
27959
|
+
}
|
|
27960
|
+
const mine = ownReaction(msg.reactions, auth.userId);
|
|
27961
|
+
console.log(heading(`Reactions (${msg.reactions.length})
|
|
27962
|
+
`));
|
|
27963
|
+
for (const [emoji3, count] of groupReactions(msg.reactions)) {
|
|
27964
|
+
const you = emoji3 === mine ? dim(" (includes you)") : "";
|
|
27965
|
+
console.log(label(emoji3, `×${count}`) + you);
|
|
27966
|
+
}
|
|
27967
|
+
});
|
|
27720
27968
|
const group = chat.command("group").description("Group chats — create, list, info, rename, add/remove members, leave");
|
|
27721
27969
|
registerChatGroupCommand(group);
|
|
27722
27970
|
}
|
|
@@ -28154,18 +28402,32 @@ async function fetchWithTimeout2(url2, init = {}, timeoutMs) {
|
|
|
28154
28402
|
}
|
|
28155
28403
|
function registerServicesCommand(program2) {
|
|
28156
28404
|
const svc = program2.command("service").alias("svc").description("Call any MPP service — pay in AMIKO, USDC, or USDT");
|
|
28157
|
-
svc.command("tts <voiceId> <text>").description("Text-to-speech
|
|
28405
|
+
svc.command("tts <voiceId> <text>").description("Text-to-speech via mpp /v1/tts (pay-per-call)").option("--provider <name>", "elevenlabs (default) | minimax").option("--model <model>", "MiniMax speech-* model when provider=minimax").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) => {
|
|
28406
|
+
const provider = typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
|
|
28407
|
+
const body = { text };
|
|
28408
|
+
if (provider === "minimax") {
|
|
28409
|
+
body.provider = "minimax";
|
|
28410
|
+
if (opts.model)
|
|
28411
|
+
body.model_id = opts.model;
|
|
28412
|
+
else
|
|
28413
|
+
body.model_id = "speech-2.8-turbo";
|
|
28414
|
+
} else if (provider && provider !== "elevenlabs") {
|
|
28415
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
28416
|
+
process.exit(1);
|
|
28417
|
+
}
|
|
28418
|
+
const providerArgs = provider ? ` --provider ${provider}` : "";
|
|
28419
|
+
const modelArgs = provider === "minimax" ? ` --model ${JSON.stringify(typeof opts.model === "string" && opts.model ? opts.model : "speech-2.8-turbo")}` : "";
|
|
28158
28420
|
await requireApproval({
|
|
28159
|
-
cost: "
|
|
28160
|
-
summary: `Text-to-speech (voice ${voiceId}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
|
|
28421
|
+
cost: "variable (see mpp pricing)",
|
|
28422
|
+
summary: `Text-to-speech (voice ${voiceId}${provider ? `, ${provider}` : ""}): "${text.slice(0, 60)}${text.length > 60 ? "…" : ""}"`,
|
|
28161
28423
|
yes: opts.yes,
|
|
28162
|
-
commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}`
|
|
28424
|
+
commandExample: `amiko markets service tts ${voiceId} ${JSON.stringify(text)}${providerArgs}${modelArgs}`
|
|
28163
28425
|
});
|
|
28164
28426
|
await callService({
|
|
28165
28427
|
name: "Text-to-Speech",
|
|
28166
28428
|
method: "POST",
|
|
28167
28429
|
path: `/v1/tts/${encodeURIComponent(voiceId)}`,
|
|
28168
|
-
body
|
|
28430
|
+
body,
|
|
28169
28431
|
amiko: "1",
|
|
28170
28432
|
opts
|
|
28171
28433
|
});
|
|
@@ -29044,8 +29306,18 @@ function twinFromFlag2(opts) {
|
|
|
29044
29306
|
throw e5;
|
|
29045
29307
|
}
|
|
29046
29308
|
}
|
|
29309
|
+
function parseProvider(raw, fallback2) {
|
|
29310
|
+
if (raw == null || raw.trim() === "")
|
|
29311
|
+
return fallback2;
|
|
29312
|
+
const p = raw.trim().toLowerCase();
|
|
29313
|
+
if (p !== "minimax" && p !== "elevenlabs") {
|
|
29314
|
+
console.error(error("--provider must be minimax or elevenlabs"));
|
|
29315
|
+
process.exit(1);
|
|
29316
|
+
}
|
|
29317
|
+
return p;
|
|
29318
|
+
}
|
|
29047
29319
|
function registerVoiceCommand(program2) {
|
|
29048
|
-
program2.command("design").description("Design a voice from a text description").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
29320
|
+
program2.command("design").description("Design a voice from a text description (ElevenLabs)").requiredOption("--description <text>", "Voice description (min 20 chars)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
29049
29321
|
if (opts.description.trim().length < 20) {
|
|
29050
29322
|
console.error(error("--description must be at least 20 characters"));
|
|
29051
29323
|
process.exit(1);
|
|
@@ -29073,7 +29345,7 @@ function registerVoiceCommand(program2) {
|
|
|
29073
29345
|
process.exit(1);
|
|
29074
29346
|
}
|
|
29075
29347
|
});
|
|
29076
|
-
program2.command("create").description("Create a voice from a previously designed preview").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
29348
|
+
program2.command("create").description("Create a voice from a previously designed preview (ElevenLabs)").requiredOption("--sample <id>", "generated_voice_id from voice design").option("--description <text>", "Optional voice description").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
|
|
29077
29349
|
const { twinId, auth } = twinFromFlag2(opts);
|
|
29078
29350
|
const spinner = opts.json ? null : ora("Creating voice...").start();
|
|
29079
29351
|
try {
|
|
@@ -29099,7 +29371,8 @@ function registerVoiceCommand(program2) {
|
|
|
29099
29371
|
process.exit(1);
|
|
29100
29372
|
}
|
|
29101
29373
|
});
|
|
29102
|
-
program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
|
|
29374
|
+
program2.command("clone <source>").description("Clone a voice from an audio file (path | https URL | -). Default provider: minimax").option("--provider <name>", "minimax (default, mpp rapid clone) | elevenlabs (platform IVC)", "minimax").option("--voice-id <id>", "MiniMax: optional custom voice_id (8–256 chars)").option("--model <model>", "MiniMax preview TTS model", "speech-2.8-turbo").option("--preview-text <text>", "MiniMax: preview line (empty string skips preview)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
|
|
29375
|
+
const provider = parseProvider(opts.provider, "minimax");
|
|
29103
29376
|
const { twinId, auth } = twinFromFlag2(opts);
|
|
29104
29377
|
const spinner = opts.json ? null : ora("Reading audio...").start();
|
|
29105
29378
|
try {
|
|
@@ -29109,17 +29382,30 @@ function registerVoiceCommand(program2) {
|
|
|
29109
29382
|
hintMime: "audio/mpeg"
|
|
29110
29383
|
});
|
|
29111
29384
|
if (spinner)
|
|
29112
|
-
spinner.text = `Cloning voice from ${file2.name}...`;
|
|
29385
|
+
spinner.text = `Cloning voice (${provider}) from ${file2.name}...`;
|
|
29113
29386
|
const form = new FormData;
|
|
29114
29387
|
form.set("audio", new Blob([new Uint8Array(file2.buffer)], { type: file2.mime }), file2.name);
|
|
29388
|
+
form.set("provider", provider);
|
|
29389
|
+
if (provider === "minimax") {
|
|
29390
|
+
if (opts.voiceId)
|
|
29391
|
+
form.set("voice_id", opts.voiceId);
|
|
29392
|
+
if (opts.model)
|
|
29393
|
+
form.set("model_id", opts.model);
|
|
29394
|
+
if (opts.previewText !== undefined)
|
|
29395
|
+
form.set("text", opts.previewText);
|
|
29396
|
+
}
|
|
29115
29397
|
const result = await amikoWebFetch(auth, `/api/agents/${twinId}/voice/clone`, { method: "POST", multipart: form, timeoutMs: 180000 });
|
|
29116
29398
|
spinner?.stop();
|
|
29117
29399
|
renderOutput(result, (r) => {
|
|
29118
|
-
console.log(success(
|
|
29400
|
+
console.log(success(`Voice cloned (${String(r.provider ?? provider)})`));
|
|
29119
29401
|
if (r.voice_id)
|
|
29120
29402
|
console.log(label("Voice ID", String(r.voice_id)));
|
|
29121
29403
|
if (r.voice_status)
|
|
29122
29404
|
console.log(label("Status", String(r.voice_status)));
|
|
29405
|
+
if (r.demo_audio)
|
|
29406
|
+
console.log(label("Demo", String(r.demo_audio)));
|
|
29407
|
+
console.log("");
|
|
29408
|
+
console.log(dim(`Synthesize: amiko create tts "Hello" --provider ${provider} --voice ${String(r.voice_id ?? "<id>")} --yes`));
|
|
29123
29409
|
}, { json: opts.json });
|
|
29124
29410
|
} catch (e5) {
|
|
29125
29411
|
spinner?.stop();
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -33,6 +33,7 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
|
|
|
33
33
|
| "what comments are on my post?" | shell → `amiko post comments --id <postId>` |
|
|
34
34
|
| "search memory for X" | shell → `amiko memory search "X"` |
|
|
35
35
|
| "share the group's invite link" (asked by an admin) | shell → `amiko chat group info "<group>"` (confirm they're admin) → `amiko chat group invite "<group>"` |
|
|
36
|
+
| "react to the latest message from Ava about the team with a heart" | shell → `amiko chat read "Ava"` (find that message, copy its `id`) → `amiko chat react <messageId> ❤️ --yes` |
|
|
36
37
|
| "what can amiko do?" | shell → `amiko --help` |
|
|
37
38
|
|
|
38
39
|
The CLI is installed globally and is pre-authenticated when you're inside your workspace folder. Never suggest `amiko login` or `amiko connect` — they don't exist.
|
|
@@ -129,6 +130,7 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
|
|
|
129
130
|
- `amiko chat send <target> "message"` — send **as the owner**. Delivered in real time. `--yes` required in non-interactive shells (it's an outward message to a real person). `<target>` = a conversation id, a **user id**, an `@handle`, or a name. A user id / @handle / unambiguous name opens (or reuses) the DM for you — but only **after** the owner approves the send (via the confirmation, or `--yes` in your shell); nothing is created if approval is refused. **The owner never needs to start the conversation first, and there is no platform authorization step for starting a DM.** For **group chats**, use the conversation id from `list`.
|
|
130
131
|
- **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≤10MB or an Amiko URL — e.g. a generated image) and `--audio <pathOrUrl>` (local ≤25MB or an Amiko URL → sent as a voice note). One media block per message (image XOR audio). **Video is not supported** on chat yet. To share a generated asset, pass its URL from `amiko create status` (no re-upload). A local audio file is sent as a voice note with the text as a separate message.
|
|
131
132
|
- Plain names resolve against the owner's **friends first**, then people search; if ambiguous the CLI errors listing the candidates — ask the owner which one, don't blind-send. A plain name that exactly matches one of the owner's group titles targets that **group**. If a send fails, surface the CLI's error verbatim — don't invent permission explanations or ask the owner to open the chat from the app.
|
|
133
|
+
- **Reactions** — `amiko chat react <messageId> <emoji>`, `amiko chat unreact <messageId>`, `amiko chat reactions <messageId>`. The owner **won't give you a message id** — they'll say something like *"react to the latest Ava message about the team with a heart"*. Get ids from `amiko chat read <target>`: it prints each message's `id` and any existing reactions — match the message by its **content**, then act on that id. `react` adds (or replaces) your reaction — **one per message**, a new emoji replaces the previous one, and re-reacting the same emoji is a no-op (use `unreact` to remove). Pass the **actual emoji character** (`❤️`, `👍`, `😂`). `react`/`unreact` are outward (other people see them), so `--yes` is required in non-interactive shells and the confirmation shows a preview of the target message; `reactions` (read) is ungated. These act **as the owner** (人对人), just like `chat send` — not the twin's own `session_*`.
|
|
132
134
|
|
|
133
135
|
### Group chats — `amiko chat group`
|
|
134
136
|
|