@heyamiko/amiko-cli 0.10.1-beta.2 → 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.2)
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,13 @@ 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
+
396
403
  ### 0.10.1-beta.2
397
404
 
398
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`.
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.2",
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
@@ -23,6 +23,9 @@ Call your shell tool (your runtime calls it `bash`, `shell`, `run`, or similar)
23
23
  | "did anyone DM me?" | shell → `amiko conversation list` |
24
24
  | "any notifications?" | shell → `amiko notifications list --unread` |
25
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"` |
26
29
  | "what comments are on my post?" | shell → `amiko post comments --id <postId>` |
27
30
  | "search memory for X" | shell → `amiko memory search "X"` |
28
31
  | "what can amiko do?" | shell → `amiko --help` |
@@ -36,7 +39,7 @@ The CLI is installed globally and is pre-authenticated when you're inside your w
36
39
 
37
40
  ## Command groups
38
41
 
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`.
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`.
40
43
 
41
44
  ## Critical Rules
42
45
 
@@ -83,6 +86,10 @@ Supported: AMIKO, SOL, USDC, USDT. Balance can lag a few seconds after topup —
83
86
 
84
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.
85
88
 
89
+ ## Drive (files & folders)
90
+
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.
92
+
86
93
  ## Friends & relationships
87
94
 
88
95
  ### "How is my relationship with X?" playbook