@heyamiko/amiko-cli 0.14.0-beta.20 β†’ 0.14.0-beta.21

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
@@ -259,6 +259,9 @@ amiko chat send "Trip planning" "standup in 5 @all" --yes # @all β€” notifie
259
259
  amiko chat send "Trip planning" "standup in 5" --all --yes # same, via the explicit flag
260
260
  amiko chat send @sophie "look πŸ‘€" --image ./cat.png --yes # attach a local image
261
261
  amiko chat send @sophie "made this 🎡" --audio <generated-url> --yes # attach audio (e.g. from `amiko create`)
262
+ amiko chat send @sophie --gif "happy dance" --yes # send the top Klipy GIF for a search (message optional)
263
+ amiko chat gifs "happy dance" # browse GIFs first, then send a specific one:
264
+ amiko chat send @sophie "we did it!" --gif <gifUrl> --yes # attach an exact GIF by its Klipy URL
262
265
  amiko chat receipts <messageId> # who read your message + when (id from `chat read`)
263
266
  amiko chat pin <messageId> --yes # pin for everyone (id from `chat read`; groups: admins only)
264
267
  amiko chat unpin <messageId> --yes # remove a pin for everyone
@@ -267,7 +270,8 @@ amiko chat pinned "Trip planning" # list a conversation's pinned
267
270
 
268
271
  - **`<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".
269
272
  - `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.
270
- - **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.)
273
+ - **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≀10MB or an Amiko URL), `--audio <pathOrUrl>` (local ≀25MB or an Amiko URL β†’ sent as a voice note), and `--gif <queryOrUrl>` (search words send the **top Klipy result**; or pass an exact Klipy URL from `amiko chat gifs` β€” non-Klipy URLs are refused; to send another image, use `--image` with a local file or an Amiko URL). One media block per message (image *or* audio *or* gif). **General video files aren'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.)
274
+ - **GIFs**: `amiko chat gifs [query]` lists Klipy GIFs (trending with no query; `--page`/`--limit`/`--raw`) β€” read-only and ungated, powered by KLIPY. The message text is optional with `--gif` (a captionless GIF sends as just the GIF); a caption renders under the GIF on every client.
271
275
  - **@all in groups**: `--all` prepends an @all mention, and a standalone `@all` word typed in the message converts too β€” either way **every member** is notified. Group **admins/owners** can always use it; everyone else only after an admin runs `amiko chat group mention-all <group> on`. Without permission, `--all` fails with the fix named, while a typed `@all` is delivered as plain text (with a note). Incoming mention markup renders as plain `@name`/`@all` in `chat read` and `chat list`.
