@heyamiko/amiko-cli 0.10.1-beta.2 → 0.10.1-beta.4

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.2)
1
+ # @heyamiko/amiko-cli (v0.10.1-beta.4)
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,17 @@ npm publish
393
393
 
394
394
  ## Changelog
395
395
 
396
+ ### 0.10.1-beta.4
397
+
398
+ - **`amiko conversation` removed (breaking).** DMs / chat history are now served by the openhermit gateway's built-in `session_list` / `session_read` tools (unified view across the agent's local sessions and platform DMs), so the CLI no longer needs `conversation list / find / create / read / send`. `src/commands/conversation.ts` deleted; index registration + command group removed; SKILL.md frontmatter and Conversations section rewritten to route DM questions to the gateway tools and the remaining notifications-only block renamed `## Notifications`. Skill description and command-groups paragraph updated; the "did anyone DM me?" example row dropped from the cold-start examples table. Update any scripts that ran `amiko conversation` — there is no shim, the command returns "unknown command".
399
+
400
+ ### 0.10.1-beta.3
401
+
402
+ - **`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:
403
+ - **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).
404
+ - **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).
405
+ - SKILL.md picks up a one-paragraph Drive section + three new rows in the Examples table (upload / download / search intents → exact `amiko drive` commands).
406
+
396
407
  ### 0.10.1-beta.2
397
408
 
398
409
  - **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`.
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,53 @@ 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);
29295
29326
  }, { json: opts.json });
