@heyamiko/amiko-cli 0.14.0-beta.5 → 0.14.0-beta.7

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 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** (resolved via user search; if ambiguous, the CLI lists candidates instead of guessing).
243
- - A DM target that doesn't exist yet is **created automatically** (find-or-create).
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,15 @@ npm publish
560
562
 
561
563
  ## Changelog
562
564
 
565
+ ### 0.14.0-beta.7
566
+
567
+ - **`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).
568
+
569
+ ### 0.14.0-beta.6
570
+
571
+ - **`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.
572
+ - **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.
573
+
563
574
  ### 0.14.0-beta.5
564
575
 
565
576
  - **`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
- console.log(`Cost: ${args.cost}`);
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
- console.error(`This command will cost ${args.cost}.`);
24844
- console.error(`Action: ${args.summary}`);
24845
- console.error("");
24846
- console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
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
- cost: "no charge — creates a group chat with real people",
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)}`
@@ -27172,6 +27186,31 @@ function registerChatGroupCommand(group) {
27172
27186
  ];
27173
27187
  console.log(table(rows));
27174
27188
  });
27189
+ 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) => {
27190
+ const auth = authOrExit();
27191
+ const conv = await resolveGroupOrExit(auth, target);
27192
+ let data;
27193
+ try {
27194
+ data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/invite`, { timeoutMs: 20000 });
27195
+ } catch (e5) {
27196
+ if (statusOf2(e5) === 403) {
27197
+ console.error(error(`Can't view the invite link. ${ADMIN_TIP}`));
27198
+ process.exit(1);
27199
+ }
27200
+ exitWithError(e5, "Failed to load invite link");
27201
+ }
27202
+ if (!data.url || !data.qrUrl) {
27203
+ exitWithError(null, "The invite link response was missing its link or QR URL — nothing to share.");
27204
+ }
27205
+ if (opts.raw) {
27206
+ console.log(JSON.stringify(data, null, 2));
27207
+ return;
27208
+ }
27209
+ console.log(heading(conv.title ?? "(untitled group)"));
27210
+ console.log(label("Invite link", data.url));
27211
+ console.log(label("QR image", data.qrUrl));
27212
+ console.log(dim("Share with group admins only — anyone who opens it can join."));
27213
+ });
27175
27214
  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) => {
27176
27215
  const auth = authOrExit();
27177
27216
  const conv = await resolveGroupOrExit(auth, target);
@@ -27201,7 +27240,7 @@ function registerChatGroupCommand(group) {
27201
27240
  const members = await resolveMembersOrExit(auth, opts.member);
27202
27241
  const conv = await resolveGroupOrExit(auth, target);
27203
27242
  await requireApproval({
27204
- cost: "no charge — adds people to the group",
27243
+ free: "adds real people to the group",
27205
27244
  summary: `Add ${members.map(memberLabel).join(", ")} to group "${conv.title ?? conv.id}"`,
27206
27245
  yes: opts.yes,
27207
27246
  commandExample: `amiko chat group add ${JSON.stringify(target)} ${memberFlags(opts.member)}`
@@ -27354,33 +27393,91 @@ async function findOrCreateDm(auth, userId) {
27354
27393
  throw new Error("Could not open a conversation with that user.");
27355
27394
  return id;
27356
27395
  }
27396
+ async function fetchUserById(auth, id) {
27397
+ try {
27398
+ const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
27399
+ return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
27400
+ } catch (e5) {
27401
+ if (e5.status === 404)
27402
+ return null;
27403
+ throw e5;
27404
+ }
27405
+ }
27406
+ async function findDmByUserId(auth, userId) {
27407
+ const want = normalizeUserId(userId);
27408
+ const limit = 100;
27409
+ for (let offset = 0;offset < 1e4; offset += limit) {
27410
+ const data = await amikoWebFetch(auth, "/api/conversations", {
27411
+ query: { limit, offset },
27412
+ timeoutMs: 20000
27413
+ });
27414
+ const page = data.conversations ?? [];
27415
+ const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
27416
+ if (dm)
27417
+ return dm.id;
27418
+ if (page.length < limit || data.pagination?.has_more === false)
27419
+ return null;
27420
+ }
27421
+ return null;
27422
+ }
27423
+ function personLabel(u) {
27424
+ const name = u.name ?? u.id;
27425
+ return u.handle ? `${name} (@${u.handle})` : name;
27426
+ }
27357
27427
  async function resolveConversation(auth, target) {
27358
27428
  const t = target.trim();
27359
- if (looksLikeConversationId(t))
27360
- return t;
27429
+ if (!t) {
27430
+ throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
27431
+ }
27432
+ const person = (u) => ({
27433
+ kind: "user",
27434
+ userId: u.id,
27435
+ label: `DM with ${personLabel(u)}`
27436
+ });
27361
27437
  if (t.startsWith("@")) {
27362
27438
  const handle = t.slice(1);
27363
- const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, { timeoutMs: 15000 });
27439
+ const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
27440
+ timeoutMs: 15000
27441
+ });
27364
27442
  if (!prof.user?.id)
27365
27443
  throw new Error(`No user @${handle}.`);
27366
- return findOrCreateDm(auth, prof.user.id);
27444
+ return person({ ...prof.user, id: prof.user.id });
27445
+ }
27446
+ const idish = normalizeUserId(t);
27447
+ if (looksLikeConversationId(idish)) {
27448
+ if (idish === t) {
27449
+ try {
27450
+ const conv = await fetchConversationById(auth, t);
27451
+ return {
27452
+ kind: "conversation",
27453
+ id: conv.id,
27454
+ label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`
27455
+ };
27456
+ } catch (e5) {
27457
+ const status = e5.status;
27458
+ if (status === 403) {
27459
+ throw new Error(`Conversation ${t} exists but you are not a participant.`);
27460
+ }
27461
+ if (status !== 404)
27462
+ throw e5;
27463
+ }
27464
+ }
27465
+ const user = await fetchUserById(auth, idish);
27466
+ if (!user) {
27467
+ throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
27468
+ }
27469
+ return person(user);
27367
27470
  }
27368
27471
  const groupId = await findGroupIdByExactTitle(auth, t);
27369
27472
  if (groupId)
27370
- return groupId;
27371
- const res = await amikoWebFetch(auth, "/api/search", {
27372
- query: { q: t, type: "people", limit: 8 },
27373
- timeoutMs: 15000
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.`);
27473
+ return { kind: "conversation", id: groupId, label: `group "${t}"` };
27474
+ const { resolved, failures } = await resolveMembers(auth, [t]);
27475
+ if (failures.length || resolved.length !== 1) {
27476
+ const reason = failures[0]?.reason ?? "could not be resolved";
27477
+ throw new Error(`"${t}" ${reason}`);
27382
27478
  }
27383
- return findOrCreateDm(auth, users[0].id);
27479
+ const p = resolved[0];
27480
+ return person({ id: p.userId, name: p.name, handle: p.handle });
27384
27481
  }
27385
27482
  var HTTPS_RE = /^https:\/\//i;
27386
27483
  var CHAT_IMAGE_MIME = new Set([
@@ -27467,11 +27564,20 @@ function registerChatCommand(chat) {
27467
27564
  console.log(dim(` ${unread.trim()}`));
27468
27565
  }
27469
27566
  });
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) => {
27567
+ 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
27568
  const auth = authOrExit();
27472
27569
  let convId;
27473
27570
  try {
27474
- convId = await resolveConversation(auth, target);
27571
+ const resolved = await resolveConversation(auth, target);
27572
+ if (resolved.kind === "user") {
27573
+ const existing = await findDmByUserId(auth, resolved.userId);
27574
+ if (!existing) {
27575
+ throw new Error(`No ${resolved.label} yet — start one with \`amiko chat send\`.`);
27576
+ }
27577
+ convId = existing;
27578
+ } else {
27579
+ convId = resolved.id;
27580
+ }
27475
27581
  } catch (e5) {
27476
27582
  console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
27477
27583
  process.exit(1);
@@ -27502,26 +27608,33 @@ function registerChatCommand(chat) {
27502
27608
  console.log(dim(` ${when}`));
27503
27609
  }
27504
27610
  });
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) => {
27611
+ 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
27612
  const auth = authOrExit();
27507
27613
  if (opts.image && opts.audio) {
27508
27614
  console.error(error("Attach either --image or --audio, not both (one media block per message)."));
27509
27615
  process.exit(1);
27510
27616
  }
27511
- let convId;
27617
+ let resolved;
27512
27618
  try {
27513
- convId = await resolveConversation(auth, target);
27619
+ resolved = await resolveConversation(auth, target);
27514
27620
  } catch (e5) {
27515
27621
  console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
27516
27622
  process.exit(1);
27517
27623
  }
27518
27624
  const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
27519
27625
  await requireApproval({
27520
- cost: "no charge — sends a message as you (the owner)",
27521
- summary: `Send to ${target}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
27626
+ free: "sends a message as you (the owner) — the recipients see it",
27627
+ summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
27522
27628
  yes: opts.yes,
27523
27629
  commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
27524
27630
  });
27631
+ let convId;
27632
+ try {
27633
+ convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
27634
+ } catch (e5) {
27635
+ console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
27636
+ process.exit(1);
27637
+ }
27525
27638
  const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
27526
27639
  const post = (body) => amikoWebFetch(auth, messagesPath, {
27527
27640
  method: "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.14.0-beta.5",
3
+ "version": "0.14.0-beta.7",
4
4
  "description": "Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace agents",
5
5
  "type": "module",
6
6
  "bin": {
package/skills/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: amiko-cli
3
- description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, generate media — images, video, speech, music, and SFX — via Create Studio (`amiko create`, async and charged on success), call paid MPP marketplace services (X/Twitter search, Amazon product search, TTS/STT, AI chat), manage the twin's identity and drive (files, folders, RAG), voice and avatar, and social graph (friends, posts, comments, feed). Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). The owner's DMs and group chats are `amiko chat` (list / read / send / create + manage group chats AS the owner, 人对人); the agent's OWN sessions are the openhermit gateway's session_list / session_send (AS the agent) — different identities, different surfaces. Use this skill whenever the user asks about their Amiko notifications, chats/DMs, drive, memory, social graph, wallets, credits, or marketplace services — anything that would show up in their Amiko account or cost AMIKO/credits.
3
+ description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, generate media — images, video, speech, music, and SFX — via Create Studio (`amiko create`, async and charged on success), call paid MPP marketplace services (X/Twitter search, Amazon product search, TTS/STT, AI chat), manage the twin's identity and drive (files, folders, RAG), voice and avatar, and social graph (friends, posts, comments, feed). Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). The owner's DMs and group chats are `amiko chat` (list / read / send / create + manage group chats AS the owner, including sharing a group's invite/join link + QR, 人对人); the agent's OWN sessions are the openhermit gateway's session_list / session_send (AS the agent) — different identities, different surfaces. Use this skill whenever the user asks about their Amiko notifications, chats/DMs, drive, memory, social graph, wallets, credits, or marketplace services — anything that would show up in their Amiko account or cost AMIKO/credits.
4
4
  homepage: https://platform.heyamiko.com
5
5
  metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["node"]}}}
6
6
  ---
@@ -32,6 +32,7 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
32
32
  | "find files about Q1 revenue" | shell → `amiko drive search "Q1 revenue"` |
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
+ | "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>"` |
35
36
  | "what can amiko do?" | shell → `amiko --help` |
36
37
 
37
38
  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.
@@ -49,7 +50,7 @@ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `
49
50
 
50
51
  ## Critical Rules
51
52
 
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` (no charge, so nothing to quote; but 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`.
53
+ 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
54
  2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
54
55
  3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
55
56
  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,21 +125,22 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
124
125
  `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
126
 
126
127
  - `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). For **group chats**, use the conversation id from `list`; for a **DM**, an `@handle`/name resolves + finds-or-creates the DM.
128
+ - `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.
129
+ - `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
130
  - **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
- - Name resolution goes through user search; if ambiguous it lists candidates — don't blind-send. A plain name that exactly matches one of the owner's group titles targets that **group**. Server enforces who you're allowed to message (friend/participant rules); surface its error, don't retry.
131
+ - 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
132
 
132
133
  ### Group chats — `amiko chat group`
133
134
 
134
- Create and manage the owner's group chats: `create <name> --member <who>…`, `list`, `info <group>`, `rename <group> <newTitle>`, `add <group> --member <who>…`, `remove <group> --member <who>…`, `promote <group> --member <who>`, `leave <group>` (alias `delete`).
135
+ Create and manage the owner's group chats: `create <name> --member <who>…`, `list`, `info <group>`, `invite <group>`, `rename <group> <newTitle>`, `add <group> --member <who>…`, `remove <group> --member <who>…`, `promote <group> --member <who>`, `leave <group>` (alias `delete`).
135
136
 
136
137
  - One-shot example — "create a group called Leandro testing with Sophie, Mars and Matthew":
137
138
  `amiko chat group create "Leandro testing" --member Sophie --member Mars --member Matthew --yes`
138
139
  - `--member` repeats per person and takes a name, `@handle`, or user id. Plain names resolve against the owner's **friends** first, then people search; an ambiguous name errors listing candidates — prefer an exact `@handle` or a user id from `amiko friends list --json`. If any member fails to resolve, the whole command aborts **before anything changes** — fix and re-run.
139
140
  - `<group>` is a conversation id (from `chat group list`) or a title (matched case-insensitively — exact matches win before substring matches; if several groups still match, the CLI errors listing the candidates; only the ~100 most recent conversations are scanned, so use the id for old groups).
140
141
  - **`leave` (alias `delete`) only hides the group for the owner — it does NOT delete it for other members; there is no true group deletion.** Say so if the owner asks to delete a group. Rename, add, remove-others, and promote require the owner to be a group **admin** (the creator is one automatically); a 403 means they're not — report it, don't retry.
141
- - Gating: `create`/`add` message real people and `remove`/`leave`/`rename`/`promote` are destructive — all require `--yes` in your shell (see Critical Rules). Reads (`list`, `info`) are free and ungated.
142
+ - `amiko chat group invite <group>` — show the group's **invite/join link + a QR image URL** (both are shareable URLs; the QR encodes the same join link). Use it when a group admin asks how to invite people or for the group's link or QR. **Admins only:** before sharing, confirm the person asking holds the `admin`/`owner` role — check the ROLE column in `amiko chat group info <group>`. If they're a regular member, politely decline and do **not** reveal the link or QR. The link lets anyone who opens it join the group, so treat it like a credential. A 403 means your owner isn't an admin of that group — report it, don't retry.
143
+ - Gating: `create`/`add` message real people and `remove`/`leave`/`rename`/`promote` are destructive — all require `--yes` in your shell (see Critical Rules). Reads (`list`, `info`, `invite`) are free and ungated.
142
144
 
143
145
  ## Card — behavior notes
144
146