272
276
  - `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.
273
277
  - **`chat receipts <messageId>`** shows read receipts for **your own** messages (server-enforced; 403 otherwise): sent time, who read it with each person's first-read time, and who it was delivered to but hasn't read. Reads from before receipts tracking show as "exact time unknown". Read-only, ungated.
@@ -597,6 +601,10 @@ npm publish
597
601
 
598
602
  ## Changelog
599
603
 
604
+ ### 0.14.0-beta.21
605
+
606
+ - **Chat GIFs: `amiko chat gifs [query]` + `amiko chat send --gif <queryOrUrl>`.** `chat gifs` browses Klipy GIFs via the platform proxy (`GET /api/gifs`; trending with no query, `--page`/`--limit`/`--raw`, "Powered by KLIPY") β€” read-only and ungated. `chat send --gif` attaches a GIF to a message: search words send the top result, a Klipy URL (from `chat gifs`) attaches exactly, and non-Klipy URLs are refused. Sends use the cross-client GIF contract β€” Klipy mp4/webm β†’ `message_type: "video"` + `metadata.videos`, `.gif` β†’ `"image"` + `metadata.images`, each record `{url, name, contentType, kind: "gif", width?, height?}` β€” so web/desktop/mobile render a looping watermarked GIF. The `<message>` positional is now optional when `--gif` is present (`content` falls back to `"GIF"`); captions ride as `metadata.image_caption`, which the `--image` path now also sets so image captions actually display in bubbles. One media block per message (`--image`/`--audio`/`--gif` are mutually exclusive). Requires the companion amiko-web `feat/chat-gif-picker` deploy (`GET /api/gifs`) β€” the twin token already authenticates against it, so no other backend work is needed.
607
+
600
608
  ### 0.14.0-beta.20
601
609
 
602
610
  - **Draft posts: `amiko post create --draft`, `amiko post drafts`, `amiko post publish <postId>`.** `--draft` saves via `POST /api/posts { status: "draft" }` β€” no side effects fire and no share URL is printed (drafts 404 by id until published; success copy says "Draft saved" with a publish hint, and `post_url` is omitted from `--json`). `drafts` lists the owner's draft posts (`GET /api/posts?status=draft`, offset pagination via `--limit`/`--offset`; owner-wide across all the owner's twins). `publish` PATCHes `{ status: "published" }`, stamps a fresh timestamp, fires mention/hashtag notifications, and prints the canonical share URL; a 404 points at `amiko post drafts` since drafts can't be fetched by id. Requires the amiko-web draft-posts API (`feat/draft-posts`).
package/dist/index.js CHANGED
@@ -27671,6 +27671,102 @@ function registerChatGroupCommand(group) {
27671
27671
  });
27672
27672
  }
27673
27673
 
27674
+ // src/lib/gifs.ts
27675
+ function parseUrl(url2) {
27676
+ try {
27677
+ return new URL(url2);
27678
+ } catch {
27679
+ return null;
27680
+ }
27681
+ }
27682
+ function isKlipyMediaUrl(url2) {
27683
+ const parsed = parseUrl(url2);
27684
+ if (!parsed)
27685
+ return false;
27686
+ return parsed.hostname === "klipy.com" || parsed.hostname.endsWith(".klipy.com");
27687
+ }
27688
+ var KLIPY_VIDEO_EXTENSION_RE = /\.(?:mp4|webm)(?:[?#].*)?$/i;
27689
+ function isKlipyVideoMediaUrl(url2) {
27690
+ if (!isKlipyMediaUrl(url2))
27691
+ return false;
27692
+ const parsed = parseUrl(url2);
27693
+ if (!parsed)
27694
+ return false;
27695
+ return KLIPY_VIDEO_EXTENSION_RE.test(parsed.pathname);
27696
+ }
27697
+ async function searchGifs(auth, q, page = 1) {
27698
+ return amikoWebFetch(auth, "/api/gifs", {
27699
+ query: { q, page }
27700
+ });
27701
+ }
27702
+ function urlBasename(url2) {
27703
+ const last = parseUrl(url2)?.pathname.split("/").pop();
27704
+ if (!last)
27705
+ return "GIF";
27706
+ try {
27707
+ return decodeURIComponent(last);
27708
+ } catch {
27709
+ return last;
27710
+ }
27711
+ }
27712
+ function gifToMetadata(gif) {
27713
+ return {
27714
+ url: gif.url,
27715
+ name: gif.title || urlBasename(gif.url),
27716
+ contentType: isKlipyVideoMediaUrl(gif.url) ? "video/mp4" : "image/gif",
27717
+ kind: "gif",
27718
+ ...gif.width && gif.height ? { width: gif.width, height: gif.height } : {}
27719
+ };
27720
+ }
27721
+
27722
+ // src/commands/chat-gifs.ts
27723
+ var DEFAULT_LIMIT = 10;
27724
+ var MAX_TITLE = 40;
27725
+ function clampLimit(raw) {
27726
+ const n = Number.parseInt(raw, 10);
27727
+ if (!Number.isFinite(n))
27728
+ return DEFAULT_LIMIT;
27729
+ return Math.min(24, Math.max(1, n));
27730
+ }
27731
+ function truncate(text, max) {
27732
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
27733
+ }
27734
+ function registerChatGifsCommand(chat) {
27735
+ chat.command("gifs [query]").description("Search Klipy GIFs (trending when no query) β€” pass a result URL to `amiko chat send --gif`").option("--page <n>", "Result page (24 per page)", "1").option("--limit <n>", "Max results shown (1-24)", String(DEFAULT_LIMIT)).option("--raw", "Output raw JSON").action(async (query, opts) => {
27736
+ const auth = authOrExit();
27737
+ const q = query?.trim() ?? "";
27738
+ const page = Math.max(1, Number.parseInt(opts.page, 10) || 1);
27739
+ let data;
27740
+ try {
27741
+ data = await searchGifs(auth, q, page);
27742
+ } catch (e5) {
27743
+ console.error(error(e5 instanceof Error ? e5.message : "GIF search failed"));
27744
+ process.exit(1);
27745
+ }
27746
+ renderOutput(data, (d) => {
27747
+ const items = (d.items ?? []).slice(0, clampLimit(opts.limit));
27748
+ if (items.length === 0) {
27749
+ console.log(dim(q ? `No GIFs found for "${q}".` : "No trending GIFs."));
27750
+ console.log(dim("Powered by KLIPY"));
27751
+ return;
27752
+ }
27753
+ console.log(heading(q ? `GIFs matching "${q}" (page ${d.page})` : `Trending GIFs (page ${d.page})`));
27754
+ const rows = [
27755
+ [dim("TITLE"), dim("SIZE"), dim("URL")],
27756
+ ...items.map((gif) => [
27757
+ truncate(gif.title || "GIF", MAX_TITLE),
27758
+ gif.width && gif.height ? `${gif.width}x${gif.height}` : "-",
27759
+ gif.gifUrl
27760
+ ])
27761
+ ];
27762
+ console.log(table(rows));
27763
+ console.log(dim(`
27764
+ Send one: amiko chat send <target> --gif <url>${d.hasNext ? ` (more: --page ${d.page + 1})` : ""}`));
27765
+ console.log(dim("Powered by KLIPY"));
27766
+ }, { json: opts.raw });
27767
+ });
27768
+ }
27769
+
27674
27770
  // src/commands/chat.ts
27675
27771
  var PIN_LIMIT = 20;
27676
27772
  function pinErrorMessage(e5, fallback2) {
@@ -27789,6 +27885,7 @@ async function resolveConversation(auth, target) {
27789
27885
  return person({ id: p.userId, name: p.name, handle: p.handle });
27790
27886
  }
27791
27887
  var HTTPS_RE = /^https:\/\//i;
27888
+ var URL_SHAPED_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
27792
27889
  var CHAT_IMAGE_MIME = new Set([
27793
27890
  "image/jpeg",
27794
27891
  "image/jpg",
@@ -27921,10 +28018,15 @@ function registerChatCommand(chat) {
27921
28018
  }
27922
28019
  }
27923
28020
  });
27924
- 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("--all", "Mention @all β€” notifies every member of the group (admins/owners always; others only when enabled via `amiko chat group mention-all`)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
28021
+ 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). The message is optional only with --gif.").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("--gif <queryOrUrl>", "Attach a GIF: search words (sends the top Klipy result) or a Klipy URL from `amiko chat gifs`").option("--all", "Mention @all β€” notifies every member of the group (admins/owners always; others only when enabled via `amiko chat group mention-all`)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
27925
28022
  const auth = authOrExit();
27926
- if (opts.image && opts.audio) {
27927
- console.error(error("Attach either --image or --audio, not both (one media block per message)."));
28023
+ const mediaFlagCount = [opts.image, opts.audio, opts.gif].filter(Boolean).length;
28024
+ if (mediaFlagCount > 1) {
28025
+ console.error(error("Attach only one of --image, --audio, or --gif (one media block per message)."));
28026
+ process.exit(1);
28027
+ }
28028
+ if (message === undefined && !opts.gif) {
28029
+ console.error(error("A message is required (it may only be omitted when sending a GIF with --gif)."));
27928
28030
  process.exit(1);
27929
28031
  }
27930
28032
  let resolved;
@@ -27935,9 +28037,10 @@ function registerChatCommand(chat) {
27935
28037
  process.exit(1);
27936
28038
  }
27937
28039
  const wantsAll = Boolean(opts.all);
27938
- let outgoing = message;
28040
+ const messageText = message ?? "";
28041
+ let outgoing = messageText;
27939
28042
  let mentionAllActive = false;
27940
- if (wantsAll || hasMentionAllLiteral(message) || hasMentionAllToken(message)) {
28043
+ if (wantsAll || hasMentionAllLiteral(messageText) || hasMentionAllToken(messageText)) {
27941
28044
  const plainTextNote = "Note: @all only notifies people in group chats β€” sending it as plain text.";
27942
28045
  let conv = null;
27943
28046
  if (resolved.kind === "conversation") {
@@ -27961,19 +28064,45 @@ function registerChatCommand(chat) {
27961
28064
  }
27962
28065
  console.log(dim('Note: you don\'t have @all permission in this group β€” "@all" goes out as plain text. An admin can enable it with `amiko chat group mention-all <group> on`.'));
27963
28066
  } else {
27964
- outgoing = rewriteMentionAllLiteral(message);
28067
+ outgoing = rewriteMentionAllLiteral(messageText);
27965
28068
  if (wantsAll)
27966
28069
  outgoing = applyMentionAll(outgoing);
27967
28070
  mentionAllActive = true;
27968
28071
  }
27969
28072
  }
27970
- const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
28073
+ let gifAttachment = null;
28074
+ if (opts.gif) {
28075
+ try {
28076
+ if (URL_SHAPED_RE.test(opts.gif)) {
28077
+ if (!HTTPS_RE.test(opts.gif) || !isKlipyMediaUrl(opts.gif)) {
28078
+ throw new Error("--gif accepts search words or an https Klipy URL (from `amiko chat gifs`). For other images use --image with a local file or an Amiko URL.");
28079
+ }
28080
+ gifAttachment = gifToMetadata({ url: opts.gif });
28081
+ } else {
28082
+ const results = await searchGifs(auth, opts.gif);
28083
+ const top = results.items?.[0];
28084
+ if (!top) {
28085
+ throw new Error(`No GIFs found for "${opts.gif}" β€” try different words or browse with \`amiko chat gifs\`.`);
28086
+ }
28087
+ gifAttachment = gifToMetadata({
28088
+ url: top.gifUrl,
28089
+ title: top.title,
28090
+ width: top.width,
28091
+ height: top.height
28092
+ });
28093
+ }
28094
+ } catch (e5) {
28095
+ console.error(error(e5 instanceof Error ? e5.message : "Could not resolve the GIF"));
28096
+ process.exit(1);
28097
+ }
28098
+ }
28099
+ const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : gifAttachment ? `gif "${gifAttachment.name.slice(0, 40)}"` : "";
27971
28100
  const shown = mentionMarkupToPlainText(outgoing);
