@heyamiko/amiko-cli 0.12.0-beta.4 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- # @heyamiko/amiko-cli (v0.12.0-beta.4)
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
 
@@ -221,10 +221,13 @@ amiko chat read @sophie # recent messages with a user (
221
221
  amiko chat read <conversationId> --limit 40 # by conversation id (from `chat list`) — works for groups too
222
222
  amiko chat send @sophie "on my way!" --yes # send as the owner
223
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`)
224
226
  ```
225
227
 
226
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).
227
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.)
228
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.
229
232
 
230
233
  ## Amazon
package/dist/index.js CHANGED
@@ -28924,6 +28924,103 @@ 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
+
28927
29024
  // src/commands/chat.ts
28928
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;
28929
29026
  function authOrExit() {
@@ -28980,6 +29077,52 @@ async function resolveConversation(auth, target) {
28980
29077
  }
28981
29078
  return findOrCreateDm(auth, users[0].id);
28982
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
+ }
28983
29126
  function registerChatCommand(chat) {
28984
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) => {
28985
29128
  const auth = authOrExit();
@@ -29054,8 +29197,12 @@ function registerChatCommand(chat) {
29054
29197
  console.log(dim(` ${when}`));
29055
29198
  }
29056
29199
  });
29057
- chat.command("send <target> <message>").description("Send a message as yourself (the owner) to a conversation (id, @handle, or name)").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--raw", "Output raw JSON").action(async (target, message, opts) => {
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) => {
29058
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
+ }
29059
29206
  let convId;
