@wirecat/drive-cli 0.2.0 → 0.4.0

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.
Files changed (90) hide show
  1. package/README.md +9 -8
  2. package/dist/api-contract.d.ts.map +1 -1
  3. package/dist/api-contract.js +4 -2
  4. package/dist/api-contract.js.map +1 -1
  5. package/dist/bin/drive.js +16 -20
  6. package/dist/bin/drive.js.map +1 -1
  7. package/dist/catalog-commands.d.ts +13 -0
  8. package/dist/catalog-commands.d.ts.map +1 -0
  9. package/dist/catalog-commands.js +111 -0
  10. package/dist/catalog-commands.js.map +1 -0
  11. package/dist/catalog.d.ts +43 -0
  12. package/dist/catalog.d.ts.map +1 -0
  13. package/dist/catalog.js +107 -0
  14. package/dist/catalog.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +17 -3
  18. package/dist/config.js.map +1 -1
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +13 -5
  21. package/dist/errors.js.map +1 -1
  22. package/dist/extraction.d.ts +1 -1
  23. package/dist/extraction.d.ts.map +1 -1
  24. package/dist/extraction.js +9 -5
  25. package/dist/extraction.js.map +1 -1
  26. package/dist/failure.d.ts +65 -0
  27. package/dist/failure.d.ts.map +1 -0
  28. package/dist/failure.js +86 -0
  29. package/dist/failure.js.map +1 -0
  30. package/dist/google-api.d.ts.map +1 -1
  31. package/dist/google-api.js +3 -1
  32. package/dist/google-api.js.map +1 -1
  33. package/dist/google.d.ts +5 -0
  34. package/dist/google.d.ts.map +1 -1
  35. package/dist/google.js +80 -12
  36. package/dist/google.js.map +1 -1
  37. package/dist/knowledge-commands.d.ts.map +1 -1
  38. package/dist/knowledge-commands.js +150 -113
  39. package/dist/knowledge-commands.js.map +1 -1
  40. package/dist/knowledge.d.ts +2 -2
  41. package/dist/knowledge.d.ts.map +1 -1
  42. package/dist/knowledge.js +11 -5
  43. package/dist/knowledge.js.map +1 -1
  44. package/dist/login.d.ts +1 -1
  45. package/dist/login.d.ts.map +1 -1
  46. package/dist/login.js +11 -5
  47. package/dist/login.js.map +1 -1
  48. package/dist/mcp.d.ts +22 -4
  49. package/dist/mcp.d.ts.map +1 -1
  50. package/dist/mcp.js +7 -15
  51. package/dist/mcp.js.map +1 -1
  52. package/dist/model.d.ts +67 -11
  53. package/dist/model.d.ts.map +1 -1
  54. package/dist/model.js +17 -0
  55. package/dist/model.js.map +1 -1
  56. package/dist/policy.d.ts.map +1 -1
  57. package/dist/policy.js +11 -4
  58. package/dist/policy.js.map +1 -1
  59. package/dist/profile-lock.d.ts.map +1 -1
  60. package/dist/profile-lock.js +4 -2
  61. package/dist/profile-lock.js.map +1 -1
  62. package/dist/program.d.ts.map +1 -1
  63. package/dist/program.js +191 -152
  64. package/dist/program.js.map +1 -1
  65. package/dist/registry.d.ts +7 -1
  66. package/dist/registry.d.ts.map +1 -1
  67. package/dist/registry.js +28 -2
  68. package/dist/registry.js.map +1 -1
  69. package/dist/remote-commands.d.ts.map +1 -1
  70. package/dist/remote-commands.js +95 -76
  71. package/dist/remote-commands.js.map +1 -1
  72. package/dist/scope.d.ts +3 -0
  73. package/dist/scope.d.ts.map +1 -0
  74. package/dist/scope.js +15 -0
  75. package/dist/scope.js.map +1 -0
  76. package/dist/state.d.ts.map +1 -1
  77. package/dist/state.js +27 -6
  78. package/dist/state.js.map +1 -1
  79. package/dist/store.d.ts +4 -2
  80. package/dist/store.d.ts.map +1 -1
  81. package/dist/store.js +90 -10
  82. package/dist/store.js.map +1 -1
  83. package/dist/sync.d.ts +6 -2
  84. package/dist/sync.d.ts.map +1 -1
  85. package/dist/sync.js +121 -52
  86. package/dist/sync.js.map +1 -1
  87. package/dist/tokens.d.ts.map +1 -1
  88. package/dist/tokens.js +8 -4
  89. package/dist/tokens.js.map +1 -1
  90. package/package.json +7 -4
