@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.
- package/README.md +9 -8
- package/dist/api-contract.d.ts.map +1 -1
- package/dist/api-contract.js +4 -2
- package/dist/api-contract.js.map +1 -1
- package/dist/bin/drive.js +16 -20
- package/dist/bin/drive.js.map +1 -1
- package/dist/catalog-commands.d.ts +13 -0
- package/dist/catalog-commands.d.ts.map +1 -0
- package/dist/catalog-commands.js +111 -0
- package/dist/catalog-commands.js.map +1 -0
- package/dist/catalog.d.ts +43 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +107 -0
- package/dist/catalog.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -3
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -5
- package/dist/errors.js.map +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.d.ts.map +1 -1
- package/dist/extraction.js +9 -5
- package/dist/extraction.js.map +1 -1
- package/dist/failure.d.ts +65 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +86 -0
- package/dist/failure.js.map +1 -0
- package/dist/google-api.d.ts.map +1 -1
- package/dist/google-api.js +3 -1
- package/dist/google-api.js.map +1 -1
- package/dist/google.d.ts +5 -0
- package/dist/google.d.ts.map +1 -1
- package/dist/google.js +80 -12
- package/dist/google.js.map +1 -1
- package/dist/knowledge-commands.d.ts.map +1 -1
- package/dist/knowledge-commands.js +150 -113
- package/dist/knowledge-commands.js.map +1 -1
- package/dist/knowledge.d.ts +2 -2
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +11 -5
- package/dist/knowledge.js.map +1 -1
- package/dist/login.d.ts +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +11 -5
- package/dist/login.js.map +1 -1
- package/dist/mcp.d.ts +22 -4
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +7 -15
- package/dist/mcp.js.map +1 -1
- package/dist/model.d.ts +67 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +17 -0
- package/dist/model.js.map +1 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +11 -4
- package/dist/policy.js.map +1 -1
- package/dist/profile-lock.d.ts.map +1 -1
- package/dist/profile-lock.js +4 -2
- package/dist/profile-lock.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +191 -152
- package/dist/program.js.map +1 -1
- package/dist/registry.d.ts +7 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +28 -2
- package/dist/registry.js.map +1 -1
- package/dist/remote-commands.d.ts.map +1 -1
- package/dist/remote-commands.js +95 -76
- package/dist/remote-commands.js.map +1 -1
- package/dist/scope.d.ts +3 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +15 -0
- package/dist/scope.js.map +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +27 -6
- package/dist/state.js.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +90 -10
- package/dist/store.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +121 -52
- package/dist/sync.js.map +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +8 -4
- package/dist/tokens.js.map +1 -1
- 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-
|
|
5
|
+
import { commandWords, liftProfile, parseDuration, refuseCommandName } from "@wirecat/cli-core/cli";
|
|
6
6
|
import { Command, Help, InvalidArgumentError, Option } from "commander";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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("
|
|
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>", "
|
|
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("
|
|
133
|
-
session
|
|
134
|
-
.
|
|
135
|
-
.
|
|
136
|
-
.option("--
|
|
137
|
-
.option("--
|
|
138
|
-
.option("--
|
|
139
|
-
.option("--
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
+
selected_folders: p.roots.length,
|
|
167
165
|
storage: p.storage,
|
|
168
166
|
})),
|
|
169
167
|
}));
|
|
170
|
-
session
|
|
171
|
-
|
|
172
|
-
|
|
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,
|
|
177
|
+
output({ profile: name, signed_out: true, cached_data: "retained" });
|
|
181
178
|
});
|
|
182
|
-
const folders = program.command("folders").description("
|
|
183
|
-
|
|
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("<
|
|
186
|
-
.description("
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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("<
|
|
208
|
-
.description("
|
|
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
|
-
.
|
|
222
|
-
.
|
|
223
|
-
|
|
224
|
-
.option("--
|
|
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.
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
+
remote_checked: false,
|
|
253
251
|
});
|
|
254
252
|
});
|
|
255
|
-
const documents = program.command("documents").description("
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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("<
|
|
295
|
-
.description("
|
|
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({
|
|
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("<
|
|
303
|
-
.
|
|
304
|
-
.
|
|
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
|
-
|
|
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("
|
|
314
|
-
.option("--limit <n>", "
|
|
315
|
-
.option("--offset <n>", "results to skip", offset, 0)
|
|
316
|
-
.option("--exact", "
|
|
317
|
-
.action(async (query, args) => output(await withIndex((index) => index.search(activeProfile(env, selected()).principal, query, {
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
392
|
+
dry_run: true,
|
|
357
393
|
method,
|
|
358
394
|
mutates: descriptor.mutates,
|
|
359
395
|
parameters: params,
|
|
360
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
config
|
|
382
|
-
|
|
383
|
-
.
|
|
384
|
-
|
|
385
|
-
.
|
|
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
|
|
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 (
|
|
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
|
-
|
|
424
|
-
|
|
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("
|
|
452
|
-
.command("
|
|
453
|
-
.
|
|
454
|
-
.
|
|
455
|
-
.
|
|
456
|
-
.
|
|
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("
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
.
|
|
488
|
-
|
|
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
|
-
"
|
|
511
|
-
"
|
|
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
|
|
521
|
-
"
|
|
522
|
-
"models
|
|
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:
|
|
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", "
|
|
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.
|
|
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({
|
|
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")
|