29060
29207
  try {
29061
29208
  convId = await resolveConversation(auth, target);
@@ -29063,15 +29210,38 @@ function registerChatCommand(chat) {
29063
29210
  console.error(error(e5 instanceof Error ? e5.message : "Could not resolve target"));
29064
29211
  process.exit(1);
29065
29212
  }
29213
+ const media = opts.image ? `image ${opts.image}` : opts.audio ? `audio ${opts.audio}` : "";
29066
29214
  await requireApproval({
29067
29215
  cost: "no charge — sends a message as you (the owner)",
29068
- summary: `Send to ${target}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
29216
+ summary: `Send to ${target}${media ? ` [+${media}]` : ""}: ${message.slice(0, 80)}${message.length > 80 ? "…" : ""}`,
29069
29217
  yes: opts.yes,
29070
29218
  commandExample: `amiko chat send ${JSON.stringify(target)} ${JSON.stringify(message)}`
29071
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
+ });
29072
29226
  let data;
29073
29227
  try {
29074
- data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(convId)}/messages`, { method: "POST", body: { content: message }, timeoutMs: 20000 });
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
+ }
29075
29245
  } catch (e5) {
29076
29246
  console.error(error(e5 instanceof Error ? e5.message : "Send failed"));
29077
29247
  process.exit(1);
@@ -29080,7 +29250,8 @@ function registerChatCommand(chat) {
29080
29250
  console.log(JSON.stringify(data, null, 2));
29081
29251
  return;
29082
29252
  }
29083
- console.log(success("Message sent") + dim(` (conversation ${convId.slice(0, 8)}…)`));
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)}…)`));
29084
29255
  });
29085
29256
  }
29086
29257
 
@@ -29634,105 +29805,6 @@ function registerTwinCommand(program2) {
29634
29805
  // src/commands/drive.ts
29635
29806
  import { writeFile } from "node:fs/promises";
29636
29807
  import { basename as basename2, resolve as resolvePath } from "node:path";
29637
-
29638
- // src/lib/file-input.ts
29639
- import { readFile } from "node:fs/promises";
29640
- import { basename, extname } from "node:path";
29641
- var MAX_BYTES = 100 * 1024 * 1024;
29642
- var FETCH_TIMEOUT_MS = 30000;
29643
- async function resolveFileInput(src, opts = {}) {
29644
- if (src === "-" || opts.stdin) {
29645
- const buffer2 = await readStdin();
29646
- const name2 = opts.hintName ?? "stdin";
29647
- const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
29648
- return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
29649
- }
29650
- if (/^https?:\/\//i.test(src)) {
29651
- const buffer2 = await fetchRemote(src);
29652
- const urlPath = new URL(src).pathname;
29653
- const name2 = opts.hintName ?? (basename(urlPath) || "download");
29654
- const mime2 = opts.hintMime ?? detectMime(buffer2, name2);
29655
- return { buffer: buffer2, name: name2, mime: mime2, size: buffer2.length };
29656
- }
29657
- const buffer = await readFile(src);
29658
- if (buffer.length > MAX_BYTES) {
29659
- throw new Error(`File too large: ${buffer.length} bytes exceeds ${MAX_BYTES} byte cap.`);
29660
- }
29661
- const name = opts.hintName ?? basename(src);
29662
- const mime = opts.hintMime ?? detectMime(buffer, name);
29663
- return { buffer, name, mime, size: buffer.length };
29664
- }
29665
- async function readStdin() {
29666
- const chunks = [];
29667
- let total = 0;
29668
- for await (const chunk of process.stdin) {
29669
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
29670
- total += buf.length;
29671
- if (total > MAX_BYTES) {
29672
- throw new Error(`stdin exceeded ${MAX_BYTES} byte cap.`);
29673
- }
29674
- chunks.push(buf);
29675
- }
29676
- return Buffer.concat(chunks);
29677
- }
29678
- async function fetchRemote(url2) {
29679
- const controller = new AbortController;
29680
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
29681
- try {
29682
- const res = await fetch(url2, { signal: controller.signal });
29683
- if (!res.ok) {
29684
- throw new Error(`Fetch ${url2} failed: ${res.status} ${res.statusText}`);
29685
- }
29686
- const len = Number(res.headers.get("content-length") || 0);
29687
- if (len > MAX_BYTES) {
29688
- throw new Error(`Remote file too large: ${len} bytes exceeds ${MAX_BYTES} byte cap.`);
29689
- }
29690
- const buf = Buffer.from(await res.arrayBuffer());
29691
- if (buf.length > MAX_BYTES) {
29692
- throw new Error(`Remote file too large: ${buf.length} bytes exceeds ${MAX_BYTES} byte cap.`);
29693
- }
29694
- return buf;
29695
- } finally {
29696
- clearTimeout(timer);
29697
- }
29698
- }
29699
- var MAGIC = [
29700
- [Buffer.from([37, 80, 68, 70]), "application/pdf"],
29701
- [Buffer.from([137, 80, 78, 71]), "image/png"],
29702
- [Buffer.from([255, 216, 255]), "image/jpeg"],
29703
- [Buffer.from([71, 73, 70, 56]), "image/gif"],
29704
- [Buffer.from([82, 73, 70, 70]), "audio/wav"],
29705
- [Buffer.from([73, 68, 51]), "audio/mpeg"],
29706
- [Buffer.from([255, 251]), "audio/mpeg"],
29707
- [Buffer.from([80, 75, 3, 4]), "application/zip"]
29708
- ];
29709
- var EXT_MIME = {
29710
- ".pdf": "application/pdf",
29711
- ".png": "image/png",
29712
- ".jpg": "image/jpeg",
29713
- ".jpeg": "image/jpeg",
29714
- ".gif": "image/gif",
29715
- ".webp": "image/webp",
29716
- ".txt": "text/plain",
29717
- ".md": "text/markdown",
29718
- ".json": "application/json",
29719
- ".wav": "audio/wav",
29720
- ".mp3": "audio/mpeg",
29721
- ".m4a": "audio/mp4",
29722
- ".ogg": "audio/ogg",
29723
- ".zip": "application/zip"
29724
- };
29725
- function detectMime(buffer, name) {
29726
- for (const [sig, mime] of MAGIC) {
29727
- if (buffer.length >= sig.length && buffer.subarray(0, sig.length).equals(sig)) {
29728
- return mime;
29729
- }
29730
- }
29731
- const ext = extname(name).toLowerCase();
29732
- return EXT_MIME[ext] ?? "application/octet-stream";
29733
- }
29734
-
29735
- // src/commands/drive.ts
29736
29808
  function fmtBytes(n) {
29737
29809
  if (!n || n <= 0)
29738
29810
  return "-";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.12.0-beta.4",
3
+ "version": "0.12.0-beta.5",
4
4
  "description": "Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace agents",
5
5
  "type": "module",
6
6
  "bin": {
package/skills/SKILL.md CHANGED
@@ -103,6 +103,7 @@ All paid markets commands auto-select the twin's active Solana wallet (see `--wa
103
103
  - `amiko chat list` — all conversations, **DMs and group chats** (id, type, peer/title, last message, unread).
104
104
  - `amiko chat read <target>` — recent messages. `<target>` = a conversation id (from `list`), an `@handle`, or a name.
105
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.
106
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.
107
108
 
108
109
  ## Drive (files & folders)