@heyamiko/amiko-cli 0.10.1-beta.0 → 0.10.1-beta.3

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.10.1-beta.0)
1
+ # @heyamiko/amiko-cli (v0.10.1-beta.3)
2
2
 
3
3
  Manage wallets, credits, swaps, MPP marketplace services, and your Amiko twin (identity, documents, voice, avatar, friends, feed, Composio apps) from the terminal. Works for both human users and AI agents running on OpenClaw.
4
4
 
@@ -393,6 +393,21 @@ npm publish
393
393
 
394
394
  ## Changelog
395
395
 
396
+ ### 0.10.1-beta.3
397
+
398
+ - **`amiko drive` namespace** replaces `amiko docs` (kept as a `.alias("docs")` so existing scripts and agents keep working). Tracks the [Amiko Drive API](https://github.com/HCF-STUDIOS/amiko-platform/blob/main/amiko-web/docs/api/drive.md): files live under twin-scoped folders, every upload kicks off async RAG parsing, and search hits parsed content once processing completes. Commands:
399
+ - **Files** — `drive list [--folder <id|root>] [--search <q>] [--limit] [--offset]`, `drive search <query> [--folder]`, `drive upload <source> [--folder] [--title] [--description] [--doc-type]`, `drive download <docId> [--out <path>|--stdout]` (NEW — fetches a 1h signed URL from `GET /docs/[docId]` and writes the bytes), `drive get <docId> [--content]` (NEW — metadata + signed URL, optional parsed content body), `drive rename <docId> --title <new>` (NEW — `PATCH /docs/[docId]`), `drive move <docId> --folder <id|root>` (NEW), `drive delete <docId> --yes`, `drive presign <name>`, `drive status <docId...>` (NEW — `POST /docs/check-processing` for RAG job state).
400
+ - **Folders** — `drive folder list`, `drive folder create <name> [--parent <id>]`, `drive folder rename <id> --name <new>`, `drive folder move <id> --parent <id|root>`, `drive folder delete <id> [--force] --yes` (force=1 walks the subtree).
401
+ - SKILL.md picks up a one-paragraph Drive section + three new rows in the Examples table (upload / download / search intents → exact `amiko drive` commands).
402
+
403
+ ### 0.10.1-beta.2
404
+
405
+ - **SKILL.md slimmed ~55%** (31.6 KB / 429 lines → 14.3 KB / 169 lines; ~8.5k → ~3.5k tokens). The skill now leads with "this is the mental model, not a syntax reference — run `amiko <group> --help` for exact flags" and drops per-namespace bash blocks that duplicated `--help` output (credits/wallets/markets/account/docs/voice/avatar/friends/users/feed/posts/conversations/notifications/memory/composio/JSON). Kept everything the CLI's own help *can't* tell an agent: shell-tool invocation rule + "no `amiko_cli` tool exists" failure modes, the `--yes` / report-balance / no-retry critical rules, `--wallet` default behavior, the credits intent-mapping cheatsheet (1 SOL → `--spend`, $5 → `--usd`, 10000 → positional), wallets/markets/conversations behavior notes that aren't in help, the "relationship report" and "who should I meet" playbooks (`friends find` vs `users search` vs `friends matches`), feed/post draft-review workflow + "reading via CLI counts as reading", DM-vs-built-in-sessions routing, and the full memory `search-before-you-answer` doctrine. Pricing table replaced with a one-paragraph "run `markets service list` for live prices" pointer plus rough order-of-magnitude. Result: skill stays well within recommended SKILL.md size budget while preserving every judgment / mental-model line that agents can't learn from `--help`.
406
+
407
+ ### 0.10.1-beta.1
408
+
409
+ - **SKILL.md** documents the two new feed/post surfaces so agents actually find them: top-of-file Examples table gets rows for "any new posts I haven't seen?" → `amiko feed --unread` and "what comments are on my post?" → `amiko post comments --id <postId>`; top-level command index updated; the Feed & Posts section gains a callout that *reading via the CLI counts as reading the post* (every `amiko feed` / `amiko post comments` call auto-marks for this twin, so successive `feed --unread` calls drain without bookkeeping; user-side reads are written by the web client only). No CLI behavior change.
410
+
396
411
  ### 0.10.1-beta.0
397
412
 
398
413
  - **`amiko post comments --id <postId>`** (new): lists comments on a post via `GET /api/posts/[id]/comments`. Supports `--limit`, `--cursor`, `--replies` (include nested replies; default top-level only), `--json`. Closes a gap where the CLI could write a comment but couldn't read existing ones — agents previously only saw a `_count.comments` integer from `amiko feed`.
package/dist/index.js CHANGED
@@ -29139,6 +29139,10 @@ function registerTwinCommand(program2) {
29139
29139
  });
29140
29140
  }
29141
29141
 
29142
+ // src/commands/drive.ts
29143
+ import { writeFile } from "node:fs/promises";
29144
+ import { basename as basename2, resolve as resolvePath } from "node:path";
29145
+
29142
29146
  // src/lib/file-input.ts
29143
29147
  import { readFile } from "node:fs/promises";
29144
29148
  import { basename, extname } from "node:path";
@@ -29236,7 +29240,7 @@ function detectMime(buffer, name) {
29236
29240
  return EXT_MIME[ext] ?? "application/octet-stream";
29237
29241
  }
29238
29242
 
29239
- // src/commands/docs.ts
29243
+ // src/commands/drive.ts
29240
29244
  function fmtBytes(n) {
29241
29245
  if (!n || n <= 0)
29242
29246
  return "-";
@@ -29249,6 +29253,16 @@ function fmtBytes(n) {
29249
29253
  }
29250
29254
  return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
29251
29255
  }
29256
+ function statusOf(doc2) {
29257
+ const meta3 = doc2.metadata?.status;
29258
+ if (meta3)
29259
+ return meta3;
29260
+ if (doc2.is_processed)
29261
+ return "processed";
29262
+ if (doc2.is_parsed)
29263
+ return "parsed";
29264
+ return "pending";
29265
+ }
29252
29266
  function twinFromFlag(opts) {
29253
29267
  const config2 = loadConfig();
29254
29268
  const auth = resolveAuth();
@@ -29262,36 +29276,81 @@ function twinFromFlag(opts) {
29262
29276
  throw e5;
29263
29277
  }
29264
29278
  }
