@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 +4 -1
- package/dist/index.js +175 -103
- package/package.json +1 -1
- package/skills/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @heyamiko/amiko-cli (v0.12.0-beta.
|
|
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
|
-
|
|
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
|
-
|
|
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
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)
|