@wirecat/drive-cli 0.0.0-stage → 0.2.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 (127) hide show
  1. package/LICENSE +204 -0
  2. package/README.md +195 -2
  3. package/dist/api-contract.d.ts +7 -0
  4. package/dist/api-contract.d.ts.map +1 -0
  5. package/dist/api-contract.js +42 -0
  6. package/dist/api-contract.js.map +1 -0
  7. package/dist/bin/drive.d.ts +3 -0
  8. package/dist/bin/drive.d.ts.map +1 -0
  9. package/dist/bin/drive.js +41 -0
  10. package/dist/bin/drive.js.map +1 -0
  11. package/dist/config.d.ts +74 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +83 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/documents.d.ts +4 -0
  16. package/dist/documents.d.ts.map +1 -0
  17. package/dist/documents.js +36 -0
  18. package/dist/documents.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +39 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/extraction.d.ts +14 -0
  24. package/dist/extraction.d.ts.map +1 -0
  25. package/dist/extraction.js +49 -0
  26. package/dist/extraction.js.map +1 -0
  27. package/dist/generated/definitions.d.ts +3 -0
  28. package/dist/generated/definitions.d.ts.map +1 -0
  29. package/dist/generated/definitions.js +9798 -0
  30. package/dist/generated/definitions.js.map +1 -0
  31. package/dist/generated/manifest.d.ts +3 -0
  32. package/dist/generated/manifest.d.ts.map +1 -0
  33. package/dist/generated/manifest.js +8346 -0
  34. package/dist/generated/manifest.js.map +1 -0
  35. package/dist/generated/metadata.d.ts +74 -0
  36. package/dist/generated/metadata.d.ts.map +1 -0
  37. package/dist/generated/metadata.js +342 -0
  38. package/dist/generated/metadata.js.map +1 -0
  39. package/dist/generated/schemas.d.ts +649 -0
  40. package/dist/generated/schemas.d.ts.map +1 -0
  41. package/dist/generated/schemas.js +2602 -0
  42. package/dist/generated/schemas.js.map +1 -0
  43. package/dist/generated/types.d.ts +3940 -0
  44. package/dist/generated/types.d.ts.map +1 -0
  45. package/dist/generated/types.js +5 -0
  46. package/dist/generated/types.js.map +1 -0
  47. package/dist/google-api.d.ts +15 -0
  48. package/dist/google-api.d.ts.map +1 -0
  49. package/dist/google-api.js +94 -0
  50. package/dist/google-api.js.map +1 -0
  51. package/dist/google.d.ts +8 -0
  52. package/dist/google.d.ts.map +1 -0
  53. package/dist/google.js +173 -0
  54. package/dist/google.js.map +1 -0
  55. package/dist/index.d.ts +9 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +7 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/knowledge-commands.d.ts +19 -0
  60. package/dist/knowledge-commands.d.ts.map +1 -0
  61. package/dist/knowledge-commands.js +184 -0
  62. package/dist/knowledge-commands.js.map +1 -0
  63. package/dist/knowledge.d.ts +50 -0
  64. package/dist/knowledge.d.ts.map +1 -0
  65. package/dist/knowledge.js +249 -0
  66. package/dist/knowledge.js.map +1 -0
  67. package/dist/login.d.ts +25 -0
  68. package/dist/login.d.ts.map +1 -0
  69. package/dist/login.js +116 -0
  70. package/dist/login.js.map +1 -0
  71. package/dist/mcp.d.ts +35 -0
  72. package/dist/mcp.d.ts.map +1 -0
  73. package/dist/mcp.js +147 -0
  74. package/dist/mcp.js.map +1 -0
  75. package/dist/model.d.ts +100 -0
  76. package/dist/model.d.ts.map +1 -0
  77. package/dist/model.js +3 -0
  78. package/dist/model.js.map +1 -0
  79. package/dist/oauth-application.d.ts +5 -0
  80. package/dist/oauth-application.d.ts.map +1 -0
  81. package/dist/oauth-application.js +8 -0
  82. package/dist/oauth-application.js.map +1 -0
  83. package/dist/policy.d.ts +16 -0
  84. package/dist/policy.d.ts.map +1 -0
  85. package/dist/policy.js +65 -0
  86. package/dist/policy.js.map +1 -0
  87. package/dist/profile-lock.d.ts +5 -0
  88. package/dist/profile-lock.d.ts.map +1 -0
  89. package/dist/profile-lock.js +27 -0
  90. package/dist/profile-lock.js.map +1 -0
  91. package/dist/program.d.ts +20 -0
  92. package/dist/program.d.ts.map +1 -0
  93. package/dist/program.js +655 -0
  94. package/dist/program.js.map +1 -0
  95. package/dist/registry.d.ts +7 -0
  96. package/dist/registry.d.ts.map +1 -0
  97. package/dist/registry.js +15 -0
  98. package/dist/registry.js.map +1 -0
  99. package/dist/remote-commands.d.ts +10 -0
  100. package/dist/remote-commands.d.ts.map +1 -0
  101. package/dist/remote-commands.js +169 -0
  102. package/dist/remote-commands.js.map +1 -0
  103. package/dist/schema.d.ts +6 -0
  104. package/dist/schema.d.ts.map +1 -0
  105. package/dist/schema.js +77 -0
  106. package/dist/schema.js.map +1 -0
  107. package/dist/state.d.ts +11 -0
  108. package/dist/state.d.ts.map +1 -0
  109. package/dist/state.js +90 -0
  110. package/dist/state.js.map +1 -0
  111. package/dist/store.d.ts +5 -0
  112. package/dist/store.d.ts.map +1 -0
  113. package/dist/store.js +135 -0
  114. package/dist/store.js.map +1 -0
  115. package/dist/sync.d.ts +20 -0
  116. package/dist/sync.d.ts.map +1 -0
  117. package/dist/sync.js +278 -0
  118. package/dist/sync.js.map +1 -0
  119. package/dist/tokens.d.ts +26 -0
  120. package/dist/tokens.d.ts.map +1 -0
  121. package/dist/tokens.js +83 -0
  122. package/dist/tokens.js.map +1 -0
  123. package/dist/version.d.ts +2 -0
  124. package/dist/version.d.ts.map +1 -0
  125. package/dist/version.js +3 -0
  126. package/dist/version.js.map +1 -0
  127. package/package.json +82 -4
