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