29296
29327
  } catch (e5) {
29297
29328
  spinner?.stop();
@@ -29299,7 +29330,35 @@ function registerDocsCommand(program2) {
29299
29330
  process.exit(1);
29300
29331
  }
29301
29332
  });
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) => {
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).`));
29354
+ }, { json: opts.json });
29355
+ } catch (e5) {
29356
+ spinner?.stop();
29357
+ console.error(error(e5 instanceof Error ? e5.message : String(e5)));
29358
+ process.exit(1);
29359
+ }
29360
+ });
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
@@ -30668,187 +30951,6 @@ function registerComposioCommand(program2) {
30668
30951
  });
30669
30952
  }
30670
30953
 
30671
- // src/commands/conversation.ts
30672
- function describeConversation(c, selfUserId) {
30673
- if (c.title)
30674
- return c.title;
30675
- const others = c.participants?.filter((p) => !(p.participant_type === "user" && p.participant_id === selfUserId)) ?? [];
30676
- return others.map((p) => p.name ?? p.participant_id).join(", ") || c.id;
30677
- }
30678
- function findDirectWithUser(list, selfUserId, otherUserId) {
30679
- return list.find((c) => {
30680
- if (c.conversation_type !== "direct")
30681
- return false;
30682
- const ps = c.participants ?? [];
30683
- const userIds = ps.filter((p) => p.participant_type === "user" && !p.left_at).map((p) => p.participant_id);
30684
- if (userIds.length !== 2)
30685
- return false;
30686
- if (selfUserId && !userIds.includes(selfUserId))
30687
- return false;
30688
- return userIds.includes(otherUserId);
30689
- });
30690
- }
30691
- function registerConversationCommand(program2) {
30692
- program2.command("list").description("List your conversations").option("--archived <state>", "active | archived | all", "active").option("--limit <n>", "Max results (1–100)", "50").option("--offset <n>", "Pagination offset", "0").option("--json", "Output as JSON").action(async (opts) => {
30693
- const auth = resolveAuth();
30694
- const config2 = loadConfig();
30695
- const spinner = opts.json ? null : ora("Loading conversations...").start();
30696
- try {
30697
- const data = await amikoWebFetch(auth, "/api/conversations", {
30698
- query: {
30699
- archived: opts.archived,
30700
- limit: opts.limit,
30701
- offset: opts.offset
30702
- }
30703
- });
30704
- spinner?.stop();
30705
- renderOutput(data, (d) => {
30706
- if (!d.conversations.length) {
30707
- console.log(dim("No conversations."));
30708
- return;
30709
- }
30710
- console.log(heading(`Conversations — ${d.conversations.length} of ${d.pagination.total}`));
30711
- for (const c of d.conversations) {
30712
- console.log("");
30713
- console.log(label("ID", c.id));
30714
- console.log(label("With", describeConversation(c, config2.userId)));
30715
- console.log(label("Type", c.conversation_type));
30716
- if (c.last_activity_at)
30717
- console.log(label("Last activity", c.last_activity_at));
30718
- if (c.unread_count)
30719
- console.log(label("Unread", String(c.unread_count)));
30720
- }
30721
- if (d.pagination.has_more) {
30722
- console.log("");
30723
- console.log(dim(`More available — pass --offset ${Number(opts.offset ?? 0) + d.conversations.length}`));
30724
- }
30725
- }, { json: opts.json });
30726
- } catch (e5) {
30727
- spinner?.stop();
30728
- console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30729
- process.exit(1);
30730
- }
30731
- });
30732
- program2.command("find").description("Find an existing direct conversation with a user").requiredOption("--user-id <id>", "Internal user id of the other party").option("--json", "Output as JSON").action(async (opts) => {
30733
- const auth = resolveAuth();
30734
- const config2 = loadConfig();
30735
- const spinner = opts.json ? null : ora("Searching...").start();
30736
- try {
30737
- const data = await amikoWebFetch(auth, "/api/conversations", { query: { archived: "all", limit: 100 } });
30738
- spinner?.stop();
30739
- const match = findDirectWithUser(data.conversations, config2.userId, opts.userId);
30740
- if (opts.json) {
30741
- console.log(JSON.stringify({ conversation: match ?? null }, null, 2));
30742
- return;
30743
- }
30744
- if (!match) {
30745
- console.log(dim(`No direct conversation with user ${opts.userId}. Run \`amiko conversation create --user-id ${opts.userId}\` to start one.`));
30746
- return;
30747
- }
30748
- console.log(success("Found conversation"));
30749
- console.log(label("ID", match.id));
30750
- console.log(label("With", describeConversation(match, config2.userId)));
30751
- if (match.last_activity_at)
30752
- console.log(label("Last activity", match.last_activity_at));
30753
- } catch (e5) {
30754
- spinner?.stop();
30755
- console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30756
- process.exit(1);
30757
- }
30758
- });
30759
- program2.command("create").description("Create (or reuse) a direct conversation with another user").requiredOption("--user-id <id>", "Internal user id of the other party").option("--json", "Output as JSON").action(async (opts) => {
30760
- const auth = resolveAuth();
30761
- const spinner = opts.json ? null : ora("Creating conversation...").start();
30762
- try {
30763
- const data = await amikoWebFetch(auth, "/api/conversations", {
30764
- method: "POST",
30765
- body: {
30766
- participant_ids: [opts.userId],
30767
- participant_types: ["user"],
30768
- conversation_type: "direct"
30769
- }
30770
- });
30771
- spinner?.stop();
30772
- if (opts.json) {
30773
- console.log(JSON.stringify(data, null, 2));
30774
- return;
30775
- }
30776
- console.log(success(data.reused_existing ? "Reused existing conversation" : "Conversation created"));
30777
- console.log(label("ID", data.conversation.id));
30778
- } catch (e5) {
30779
- spinner?.stop();
30780
- console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30781
- process.exit(1);
30782
- }
30783
- });
30784
- program2.command("read").description("Read messages from a conversation (oldest-first, paginated)").requiredOption("--id <conversationId>", "Target conversation id").option("--limit <n>", "Max messages (1–100)", "50").option("--before-cursor <cursor>", "Page back via next_cursor from a prior call").option("--json", "Output as JSON").action(async (opts) => {
30785
- const auth = resolveAuth();
30786
- const spinner = opts.json ? null : ora("Loading messages...").start();
30787
- try {
30788
- const data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(opts.id)}/messages`, {
30789
- query: {
30790
- limit: opts.limit,
30791
- before_cursor: opts.beforeCursor
30792
- }
30793
- });
30794
- spinner?.stop();
30795
- if (opts.json) {
30796
- console.log(JSON.stringify(data, null, 2));
30797
- return;
30798
- }
30799
- if (!data.messages.length) {
30800
- console.log(dim("No messages."));
30801
- return;
30802
- }
30803
- console.log(heading(`Messages — ${data.messages.length}`));
30804
- for (const m of data.messages) {
30805
- console.log("");
30806
- console.log(label(m.name ?? m.sender_id ?? "?", m.created_at?.slice(0, 19) ?? ""));
30807
- if (m.content)
30808
- console.log(m.content);
30809
- if (m.message_type && m.message_type !== "text")
30810
- console.log(dim(`[${m.message_type}]`));
30811
- console.log(dim(`id: ${m.id}`));
30812
- }
30813
- if (data.has_more && data.next_cursor) {
30814
- console.log("");
30815
- console.log(dim(`Older messages available — pass --before-cursor ${data.next_cursor}`));
30816
- }
30817
- } catch (e5) {
30818
- spinner?.stop();
30819
- console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30820
- process.exit(1);
30821
- }
30822
- });
30823
- program2.command("send").description("Send a message into a conversation").requiredOption("--id <conversationId>", "Target conversation id").requiredOption("--message <text>", "Message body").option("--reply-to <messageId>", "Reply to a specific message").option("--as-agent", "Send as the twin agent (sender_type=agent, is_auto_reply=true). Twin must be a participant.").option("--json", "Output as JSON").action(async (opts) => {
30824
- const auth = requireAuth();
30825
- const spinner = opts.json ? null : ora("Sending message...").start();
30826
- try {
30827
- const data = await amikoWebFetch(auth, `/api/conversations/${encodeURIComponent(opts.id)}/messages`, {
30828
- method: "POST",
30829
- body: {
30830
- content: opts.message,
30831
- message_type: "text",
30832
- ...opts.replyTo ? { reply_to_id: opts.replyTo } : {},
30833
- ...opts.asAgent ? { as_agent: true } : {}
30834
- }
30835
- });
30836
- spinner?.stop();
30837
- if (opts.json) {
30838
- console.log(JSON.stringify(data, null, 2));
30839
- return;
30840
- }
30841
- console.log(success("Message sent"));
30842
- if (data.message?.id)
30843
- console.log(label("Message ID", data.message.id));
30844
- } catch (e5) {
30845
- spinner?.stop();
30846
- console.error(error(e5 instanceof Error ? e5.message : String(e5)));
30847
- process.exit(1);
30848
- }
30849
- });
30850
- }
30851
-
30852
30954
  // src/commands/notifications.ts
30853
30955
  function registerNotificationsCommand(program2) {
30854
30956
  program2.command("list").description("List platform notifications (friend requests, system alerts, activity)").option("--limit <n>", "Max results (1–100)", "20").option("--cursor <iso>", "Page older items via next_cursor from a prior call").option("--unread", "Show only unread").option("--json", "Output as JSON").action(async (opts) => {
@@ -31513,7 +31615,7 @@ program2.name("amiko").description("Amiko CLI — swap tokens, manage credits, b
31513
31615
  var markets = program2.command("markets").description("MPP marketplace — discover and call paid AMIKO services");
31514
31616
  var wallets = program2.command("wallets").description("Manage twin wallets — list, swap, bridge");
31515
31617
  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");
31618
+ var drive = program2.command("drive").alias("docs").description("Manage twin drive — upload/download/search files, organise into folders");
31517
31619
  var voice = program2.command("voice").description("Manage twin voice — design, create, clone, reset");
31518
31620
  var avatar = program2.command("avatar").description("Manage twin avatar image");
31519
31621
  var friends = program2.command("friends").description("Manage friendships — list, requests, add, accept, remove, reports, matches");
@@ -31521,7 +31623,6 @@ var users = program2.command("users").description("Search users and view public
31521
31623
  var post = program2.command("post").description("Create posts and comments on the feed");
31522
31624
  var review = program2.command("review").description("Review queue — list, approve, or reject twin-drafted comments");
31523
31625
  var composio = program2.command("composio").description("Manage Composio app connections");
31524
- var conversation = program2.command("conversation").description("Conversations — list, find, create, send messages between users/agents");
31525
31626
  var notifications = program2.command("notifications").description("Platform notifications — list, mark as read");
31526
31627
  registerCreditsCommand(program2);
31527
31628
  registerSearchCommand(markets);
@@ -31539,7 +31640,7 @@ registerDiscoveryCommand(markets);
31539
31640
  registerUpdateCommand(program2);
31540
31641
  registerAccountsCommand(program2);
31541
31642
  registerTwinCommand(twin);
31542
- registerDocsCommand(docs);
31643
+ registerDriveCommand(drive);
31543
31644
  registerVoiceCommand(voice);
31544
31645
  registerAvatarCommand(avatar);
31545
31646
  registerFriendsCommand(friends);
@@ -31548,7 +31649,6 @@ registerFeedCommand(program2);
31548
31649
  registerPostCommand(post);
31549
31650
  registerReviewCommand(review);
31550
31651
  registerComposioCommand(composio);
31551
- registerConversationCommand(conversation);
31552
31652
  registerNotificationsCommand(notifications);
31553
31653
  registerMemoryCommand(program2);
31554
31654
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.10.1-beta.2",
3
+ "version": "0.10.1-beta.4",
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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: amiko-cli
3
- description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read and send platform DMs with other users (conversations list/find/read/send, the right tool when the owner asks "did X message me?" or "check my chat history with Y" — NOT the built-in sessions_list/sessions_history, which only see the agent's own local sessions), read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, call paid MPP marketplace services (X/Twitter search, image generation, Amazon product search, TTS/STT, AI chat, SFX/music), manage the twin's identity and RAG documents, voice and avatar, social graph (friends, posts, comments, feed), and Composio OAuth connections. Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). Use this skill whenever the user asks about their Amiko inbox/DMs/notifications or to do anything that would show up in their Amiko account or cost AMIKO/credits.
3
+ description: The Amiko CLI lets an agent act on the Amiko platform end-to-end — read platform notifications (friend requests, mentions, system alerts), search and write **cross-agent memories** about the owner (what other agents already know — preferences, decisions, facts), manage Solana/Base wallets (create, swap via Jupiter, bridge USDC via Across, transfer tokens to external addresses), top up and spend credits, call paid MPP marketplace services (X/Twitter search, image generation, Amazon product search, TTS/STT, AI chat, SFX/music), manage the twin's identity and drive (files, folders, RAG), voice and avatar, social graph (friends, posts, comments, feed), and Composio OAuth connections. Auth is automatic when run from the agent's workspace folder; payments are platform-custodied (no keys on disk). DMs / chat sessions are NOT in this CLI — use the openhermit gateway's built-in session_list / session_read tools for both local sessions and platform DMs. Use this skill whenever the user asks about their Amiko notifications, drive, memory, social graph, wallets, credits, or marketplace services — anything that would show up in their Amiko account or cost AMIKO/credits.
4
4
  homepage: https://platform.heyamiko.com
5
5
  metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["node"]}}}
6
6
  ---
@@ -20,9 +20,11 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
20
20
  | "what's my balance?" | shell → `amiko credits balance` |
21
21
  | "top up 10k credits with AMIKO" | shell → `amiko credits topup 10000 --token AMIKO --yes` |
22
22
  | "swap 1 SOL to USDC" | shell → `amiko wallets swap quote 1 SOL USDC` (then send with `--yes` after approval) |
23
- | "did anyone DM me?" | shell → `amiko conversation list` |
24
23
  | "any notifications?" | shell → `amiko notifications list --unread` |
25
24
  | "any new posts I haven't seen?" | shell → `amiko feed --unread` |
25
+ | "upload report.pdf to my drive" | shell → `amiko drive upload ./report.pdf` |
26
+ | "download the file with id X" | shell → `amiko drive download X` |
27
+ | "find files about Q1 revenue" | shell → `amiko drive search "Q1 revenue"` |
26
28
  | "what comments are on my post?" | shell → `amiko post comments --id <postId>` |
27
29
  | "search memory for X" | shell → `amiko memory search "X"` |
28
30
  | "what can amiko do?" | shell → `amiko --help` |
@@ -36,7 +38,9 @@ The CLI is installed globally and is pre-authenticated when you're inside your w
36
38
 
37
39
  ## Command groups
38
40
 
39
- Discover everything via `amiko --help`. Groups: `markets` (paid MPP services), `wallets`, `credits`, `twin`, `docs`, `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`.
41
+ 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`, `notifications`, `memory`, plus the top-level `accounts`, `info`, `config`, `update`. All twin-scoped commands accept `--twin <id>`. Most commands support `--json`.
42
+
43
+ > **DMs / chat sessions are NOT in this CLI.** Use the openhermit gateway's built-in `session_list` / `session_read` tools for both the agent's local sessions and platform DMs with other users. The previous `amiko conversation` namespace was removed in 0.10.1-beta.4.
40
44
 
41
45
  ## Critical Rules
42
46
 
@@ -83,6 +87,10 @@ Supported: AMIKO, SOL, USDC, USDT. Balance can lag a few seconds after topup —
83
87
 
84
88
  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.
85
89
 
90
+ ## Drive (files & folders)
91
+
92
+ 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.
93
+
86
94
  ## Friends & relationships
87
95
 
88
96
  ### "How is my relationship with X?" playbook
@@ -110,18 +118,13 @@ Workflow: start with `friends matches --limit 20 --json`; narrow with `--dimensi
110
118
 
111
119
  **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.
112
120
 
113
- ## Conversations & Notifications
121
+ ## Notifications
114
122
 
115
- > **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
+ > **Default platform = Amiko.** When the owner asks about notifications **without naming a platform**, assume Amiko and answer with `amiko notifications`. Only ask if they explicitly mention a non-Amiko channel.
116
124
  >
117
- > **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>`.
118
-
119
- Behavior notes:
120
- - `conversation find` filters client-side for a 2-user direct conversation containing both you and `--user-id`.
121
- - `conversation create` is idempotent — server reuses an existing direct DM (`reused_existing: true`).
122
- - `--user-id` expects the **internal** user id (from `amiko users search`), not a Privy DID or handle.
123
- - `conversation send --as-agent` sends as the twin (`sender_type=agent`).
124
- - Platform notifications cover friend requests, mentions, system alerts, and post-related events. They don't replace `conversation read` for actual chat content.
125
+ > **DMs / chat history are handled by the openhermit gateway**, not the CLI. For "did X message me?" / "check my chat with Y", use the built-in `session_list` / `session_read` tools — they cover both the agent's local sessions and platform DMs in the unified gateway view.
126
+
127
+ Platform notifications cover friend requests, mentions, system alerts, and post-related events.
125
128
 
126
129
  ## Memory (cross-agent) — READ THIS BEFORE ANSWERING ANYTHING ABOUT THE OWNER
127
130