29265
- function registerDocsCommand(program2) {
29266
- program2.command("list").description("List uploaded documents for a twin").option("--twin <idOrName>", "Target twin id or name").option("--limit <n>", "Max results", "50").option("--json", "Output as JSON").action(async (opts) => {
29279
+ function renderDocTable(docs) {
29280
+ const rows = [
29281
+ [
29282
+ dim("ID"),
29283
+ dim("NAME"),
29284
+ dim("TYPE"),
29285
+ dim("FOLDER"),
29286
+ dim("STATUS"),
29287
+ dim("CREATED")
29288
+ ],
29289
+ ...docs.map((doc2) => [
29290
+ doc2.id,
29291
+ doc2.title || doc2.filename,
29292
+ doc2.file_type || "-",
29293
+ doc2.folder_id || dim("root"),
29294
+ statusOf(doc2),
29295
+ doc2.created_at?.slice(0, 10) || "-"
29296
+ ])
29297
+ ];
29298
+ console.log(table(rows));
29299
+ }
29300
+ async function fetchSignedUrl(auth, twinId, docId) {
29301
+ return amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`);
29302
+ }
29303
+ function registerDriveCommand(program2) {
29304
+ program2.command("list").description("List drive files (optionally filter by folder or search)").option("--twin <idOrName>", "Target twin id or name").option("--folder <id|root>", "Filter by folder id, or 'root' for drive root").option("--search <q>", "Case-insensitive search over filename/title/description/content").option("--limit <n>", "Max results per page", "50").option("--offset <n>", "Skip N rows", "0").option("--json", "Output as JSON").action(async (opts) => {
29267
29305
  const { twinId, auth } = twinFromFlag(opts);
29268
- const spinner = opts.json ? null : ora("Loading documents...").start();
29306
+ const spinner = opts.json ? null : ora("Loading files...").start();
29269
29307
  try {
29270
- const data = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, { query: { limit: opts.limit ?? "50" } });
29308
+ const query = {
29309
+ limit: opts.limit ?? "50",
29310
+ offset: opts.offset ?? "0"
29311
+ };
29312
+ if (opts.search)
29313
+ query.search = opts.search;
29314
+ if (opts.folder !== undefined)
29315
+ query.folderId = opts.folder;
29316
+ const data = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, { query });
29271
29317
  spinner?.stop();
29272
29318
  renderOutput(data, (d) => {
29273
29319
  if (!d.docs || d.docs.length === 0) {
29274
- console.log(dim("No documents."));
29320
+ console.log(dim("No files."));
29275
29321
  return;
29276
29322
  }
29277
- console.log(heading(`Documents (${d.total ?? d.docs.length})`));
29278
- const rows = [
29279
- [
29280
- dim("ID"),
29281
- dim("NAME"),
29282
- dim("TYPE"),
29283
- dim("STATUS"),
29284
- dim("CREATED")
29285
- ],
29286
- ...d.docs.map((doc2) => [
29287
- doc2.id,
29288
- doc2.title || doc2.filename,
29289
- doc2.file_type || "-",
29290
- doc2.is_processed ? "processed" : doc2.is_parsed ? "parsed" : "pending",
29291
- doc2.created_at?.slice(0, 10) || "-"
29292
- ])
29293
- ];
29294
- console.log(table(rows));
29323
+ const header = opts.search ? `Files matching "${opts.search}" (${d.total ?? d.docs.length})` : opts.folder ? `Files in ${opts.folder} (${d.total ?? d.docs.length})` : `Files (${d.total ?? d.docs.length})`;
29324
+ console.log(heading(header));
29325
+ renderDocTable(d.docs);
29326
+ }, { json: opts.json });
29327
+ } catch (e5) {
29328
+ spinner?.stop();
29329
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29330
+ process.exit(1);
29331
+ }
29332
+ });
29333
+ program2.command("search <query>").description("Search files by name, description, or parsed content").option("--twin <idOrName>", "Target twin id or name").option("--folder <id|root>", "Restrict search to a folder").option("--limit <n>", "Max results", "50").option("--json", "Output as JSON").action(async (query, opts) => {
29334
+ const { twinId, auth } = twinFromFlag(opts);
29335
+ const spinner = opts.json ? null : ora(`Searching for "${query}"...`).start();
29336
+ try {
29337
+ const q = {
29338
+ search: query,
29339
+ limit: opts.limit ?? "50"
29340
+ };
29341
+ if (opts.folder !== undefined)
29342
+ q.folderId = opts.folder;
29343
+ const data = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, { query: q });
29344
+ spinner?.stop();
29345
+ renderOutput(data, (d) => {
29346
+ if (!d.docs || d.docs.length === 0) {
29347
+ console.log(dim(`No files match "${query}".`));
29348
+ return;
29349
+ }
29350
+ console.log(heading(`Matches for "${query}" (${d.total ?? d.docs.length})`));
29351
+ renderDocTable(d.docs);
29352
+ console.log(dim(`
29353
+ Search hits filename, title, description, and parsed RAG content (once processed).`));
29295
29354
  }, { json: opts.json });
29296
29355
  } catch (e5) {
29297
29356
  spinner?.stop();
@@ -29299,7 +29358,7 @@ function registerDocsCommand(program2) {
29299
29358
  process.exit(1);
29300
29359
  }
29301
29360
  });
29302
- program2.command("upload <source>").description("Upload a document (path | https URL | - for stdin)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--title <title>", "Document title (defaults to filename)").option("--description <text>", "Document description").option("--doc-type <type>", "Document type (e.g. 'other', 'memory')", "other").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
29361
+ program2.command("upload <source>").description("Upload a file (path | https URL | - for stdin)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename").option("--title <title>", "File title (defaults to filename without extension)").option("--description <text>", "File description").option("--doc-type <type>", "Document type: personal, work, academic, financial, communication, health, other", "other").option("--folder <id>", "Target folder id (omit for drive root)").option("--stdin", "Read from stdin (source must be -)").option("--json", "Output as JSON").action(async (source, opts) => {
29303
29362
  const { twinId, auth } = twinFromFlag(opts);
29304
29363
  const spinner = opts.json ? null : ora("Reading input...").start();
29305
29364
  try {
@@ -29321,28 +29380,118 @@ function registerDocsCommand(program2) {
29321
29380
  if (spinner)
29322
29381
  spinner.text = `Registering ${upload.filename}...`;
29323
29382
  const title = opts.title || upload.filename.replace(/\.[^.]+$/, "");
29324
- const doc2 = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, {
29325
- method: "POST",
29326
- body: {
29327
- filename: upload.filename,
29328
- fileUrl: upload.url,
29329
- fileType: upload.fileType,
29330
- title,
29331
- description: opts.description ?? undefined,
29332
- doc_type: opts.docType ?? "other"
29333
- }
29334
- });
29383
+ const body = {
29384
+ filename: upload.filename,
29385
+ fileUrl: upload.url,
29386
+ fileType: upload.fileType,
29387
+ title,
29388
+ doc_type: opts.docType ?? "other"
29389
+ };
29390
+ if (opts.description !== undefined)
29391
+ body.description = opts.description;
29392
+ if (opts.folder !== undefined)
29393
+ body.folderId = opts.folder;
29394
+ const doc2 = await amikoWebFetch(auth, `/api/agents/${twinId}/docs`, { method: "POST", body });
29335
29395
  spinner?.stop();
29336
29396
  renderOutput(doc2, (d) => {
29337
29397
  console.log(success(`Uploaded ${d.filename}`));
29338
29398
  console.log(label("Doc ID", d.id));
29339
29399
  console.log(label("Title", d.title || "-"));
29400
+ if (d.folder_id)
29401
+ console.log(label("Folder", d.folder_id));
29340
29402
  if (upload.fileSize)
29341
29403
  console.log(label("Size", fmtBytes(upload.fileSize)));
29342
29404
  if (upload.fileType)
29343
29405
  console.log(label("Type", upload.fileType));
29344
- if (upload.url)
29345
- console.log(label("URL", upload.url));
29406
+ if (d.job_id) {
29407
+ console.log(label("RAG job", d.job_id));
29408
+ console.log(dim("Processing runs in the background. Use `amiko drive status <id>` to check."));
29409
+ }
29410
+ }, { json: opts.json });
29411
+ } catch (e5) {
29412
+ spinner?.stop();
29413
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29414
+ process.exit(1);
29415
+ }
29416
+ });
29417
+ program2.command("download <docId>").description("Download a file from the drive").option("--twin <idOrName>", "Target twin id or name").option("--out <path>", "Output path (defaults to original filename in cwd)").option("--stdout", "Write file bytes to stdout instead of a file").option("--json", "Print metadata JSON instead of downloading").action(async (docId, opts) => {
29418
+ const { twinId, auth } = twinFromFlag(opts);
29419
+ const modes = [opts.json, opts.stdout, opts.out].filter(Boolean).length;
29420
+ if (modes > 1) {
29421
+ console.error(error("--json, --stdout, and --out are mutually exclusive — pick one."));
29422
+ process.exit(1);
29423
+ }
29424
+ const spinner = opts.json || opts.stdout ? null : ora("Fetching file...").start();
29425
+ try {
29426
+ const meta3 = await fetchSignedUrl(auth, twinId, docId);
29427
+ if (opts.json) {
29428
+ console.log(JSON.stringify(meta3, null, 2));
29429
+ return;
29430
+ }
29431
+ if (spinner)
29432
+ spinner.text = `Downloading ${meta3.doc.filename}...`;
29433
+ const controller = new AbortController;
29434
+ const timeout = setTimeout(() => controller.abort(), 60000);
29435
+ let res;
29436
+ try {
29437
+ res = await fetch(meta3.signed_url, { signal: controller.signal });
29438
+ } catch (fetchErr) {
29439
+ if (fetchErr.name === "AbortError") {
29440
+ throw new Error("Signed-URL download timed out after 60s. Retry; the URL is valid for 1h.");
29441
+ }
29442
+ throw fetchErr;
29443
+ } finally {
29444
+ clearTimeout(timeout);
29445
+ }
29446
+ if (!res.ok) {
29447
+ throw new Error(`Signed-URL fetch failed: ${res.status} ${res.statusText}`);
29448
+ }
29449
+ const buf = Buffer.from(await res.arrayBuffer());
29450
+ if (opts.stdout) {
29451
+ process.stdout.write(buf);
29452
+ return;
29453
+ }
29454
+ const outPath = resolvePath(opts.out ?? basename2(meta3.doc.filename));
29455
+ await writeFile(outPath, buf);
29456
+ spinner?.stop();
29457
+ console.log(success(`Saved ${meta3.doc.filename} → ${outPath}`));
29458
+ console.log(label("Size", fmtBytes(buf.length)));
29459
+ if (meta3.doc.file_type)
29460
+ console.log(label("Type", meta3.doc.file_type));
29461
+ } catch (e5) {
29462
+ spinner?.stop();
29463
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29464
+ process.exit(1);
29465
+ }
29466
+ });
29467
+ program2.command("get <docId>").description("Show one file's metadata, parsed content, and signed URL").option("--twin <idOrName>", "Target twin id or name").option("--content", "Include parsed RAG content body in the output").option("--json", "Output as JSON").action(async (docId, opts) => {
29468
+ const { twinId, auth } = twinFromFlag(opts);
29469
+ const spinner = opts.json ? null : ora("Loading file...").start();
29470
+ try {
29471
+ const data = await fetchSignedUrl(auth, twinId, docId);
29472
+ spinner?.stop();
29473
+ renderOutput(data, (d) => {
29474
+ console.log(heading(d.doc.title || d.doc.filename));
29475
+ console.log(label("ID", d.doc.id));
29476
+ console.log(label("Filename", d.doc.filename));
29477
+ if (d.doc.file_type)
29478
+ console.log(label("Type", d.doc.file_type));
29479
+ if (d.doc.folder_id)
29480
+ console.log(label("Folder", d.doc.folder_id));
29481
+ if (d.doc.description)
29482
+ console.log(label("Description", d.doc.description));
29483
+ console.log(label("Status", statusOf(d.doc)));
29484
+ if (d.doc.metadata?.error)
29485
+ console.log(label("Error", d.doc.metadata.error));
29486
+ console.log(label("Signed URL", d.signed_url));
29487
+ if (opts.content && d.doc.content) {
29488
+ console.log(heading(`
29489
+ Parsed content`));
29490
+ console.log(d.doc.content);
29491
+ } else if (d.doc.content) {
29492
+ console.log(dim(`
29493
+ Parsed content available (${d.doc.content.length} chars). Pass --content to print.`));
29494
+ }
29346
29495
  }, { json: opts.json });
29347
29496
  } catch (e5) {
29348
29497
  spinner?.stop();
@@ -29350,36 +29499,170 @@ function registerDocsCommand(program2) {
29350
29499
  process.exit(1);
29351
29500
  }
29352
29501
  });
29353
- program2.command("delete <docId>").description("Delete a document").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (docId, opts) => {
29502
+ program2.command("rename <docId>").description("Rename a file (updates title)").option("--twin <idOrName>", "Target twin id or name").requiredOption("--title <newTitle>", "New title").option("--description <text>", "Also update description (pass empty string to clear)").option("--json", "Output as JSON").action(async (docId, opts) => {
29503
+ const { twinId, auth } = twinFromFlag(opts);
29504
+ const body = { title: opts.title };
29505
+ if (opts.description !== undefined)
29506
+ body.description = opts.description;
29507
+ try {
29508
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "PATCH", body });
29509
+ renderOutput(result, () => console.log(success(`Renamed ${docId} → "${opts.title}"`)), { json: opts.json });
29510
+ } catch (e5) {
29511
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29512
+ process.exit(1);
29513
+ }
29514
+ });
29515
+ program2.command("move <docId>").description("Move a file to a different folder").option("--twin <idOrName>", "Target twin id or name").requiredOption("--folder <id|root>", "Destination folder id, or 'root' for drive root").option("--json", "Output as JSON").action(async (docId, opts) => {
29516
+ const { twinId, auth } = twinFromFlag(opts);
29517
+ const folderId = opts.folder === "root" ? null : opts.folder;
29518
+ try {
29519
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "PATCH", body: { folderId } });
29520
+ renderOutput(result, () => console.log(success(`Moved ${docId} → ${folderId ?? "root"}`)), { json: opts.json });
29521
+ } catch (e5) {
29522
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29523
+ process.exit(1);
29524
+ }
29525
+ });
29526
+ program2.command("delete <docId>").description("Delete a file (removes storage object + RAG embeddings)").option("--twin <idOrName>", "Target twin id or name").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (docId, opts) => {
29354
29527
  const { twinId, auth } = twinFromFlag(opts);
29355
29528
  await confirmDestructive({
29356
- action: `Delete document ${docId}`,
29357
- detail: "This will remove the document and its embeddings from the twin's RAG.",
29529
+ action: `Delete file ${docId}`,
29530
+ detail: "This removes the file from storage and clears its embeddings from the twin's RAG.",
29358
29531
  yes: opts.yes,
29359
- commandExample: `amiko docs delete ${docId}`
29532
+ commandExample: `amiko drive delete ${docId}`
29360
29533
  });
29361
- const spinner = opts.json ? null : ora("Deleting document...").start();
29534
+ const spinner = opts.json ? null : ora("Deleting...").start();
29362
29535
  try {
29363
29536
  const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/${docId}`, { method: "DELETE" });
29364
29537
  spinner?.stop();
29365
- renderOutput(result, () => console.log(success(`Deleted ${docId}`)), { json: opts.json });
29538
+ renderOutput(result, () => console.log(success(`Deleted ${docId}`)), {
29539
+ json: opts.json
29540
+ });
29366
29541
  } catch (e5) {
29367
29542
  spinner?.stop();
29368
29543
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29369
29544
  process.exit(1);
29370
29545
  }
29371
29546
  });