package/dist/program.js CHANGED
@@ -2,22 +2,22 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { CliError, createDeadline, createOutputBuffer, createRenderer, processStreams, visibleControls, } from "@wirecat/cli-base";
3
3
  import { annotate, describeOptions, flatten, metaOf } from "@wirecat/cli-base/commands";
4
4
  import { formatSuggestions, suggest } from "@wirecat/cli-base/completion";
5
- import { commandWords, liftProfile, parseDuration, refuseCommandName } from "@wirecat/cli-messaging/cli";
5
+ import { commandWords, liftProfile, parseDuration, refuseCommandName } from "@wirecat/cli-core/cli";
6
6
  import { Command, Help, InvalidArgumentError, Option } from "commander";
7
- import { operationOf, validateRequest } from "./api-contract.js";
8
- import { activeProfile, application, fileId, profileName, readConfig, writeConfig } from "./config.js";
7
+ import { validateRequest } from "./api-contract.js";
8
+ import { installCatalogCommands } from "./catalog-commands.js";
9
+ import { activeProfile, application, checkTextLimit, fileId, pictureSearch, profileName, readConfig, writeConfig, } from "./config.js";
9
10
  import { googleError } from "./errors.js";
10
11
  import { memoryModel } from "./extraction.js";
11
- import { googleSource } from "./google.js";
12
+ import { googleSource, MAX_ENTRIES } from "./google.js";
12
13
  import { apiMethod, googleApi } from "./google-api.js";
13
14
  import { installKnowledgeCommands } from "./knowledge-commands.js";
14
15
  import { login } from "./login.js";
15
- import { FOLDER } from "./model.js";
16
+ import { FOLDER, metadataJson, skipJson } from "./model.js";
16
17
  import { checkPolicy, guardedGoogleWrite, methodPolicyKeys } from "./policy.js";
17
18
  import { lockProfile } from "./profile-lock.js";
18
- import { ACCOUNT_FREE_COMMANDS, describeDriveProgram } from "./registry.js";
19
+ import { ACCOUNT_FREE_COMMANDS, commandsDocument, describeDriveProgram, examples } from "./registry.js";
19
20
  import { installRemoteCommands } from "./remote-commands.js";
20
- import { requestSchema } from "./schema.js";
21
21
  import { readSnapshot, readState } from "./state.js";
22
22
  import { openIndex } from "./store.js";
23
23
  import { sync } from "./sync.js";
@@ -39,7 +39,7 @@ export const createProgram = (options = {}) => {
39
39
  const env = options.env ?? process.env;
40
40
  const streams = options.streams ?? processStreams;
41
41
  const program = new Command("drive")
42
- .description("Manage Google Drive and Docs, local Markdown, shared knowledge and search")
42
+ .description("manage Google Drive and Docs, local Markdown, shared knowledge and search")
43
43
  .version(VERSION)
44
44
  .usage("<profile> [options] <command>")
45
45
  .option("--profile <name>", "explicit Google account profile (alternative to the first word)")
@@ -47,8 +47,9 @@ export const createProgram = (options = {}) => {
47
47
  .option("--jsonl", "print machine-readable JSON lines")
48
48
  .option("--agent-json", "show control and direction characters visibly in machine output")
49
49
  .option("--quiet", "suppress optional diagnostics")
50
+ .option("--debug", "on failure, show the full error record: cause and stack")
50
51
  .option("--timeout <duration>", "whole-command deadline, e.g. 30s", (raw) => parseDuration(raw, "--timeout"))
51
- .option("--max-output-bytes <n>", "bound buffered result output", offset, 4 * 1024 * 1024)
52
+ .option("--max-output-bytes <n>", "largest result to print, in bytes; 0 for no limit", offset, 4 * 1024 * 1024)
52
53
  .option("--read-only", "refuse local and remote mutations")
53
54
  .option("--offline", "answer only operations that use local cached data")
54
55
  .option("--dry-run", "validate command/policy and preview without acting")
@@ -69,7 +70,7 @@ export const createProgram = (options = {}) => {
69
70
  throw new CliError("validation_error", "choose a profile explicitly: drive <profile> <command> or drive --profile <name> <command>; " +
70
71
  (choices.length
71
72
  ? `configured profiles: ${choices.join(", ")}`
72
- : "no profiles configured; connect with drive personal session start"), { profiles: choices });
73
+ : "no profiles configured; connect with drive personal session start"), { reason: "profile.required", profiles: choices });
73
74
  }
74
75
  return profileName(name);
75
76
  };
@@ -129,16 +130,14 @@ export const createProgram = (options = {}) => {
129
130
  await index.close();
130
131
  }