27972
28101
  await requireApproval({
27973
28102
  free: "sends a message as you (the owner) β€” the recipients see it",
27974
28103
  summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}${mentionAllActive ? " [@all β€” notifies every member]" : ""}: ${shown.slice(0, 80)}${shown.length > 80 ? "…" : ""}`,
27975
28104
  yes: opts.yes,
27976
- commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}${opts.all ? " --all" : ""}`
28105
+ commandExample: `amiko chat send ${JSON.stringify(target)}${message !== undefined ? ` ${JSON.stringify(message)}` : ""}${opts.image ? ` --image ${JSON.stringify(opts.image)}` : ""}${opts.audio ? ` --audio ${JSON.stringify(opts.audio)}` : ""}${opts.gif ? ` --gif ${JSON.stringify(opts.gif)}` : ""}${opts.all ? " --all" : ""}`
27977
28106
  });
27978
28107
  let convId;
27979
28108
  try {
@@ -27990,9 +28119,22 @@ function registerChatCommand(chat) {
27990
28119
  });
27991
28120
  let data;
27992
28121
  try {
27993
- if (opts.image) {
28122
+ if (gifAttachment) {
28123
+ const isVideo = isKlipyVideoMediaUrl(gifAttachment.url);
28124
+ data = await post({
28125
+ content: outgoing || "GIF",
28126
+ message_type: isVideo ? "video" : "image",
28127
+ metadata: {
28128
+ [isVideo ? "videos" : "images"]: [gifAttachment],
28129
+ image_caption: outgoing || null
28130
+ }
28131
+ });
28132
+ } else if (opts.image) {
27994
28133
  const img = HTTPS_RE.test(opts.image) ? { url: opts.image, name: urlName(opts.image) } : await uploadChatImage(auth, convId, opts.image);
27995
- data = await post({ content: outgoing, metadata: { images: [img] } });
28134
+ data = await post({
28135
+ content: outgoing,
28136
+ metadata: { images: [img], image_caption: outgoing || null }
28137
+ });
27996
28138
  } else if (opts.audio) {
27997
28139
  if (HTTPS_RE.test(opts.audio)) {
27998
28140
  data = await post({
@@ -28015,7 +28157,7 @@ function registerChatCommand(chat) {
28015
28157
  console.log(JSON.stringify(data, null, 2));
28016
28158
  return;
28017
28159
  }
28018
- const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : "Message sent";
28160
+ const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : gifAttachment ? outgoing ? "Message + GIF sent" : "GIF sent" : "Message sent";
28019
28161
  console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
28020
28162
  });
28021
28163
  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) => {
@@ -28296,6 +28438,7 @@ function registerChatCommand(chat) {
28296
28438
  }
28297
28439
  }
28298
28440
  });
28441
+ registerChatGifsCommand(chat);
28299
28442
  const group = chat.command("group").description("Group chats β€” create, list, info, rename, add/remove members, @all permission, leave");
28300
28443
  registerChatGroupCommand(group);
28301
28444
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.14.0-beta.20",
3
+ "version": "0.14.0-beta.21",
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, including sharing a group's invite/join link + QR and @all group announcements, δΊΊε―ΉδΊΊ); 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 sending GIFs (`chat gifs` search + `chat send --gif`), sharing a group's invite/join link + QR and @all group announcements, δΊΊε―ΉδΊΊ); 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
  ---
