@heyamiko/amiko-cli 0.12.0-beta.3 → 0.12.0-beta.5
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 +21 -1
- package/dist/index.js +333 -99
- package/package.json +1 -1
- package/skills/SKILL.md +15 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @heyamiko/amiko-cli (v0.
|
|
1
|
+
# @heyamiko/amiko-cli (v0.12.0-beta.5)
|
|
2
2
|
|
|
3
3
|
Manage wallets, credits, swaps, MPP marketplace services, and your Amiko twin (identity, documents, voice, avatar, friends, feed) from the terminal. Works for both human users and AI agents running on OpenClaw.
|
|
4
4
|
|
|
@@ -210,6 +210,26 @@ amiko create media --service video --limit 5
|
|
|
210
210
|
|
|
211
211
|
> `amiko markets image` still exists and is unchanged — it's the direct MPP pay-per-call path (pre-pay in AMIKO). Use `amiko create` for the Create Studio experience (async, charge-on-success); use `amiko markets` for raw MPP endpoints.
|
|
212
212
|
|
|
213
|
+
## Chat
|
|
214
|
+
|
|
215
|
+
Your conversations — **DMs and group chats** — acting **as you (the owner)**. (This is distinct from the openhermit gateway's `session_*` tools, which act as the *agent* on the agent's own sessions.)
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
amiko chat list # all conversations (DM + group): id, peer/title, last msg, unread
|
|
219
|
+
amiko chat list --limit 50 --archived # include archived
|
|
220
|
+
amiko chat read @sophie # recent messages with a user (by @handle)
|
|
221
|
+
amiko chat read <conversationId> --limit 40 # by conversation id (from `chat list`) — works for groups too
|
|
222
|
+
amiko chat send @sophie "on my way!" --yes # send as the owner
|
|
223
|
+
amiko chat send <groupConversationId> "hi all" --yes # send to a group chat
|
|
224
|
+
amiko chat send @sophie "look 👀" --image ./cat.png --yes # attach a local image
|
|
225
|
+
amiko chat send @sophie "made this 🎵" --audio <generated-url> --yes # attach audio (e.g. from `amiko create`)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- **`<target>`** for `read`/`send` is a **conversation id** (from `chat list` — use this for groups), an **`@handle`**, or a **name** (resolved via user search; if ambiguous, the CLI lists candidates instead of guessing).
|
|
229
|
+
- A DM target that doesn't exist yet is **created automatically** (find-or-create).
|
|
230
|
+
- **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.)
|
|
231
|
+
- `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.
|
|
232
|
+
|
|
213
233
|
## Amazon
|
|
214
234
|
|
|
215
235
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -28924,6 +28924,337 @@ More available — raise --limit to see more.`));
|
|
|
28924
28924
|
});
|
|
28925
28925
|
}
|
|
28926
28926
|
|
|
28927
|
+
// src/lib/file-input.ts
|
|
28928
|
+
import { readFile } from "node:fs/promises";
|
|
28929
|
+
import { basename, extname } from "node:path";
|
|
28930
|
+
var MAX_BYTES = 100 * 1024 * 1024;
|
|
28931
|
+
var FETCH_TIMEOUT_MS = 30000;
|
|
28932
|
+
async function resolveFileInput(src, opts = {}) {
|
|
28933
|
+
if (src === "-" || opts.stdin) {
|
|
28934
|
+
const buffer2 = await readStdin();
|
|
28935
|
+
const name2 = opts.hintName ?? "stdin";
|
|
28936
|
+
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28937
|
+
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28938
|
+
}
|
|
28939
|
+
if (/^https?:\/\//i.test(src)) {
|
|
28940
|
+
const buffer2 = await fetchRemote(src);
|
|
28941
|
+
const urlPath = new URL(src).pathname;
|
|
28942
|
+
const name2 = opts.hintName ?? (basename(urlPath) || "download");
|
|
28943
|
+
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
28944
|
+
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
28945
|
+
}
|
|
28946
|
+
const buffer = await readFile(src);
|
|
28947
|
+
if (buffer.length > MAX_BYTES) {
|
|
28948
|
+
throw new Error(`File too large: ${buffer.length} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
28949
|
+
}
|
|
28950
|
+
const name = opts.hintName ?? basename(src);
|
|
28951
|
+
const mime = opts.hintMime ?? detectMime(buffer, name);
|
|
28952
|
+
return { buffer, name, mime, size: buffer.length };
|
|
28953
|
+
}
|
|
28954
|
+
async function readStdin() {
|
|
28955
|
+
const chunks = [];
|
|
28956
|
+
let total = 0;
|
|
28957
|
+
for await (const chunk of process.stdin) {
|
|
28958
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28959
|
+
total += buf.length;
|
|
28960
|
+
if (total > MAX_BYTES) {
|
|
28961
|
+
throw new Error(`stdin exceeded ${MAX_BYTES} byte cap.`);
|
|
28962
|
+
}
|
|
28963
|
+
chunks.push(buf);
|
|
28964
|
+
}
|
|
28965
|
+
return Buffer.concat(chunks);
|
|
28966
|
+
}
|
|
28967
|
+
async function fetchRemote(url2) {
|
|
28968
|
+
const controller = new AbortController;
|
|
28969
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
28970
|
+
try {
|
|
28971
|
+
const res = await fetch(url2, { signal: controller.signal });
|
|
28972
|
+
if (!res.ok) {
|
|
28973
|
+
throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
|
|
28974
|
+
}
|
|
28975
|
+
const len = Number(res.headers.get("content-length") || 0);
|
|
28976
|
+
if (len > MAX_BYTES) {
|
|
28977
|
+
throw new Error(`Remote file too large: ${len} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
28978
|
+
}
|
|
28979
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
28980
|
+
if (buf.length > MAX_BYTES) {
|
|
28981
|
+
throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
28982
|
+
}
|
|
28983
|
+
return buf;
|
|
28984
|
+
} finally {
|
|
28985
|
+
clearTimeout(timer);
|
|
28986
|
+
}
|
|
28987
|
+
}
|
|
28988
|
+
var MAGIC = [
|
|
28989
|
+
[Buffer.from([37, 80, 68, 70]), "application/pdf"],
|
|
28990
|
+
[Buffer.from([137, 80, 78, 71]), "image/png"],
|
|
28991
|
+
[Buffer.from([255, 216, 255]), "image/jpeg"],
|
|
28992
|
+
[Buffer.from([71, 73, 70, 56]), "image/gif"],
|
|
28993
|
+
[Buffer.from([82, 73, 70, 70]), "audio/wav"],
|
|
28994
|
+
[Buffer.from([73, 68, 51]), "audio/mpeg"],
|
|
28995
|
+
[Buffer.from([255, 251]), "audio/mpeg"],
|
|
28996
|
+
[Buffer.from([80, 75, 3, 4]), "application/zip"]
|
|
28997
|
+
];
|
|
28998
|
+
var EXT_MIME = {
|
|
28999
|
+
".pdf": "application/pdf",
|
|
29000
|
+
".png": "image/png",
|
|
29001
|
+
".jpg": "image/jpeg",
|
|
29002
|
+
".jpeg": "image/jpeg",
|
|
29003
|
+
".gif": "image/gif",
|
|
29004
|
+
".webp": "image/webp",
|
|
29005
|
+
".txt": "text/plain",
|
|
29006
|
+
".md": "text/markdown",
|
|
29007
|
+
".json": "application/json",
|
|
29008
|
+
".wav": "audio/wav",
|
|
29009
|
+
".mp3": "audio/mpeg",
|
|
29010
|
+
".m4a": "audio/mp4",
|
|
29011
|
+
".ogg": "audio/ogg",
|
|
29012
|
+
".zip": "application/zip"
|
|
29013
|
+
};
|
|
29014
|
+
function detectMime(buffer, name) {
|
|
29015
|
+
for (const [sig, mime] of MAGIC) {
|
|
29016
|
+
if (buffer.length >= sig.length && buffer.subarray(0, sig.length).equals(sig)) {
|
|
29017
|
+
return mime;
|
|
29018
|
+
}
|
|
29019
|
+
}
|
|
29020
|
+
const ext = extname(name).toLowerCase();
|
|
29021
|
+
return EXT_MIME[ext] ?? "application/octet-stream";
|
|
29022
|
+
}
|
|
29023
|
+
|
|
29024
|
+
// src/commands/chat.ts
|
|
29025
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29026
|
+
function authOrExit() {
|
|
29027
|
+
try {
|
|
29028
|
+
return requireAuth();
|
|
29029
|
+
} catch (e5) {
|
|
29030
|
+
console.error(error(e5 instanceof Error ? e5.message : "auth failed"));
|
|
29031
|
+
process.exit(1);
|
|
29032
|
+
}
|
|
29033
|
+
}
|
|
29034
|
+
function convLabel(conv, selfUserId) {
|
|
29035
|
+
if (conv.title)
|
|
29036
|
+
return conv.title;
|
|
29037
|
+
const others = (conv.participants ?? []).filter((p) => !(p.participant_type === "user" && p.participant_id === selfUserId)).map((p) => p.user?.name ?? p.twin?.name ?? p.participant_id.slice(0, 8)).filter((n) => !!n);
|
|
29038
|
+
return others.length ? others.join(", ") : "(conversation)";
|
|
29039
|
+
}
|
|
29040
|
+
async function findOrCreateDm(auth, userId) {
|
|
29041
|
+
const data = await amikoWebFetch(auth, "/api/conversations", {
|
|
29042
|
+
method: "POST",
|
|
29043
|
+
body: {
|
|
29044
|
+
participant_ids: [userId],
|
|
29045
|
+
participant_types: ["user"],
|
|
29046
|
+
conversation_type: "direct"
|
|
29047
|
+
},
|
|
29048
|
+
timeoutMs: 20000
|
|
29049
|
+
});
|
|
29050
|
+
const id = data.conversation?.id;
|
|
29051
|
+
if (!id)
|
|
29052
|
+
throw new Error("Could not open a conversation with that user.");
|
|
29053
|
+
return id;
|
|
29054
|
+
}
|
|
29055
|
+
async function resolveConversation(auth, target) {
|
|
29056
|
+
const t = target.trim();
|
|
29057
|
+
if (UUID_RE.test(t))
|
|
29058
|
+
return t;
|
|
29059
|
+
if (t.startsWith("@")) {
|
|
29060
|
+
const handle = t.slice(1);
|
|
29061
|
+
const prof = await amikoWebFetch(auth, `/api/users/${encodeURIComponent(handle)}`, { timeoutMs: 15000 });
|
|
29062
|
+
if (!prof.user?.id)
|
|
29063
|
+
throw new Error(`No user @${handle}.`);
|
|
29064
|
+
return findOrCreateDm(auth, prof.user.id);
|
|
29065
|
+
}
|
|
29066
|
+
const res = await amikoWebFetch(auth, "/api/search", {
|
|
29067
|
+
query: { q: t, type: "people", limit: 8 },
|
|
29068
|
+
timeoutMs: 15000
|
|
29069
|
+
});
|
|
29070
|
+
const users = res.users ?? [];
|
|
29071
|
+
if (users.length === 0) {
|
|
29072
|
+
throw new Error(`No user matching "${t}". Use an exact @handle, or a conversation id from \`amiko chat list\`.`);
|
|
29073
|
+
}
|
|
29074
|
+
if (users.length > 1) {
|
|
29075
|
+
const names = users.map((u) => `${u.name ?? "?"} (@${u.handle ?? u.id.slice(0, 8)})`).join(", ");
|
|
29076
|
+
throw new Error(`"${t}" is ambiguous — matches: ${names}. Use an exact @handle or a conversation id.`);
|
|
29077
|
+
}
|
|
29078
|
+
return findOrCreateDm(auth, users[0].id);
|
|
29079
|
+
}
|
|
29080
|
+
var HTTPS_RE = /^https:\/\//i;
|
|
29081
|
+
var CHAT_IMAGE_MIME = new Set([
|
|
29082
|
+
"image/jpeg",
|
|
29083
|
+
"image/jpg",
|
|
29084
|
+
"image/png",
|
|
29085
|
+
"image/webp",
|
|
29086
|
+
"image/gif"
|
|
29087
|
+
]);
|
|
29088
|
+
var MAX_CHAT_IMAGE = 10 * 1024 * 1024;
|
|
29089
|
+
var MAX_CHAT_AUDIO = 25 * 1024 * 1024;
|
|
29090
|
+
function urlName(url2) {
|
|
29091
|
+
try {
|
|
29092
|
+
const p = new URL(url2).pathname.split("/").pop();
|
|
29093
|
+
return p && p.length ? decodeURIComponent(p) : "attachment";
|
|
29094
|
+
} catch {
|
|
29095
|
+
return "attachment";
|
|
29096
|
+
}
|
|
29097
|
+
}
|
|
29098
|
+
async function uploadChatImage(auth, convId, path2) {
|
|
29099
|
+
const f = await resolveFileInput(path2);
|
|
29100
|
+
if (!CHAT_IMAGE_MIME.has(f.mime)) {
|
|
29101
|
+
throw new Error(`"${path2}" is not a supported image (detected ${f.mime}). Allowed: JPEG, PNG, WebP, GIF.`);
|
|
29102
|
+
}
|
|
29103
|
+
if (f.size > MAX_CHAT_IMAGE) {
|
|
29104
|
+
throw new Error(`"${path2}" exceeds the 10MB chat image limit.`);
|
|
29105
|
+
}
|
|
29106
|
+
const form = new FormData;
|
|
29107
|
+
form.set("file", new Blob([new Uint8Array(f.buffer)], { type: f.mime }), f.name);
|
|
29108
|
+
const data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/images/upload`, { method: "POST", multipart: form, timeoutMs: 60000 });
|
|
29109
|
+
if (!data.image?.url)
|
|
29110
|
+
throw new Error("Image upload returned no URL.");
|
|
29111
|
+
return { url: data.image.url, name: data.image.name ?? f.name };
|
|
29112
|
+
}
|
|
29113
|
+
async function uploadChatVoice(auth, convId, path2) {
|
|
29114
|
+
const f = await resolveFileInput(path2);
|
|
29115
|
+
if (!f.mime.startsWith("audio/")) {
|
|
29116
|
+
throw new Error(`"${path2}" is not an audio file (detected ${f.mime}).`);
|
|
29117
|
+
}
|
|
29118
|
+
if (f.size > MAX_CHAT_AUDIO) {
|
|
29119
|
+
throw new Error(`"${path2}" exceeds the 25MB chat audio limit.`);
|
|
29120
|
+
}
|
|
29121
|
+
const form = new FormData;
|
|
29122
|
+
form.set("audio", new Blob([new Uint8Array(f.buffer)], { type: f.mime }), f.name);
|
|
29123
|
+
form.set("duration", "0");
|
|
29124
|
+
await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/audio/upload`, { method: "POST", multipart: form, timeoutMs: 120000 });
|
|
29125
|
+
}
|
|
29126
|
+
function registerChatCommand(chat) {
|
|
29127
|
+
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) => {
|
|
29128
|
+
const auth = authOrExit();
|
|
29129
|
+
let data;
|
|
29130
|
+
try {
|
|
29131
|
+
data = await amikoWebFetch(auth, "/api/conversations", {
|
|
29132
|
+
query: {
|
|
29133
|
+
limit: opts.limit,
|
|
29134
|
+
...opts.archived ? { archived: "all" } : {}
|
|
29135
|
+
},
|
|
29136
|
+
timeoutMs: 20000
|
|
29137
|
+
});
|
|
29138
|
+
} catch (e5) {
|
|
29139
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to list conversations"));
|
|
29140
|
+
process.exit(1);
|
|
29141
|
+
}
|
|
29142
|
+
if (opts.raw) {
|
|
29143
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29144
|
+
return;
|
|
29145
|
+
}
|
|
29146
|
+
const convs = data.conversations ?? [];
|
|
29147
|
+
if (convs.length === 0) {
|
|
29148
|
+
console.log(dim("No conversations."));
|
|
29149
|
+
return;
|
|
29150
|
+
}
|
|
29151
|
+
console.log(heading(`Conversations (${convs.length})
|
|
29152
|
+
`));
|
|
29153
|
+
for (const c of convs) {
|
|
29154
|
+
const kind = c.conversation_type === "direct" ? "DM" : c.conversation_type;
|
|
29155
|
+
const unread = c.unread_count ? ` (${c.unread_count} unread)` : "";
|
|
29156
|
+
console.log(label(`[${kind}] ${convLabel(c, auth.userId)}`, dim(c.id)));
|
|
29157
|
+
if (c.last_message?.content) {
|
|
29158
|
+
const who = c.last_message.name ? `${c.last_message.name}: ` : "";
|
|
29159
|
+
console.log(dim(` ${who}${c.last_message.content.slice(0, 80)}`));
|
|
29160
|
+
}
|
|
29161
|
+
if (unread)
|
|
29162
|
+
console.log(dim(` ${unread.trim()}`));
|
|
29163
|
+
}
|
|
29164
|
+
});
|
|
29165
|
+
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) => {
|
|
29166
|
+
const auth = authOrExit();
|
|
29167
|
+
let convId;
|
|
29168
|
+
try {
|
|
29169
|
+
convId = await resolveConversation(auth, target);
|
|
29170
|
+
} catch (e5) {
|
|
29171
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
29172
|
+
process.exit(1);
|
|
29173
|
+
}
|
|
29174
|
+
let data;
|
|
29175
|
+
try {
|
|
29176
|
+
data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/messages`, { query: { limit: opts.limit }, timeoutMs: 20000 });
|
|
29177
|
+
} catch (e5) {
|
|
29178
|
+
console.error(error(e5 instanceof Error ? e5.message : "Failed to read messages"));
|
|
29179
|
+
process.exit(1);
|
|
29180
|
+
}
|
|
29181
|
+
if (opts.raw) {
|
|
29182
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29183
|
+
return;
|
|
29184
|
+
}
|
|
29185
|
+
const msgs = (data.messages ?? []).slice().reverse();
|
|
29186
|
+
if (msgs.length === 0) {
|
|
29187
|
+
console.log(dim("No messages."));
|
|
29188
|
+
return;
|
|
29189
|
+
}
|
|
29190
|
+
console.log(heading(`Messages (${msgs.length})
|
|
29191
|
+
`));
|
|
29192
|
+
for (const m of msgs) {
|
|
29193
|
+
const who = m.sender?.name ?? m.name ?? (m.sender_id ? m.sender_id.slice(0, 8) : "?");
|
|
29194
|
+
const when = m.created_at ? m.created_at.slice(0, 19).replace("T", " ") : "";
|
|
29195
|
+
console.log(label(who, m.content));
|
|
29196
|
+
if (when)
|
|
29197
|
+
console.log(dim(` ${when}`));
|
|
29198
|
+
}
|
|
29199
|
+
});
|
|
29200
|
+
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) => {
|
|
29201
|
+
const auth = authOrExit();
|
|
29202
|
+
if (opts.image && opts.audio) {
|
|
29203
|
+
console.error(error("Attach either --image or --audio, not both (one media block per message)."));
|
|
29204
|
+
process.exit(1);
|
|
29205
|
+
}
|
|
29206
|
+
let convId;
|
|
29207
|
+
try {
|
|
29208
|
+
convId = await resolveConversation(auth, target);
|
|
29209
|
+
} catch (e5) {
|
|
29210
|
+
console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
|
|
29211
|
+
process.exit(1);
|
|
29212
|
+
}
|
|
29213
|
+
const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
|
|
29214
|
+
await requireApproval({
|
|
29215
|
+
cost: "no charge — sends a message as you (the owner)",
|
|
29216
|
+
summary: `Send to ${target}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
|
|
29217
|
+
yes: opts.yes,
|
|
29218
|
+
commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
|
|
29219
|
+
});
|
|
29220
|
+
const messagesPath = `/api/conversations/${encodeURIComponent(convId)}/messages`;
|
|
29221
|
+
const post = (body) => amikoWebFetch(auth, messagesPath, {
|
|
29222
|
+
method: "POST",
|
|
29223
|
+
body,
|
|
29224
|
+
timeoutMs: 20000
|
|
29225
|
+
});
|
|
29226
|
+
let data;
|
|
29227
|
+
try {
|
|
29228
|
+
if (opts.image) {
|
|
29229
|
+
const img = HTTPS_RE.test(opts.image) ? { url: opts.image, name: urlName(opts.image) } : await uploadChatImage(auth, convId, opts.image);
|
|
29230
|
+
data = await post({ content: message, metadata: { images: [img] } });
|
|
29231
|
+
} else if (opts.audio) {
|
|
29232
|
+
if (HTTPS_RE.test(opts.audio)) {
|
|
29233
|
+
data = await post({
|
|
29234
|
+
content: message,
|
|
29235
|
+
message_type: "voice",
|
|
29236
|
+
metadata: { audioUrl: opts.audio }
|
|
29237
|
+
});
|
|
29238
|
+
} else {
|
|
29239
|
+
data = await post({ content: message });
|
|
29240
|
+
await uploadChatVoice(auth, convId, opts.audio);
|
|
29241
|
+
}
|
|
29242
|
+
} else {
|
|
29243
|
+
data = await post({ content: message });
|
|
29244
|
+
}
|
|
29245
|
+
} catch (e5) {
|
|
29246
|
+
console.error(error(e5 instanceof Error ? e5.message : "Send failed"));
|
|
29247
|
+
process.exit(1);
|
|
29248
|
+
}
|
|
29249
|
+
if (opts.raw) {
|
|
29250
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29251
|
+
return;
|
|
29252
|
+
}
|
|
29253
|
+
const what = opts.image ? "Message + image sent" : opts.audio ? "Message + voice note sent" : "Message sent";
|
|
29254
|
+
console.log(success(what) + dim(` (conversation ${convId.slice(0, 8)}…)`));
|
|
29255
|
+
});
|
|
29256
|
+
}
|
|
29257
|
+
|
|
28927
29258
|
// src/commands/amazon.ts
|
|
28928
29259
|
var TREASURY3 = "FPiZAf3jEnNfCwfvbv46jdPfHbmSepD4Q6iHiLJDQXvb";
|
|
28929
29260
|
async function payAmiko(auth, wallet, amount2) {
|
|
@@ -29474,105 +29805,6 @@ function registerTwinCommand(program2) {
|
|
|
29474
29805
|
// src/commands/drive.ts
|
|
29475
29806
|
import { writeFile } from "node:fs/promises";
|
|
29476
29807
|
import { basename as basename2, resolve as resolvePath } from "node:path";
|
|
29477
|
-
|
|
29478
|
-
// src/lib/file-input.ts
|
|
29479
|
-
import { readFile } from "node:fs/promises";
|
|
29480
|
-
import { basename, extname } from "node:path";
|
|
29481
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
29482
|
-
var FETCH_TIMEOUT_MS = 30000;
|
|
29483
|
-
async function resolveFileInput(src, opts = {}) {
|
|
29484
|
-
if (src === "-" || opts.stdin) {
|
|
29485
|
-
const buffer2 = await readStdin();
|
|
29486
|
-
const name2 = opts.hintName ?? "stdin";
|
|
29487
|
-
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
29488
|
-
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
29489
|
-
}
|
|
29490
|
-
if (/^https?:\/\//i.test(src)) {
|
|
29491
|
-
const buffer2 = await fetchRemote(src);
|
|
29492
|
-
const urlPath = new URL(src).pathname;
|
|
29493
|
-
const name2 = opts.hintName ?? (basename(urlPath) || "download");
|
|
29494
|
-
const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
|
|
29495
|
-
return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
|
|
29496
|
-
}
|
|
29497
|
-
const buffer = await readFile(src);
|
|
29498
|
-
if (buffer.length > MAX_BYTES) {
|
|
29499
|
-
throw new Error(`File too large: ${buffer.length} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
29500
|
-
}
|
|
29501
|
-
const name = opts.hintName ?? basename(src);
|
|
29502
|
-
const mime = opts.hintMime ?? detectMime(buffer, name);
|
|
29503
|
-
return { buffer, name, mime, size: buffer.length };
|
|
29504
|
-
}
|
|
29505
|
-
async function readStdin() {
|
|
29506
|
-
const chunks = [];
|
|
29507
|
-
let total = 0;
|
|
29508
|
-
for await (const chunk of process.stdin) {
|
|
29509
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
29510
|
-
total += buf.length;
|
|
29511
|
-
if (total > MAX_BYTES) {
|
|
29512
|
-
throw new Error(`stdin exceeded ${MAX_BYTES} byte cap.`);
|
|
29513
|
-
}
|
|
29514
|
-
chunks.push(buf);
|
|
29515
|
-
}
|
|
29516
|
-
return Buffer.concat(chunks);
|
|
29517
|
-
}
|
|
29518
|
-
async function fetchRemote(url2) {
|
|
29519
|
-
const controller = new AbortController;
|
|
29520
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
29521
|
-
try {
|
|
29522
|
-
const res = await fetch(url2, { signal: controller.signal });
|
|
29523
|
-
if (!res.ok) {
|
|
29524
|
-
throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
|
|
29525
|
-
}
|
|
29526
|
-
const len = Number(res.headers.get("content-length") || 0);
|
|
29527
|
-
if (len > MAX_BYTES) {
|
|
29528
|
-
throw new Error(`Remote file too large: ${len} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
29529
|
-
}
|
|
29530
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
29531
|
-
if (buf.length > MAX_BYTES) {
|
|
29532
|
-
throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${MAX_BYTES} byte cap.`);
|
|
29533
|
-
}
|
|
29534
|
-
return buf;
|
|
29535
|
-
} finally {
|
|
29536
|
-
clearTimeout(timer);
|
|
29537
|
-
}
|
|
29538
|
-
}
|
|
29539
|
-
var MAGIC = [
|
|
29540
|
-
[Buffer.from([37, 80, 68, 70]), "application/pdf"],
|
|
29541
|
-
[Buffer.from([137, 80, 78, 71]), "image/png"],
|
|
29542
|
-
[Buffer.from([255, 216, 255]), "image/jpeg"],
|
|
29543
|
-
[Buffer.from([71, 73, 70, 56]), "image/gif"],
|
|
29544
|
-
[Buffer.from([82, 73, 70, 70]), "audio/wav"],
|
|
29545
|
-
[Buffer.from([73, 68, 51]), "audio/mpeg"],
|
|
29546
|
-
[Buffer.from([255, 251]), "audio/mpeg"],
|
|
29547
|
-
[Buffer.from([80, 75, 3, 4]), "application/zip"]
|
|
29548
|
-
];
|
|
29549
|
-
var EXT_MIME = {
|
|
29550
|
-
".pdf": "application/pdf",
|
|
29551
|
-
".png": "image/png",
|
|
29552
|
-
".jpg": "image/jpeg",
|
|
29553
|
-
".jpeg": "image/jpeg",
|
|
29554
|
-
".gif": "image/gif",
|
|
29555
|
-
".webp": "image/webp",
|
|
29556
|
-
".txt": "text/plain",
|
|
29557
|
-
".md": "text/markdown",
|
|
29558
|
-
".json": "application/json",
|
|
29559
|
-
".wav": "audio/wav",
|
|
29560
|
-
".mp3": "audio/mpeg",
|
|
29561
|
-
".m4a": "audio/mp4",
|
|
29562
|
-
".ogg": "audio/ogg",
|
|
29563
|
-
".zip": "application/zip"
|
|
29564
|
-
};
|
|
29565
|
-
function detectMime(buffer, name) {
|
|
29566
|
-
for (const [sig, mime] of MAGIC) {
|
|
29567
|
-
if (buffer.length >= sig.length && buffer.subarray(0, sig.length).equals(sig)) {
|
|
29568
|
-
return mime;
|
|
29569
|
-
}
|
|
29570
|
-
}
|
|
29571
|
-
const ext = extname(name).toLowerCase();
|
|
29572
|
-
return EXT_MIME[ext] ?? "application/octet-stream";
|
|
29573
|
-
}
|
|
29574
|
-
|
|
29575
|
-
// src/commands/drive.ts
|
|
29576
29808
|
function fmtBytes(n) {
|
|
29577
29809
|
if (!n || n <= 0)
|
|
29578
29810
|
return "-";
|
|
@@ -31806,6 +32038,7 @@ var program2 = new Command;
|
|
|
31806
32038
|
program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace services").version(PKG_VERSION);
|
|
31807
32039
|
var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
|
|
31808
32040
|
var create2 = program2.command("create").description("Create Studio — generate images, video, speech, music, and SFX (charged on success)");
|
|
32041
|
+
var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send");
|
|
31809
32042
|
var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
|
|
31810
32043
|
var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
|
|
31811
32044
|
var drive = program2.command("drive").alias("docs").description("Manage twin drive — upload/download/search files, organise into folders");
|
|
@@ -31820,6 +32053,7 @@ registerCreditsCommand(program2);
|
|
|
31820
32053
|
registerSearchCommand(markets);
|
|
31821
32054
|
registerImageCommand(markets);
|
|
31822
32055
|
registerCreateCommand(create2);
|
|
32056
|
+
registerChatCommand(chat);
|
|
31823
32057
|
registerAmazonCommand(markets);
|
|
31824
32058
|
registerWalletsCreateCommand(wallets);
|
|
31825
32059
|
registerWalletsListCommand(wallets);
|
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). DMs
|
|
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 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.
|
|
4
4
|
homepage: https://platform.heyamiko.com
|
|
5
5
|
metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["node"]}}}
|
|
6
6
|
---
|
|
@@ -41,13 +41,13 @@ The CLI is installed globally and is pre-authenticated when you're inside your w
|
|
|
41
41
|
|
|
42
42
|
## Command groups
|
|
43
43
|
|
|
44
|
-
Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `create` (Create Studio — async media generation, charged on success), `wallets`, `credits`, `twin`, `drive` (files / folders / RAG; `docs` is an alias), `voice`, `avatar`, `friends`, `users`, `post`, `review`, `feed`, `notifications`, `memory`, plus the top-level `accounts`, `info`, `config`, `update`. All twin-scoped commands accept `--twin <id>`. Most commands support `--json`.
|
|
44
|
+
Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `create` (Create Studio — async media generation, charged on success), `chat` (owner's DMs + group chats), `wallets`, `credits`, `twin`, `drive` (files / folders / RAG; `docs` is an alias), `voice`, `avatar`, `friends`, `users`, `post`, `review`, `feed`, `notifications`, `memory`, plus the top-level `accounts`, `info`, `config`, `update`. All twin-scoped commands accept `--twin <id>`. Most commands support `--json`.
|
|
45
45
|
|
|
46
|
-
> **
|
|
46
|
+
> **Two chat surfaces — pick by identity.** `amiko chat` is the **owner's** DMs and group chats, acting **as the owner** (list / read / send). The openhermit gateway's `session_list` / `session_send` are the **agent's own** sessions, acting **as the agent**. "Send a message to Sophie for me" → `amiko chat send`; "have the twin reply as itself" → gateway. (The old `amiko conversation` namespace was removed in 0.10.1-beta.4; `amiko chat` is its owner-identity replacement.)
|
|
47
47
|
|
|
48
48
|
## Critical Rules
|
|
49
49
|
|
|
50
|
-
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` (and destructive ops like `twin update --public`, `drive delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `review reject`) unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.**
|
|
50
|
+
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `create *`, `chat send`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` (and destructive ops like `twin update --public`, `drive delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `review reject`) unless `--yes` is passed. The refusal prints the cost and the re-run command. **Quote the cost, get explicit approval, THEN append `--yes`.**
|
|
51
51
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
|
|
52
52
|
3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
|
|
53
53
|
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`.
|
|
@@ -96,6 +96,16 @@ All paid markets commands auto-select the twin's active Solana wallet (see `--wa
|
|
|
96
96
|
|
|
97
97
|
`amiko create <image|video|tts|music|sfx>` is the CLI half of the platform Create Studio. It generates through Amiko's own authenticated endpoints (not raw MPP), runs **async**, and is **charged on success** from the twin's custodial wallet — a failed or timed-out generation is **never billed**, and there is no pre-pay. **The command returns immediately** with a `jobId` and `status: PENDING` — it does NOT block for the whole generation (so you're not held for 60s–9min). It prints how to check + a rough ETA. **Poll for the result** with `amiko create status <jobId>` (re-query that job, ~24h) — wait roughly: image ~15–60s, video ~2–9min, music ~30–120s, tts/sfx ~5–20s. Or `amiko create media` (list recent generations; `--service`/`--limit`/`--raw`). Result is a permanent Supabase Storage URL. (Pass `--wait` only if you deliberately want the command to block until done.) `--token <AMIKO|USDC|USDT|SOL>` selects the charge token (default auto, AMIKO-first). Prefer `create` over `markets image` for media generation — it doesn't lose money on timeouts. `markets image` remains the raw MPP pre-pay path.
|
|
98
98
|
|
|
99
|
+
## Chat — behavior notes
|
|
100
|
+
|
|
101
|
+
`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).
|
|
102
|
+
|
|
103
|
+
- `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
|
|
104
|
+
- `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), an `@handle`, or a name.
|
|
105
|
+
- `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). For **group chats**, use the conversation id from `list`; for a **DM**, an `@handle`/name resolves + finds-or-creates the DM.
|
|
106
|
+
- **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.
|
|
107
|
+
- Name resolution goes through user search; if ambiguous it lists candidates — don't blind-send. Server enforces who you're allowed to message (friend/participant rules); surface its error, don't retry.
|
|
108
|
+
|
|
99
109
|
## Drive (files & folders)
|
|
100
110
|
|
|
101
111
|
The drive is **shared between the user and the agent** — anything either side uploads is visible to both. Uploads kick off an async RAG-parsing job (~seconds to minutes); list/search/rename/move/delete all work regardless of parsing status. `drive search` and `drive list --search` hit filename, title, description, and the parsed content body (once parsing completes). Prefer reading parsed content via the agent workspace sync over polling `drive status` from long-running tasks. `amiko docs` is kept as an alias for backward compat.
|
|
@@ -133,7 +143,7 @@ Workflow: start with `friends matches --limit 20 --json`; narrow with `--dimensi
|
|
|
133
143
|
|
|
134
144
|
> **Default platform = Amiko.** When the owner asks about notifications **without naming a platform**, assume Amiko and answer with `amiko notifications`. Only ask if they explicitly mention a non-Amiko channel.
|
|
135
145
|
>
|
|
136
|
-
> **DMs /
|
|
146
|
+
> **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).
|
|
137
147
|
|
|
138
148
|
Platform notifications cover friend requests, mentions, system alerts, and post-related events.
|
|
139
149
|
|