@@ -0,0 +1,655 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { CliError, createDeadline, createOutputBuffer, createRenderer, processStreams, visibleControls, } from "@wirecat/cli-base";
3
+ import { annotate, describeOptions, flatten, metaOf } from "@wirecat/cli-base/commands";
4
+ import { formatSuggestions, suggest } from "@wirecat/cli-base/completion";
5
+ import { commandWords, liftProfile, parseDuration, refuseCommandName } from "@wirecat/cli-messaging/cli";
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";
9
+ import { googleError } from "./errors.js";
10
+ import { memoryModel } from "./extraction.js";
11
+ import { googleSource } from "./google.js";
12
+ import { apiMethod, googleApi } from "./google-api.js";
13
+ import { installKnowledgeCommands } from "./knowledge-commands.js";
14
+ import { login } from "./login.js";
15
+ import { FOLDER } from "./model.js";
16
+ import { checkPolicy, guardedGoogleWrite, methodPolicyKeys } from "./policy.js";
17
+ import { lockProfile } from "./profile-lock.js";
18
+ import { ACCOUNT_FREE_COMMANDS, describeDriveProgram } from "./registry.js";
19
+ import { installRemoteCommands } from "./remote-commands.js";
20
+ import { requestSchema } from "./schema.js";
21
+ import { readSnapshot, readState } from "./state.js";
22
+ import { openIndex } from "./store.js";
23
+ import { sync } from "./sync.js";
24
+ import { authorizedClient, checkAuthorization, credentials } from "./tokens.js";
25
+ import { VERSION } from "./version.js";
26
+ const positive = (input) => {
27
+ const n = Number(input);
28
+ if (!Number.isSafeInteger(n) || n < 1)
29
+ throw new InvalidArgumentError("use a positive integer");
30
+ return n;
31
+ };
32
+ const offset = (input) => {
33
+ const n = Number(input);
34
+ if (!Number.isSafeInteger(n) || n < 0)
35
+ throw new InvalidArgumentError("use a nonnegative integer");
36
+ return n;
37
+ };
38
+ export const createProgram = (options = {}) => {
39
+ const env = options.env ?? process.env;
40
+ const streams = options.streams ?? processStreams;
41
+ const program = new Command("drive")
42
+ .description("Manage Google Drive and Docs, local Markdown, shared knowledge and search")
43
+ .version(VERSION)
44
+ .usage("<profile> [options] <command>")
45
+ .option("--profile <name>", "explicit Google account profile (alternative to the first word)")
46
+ .option("--json", "print machine-readable JSON")
47
+ .option("--jsonl", "print machine-readable JSON lines")
48
+ .option("--agent-json", "show control and direction characters visibly in machine output")
49
+ .option("--quiet", "suppress optional diagnostics")
50
+ .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("--read-only", "refuse local and remote mutations")
53
+ .option("--offline", "answer only operations that use local cached data")
54
+ .option("--dry-run", "validate command/policy and preview without acting")
55
+ .option("--yes", "answer a policy confirmation; cannot override deny or readonly")
56
+ .option("--allow-dangerous", "answer a permanent-deletion confirmation")
57
+ .option("--permission <key=level>", "temporary policy override; repeat to add keys", (value, previous = []) => [...previous, value]);
58
+ program
59
+ .configureOutput({
60
+ writeOut: (text) => streams.data(text),
61
+ writeErr: (text) => streams.diagnostic(text),
62
+ outputError: () => undefined,
63
+ })
64
+ .exitOverride();
65
+ const selected = () => {
66
+ const name = program.opts().profile;
67
+ if (!name) {
68
+ const choices = Object.keys(readConfig(env).profiles).sort();
69
+ throw new CliError("validation_error", "choose a profile explicitly: drive <profile> <command> or drive --profile <name> <command>; " +
70
+ (choices.length
71
+ ? `configured profiles: ${choices.join(", ")}`
72
+ : "no profiles configured; connect with drive personal session start"), { profiles: choices });
73
+ }
74
+ return profileName(name);
75
+ };
76
+ let currentAction;
77
+ let activeSignal = options.signal;
78
+ let deadline;
79
+ const confirmations = new Set();
80
+ const askOnce = async (key) => {
81
+ if (confirmations.has(key))
82
+ return true;
83
+ const answered = (await options.ask?.(key)) ?? false;
84
+ if (answered)
85
+ confirmations.add(key);
86
+ return answered;
87
+ };
88
+ const policyFlags = () => ({
89
+ ...program.opts(),
90
+ allowDangerous: program.opts().allowDangerous || currentAction?.opts().allowDelete,
91
+ });
92
+ const safeForAgent = (value) => {
93
+ if (typeof value === "string")
94
+ return visibleControls(value);
95
+ if (Array.isArray(value))
96
+ return value.map(safeForAgent);
97
+ if (value && typeof value === "object")
98
+ return Object.fromEntries(Object.entries(value).map(([key, field]) => [visibleControls(key), safeForAgent(field)]));
99
+ return value;
100
+ };
101
+ const output = (value) => {
102
+ const buffer = createOutputBuffer({ maxOutputBytes: program.opts().maxOutputBytes });
103
+ createRenderer({
104
+ format: program.opts().jsonl
105
+ ? "jsonl"
106
+ : program.opts().json || program.opts().agentJson || !process.stdout.isTTY
107
+ ? "json"
108
+ : "pretty",
109
+ color: false,
110
+ streams: { ...streams, data: (text) => buffer.data(text) },
111
+ quiet: program.opts().quiet === true,
112
+ }).result(program.opts().agentJson ? safeForAgent(value) : value);
113
+ buffer.flush((text) => streams.data(text));
114
+ };
115
+ const source = async () => {
116
+ const name = selected();
117
+ if (options.source)
118
+ return options.source(name);
119
+ const client = authorizedClient(env, name, options.keyring, activeSignal);
120
+ await checkAuthorization(client);
121
+ return googleSource(client);
122
+ };
123
+ const withIndex = async (fn) => {
124
+ const index = await (options.index?.() ?? openIndex(env));
125
+ try {
126
+ return await fn(index);
127
+ }
128
+ finally {
129
+ await index.close();
130
+ }
131
+ };
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")
142
+ .action(async (args) => {
143
+ if (args.credentialStorage !== "keyring" && args.credentialStorage !== "file")
144
+ throw new CliError("validation_error", "credential storage is keyring or file");
145
+ output(await login({
146
+ env,
147
+ profile: selected(),
148
+ application: application(env, args.clientFile),
149
+ storage: args.credentialStorage,
150
+ ...(args.write ? { write: true } : {}),
151
+ ...(args.activity ? { activity: true } : {}),
152
+ ...(args.apps ? { apps: true } : {}),
153
+ ...(options.keyring ? { keyring: options.keyring } : {}),
154
+ ...(activeSignal ? { signal: activeSignal } : {}),
155
+ openBrowser: args.browser ? (options.openBrowser ?? (async () => { })) : async () => { },
156
+ showUrl: (url) => streams.diagnostic(`Open Google sign-in: ${url}\n`),
157
+ }));
158
+ });
159
+ session
160
+ .command("list")
161
+ .description("Configured account profiles; no credentials are printed")
162
+ .action(() => output({
163
+ scope: "all-configured-profiles",
164
+ items: Object.entries(readConfig(env).profiles).map(([name, p]) => ({
165
+ profile: name,
166
+ selectedFolders: p.roots.length,
167
+ storage: p.storage,
168
+ })),
169
+ }));
170
+ session
171
+ .command("end")
172
+ .description("Remove this profile's local sign-in; cached data is retained")
173
+ .action(() => {
174
+ const name = selected();
175
+ const config = readConfig(env);
176
+ const profile = activeProfile(env, name);
177
+ credentials(env, profile.storage, options.keyring).remove(name);
178
+ // Preserve the principal and roots so reauthorization resumes the same source identities.
179
+ writeConfig(env, config);
180
+ output({ profile: name, signedOut: true, cachedData: "retained" });
181
+ });
182
+ const folders = program.command("folders").description("Select Google folders for local ingestion");
183
+ folders
184
+ .command("add")
185
+ .argument("<url-or-id>")
186
+ .description("Include a folder and its descendants")
187
+ .action(async (input) => {
188
+ const id = fileId(input);
189
+ const config = readConfig(env);
190
+ const name = selected();
191
+ const profile = activeProfile(env, name);
192
+ const metadata = await (await source()).metadata(id);
193
+ if (metadata.mime !== FOLDER || metadata.trashed)
194
+ throw new CliError("validation_error", "source is not an accessible Google folder");
195
+ if (!profile.roots.includes(metadata.id))
196
+ profile.roots.push(metadata.id);
197
+ config.profiles[name] = profile;
198
+ writeConfig(env, config);
199
+ output({ id: metadata.id, name: metadata.name, added: true });
200
+ });
201
+ folders
202
+ .command("list")
203
+ .description("Selected folder IDs")
204
+ .action(() => output({ items: activeProfile(env, selected()).roots.map((id) => ({ id })) }));
205
+ folders
206
+ .command("remove")
207
+ .argument("<url-or-id>")
208
+ .description("Stop importing a folder; sync removes sources outside the remaining scope")
209
+ .action((input) => {
210
+ const id = fileId(input);
211
+ const config = readConfig(env);
212
+ const name = selected();
213
+ const profile = activeProfile(env, name);
214
+ profile.roots = profile.roots.filter((root) => root !== id);
215
+ config.profiles[name] = profile;
216
+ writeConfig(env, config);
217
+ output({ id, removed: true, next: `run drive ${name} sync to reconcile cached sources` });
218
+ });
219
+ program
220
+ .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")
225
+ .action(async (args) => {
226
+ const remote = activeProfile(env, selected()).roots.length ? await source() : undefined;
227
+ const result = await withIndex(async (index) => ({
228
+ ...(await sync(remote, index, env, selected(), args.max, activeSignal, args.rescan)),
229
+ ...(args.embed ? { embeddings: await index.embed(600) } : {}),
230
+ }));
231
+ output(result);
232
+ if (result.errors.length || result.pending)
233
+ streams.diagnostic(`${result.pending} source operations remain; run drive ${selected()} sync again\n`);
234
+ if (result.errors.length)
235
+ throw new CliError("provider_error", "sync completed partially; failed operations remain pending");
236
+ });
237
+ program
238
+ .command("status")
239
+ .description("Local sync checkpoint, cached sources and pending work")
240
+ .action(() => {
241
+ const name = selected();
242
+ const profile = activeProfile(env, name);
243
+ const state = readState(env, name, profile.principal);
244
+ output({
245
+ profile: name,
246
+ selectedFolders: profile.roots,
247
+ documents: Object.keys(state.documents).length,
248
+ pending: state.pending?.jobs.length ?? 0,
249
+ skipped: state.skipped,
250
+ lastSync: state.lastSync ?? null,
251
+ coverage: "Google Docs and Markdown/TXT; native export tab fidelity and assets are not verified",
252
+ remoteChecked: false,
253
+ });
254
+ });
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")
261
+ .action(async (args) => {
262
+ const profile = activeProfile(env, selected());
263
+ const state = readState(env, selected(), profile.principal);
264
+ let docs = Object.values(state.documents);
265
+ if (args.tag || args.person)
266
+ docs = await withIndex(async (index) => {
267
+ if (!index.knowledge)
268
+ throw new CliError("configuration_error", "index has no shared knowledge adapter");
269
+ const filtered = [];
270
+ for (const doc of docs)
271
+ if (await index.knowledge.matches(profile.principal, doc.reference, args))
272
+ filtered.push(doc);
273
+ return filtered;
274
+ });
275
+ output({
276
+ items: docs.map((doc) => ({
277
+ ...doc.metadata,
278
+ reference: doc.reference,
279
+ tabs: doc.tabs,
280
+ })),
281
+ });
282
+ });
283
+ const imported = (input) => {
284
+ const id = fileId(input);
285
+ const profile = activeProfile(env, selected());
286
+ const state = readState(env, selected(), profile.principal);
287
+ if (!state.documents[id])
288
+ throw new CliError("not_found", "this source is not imported in the selected profile");
289
+ const doc = state.documents[id];
290
+ return { id, ref: doc.reference, hash: doc.hash, snapshot: readSnapshot(env, selected(), id) };
291
+ };
292
+ documents
293
+ .command("show")
294
+ .argument("<url-or-id>")
295
+ .description("Imported text with tab and source metadata")
296
+ .action((input) => {
297
+ const { snapshot } = imported(input);
298
+ output({ metadata: snapshot.metadata, text: snapshot.text, tabs: snapshot.tabs });
299
+ });
300
+ documents
301
+ .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")
305
+ .action((input, args) => {
306
+ const { id, snapshot } = imported(input);
307
+ writeFileSync(args.out, snapshot.markdown, { flag: "wx", mode: 0o600 });
308
+ output({ id, path: args.out, format: "markdown" });
309
+ });
310
+ program
311
+ .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 }))));
318
+ installKnowledgeCommands(program, {
319
+ document: imported,
320
+ output,
321
+ positive,
322
+ offset,
323
+ complete: () => options.complete ?? memoryModel(env, selected(), activeSignal),
324
+ run: async (input, fn) => {
325
+ const lock = await lockProfile(env, selected());
326
+ try {
327
+ const { ref } = imported(input);
328
+ return await withIndex(async (index) => {
329
+ lock.assertHeld();
330
+ if (!index.knowledge)
331
+ throw new CliError("configuration_error", "index has no shared knowledge adapter");
332
+ const result = await fn(index.knowledge, activeProfile(env, selected()).principal, ref);
333
+ lock.assertHeld();
334
+ return result;
335
+ });
336
+ }
337
+ finally {
338
+ await lock.release();
339
+ }
340
+ },
341
+ });
342
+ installRemoteCommands(program, {
343
+ output,
344
+ positive,
345
+ readBody: options.readBody ?? ((path) => readFileSync(path, "utf8")),
346
+ call: async (method, params, body) => {
347
+ const descriptor = apiMethod(method);
348
+ validateRequest(method, params, body);
349
+ const flags = policyFlags();
350
+ for (const key of methodPolicyKeys(method))
351
+ await checkPolicy(env, selected(), key, descriptor.mutates, flags, askOnce);
352
+ if (flags.offline)
353
+ throw new CliError("permission_error", "this operation needs Google; --offline was requested");
354
+ if (flags.dryRun)
355
+ return {
356
+ dryRun: true,
357
+ method,
358
+ mutates: descriptor.mutates,
359
+ parameters: params,
360
+ bodyProvided: body !== undefined,
361
+ };
362
+ const act = async () => {
363
+ try {
364
+ if (options.api)
365
+ return await options.api.call(method, params, body);
366
+ const client = authorizedClient(env, selected(), options.keyring, activeSignal);
367
+ await checkAuthorization(client, descriptor.scope);
368
+ return await googleApi(client).call(method, params, body);
369
+ }
370
+ catch (error) {
371
+ throw googleError(error);
372
+ }
373
+ };
374
+ return descriptor.mutates ? guardedGoogleWrite(env, selected(), `api.${method}`, act, flags, askOnce) : act();
375
+ },
376
+ });
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")
386
+ .action((key, raw, args) => {
387
+ const value = readConfig(env);
388
+ if (args.defaults)
389
+ value.defaults ??= {};
390
+ const target = args.defaults
391
+ ? value.defaults
392
+ : activeProfile(env, selected());
393
+ if (!args.defaults)
394
+ value.profiles[selected()] = target;
395
+ const [head, ...parts] = key.split(".");
396
+ if (!["readOnly", "permissions", "models"].includes(head ?? "") ||
397
+ parts.some((part) => ["__proto__", "prototype", "constructor"].includes(part)))
398
+ throw new CliError("validation_error", "config set accepts readOnly, permissions or models");
399
+ let parsed;
400
+ try {
401
+ parsed = JSON.parse(raw);
402
+ }
403
+ catch {
404
+ throw new CliError("validation_error", "setting value must be JSON");
405
+ }
406
+ if (!parts.length)
407
+ Object.assign(target, { [key]: parsed });
408
+ else if (head === "permissions") {
409
+ target.permissions ??= {};
410
+ Object.assign(target.permissions, { [parts.join(".")]: parsed });
411
+ }
412
+ else if (head === "models" && parts.length === 2 && parts[0] && parts[1]) {
413
+ target.models ??= {};
414
+ target.models[parts[0]] ??= {};
415
+ Object.assign(target.models[parts[0]], { [parts[1]]: parsed });
416
+ }
417
+ else
418
+ throw new CliError("validation_error", "unknown setting path");
419
+ writeConfig(env, value);
420
+ output({ key, updated: true, scope: args.defaults ? "defaults" : selected() });
421
+ });
422
+ program
423
+ .command("complete")
424
+ .description("Shell completion from the shared command registry")
425
+ .argument("[words...]")
426
+ .allowUnknownOption()
427
+ .helpOption(false)
428
+ .action(async (words) => {
429
+ if (!program.rawArgs.includes("--")) {
430
+ if (!["bash", "zsh", "fish", "powershell"].includes(words[0] ?? ""))
431
+ throw new CliError("validation_error", "choose a supported shell");
432
+ const { script } = await import("@bomb.sh/tab");
433
+ script(words[0], "drive", "drive");
434
+ }
435
+ else
436
+ streams.data(formatSuggestions(suggest({ commands: describeDriveProgram(program), globalOptions: describeOptions(program), words })));
437
+ });
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
450
+ .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")
457
+ .action((args) => {
458
+ const config = readConfig(env);
459
+ config.defaults ??= {};
460
+ config.defaults.models ??= {};
461
+ config.defaults.models[args.purpose] = {
462
+ provider: args.provider,
463
+ ...(args.model ? { model: args.model } : {}),
464
+ ...(args.baseUrl ? { baseUrl: args.baseUrl } : {}),
465
+ };
466
+ writeConfig(env, config);
467
+ output({ purpose: args.purpose, configured: true });
468
+ });
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) => {
471
+ const { mcpTools, serveMcp } = await import("./mcp.js");
472
+ if (args.tools)
473
+ output({
474
+ items: mcpTools({ ...options, env }, selected(), policyFlags()).map(({ name, schema, command }) => ({
475
+ name,
476
+ inputSchema: schema,
477
+ mutates: command.mutates,
478
+ })),
479
+ });
480
+ else
481
+ await serveMcp({ ...options, env, ...(activeSignal ? { signal: activeSignal } : {}) }, selected(), policyFlags());
482
+ });
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
+ });
497
+ for (const info of flatten(describeDriveProgram(program))) {
498
+ let command = program;
499
+ for (const part of info.path)
500
+ command = command.commands.find((child) => child.name() === part) ?? command;
501
+ const localWrites = [
502
+ "session start",
503
+ "session end",
504
+ "folders add",
505
+ "folders remove",
506
+ "sync",
507
+ "documents export",
508
+ "tags add",
509
+ "tags remove",
510
+ "links add",
511
+ "links remove",
512
+ "notes add",
513
+ "notes edit",
514
+ "notes remove",
515
+ "memories add",
516
+ "memories confirm",
517
+ "memories stale",
518
+ "memories extract",
519
+ "facts add",
520
+ "facts accept",
521
+ "people link",
522
+ "models configure",
523
+ "config set",
524
+ ];
525
+ const remoteWrites = [
526
+ "api call",
527
+ "files copy",
528
+ "files move",
529
+ "files update",
530
+ "files trash",
531
+ "files restore",
532
+ "files mkdir",
533
+ "documents create",
534
+ "documents update",
535
+ ];
536
+ const name = info.path.join(" ");
537
+ annotate(command, {
538
+ origin: metaOf(command).origin ?? "handwritten",
539
+ mutates: metaOf(command).mutates ?? (localWrites.includes(name) || remoteWrites.includes(name)),
540
+ local: localWrites.includes(name) && name !== "session start" && name !== "memories extract",
541
+ state: "beta",
542
+ });
543
+ }
544
+ class Preview extends Error {
545
+ }
546
+ const pathOf = (command) => {
547
+ const path = [];
548
+ for (let at = command; at?.parent; at = at.parent)
549
+ path.unshift(at.name());
550
+ return path;
551
+ };
552
+ program.hook("preAction", async (_root, action) => {
553
+ currentAction = action;
554
+ if (program.opts().timeout !== undefined && !deadline) {
555
+ deadline = createDeadline({
556
+ timeoutMs: program.opts().timeout,
557
+ ...(options.signal ? { signal: options.signal } : {}),
558
+ });
559
+ activeSignal = deadline.signal;
560
+ }
561
+ const path = pathOf(action);
562
+ const words = path.join(" ");
563
+ // Discovery is account-independent and must not load config, credentials or the store.
564
+ if (ACCOUNT_FREE_COMMANDS.has(words))
565
+ return;
566
+ selected();
567
+ if (["commands", "complete", "schema", "config show", "config set", "session list"].includes(words))
568
+ return;
569
+ const flags = policyFlags();
570
+ const metadata = metaOf(action);
571
+ const rawMethod = words === "api call" ? action.args[0] : undefined;
572
+ const method = metadata.operationId ?? rawMethod;
573
+ if (rawMethod) {
574
+ const descriptor = apiMethod(rawMethod);
575
+ if (descriptor.mutates && !action.opts().allowWrite)
576
+ throw new CliError("permission_error", "this method changes Google data; pass --allow-write");
577
+ 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");
579
+ if (flags.allowDangerous)
580
+ action.setOptionValue("allowDelete", true);
581
+ }
582
+ const mutates = method ? apiMethod(method).mutates : metadata.mutates === true;
583
+ const key = method ? `api.${method}` : path.join(".");
584
+ for (const permission of method ? methodPolicyKeys(method) : [key])
585
+ await checkPolicy(env, selected(), permission, mutates, flags, askOnce);
586
+ if (flags.offline &&
587
+ (method ||
588
+ ["files", "comments", "permissions", "revisions", "shared-drives"].includes(path[0] ?? "") ||
589
+ [
590
+ "session start",
591
+ "folders add",
592
+ "documents get",
593
+ "documents history",
594
+ "documents create",
595
+ "documents update",
596
+ "memories extract",
597
+ "sync",
598
+ ].includes(words)))
599
+ throw new CliError("permission_error", "this command may require network access; --offline was requested");
600
+ if (flags.dryRun &&
601
+ path[0] !== "api" &&
602
+ !["files", "comments", "permissions", "revisions", "shared-drives"].includes(path[0] ?? "") &&
603
+ !["documents get", "documents create", "documents update", "documents history"].includes(words)) {
604
+ output({ dryRun: true, command: words, mutates, local: metadata.local === true });
605
+ throw new Preview();
606
+ }
607
+ if (words === "session start")
608
+ refuseCommandName(selected(), commandWords(program), "drive");
609
+ });
610
+ const parse = program.parseAsync.bind(program);
611
+ program.parseAsync = async (argv = process.argv, settings) => {
612
+ const prefix = settings?.from === "user" ? [] : argv.slice(0, 2);
613
+ const args = settings?.from === "user" ? argv : argv.slice(2);
614
+ const lifted = liftProfile([...args], commandWords(program));
615
+ // Refuse conflicting/repeated selectors rather than letting Commander pick the last one.
616
+ const flags = lifted.rest.slice(0, lifted.rest.indexOf("--") < 0 ? lifted.rest.length : lifted.rest.indexOf("--"));
617
+ const selectors = flags.filter((arg) => arg === "--profile" || arg.startsWith("--profile="));
618
+ if (selectors.length > 1 || (lifted.profile && selectors.length))
619
+ throw new CliError("validation_error", "select a profile once: use the first word or --profile");
620
+ const rest = lifted.profile ? ["--profile", profileName(lifted.profile), ...lifted.rest] : lifted.rest;
621
+ try {
622
+ const running = parse([...prefix, ...rest], settings);
623
+ return deadline ? await deadline.race(async () => running) : await running;
624
+ }
625
+ catch (error) {
626
+ if (error instanceof Preview)
627
+ return program;
628
+ throw error;
629
+ }
630
+ finally {
631
+ deadline?.dispose();
632
+ }
633
+ };
634
+ // Imported generated children have their own Commander exit/output configuration.
635
+ for (const info of flatten(describeDriveProgram(program))) {
636
+ let command = program;
637
+ for (const part of info.path)
638
+ command = command.commands.find((child) => child.name() === part) ?? command;
639
+ command.configureHelp({
640
+ commandUsage: (target) => {
641
+ const usage = new Help().commandUsage(target);
642
+ return ACCOUNT_FREE_COMMANDS.has(pathOf(target).join(" "))
643
+ ? usage
644
+ : usage.replace(/^drive /, "drive <profile> ");
645
+ },
646
+ });
647
+ command.exitOverride().configureOutput({
648
+ writeOut: (text) => streams.data(text),
649
+ writeErr: (text) => streams.diagnostic(text),
650
+ outputError: () => { },
651
+ });
652
+ }
653
+ return program;
654
+ };
655
+ //# sourceMappingURL=program.js.map