131
132
  };
132
- const session = program.command("session").description("Google browser sign-in and local account profiles");
133
- session
134
- .command("start")
135
- .description("Approve Google access in your browser; read-only unless --write is requested")
136
- .option("--client-file <file>", "use your own Desktop OAuth application JSON instead of WireCat's app")
137
- .option("--credential-storage <storage>", "keyring or explicit private-file storage", "keyring")
138
- .option("--no-browser", "print the sign-in URL without opening a browser")
139
- .option("--write", "request remote Drive/Docs mutation permissions")
140
- .option("--activity", "also request Drive Activity history permissions")
141
- .option("--apps", "also request the installed Drive applications permission")
133
+ const session = program.command("session").description("sign in to Google and manage local profiles");
134
+ examples(session.command("start").description("sign in to Google in the browser; read-only unless you pass --write"), ["drive personal session start", "drive work session start --write --activity"])
135
+ .option("--client-file <file>", "path to your own Desktop OAuth client JSON, used instead of WireCat's")
136
+ .option("--credential-storage <storage>", "where the sign-in is kept: keyring, or file for a private file", "keyring")
137
+ .option("--no-browser", "print the sign-in URL instead of opening a browser")
138
+ .option("--write", "also ask for permission to change Drive files and Docs")
139
+ .option("--activity", "also ask for permission to read Drive Activity history")
140
+ .option("--apps", "also ask for permission to see installed Drive apps")
142
141
  .action(async (args) => {
143
142
  if (args.credentialStorage !== "keyring" && args.credentialStorage !== "file")
144
143
  throw new CliError("validation_error", "credential storage is keyring or file");
@@ -156,35 +155,33 @@ export const createProgram = (options = {}) => {
156
155
  showUrl: (url) => streams.diagnostic(`Open Google sign-in: ${url}\n`),
157
156
  }));
158
157
  });