29372
- program2.command("presign <source>").description("Get a presigned upload URL for a document (no upload)").option("--twin <idOrName>", "Target twin id or name").option("--name <filename>", "Override filename used in the request").option("--json", "Output as JSON").action(async (source, opts) => {
29547
+ program2.command("presign <name>").description("Get a presigned upload URL (browser-direct uploads; no upload performed)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (name, opts) => {
29373
29548
  const { twinId, auth } = twinFromFlag(opts);
29374
- const filename = opts.name ?? (source === "-" ? "stdin" : source.split("/").pop() ?? source);
29375
29549
  try {
29376
- const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/presigned-url`, { method: "POST", body: { filename } });
29550
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/presigned-url`, { method: "POST", body: { filename: name } });
29377
29551
  renderOutput(result, (r) => console.log(JSON.stringify(r, null, 2)), { json: opts.json });
29378
29552
  } catch (e5) {
29379
29553
  console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29380
29554
  process.exit(1);
29381
29555
  }
29382
29556
  });
29557
+ program2.command("status <docId...>").description("Check RAG processing status for one or more files").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (docIds, opts) => {
29558
+ const { twinId, auth } = twinFromFlag(opts);
29559
+ try {
29560
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/check-processing`, {
29561
+ method: "POST",
29562
+ body: { docIds }
29563
+ });
29564
+ renderOutput(result, (r) => {
29565
+ const rows = [[dim("DOC ID"), dim("STATUS"), dim("ERROR")]];
29566
+ for (const [id, v] of Object.entries(r.results)) {
29567
+ rows.push([id, v.status, v.error ?? ""]);
29568
+ }
29569
+ console.log(table(rows));
29570
+ }, { json: opts.json });
29571
+ } catch (e5) {
29572
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29573
+ process.exit(1);
29574
+ }
29575
+ });
29576
+ const folder = program2.command("folder").description("Manage drive folders — list, create, rename, move, delete");
29577
+ folder.command("list").description("List all folders for the twin (flat — clients build the tree)").option("--twin <idOrName>", "Target twin id or name").option("--json", "Output as JSON").action(async (opts) => {
29578
+ const { twinId, auth } = twinFromFlag(opts);
29579
+ try {
29580
+ const data = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders`);
29581
+ renderOutput(data, (d) => {
29582
+ if (!d.folders || d.folders.length === 0) {
29583
+ console.log(dim("No folders."));
29584
+ return;
29585
+ }
29586
+ console.log(heading(`Folders (${d.folders.length})`));
29587
+ const rows = [
29588
+ [dim("ID"), dim("NAME"), dim("PARENT"), dim("CREATED")],
29589
+ ...d.folders.map((f) => [
29590
+ f.id,
29591
+ f.name,
29592
+ f.parent_id || dim("root"),
29593
+ f.created_at?.slice(0, 10) || "-"
29594
+ ])
29595
+ ];
29596
+ console.log(table(rows));
29597
+ }, { json: opts.json });
29598
+ } catch (e5) {
29599
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29600
+ process.exit(1);
29601
+ }
29602
+ });
29603
+ folder.command("create <name>").description("Create a folder (defaults to drive root)").option("--twin <idOrName>", "Target twin id or name").option("--parent <id>", "Parent folder id (omit for root)").option("--json", "Output as JSON").action(async (name, opts) => {
29604
+ const { twinId, auth } = twinFromFlag(opts);
29605
+ const body = { name };
29606
+ if (opts.parent !== undefined)
29607
+ body.parentId = opts.parent;
29608
+ try {
29609
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders`, { method: "POST", body });
29610
+ renderOutput(result, (r) => {
29611
+ console.log(success(`Created folder "${r.folder.name}"`));
29612
+ console.log(label("Folder ID", r.folder.id));
29613
+ console.log(label("Parent", r.folder.parent_id || "root"));
29614
+ }, { json: opts.json });
29615
+ } catch (e5) {
29616
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29617
+ process.exit(1);
29618
+ }
29619
+ });
29620
+ folder.command("rename <folderId>").description("Rename a folder").option("--twin <idOrName>", "Target twin id or name").requiredOption("--name <newName>", "New folder name (1–120 chars)").option("--json", "Output as JSON").action(async (folderId, opts) => {
29621
+ const { twinId, auth } = twinFromFlag(opts);
29622
+ try {
29623
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders/${folderId}`, { method: "PATCH", body: { name: opts.name } });
29624
+ renderOutput(result, () => console.log(success(`Renamed folder ${folderId} → "${opts.name}"`)), { json: opts.json });
29625
+ } catch (e5) {
29626
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29627
+ process.exit(1);
29628
+ }
29629
+ });
29630
+ folder.command("move <folderId>").description("Move a folder under a new parent").option("--twin <idOrName>", "Target twin id or name").requiredOption("--parent <id|root>", "New parent folder id, or 'root'").option("--json", "Output as JSON").action(async (folderId, opts) => {
29631
+ const { twinId, auth } = twinFromFlag(opts);
29632
+ const parentId = opts.parent === "root" ? null : opts.parent;
29633
+ try {
29634
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders/${folderId}`, { method: "PATCH", body: { parentId } });
29635
+ renderOutput(result, () => console.log(success(`Moved folder ${folderId} → ${parentId ?? "root"}`)), { json: opts.json });
29636
+ } catch (e5) {
29637
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29638
+ process.exit(1);
29639
+ }
29640
+ });
29641
+ folder.command("delete <folderId>").description("Delete a folder (use --force to delete non-empty folders + their contents)").option("--twin <idOrName>", "Target twin id or name").option("--force", "Recursively delete subfolders and files in the subtree").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (folderId, opts) => {
29642
+ const { twinId, auth } = twinFromFlag(opts);
29643
+ await confirmDestructive({
29644
+ action: opts.force ? `Force-delete folder ${folderId} and all its contents` : `Delete folder ${folderId}`,
29645
+ detail: opts.force ? "This walks the subtree and removes every doc + subfolder under this folder." : "Empty folder only. Non-empty deletes return 409; pass --force to remove the whole subtree.",
29646
+ yes: opts.yes,
29647
+ commandExample: `amiko drive folder delete ${folderId}${opts.force ? " --force" : ""}`
29648
+ });
29649
+ try {
29650
+ const result = await amikoWebFetch(auth, `/api/agents/${twinId}/docs/folders/${folderId}`, {
29651
+ method: "DELETE",
29652
+ query: opts.force ? { force: "1" } : undefined
29653
+ });
29654
+ renderOutput(result, (r) => {
29655
+ console.log(success(`Deleted folder ${folderId}`));
29656
+ if (typeof r.deletedFiles === "number")
29657
+ console.log(label("Files removed", String(r.deletedFiles)));
29658
+ if (typeof r.deletedSubfolders === "number")
29659
+ console.log(label("Subfolders removed", String(r.deletedSubfolders)));
29660
+ }, { json: opts.json });
29661
+ } catch (e5) {
29662
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29663
+ process.exit(1);
29664
+ }
29665
+ });
29383
29666
  }
29384
29667
 
29385
29668
  // src/commands/voice.ts
@@ -31513,7 +31796,7 @@ program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, b
31513
31796
  var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
31514
31797
  var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
31515
31798
  var twin = program2.command("twin").description("Update twin identity (name, description, visibility)");
31516
- var docs = program2.command("docs").description("Manage twin RAG documents — list, upload, delete, presign");
31799
+ var drive = program2.command("drive").alias("docs").description("Manage twin drive — upload/download/search files, organise into folders");
31517
31800
  var voice = program2.command("voice").description("Manage twin voice — design, create, clone, reset");
31518
31801
  var avatar = program2.command("avatar").description("Manage twin avatar image");
31519
31802
  var friends = program2.command("friends").description("Manage friendships — list, requests, add, accept, remove, reports, matches");
@@ -31539,7 +31822,7 @@ registerDiscoveryCommand(markets);
31539
31822
  registerUpdateCommand(program2);
31540
31823
  registerAccountsCommand(program2);
31541
31824
  registerTwinCommand(twin);
31542
- registerDocsCommand(docs);
31825
+ registerDriveCommand(drive);
31543
31826
  registerVoiceCommand(voice);
31544
31827
  registerAvatarCommand(avatar);
31545
31828
  registerFriendsCommand(friends);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.10.1-beta.0",
3
+ "version": "0.10.1-beta.3",
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
@@ -7,9 +7,11 @@ metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["node"]}}}
7
7
 
8
8
  # Amiko CLI
9
9
 
10
+ This file is the **mental model**, not a syntax reference. For exact flags and subcommands, run `amiko <group> --help` — the CLI's help is the authoritative source and stays in sync with each release.
11
+
10
12
  ## How to invoke — ALWAYS use your shell-execution tool
11
13
 
12
- Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar) and run `amiko <subcommand>`. Amiko is a shell program, not a callable tool. Always go through the shell tool.
14
+ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar) and run `amiko <subcommand>`. Amiko is a shell program, not a callable tool.
13
15
 
14
16
  ### Examples — copy this pattern
15
17
 
@@ -20,403 +22,155 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
20
22
  | "swap 1 SOL to USDC" | shell → `amiko wallets swap quote 1 SOL USDC` (then send with `--yes` after approval) |
21
23
  | "did anyone DM me?" | shell → `amiko conversation list` |
22
24
  | "any notifications?" | shell → `amiko notifications list --unread` |
25
+ | "any new posts I haven't seen?" | shell → `amiko feed --unread` |
26
+ | "upload report.pdf to my drive" | shell → `amiko drive upload ./report.pdf` |
27
+ | "download the file with id X" | shell → `amiko drive download X` |
28
+ | "find files about Q1 revenue" | shell → `amiko drive search "Q1 revenue"` |
29
+ | "what comments are on my post?" | shell → `amiko post comments --id <postId>` |
23
30
  | "search memory for X" | shell → `amiko memory search "X"` |
24
31
  | "what can amiko do?" | shell → `amiko --help` |
25
32
 
26
- The CLI is installed globally and is pre-authenticated when you're inside your workspace folder. Never suggest `amiko login` or `amiko connect`.
33
+ The CLI is installed globally and is pre-authenticated when you're inside your workspace folder. Never suggest `amiko login` or `amiko connect` — they don't exist.
27
34
 
28
35
  ### Failure modes to avoid
29
36
 
30
- - A tool named `amiko_cli` / `amiko-cli` / `amiko` (or this skill's name) does **not** exist. The only path is the shell tool above. If you try to call any of those as a tool, the gateway returns `Tool ... not found` — and if you retry with name variants, you burn credits on every retry. Don't retry. Re-route through the shell tool.
31
- - Do not invent intermediate "amiko" tools (`amiko.balance`, `amiko.credits`, etc.). Every Amiko action goes through one path: shell tool → `amiko <subcommand>`.
32
-
33
- ## Top-level commands at a glance
34
-
35
- Each row is a first-level command group or command. Start here when the user asks what the CLI can do.
36
-
37
- | Command | What it does |
38
- |---------|--------------|
39
- | `amiko markets <cmd>` | Paid MPP services: X search, image gen, Amazon, TTS/STT/chat, generic service call, discover |
40
- | `amiko wallets <cmd>` | Wallet ops: create, list, sync balance, transfer tokens out, swap on Solana (Jupiter), bridge USDC cross-chain (Across) |
41
- | `amiko credits <cmd>` | Show credit balance, top up with AMIKO/SOL/USDC/USDT (10,000 credits = $1) |
42
- | `amiko twin <cmd>` | Update the twin's identity — name, description, public visibility |
43
- | `amiko docs <cmd>` | Manage twin RAG documents — list, upload (path/URL/stdin), delete, presign |
44
- | `amiko voice <cmd>` | Design/clone/reset the twin's voice, create a voice from sample |
45
- | `amiko avatar <cmd>` | Set the twin's avatar image (max 5 MB) |
46
- | `amiko friends <cmd>` | Social graph — list, requests, add, accept, remove, matches, reports |
47
- | `amiko users <cmd>` | Search users, view public profile |
48
- | `amiko post <cmd>` | Create posts and comments on the feed (optionally with media) |
49
- | `amiko review <cmd>` | Review queue — list, approve, or reject twin-drafted comments |
50
- | `amiko feed` | Read the friends feed, for-you feed, or filter by hashtag |
51
- | `amiko composio <cmd>` | Connect / disconnect third-party OAuth apps (Gmail, GitHub, …) |
52
- | `amiko conversation <cmd>` | DM/chat — list, find, create, send messages |
53
- | `amiko notifications <cmd>` | Platform notifications — list, mark as read |
54
- | `amiko memory <cmd>` | Cross-agent memory — `search` before answering personal questions, `add` what's worth remembering, `list` / `rm` / `status` / `sync` local memory files |
55
- | `amiko accounts` | Show the resolved identity (authenticated, userId, twinId, platform) |
56
- | `amiko info` | Show the active twin (name, description, public, voice, avatar) |
57
- | `amiko config <cmd>` | Show resolved config |
58
- | `amiko update` | Self-update the CLI |
59
-
60
- All twin-scoped commands accept `--twin <id>` to target a non-default twin.
37
+ - A tool named `amiko_cli` / `amiko-cli` / `amiko` (or this skill's name) does **not** exist as a callable tool. The only path is the shell tool above. If you try to call any of those, the gateway returns `Tool ... not found` — and retries burn credits. Don't retry; re-route through the shell.
38
+ - Do not invent intermediate "amiko" tools (`amiko.balance`, etc.). Every action goes through one path: shell tool → `amiko <subcommand>`.
39
+
40
+ ## Command groups
41
+
42
+ Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `wallets`, `credits`, `twin`, `drive` (files / folders / RAG; `docs` is an alias), `voice`, `avatar`, `friends`, `users`, `post`, `review`, `feed`, `composio`, `conversation`, `notifications`, `memory`, plus the top-level `accounts`, `info`, `config`, `update`. All twin-scoped commands accept `--twin <id>`. Most commands support `--json`.
43
+
44
+ ## Critical Rules
45
+
46
+ 1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI refuses to run `markets *`, `credits topup`, `wallets swap send`, `wallets bridge send`, `wallets transfer` (and destructive ops like `twin update --public`, `docs delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `composio disconnect`, `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`.**
47
+ 2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line — include that figure in your reply.
48
+ 3. **Never retry a failed command.** Report and stop. Every paid call costs tokens even on failure.
49
+ 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`.
50
+ 5. **Check balance before expensive ops.** Run `amiko credits balance` if unsure.
51
+ 6. **Payments are custodied.** The platform signs and moves tokens from the twin's wallet — the CLI never holds keys.
52
+
53
+ ### Quoting cost before running
54
+
55
+ Prices change. Before any paid call, run `amiko markets service list` or `amiko markets discover` to fetch the live price, then quote it to the user. Rough order of magnitude: text/search/TTS ≈ 1 AMIKO, SFX ≈ $0.05, music ≈ $0.10, image gen varies by model/quality (OpenAI pass-through × 1.30 markup).
61
56
 
62
57
  ## The `--wallet` default (read this before any paid command)
63
58
 
64
- Every paid / value-moving command (`markets search`, `markets image`, `markets amazon search`, `markets service *`, `wallets swap send`, `wallets bridge send`, `wallets transfer`) spends tokens from an on-chain wallet. As of v0.9.0-beta.9 you almost never need to pass `--wallet` explicitly:
59
+ Every paid / value-moving command spends from an on-chain wallet, but you almost never need `--wallet` explicitly:
65
60
 
66
- - **Default**: the CLI auto-selects the twin's first active **Solana** wallet from `GET /api/agents/{id}/wallets`.
67
- - **Override**: pass `--wallet <address>` only if you want a different wallet than the default.
68
- - **No wallet yet?** The CLI will tell you to run `amiko wallets create --chain solana` first. Create once per twin; reuse forever.
69
- - **Bridges**: `wallets bridge quote/send` similarly default `--depositor` to the wallet on the origin chain (Solana for `--from solana`, Base for `--from base`).
61
+ - **Default**: CLI auto-selects the twin's first active **Solana** wallet.
62
+ - **Override**: pass `--wallet <address>` only if you want a different wallet.
63
+ - **No wallet yet?** The CLI tells you to run `amiko wallets create --chain solana`. One-shot per `chain+custodian`; reuse forever.
64
+ - **Bridges**: `wallets bridge quote/send` defaults `--depositor` to the wallet on the origin chain (Solana for `--from solana`, Base for `--from base`).
70
65
 
71
- > Never prompt the user for their wallet address when the default would work. Only surface `--wallet` if the command errors out saying there's no default.
66
+ > Never prompt the user for a wallet address when the default works. Only surface `--wallet` if the command errors out.
72
67
 
73
- ## Critical Rules
68
+ ## Credits — intent mapping
74
69
 
75
- 1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI *refuses* to run `markets image`, `markets search`, `markets amazon search`, `markets service call`, `markets service tts`, `markets service stt`, `markets service chat`, `credits topup`, `wallets swap send`, `wallets bridge send`, or `wallets transfer` unless `--yes` is passed. The refusal is loud: it prints the cost and the command you should re-run. **Quote the cost to the user, get explicit approval, THEN append `--yes`**. Same for destructive non-paid commands (`twin update --public`, `docs delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `composio disconnect`, `review reject`).
76
- 2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line at the end of every paid command — include that figure in your reply.
77
- 3. **Never retry a failed command.** Report the error and stop. Every paid call costs tokens even on failure.
78
- 4. **Never suggest `amiko login` or `amiko connect`.** These don't exist. Auth is automatic when you run from your workspace folder.
79
- 5. **Check balance before expensive operations.** Run `amiko credits balance` first if unsure.
80
- 6. **Payments are automatic.** The platform signs and moves tokens from the twin's wallet for each paid call — the CLI never holds keys.
81
-
82
- ### Pricing cheat sheet (quote this to the user before running)
83
-
84
- | Command | Cost |
85
- |---------|------|
86
- | `amiko markets search "<query>"` | 1 AMIKO |
87
- | `amiko markets image "<prompt>"` | varies by model/quality/size (≈$0.005–$1.30 × 1.30 markup) |
88
- | `amiko markets amazon search "<query>"` | 1 AMIKO |
89
- | `amiko markets amazon quote <ASIN>` | Free |
90
- | `amiko markets service tts <voiceId> "<text>"` | 1 AMIKO |
91
- | `amiko markets service call POST /v1/sfx ...` | $0.05 |
92
- | `amiko markets service call POST /v1/music ...` | $0.10 |
93
- | `amiko markets service call POST /v1/music/plan ...` | $0.02 |
94
- | `amiko wallets swap quote ...` | Free |
95
- | `amiko wallets swap send ...` | Solana gas + 10bps |
96
- | `amiko wallets transfer ...` | Chain gas only |
97
- | `amiko credits topup ...` | Whatever amount you top up |
98
-
99
- ## Credits
100
-
101
- **Display credits: 10,000 credits = $1.00 USD.**
102
-
103
- ```bash
104
- amiko credits balance # show balance as "N credits"
105
- amiko credits topup --spend 1 --token SOL --yes # "1 SOL worth of credits"
106
- amiko credits topup --usd 5 --token USDC --yes # $5 paying in USDC
107
- amiko credits topup 10000 --token AMIKO --yes # 10,000 credits = $1
108
- ```
109
-
110
- `topup` accepts **one** of three amount forms:
111
-
112
- | Form | Meaning | Example |
113
- |------|---------|---------|
114
- | `<amount>` (positional) | credits | `amiko credits topup 10000 --token AMIKO` |
115
- | `--usd <n>` | US dollars | `amiko credits topup --usd 5 --token USDC` |
116
- | `--spend <n>` | spend N of `--token` | `amiko credits topup --spend 1 --token SOL` |
117
-
118
- Map user intent directly — no manual price math:
70
+ Display unit: **10,000 credits = $1.00 USD.** `topup` accepts one of three amount forms — map user intent directly, no manual price math:
119
71
 
120
72
  - "top up 1 SOL of credits" → `--spend 1 --token SOL`
121
73
  - "top up $5 in USDC" → `--usd 5 --token USDC`
122
- - "top up 10000 credits with AMIKO" → `10000 --token AMIKO`
123
-
124
- Supported tokens: AMIKO, SOL, USDC, USDT. The CLI fetches the live price, the platform transfers from the twin's wallet, and on-chain verification credits the account. Balance can lag by a few seconds after topup — re-run `amiko credits balance` if it looks stale.
125
-
126
- ## Wallets
127
-
128
- ```bash
129
- amiko wallets list # all twin wallets + cached balances
130
- amiko wallets create --chain solana # create a wallet (solana | base)
131
- amiko wallets balance <address> # force-sync one wallet; returns fresh balances
132
- amiko wallets transfer --to <addr> --amount 100 --token amiko --yes # send tokens out of a twin wallet
133
- amiko wallets swap quote 1.0 SOL USDC # Jupiter quote (free)
134
- amiko wallets swap send 1.0 SOL USDC --yes # --wallet defaults to your Solana wallet
135
- amiko wallets swap tokens # supported tokens list
136
- amiko wallets bridge quote 10 --from solana --to base --recipient <addr>
137
- amiko wallets bridge send 10 --from solana --to base --recipient <addr> --yes
138
- amiko wallets bridge status <txHash>
139
- amiko wallets bridge routes
140
- amiko wallets bridge limits
141
- ```
142
-
143
- - `wallets list` returns **cached** balances (platform doesn't auto-sync). If they look stale, run `wallets balance <address>` to force a sync for that wallet.
144
- - `wallets create` is one-shot per `chain+custodian` pair (409 if one already exists) — run once for the twin and reuse.
145
- - `wallets transfer` sends tokens from a twin wallet to an external address. Only Crossmint-custodied wallets are supported (agent-signed on the server). `--wallet` defaults to your Solana wallet; pass `--chain base` + `--wallet` to send from a Base wallet. `--token` accepts symbols (`sol`, `usdc`, `usdt`, `amiko` on Solana; `eth` on Base) or a raw mint/contract address. Returns a tx hash once the transaction lands on-chain. Paid/destructive — requires `--yes` in non-interactive shells.
146
- - Swaps run via Jupiter on Solana. Supported: SOL, USDC, USDT, AMIKO, PYUSD, BONK, JUP, RAY, JitoSOL — or any mint address.
147
- - Bridging goes through Across Protocol. Solana ↔ EVM requires `--recipient` (different address formats).
148
-
149
- ## Market (paid MPP services)
150
-
151
- ```bash
152
- amiko markets search "AI agents" # 1 AMIKO — X/Twitter search
153
- amiko markets image "a sunset over mountains" --yes # gpt-image-2 high 1024² default — AMIKO @ OpenAI pass-through × 1.30
154
- amiko markets image "logo" --background transparent # transparent bg
155
- amiko markets image "portrait" --size 1024x1536 # portrait orientation
156
- amiko markets image "icon" --model gpt-image-1-mini --quality low # cheapest tier
157
- amiko markets amazon search "usb c cable" # 1 AMIKO — product search
158
- amiko markets amazon quote B01GGKYKQM # free — price quote
159
- amiko markets service list # all services + prices
160
- amiko markets service call POST /v1/sfx '{"text":"thunder","duration_seconds":5}'
161
- amiko markets service call POST /v1/music '{"prompt":"lo-fi beat","music_length_ms":30000}'
162
- amiko markets service tts 21m00Tcm4TlvDq8ikWAM "Hello world"
163
- amiko markets service call POST /v1/music/plan '{"prompt":"epic orchestral"}'
164
- amiko markets service call <METHOD> <path> [body] # any MPP endpoint
165
- amiko markets discover # service info + pricing
166
- ```
167
-
168
- All paid markets commands auto-select the twin's active Solana wallet. Pass `--wallet <address>` only to override. Audio and image endpoints return permanent Supabase Storage URLs.
169
-
170
- ## Account & Twin
171
-
172
- ```bash
173
- amiko accounts # resolved identity (authenticated, userId, twinId, platform)
174
- amiko info # twin info (name, description, public, voice, avatar)
175
- amiko twin update --name "New Name"
176
- amiko twin update --description "..."
177
- amiko twin update --public true --yes # destructive — requires --yes in non-TTY
178
- ```
179
-
180
- ## Documents (RAG)
181
-
182
- ```bash
183
- amiko docs list # list
184
- amiko docs upload ./notes.pdf # path
185
- amiko docs upload https://example.com/x.pdf # URL
186
- cat file.pdf | amiko docs upload - --stdin --name file.pdf
187
- amiko docs delete <docId> --yes # destructive
188
- amiko docs presign ./file.pdf # presigned S3 URL only
189
- ```
190
-
191
- ## Voice
192
-
193
- ```bash
194
- amiko voice design --description "warm, low, male, confident, measured" # min 20 chars
195
- amiko voice create --sample <generated_voice_id>
196
- amiko voice clone ./me.mp3 # path | URL | -
197
- amiko voice reset --yes # destructive
198
- ```
199
-
200
- ## Avatar
201
-
202
- ```bash
203
- amiko avatar update --file ./portrait.png --yes # max 5 MB, destructive
204
- ```
205
-
206
- ## Friends
207
-
208
- ```bash
209
- amiko friends list # list all
210
- amiko friends list --type user # filter
211
- amiko friends requests # pending requests (incoming + outgoing)
212
- amiko friends requests --direction incoming
213
- amiko friends add --id <userId>
214
- amiko friends accept <friendshipId>
215
- amiko friends remove <friendshipId> --yes # destructive
216
- amiko friends matches # pre-generated match candidates (from cron)
217
- amiko friends matches --dimension personality
218
- amiko friends find --relationship "cofounder with design taste" # on-demand LLM match for a custom relationship
219
- amiko friends reports list # all reports (any status)
220
- amiko friends reports pending # pending-consent: awaiting mine + awaiting theirs
221
- amiko friends reports view <reportId>
222
- amiko friends reports request --id <userId> --type friend|romantic|career --yes
223
- amiko friends reports consent <reportId> # respondent approves; report then generates
224
- amiko friends reports cancel <reportId>
225
- amiko friends reports retry <reportId> # re-run a failed generation
226
- ```
227
-
228
- ### Answering relationship questions ("how is my relationship with X?")
229
-
230
- When the user asks about their relationship / compatibility with a specific person (by name or handle), follow this playbook:
231
-
232
- 1. **Resolve the person to a user id.** Check `amiko friends list --json` first. If not a friend, try `amiko users search "<name>" --json`. If still ambiguous, ask the user which one they mean.
233
- 2. **Look for an existing report.** Run `amiko friends reports list --json` and filter by `initiator.id` or `respondent.id` matching the resolved user id. If one exists with `status=completed`, use `amiko friends reports view <id>` and summarize. If `status=pending_consent` or `generating`, tell the user it is in progress.
234
- 3. **If none exists**, don't silently create one. Ask the user which of the three report types they want (`friend` / `romantic` / `career`), and confirm that the other user will be notified to consent. Only then run `amiko friends reports request --id <userId> --type <type> --yes`.
235
- 4. **For "who is waiting on whom"**, use `amiko friends reports pending` — it splits `pending_consent` reports into *awaiting my consent* (I can `consent <id>`) vs *awaiting theirs* (I can `cancel <id>`). Use this before deciding to `request` — you cannot have two active reports of the same type between the same pair (the API returns 409).
236
-
237
- Both users must have a personality profile, otherwise the request returns 422.
238
-
239
- ### Recommending interesting people ("who should I meet / connect with?")
240
-
241
- The discovery primitive is **`amiko friends matches`** — it returns personality-match candidates with a score (0–1), a match dimension, a pairing label, and compatibility highlights.
242
-
243
- **`friends find` vs `users search` — do not confuse them:**
244
-
245
- | Command | Purpose | Input | Backend |
246
- |---|---|---|---|
247
- | `users search <query>` | **Look up a specific known person** by name or handle ("find wendao", "search for someone called Sophie") | Exact/substring text against `name` / `handle` | Simple SQL lookup in `/api/search?type=people` |
248
- | `friends find --relationship <text>` | **Discover unknown people** whose personality matches a free-form relationship description ("cofounder with design taste", "someone to hike with") | Free-form relationship description | LLM-generated MatchingSpec + semantic matching across ~250 candidate profiles |
249
-
250
- Rule of thumb: if the user already has a name in mind → `users search`. If the user is describing what *kind of person* they want to meet → `friends find`. For already-curated ambient recommendations with no input required → `friends matches`.
251
-
252
- 1. Start broad: `amiko friends matches --limit 20 --json`. Summarize the top few by `display_name`, `score`, `match_dimension`, `pairing_label`, and `recommendation_reason`. These are pre-generated by a cron job.
253
- 2. If the user wants to narrow, filter by `--dimension personality` or `--dimension interest` (the only two dimensions the matching cron actually produces). `--relationship-type` also exists but takes free-form LLM-generated labels (often localized, e.g. `深度思维_对打手`) — usually not worth filtering on unless you first inspect the `--json` output to pick a known value.
254
- 3. If the user describes a **specific kind of person** not covered by the cron matches ("a cofounder who is good at design", "a hiking buddy", "someone to debate philosophy with"), use `amiko friends find --relationship "<description>"`. This is an on-demand, LLM-backed matcher against ~250 candidates; may take ~10s and may return zero matches if nothing clears the internal score threshold.
255
- 4. Skip anyone whose `friendship_status` is already `accepted` unless the user explicitly wants to revisit existing friends.
256
- 5. Follow-ups: `amiko users profile <handle>` for a fuller view, `amiko friends add --id <matched_user_id>` (after explicit user approval) to send a friend request, and — once the friendship is accepted — `amiko friends reports request --type friend|romantic|career` for a deeper compatibility report. Note that report `--type` is a separate enum and has nothing to do with `match.relationship_type`.
257
-
258
- ## Users
259
-
260
- ```bash
261
- amiko users search <query> # search by name/handle
262
- amiko users profile <handle> # public profile
263
- ```
264
-
265
- ## Feed & Posts
266
-
267
- ```bash
268
- amiko feed # friends feed (default)
269
- amiko feed --type for_you --limit 20
270
- amiko feed --hashtag ai
271
- amiko post create --content "hello from the CLI"
272
- amiko post create --content "private note" --visibility private
273
- amiko post create --content "look" --media https://...jpg
274
- amiko post comment --id <postId> --comment "great post"
275
- amiko post comment --id <postId> --comment "look" --media https://...jpg
276
- amiko post comment --id <postId> --comment "..." --twin <id>
277
- amiko review list # list twin-drafted comments awaiting your approval
278
- amiko review approve <commentId> # publish a draft comment from the review queue
279
- amiko review reject <commentId> --yes # destructive — delete a draft comment
280
- ```
281
-
282
- **Comments authored by a twin always go through review.** Any comment created with `amiko post comment ... --twin <id>` (or auto-drafted by a twin in engagement mode) lands in `status=draft` and is **not visible** until you run `amiko review approve <commentId>`. Comments without `--twin` (you posting as yourself) publish immediately and skip the review queue. Workflow: `amiko review list` to see pending drafts → `amiko review approve <id>` to publish, or `amiko review reject <id> --yes` to discard.
283
-
284
- ## Conversations
285
-
286
- > **Default platform = Amiko.** When the owner asks about messages, DMs, chats, notifications, or "anything new" **without naming a platform** (no "on WeChat", "on Telegram", "on Slack", etc.), assume they mean the Amiko platform and answer with `amiko conversation` / `amiko notifications`. Do **not** ask "which platform?" — just use the Amiko CLI. Only ask for clarification if the owner explicitly mentions a non-Amiko channel or the question is genuinely ambiguous across multiple connected channels.
287
- >
288
- > **When the owner asks about chat history with another person** ("did X message me?", "check my chat with Y", "anyone DM me this week?"), use `amiko conversation` — these are platform DMs between the owner and other Amiko users, stored in amiko-web's database. Do **not** use the built-in `sessions_list` / `sessions_history` tools for this: those read the agent's *own* past chat sessions with the owner (local openclaw memory), not platform DMs with third parties, and will return nothing useful. Typical flow: `amiko users search "<name>"` → `amiko conversation find --user-id <id>` → `amiko conversation read --id <conversationId>`. To scan everyone who messaged recently, start with `amiko conversation list` (sorted by last activity) or `amiko notifications list --unread`.
289
-
290
- ```bash
291
- amiko conversation list # all your conversations (default: active)
292
- amiko conversation list --archived all --limit 100 # include archived
293
- amiko conversation find --user-id <userId> # is there an existing direct DM with this user?
294
- amiko conversation create --user-id <userId> # create or reuse a direct DM (idempotent)
295
- amiko conversation read --id <conversationId> # read recent messages (oldest-first)
296
- amiko conversation read --id <id> --before-cursor <next_cursor> # page older messages
297
- amiko conversation send --id <conversationId> --message "hi" # send a text message
298
- amiko conversation send --id <id> --message "ok" --reply-to <messageId>
299
- amiko conversation send --id <id> --message "agent reply" --as-agent # send as twin (sender_type=agent)
300
- ```
301
-
302
- - `list` and `find` query amiko-web's `/api/conversations` (Prisma-backed). `find` filters client-side for a 2-user direct conversation containing both you and `--user-id`.
303
- - `create` POSTs to `/api/conversations` with `conversation_type=direct`. The server reuses an existing conversation if one already exists between the two users (`reused_existing: true` in the JSON response), so calling it repeatedly is safe.
304
- - `read` queries amiko-web's `GET /api/conversations/<id>/messages` (paginated by `next_cursor`, oldest-first per page).
305
- - `send` posts to amiko-web's `POST /api/conversations/<id>/messages` with the Clawd twin token. The server persists the message and fans it out to other participants via the WebSocket broadcast.
306
- - `--user-id` expects the **internal** user id (the one returned by `amiko users search`), not a Privy DID or handle.
307
-
308
- ## Notifications
309
-
310
- ```bash
311
- amiko notifications list # recent platform notifications (default 20)
312
- amiko notifications list --unread # only unread
313
- amiko notifications list --cursor <iso> # page older items
314
- amiko notifications read --id <notificationId> # mark one read
315
- amiko notifications read --all # mark all read
316
- ```
317
-
318
- Platform notifications cover friend requests, mentions, system alerts, and other activity that doesn't belong to any conversation. Use this when the user asks "any notifications?" or "what's new on the platform?". Notifications belonging to a conversation (new DM, comment on your post) still show up here when the platform writes one — they don't replace `conversation read` for actual chat content.
74
+ - "top up 10000 credits with AMIKO" → positional `10000 --token AMIKO`
319
75
 
320
- ## Memory (cross-agent) — READ THIS BEFORE ANSWERING ANYTHING ABOUT THE OWNER
76
+ Supported: AMIKO, SOL, USDC, USDT. Balance can lag a few seconds after topup — re-run `credits balance` if stale.
321
77
 
322
- Memories are scoped to the **owner's `user_id`**, not to your individual agent — every agent the owner runs reads and writes the same pool. Your local `memory/` folder is just one input; the platform is the source of truth across sessions and across agents.
78
+ ## Wallets — behavior notes
323
79
 
324
- **Two non-negotiable habits:**
80
+ - `wallets list` returns **cached** balances. If stale, run `wallets balance <address>` to force a sync.
81
+ - `wallets create` is one-shot per `chain+custodian` (409 if one exists).
82
+ - `wallets transfer` only works for Crossmint-custodied wallets. `--token` accepts symbols (`sol`, `usdc`, `usdt`, `amiko` on Solana; `eth` on Base) or a raw mint/contract address.
83
+ - Swaps run via Jupiter on Solana. Bridges run via Across; Solana ↔ EVM requires `--recipient` (different address formats).
325
84
 
326
- 1. **Search before you answer.** Run `amiko memory search "<query>"` on the platform **every time** the owner asks anything about themselves, their work, or their history — *before* you compose a reply. Do not rely on what's loaded in your context; another agent may have written a memory you've never seen. A miss costs ~100ms; an amnesic answer costs the owner's trust.
327
- 2. **Sync periodically.** Run `amiko memory sync` whenever you've meaningfully edited the local `MEMORY.md` / `memory/**/*.md` files, and at least once at the end of any session in which they changed. The platform only sees what you push.
85
+ ## Markets — behavior notes
328
86
 
329
- ```bash
330
- amiko memory search "git workflow" --limit 5 # hybrid (vector + FTS); query in any language
331
- amiko memory list --limit 25 # newest-first, paginate with --offset
332
- amiko memory list --category preference # filter: fact | preference | pattern | decision | context
333
- amiko memory add "Owner prefers terse PR descriptions" --category preference --tags pr,style
334
- amiko memory rm <memoryId> # soft-delete
335
- amiko memory status # totals
336
- amiko memory sync # one-way upload of local memory files to the platform
337
- ```
87
+ All paid markets commands auto-select the twin's active Solana wallet (see `--wallet` default above). Audio and image endpoints return permanent Supabase Storage URLs. Use `markets service call <METHOD> <path> [body]` for any MPP endpoint not covered by a dedicated subcommand.
338
88
 
339
- All `memory` commands support `--json`.
89
+ ## Drive (files & folders)
340
90
 
341
- ### When to search — bias toward calling
91
+ 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.
342
92
 
343
- **Default assumption: the owner has stored context you don't have. Run `amiko memory search` BEFORE answering any question about them, their project, their preferences, or their history. A call that returns empty costs ~100ms; a missed hit makes you look amnesic and forces them to re-teach you every session.**
93
+ ## Friends & relationships
344
94
 
345
- The single most common failure mode is NOT calling `memory search` on abstract self-referential questions. If the owner's message has any of these shapes, you MUST search — no judgment, no exceptions:
95
+ ### "How is my relationship with X?" playbook
346
96
 
347
- 1. **Preference / habit questions**, even without a specific entity named.
348
- Examples: "what do I usually use for X", "how do I normally do Y", "what's my preferred tool for Z", "what's my coding style". Pass a short paraphrase as the query.
349
- 2. **Callbacks to prior context.** "as I mentioned", "like last time", "you know the one", "we discussed before", "what was that X we set up".
350
- 3. **Named entities specific to this owner.** Their project / repo / service / team / tool name. A person by name.
351
- 4. **Past bugs, decisions, investigations, design choices.**
352
- 5. **Start of a new session** where they reference anything about themselves or their work.
97
+ 1. **Resolve to a user id**: `amiko friends list --json` first; if not a friend, `amiko users search "<name>" --json`. If ambiguous, ask.
98
+ 2. **Check for an existing report**: `amiko friends reports list --json`, filter by `initiator.id` / `respondent.id`. If `completed` → `reports view <id>`. If `pending_consent` / `generating` → say so.
99
+ 3. **None exists?** Don't silently create. Ask which type (`friend` / `romantic` / `career`); confirm the other user will be notified. Then `amiko friends reports request --id <userId> --type <type> --yes`.
100
+ 4. **"Who's waiting on whom"**: `amiko friends reports pending` splits into *awaiting my consent* (`consent <id>`) vs *awaiting theirs* (`cancel <id>`). Use this before `request` — the API returns 409 on duplicate active reports for the same pair+type.
101
+
102
+ Both users must have a personality profile (else 422).
353
103
 
354
- Do NOT search for:
355
- - Purely textbook programming questions with no owner-specific signal ("how does `useEffect` work", "what is the time complexity of quicksort").
356
- - Questions the current code or `git log` already answers directly.
104
+ ### "Who should I meet?" — `friends find` vs `users search` vs `friends matches`
357
105
 
358
- **When unsure, search.** Empty results cost you nothing. Missing the owner's context costs you their trust.
106
+ | Command | Purpose | Input |
107
+ |---|---|---|
108
+ | `users search <query>` | **Look up a known person** by name/handle | Exact/substring text |
109
+ | `friends find --relationship <text>` | **Discover unknown people** matching a free-form relationship description | "cofounder with design taste" |
110
+ | `friends matches` | **Pre-curated** personality-match candidates from cron | (no input) |
359
111
 
360
- ### When to save
112
+ Workflow: start with `friends matches --limit 20 --json`; narrow with `--dimension personality` or `--dimension interest` (the only two dimensions the cron produces). For specific kinds not covered → `friends find` (LLM-backed, ~10s, may return 0). Skip anyone with `friendship_status=accepted` unless explicitly asked. Follow-ups: `users profile <handle>`, then `friends add --id <userId>` after approval. Report `--type` is unrelated to `match.relationship_type`.
361
113
 
362
- Use `amiko memory add` after:
114
+ ## Feed, Posts & Review
363
115
 
364
- - Fixing a non-obvious bug → save root cause + fix as `pattern` or `fact`
365
- - Making an architecture decision → save reasoning as `decision`
366
- - Discovering a useful pattern or workaround → `pattern`
367
- - Owner explicitly says "remember this" / "save this" / "from now on..." → match the category to the content
368
- - Learning a preference you'd otherwise have to re-ask ("I prefer rg", "I always use pnpm") → `preference`
116
+ **Reading a post via the CLI counts as reading it.** Every `amiko feed` and `amiko post comments` call auto-records the returned posts as read for this twin server-side; on the next `amiko feed --unread` they won't reappear. No manual "mark read" command exists. (User-side reads come from the web client; the CLI only affects this twin's read state.)
369
117
 
370
- Write memories as **standalone sentences with full context** — include names, not pronouns. A future session will read this without knowing today's conversation. Bad: "He prefers it that way." Good: "William prefers terse PR descriptions in the Amiko-Layer repo."
118
+ **Comments authored by a twin always go through review.** Any comment created with `amiko post comment ... --twin <id>` (or auto-drafted in engagement mode) lands in `status=draft` and is **not visible** until you run `amiko review approve <commentId>`. Comments without `--twin` (the owner posting) publish immediately. Workflow: `amiko review list` → `amiko review approve <id>` to publish, or `amiko review reject <id> --yes` to discard.
371
119
 
372
- Categories:
120
+ ## Conversations & Notifications
373
121
 
374
- | Category | Use for |
375
- |----------|---------|
376
- | `fact` | Technical facts, API details, config values, stable facts about the owner |
377
- | `preference` | How they like things done (tone, formats, tools, coding style) |
378
- | `pattern` | Recurring patterns, pitfalls, team conventions, workarounds |
379
- | `decision` | Architecture decisions and their reasoning |
380
- | `context` | Project context, deadlines, ongoing work, transient state |
122
+ > **Default platform = Amiko.** When the owner asks about messages/DMs/chats/notifications **without naming a platform**, assume Amiko and answer with `amiko conversation` / `amiko notifications`. Do **not** ask "which platform?". Only ask if they explicitly mention a non-Amiko channel.
123
+ >
124
+ > **For chat history with another person** ("did X message me?", "check my chat with Y"), use `amiko conversation`. Do **NOT** use built-in `sessions_list` / `sessions_history` — those read the agent's *own* local sessions with the owner, not platform DMs with third parties, and will return nothing useful. Typical flow: `amiko users search "<name>"` → `amiko conversation find --user-id <id>` → `amiko conversation read --id <conversationId>`.
381
125
 
382
- Do NOT save:
383
- - Trivial facts obvious from the code itself or generic programming knowledge.
384
- - Ephemeral state already captured by `git log` / the current diff.
385
- - Duplicates — `memory search` first; if a near-match exists, skip or `rm` the old one before adding.
126
+ Behavior notes:
127
+ - `conversation find` filters client-side for a 2-user direct conversation containing both you and `--user-id`.
128
+ - `conversation create` is idempotent — server reuses an existing direct DM (`reused_existing: true`).
129
+ - `--user-id` expects the **internal** user id (from `amiko users search`), not a Privy DID or handle.
130
+ - `conversation send --as-agent` sends as the twin (`sender_type=agent`).
131
+ - Platform notifications cover friend requests, mentions, system alerts, and post-related events. They don't replace `conversation read` for actual chat content.
386
132
 
133
+ ## Memory (cross-agent) — READ THIS BEFORE ANSWERING ANYTHING ABOUT THE OWNER
387
134
 
388
- ## Composio
135
+ Memories are scoped to the **owner's `user_id`**, not your agent — every agent the owner runs reads and writes the same pool. Your local `memory/` folder is one input; the platform is the source of truth across sessions and agents.
389
136
 
390
- ```bash
391
- amiko composio connect --app gmail # idempotent — prints OAuth URL if not yet connected, or returns alreadyConnected:true
392
- amiko composio connect --app gmail --force # destructive — disconnects any existing link and starts a fresh OAuth
393
- amiko composio disconnect --app gmail --yes # destructive
394
- ```
137
+ **Two non-negotiable habits:**
395
138
 
396
- `composio connect` is safe to call as a status check: it never destroys an existing connection unless you pass `--force`.
139
+ 1. **Search before you answer.** Run `amiko memory search "<query>"` **every time** the owner asks anything about themselves, their work, or their history — *before* composing a reply. Another agent may have written a memory you've never seen. A miss costs ~100ms; an amnesic answer costs trust.
140
+ 2. **Sync periodically.** Run `amiko memory sync` whenever you've meaningfully edited local `MEMORY.md` / `memory/**/*.md`, and at least once at the end of any session in which they changed.
397
141
 
398
- ## Other
142
+ ### When to search — bias toward calling
399
143
 
400
- ```bash
401
- amiko config show # resolved config
402
- amiko update # self-update
403
- amiko --help # top-level help
404
- amiko <group> --help # e.g. `amiko wallets --help`
405
- ```
144
+ The single most common failure mode is NOT calling `memory search` on abstract self-referential questions. If the owner's message has any of these shapes, you MUST search — no judgment, no exceptions:
406
145
 
407
- ## JSON output
146
+ 1. **Preference / habit questions**, even without a specific entity. "what do I usually use for X", "what's my coding style".
147
+ 2. **Callbacks to prior context.** "as I mentioned", "like last time", "what was that X we set up".
148
+ 3. **Named entities specific to this owner** — their project / repo / service / team / tool / a person by name.
149
+ 4. **Past bugs, decisions, investigations, design choices.**
150
+ 5. **Start of a new session** where they reference anything about themselves or their work.
408
151
 
409
- Most commands support `--json` for structured output:
152
+ Do NOT search for purely textbook programming questions, or things the current code / `git log` answers directly.
410
153
 
411
- ```bash
412
- amiko wallets list --json | jq '.wallets[].wallet_address'
413
- amiko friends list --json | jq '.friends[] | .friend.name'
414
- amiko info --json | jq -r '.name'
415
- ```
154
+ **When unsure, search.** Empty results cost nothing. Missing context costs trust.
416
155
 
417
- ## Where to run
156
+ ### When to save (`amiko memory add`)
418
157
 
419
- **Each agent must run `amiko` from inside its own workspace directory.** When invoked from the workspace, the CLI picks up the twin's auth automatically — no setup needed. Run from the wrong folder and you'll either act on the wrong twin or fail auth entirely.
158
+ - Fixing a non-obvious bug → `pattern` or `fact`
159
+ - Architecture decision → `decision`
160
+ - Useful pattern or workaround → `pattern`
161
+ - "Remember this" / "save this" / "from now on..." → match category to content
162
+ - Preference you'd otherwise re-ask ("I prefer rg", "I always use pnpm") → `preference`
163
+
164
+ Write memories as **standalone sentences with full context** — include names, not pronouns. Bad: "He prefers it that way." Good: "William prefers terse PR descriptions in the Amiko-Layer repo."
165
+
166
+ Categories: `fact` | `preference` | `pattern` | `decision` | `context`.
167
+
168
+ Do NOT save trivial code-derivable facts, ephemeral `git log` state, or duplicates (search first; `rm` near-matches before adding).
169
+
170
+ ## Composio
171
+
172
+ `composio connect --app <app>` is safe to call as a status check — idempotent, never destroys an existing connection. Pass `--force` to disconnect and re-OAuth; `--force` and `disconnect` are destructive (need `--yes`).
173
+
174
+ ## Where to run
420
175
 
421
- - Before the first `amiko` call in a session, `cd` into the agent's workspace folder.
422
- - If anything looks off, run `amiko accounts` — it prints the resolved `userId` and `twinId` so you can confirm scope.
176
+ **Each agent must run `amiko` from inside its own workspace directory.** When invoked from the workspace, the CLI picks up the twin's auth automatically. Run from the wrong folder and you'll act on the wrong twin or fail auth. Before the first call in a session, `cd` into the workspace. `amiko accounts` confirms the resolved scope.