@heyamiko/amiko-cli 0.14.0-beta.5 → 0.14.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/index.js +144 -31
- package/package.json +1 -1
- package/skills/SKILL.md +9 -7
package/README.md
CHANGED
|
@@ -234,13 +234,15 @@ amiko chat list --limit 50 --archived # include archived
|
|
|
234
234
|
amiko chat read @sophie # recent messages with a user (by @handle)
|
|
235
235
|
amiko chat read <conversationId> --limit 40 # by conversation id (from `chat list`) — works for groups too
|
|
236
236
|
amiko chat send @sophie "on my way!" --yes # send as the owner
|
|
237
|
+
amiko chat send <userId> "hey!" --yes # user id works too — the DM is opened automatically
|
|
238
|
+
amiko chat send Mars "hey!" --yes # or a name (friends first, then people search)
|
|
237
239
|
amiko chat send <groupConversationId> "hi all" --yes # send to a group chat
|
|
238
240
|
amiko chat send @sophie "look 👀" --image ./cat.png --yes # attach a local image
|
|
239
241
|
amiko chat send @sophie "made this 🎵" --audio <generated-url> --yes # attach audio (e.g. from `amiko create`)
|
|
240
242
|
```
|
|
241
243
|
|
|
242
|
-
- **`<target>`** for `read`/`send` is a **conversation id** (from `chat list` — use this for groups), an **`@handle`**, or a **name**
|
|
243
|
-
-
|
|
244
|
+
- **`<target>`** for `read`/`send` is a **conversation id** (from `chat list` — use this for groups), a **user id**, an **`@handle`**, or a **name**. Names resolve against your **friends first**, then people search; if ambiguous, the CLI lists candidates instead of guessing. A cuid that isn't one of your conversations is retried as a user id, so pasting a user id "just works".
|
|
245
|
+
- `send` to a person you have no DM with yet **creates the DM automatically** (find-or-create, after the confirmation). `read` never creates one — it errors if no DM exists yet.
|
|
244
246
|
- **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≤10MB or an Amiko URL) and `--audio <pathOrUrl>` (local ≤25MB or an Amiko URL → sent as a voice note). One media block per message (image *or* audio). **Video isn't supported in chat yet.** A local file is uploaded; an Amiko URL (e.g. a generated asset from `amiko create`) attaches directly with no re-upload. (A local *audio* file is sent as a voice note, with your text as a separate message.)
|
|
245
247
|
- `chat send` messages a real person, so it's **gated on `--yes`** in non-interactive shells. Delivery is real-time. `--raw` prints raw JSON on any subcommand.
|
|
246
248
|
|
|
@@ -560,6 +562,15 @@ npm publish
|
|
|
560
562
|
|
|
561
563
|
## Changelog
|
|
562
564
|
|
|
565
|
+
### 0.14.0-beta.7
|
|
566
|
+
|
|
567
|
+
- **`amiko chat group invite <group>` — share a group's invite/join link + QR.** New read-only subcommand that fetches `GET /api/conversations/{id}/invite` (twin token) and prints the join link plus a scannable QR image URL. Admins-only: SKILL.md instructs the agent to confirm the asker holds `admin`/`owner` (via `amiko chat group info`) before sharing, and the endpoint is admin-gated server-side (403 → admin tip). Fails loudly if the response is missing its link/QR instead of printing a placeholder. Requires the companion amiko-web change (adds `qrUrl` to the invite response + a public `GET /api/conversations/join/[token]/qr` PNG route).
|
|
568
|
+
|
|
569
|
+
### 0.14.0-beta.6
|
|
570
|
+
|
|
571
|
+
- **`amiko chat send`/`read` accept user ids — DMs no longer require an existing conversation.** Target resolution is rebuilt: a cuid target is verified as a conversation first and, on 404, retried as a user id (user ids and conversation ids are both cuids — previously a user id went straight to the messages route and died with "Conversation not found"). Plain names now resolve **friends-first** (then people search) via the shared `resolveMembers`, with ambiguity reported as a candidate list. Resolution is fully read-only: for `send`, a missing DM is found-or-created only **after** the `--yes`/approval gate (no more empty conversations from an unapproved send), and `read` never creates a DM at all — it errors if none exists. The confirmation summary now shows the **resolved** recipient (`Send to DM with Mars (@mars): …`) so a wrong-person resolution is visible before anything goes out. Unknown cuids are rejected before any conversation is created (the backend accepts dangling participant ids silently), and a 403 on a real conversation no longer falls through to DM creation.
|
|
572
|
+
- **Free outward actions no longer talk about cost.** `requireApproval` gained a `free` mode used by `chat send` and `chat group create/add`: the non-interactive refusal now says the action is free and tells the agent to confirm the action itself — instead of the old "This command will cost no charge… quote the cost to the user", which made twins ask owners to "approve the paid operation, cost 0". SKILL.md's Critical Rule 1 spells out the anti-pattern, documents user-id DM targets ("the owner never needs to start the conversation first"), and drops the false "server enforces friend rules" claim that fed authorization hallucinations.
|
|
573
|
+
|
|
563
574
|
### 0.14.0-beta.5
|
|
564
575
|
|
|
565
576
|
- **`amiko chat group` — group-chat management as the owner.** New subcommand group under `chat`: `create` (name + repeatable `--member`, one atomic POST), `list`, `info` (members + roles), `rename`, `add`, `remove`, `promote`, and `leave` (alias `delete`). Members resolve friends-first by name (then people search), all-or-nothing before any mutation; groups address by id or case-insensitive title. Uses the existing amiko-web conversation routes (twin token) — rename/add/remove-others/promote are admin-enforced server-side, and `leave|delete` is honest about the platform's semantics: it only hides the group for the caller, there is no delete-for-everyone. `create`/`add` gate on `requireApproval`, the destructive rest on `confirmDestructive` (both exit 2 without `--yes` in non-interactive shells). Plain-name `chat send`/`read` targets now prefer an exact, unambiguous group-title match over people search. Shared conversation types + resolution helpers extracted to `src/lib/conversations.ts`; `convLabel` self-filtering now strips the `did:privy:` prefix so DM labels no longer include the owner. SKILL.md gains a Group chats block + gating-list updates.
|
package/dist/index.js
CHANGED
|
@@ -24825,12 +24825,19 @@ async function confirmDestructive(args) {
|
|
|
24825
24825
|
process.exit(2);
|
|
24826
24826
|
}
|
|
24827
24827
|
async function requireApproval(args) {
|
|
24828
|
+
if (args.cost === undefined === (args.free === undefined)) {
|
|
24829
|
+
throw new Error("requireApproval: set exactly one of `cost` or `free`.");
|
|
24830
|
+
}
|
|
24828
24831
|
if (args.yes)
|
|
24829
24832
|
return;
|
|
24830
24833
|
if (process.stdin.isTTY) {
|
|
24831
24834
|
console.log("");
|
|
24832
24835
|
console.log(`About to run: ${args.summary}`);
|
|
24833
|
-
|
|
24836
|
+
if (args.cost !== undefined) {
|
|
24837
|
+
console.log(`Cost: ${args.cost}`);
|
|
24838
|
+
} else {
|
|
24839
|
+
console.log(`Free — but this is a real outward action other people will see (${args.free}).`);
|
|
24840
|
+
}
|
|
24834
24841
|
const ok = await promptYesNo("Proceed? [y/N] ");
|
|
24835
24842
|
if (!ok) {
|
|
24836
24843
|
console.log(dim("Cancelled."));
|
|
@@ -24840,10 +24847,17 @@ async function requireApproval(args) {
|
|
|
24840
24847
|
}
|
|
24841
24848
|
console.error(error(`Refusing to run without --yes (non-interactive shell detected).`));
|
|
24842
24849
|
console.error("");
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24850
|
+
if (args.cost !== undefined) {
|
|
24851
|
+
console.error(`This command will cost ${args.cost}.`);
|
|
24852
|
+
console.error(`Action: ${args.summary}`);
|
|
24853
|
+
console.error("");
|
|
24854
|
+
console.error(dim(`Quote the cost to the user, get their explicit approval, then re-run with --yes:`));
|
|
24855
|
+
} else {
|
|
24856
|
+
console.error(`This action is FREE — do not quote a price or say "cost: 0". It is outward-facing: ${args.free}.`);
|
|
24857
|
+
console.error(`Action: ${args.summary}`);
|
|
24858
|
+
console.error("");
|
|
24859
|
+
console.error(dim(`Ask the owner to confirm the action itself (what will be sent and who sees it), then re-run with --yes:`));
|
|
24860
|
+
}
|
|
24847
24861
|
console.error(dim(` ${args.commandExample} --yes`));
|
|
24848
24862
|
process.exit(2);
|
|
24849
24863
|
}
|
|
@@ -27078,7 +27092,7 @@ function registerChatGroupCommand(group) {
|
|
|
27078
27092
|
const auth = authOrExit();
|
|
27079
27093
|
const members = await resolveMembersOrExit(auth, opts.member);
|
|
27080
27094
|
await requireApproval({
|
|
27081
|
-
|
|
27095
|
+
free: "creates a group chat that real people are added to",
|
|
27082
27096
|
summary: `Create group "${name}" with ${members.map(memberLabel).join(", ")}`,
|
|
27083
27097
|
yes: opts.yes,
|
|
27084
27098
|
commandExample: `amiko chat group create ${JSON.stringify(name)} ${memberFlags(opts.member)}`
|
|
@@ -27172,6 +27186,31 @@ function registerChatGroupCommand(group) {
|
|
|
27172
27186
|
];
|
|
27173
27187
|
console.log(table(rows));
|
|
27174
27188
|
});
|
|
27189
|
+
group.command("invite <group>").description("Show a group's invite/join link + QR image URL (admins only)").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
27190
|
+
const auth = authOrExit();
|
|
27191
|
+
const conv = await resolveGroupOrExit(auth, target);
|
|
27192
|
+
let data;
|
|
27193
|
+
try {
|
|
27194
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(conv.id)}/invite`, { timeoutMs: 20000 });
|
|
27195
|
+
} catch (e5) {
|
|
27196
|
+
if (statusOf2(e5) === 403) {
|
|
27197
|
+
console.error(error(`Can't view the invite link. ${ADMIN_TIP}`));
|
|
27198
|
+
process.exit(1);
|
|
27199
|
+
}
|
|
27200
|
+
exitWithError(e5, "Failed to load invite link");
|
|
27201
|
+
}
|
|
27202
|
+
if (!data.url || !data.qrUrl) {
|
|
27203
|
+
exitWithError(null, "The invite link response was missing its link or QR URL — nothing to share.");
|
|
27204
|
+
}
|
|
27205
|
+
if (opts.raw) {
|
|
27206
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27207
|
+
return;
|
|
27208
|
+
}
|
|
27209
|
+
console.log(heading(conv.title ?? "(untitled group)"));
|
|
27210
|
+
console.log(label("Invite link", data.url));
|
|
27211
|
+
console.log(label("QR image", data.qrUrl));
|
|
27212
|
+
console.log(dim("Share with group admins only — anyone who opens it can join."));
|
|
27213
|
+
});
|
|
27175
27214
|
group.command("rename <group> <newTitle>").description("Rename a group (admins only)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, newTitle, opts) => {
|
|
27176
27215
|
const auth = authOrExit();
|
|
27177
27216
|
const conv = await resolveGroupOrExit(auth, target);
|
|
@@ -27201,7 +27240,7 @@ function registerChatGroupCommand(group) {
|
|
|
27201
27240
|
const members = await resolveMembersOrExit(auth, opts.member);
|
|
27202
27241
|
const conv = await resolveGroupOrExit(auth, target);
|
|
27203
27242
|
await requireApproval({
|
|
27204
|
-
|
|
27243
|
+
free: "adds real people to the group",
|
|
27205
27244
|
summary: `Add ${members.map(memberLabel).join(", ")} to group "${conv.title ?? conv.id}"`,
|
|
27206
27245
|
yes: opts.yes,
|
|
27207
27246
|
commandExample: `amiko chat group add ${JSON.stringify(target)} ${memberFlags(opts.member)}`
|
|
@@ -27354,33 +27393,91 @@ async function findOrCreateDm(auth, userId) {
|
|
|
27354
27393
|
throw new Error("Could not open a conversation with that user.");
|
|
27355
27394
|
return id;
|
|
27356
27395
|
}
|
|
27396
|
+
async function fetchUserById(auth, id) {
|
|
27397
|
+
try {
|
|
27398
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
|
|
27399
|
+
return prof.user?.id ? { ...prof.user, id: prof.user.id } : null;
|
|
27400
|
+
} catch (e5) {
|
|
27401
|
+
if (e5.status === 404)
|
|
27402
|
+
return null;
|
|
27403
|
+
throw e5;
|
|
27404
|
+
}
|
|
27405
|
+
}
|
|
27406
|
+
async function findDmByUserId(auth, userId) {
|
|
27407
|
+
const want = normalizeUserId(userId);
|
|
27408
|
+
const limit = 100;
|
|
27409
|
+
for (let offset = 0;offset < 1e4; offset += limit) {
|
|
27410
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
27411
|
+
query: { limit, offset },
|
|
27412
|
+
timeoutMs: 20000
|
|
27413
|
+
});
|
|
27414
|
+
const page = data.conversations ?? [];
|
|
27415
|
+
const dm = page.find((c) => c.conversation_type === "direct" && (c.participants ?? []).some((p) => p.participant_type === "user" && normalizeUserId(p.participant_id) === want));
|
|
27416
|
+
if (dm)
|
|
27417
|
+
return dm.id;
|
|
27418
|
+
if (page.length < limit || data.pagination?.has_more === false)
|
|
27419
|
+
return null;
|
|
27420
|
+
}
|
|
27421
|
+
return null;
|
|
27422
|
+
}
|
|
27423
|
+
function personLabel(u) {
|
|
27424
|
+
const name = u.name ?? u.id;
|
|
27425
|
+
return u.handle ? `${name} (@${u.handle})` : name;
|
|
27426
|
+
}
|
|
27357
27427
|
async function resolveConversation(auth, target) {
|
|
27358
27428
|
const t = target.trim();
|
|
27359
|
-
if (
|
|
27360
|
-
|
|
27429
|
+
if (!t) {
|
|
27430
|
+
throw new Error("No target given. Pass a conversation id, a user id, an @handle, or a name.");
|
|
27431
|
+
}
|
|
27432
|
+
const person = (u) => ({
|
|
27433
|
+
kind: "user",
|
|
27434
|
+
userId: u.id,
|
|
27435
|
+
label: `DM with ${personLabel(u)}`
|
|
27436
|
+
});
|
|
27361
27437
|
if (t.startsWith("@")) {
|
|
27362
27438
|
const handle = t.slice(1);
|
|
27363
|
-
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27439
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, {
|
|
27440
|
+
timeoutMs: 15000
|
|
27441
|
+
});
|
|
27364
27442
|
if (!prof.user?.id)
|
|
27365
27443
|
throw new Error(`No user @${handle}.`);
|
|
27366
|
-
return
|
|
27444
|
+
return person({ ...prof.user, id: prof.user.id });
|
|
27445
|
+
}
|
|
27446
|
+
const idish = normalizeUserId(t);
|
|
27447
|
+
if (looksLikeConversationId(idish)) {
|
|
27448
|
+
if (idish === t) {
|
|
27449
|
+
try {
|
|
27450
|
+
const conv = await fetchConversationById(auth, t);
|
|
27451
|
+
return {
|
|
27452
|
+
kind: "conversation",
|
|
27453
|
+
id: conv.id,
|
|
27454
|
+
label: conv.conversation_type === "direct" ? `DM with ${convLabel(conv, auth.userId)}` : `group "${convLabel(conv, auth.userId)}"`
|
|
27455
|
+
};
|
|
27456
|
+
} catch (e5) {
|
|
27457
|
+
const status = e5.status;
|
|
27458
|
+
if (status === 403) {
|
|
27459
|
+
throw new Error(`Conversation ${t} exists but you are not a participant.`);
|
|
27460
|
+
}
|
|
27461
|
+
if (status !== 404)
|
|
27462
|
+
throw e5;
|
|
27463
|
+
}
|
|
27464
|
+
}
|
|
27465
|
+
const user = await fetchUserById(auth, idish);
|
|
27466
|
+
if (!user) {
|
|
27467
|
+
throw new Error(`"${t}" is neither a conversation nor a user id — check \`amiko chat list\` or \`amiko friends list --json\`.`);
|
|
27468
|
+
}
|
|
27469
|
+
return person(user);
|
|
27367
27470
|
}
|
|
27368
27471
|
const groupId = await findGroupIdByExactTitle(auth, t);
|
|
27369
27472
|
if (groupId)
|
|
27370
|
-
return groupId;
|
|
27371
|
-
const
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
const users = res.users ?? [];
|
|
27376
|
-
if (users.length === 0) {
|
|
27377
|
-
throw new Error(`No user matching "${t}". Use an exact @handle, or a conversation id from \`amiko chat list\`.`);
|
|
27378
|
-
}
|
|
27379
|
-
if (users.length > 1) {
|
|
27380
|
-
const names = users.map((u) => `${u.name ?? "?"} (@${u.handle ?? u.id.slice(0, 8)})`).join(", ");
|
|
27381
|
-
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use an exact @handle or a conversation id.`);
|
|
27473
|
+
return { kind: "conversation", id: groupId, label: `group "${t}"` };
|
|
27474
|
+
const { resolved, failures } = await resolveMembers(auth, [t]);
|
|
27475
|
+
if (failures.length || resolved.length !== 1) {
|
|
27476
|
+
const reason = failures[0]?.reason ?? "could not be resolved";
|
|
27477
|
+
throw new Error(`"${t}" ${reason}`);
|
|
27382
27478
|
}
|
|
27383
|
-
|
|
27479
|
+
const p = resolved[0];
|
|
27480
|
+
return person({ id: p.userId, name: p.name, handle: p.handle });
|
|
27384
27481
|
}
|
|
27385
27482
|
var HTTPS_RE = /^https:\/\//i;
|
|
27386
27483
|
var CHAT_IMAGE_MIME = new Set([
|
|
@@ -27467,11 +27564,20 @@ function registerChatCommand(chat) {
|
|
|
27467
27564
|
console.log(dim(` ${unread.trim()}`));
|
|
27468
27565
|
}
|
|
27469
27566
|
});
|
|
27470
|
-
chat.command("read <target>").description("Read recent messages in a conversation (conversation id, @handle, or name)").option("--limit <n>", "Max messages", "20").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
27567
|
+
chat.command("read <target>").description("Read recent messages in a conversation (conversation id, user id, @handle, or name)").option("--limit <n>", "Max messages", "20").option("--raw", "Output raw JSON").action(async (target, opts) => {
|
|
27471
27568
|
const auth = authOrExit();
|
|
27472
27569
|
let convId;
|
|
27473
27570
|
try {
|
|
27474
|
-
|
|
27571
|
+
const resolved = await resolveConversation(auth, target);
|
|
27572
|
+
if (resolved.kind === "user") {
|
|
27573
|
+
const existing = await findDmByUserId(auth, resolved.userId);
|
|
27574
|
+
if (!existing) {
|
|
27575
|
+
throw new Error(`No ${resolved.label} yet — start one with \`amiko chat send\`.`);
|
|
27576
|
+
}
|
|
27577
|
+
convId = existing;
|
|
27578
|
+
} else {
|
|
27579
|
+
convId = resolved.id;
|
|
27580
|
+
}
|
|
27475
27581
|
} catch (e5) {
|
|
27476
27582
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
27477
27583
|
process.exit(1);
|
|
@@ -27502,26 +27608,33 @@ function registerChatCommand(chat) {
|
|
|
27502
27608
|
console.log(dim(` ${when}`));
|
|
27503
27609
|
}
|
|
27504
27610
|
});
|
|
27505
|
-
chat.command("send <target> <message>").description("Send a message as yourself (the owner) to a conversation (id, @handle, or name)").option("--image <pathOrUrl>", "Attach an image: local file (jpg/png/webp/gif, ≤10MB) or an Amiko URL (e.g. from `amiko create`)").option("--audio <pathOrUrl>", "Attach audio as a voice note: local file (≤25MB) or an Amiko URL").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
|
|
27611
|
+
chat.command("send <target> <message>").description("Send a message as yourself (the owner) to a conversation or person (conversation id, user id, @handle, or name — DMs are opened automatically)").option("--image <pathOrUrl>", "Attach an image: local file (jpg/png/webp/gif, ≤10MB) or an Amiko URL (e.g. from `amiko create`)").option("--audio <pathOrUrl>", "Attach audio as a voice note: local file (≤25MB) or an Amiko URL").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
|
|
27506
27612
|
const auth = authOrExit();
|
|
27507
27613
|
if (opts.image && opts.audio) {
|
|
27508
27614
|
console.error(error("Attach either --image or --audio, not both (one media block per message)."));
|
|
27509
27615
|
process.exit(1);
|
|
27510
27616
|
}
|
|
27511
|
-
let
|
|
27617
|
+
let resolved;
|
|
27512
27618
|
try {
|
|
27513
|
-
|
|
27619
|
+
resolved = await resolveConversation(auth, target);
|
|
27514
27620
|
} catch (e5) {
|
|
27515
27621
|
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
27516
27622
|
process.exit(1);
|
|
27517
27623
|
}
|
|
27518
27624
|
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
|
|
27519
27625
|
await requireApproval({
|
|
27520
|
-
|
|
27521
|
-
summary: `Send to ${
|
|
27626
|
+
free: "sends a message as you (the owner) — the recipients see it",
|
|
27627
|
+
summary: `Send to ${resolved.label}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
|
|
27522
27628
|
yes: opts.yes,
|
|
27523
27629
|
commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
|
|
27524
27630
|
});
|
|
27631
|
+
let convId;
|
|
27632
|
+
try {
|
|
27633
|
+
convId = resolved.kind === "user" ? await findOrCreateDm(auth, resolved.userId) : resolved.id;
|
|
27634
|
+
} catch (e5) {
|
|
27635
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not open the conversation"));
|
|
27636
|
+
process.exit(1);
|
|
27637
|
+
}
|
|
27525
27638
|
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
27526
27639
|
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
27527
27640
|
method: "POST",
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: amiko-cli
|
|
3
|
-
description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, generate media — images, video, speech, music, and SFX — via Create Studio (`amiko create`, async and charged on success), call paid MPP marketplace services (X/Twitter search, Amazon product search, TTS/STT, AI chat), manage the twin's identity and drive (files, folders, RAG), voice and avatar, and social graph (friends, posts, comments, feed). Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). The owner's DMs and group chats are `amiko chat` (list / read / send / create + manage group chats AS the owner, 人对人); the agent's OWN sessions are the openhermit gateway's session_list / session_send (AS the agent) — different identities, different surfaces. Use this skill whenever the user asks about their Amiko notifications, chats/DMs, drive, memory, social graph, wallets, credits, or marketplace services — anything that would show up in their Amiko account or cost AMIKO/credits.
|
|
3
|
+
description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, generate media — images, video, speech, music, and SFX — via Create Studio (`amiko create`, async and charged on success), call paid MPP marketplace services (X/Twitter search, Amazon product search, TTS/STT, AI chat), manage the twin's identity and drive (files, folders, RAG), voice and avatar, and social graph (friends, posts, comments, feed). Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). The owner's DMs and group chats are `amiko chat` (list / read / send / create + manage group chats AS the owner, including sharing a group's invite/join link + QR, 人对人); the agent's OWN sessions are the openhermit gateway's session_list / session_send (AS the agent) — different identities, different surfaces. Use this skill whenever the user asks about their Amiko notifications, chats/DMs, drive, memory, social graph, wallets, credits, or marketplace services — anything that would show up in their Amiko account or cost AMIKO/credits.
|
|
4
4
|
homepage: https://platform.heyamiko.com
|
|
5
5
|
metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["node"]}}}
|
|
6
6
|
---
|
|
@@ -32,6 +32,7 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
|
|
|
32
32
|
| "find files about Q1 revenue" | shell → `amiko drive search "Q1 revenue"` |
|
|
33
33
|
| "what comments are on my post?" | shell → `amiko post comments --id <postId>` |
|
|
34
34
|
| "search memory for X" | shell → `amiko memory search "X"` |
|
|
35
|
+
| "share the group's invite link" (asked by an admin) | shell → `amiko chat group info "<group>"` (confirm they're admin) → `amiko chat group invite "<group>"` |
|
|
35
36
|
| "what can amiko do?" | shell → `amiko --help` |
|
|
36
37
|
|
|
37
38
|
The CLI is installed globally and is pre-authenticated when you're inside your workspace folder. Never suggest `amiko login` or `amiko connect` — they don't exist.
|
|
@@ -49,7 +50,7 @@ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `
|
|
|
49
50
|
|
|
50
51
|
## Critical Rules
|
|
51
52
|
|
|
52
|
-
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `card mint`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.** The same `--yes` gate also covers **free outward social actions** — `chat send`, `chat group create`, `chat group add` (
|
|
53
|
+
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `card mint`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.** The same `--yes` gate also covers **free outward social actions** — `chat send`, `chat group create`, `chat group add` (real people see them, so get the owner's explicit approval for the action itself) — and **destructive ops** like `chat group remove/leave/rename/promote`, `twin update --public`, `drive delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `review reject`. **For these free actions never mention cost, never say "the cost is 0", and never call it a paid operation** — ask for plain confirmation of the action itself (e.g. "Send "hi" to Mars — go ahead?").
|
|
53
54
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
|
|
54
55
|
3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
|
|
55
56
|
4. **Auth is automatic.** Never suggest `amiko login` / `amiko connect`. Run from the agent's workspace folder; if anything looks off, `amiko accounts` shows the resolved `userId` / `twinId`.
|
|
@@ -124,21 +125,22 @@ MiniMax Hailuo is silent — there is no CLI mux step to attach a separate music
|
|
|
124
125
|
`amiko chat` is the **owner's** conversations, acting **as the owner** (人对人) — NOT the agent's own sessions (those are the gateway's `session_*`, as the agent).
|
|
125
126
|
|
|
126
127
|
- `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
|
|
127
|
-
- `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), an `@handle`, or a name.
|
|
128
|
-
- `amiko chat send <target> "message"` — send **as the owner**. Delivered in real time. `--yes` required in non-interactive shells (it's an outward message to a real person).
|
|
128
|
+
- `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), a **user id**, an `@handle`, or a name. Reading never creates a conversation — if there's no DM with that person yet, it says so.
|
|
129
|
+
- `amiko chat send <target> "message"` — send **as the owner**. Delivered in real time. `--yes` required in non-interactive shells (it's an outward message to a real person). `<target>` = a conversation id, a **user id**, an `@handle`, or a name. A user id / @handle / unambiguous name opens (or reuses) the DM for you — but only **after** the owner approves the send (via the confirmation, or `--yes` in your shell); nothing is created if approval is refused. **The owner never needs to start the conversation first, and there is no platform authorization step for starting a DM.** For **group chats**, use the conversation id from `list`.
|
|
129
130
|
- **Inline media**: `--image <pathOrUrl>` (jpg/png/webp/gif, local ≤10MB or an Amiko URL — e.g. a generated image) and `--audio <pathOrUrl>` (local ≤25MB or an Amiko URL → sent as a voice note). One media block per message (image XOR audio). **Video is not supported** on chat yet. To share a generated asset, pass its URL from `amiko create status` (no re-upload). A local audio file is sent as a voice note with the text as a separate message.
|
|
130
|
-
-
|
|
131
|
+
- Plain names resolve against the owner's **friends first**, then people search; if ambiguous the CLI errors listing the candidates — ask the owner which one, don't blind-send. A plain name that exactly matches one of the owner's group titles targets that **group**. If a send fails, surface the CLI's error verbatim — don't invent permission explanations or ask the owner to open the chat from the app.
|
|
131
132
|
|
|
132
133
|
### Group chats — `amiko chat group`
|
|
133
134
|
|
|
134
|
-
Create and manage the owner's group chats: `create <name> --member <who>…`, `list`, `info <group>`, `rename <group> <newTitle>`, `add <group> --member <who>…`, `remove <group> --member <who>…`, `promote <group> --member <who>`, `leave <group>` (alias `delete`).
|
|
135
|
+
Create and manage the owner's group chats: `create <name> --member <who>…`, `list`, `info <group>`, `invite <group>`, `rename <group> <newTitle>`, `add <group> --member <who>…`, `remove <group> --member <who>…`, `promote <group> --member <who>`, `leave <group>` (alias `delete`).
|
|
135
136
|
|
|
136
137
|
- One-shot example — "create a group called Leandro testing with Sophie, Mars and Matthew":
|
|
137
138
|
`amiko chat group create "Leandro testing" --member Sophie --member Mars --member Matthew --yes`
|
|
138
139
|
- `--member` repeats per person and takes a name, `@handle`, or user id. Plain names resolve against the owner's **friends** first, then people search; an ambiguous name errors listing candidates — prefer an exact `@handle` or a user id from `amiko friends list --json`. If any member fails to resolve, the whole command aborts **before anything changes** — fix and re-run.
|
|
139
140
|
- `<group>` is a conversation id (from `chat group list`) or a title (matched case-insensitively — exact matches win before substring matches; if several groups still match, the CLI errors listing the candidates; only the ~100 most recent conversations are scanned, so use the id for old groups).
|
|
140
141
|
- **`leave` (alias `delete`) only hides the group for the owner — it does NOT delete it for other members; there is no true group deletion.** Say so if the owner asks to delete a group. Rename, add, remove-others, and promote require the owner to be a group **admin** (the creator is one automatically); a 403 means they're not — report it, don't retry.
|
|
141
|
-
-
|
|
142
|
+
- `amiko chat group invite <group>` — show the group's **invite/join link + a QR image URL** (both are shareable URLs; the QR encodes the same join link). Use it when a group admin asks how to invite people or for the group's link or QR. **Admins only:** before sharing, confirm the person asking holds the `admin`/`owner` role — check the ROLE column in `amiko chat group info <group>`. If they're a regular member, politely decline and do **not** reveal the link or QR. The link lets anyone who opens it join the group, so treat it like a credential. A 403 means your owner isn't an admin of that group — report it, don't retry.
|
|
143
|
+
- Gating: `create`/`add` message real people and `remove`/`leave`/`rename`/`promote` are destructive — all require `--yes` in your shell (see Critical Rules). Reads (`list`, `info`, `invite`) are free and ungated.
|
|
142
144
|
|
|
143
145
|
## Card — behavior notes
|
|
144
146
|
|