@heyamiko/amiko-cli 0.14.0-beta.18 β 0.14.0-beta.19
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 +8 -0
- package/dist/index.js +125 -0
- package/package.json +1 -1
- package/skills/SKILL.md +4 -1
package/README.md
CHANGED
|
@@ -260,6 +260,9 @@ amiko chat send "Trip planning" "standup in 5" --all --yes # same, via the ex
|
|
|
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
262
|
amiko chat receipts <messageId> # who read your message + when (id from `chat read`)
|
|
263
|
+
amiko chat pin <messageId> --yes # pin for everyone (id from `chat read`; groups: admins only)
|
|
264
|
+
amiko chat unpin <messageId> --yes # remove a pin for everyone
|
|
265
|
+
amiko chat pinned "Trip planning" # list a conversation's pinned messages, oldest first
|
|
263
266
|
```
|
|
264
267
|
|
|
265
268
|
- **`<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".
|
|
@@ -268,6 +271,7 @@ amiko chat receipts <messageId> # who read your message + when
|
|
|
268
271
|
- **@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`.
|
|
269
272
|
- `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.
|
|
270
273
|
- **`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.
|
|
274
|
+
- **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.
|
|
271
275
|
|
|
272
276
|
### Group chats
|
|
273
277
|
|
|
@@ -590,6 +594,10 @@ npm publish
|
|
|
590
594
|
|
|
591
595
|
## Changelog
|
|
592
596
|
|
|
597
|
+
### 0.14.0-beta.19
|
|
598
|
+
|
|
599
|
+
- **Pinned messages: `amiko chat pin <messageId>`, `amiko chat unpin <messageId>`, `amiko chat pinned <target>`.** Pins are conversation-wide: `pin` posts an "X pinned a message" announcement to the whole chat (POST `/api/messages/{id}/pin`), so it sits behind the outward-action `--yes` gate with a content preview; `unpin` (DELETE, silent but removes shared state for everyone) sits behind the destructive gate. Groups are admin-gated server-side β a 403 names the rule and the `chat group info` check; DMs let either side pin. 409 at the 20-pin cap and 400 for unpinnable (system/in-flight) messages get status-specific copy. `pinned <target>` is an ungated read using the same target resolution as `chat read` (conversation id, user id, `@handle`, or name; never creates a DM), listing pins oldest-first with sender, message id, and pinner name; `--raw` on all three. Requires the companion amiko-web `feat/chat-pinned-messages` deploy.
|
|
600
|
+
|
|
593
601
|
### 0.14.0-beta.18
|
|
594
602
|
|
|
595
603
|
- **Read receipts: `amiko chat receipts <messageId>`.** Shows when the message was sent, who has read it (per-person first-read time), and who it was delivered to but hasn't read yet. Sender-only, enforced server-side β works for messages sent as the owner (`chat send`); anything else 403s with a clear message. Reader names resolve from the conversation's participant list (short-id fallback). Read-only and ungated, like `chat reactions`; `--raw` prints the API payload. Rows read before receipts tracking existed render as "exact time unknown". Requires the companion amiko-web `feat/chat-read-receipts` deploy (`GET /api/messages/{id}/receipts`).
|
package/dist/index.js
CHANGED
|
@@ -27672,6 +27672,20 @@ function registerChatGroupCommand(group) {
|
|
|
27672
27672
|
}
|
|
27673
27673
|
|
|
27674
27674
|
// src/commands/chat.ts
|
|
27675
|
+
var PIN_LIMIT = 20;
|
|
27676
|
+
function pinErrorMessage(e5, fallback2) {
|
|
27677
|
+
const status = e5.status;
|
|
27678
|
+
if (status === 403) {
|
|
27679
|
+
return "Only group admins can pin or unpin messages in a group β check roles with `amiko chat group info`. In a DM either side can pin.";
|
|
27680
|
+
}
|
|
27681
|
+
if (status === 409) {
|
|
27682
|
+
return `Pin limit reached (${PIN_LIMIT}) β unpin one first (see them with \`amiko chat pinned\`).`;
|
|
27683
|
+
}
|
|
27684
|
+
if (status === 400) {
|
|
27685
|
+
return "This message can't be pinned (system and in-flight messages aren't pinnable).";
|
|
27686
|
+
}
|
|
27687
|
+
return e5 instanceof Error ? e5.message : fallback2;
|
|
27688
|
+
}
|
|
27675
27689
|
async function findOrCreateDm(auth, userId) {
|
|
27676
27690
|
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
27677
27691
|
method: "POST",
|
|
@@ -28171,6 +28185,117 @@ function registerChatCommand(chat) {
|
|
|
28171
28185
|
}
|
|
28172
28186
|
}
|
|
28173
28187
|
});
|
|
28188
|
+
chat.command("pin <messageId>").description("Pin a message for everyone in the conversation. Get the message id from `amiko chat read`. Groups require the admin role; the chat sees a pinned-message announcement.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
28189
|
+
const auth = authOrExit();
|
|
28190
|
+
let msg;
|
|
28191
|
+
try {
|
|
28192
|
+
msg = await fetchMessage(auth, messageId);
|
|
28193
|
+
} catch (e5) {
|
|
28194
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
28195
|
+
process.exit(1);
|
|
28196
|
+
}
|
|
28197
|
+
await requireApproval({
|
|
28198
|
+
free: "pins the message for everyone β the chat sees a pinned-message announcement",
|
|
28199
|
+
summary: `Pin "${previewText(msg)}"`,
|
|
28200
|
+
yes: opts.yes,
|
|
28201
|
+
commandExample: `amiko chat pin ${JSON.stringify(messageId)}`
|
|
28202
|
+
});
|
|
28203
|
+
let data;
|
|
28204
|
+
try {
|
|
28205
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "POST", timeoutMs: 20000 });
|
|
28206
|
+
} catch (e5) {
|
|
28207
|
+
console.error(error(pinErrorMessage(e5, "Failed to pin the message")));
|
|
28208
|
+
process.exit(1);
|
|
28209
|
+
}
|
|
28210
|
+
if (opts.raw) {
|
|
28211
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28212
|
+
return;
|
|
28213
|
+
}
|
|
28214
|
+
console.log(success(`Pinned "${previewText(msg)}"`));
|
|
28215
|
+
const total = data.pins?.length;
|
|
28216
|
+
if (typeof total === "number") {
|
|
28217
|
+
console.log(dim(` ${total}/${PIN_LIMIT} pinned in this conversation β see them with \`amiko chat pinned\`.`));
|
|
28218
|
+
}
|
|
28219
|
+
});
|
|
28220
|
+
chat.command("unpin <messageId>").description("Unpin a message β removes it from the conversation's pinned list for everyone. Get the message id from `amiko chat read` or `amiko chat pinned`.").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (messageId, opts) => {
|
|
28221
|
+
const auth = authOrExit();
|
|
28222
|
+
let msg;
|
|
28223
|
+
try {
|
|
28224
|
+
msg = await fetchMessage(auth, messageId);
|
|
28225
|
+
} catch (e5) {
|
|
28226
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not load that message"));
|
|
28227
|
+
process.exit(1);
|
|
28228
|
+
}
|
|
28229
|
+
await confirmDestructive({
|
|
28230
|
+
action: `Unpin "${previewText(msg)}" for everyone in the conversation`,
|
|
28231
|
+
yes: opts.yes,
|
|
28232
|
+
commandExample: `amiko chat unpin ${JSON.stringify(messageId)}`
|
|
28233
|
+
});
|
|
28234
|
+
let data;
|
|
28235
|
+
try {
|
|
28236
|
+
data = await amikoWebFetch(auth, `/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "DELETE", timeoutMs: 20000 });
|
|
28237
|
+
} catch (e5) {
|
|
28238
|
+
console.error(error(pinErrorMessage(e5, "Failed to unpin the message")));
|
|
28239
|
+
process.exit(1);
|
|
28240
|
+
}
|
|
28241
|
+
if (opts.raw) {
|
|
28242
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28243
|
+
return;
|
|
28244
|
+
}
|
|
28245
|
+
console.log(success(`Unpinned "${previewText(msg)}"`));
|
|
28246
|
+
const remaining = data.pins?.length;
|
|
28247
|
+
if (typeof remaining === "number" && remaining > 0) {
|
|
28248
|
+
console.log(dim(` ${remaining} still pinned in this conversation.`));
|
|
28249
|
+
}
|
|
28250
|
+
});
|
|
28251
|
+
chat.command("pinned <target>").description("List a conversation's pinned messages, oldest first (conversation id, user id, @handle, or name)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
28252
|
+
const auth = authOrExit();
|
|
28253
|
+
let convId;
|
|
28254
|
+
try {
|
|
28255
|
+
const resolved = await resolveConversation(auth, target);
|
|
28256
|
+
if (resolved.kind === "user") {
|
|
28257
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
28258
|
+
if (!existing) {
|
|
28259
|
+
throw new Error(`No ${resolved.label} yet β start one with \`amiko chat send\`.`);
|
|
28260
|
+
}
|
|
28261
|
+
convId = existing;
|
|
28262
|
+
} else {
|
|
28263
|
+
convId = resolved.id;
|
|
28264
|
+
}
|
|
28265
|
+
} catch (e5) {
|
|
28266
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
28267
|
+
process.exit(1);
|
|
28268
|
+
}
|
|
28269
|
+
let data;
|
|
28270
|
+
try {
|
|
28271
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/pinned-messages`, { timeoutMs: 20000 });
|
|
28272
|
+
} catch (e5) {
|
|
28273
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to load pinned messages"));
|
|
28274
|
+
process.exit(1);
|
|
28275
|
+
}
|
|
28276
|
+
if (opts.raw) {
|
|
28277
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28278
|
+
return;
|
|
28279
|
+
}
|
|
28280
|
+
const pins = data.pins ?? [];
|
|
28281
|
+
if (pins.length === 0) {
|
|
28282
|
+
console.log(dim("No pinned messages."));
|
|
28283
|
+
return;
|
|
28284
|
+
}
|
|
28285
|
+
console.log(heading(`Pinned messages (${pins.length})
|
|
28286
|
+
`));
|
|
28287
|
+
for (const pin of pins) {
|
|
28288
|
+
const m = pin.message;
|
|
28289
|
+
const who = m.name ?? (m.sender_id ? m.sender_id.slice(0, 8) : "?");
|
|
28290
|
+
const when = m.created_at ? m.created_at.slice(0, 19).replace("T", " ") : "";
|
|
28291
|
+
console.log(label(who, mentionMarkupToPlainText(m.content ?? "")));
|
|
28292
|
+
const meta3 = [when, `id ${m.id}`].filter(Boolean).join(" ");
|
|
28293
|
+
console.log(dim(` ${meta3}`));
|
|
28294
|
+
if (pin.pinned_by_name) {
|
|
28295
|
+
console.log(dim(` pinned by ${pin.pinned_by_name}`));
|
|
28296
|
+
}
|
|
28297
|
+
}
|
|
28298
|
+
});
|
|
28174
28299
|
const group = chat.command("group").description("Group chats β create, list, info, rename, add/remove members, @all permission, leave");
|
|
28175
28300
|
registerChatGroupCommand(group);
|
|
28176
28301
|
}
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -35,6 +35,8 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
|
|
|
35
35
|
| "share the group's invite link" (asked by an admin) | shell β `amiko chat group info "<group>"` (confirm they're admin) β `amiko chat group invite "<group>"` |
|
|
36
36
|
| "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` |
|
|
37
37
|
| "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>` |
|
|
38
|
+
| "pin that hackathon message in the builders group" | shell β `amiko chat read "<group>"` (find the message, copy its `id`) β `amiko chat pin <messageId> --yes` |
|
|
39
|
+
| "what's pinned in the team chat?" | shell β `amiko chat pinned "<group>"` |
|
|
38
40
|
| "what can amiko do?" | shell β `amiko --help` |
|
|
39
41
|
| "what's my MiniMax / ElevenLabs voice id?" | shell β `amiko info` |
|
|
40
42
|
| "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` |
|
|
@@ -54,7 +56,7 @@ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `
|
|
|
54
56
|
|
|
55
57
|
## Critical Rules
|
|
56
58
|
|
|
57
|
-
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/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?").
|
|
59
|
+
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?").
|
|
58
60
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line β include that figure in your reply.
|
|
59
61
|
3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
|
|
60
62
|
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`.
|
|
@@ -150,6 +152,7 @@ MiniMax Hailuo is silent β there is no CLI mux step to attach a separate music
|
|
|
150
152
|
- 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.
|
|
151
153
|
- **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_*`.
|
|
152
154
|
- **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.
|
|
155
|
+
- **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.
|
|
153
156
|
|
|
154
157
|
### Group chats β `amiko chat group`
|
|
155
158
|
|