@heyamiko/amiko-cli 0.14.0-beta.5 → 0.14.0-beta.6
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 +9 -2
- package/dist/index.js +119 -31
- package/package.json +1 -1
- package/skills/SKILL.md +4 -4
package/README.md
CHANGED
|
@@ -234,13 +234,15 @@ amiko chat list --limit 50 --archived # include archived
|
|
|
234
234
|
amiko chat read @sophie # recent messages with a user (by @handle)
|
|
235
235
|
amiko chat read <conversationId> --limit 40 # by conversation id (from `chat list`) — works for groups too
|
|
236
236
|
amiko chat send @sophie "on my way!" --yes # send as the owner
|
|
237
|
+
amiko chat send <userId> "hey!" --yes # user id works too — the DM is opened automatically
|
|
238
|
+
amiko chat send Mars "hey!" --yes # or a name (friends first, then people search)
|
|
237
239
|
amiko chat send <groupConversationId> "hi all" --yes # send to a group chat
|
|
238
240
|
amiko chat send @sophie "look 👀" --image ./cat.png --yes # attach a local image
|
|
239
241
|
amiko chat send @sophie "made this 🎵" --audio <generated-url> --yes # attach audio (e.g. from `amiko create`)
|
|
240
242
|
```
|
|
241
243
|
|
|
242
|
-
- **`<target>`** for `read`/`send` is a **conversation id** (from `chat list` — use this for groups), an **`@handle`**, or a **name**
|
|
243
|
-
-
|
|
244
|
+
- **`<target>`** for `read`/`send` is a **conversation id** (from `chat list` — use this for groups), a **user id**, an **`@handle`**, or a **name**. Names resolve against your **friends first**, then people search; if ambiguous, the CLI lists candidates instead of guessing. A cuid that isn't one of your conversations is retried as a user id, so pasting a user id "just works".
|
|
245
|
+
- `send` to a person you have no DM with yet **creates the DM automatically** (find-or-create, after the confirmation). `read` never creates one — it errors if no DM exists yet.
|
|
244
246
|
- **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≤10MB or an Amiko URL) and `--audio <pathOrUrl>` (local ≤25MB or an Amiko URL → sent as a voice note). One media block per message (image *or* audio). **Video isn't supported in chat yet.** A local file is uploaded; an Amiko URL (e.g. a generated asset from `amiko create`) attaches directly with no re-upload. (A local *audio* file is sent as a voice note, with your text as a separate message.)
|
|
245
247
|
- `chat send` messages a real person, so it's **gated on `--yes`** in non-interactive shells. Delivery is real-time. `--raw` prints raw JSON on any subcommand.
|
|
246
248
|
|
|
@@ -560,6 +562,11 @@ npm publish
|
|
|
560
562
|
|
|
561
563
|
## Changelog
|
|
562
564
|
|
|
565
|
+
### 0.14.0-beta.6
|
|
566
|
+
|
|
567
|
+
- **`amiko chat send`/`read` accept user ids — DMs no longer require an existing conversation.** Target resolution is rebuilt: a cuid target is verified as a conversation first and, on 404, retried as a user id (user ids and conversation ids are both cuids — previously a user id went straight to the messages route and died with "Conversation not found"). Plain names now resolve **friends-first** (then people search) via the shared `resolveMembers`, with ambiguity reported as a candidate list. Resolution is fully read-only: for `send`, a missing DM is found-or-created only **after** the `--yes`/approval gate (no more empty conversations from an unapproved send), and `read` never creates a DM at all — it errors if none exists. The confirmation summary now shows the **resolved** recipient (`Send to DM with Mars (@mars): …`) so a wrong-person resolution is visible before anything goes out. Unknown cuids are rejected before any conversation is created (the backend accepts dangling participant ids silently), and a 403 on a real conversation no longer falls through to DM creation.
|
|
568
|
+
- **Free outward actions no longer talk about cost.** `requireApproval` gained a `free` mode used by `chat send` and `chat group create/add`: the non-interactive refusal now says the action is free and tells the agent to confirm the action itself — instead of the old "This command will cost no charge… quote the cost to the user", which made twins ask owners to "approve the paid operation, cost 0". SKILL.md's Critical Rule 1 spells out the anti-pattern, documents user-id DM targets ("the owner never needs to start the conversation first"), and drops the false "server enforces friend rules" claim that fed authorization hallucinations.
|
|
569
|
+
|
|
563
570
|
### 0.14.0-beta.5
|
|
564
571
|
|
|
565
572
|
- **`amiko chat group` — group-chat management as the owner.** New subcommand group under `chat`: `create` (name + repeatable `--member`, one atomic POST), `list`, `info` (members + roles), `rename`, `add`, `remove`, `promote`, and `leave` (alias `delete`). Members resolve friends-first by name (then people search), all-or-nothing before any mutation; groups address by id or case-insensitive title. Uses the existing amiko-web conversation routes (twin token) — rename/add/remove-others/promote are admin-enforced server-side, and `leave|delete` is honest about the platform's semantics: it only hides the group for the caller, there is no delete-for-everyone. `create`/`add` gate on `requireApproval`, the destructive rest on `confirmDestructive` (both exit 2 without `--yes` in non-interactive shells). Plain-name `chat send`/`read` targets now prefer an exact, unambiguous group-title match over people search. Shared conversation types + resolution helpers extracted to `src/lib/conversations.ts`; `convLabel` self-filtering now strips the `did:privy:` prefix so DM labels no longer include the owner. SKILL.md gains a Group chats block + gating-list updates.
|
package/dist/index.js
CHANGED
|
@@ -24825,12 +24825,19 @@ async function confirmDestructive(args) {
|
|
|
24825
24825
|
process.exit(2);
|
|
24826
24826
|
}
|
|
24827
24827
|
async function requireApproval(args) {
|
|
24828
|
+
if (args.cost === undefined === (args.free === undefined)) {
|
|
24829
|
+
throw new Error("requireApproval: set exactly one of `cost` or `free`.");
|
|
24830
|
+
}
|
|
24828
24831
|
if (args.yes)
|
|
24829
24832
|
return;
|
|
24830
24833
|
if (process.stdin.isTTY) {
|
|
24831
24834
|
console.log("");
|
|
24832
24835
|
console.log(`About to run: ${args.summary}`);
|
|
24833
|
-
|
|
24836
|
+
if (args.cost !== undefined) {
|
|
24837
|
+
console.log(`Cost: ${args.cost}`);
|
|
24838
|
+
} else {
|
|
24839
|
+
console.log(`Free — but this is a real outward action other people will see (${args.free}).`);
|
|
24840
|
+
}
|
|
24834
24841
|
const ok = await promptYesNo("Proceed? [y/N] ");
|
|
24835
24842
|
if (!ok) {
|
|
24836
24843
|
console.log(dim("Cancelled."));
|
|
@@ -24840,10 +24847,17 @@ async function requireApproval(args) {
|
|
|
24840
24847
|
}
|
|
24841
24848
|
console.error(error(`Refusing to run without --yes (non-interactive shell detected).`));
|
|
24842
24849
|
console.error("");
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24850
|
+
if (args.cost !== undefined) {
|
|
24851
|
+
console.error(`This command will cost ${args.cost}.`);
|
|
24852
|
+
console.error(`Action: ${args.summary}`);
|
|
24853
|
+
console.error("");
|
|
24854
|
+
console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
|
|
24855
|
+
} else {
|
|
24856
|
+
console.error(`This action is FREE — do not quote a price or say "cost: 0". It is outward-facing: ${args.free}.`);
|
|
24857
|
+
console.error(`Action: ${args.summary}`);
|
|
24858
|
+
console.error("");
|
|
24859
|
+
console.error(dim(`Ask the owner to confirm the action itself (what will be sent and who sees it), then re-run with --yes:`));
|
|
24860
|
+
}
|
|
24847
24861
|
console.error(dim(` ${args.commandExample} --yes`));
|
|
24848
24862
|
process.exit(2);
|
|
24849
24863
|
}
|
|
@@ -27078,7 +27092,7 @@ function registerChatGroupCommand(group) {
|
|
|
27078
27092
|
const auth = authOrExit();
|
|
27079
27093
|
const members = await resolveMembersOrExit(auth, opts.member);
|
|
27080
27094
|
await requireApproval({
|
|
27081
|
-
|
|
27095
|
+
free: "creates a group chat that real people are added to",
|
|
27082
27096
|
summary: `Create group "${name}" with ${members.map(memberLabel).join(", ")}`,
|
|
27083
27097
|
yes: opts.yes,
|
|
27084
27098
|
commandExample: `amiko chat group create ${JSON.stringify(name)} ${memberFlags(opts.member)}`
|
|
@@ -27201,7 +27215,7 @@ function registerChatGroupCommand(group) {
|
|
|
27201
27215
|
const members = await resolveMembersOrExit(auth, opts.member);
|
|
27202
27216
|
const conv = await resolveGroupOrExit(auth, target);
|
|
27203
27217
|
await requireApproval({
|
|
27204
|
-
|
|
27218
|
+
free: "adds real people to the group",
|
|
27205
27219
|
summary: `Add ${members.map(memberLabel).join(", ")} to group "${conv.title ?? conv.id}"`,
|
|
27206
27220
|
yes: opts.yes,
|
|
27207
27221
|
commandExample: `amiko chat group add ${JSON.stringify(target)} ${memberFlags(opts.member)}`
|
|
@@ -27354,33 +27368,91 @@ async function findOrCreateDm(auth, userId) {
|
|
|
27354
27368
|
throw new Error("Could not open a conversation with that user.");
|
|
27355
27369
|
return id;
|
|
27356
27370
|
}
|
|
27371
|
+
async function fetchUserById(auth, id) {
|
|
27372
|
+
try {
|
|
27373
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
|
|
27374
|
+
return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
|
|
27375
|
+
} catch (e5) {
|
|
27376
|
+
if (e5.status === 404)
|
|
27377
|
+
return null;
|
|
27378
|
+
throw e5;
|
|
27379
|
+
}
|
|
27380
|
+
}
|
|
27381
|
+
async function findDmByUserId(auth, userId) {
|
|
27382
|
+
const want = normalizeUserId(userId);
|
|
27383
|
+
const limit = 100;
|
|
27384
|
+
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
27385
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
27386
|
+
query: { limit, offset },
|
|
27387
|
+
timeoutMs: 20000
|
|
27388
|
+
});
|
|
27389
|
+
const page = data.conversations ?? [];
|
|
27390
|
+
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
27391
|
+
if (dm)
|
|
27392
|
+
return dm.id;
|
|
27393
|
+
if (page.length < limit || data.pagination?.has_more === false)
|
|
27394
|
+
return null;
|
|
27395
|
+
}
|
|
27396
|
+
return null;
|
|
27397
|
+
}
|
|
27398
|
+
function personLabel(u) {
|
|
27399
|
+
const name = u.name ?? u.id;
|
|
27400
|
+
return u.handle ? `${name} (@${u.handle})` : name;
|
|
27401
|
+
}
|
|
27357
27402
|
async function resolveConversation(auth, target) {
|
|
27358
27403
|
const t = target.trim();
|
|
27359
|
-
if (
|
|
27360
|
-
|
|
27404
|
+
if (!t) {
|
|
27405
|
+
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
27406
|
+
}
|
|
27407
|
+
const person = (u) => ({
|
|
27408
|
+
kind: "user",
|
|
27409
|
+
userId: u.id,
|
|
27410
|
+
label: `DM with ${personLabel(u)}`
|
|
27411
|
+
});
|
|
27361
27412
|
if (t.startsWith("@")) {
|
|
27362
27413
|
const handle = t.slice(1);
|
|
27363
|
-
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27414
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27415
|
+
timeoutMs: 15000
|
|
27416
|
+
});
|
|
27364
27417
|
if (!prof.user?.id)
|
|
27365
27418
|
throw new Error(`No user @${handle}.`);
|
|
27366
|
-
return
|
|
27419
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
27420
|
+
}
|
|
27421
|
+
const idish = normalizeUserId(t);
|
|
27422
|
+
if (looksLikeConversationId(idish)) {
|
|
27423
|
+
if (idish === t) {
|
|
27424
|
+
try {
|
|
27425
|
+
const conv = await fetchConversationById(auth, t);
|
|
27426
|
+
return {
|
|
27427
|
+
kind: "conversation",
|
|
27428
|
+
id: conv.id,
|
|
27429
|
+
label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`
|
|
27430
|
+
};
|
|
27431
|
+
} catch (e5) {
|
|
27432
|
+
const status = e5.status;
|
|
27433
|
+
if (status === 403) {
|
|
27434
|
+
throw new Error(`Conversation ${t} exists but you are not a participant.`);
|
|
27435
|
+
}
|
|
27436
|
+
if (status !== 404)
|
|
27437
|
+
throw e5;
|
|
27438
|
+
}
|
|
27439
|
+
}
|
|
27440
|
+
const user = await fetchUserById(auth, idish);
|
|
27441
|
+
if (!user) {
|
|
27442
|
+
throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
|
|
27443
|
+
}
|
|
27444
|
+
return person(user);
|
|
27367
27445
|
}
|
|
27368
27446
|
const groupId = await findGroupIdByExactTitle(auth, t);
|
|
27369
27447
|
if (groupId)
|
|
27370
|
-
return groupId;
|
|
27371
|
-
const
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
const users = res.users ?? [];
|
|
27376
|
-
if (users.length === 0) {
|
|
27377
|
-
throw new Error(`No user matching "${t}". Use an exact @handle, or a conversation id from \`amiko chat list\`.`);
|
|
27378
|
-
}
|
|
27379
|
-
if (users.length > 1) {
|
|
27380
|
-
const names = users.map((u) => `${u.name ?? "?"} (@${u.handle ?? u.id.slice(0, 8)})`).join(", ");
|
|
27381
|
-
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use an exact @handle or a conversation id.`);
|
|
27448
|
+
return { kind: "conversation", id: groupId, label: `group "${t}"` };
|
|
27449
|
+
const { resolved, failures } = await resolveMembers(auth, [t]);
|
|
27450
|
+
if (failures.length || resolved.length !== 1) {
|
|
27451
|
+
const reason = failures[0]?.reason ?? "could not be resolved";
|
|
27452
|
+
throw new Error(`"${t}" ${reason}`);
|
|
27382
27453
|
}
|
|
27383
|
-
|
|
27454
|
+
const p = resolved[0];
|
|
27455
|
+
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
27384
27456
|
}
|
|
27385
27457
|
var HTTPS_RE = /^https:\/\//i;
|
|
27386
27458
|
var CHAT_IMAGE_MIME = new Set([
|
|
@@ -27467,11 +27539,20 @@ function registerChatCommand(chat) {
|
|
|
27467
27539
|
console.log(dim(` ${unread.trim()}`));
|
|
27468
27540
|
}
|
|
27469
27541
|
});
|
|
27470
|
-
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) => {
|
|
27542
|
+
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) => {
|
|
27471
27543
|
const auth = authOrExit();
|
|
27472
27544
|
let convId;
|
|
27473
27545
|
try {
|
|
27474
|
-
|
|
27546
|
+
const resolved = await resolveConversation(auth, target);
|
|
27547
|
+
if (resolved.kind === "user") {
|
|
27548
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
27549
|
+
if (!existing) {
|
|
27550
|
+
throw new Error(`No ${resolved.label} yet — start one with \`amiko chat send\`.`);
|
|
27551
|
+
}
|
|
27552
|
+
convId = existing;
|
|
27553
|
+
} else {
|
|
27554
|
+
convId = resolved.id;
|
|
27555
|
+
}
|
|
27475
27556
|
} catch (e5) {
|
|
27476
27557
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
27477
27558
|
process.exit(1);
|
|
@@ -27502,26 +27583,33 @@ function registerChatCommand(chat) {
|
|
|
27502
27583
|
console.log(dim(` ${when}`));
|
|
27503
27584
|
}
|
|
27504
27585
|
});
|
|
27505
|
-
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) => {
|
|
27586
|
+
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) => {
|
|
27506
27587
|
const auth = authOrExit();
|
|
27507
27588
|
if (opts.image && opts.audio) {
|
|
27508
27589
|
console.error(error("Attach either --image or --audio, not both (one media block per message)."));
|
|
27509
27590
|
process.exit(1);
|
|
27510
27591
|
}
|
|
27511
|
-
let
|
|
27592
|
+
let resolved;
|
|
27512
27593
|
try {
|
|
27513
|
-
|
|
27594
|
+
resolved = await resolveConversation(auth, target);
|
|
27514
27595
|
} catch (e5) {
|
|
27515
27596
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
27516
27597
|
process.exit(1);
|
|
27517
27598
|
}
|
|
27518
27599
|
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
|
|
27519
27600
|
await requireApproval({
|
|
27520
|
-
|
|
27521
|
-
summary: `Send to ${
|
|
27601
|
+
free: "sends a message as you (the owner) — the recipients see it",
|
|
27602
|
+
summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
|
|
27522
27603
|
yes: opts.yes,
|
|
27523
27604
|
commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
|
|
27524
27605
|
});
|
|
27606
|
+
let convId;
|
|
27607
|
+
try {
|
|
27608
|
+
convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
|
|
27609
|
+
} catch (e5) {
|
|
27610
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
|
|
27611
|
+
process.exit(1);
|
|
27612
|
+
}
|
|
27525
27613
|
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
27526
27614
|
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
27527
27615
|
method: "POST",
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `
|
|
|
49
49
|
|
|
50
50
|
## Critical Rules
|
|
51
51
|
|
|
52
|
-
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `card mint`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.** The same `--yes` gate also covers **free outward social actions** — `chat send`, `chat group create`, `chat group add` (
|
|
52
|
+
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `card mint`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.** The same `--yes` gate also covers **free outward social actions** — `chat send`, `chat group create`, `chat group add` (real people see them, so get the owner's explicit approval for the action itself) — and **destructive ops** like `chat group remove/leave/rename/promote`, `twin update --public`, `drive delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `review reject`. **For these free actions never mention cost, never say "the cost is 0", and never call it a paid operation** — ask for plain confirmation of the action itself (e.g. "Send "hi" to Mars — go ahead?").
|
|
53
53
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
|
|
54
54
|
3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
|
|
55
55
|
4. **Auth is automatic.** Never suggest `amiko login` / `amiko connect`. Run from the agent's workspace folder; if anything looks off, `amiko accounts` shows the resolved `userId` / `twinId`.
|
|
@@ -124,10 +124,10 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
|
|
|
124
124
|
`amiko chat` is the **owner's** conversations, acting **as the owner** (人对人) — NOT the agent's own sessions (those are the gateway's `session_*`, as the agent).
|
|
125
125
|
|
|
126
126
|
- `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
|
|
127
|
-
- `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), an `@handle`, or a name.
|
|
128
|
-
- `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).
|
|
127
|
+
- `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), a **user id**, an `@handle`, or a name. Reading never creates a conversation — if there's no DM with that person yet, it says so.
|
|
128
|
+
- `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`.
|
|
129
129
|
- **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.
|
|
130
|
-
-
|
|
130
|
+
- 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.
|
|
131
131
|
|
|
132
132
|
### Group chats — `amiko chat group`
|
|
133
133
|
|