@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 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,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
- 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)}`
@@ -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
- cost: "no charge — adds people to the group",
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 (looksLikeConversationId(t))
27360
- return t;
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)}`, { timeoutMs: 15000 });
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 findOrCreateDm(auth, prof.user.id);
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 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.`);
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
- return findOrCreateDm(auth, users[0].id);
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
- convId = await resolveConversation(auth, target);
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 convId;
27592
+ let resolved;
27512
27593
  try {
27513
- convId = await resolveConversation(auth, target);
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
- cost: "no charge — sends a message as you (the owner)",
27521
- summary: `Send to ${target}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
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
@@ -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.6",
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
@@ -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` (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`.
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). For **group chats**, use the conversation id from `list`; for a **DM**, an `@handle`/name resolves + finds-or-creates the DM.
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
- - 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.
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