@@ -35,6 +35,7 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
35
35
  | "publish that draft" | shell β†’ `amiko post drafts` (copy the id) β†’ `amiko post publish <postId>` |
36
36
  | "search memory for X" | shell β†’ `amiko memory search "X"` |
37
37
  | "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>"` |
38
+ | "send Sophie a happy-dance GIF" | shell β†’ `amiko chat send Sophie --gif "happy dance" --yes` (top result; to pick a specific one, browse `amiko chat gifs "happy dance"` first and pass its URL to `--gif`) |
38
39
  | "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` |
39
40
  | "did Sophie read my message about the demo?" | shell β†’ `amiko chat read "Sophie"` (find the owner's message, copy its `id`) β†’ `amiko chat receipts <messageId>` |
40
41
  | "pin that hackathon message in the builders group" | shell β†’ `amiko chat read "<group>"` (find the message, copy its `id`) β†’ `amiko chat pin <messageId> --yes` |
@@ -150,7 +151,8 @@ MiniMax Hailuo is silent β€” there is no CLI mux step to attach a separate music
150
151
  - `amiko chat list` β€” all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
151
152
  - `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.
152
153
  - `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`. **@all in groups:** add `--all` (or write a standalone `@all` word in the message) to notify **every member** β€” allowed for group admins/owners, or for everyone once an admin enables it (`amiko chat group mention-all <group> on`). The CLI pre-checks permission: `--all` without it fails and names the fix; a plain `@all` word still sends, as ordinary text, with a note. It pings the whole group β€” use it only when the owner clearly wants everyone notified.
153
- - **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.
154
+ - **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≀10MB or an Amiko URL β€” e.g. a generated image), `--audio <pathOrUrl>` (local ≀25MB or an Amiko URL β†’ sent as a voice note), and `--gif <queryOrUrl>` (see GIFs below). One media block per message (image XOR audio XOR gif). **General video files are 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.
155
+ - **GIFs** β€” `amiko chat send <target> --gif "<search words>"` sends the **top Klipy result** for those words (the message text is optional with `--gif`; when present it becomes the GIF's caption). To send a *specific* GIF, browse first with `amiko chat gifs "<search words>"` (trending when no query; `--page`/`--limit`/`--raw`; read-only and ungated) and pass the chosen result's URL to `--gif`. Only Klipy URLs are accepted there β€” any other image must go through `--image` as a local file or an Amiko URL (arbitrary external URLs aren't supported). Recipients on web/desktop/mobile see a looping GIF (with the KLIPY watermark), so this is the right tool when the owner asks to "send a GIF" β€” don't generate media with `amiko create` for that. The send itself stays `--yes`-gated like every `chat send`.
154
156
  - 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.
155
157
  - **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_*`.
156
158
  - **Read receipts** β€” `amiko chat receipts <messageId>` shows when the message was sent, who has read it (with each person's first-read time), and who it was delivered to but hasn't read yet. **Only works for messages the owner sent** (server-enforced) β€” a 403 means it wasn't the owner's message; report that, don't retry. Get the id from `amiko chat read <target>`, matching the message by its **content**. Read-only and ungated, like `reactions`. Rows marked "exact time unknown" were read before receipts tracking existed β€” never invent a time for them.