@heyamiko/amiko-cli 0.12.0-beta.4 → 0.12.0-beta.6
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 +24 -1
- package/dist/index.js +391 -105
- package/package.json +1 -1
- package/skills/SKILL.md +12 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @heyamiko/amiko-cli (v0.12.0-beta.
|
|
1
|
+
# @heyamiko/amiko-cli (v0.12.0-beta.6)
|
|
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,12 +221,35 @@ 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
|
|
|
233
|
+
## Twin Cards
|
|
234
|
+
|
|
235
|
+
Generate, view, download, and mint your **Twin Card** (template `twins_take`, flavors **work / play / love**).
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
amiko card templates # available flavors (DB-managed)
|
|
239
|
+
amiko card create --flavor work # generate → prints slug + share link
|
|
240
|
+
amiko card create --flavor love --reroll 1 # a fresh take
|
|
241
|
+
amiko card list # your latest card
|
|
242
|
+
amiko card show <slug> # stored card data (JSON)
|
|
243
|
+
amiko card image <slug> --flavor work --out card.png # download the PNG
|
|
244
|
+
amiko card share <slug> # print https://platform.heyamiko.com/s/<slug>
|
|
245
|
+
amiko card mint <slug> --flavor work --yes # mint as an NFT (gated on --yes)
|
|
246
|
+
amiko card mint <slug> --status # check an existing mint
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- Flavor defaults to `love`; flavors are DB-managed — `card templates` lists the valid ones.
|
|
250
|
+
- `create` errors are user-fixable and exit with **code 2** (not a crash): `profile_not_ready`, `guardrails_failed`, `rate_limited` (reroll cooldown).
|
|
251
|
+
- `card mint` creates an on-chain NFT → **gated on `--yes`**. All subcommands support `--json`.
|
|
252
|
+
|
|
230
253
|
## Amazon
|
|
231
254
|
|
|
232
255
|
```bash
|
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,220 @@ 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)}…)`));
|
|
29255
|
+
});
|
|
29256
|
+
}
|
|
29257
|
+
|
|
29258
|
+
// src/commands/card.ts
|
|
29259
|
+
import { writeFile } from "node:fs/promises";
|
|
29260
|
+
var TEMPLATE_ID = "twins_take";
|
|
29261
|
+
var DEFAULT_FLAVOR = "love";
|
|
29262
|
+
function authOrExit2() {
|
|
29263
|
+
try {
|
|
29264
|
+
return requireAuth();
|
|
29265
|
+
} catch (e5) {
|
|
29266
|
+
console.error(error(e5 instanceof Error ? e5.message : "auth failed"));
|
|
29267
|
+
process.exit(1);
|
|
29268
|
+
}
|
|
29269
|
+
}
|
|
29270
|
+
function platformBase(auth) {
|
|
29271
|
+
return (auth.platformUrl ?? "https://platform.heyamiko.com").replace(/\/$/, "");
|
|
29272
|
+
}
|
|
29273
|
+
function cardErrorExit(e5) {
|
|
29274
|
+
const err = e5;
|
|
29275
|
+
let body = {};
|
|
29276
|
+
try {
|
|
29277
|
+
body = JSON.parse(err?.body ?? "{}");
|
|
29278
|
+
} catch {}
|
|
29279
|
+
const code = body.error ?? "";
|
|
29280
|
+
const map2 = {
|
|
29281
|
+
profile_not_ready: {
|
|
29282
|
+
msg: "Your Amiko profile isn't complete enough to generate a card yet — flesh out your profile first.",
|
|
29283
|
+
exit: 2
|
|
29284
|
+
},
|
|
29285
|
+
guardrails_failed: {
|
|
29286
|
+
msg: "Content guardrails didn't pass — try again, or a different --flavor.",
|
|
29287
|
+
exit: 2
|
|
29288
|
+
},
|
|
29289
|
+
rate_limited: {
|
|
29290
|
+
msg: `Card generation is rate-limited right now — try again later${body.retry_after_seconds ? ` (~${body.retry_after_seconds}s)` : ""}.`,
|
|
29291
|
+
exit: 2
|
|
29292
|
+
},
|
|
29293
|
+
unknown_flavor: {
|
|
29294
|
+
msg: "Unknown flavor. Run `amiko card templates` to see the valid flavors.",
|
|
29295
|
+
exit: 1
|
|
29296
|
+
},
|
|
29297
|
+
unknown_template: { msg: "Only the twins_take card can be generated.", exit: 1 },
|
|
29298
|
+
template_not_generatable: {
|
|
29299
|
+
msg: "Only the twins_take card can be generated.",
|
|
29300
|
+
exit: 1
|
|
29301
|
+
}
|
|
29302
|
+
};
|
|
29303
|
+
const hit = map2[code];
|
|
29304
|
+
if (hit) {
|
|
29305
|
+
console.error(error(hit.msg));
|
|
29306
|
+
process.exit(hit.exit);
|
|
29307
|
+
}
|
|
29308
|
+
console.error(error(e5 instanceof Error ? e5.message : "Card request failed"));
|
|
29309
|
+
process.exit(1);
|
|
29310
|
+
}
|
|
29311
|
+
function registerCardCommand(card) {
|
|
29312
|
+
card.command("templates").description("List the available card flavors (work / play / love)").option("--json", "Output raw JSON").action(async (opts) => {
|
|
29313
|
+
const auth = authOrExit2();
|
|
29314
|
+
let data;
|
|
29315
|
+
try {
|
|
29316
|
+
data = await amikoWebFetch(auth, "/api/twin-cards/templates", {
|
|
29317
|
+
timeoutMs: 15000
|
|
29318
|
+
});
|
|
29319
|
+
} catch (e5) {
|
|
29320
|
+
cardErrorExit(e5);
|
|
29321
|
+
}
|
|
29322
|
+
if (opts.json) {
|
|
29323
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29324
|
+
return;
|
|
29325
|
+
}
|
|
29326
|
+
const flavors = data.templates ?? [];
|
|
29327
|
+
if (flavors.length === 0) {
|
|
29328
|
+
console.log(dim("No flavors available."));
|
|
29329
|
+
return;
|
|
29330
|
+
}
|
|
29331
|
+
console.log(heading(`Card flavors (${flavors.length})
|
|
29332
|
+
`));
|
|
29333
|
+
for (const f of flavors)
|
|
29334
|
+
console.log(label(f.key, f.name ?? ""));
|
|
29335
|
+
});
|
|
29336
|
+
card.command("list").description("Show your most recently generated card").option("--json", "Output raw JSON").action(async (opts) => {
|
|
29337
|
+
const auth = authOrExit2();
|
|
29338
|
+
let data;
|
|
29339
|
+
try {
|
|
29340
|
+
data = await amikoWebFetch(auth, "/api/twin-cards/me", {
|
|
29341
|
+
timeoutMs: 15000
|
|
29342
|
+
});
|
|
29343
|
+
} catch (e5) {
|
|
29344
|
+
cardErrorExit(e5);
|
|
29345
|
+
}
|
|
29346
|
+
if (opts.json) {
|
|
29347
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29348
|
+
return;
|
|
29349
|
+
}
|
|
29350
|
+
if (!data.slug) {
|
|
29351
|
+
console.log(dim("No card yet — generate one with `amiko card create`."));
|
|
29352
|
+
return;
|
|
29353
|
+
}
|
|
29354
|
+
console.log(heading(`Your latest card
|
|
29355
|
+
`));
|
|
29356
|
+
console.log(label("Slug", data.slug));
|
|
29357
|
+
console.log(label("Flavors", Object.keys(data.cards ?? {}).join(", ") || "-"));
|
|
29358
|
+
if (data.generated_at)
|
|
29359
|
+
console.log(label("Generated", data.generated_at));
|
|
29360
|
+
console.log(label("Share", `${platformBase(auth)}/s/${data.slug}`));
|
|
29361
|
+
});
|
|
29362
|
+
card.command("create").description("Generate (or reroll) your Twin Card").option("--flavor <flavor>", "Flavor: work, play, love (DB-managed; see `card templates`)", DEFAULT_FLAVOR).option("--reroll <n>", "Reroll number (≥0) for a fresh take", "0").option("--json", "Output raw JSON").action(async (opts) => {
|
|
29363
|
+
const auth = authOrExit2();
|
|
29364
|
+
let data;
|
|
29365
|
+
try {
|
|
29366
|
+
data = await amikoWebFetch(auth, "/api/share/generate-card", {
|
|
29367
|
+
method: "POST",
|
|
29368
|
+
body: {
|
|
29369
|
+
template_id: TEMPLATE_ID,
|
|
29370
|
+
flavor: opts.flavor,
|
|
29371
|
+
reroll_number: Math.max(0, Math.floor(Number(opts.reroll) || 0))
|
|
29372
|
+
},
|
|
29373
|
+
timeoutMs: 120000
|
|
29374
|
+
});
|
|
29375
|
+
} catch (e5) {
|
|
29376
|
+
cardErrorExit(e5);
|
|
29377
|
+
}
|
|
29378
|
+
if (opts.json) {
|
|
29379
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29380
|
+
return;
|
|
29381
|
+
}
|
|
29382
|
+
console.log(heading(`${opts.flavor} card ready
|
|
29383
|
+
`));
|
|
29384
|
+
if (data.slug) {
|
|
29385
|
+
console.log(label("Slug", data.slug));
|
|
29386
|
+
console.log(label("Share", `${platformBase(auth)}/s/${data.slug}`));
|
|
29387
|
+
console.log(dim(`Image: amiko card image ${data.slug} --flavor ${opts.flavor}`));
|
|
29388
|
+
}
|
|
29389
|
+
});
|
|
29390
|
+
card.command("show <slug>").description("Read a card's stored data").option("--json", "Output raw JSON").action(async (slug, opts) => {
|
|
29391
|
+
const auth = authOrExit2();
|
|
29392
|
+
let data;
|
|
29393
|
+
try {
|
|
29394
|
+
data = await amikoWebFetch(auth, `/api/share/${encodeURIComponent(slug)}`, { timeoutMs: 15000 });
|
|
29395
|
+
} catch (e5) {
|
|
29396
|
+
cardErrorExit(e5);
|
|
29397
|
+
}
|
|
29398
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29399
|
+
});
|
|
29400
|
+
card.command("image <slug>").description("Download the card image (PNG)").option("--flavor <flavor>", "Flavor: work, play, love", DEFAULT_FLAVOR).option("--out <path>", "Output file (default: card-<slug>-<flavor>.png)").action(async (slug, opts) => {
|
|
29401
|
+
const auth = authOrExit2();
|
|
29402
|
+
const url2 = `${platformBase(auth)}/api/share/${encodeURIComponent(slug)}/image?flavor=${encodeURIComponent(opts.flavor)}`;
|
|
29403
|
+
let buf;
|
|
29404
|
+
try {
|
|
29405
|
+
const res = await fetch(url2, {
|
|
29406
|
+
headers: { Authorization: `Bearer ${auth.twinToken ?? ""}` },
|
|
29407
|
+
signal: AbortSignal.timeout(60000)
|
|
29408
|
+
});
|
|
29409
|
+
if (!res.ok) {
|
|
29410
|
+
console.error(error(`Failed to fetch card image: ${res.status} ${res.statusText}`));
|
|
29411
|
+
process.exit(1);
|
|
29412
|
+
}
|
|
29413
|
+
buf = Buffer.from(await res.arrayBuffer());
|
|
29414
|
+
} catch (e5) {
|
|
29415
|
+
console.error(error(e5 instanceof Error ? e5.message : "Image download failed"));
|
|
29416
|
+
process.exit(1);
|
|
29417
|
+
}
|
|
29418
|
+
const out = opts.out ?? `card-${slug}-${opts.flavor}.png`;
|
|
29419
|
+
await writeFile(out, buf);
|
|
29420
|
+
console.log(success(`Saved ${buf.length.toLocaleString()} bytes → ${out}`));
|
|
29421
|
+
});
|
|
29422
|
+
card.command("mint <slug>").description("Mint a card as an NFT (or --status to check an existing mint)").option("--flavor <flavor>", "Flavor: work, play, love", DEFAULT_FLAVOR).option("--name <name>", "NFT name (optional)").option("--description <desc>", "NFT description (optional)").option("--status", "Check mint status instead of minting").option("--yes", "Skip the confirmation (required in non-interactive shells)").option("--json", "Output raw JSON").action(async (slug, opts) => {
|
|
29423
|
+
const auth = authOrExit2();
|
|
29424
|
+
if (opts.status) {
|
|
29425
|
+
let data2;
|
|
29426
|
+
try {
|
|
29427
|
+
data2 = await amikoWebFetch(auth, `/api/share/twin-cards/mint?slug=${encodeURIComponent(slug)}`, { timeoutMs: 15000 });
|
|
29428
|
+
} catch (e5) {
|
|
29429
|
+
cardErrorExit(e5);
|
|
29430
|
+
}
|
|
29431
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
29432
|
+
return;
|
|
29433
|
+
}
|
|
29434
|
+
await requireApproval({
|
|
29435
|
+
cost: "mints an on-chain NFT (cNFT) from your twin card",
|
|
29436
|
+
summary: `Mint ${opts.flavor} card ${slug} as an NFT`,
|
|
29437
|
+
yes: opts.yes,
|
|
29438
|
+
commandExample: `amiko card mint ${slug} --flavor ${opts.flavor}`
|
|
29439
|
+
});
|
|
29440
|
+
let data;
|
|
29441
|
+
try {
|
|
29442
|
+
data = await amikoWebFetch(auth, "/api/share/twin-cards/mint", {
|
|
29443
|
+
method: "POST",
|
|
29444
|
+
body: {
|
|
29445
|
+
slug,
|
|
29446
|
+
flavor: opts.flavor,
|
|
29447
|
+
...opts.name ? { name: opts.name } : {},
|
|
29448
|
+
...opts.description ? { description: opts.description } : {}
|
|
29449
|
+
},
|
|
29450
|
+
timeoutMs: 120000
|
|
29451
|
+
});
|
|
29452
|
+
} catch (e5) {
|
|
29453
|
+
cardErrorExit(e5);
|
|
29454
|
+
}
|
|
29455
|
+
if (opts.json) {
|
|
29456
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29457
|
+
return;
|
|
29458
|
+
}
|
|
29459
|
+
console.log(success(`Mint ${data.status ?? "submitted"}`));
|
|
29460
|
+
if (data.mint_address)
|
|
29461
|
+
console.log(label("Mint", data.mint_address));
|
|
29462
|
+
console.log(dim(`Check status: amiko card mint ${slug} --status`));
|
|
29463
|
+
});
|
|
29464
|
+
card.command("share <slug>").description("Print the public share link for a card").action(async (slug) => {
|
|
29465
|
+
const auth = authOrExit2();
|
|
29466
|
+
console.log(`${platformBase(auth)}/s/${slug}`);
|
|
29084
29467
|
});
|
|
29085
29468
|
}
|
|
29086
29469
|
|
|
@@ -29632,107 +30015,8 @@ function registerTwinCommand(program2) {
|
|
|
29632
30015
|
}
|
|
29633
30016
|
|
|
29634
30017
|
// src/commands/drive.ts
|
|
29635
|
-
import { writeFile } from "node:fs/promises";
|
|
30018
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
29636
30019
|
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
30020
|
function fmtBytes(n) {
|
|
29737
30021
|
if (!n || n <= 0)
|
|
29738
30022
|
return "-";
|
|
@@ -29944,7 +30228,7 @@ Search hits filename, title, description, and parsed RAG content (once processed
|
|
|
29944
30228
|
return;
|
|
29945
30229
|
}
|
|
29946
30230
|
const outPath = resolvePath(opts.out ?? basename2(meta3.doc.filename));
|
|
29947
|
-
await
|
|
30231
|
+
await writeFile2(outPath, buf);
|
|
29948
30232
|
spinner?.stop();
|
|
29949
30233
|
console.log(success(`Saved ${meta3.doc.filename} → ${outPath}`));
|
|
29950
30234
|
console.log(label("Size", fmtBytes(buf.length)));
|
|
@@ -31967,6 +32251,7 @@ program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, b
|
|
|
31967
32251
|
var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
|
|
31968
32252
|
var create2 = program2.command("create").description("Create Studio — generate images, video, speech, music, and SFX (charged on success)");
|
|
31969
32253
|
var chat = program2.command("chat").description("Your conversations (DMs + group chats) as the owner — list, read, send");
|
|
32254
|
+
var card = program2.command("card").description("Twin Cards (work/play/love) — generate, view, download, mint");
|
|
31970
32255
|
var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
|
|
31971
32256
|
var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
|
|
31972
32257
|
var drive = program2.command("drive").alias("docs").description("Manage twin drive — upload/download/search files, organise into folders");
|
|
@@ -31982,6 +32267,7 @@ registerSearchCommand(markets);
|
|
|
31982
32267
|
registerImageCommand(markets);
|
|
31983
32268
|
registerCreateCommand(create2);
|
|
31984
32269
|
registerChatCommand(chat);
|
|
32270
|
+
registerCardCommand(card);
|
|
31985
32271
|
registerAmazonCommand(markets);
|
|
31986
32272
|
registerWalletsCreateCommand(wallets);
|
|
31987
32273
|
registerWalletsListCommand(wallets);
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -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), `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`.
|
|
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), `card` (Twin Cards — work/play/love), `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 *`, `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`.**
|
|
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`, `card mint`, `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`.
|
|
@@ -103,8 +103,18 @@ 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
|
|
|
109
|
+
## Card — behavior notes
|
|
110
|
+
|
|
111
|
+
`amiko card` = the owner's **Twin Card** (shareable card, template `twins_take`, flavors **work / play / love** — DB-managed, discover with `card templates`).
|
|
112
|
+
|
|
113
|
+
- `card create --flavor <work|play|love> [--reroll <n>]` — generate/reroll; returns a slug + share link (`/s/<slug>`). Flavor defaults to `love`. Errors are user-fixable and exit **non-zero code 2** (don't treat as a crash): `profile_not_ready` (complete the profile first), `guardrails_failed` (retry / other flavor), `rate_limited` (reroll cooldown — try later).
|
|
114
|
+
- `card list` — your latest card; `card show <slug>` — its data; `card image <slug> --flavor <f> [--out <path>]` — download the PNG; `card share <slug>` — print the public link.
|
|
115
|
+
- `card mint <slug> --flavor <f>` — mint as an on-chain NFT → **`--yes` gated**. `card mint <slug> --status` checks an existing mint.
|
|
116
|
+
- All `--json`. Auth is the injected twin token (same as `post`/`create`); no login.
|
|
117
|
+
|
|
108
118
|
## Drive (files & folders)
|
|
109
119
|
|
|
110
120
|
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.
|