159
- session
160
- .command("list")
161
- .description("Configured account profiles; no credentials are printed")
162
- .action(() => output({
158
+ examples(session.command("list").description("list the configured profiles; credentials are never printed"), [
159
+ "drive personal session list",
160
+ ]).action(() => output({
163
161
  scope: "all-configured-profiles",
164
162
  items: Object.entries(readConfig(env).profiles).map(([name, p]) => ({
165
163
  profile: name,
166
- selectedFolders: p.roots.length,
164
+ selected_folders: p.roots.length,
167
165
  storage: p.storage,
168
166
  })),
169
167
  }));
170
- session
171
- .command("end")
172
- .description("Remove this profile's local sign-in; cached data is retained")
173
- .action(() => {
168
+ examples(session.command("end").description("sign this profile out; imported documents stay"), [
169
+ "drive personal session end",
170
+ ]).action(() => {
174
171
  const name = selected();
175
172
  const config = readConfig(env);
176
173
  const profile = activeProfile(env, name);
177
174
  credentials(env, profile.storage, options.keyring).remove(name);
178
175
  // Preserve the principal and roots so reauthorization resumes the same source identities.
179
176
  writeConfig(env, config);
180
- output({ profile: name, signedOut: true, cachedData: "retained" });
177
+ output({ profile: name, signed_out: true, cached_data: "retained" });
181
178
  });
182
- const folders = program.command("folders").description("Select Google folders for local ingestion");
183
- folders
179
+ const folders = program.command("folders").description("choose the Google Drive folders that sync imports");
180
+ const folderArgument = "folder id or Google Drive URL";
181
+ examples(folders
184
182
  .command("add")
185
- .argument("<url-or-id>")
186
- .description("Include a folder and its descendants")
187
- .action(async (input) => {
183
+ .argument("<folder>", folderArgument)
184
+ .description("import a folder and everything under it on the next sync"), ["drive personal folders add FOLDER_ID"]).action(async (input) => {
188
185
  const id = fileId(input);
189
186
  const config = readConfig(env);
190
187
  const name = selected();
@@ -198,15 +195,13 @@ export const createProgram = (options = {}) => {
198
195
  writeConfig(env, config);
199
196
  output({ id: metadata.id, name: metadata.name, added: true });
200
197
  });
201
- folders
202
- .command("list")
203
- .description("Selected folder IDs")
204
- .action(() => output({ items: activeProfile(env, selected()).roots.map((id) => ({ id })) }));
205
- folders
198
+ examples(folders.command("list").description("list the folder ids sync imports"), [
199
+ "drive personal folders list",
200
+ ]).action(() => output({ items: activeProfile(env, selected()).roots.map((id) => ({ id })) }));
201
+ examples(folders
206
202
  .command("remove")
207
- .argument("<url-or-id>")
208
- .description("Stop importing a folder; sync removes sources outside the remaining scope")
209
- .action((input) => {
203
+ .argument("<folder>", folderArgument)
204
+ .description("stop importing a folder; the next sync drops its documents"), ["drive personal folders remove FOLDER_ID"]).action((input) => {
210
205
  const id = fileId(input);
211
206
  const config = readConfig(env);
212
207
  const name = selected();
@@ -216,48 +211,53 @@ export const createProgram = (options = {}) => {
216
211
  writeConfig(env, config);
217
212
  output({ id, removed: true, next: `run drive ${name} sync to reconcile cached sources` });
218
213
  });
219
- program
214
+ examples(program
220
215
  .command("sync")
221
- .description("Fetch selected sources and resume a durable pending batch")
222
- .option("--max <n>", "maximum source operations this run", positive, 200)
223
- .option("--rescan", "reconcile selected folders from a fresh change token")
224
- .option("--embed", "embed pending shared note chunks using an already-downloaded model")
216
+ .summary("import what changed in the chosen folders, and finish an unfinished sync")
217
+ .description("import what changed in the chosen folders, and finish an unfinished sync; " +
218
+ `one sync reads at most ${MAX_ENTRIES} entries under the chosen folders, and as many changes`), ["drive personal sync", "drive personal sync --rescan --limit 500"])
219
+ .option("--limit <n>", "how many documents to read from Google in this run", positive, 200)
220
+ .option("--rescan", "read every chosen folder again instead of only what changed")
221
+ .option("--embed", "also index pending text for meaning search, with a model already downloaded")
225
222
  .action(async (args) => {
226
223
  const remote = activeProfile(env, selected()).roots.length ? await source() : undefined;
227
224
  const result = await withIndex(async (index) => ({
228
- ...(await sync(remote, index, env, selected(), args.max, activeSignal, args.rescan)),
225
+ ...(await sync(remote, index, env, selected(), args.limit, activeSignal, args.rescan)),
229
226
  ...(args.embed ? { embeddings: await index.embed(600) } : {}),
230
227
  }));
231
228
  output(result);
232
229
  if (result.errors.length || result.pending)
233
230
  streams.diagnostic(`${result.pending} source operations remain; run drive ${selected()} sync again\n`);
234
231
  if (result.errors.length)
235
- throw new CliError("provider_error", "sync completed partially; failed operations remain pending");
232
+ throw new CliError("provider_error", "sync completed partially; failed operations remain pending", {
233
+ reason: "sync.partial",
234
+ });
236
235
  });
237
- program
238
- .command("status")
239
- .description("Local sync checkpoint, cached sources and pending work")
240
- .action(() => {
236
+ examples(program.command("status").description("show the last sync, imported documents and pending work"), [
237
+ "drive personal status",
238
+ ]).action(() => {
241
239
  const name = selected();
242
240
  const profile = activeProfile(env, name);
243
241
  const state = readState(env, name, profile.principal);
244
242
  output({
245
243
  profile: name,
246
- selectedFolders: profile.roots,
244
+ selected_folders: profile.roots,
247
245
  documents: Object.keys(state.documents).length,
248
246
  pending: state.pending?.jobs.length ?? 0,
249
- skipped: state.skipped,
250
- lastSync: state.lastSync ?? null,
247
+ skipped: state.skipped.map(skipJson),
248
+ last_sync: state.lastSync ?? null,
251
249
  coverage: "Google Docs and Markdown/TXT; native export tab fidelity and assets are not verified",
252
- remoteChecked: false,
250
+ remote_checked: false,
253
251
  });
254
252
  });
255
- const documents = program.command("documents").description("Locally imported sources and exports");
256
- documents
257
- .command("list")
258
- .description("Imported sources with original Google URLs")
259
- .option("--tag <tag>", "filter by a local shared tag")
260
- .option("--person <reference>", "filter by a linked shared person reference")
253
+ const documents = program.command("documents").description("read imported documents and Google Docs");
254
+ const documentArgument = "document id or Google Docs URL";
255
+ examples(documents.command("list").description("list imported documents with their Google URLs"), [
256
+ "drive personal documents list",
257
+ "drive personal documents list --tag budget",
258
+ ])
259
+ .option("--tag <tag>", "only documents with this tag")
260
+ .option("--person <reference>", "only documents linked to this person:<uid> reference")
261
261
  .action(async (args) => {
262
262
  const profile = activeProfile(env, selected());
263
263
  const state = readState(env, selected(), profile.principal);
@@ -274,7 +274,7 @@ export const createProgram = (options = {}) => {
274
274
  });
275
275
  output({
276
276
  items: docs.map((doc) => ({
277
- ...doc.metadata,
277
+ ...metadataJson(doc.metadata),
278
278
  reference: doc.reference,
279
279
  tabs: doc.tabs,
280
280
  })),
@@ -285,36 +285,57 @@ export const createProgram = (options = {}) => {
285
285
  const profile = activeProfile(env, selected());
286
286
  const state = readState(env, selected(), profile.principal);
287
287
  if (!state.documents[id])
288
- throw new CliError("not_found", "this source is not imported in the selected profile");
288
+ throw new CliError("not_found", "this source is not imported in the selected profile", {
289
+ reason: "document.not-imported",
290
+ });
289
291
  const doc = state.documents[id];
290
292
  return { id, ref: doc.reference, hash: doc.hash, snapshot: readSnapshot(env, selected(), id) };
291
293
  };
292
- documents
294
+ examples(documents
293
295
  .command("show")
294
- .argument("<url-or-id>")
295
- .description("Imported text with tab and source metadata")
296
- .action((input) => {
296
+ .argument("<document>", documentArgument)
297
+ .description("show an imported document's text, tabs and metadata"), ["drive personal documents show DOC_ID"]).action((input) => {
297
298
  const { snapshot } = imported(input);
298
- output({ metadata: snapshot.metadata, text: snapshot.text, tabs: snapshot.tabs });
299
+ output({
300
+ metadata: metadataJson(snapshot.metadata),
301
+ text: snapshot.text,
302
+ tabs: snapshot.tabs,
303
+ ...(snapshot.notRead ? { not_read: snapshot.notRead } : {}),
304
+ });
299
305
  });
300
- documents
306
+ examples(documents
301
307
  .command("export")
302
- .argument("<url-or-id>")
303
- .requiredOption("--out <file>", "destination Markdown file")
304
- .description("Write the native Markdown snapshot; existing files are refused")
308
+ .argument("<document>", documentArgument)
309
+ .description("write an imported document to a new Markdown file"), ["drive personal documents export DOC_ID --out notes.md"])
310
+ .requiredOption("--out <file>", "path of the Markdown file to write; an existing file is refused")
305
311
  .action((input, args) => {
306
312
  const { id, snapshot } = imported(input);
307
- writeFileSync(args.out, snapshot.markdown, { flag: "wx", mode: 0o600 });
313
+ try {
314
+ writeFileSync(args.out, snapshot.markdown, { flag: "wx", mode: 0o600 });
315
+ }
316
+ catch (error) {
317
+ if (error.code === "EEXIST")
318
+ throw new CliError("validation_error", "the output file already exists; choose another --out", {
319
+ reason: "export.exists",
320
+ param: "--out",
321
+ });
322
+ throw error;
323
+ }
308
324
  output({ id, path: args.out, format: "markdown" });
309
325
  });
310
- program
326
+ examples(program
311
327
  .command("search")
312
- .argument("<query>")
313
- .description("Shared keyword and optional semantic search over imported Google sources")
314
- .option("--limit <n>", "maximum results", positive, 20)
315
- .option("--offset <n>", "results to skip", offset, 0)
316
- .option("--exact", "keyword search only, without stemming or meaning")
317
- .action(async (query, args) => output(await withIndex((index) => index.search(activeProfile(env, selected()).principal, query, { ...args, exact: args.exact ?? false }))));
328
+ .argument("<query>", "words to look for")
329
+ .description("search imported documents by words, and by meaning when indexed"), ["drive personal search budget", `drive personal search "quarterly plan" --exact --limit 5`])
330
+ .option("--limit <n>", "how many results to return", positive, 20)
331
+ .option("--offset <n>", "how many results to skip", offset, 0)
332
+ .option("--exact", "match the words exactly: no word forms, no meaning")
333
+ .action(async (query, args) => output(await withIndex((index) => index.search(activeProfile(env, selected()).principal, query, {
334
+ ...args,
335
+ exact: args.exact ?? false,
336
+ ...(pictureSearch(env, selected()) ? { pictures: true } : {}),
337
+ }))));
338
+ installCatalogCommands(program, { env, selected, output, positive, offset, withIndex });
318
339
  installKnowledgeCommands(program, {
319
340
  document: imported,
320
341
  output,
@@ -324,12 +345,14 @@ export const createProgram = (options = {}) => {
324
345
  run: async (input, fn) => {
325
346
  const lock = await lockProfile(env, selected());
326
347
  try {
327
- const { ref } = imported(input);
328
348
  return await withIndex(async (index) => {
329
349
  lock.assertHeld();
330
350
  if (!index.knowledge)
331
351
  throw new CliError("configuration_error", "index has no shared knowledge adapter");
332
- const result = await fn(index.knowledge, activeProfile(env, selected()).principal, ref);
352
+ const principal = activeProfile(env, selected()).principal;
353
+ const found = await index.catalog?.get(principal, fileId(input));
354
+ const ref = found?.ref ?? imported(input).ref;
355
+ const result = await fn(index.knowledge, principal, ref);
333
356
  lock.assertHeld();
334
357
  return result;
335
358
  });
@@ -342,7 +365,18 @@ export const createProgram = (options = {}) => {
342
365
  installRemoteCommands(program, {
343
366
  output,
344
367
  positive,
345
- readBody: options.readBody ?? ((path) => readFileSync(path, "utf8")),
368
+ readBody: options.readBody ??
369
+ ((path) => {
370
+ try {
371
+ return readFileSync(path, "utf8");
372
+ }
373
+ catch {
374
+ throw new CliError("validation_error", "the request body file could not be read", {
375
+ reason: "body.unreadable",
376
+ param: "--body-file",
377
+ });
378
+ }
379
+ }),
346
380
  call: async (method, params, body) => {
347
381
  const descriptor = apiMethod(method);
348
382
  validateRequest(method, params, body);
@@ -350,14 +384,16 @@ export const createProgram = (options = {}) => {
350
384
  for (const key of methodPolicyKeys(method))
351
385
  await checkPolicy(env, selected(), key, descriptor.mutates, flags, askOnce);
352
386
  if (flags.offline)
353
- throw new CliError("permission_error", "this operation needs Google; --offline was requested");
387
+ throw new CliError("permission_error", "this operation needs Google; --offline was requested", {
388
+ reason: "offline.refused",
389
+ });
354
390
  if (flags.dryRun)
355
391
  return {
356
- dryRun: true,
392
+ dry_run: true,
357
393
  method,
358
394
  mutates: descriptor.mutates,
359
395
  parameters: params,
360
- bodyProvided: body !== undefined,
396
+ body_provided: body !== undefined,
361
397
  };
362
398
  const act = async () => {
363
399
  try {
@@ -374,15 +410,17 @@ export const createProgram = (options = {}) => {
374
410
  return descriptor.mutates ? guardedGoogleWrite(env, selected(), `api.${method}`, act, flags, askOnce) : act();
375
411
  },
376
412
  });
377
- const config = program
378
- .command("config")
379
- .description("Shared-style defaults/profile settings; credentials are separate");
380
- config.command("show").action(() => output(readConfig(env)));
381
- config
382
- .command("set")
383
- .argument("<key>")
384
- .argument("<json-value>")
385
- .option("--defaults", "change defaults rather than the selected profile")
413
+ const config = program.command("config").description("read and change settings; credentials are kept apart");
414
+ examples(config.command("show").description("show the defaults and every profile's settings"), [
415
+ "drive personal config show",
416
+ ]).action(() => output(readConfig(env)));
417
+ examples(config.command("set").description("set one setting of this profile, or of the defaults"), [
418
+ "drive personal config set readOnly true",
419
+ `drive personal config set permissions.files '"readonly"' --defaults`,
420
+ ])
421
+ .argument("<key>", "readOnly, maxTextChars, pictureSearch, permissions.<key> or models.<purpose>.<field>")
422
+ .argument("<json-value>", "the new value, as JSON")
423
+ .option("--defaults", "change the defaults instead of this profile")
386
424
  .action((key, raw, args) => {
387
425
  const value = readConfig(env);
388
426
  if (args.defaults)
@@ -393,9 +431,9 @@ export const createProgram = (options = {}) => {
393
431
  if (!args.defaults)
394
432
  value.profiles[selected()] = target;
395
433
  const [head, ...parts] = key.split(".");
396
- if (!["readOnly", "permissions", "models"].includes(head ?? "") ||
434
+ if (!["readOnly", "permissions", "models", "maxTextChars", "pictureSearch"].includes(head ?? "") ||
397
435
  parts.some((part) => ["__proto__", "prototype", "constructor"].includes(part)))
398
- throw new CliError("validation_error", "config set accepts readOnly, permissions or models");
436
+ throw new CliError("validation_error", "config set accepts readOnly, permissions, models, maxTextChars or pictureSearch");
399
437
  let parsed;
400
438
  try {
401
439
  parsed = JSON.parse(raw);
@@ -403,7 +441,14 @@ export const createProgram = (options = {}) => {
403
441
  catch {
404
442
  throw new CliError("validation_error", "setting value must be JSON");
405
443
  }
406
- if (!parts.length)
444
+ if (head === "pictureSearch") {
445
+ if (args.defaults || parts.length || typeof parsed !== "boolean")
446
+ throw new CliError("validation_error", "pictureSearch is true or false, set per profile");
447
+ target.pictureSearch = parsed;
448
+ }
449
+ else if (head === "maxTextChars" && !parts.length)
450
+ target.maxTextChars = checkTextLimit(parsed);
451
+ else if (!parts.length)
407
452
  Object.assign(target, { [key]: parsed });
408
453
  else if (head === "permissions") {
409
454
  target.permissions ??= {};
@@ -419,10 +464,10 @@ export const createProgram = (options = {}) => {
419
464
  writeConfig(env, value);
420
465
  output({ key, updated: true, scope: args.defaults ? "defaults" : selected() });
421
466
  });
422
- program
423
- .command("complete")
424
- .description("Shell completion from the shared command registry")
425
- .argument("[words...]")
467
+ examples(program.command("complete").description("print a shell completion script, or complete words"), [
468
+ "drive complete zsh",
469
+ ])
470
+ .argument("[words...]", "a shell name, or after -- the words typed so far")
426
471
  .allowUnknownOption()
427
472
  .helpOption(false)
428
473
  .action(async (words) => {
@@ -435,25 +480,17 @@ export const createProgram = (options = {}) => {
435
480
  else
436
481
  streams.data(formatSuggestions(suggest({ commands: describeDriveProgram(program), globalOptions: describeOptions(program), words })));
437
482
  });
438
- program
439
- .command("schema")
440
- .argument("<method>")
441
- .description("Generated request schema and operation contract without Google access")
442
- .action((method) => {
443
- const operation = operationOf(method);
444
- output({
445
- operation,
446
- requestSchema: requestSchema(method),
447
- });
448
- });
449
- program
483
+ examples(program
450
484
  .command("models")
451
- .description("Configure the shared model gateway; keys stay outside config")
452
- .command("configure")
453
- .option("--purpose <purpose>", "shared model purpose", "memories")
454
- .addOption(new Option("--provider <provider>").choices(["openai", "anthropic", "off"]).makeOptionMandatory())
455
- .option("--model <model>", "model identifier for the selected provider")
456
- .option("--base-url <url>", "optional HTTP/S model endpoint without URL credentials")
485
+ .description("choose the model that proposes memories; keys stay out of config")
486
+ .command("set")
487
+ .description("set the provider and model for one use"), ["drive personal models set --provider openai --model MODEL_ID", "drive personal models set --provider off"])
488
+ .option("--purpose <purpose>", "what the model is used for", "memories")
489
+ .addOption(new Option("--provider <provider>", "who runs the model, or off to use none")
490
+ .choices(["openai", "anthropic", "off"])
491
+ .makeOptionMandatory())
492
+ .option("--model <model>", "model id at the provider")
493
+ .option("--base-url <url>", "HTTP or HTTPS address of the model API, without credentials in it")
457
494
  .action((args) => {
458
495
  const config = readConfig(env);
459
496
  config.defaults ??= {};
@@ -466,34 +503,31 @@ export const createProgram = (options = {}) => {
466
503
  writeConfig(env, config);
467
504
  output({ purpose: args.purpose, configured: true });
468
505
  });
469
- const mcp = program.command("mcp").description("Expose guarded commands over MCP stdio");
470
- mcp.option("--tools", "list offered tools without starting a server").action(async (args) => {
506
+ const mcp = examples(program.command("mcp").description("serve drive's commands to an agent over MCP on stdio"), [
507
+ "drive personal mcp",
508
+ "drive personal mcp --tools",
509
+ ]);
510
+ mcp
511
+ .option("--tools", "list the tools offered instead of starting the server")
512
+ .action(async (args) => {
471
513
  const { mcpTools, serveMcp } = await import("./mcp.js");
472
514
  if (args.tools)
473
515
  output({
474
516
  items: mcpTools({ ...options, env }, selected(), policyFlags()).map(({ name, schema, command }) => ({
475
517
  name,
476
- inputSchema: schema,
518
+ input_schema: schema,
477
519
  mutates: command.mutates,
478
520
  })),
479
521
  });
480
522
  else
481
523
  await serveMcp({ ...options, env, ...(activeSignal ? { signal: activeSignal } : {}) }, selected(), policyFlags());
482
524
  });
483
- program
484
- .command("commands")
485
- .argument("[path...]")
486
- .description("Discover the command tree and options as shared registry data")
487
- .action((path) => {
488
- const commands = describeDriveProgram(program);
489
- const found = path.length ? flatten(commands).filter((item) => item.path.join(" ") === path.join(" ")) : commands;
490
- if (path.length && !found.length)
491
- throw new CliError("not_found", "command path was not found");
492
- output({
493
- items: found,
494
- ...(path.length ? {} : { cli: "drive", commands, globalOptions: describeOptions(program) }),
495
- });
496
- });
525
+ examples(program.command("commands").description("print the command tree and its options as data"), [
526
+ "drive commands --json",
527
+ "drive commands files copy --json",
528
+ ])
529
+ .argument("[path...]", "only this command or group, as words")
530
+ .action((path) => output(commandsDocument(program, path)));
497
531
  for (const info of flatten(describeDriveProgram(program))) {
498
532
  let command = program;
499
533
  for (const part of info.path)
@@ -507,8 +541,8 @@ export const createProgram = (options = {}) => {
507
541
  "documents export",
508
542
  "tags add",
509
543
  "tags remove",
510
- "links add",
511
- "links remove",
544
+ "relationships add",
545
+ "relationships remove",
512
546
  "notes add",
513
547
  "notes edit",
514
548
  "notes remove",
@@ -517,9 +551,9 @@ export const createProgram = (options = {}) => {
517
551
  "memories stale",
518
552
  "memories extract",
519
553
  "facts add",
520
- "facts accept",
521
- "people link",
522
- "models configure",
554
+ "facts confirm",
555
+ "identities link",
556
+ "models set",
523
557
  "config set",
524
558
  ];
525
559
  const remoteWrites = [
@@ -537,7 +571,8 @@ export const createProgram = (options = {}) => {
537
571
  annotate(command, {
538
572
  origin: metaOf(command).origin ?? "handwritten",
539
573
  mutates: metaOf(command).mutates ?? (localWrites.includes(name) || remoteWrites.includes(name)),
540
- local: localWrites.includes(name) && name !== "session start" && name !== "memories extract",
574
+ local: metaOf(command).local ??
575
+ (localWrites.includes(name) && name !== "session start" && name !== "memories extract"),
541
576
  state: "beta",
542
577
  });
543
578
  }
@@ -564,18 +599,20 @@ export const createProgram = (options = {}) => {
564
599
  if (ACCOUNT_FREE_COMMANDS.has(words))
565
600
  return;
566
601
  selected();
567
- if (["commands", "complete", "schema", "config show", "config set", "session list"].includes(words))
602
+ if (["commands", "complete", "config show", "config set", "session list"].includes(words))
568
603
  return;
569
604
  const flags = policyFlags();
570
605
  const metadata = metaOf(action);
571
606
  const rawMethod = words === "api call" ? action.args[0] : undefined;
572
- const method = metadata.operationId ?? rawMethod;
607
+ const method = metadata.operation_id ?? rawMethod;
573
608
  if (rawMethod) {
574
609
  const descriptor = apiMethod(rawMethod);
575
610
  if (descriptor.mutates && !action.opts().allowWrite)
576
- throw new CliError("permission_error", "this method changes Google data; pass --allow-write");
611
+ throw new CliError("permission_error", "this method changes Google data; pass --allow-write", {
612
+ reason: "write.not-allowed",
613
+ });
577
614
  if (descriptor.destructive && !action.opts().allowDelete && !flags.allowDangerous)
578
- throw new CliError("permission_error", "this method deletes Google data; pass --allow-delete or --allow-dangerous");
615
+ throw new CliError("permission_error", "this method deletes Google data; pass --allow-delete or --allow-dangerous", { reason: "delete.not-allowed" });
579
616
  if (flags.allowDangerous)
580
617
  action.setOptionValue("allowDelete", true);
581
618
  }
@@ -596,12 +633,14 @@ export const createProgram = (options = {}) => {
596
633
  "memories extract",
597
634
  "sync",
598
635
  ].includes(words)))
599
- throw new CliError("permission_error", "this command may require network access; --offline was requested");
636
+ throw new CliError("permission_error", "this command may require network access; --offline was requested", {
637
+ reason: "offline.refused",
638
+ });
600
639
  if (flags.dryRun &&
601
640
  path[0] !== "api" &&
602
641
  !["files", "comments", "permissions", "revisions", "shared-drives"].includes(path[0] ?? "") &&
603
642
  !["documents get", "documents create", "documents update", "documents history"].includes(words)) {
604
- output({ dryRun: true, command: words, mutates, local: metadata.local === true });
643
+ output({ dry_run: true, command: words, mutates, local: metadata.local === true });
605
644
  throw new Preview();
606
645
  }
607
646
  if (words === "session start")