@heyamiko/amiko-cli 0.14.0-beta.21 → 0.14.0-beta.23
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 +30 -0
- package/dist/index.js +419 -92
- package/package.json +1 -1
- package/skills/SKILL.md +17 -5
package/README.md
CHANGED
|
@@ -249,6 +249,7 @@ Your conversations — **DMs and group chats** — acting **as you (the owner)**
|
|
|
249
249
|
```bash
|
|
250
250
|
amiko chat list # all conversations (DM + group): id, peer/title, last msg, unread
|
|
251
251
|
amiko chat list --limit 50 --archived # include archived
|
|
252
|
+
amiko chat list --mentions # only chats with an unread @mention of you or a reply to you
|
|
252
253
|
amiko chat read @sophie # recent messages with a user (by @handle)
|
|
253
254
|
amiko chat read <conversationId> --limit 40 # by conversation id (from `chat list`) — works for groups too
|
|
254
255
|
amiko chat send @sophie "on my way!" --yes # send as the owner
|
|
@@ -266,16 +267,19 @@ amiko chat receipts <messageId> # who read your message + when
|
|
|
266
267
|
amiko chat pin <messageId> --yes # pin for everyone (id from `chat read`; groups: admins only)
|
|
267
268
|
amiko chat unpin <messageId> --yes # remove a pin for everyone
|
|
268
269
|
amiko chat pinned "Trip planning" # list a conversation's pinned messages, oldest first
|
|
270
|
+
amiko chat mark-read --all --yes # mark EVERY conversation as read (clears all unread badges)
|
|
269
271
|
```
|
|
270
272
|
|
|
271
273
|
- **`<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".
|
|
272
274
|
- `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.
|
|
275
|
+
- **Unread mentions & replies**: rows whose unread messages **concern you directly** get an `@you` marker next to the unread count, and `chat list --mentions` filters to just those. The flag is server-computed over your unread messages and covers three cases: a direct **@mention** of you, a permitted **@all** in a group, and a **reply to one of your messages**. Composes with `--limit`/`--archived`/`--raw` (the raw payload is filtered too). Requires the amiko-web `has_unread_mention` deploy. On an older server the field is absent and reads as **false for every conversation**: plain `chat list` still shows unread counts, just without `@you`, and `chat list --mentions` returns no matches.
|
|
273
276
|
- **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
277
|
- **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.
|
|
275
278
|
- **@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`.
|
|
276
279
|
- `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.
|
|
277
280
|
- **`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.
|
|
278
281
|
- **Pinned messages**: `chat pin <messageId>` pins for the **whole conversation** (the chat sees an "X pinned a message" announcement) and `chat unpin <messageId>` removes it for everyone — both gated on `--yes`. In **groups** only admins can pin/unpin (403 otherwise); in DMs either side can. Max 20 pins per conversation. `chat pinned <target>` (same targets as `read`; never creates a DM) lists them oldest-first with each message's id and pinner — read-only, ungated.
|
|
282
|
+
- **Mark all read**: `chat mark-read --all` marks every conversation as read in one call — unread badges clear on all your devices, your chat/mention notifications clear, and senders see read ticks on their messages. Irreversible (there is no "mark unread"), so it's gated on `--yes` in non-interactive shells. `--all` is required (mirrors `notifications read --all`); `--raw` prints the server payload.
|
|
279
283
|
|
|
280
284
|
### Group chats
|
|
281
285
|
|
|
@@ -299,6 +303,23 @@ amiko chat group leave <groupIdOrTitle> --yes # alias: delete — see caveat
|
|
|
299
303
|
- **`mention-all <group> <on|off>`** controls who may @all in the group: `on` lets every member, `off` (the default) restricts it to admins. The current setting shows in `amiko chat group info` as `@all mentions: everyone | admins only`.
|
|
300
304
|
- `create`/`add` are outward social actions and `rename`/`remove`/`leave`/`promote`/`mention-all` are destructive, so all are **gated on `--yes`** in non-interactive shells. `--raw` prints JSON everywhere.
|
|
301
305
|
|
|
306
|
+
### Chat lists
|
|
307
|
+
|
|
308
|
+
Your **chat lists** — private folders of conversations (the same lists as the app sidebar). Only you ever see them; putting a chat in a list notifies nobody and changes nothing about the conversation itself.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
amiko chat lists # every list with its conversations (names resolved)
|
|
312
|
+
amiko chat lists create "Work" --conversation "Trip planning" --conversation @sophie --yes
|
|
313
|
+
amiko chat lists rename "Work" "Focus" --yes
|
|
314
|
+
amiko chat lists add "Focus" --conversation <conversationId> --yes
|
|
315
|
+
amiko chat lists remove "Focus" --conversation @sophie --yes
|
|
316
|
+
amiko chat lists delete "Focus" --yes # the conversations themselves are untouched
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- **`<list>`** is a list id or name (case-insensitive; exact beats substring; ambiguity errors listing candidates). **`--conversation`** is repeatable and takes the same targets as `chat send` — conversation id, user id, `@handle`, group title, or name — except a person must already have a DM with you: organizing lists **never creates conversations**.
|
|
320
|
+
- The server stores a list as a plain conversation-id array and `PATCH` replaces it wholesale, so `add`/`remove` read-modify-write the full array — existing entries always survive an `add`, and one unresolvable `--conversation` aborts the whole command before anything is written.
|
|
321
|
+
- Lists are private, but mutations still take the `--yes` gate in non-interactive shells (they reshape your chat sidebar on every device). The bare listing is an ungated read; `--raw` prints JSON everywhere.
|
|
322
|
+
|
|
302
323
|
## Twin Cards
|
|
303
324
|
|
|
304
325
|
Generate, view, download, and mint your **Twin Card** (template `twins_take`, flavors **work / play / love**).
|
|
@@ -601,6 +622,15 @@ npm publish
|
|
|
601
622
|
|
|
602
623
|
## Changelog
|
|
603
624
|
|
|
625
|
+
### 0.14.0-beta.23
|
|
626
|
+
|
|
627
|
+
- **Chat lists: `amiko chat lists`.** View and manage the owner's private conversation folders against amiko-web `/api/chat-lists`: bare `lists` renders every list with conversation names resolved, plus `create` / `rename` / `add` / `remove` / `delete` subcommands (all `--yes`-gated). The server stores a wholesale `conversation_ids` array, so `add`/`remove` read the current array, mutate locally, and PATCH it back — existing entries always survive an `add`. `--conversation` targets resolve like `chat send` (id / user id / `@handle` / group title / name) but map people to their **existing** DM only — list organization never creates a conversation.
|
|
628
|
+
- **Unread mentions in `chat list`: `@you` marker + `--mentions` filter.** Conversations whose unread messages contain a direct @mention of the owner, a permitted @all, or a **reply to one of the owner's messages** now render `(N unread, @you)`, and `chat list --mentions` keeps only those rows (the `--raw` payload is filtered too, and the flag is ignored on fully read conversations). Driven by the server-computed `has_unread_mention` field; requires the companion amiko-web deploy (`leandrogavidia/global-quick-fixes`) — on an older server the field is absent and reads as false for every conversation, so plain `chat list` still shows unread counts without `@you` and `--mentions` returns no matches. Also moves `resolveConversation`/`findDmByUserId` from `chat.ts` into `lib/conversations.ts` so `chat lists` can reuse them.
|
|
629
|
+
|
|
630
|
+
### 0.14.0-beta.22
|
|
631
|
+
|
|
632
|
+
- **Mark all conversations read: `amiko chat mark-read --all`.** Hits `POST /api/conversations/read-all` as the owner — bulk monotonic `last_read_at` update across every active membership, clears the owner's unread chat/mention notifications (count echoed as "N chat notifications cleared"), busts the per-user conversation cache, and broadcasts read ticks to peers in up to 50 most-recently-active unread conversations. Requires `--all` explicitly (mirrors `notifications read --all`) and sits behind the destructive `--yes` gate — irreversible, and senders see read ticks. `--raw` prints the server payload; success copy never counts conversations (the response's id list is capped at 50). Requires the companion amiko-web read-all deploy (`leandrogavidia/global-quick-fixes`).
|
|
633
|
+
|
|
604
634
|
### 0.14.0-beta.21
|
|
605
635
|
|
|
606
636
|
- **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.
|
package/dist/index.js
CHANGED
|
@@ -27195,6 +27195,93 @@ async function resolveGroup(auth, target) {
|
|
|
27195
27195
|
}
|
|
27196
27196
|
return fetchConversationById(auth, matches[0].id);
|
|
27197
27197
|
}
|
|
27198
|
+
async function fetchUserById(auth, id) {
|
|
27199
|
+
try {
|
|
27200
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
|
|
27201
|
+
return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
|
|
27202
|
+
} catch (e5) {
|
|
27203
|
+
if (e5.status === 404)
|
|
27204
|
+
return null;
|
|
27205
|
+
throw e5;
|
|
27206
|
+
}
|
|
27207
|
+
}
|
|
27208
|
+
async function findDmByUserId(auth, userId) {
|
|
27209
|
+
const want = normalizeUserId(userId);
|
|
27210
|
+
const limit = 100;
|
|
27211
|
+
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
27212
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
27213
|
+
query: { limit, offset },
|
|
27214
|
+
timeoutMs: 20000
|
|
27215
|
+
});
|
|
27216
|
+
const page = data.conversations ?? [];
|
|
27217
|
+
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
27218
|
+
if (dm)
|
|
27219
|
+
return dm.id;
|
|
27220
|
+
if (page.length < limit || data.pagination?.has_more === false)
|
|
27221
|
+
return null;
|
|
27222
|
+
}
|
|
27223
|
+
return null;
|
|
27224
|
+
}
|
|
27225
|
+
function personLabel(u) {
|
|
27226
|
+
const name = u.name ?? u.id;
|
|
27227
|
+
return u.handle ? `${name} (@${u.handle})` : name;
|
|
27228
|
+
}
|
|
27229
|
+
async function resolveConversation(auth, target) {
|
|
27230
|
+
const t = target.trim();
|
|
27231
|
+
if (!t) {
|
|
27232
|
+
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
27233
|
+
}
|
|
27234
|
+
const person = (u) => ({
|
|
27235
|
+
kind: "user",
|
|
27236
|
+
userId: u.id,
|
|
27237
|
+
label: `DM with ${personLabel(u)}`
|
|
27238
|
+
});
|
|
27239
|
+
if (t.startsWith("@")) {
|
|
27240
|
+
const handle = t.slice(1);
|
|
27241
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27242
|
+
timeoutMs: 15000
|
|
27243
|
+
});
|
|
27244
|
+
if (!prof.user?.id)
|
|
27245
|
+
throw new Error(`No user @${handle}.`);
|
|
27246
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
27247
|
+
}
|
|
27248
|
+
const idish = normalizeUserId(t);
|
|
27249
|
+
if (looksLikeConversationId(idish)) {
|
|
27250
|
+
if (idish === t) {
|
|
27251
|
+
try {
|
|
27252
|
+
const conv = await fetchConversationById(auth, t);
|
|
27253
|
+
return {
|
|
27254
|
+
kind: "conversation",
|
|
27255
|
+
id: conv.id,
|
|
27256
|
+
label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`,
|
|
27257
|
+
conversation: conv
|
|
27258
|
+
};
|
|
27259
|
+
} catch (e5) {
|
|
27260
|
+
const status = e5.status;
|
|
27261
|
+
if (status === 403) {
|
|
27262
|
+
throw new Error(`Conversation ${t} exists but you are not a participant.`);
|
|
27263
|
+
}
|
|
27264
|
+
if (status !== undefined && status !== 404)
|
|
27265
|
+
throw e5;
|
|
27266
|
+
}
|
|
27267
|
+
}
|
|
27268
|
+
const user = await fetchUserById(auth, idish);
|
|
27269
|
+
if (!user) {
|
|
27270
|
+
throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
|
|
27271
|
+
}
|
|
27272
|
+
return person(user);
|
|
27273
|
+
}
|
|
27274
|
+
const groupId = await findGroupIdByExactTitle(auth, t);
|
|
27275
|
+
if (groupId)
|
|
27276
|
+
return { kind: "conversation", id: groupId, label: `group "${t}"` };
|
|
27277
|
+
const { resolved, failures } = await resolveMembers(auth, [t]);
|
|
27278
|
+
if (failures.length || resolved.length !== 1) {
|
|
27279
|
+
const reason = failures[0]?.reason ?? "could not be resolved";
|
|
27280
|
+
throw new Error(`"${t}" ${reason}`);
|
|
27281
|
+
}
|
|
27282
|
+
const p = resolved[0];
|
|
27283
|
+
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
27284
|
+
}
|
|
27198
27285
|
|
|
27199
27286
|
// src/lib/mentions.ts
|
|
27200
27287
|
var MENTION_ALL_TOKEN = "@[all](all:all)";
|
|
@@ -27767,6 +27854,296 @@ Send one: amiko chat send <target> --gif <url>${d.hasNext ? ` (more: --page ${d
|
|
|
27767
27854
|
});
|
|
27768
27855
|
}
|
|
27769
27856
|
|
|
27857
|
+
// src/commands/chat-lists.ts
|
|
27858
|
+
var collect2 = (v, prev) => [...prev, v];
|
|
27859
|
+
function exitWithError2(e5, fallback2) {
|
|
27860
|
+
console.error(error(e5 instanceof Error ? e5.message : fallback2));
|
|
27861
|
+
process.exit(1);
|
|
27862
|
+
}
|
|
27863
|
+
function conversationFlags(tokens) {
|
|
27864
|
+
return tokens.map((t) => ` --conversation ${JSON.stringify(t)}`).join("");
|
|
27865
|
+
}
|
|
27866
|
+
function countNoun(n) {
|
|
27867
|
+
return `${n} conversation${n === 1 ? "" : "s"}`;
|
|
27868
|
+
}
|
|
27869
|
+
async function fetchChatLists(auth) {
|
|
27870
|
+
const data = await amikoWebFetch(auth, "/api/chat-lists", { timeoutMs: 20000 });
|
|
27871
|
+
return data.chatLists ?? [];
|
|
27872
|
+
}
|
|
27873
|
+
async function resolveList(auth, target) {
|
|
27874
|
+
const t = target.trim();
|
|
27875
|
+
if (!t) {
|
|
27876
|
+
throw new Error("No list given. Run `amiko chat lists` and pass a list id or name.");
|
|
27877
|
+
}
|
|
27878
|
+
const all = await fetchChatLists(auth);
|
|
27879
|
+
if (looksLikeConversationId(t)) {
|
|
27880
|
+
const byId = all.find((l) => l.id === t);
|
|
27881
|
+
if (byId)
|
|
27882
|
+
return byId;
|
|
27883
|
+
}
|
|
27884
|
+
const lower = t.toLowerCase();
|
|
27885
|
+
const exact = all.filter((l) => l.name.toLowerCase() === lower);
|
|
27886
|
+
const matches = exact.length ? exact : all.filter((l) => l.name.toLowerCase().includes(lower));
|
|
27887
|
+
if (matches.length === 0) {
|
|
27888
|
+
throw new Error(`No chat list matching "${t}". Run \`amiko chat lists\` to see them.`);
|
|
27889
|
+
}
|
|
27890
|
+
if (matches.length > 1) {
|
|
27891
|
+
const names = matches.map((l) => `"${l.name}" (${l.id})`).join(", ");
|
|
27892
|
+
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use the list's id.`);
|
|
27893
|
+
}
|
|
27894
|
+
return matches[0];
|
|
27895
|
+
}
|
|
27896
|
+
async function resolveListOrExit(auth, target) {
|
|
27897
|
+
try {
|
|
27898
|
+
return await resolveList(auth, target);
|
|
27899
|
+
} catch (e5) {
|
|
27900
|
+
exitWithError2(e5, "Could not resolve the chat list");
|
|
27901
|
+
}
|
|
27902
|
+
}
|
|
27903
|
+
async function resolveEntriesOrExit(auth, tokens) {
|
|
27904
|
+
if (tokens.length === 0) {
|
|
27905
|
+
console.error(error("At least one --conversation is required."));
|
|
27906
|
+
process.exit(1);
|
|
27907
|
+
}
|
|
27908
|
+
const entries = [];
|
|
27909
|
+
const failures = [];
|
|
27910
|
+
const seen = new Set;
|
|
27911
|
+
for (const raw of tokens) {
|
|
27912
|
+
try {
|
|
27913
|
+
const resolved = await resolveConversation(auth, raw);
|
|
27914
|
+
let entry;
|
|
27915
|
+
if (resolved.kind === "conversation") {
|
|
27916
|
+
entry = { id: resolved.id, label: resolved.label };
|
|
27917
|
+
} else {
|
|
27918
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
27919
|
+
if (!existing) {
|
|
27920
|
+
throw new Error(`no ${resolved.label} exists yet — a chat list can only hold existing conversations.`);
|
|
27921
|
+
}
|
|
27922
|
+
entry = { id: existing, label: resolved.label };
|
|
27923
|
+
}
|
|
27924
|
+
if (seen.has(entry.id))
|
|
27925
|
+
continue;
|
|
27926
|
+
seen.add(entry.id);
|
|
27927
|
+
entries.push(entry);
|
|
27928
|
+
} catch (e5) {
|
|
27929
|
+
failures.push({
|
|
27930
|
+
token: raw,
|
|
27931
|
+
reason: e5 instanceof Error ? e5.message : "could not be resolved"
|
|
27932
|
+
});
|
|
27933
|
+
}
|
|
27934
|
+
}
|
|
27935
|
+
if (failures.length > 0) {
|
|
27936
|
+
console.error(error("Could not resolve every conversation — nothing was changed."));
|
|
27937
|
+
for (const entry of entries) {
|
|
27938
|
+
console.error(dim(` resolved: ${entry.label} → ${entry.id}`));
|
|
27939
|
+
}
|
|
27940
|
+
for (const f of failures) {
|
|
27941
|
+
console.error(error(` ${JSON.stringify(f.token)}: ${f.reason}`));
|
|
27942
|
+
}
|
|
27943
|
+
process.exit(1);
|
|
27944
|
+
}
|
|
27945
|
+
return entries;
|
|
27946
|
+
}
|
|
27947
|
+
async function conversationLabels(auth) {
|
|
27948
|
+
const labels = new Map;
|
|
27949
|
+
try {
|
|
27950
|
+
const data = await amikoWebFetch(auth, "/api/conversations", { query: { limit: 100, archived: "all" }, timeoutMs: 20000 });
|
|
27951
|
+
for (const c of data.conversations ?? []) {
|
|
27952
|
+
labels.set(c.id, convLabel(c, auth.userId));
|
|
27953
|
+
}
|
|
27954
|
+
} catch {}
|
|
27955
|
+
return labels;
|
|
27956
|
+
}
|
|
27957
|
+
function listNotFound(e5) {
|
|
27958
|
+
return e5.status === 404;
|
|
27959
|
+
}
|
|
27960
|
+
function registerChatListsCommand(lists) {
|
|
27961
|
+
lists.option("--raw", "Output raw JSON").action(async (opts) => {
|
|
27962
|
+
const auth = authOrExit();
|
|
27963
|
+
let all;
|
|
27964
|
+
try {
|
|
27965
|
+
all = await fetchChatLists(auth);
|
|
27966
|
+
} catch (e5) {
|
|
27967
|
+
exitWithError2(e5, "Failed to load chat lists");
|
|
27968
|
+
}
|
|
27969
|
+
if (opts.raw) {
|
|
27970
|
+
console.log(JSON.stringify({ chatLists: all }, null, 2));
|
|
27971
|
+
return;
|
|
27972
|
+
}
|
|
27973
|
+
if (all.length === 0) {
|
|
27974
|
+
console.log(dim("No chat lists."));
|
|
27975
|
+
console.log(dim("Create one with `amiko chat lists create <name> --conversation <who>`."));
|
|
27976
|
+
return;
|
|
27977
|
+
}
|
|
27978
|
+
const labels = await conversationLabels(auth);
|
|
27979
|
+
console.log(heading(`Chat lists (${all.length})
|
|
27980
|
+
`));
|
|
27981
|
+
for (const l of all) {
|
|
27982
|
+
const ids = l.conversation_ids ?? [];
|
|
27983
|
+
console.log(label(l.name, dim(`${l.id} · ${countNoun(ids.length)}`)));
|
|
27984
|
+
for (const id of ids) {
|
|
27985
|
+
const name = labels.get(id);
|
|
27986
|
+
console.log(dim(name ? ` - ${name} (${id})` : ` - ${id}`));
|
|
27987
|
+
}
|
|
27988
|
+
}
|
|
27989
|
+
});
|
|
27990
|
+
lists.command("create <name>").description("Create a chat list, optionally with initial conversations").option("--conversation <target>", "Conversation to include (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (name, opts) => {
|
|
27991
|
+
const auth = authOrExit();
|
|
27992
|
+
const entries = opts.conversation.length > 0 ? await resolveEntriesOrExit(auth, opts.conversation) : [];
|
|
27993
|
+
await confirmDestructive({
|
|
27994
|
+
action: `Create chat list "${name}"${entries.length ? ` with ${entries.map((e5) => e5.label).join(", ")}` : " (empty)"}`,
|
|
27995
|
+
detail: "Chat lists are private folders — only you see them, in every Amiko app.",
|
|
27996
|
+
yes: opts.yes,
|
|
27997
|
+
commandExample: `amiko chat lists create ${JSON.stringify(name)}${conversationFlags(opts.conversation)}`
|
|
27998
|
+
});
|
|
27999
|
+
let data;
|
|
28000
|
+
try {
|
|
28001
|
+
data = await amikoWebFetch(auth, "/api/chat-lists", {
|
|
28002
|
+
method: "POST",
|
|
28003
|
+
body: { name, conversation_ids: entries.map((e5) => e5.id) },
|
|
28004
|
+
timeoutMs: 20000
|
|
28005
|
+
});
|
|
28006
|
+
} catch (e5) {
|
|
28007
|
+
exitWithError2(e5, "Failed to create the chat list");
|
|
28008
|
+
}
|
|
28009
|
+
if (opts.raw) {
|
|
28010
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28011
|
+
return;
|
|
28012
|
+
}
|
|
28013
|
+
console.log(success(`Chat list created: ${name}`));
|
|
28014
|
+
if (data.chatList?.id)
|
|
28015
|
+
console.log(label("ID", data.chatList.id));
|
|
28016
|
+
if (entries.length) {
|
|
28017
|
+
console.log(label("Conversations", entries.map((e5) => e5.label).join(", ")));
|
|
28018
|
+
}
|
|
28019
|
+
});
|
|
28020
|
+
lists.command("rename <list> <newName>").description("Rename a chat list (list id or name)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, newName, opts) => {
|
|
28021
|
+
const auth = authOrExit();
|
|
28022
|
+
const list = await resolveListOrExit(auth, target);
|
|
28023
|
+
await confirmDestructive({
|
|
28024
|
+
action: `Rename chat list "${list.name}" to "${newName}"`,
|
|
28025
|
+
yes: opts.yes,
|
|
28026
|
+
commandExample: `amiko chat lists rename ${JSON.stringify(target)} ${JSON.stringify(newName)}`
|
|
28027
|
+
});
|
|
28028
|
+
let data;
|
|
28029
|
+
try {
|
|
28030
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, { method: "PATCH", body: { name: newName }, timeoutMs: 20000 });
|
|
28031
|
+
} catch (e5) {
|
|
28032
|
+
exitWithError2(e5, "Rename failed");
|
|
28033
|
+
}
|
|
28034
|
+
if (opts.raw) {
|
|
28035
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28036
|
+
return;
|
|
28037
|
+
}
|
|
28038
|
+
console.log(success(`Chat list renamed to: ${newName}`) + dim(` (${list.id})`));
|
|
28039
|
+
});
|
|
28040
|
+
lists.command("add <list>").description("Add conversations to a chat list (list id or name)").option("--conversation <target>", "Conversation to add (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
28041
|
+
const auth = authOrExit();
|
|
28042
|
+
const entries = await resolveEntriesOrExit(auth, opts.conversation);
|
|
28043
|
+
const list = await resolveListOrExit(auth, target);
|
|
28044
|
+
const current = list.conversation_ids ?? [];
|
|
28045
|
+
const currentSet = new Set(current);
|
|
28046
|
+
const fresh = entries.filter((e5) => !currentSet.has(e5.id));
|
|
28047
|
+
const already = entries.filter((e5) => currentSet.has(e5.id));
|
|
28048
|
+
if (fresh.length === 0) {
|
|
28049
|
+
console.log(dim(`Already in "${list.name}" — nothing to add.`));
|
|
28050
|
+
return;
|
|
28051
|
+
}
|
|
28052
|
+
await confirmDestructive({
|
|
28053
|
+
action: `Add ${fresh.map((e5) => e5.label).join(", ")} to chat list "${list.name}"`,
|
|
28054
|
+
yes: opts.yes,
|
|
28055
|
+
commandExample: `amiko chat lists add ${JSON.stringify(target)}${conversationFlags(opts.conversation)}`
|
|
28056
|
+
});
|
|
28057
|
+
let data;
|
|
28058
|
+
try {
|
|
28059
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, {
|
|
28060
|
+
method: "PATCH",
|
|
28061
|
+
body: {
|
|
28062
|
+
conversation_ids: [...current, ...fresh.map((e5) => e5.id)]
|
|
28063
|
+
},
|
|
28064
|
+
timeoutMs: 20000
|
|
28065
|
+
});
|
|
28066
|
+
} catch (e5) {
|
|
28067
|
+
if (listNotFound(e5)) {
|
|
28068
|
+
exitWithError2(null, `Chat list "${list.name}" no longer exists.`);
|
|
28069
|
+
}
|
|
28070
|
+
exitWithError2(e5, "Failed to update the chat list");
|
|
28071
|
+
}
|
|
28072
|
+
if (opts.raw) {
|
|
28073
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28074
|
+
return;
|
|
28075
|
+
}
|
|
28076
|
+
console.log(success(`Added ${fresh.map((e5) => e5.label).join(", ")} to "${list.name}"`) + dim(` (now ${countNoun(current.length + fresh.length)})`));
|
|
28077
|
+
for (const e5 of already) {
|
|
28078
|
+
console.log(dim(` ${e5.label} was already in the list.`));
|
|
28079
|
+
}
|
|
28080
|
+
});
|
|
28081
|
+
lists.command("remove <list>").description("Remove conversations from a chat list (the conversations themselves are untouched)").option("--conversation <target>", "Conversation to remove (conversation id, user id, @handle, group title, or name); repeat per conversation", collect2, []).option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
28082
|
+
const auth = authOrExit();
|
|
28083
|
+
const entries = await resolveEntriesOrExit(auth, opts.conversation);
|
|
28084
|
+
const list = await resolveListOrExit(auth, target);
|
|
28085
|
+
const current = list.conversation_ids ?? [];
|
|
28086
|
+
const currentSet = new Set(current);
|
|
28087
|
+
const inList = entries.filter((e5) => currentSet.has(e5.id));
|
|
28088
|
+
const notInList = entries.filter((e5) => !currentSet.has(e5.id));
|
|
28089
|
+
if (inList.length === 0) {
|
|
28090
|
+
console.log(dim(`None of those are in "${list.name}".`));
|
|
28091
|
+
return;
|
|
28092
|
+
}
|
|
28093
|
+
await confirmDestructive({
|
|
28094
|
+
action: `Remove ${inList.map((e5) => e5.label).join(", ")} from chat list "${list.name}"`,
|
|
28095
|
+
yes: opts.yes,
|
|
28096
|
+
commandExample: `amiko chat lists remove ${JSON.stringify(target)}${conversationFlags(opts.conversation)}`
|
|
28097
|
+
});
|
|
28098
|
+
const removeIds = new Set(inList.map((e5) => e5.id));
|
|
28099
|
+
let data;
|
|
28100
|
+
try {
|
|
28101
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, {
|
|
28102
|
+
method: "PATCH",
|
|
28103
|
+
body: {
|
|
28104
|
+
conversation_ids: current.filter((id) => !removeIds.has(id))
|
|
28105
|
+
},
|
|
28106
|
+
timeoutMs: 20000
|
|
28107
|
+
});
|
|
28108
|
+
} catch (e5) {
|
|
28109
|
+
if (listNotFound(e5)) {
|
|
28110
|
+
exitWithError2(null, `Chat list "${list.name}" no longer exists.`);
|
|
28111
|
+
}
|
|
28112
|
+
exitWithError2(e5, "Failed to update the chat list");
|
|
28113
|
+
}
|
|
28114
|
+
if (opts.raw) {
|
|
28115
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28116
|
+
return;
|
|
28117
|
+
}
|
|
28118
|
+
console.log(success(`Removed ${inList.map((e5) => e5.label).join(", ")} from "${list.name}"`) + dim(` (now ${countNoun(current.length - inList.length)})`));
|
|
28119
|
+
for (const e5 of notInList) {
|
|
28120
|
+
console.log(dim(` ${e5.label} was not in the list.`));
|
|
28121
|
+
}
|
|
28122
|
+
});
|
|
28123
|
+
lists.command("delete <list>").description("Delete a chat list (the conversations in it are untouched)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
28124
|
+
const auth = authOrExit();
|
|
28125
|
+
const list = await resolveListOrExit(auth, target);
|
|
28126
|
+
const count = (list.conversation_ids ?? []).length;
|
|
28127
|
+
await confirmDestructive({
|
|
28128
|
+
action: `Delete chat list "${list.name}" (${countNoun(count)})`,
|
|
28129
|
+
detail: "The list's name and membership are gone for good; the conversations themselves are not affected.",
|
|
28130
|
+
yes: opts.yes,
|
|
28131
|
+
commandExample: `amiko chat lists delete ${JSON.stringify(target)}`
|
|
28132
|
+
});
|
|
28133
|
+
let data;
|
|
28134
|
+
try {
|
|
28135
|
+
data = await amikoWebFetch(auth, `/api/chat-lists/${encodeURIComponent(list.id)}`, { method: "DELETE", timeoutMs: 20000 });
|
|
28136
|
+
} catch (e5) {
|
|
28137
|
+
exitWithError2(e5, "Failed to delete the chat list");
|
|
28138
|
+
}
|
|
28139
|
+
if (opts.raw) {
|
|
28140
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28141
|
+
return;
|
|
28142
|
+
}
|
|
28143
|
+
console.log(success(`Chat list deleted: ${list.name}`));
|
|
28144
|
+
});
|
|
28145
|
+
}
|
|
28146
|
+
|
|
27770
28147
|
// src/commands/chat.ts
|
|
27771
28148
|
var PIN_LIMIT = 20;
|
|
27772
28149
|
function pinErrorMessage(e5, fallback2) {
|
|
@@ -27797,93 +28174,6 @@ async function findOrCreateDm(auth, userId) {
|
|
|
27797
28174
|
throw new Error("Could not open a conversation with that user.");
|
|
27798
28175
|
return id;
|
|
27799
28176
|
}
|
|
27800
|
-
async function fetchUserById(auth, id) {
|
|
27801
|
-
try {
|
|
27802
|
-
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
|
|
27803
|
-
return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
|
|
27804
|
-
} catch (e5) {
|
|
27805
|
-
if (e5.status === 404)
|
|
27806
|
-
return null;
|
|
27807
|
-
throw e5;
|
|
27808
|
-
}
|
|
27809
|
-
}
|
|
27810
|
-
async function findDmByUserId(auth, userId) {
|
|
27811
|
-
const want = normalizeUserId(userId);
|
|
27812
|
-
const limit = 100;
|
|
27813
|
-
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
27814
|
-
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
27815
|
-
query: { limit, offset },
|
|
27816
|
-
timeoutMs: 20000
|
|
27817
|
-
});
|
|
27818
|
-
const page = data.conversations ?? [];
|
|
27819
|
-
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
27820
|
-
if (dm)
|
|
27821
|
-
return dm.id;
|
|
27822
|
-
if (page.length < limit || data.pagination?.has_more === false)
|
|
27823
|
-
return null;
|
|
27824
|
-
}
|
|
27825
|
-
return null;
|
|
27826
|
-
}
|
|
27827
|
-
function personLabel(u) {
|
|
27828
|
-
const name = u.name ?? u.id;
|
|
27829
|
-
return u.handle ? `${name} (@${u.handle})` : name;
|
|
27830
|
-
}
|
|
27831
|
-
async function resolveConversation(auth, target) {
|
|
27832
|
-
const t = target.trim();
|
|
27833
|
-
if (!t) {
|
|
27834
|
-
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
27835
|
-
}
|
|
27836
|
-
const person = (u) => ({
|
|
27837
|
-
kind: "user",
|
|
27838
|
-
userId: u.id,
|
|
27839
|
-
label: `DM with ${personLabel(u)}`
|
|
27840
|
-
});
|
|
27841
|
-
if (t.startsWith("@")) {
|
|
27842
|
-
const handle = t.slice(1);
|
|
27843
|
-
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27844
|
-
timeoutMs: 15000
|
|
27845
|
-
});
|
|
27846
|
-
if (!prof.user?.id)
|
|
27847
|
-
throw new Error(`No user @${handle}.`);
|
|
27848
|
-
return person({ ...prof.user, id: prof.user.id });
|
|
27849
|
-
}
|
|
27850
|
-
const idish = normalizeUserId(t);
|
|
27851
|
-
if (looksLikeConversationId(idish)) {
|
|
27852
|
-
if (idish === t) {
|
|
27853
|
-
try {
|
|
27854
|
-
const conv = await fetchConversationById(auth, t);
|
|
27855
|
-
return {
|
|
27856
|
-
kind: "conversation",
|
|
27857
|
-
id: conv.id,
|
|
27858
|
-
label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`,
|
|
27859
|
-
conversation: conv
|
|
27860
|
-
};
|
|
27861
|
-
} catch (e5) {
|
|
27862
|
-
const status = e5.status;
|
|
27863
|
-
if (status === 403) {
|
|
27864
|
-
throw new Error(`Conversation ${t} exists but you are not a participant.`);
|
|
27865
|
-
}
|
|
27866
|
-
if (status !== 404)
|
|
27867
|
-
throw e5;
|
|
27868
|
-
}
|
|
27869
|
-
}
|
|
27870
|
-
const user = await fetchUserById(auth, idish);
|
|
27871
|
-
if (!user) {
|
|
27872
|
-
throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
|
|
27873
|
-
}
|
|
27874
|
-
return person(user);
|
|
27875
|
-
}
|
|
27876
|
-
const groupId = await findGroupIdByExactTitle(auth, t);
|
|
27877
|
-
if (groupId)
|
|
27878
|
-
return { kind: "conversation", id: groupId, label: `group "${t}"` };
|
|
27879
|
-
const { resolved, failures } = await resolveMembers(auth, [t]);
|
|
27880
|
-
if (failures.length || resolved.length !== 1) {
|
|
27881
|
-
const reason = failures[0]?.reason ?? "could not be resolved";
|
|
27882
|
-
throw new Error(`"${t}" ${reason}`);
|
|
27883
|
-
}
|
|
27884
|
-
const p = resolved[0];
|
|
27885
|
-
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
27886
|
-
}
|
|
27887
28177
|
var HTTPS_RE = /^https:\/\//i;
|
|
27888
28178
|
var URL_SHAPED_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
27889
28179
|
var CHAT_IMAGE_MIME = new Set([
|
|
@@ -27932,7 +28222,7 @@ async function uploadChatVoice(auth, convId, path2) {
|
|
|
27932
28222
|
await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/audio/upload`, { method: "POST", multipart: form, timeoutMs: 120000 });
|
|
27933
28223
|
}
|
|
27934
28224
|
function registerChatCommand(chat) {
|
|
27935
|
-
chat.command("list").description("List your conversations (DMs and group chats)").option("--limit <n>", "Max conversations", "30").option("--archived", "Include archived conversations").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
28225
|
+
chat.command("list").description("List your conversations (DMs and group chats)").option("--limit <n>", "Max conversations", "30").option("--archived", "Include archived conversations").option("--mentions", "Only conversations with an unread @mention of you, a permitted @all, or a reply to one of your messages").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
27936
28226
|
const auth = authOrExit();
|
|
27937
28227
|
let data;
|
|
27938
28228
|
try {
|
|
@@ -27947,20 +28237,21 @@ function registerChatCommand(chat) {
|
|
|
27947
28237
|
console.error(error(e5 instanceof Error ? e5.message : "Failed to list conversations"));
|
|
27948
28238
|
process.exit(1);
|
|
27949
28239
|
}
|
|
28240
|
+
const mentionsMe = (c) => !!c.unread_count && !!c.has_unread_mention;
|
|
28241
|
+
const convs = opts.mentions ? (data.conversations ?? []).filter(mentionsMe) : data.conversations ?? [];
|
|
27950
28242
|
if (opts.raw) {
|
|
27951
|
-
console.log(JSON.stringify(data, null, 2));
|
|
28243
|
+
console.log(JSON.stringify(opts.mentions ? { ...data, conversations: convs } : data, null, 2));
|
|
27952
28244
|
return;
|
|
27953
28245
|
}
|
|
27954
|
-
const convs = data.conversations ?? [];
|
|
27955
28246
|
if (convs.length === 0) {
|
|
27956
|
-
console.log(dim("No conversations."));
|
|
28247
|
+
console.log(dim(opts.mentions ? "No conversations with unread mentions or replies to you." : "No conversations."));
|
|
27957
28248
|
return;
|
|
27958
28249
|
}
|
|
27959
28250
|
console.log(heading(`Conversations (${convs.length})
|
|
27960
28251
|
`));
|
|
27961
28252
|
for (const c of convs) {
|
|
27962
28253
|
const kind = c.conversation_type === "direct" ? "DM" : c.conversation_type;
|
|
27963
|
-
const unread = c.unread_count ? ` (${c.unread_count} unread)` : "";
|
|
28254
|
+
const unread = c.unread_count ? ` (${c.unread_count} unread${mentionsMe(c) ? ", @you" : ""})` : "";
|
|
27964
28255
|
console.log(label(`[${kind}] ${convLabel(c, auth.userId)}`, dim(c.id)));
|
|
27965
28256
|
if (c.last_message?.content) {
|
|
27966
28257
|
const who = c.last_message.name ? `${c.last_message.name}: ` : "";
|
|
@@ -28438,9 +28729,45 @@ function registerChatCommand(chat) {
|
|
|
28438
28729
|
}
|
|
28439
28730
|
}
|
|
28440
28731
|
});
|
|
28732
|
+
chat.command("mark-read").description("Mark ALL conversations as read (requires --all). Clears every unread badge on the owner's devices; senders see read ticks.").option("--all", "Mark every conversation as read").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (opts) => {
|
|
28733
|
+
if (!opts.all) {
|
|
28734
|
+
console.error(error("Pass --all to mark every conversation as read"));
|
|
28735
|
+
process.exit(1);
|
|
28736
|
+
}
|
|
28737
|
+
const auth = authOrExit();
|
|
28738
|
+
await confirmDestructive({
|
|
28739
|
+
action: "Mark ALL of the owner's conversations as read",
|
|
28740
|
+
detail: "Clears every unread badge on web and mobile; senders see read ticks on their messages. This cannot be undone.",
|
|
28741
|
+
yes: opts.yes,
|
|
28742
|
+
commandExample: "amiko chat mark-read --all"
|
|
28743
|
+
});
|
|
28744
|
+
let data;
|
|
28745
|
+
try {
|
|
28746
|
+
data = await amikoWebFetch(auth, "/api/conversations/read-all", {
|
|
28747
|
+
method: "POST",
|
|
28748
|
+
timeoutMs: 20000
|
|
28749
|
+
});
|
|
28750
|
+
} catch (e5) {
|
|
28751
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to mark all as read"));
|
|
28752
|
+
process.exit(1);
|
|
28753
|
+
}
|
|
28754
|
+
if (opts.raw) {
|
|
28755
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28756
|
+
return;
|
|
28757
|
+
}
|
|
28758
|
+
console.log(success("All conversations marked as read."));
|
|
28759
|
+
const cleared = data.cleared_notifications;
|
|
28760
|
+
if (typeof cleared === "number" && cleared > 0) {
|
|
28761
|
+
console.log(dim(` ${cleared} chat notification${cleared === 1 ? "" : "s"} cleared`));
|
|
28762
|
+
} else if ((data.unread_conversation_ids ?? []).length === 0) {
|
|
28763
|
+
console.log(dim(" Nothing was unread."));
|
|
28764
|
+
}
|
|
28765
|
+
});
|
|
28441
28766
|
registerChatGifsCommand(chat);
|
|
28442
28767
|
const group = chat.command("group").description("Group chats — create, list, info, rename, add/remove members, @all permission, leave");
|
|
28443
28768
|
registerChatGroupCommand(group);
|
|
28769
|
+
const lists = chat.command("lists").description("Your chat lists (private folders of conversations) — run bare to see them; create, rename, add/remove conversations, delete");
|
|
28770
|
+
registerChatListsCommand(lists);
|
|
28444
28771
|
}
|
|
28445
28772
|
|
|
28446
28773
|
// src/commands/card.ts
|
package/package.json
CHANGED
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 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.
|
|
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 / mark everything read / see which chats have unread @mentions or replies to the owner (`chat list --mentions`) / organize chats into private folders (`chat lists`) / 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
|
---
|
|
@@ -40,6 +40,10 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
|
|
|
40
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>` |
|
|
41
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` |
|
|
42
42
|
| "what's pinned in the team chat?" | shell → `amiko chat pinned "<group>"` |
|
|
43
|
+
| "mark all my chats as read" | shell → `amiko chat mark-read --all --yes` (after the owner confirms — it clears every badge and senders see read ticks) |
|
|
44
|
+
| "any unread mentions or replies to me?" | shell → `amiko chat list --mentions` (chats whose unread messages @mention the owner or reply to them) |
|
|
45
|
+
| "what chat lists do I have? what's in my Work list?" | shell → `amiko chat lists` |
|
|
46
|
+
| "add the team group to my Work list" | shell → `amiko chat lists add "Work" --conversation "<group>" --yes` |
|
|
43
47
|
| "what can amiko do?" | shell → `amiko --help` |
|
|
44
48
|
| "what's my MiniMax / ElevenLabs voice id?" | shell → `amiko info` |
|
|
45
49
|
| "speak with my cloned MiniMax voice" | shell → `amiko info`, quote the cost, obtain explicit approval, then run `amiko create tts "…" --provider minimax --voice <minimax_voice_id> --yes` |
|
|
@@ -59,7 +63,7 @@ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `
|
|
|
59
63
|
|
|
60
64
|
## Critical Rules
|
|
61
65
|
|
|
62
|
-
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 pin` (the whole chat sees a pinned-message announcement), `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 unpin` (removes the pin for everyone), `chat group remove/leave/rename/promote/mention-all`, `twin update --public`, `drive delete`, `drive share` / `drive folder share` (exposes the file — or the folder's ENTIRE subtree — to anyone with the link), `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?").
|
|
66
|
+
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 pin` (the whole chat sees a pinned-message announcement), `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 unpin` (removes the pin for everyone), `chat mark-read --all` (irreversibly marks every conversation read — senders see read ticks), `chat group remove/leave/rename/promote/mention-all`, `chat lists create/rename/add/remove/delete` (private to the owner, but they reshape the owner's chat UI), `twin update --public`, `drive delete`, `drive share` / `drive folder share` (exposes the file — or the folder's ENTIRE subtree — to anyone with the link), `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?").
|
|
63
67
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
|
|
64
68
|
3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
|
|
65
69
|
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`.
|
|
@@ -148,7 +152,7 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
|
|
|
148
152
|
|
|
149
153
|
`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).
|
|
150
154
|
|
|
151
|
-
- `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
|
|
155
|
+
- `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread). Rows with unread messages that concern the owner directly show an **`@you` marker**, and `--mentions` filters to just those. `@you` / `--mentions` means the unread messages include a **direct @mention of the owner, a permitted @all in a group, or a reply to one of the owner's messages** — so "did anyone reply to me?" is also answered here, not just literal mentions. No `@you` rows under `--mentions` = nothing unread needs the owner's attention specifically. (On servers that predate the flag it reads as false for every conversation: unread counts still show, `@you` never does, and `--mentions` matches nothing — an empty result there is NOT proof nobody mentioned or replied.)
|
|
152
156
|
- `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.
|
|
153
157
|
- `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.
|
|
154
158
|
- **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.
|
|
@@ -157,6 +161,7 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
|
|
|
157
161
|
- **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_*`.
|
|
158
162
|
- **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.
|
|
159
163
|
- **Pinned messages** — `amiko chat pin <messageId>`, `amiko chat unpin <messageId>`, `amiko chat pinned <target>`. Pins are **conversation-wide**: everyone sees them, and `pin` posts an "X pinned a message" announcement to the whole chat, so both `pin` and `unpin` are `--yes`-gated with a preview of the target message. The owner won't give you a message id — get it from `amiko chat read <target>` (or `amiko chat pinned <target>` when unpinning), matching by **content**. In **groups** only admins can pin/unpin (a 403 names that rule; check roles with `amiko chat group info`, report it, don't retry); in DMs either side can. Max 20 pins per conversation — a "Pin limit reached" error means unpin one first, ask the owner which. Re-pinning an already-pinned message is a harmless no-op. `pinned <target>` is an ungated read (same targets as `chat read`: conversation id, user id, `@handle`, or name; it never creates a DM) listing pins oldest-first with each message's `id` and who pinned it.
|
|
164
|
+
- **Mark all read** — `amiko chat mark-read --all` marks EVERY conversation read as the owner in one shot: unread badges clear on all the owner's devices, the owner's chat/mention notifications clear, and senders see read ticks on their messages (the ~50 most recent conversations flip live; the rest on their next refresh). Irreversible — there is no "mark unread" — so it's `--yes`-gated: only run it when the owner explicitly asks to clear everything, never as routine tidying. The success line won't say how many conversations were affected (the server doesn't report a full count); don't invent a number.
|
|
160
165
|
|
|
161
166
|
### Group chats — `amiko chat group`
|
|
162
167
|
|
|
@@ -171,7 +176,14 @@ Create and manage the owner's group chats: `create <name> --member <who>…`, `l
|
|
|
171
176
|
- `amiko chat group mention-all <group> <on|off>` — allow **everyone** in the group to use @all (`on`) or restrict it to admins (`off`, the default). **Admins only**, server-enforced — a 403 means the owner isn't an admin; report it, don't retry. The current setting shows in `amiko chat group info` (`@all mentions: everyone | admins only`).
|
|
172
177
|
- Gating: `create`/`add` message real people and `remove`/`leave`/`rename`/`promote`/`mention-all` are destructive — all require `--yes` in your shell (see Critical Rules). Reads (`list`, `info`, `invite`) are free and ungated.
|
|
173
178
|
|
|
174
|
-
|
|
179
|
+
### Chat lists — `amiko chat lists`
|
|
180
|
+
|
|
181
|
+
The owner's **chat lists**: private folders of conversations (the sidebar folders in the Amiko apps). **Only the owner ever sees them** — putting a chat in a list notifies nobody and changes nothing about the conversation itself.
|
|
182
|
+
|
|
183
|
+
- `amiko chat lists` (bare) — every list with its conversations, names resolved. Ungated read; `--raw` for JSON.
|
|
184
|
+
- `amiko chat lists create <name> [--conversation <who>…]`, `rename <list> <newName>`, `add <list> --conversation <who>…`, `remove <list> --conversation <who>…`, `delete <list>` — all mutations are `--yes`-gated. They're private, so confirm the action itself; never mention cost or "other people will see this." Every mutation also accepts `--raw` to print the server's JSON response instead of the summary line.
|
|
185
|
+
- `<list>` is a list id or name (case-insensitive, exact wins before substring; ambiguity errors listing candidates). `--conversation` repeats and takes a conversation id, user id, `@handle`, group title, or name — the same targets as `chat send`, except a person must **already have a DM** with the owner (organizing lists never creates conversations; the CLI errors if no DM exists yet).
|
|
186
|
+
- `add`/`remove` only change list membership. `delete` removes the list itself; the conversations in it are untouched — say so if the owner hesitates.
|
|
175
187
|
|
|
176
188
|
`amiko card` = the owner's **Twin Card** (shareable card, template `twins_take`, flavors **work / play / love** — DB-managed, discover with `card templates`).
|
|
177
189
|
|
|
@@ -225,7 +237,7 @@ Workflow: start with `friends matches --limit 20 --json`; narrow with `--dimensi
|
|
|
225
237
|
>
|
|
226
238
|
> **Owner DMs / group chats: `amiko chat`.** For "did X message me?" / "what did Sophie and I say?" / "message Y for me", use `amiko chat list` / `amiko chat read <who>` / `amiko chat send <who> "…"` — these act **as the owner** (人对人). The gateway's `session_*` tools are the **agent's own** sessions (as the agent), a separate surface (see Chat behavior notes).
|
|
227
239
|
|
|
228
|
-
Platform notifications cover friend requests, mentions, system alerts, and post-related events.
|
|
240
|
+
Platform notifications cover friend requests, mentions, system alerts, and post-related events. For "**was I mentioned / did anyone reply to me** in chat?" specifically, prefer `amiko chat list --mentions` — it covers replies to the owner's messages (which notification rows don't) and reflects what's still unread.
|
|
229
241
|
|
|
230
242
|
## Where to run
|
|
231
243
|
|