@persistmemory/cli 0.1.2 → 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.
package/dist/index.js CHANGED
@@ -232,6 +232,27 @@ var HttpClient = class {
232
232
  ...options ? { options } : {}
233
233
  });
234
234
  }
235
+ /** POST with a file as the body. The type describes the bytes, not JSON. */
236
+ async postBytes(path, bytes, contentType, query, options) {
237
+ return this.#request({
238
+ method: "POST",
239
+ path,
240
+ rawBody: bytes,
241
+ contentType,
242
+ ...query ? { query } : {},
243
+ ...options ? { options } : {}
244
+ });
245
+ }
246
+ /** GET that returns bytes rather than JSON, for downloading a file. */
247
+ async getBytes(path, query, options) {
248
+ return this.#request({
249
+ method: "GET",
250
+ path,
251
+ rawResponse: true,
252
+ ...query ? { query } : {},
253
+ ...options ? { options } : {}
254
+ });
255
+ }
235
256
  async patch(path, body, options) {
236
257
  return this.#request({
237
258
  method: "PATCH",
@@ -284,11 +305,20 @@ var HttpClient = class {
284
305
  this.#fetch(url, {
285
306
  method: request.method,
286
307
  headers: this.#headers(request),
287
- ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
308
+ ...request.rawBody !== void 0 ? { body: request.rawBody } : request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
288
309
  signal: deadline.signal
289
310
  }),
290
311
  deadline.signal
291
312
  );
313
+ if (request.rawResponse && response.ok) {
314
+ const disposition = response.headers.get("content-disposition") ?? "";
315
+ const named = /filename="([^"]+)"/.exec(disposition)?.[1];
316
+ return {
317
+ bytes: new Uint8Array(await response.arrayBuffer()),
318
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
319
+ ...named ? { filename: named } : {}
320
+ };
321
+ }
292
322
  const payload = await readBody(response);
293
323
  if (!response.ok) throw errorFromResponse(response.status, payload, response.headers);
294
324
  return payload;
@@ -309,9 +339,11 @@ var HttpClient = class {
309
339
  return {
310
340
  // The only place the key is ever read.
311
341
  authorization: `Bearer ${this.#apiKey}`,
312
- accept: "application/json",
342
+ // A download route answers with the file's own type, so `*/*` rather
343
+ // than a promise to accept only JSON that the server would have to break.
344
+ accept: request.rawResponse ? "*/*" : "application/json",
313
345
  "user-agent": this.#userAgent,
314
- ...request.body !== void 0 ? { "content-type": "application/json" } : {},
346
+ ...request.rawBody !== void 0 ? { "content-type": request.contentType ?? "application/octet-stream" } : request.body !== void 0 ? { "content-type": "application/json" } : {},
315
347
  ...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {}
316
348
  };
317
349
  }
@@ -342,7 +374,7 @@ var SignalFired = class extends Error {
342
374
  };
343
375
  function untilAborted(work, signal) {
344
376
  work.catch(() => void 0);
345
- return new Promise((resolve7, reject) => {
377
+ return new Promise((resolve8, reject) => {
346
378
  if (signal.aborted) {
347
379
  reject(new SignalFired());
348
380
  return;
@@ -352,7 +384,7 @@ function untilAborted(work, signal) {
352
384
  work.then(
353
385
  (value) => {
354
386
  signal.removeEventListener("abort", onAbort);
355
- resolve7(value);
387
+ resolve8(value);
356
388
  },
357
389
  (error) => {
358
390
  signal.removeEventListener("abort", onAbort);
@@ -362,14 +394,14 @@ function untilAborted(work, signal) {
362
394
  });
363
395
  }
364
396
  function defaultSleep(ms, signal) {
365
- return new Promise((resolve7, reject) => {
397
+ return new Promise((resolve8, reject) => {
366
398
  if (signal?.aborted) {
367
399
  reject(new AbortError());
368
400
  return;
369
401
  }
370
402
  const timer = setTimeout(() => {
371
403
  signal?.removeEventListener("abort", onAbort);
372
- resolve7();
404
+ resolve8();
373
405
  }, ms);
374
406
  function onAbort() {
375
407
  clearTimeout(timer);
@@ -888,6 +920,119 @@ var Conversations = class {
888
920
  );
889
921
  }
890
922
  };
923
+ var Google = class {
924
+ #http;
925
+ constructor(http) {
926
+ this.#http = http;
927
+ }
928
+ /** Files by name, newest first. Omit the query for recently changed ones. */
929
+ async searchDrive(params = {}, options) {
930
+ return this.#http.get(
931
+ "/api/v1/google/drive/files",
932
+ {
933
+ ...params.query !== void 0 ? { query: params.query } : {},
934
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
935
+ },
936
+ options
937
+ );
938
+ }
939
+ async getDriveFile(fileId, options) {
940
+ return this.#http.get(
941
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}`,
942
+ void 0,
943
+ options
944
+ );
945
+ }
946
+ /**
947
+ * The bytes of a Drive file.
948
+ *
949
+ * A Google Doc, Sheet or Slide holds no bytes of its own and is exported on
950
+ * the way - a document as PDF, a spreadsheet as CSV - so `filename` comes
951
+ * back describing what it BECAME. Writing it under the id instead produces a
952
+ * file nothing will open.
953
+ */
954
+ async downloadDriveFile(fileId, options) {
955
+ return this.#http.getBytes(
956
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`,
957
+ void 0,
958
+ options
959
+ );
960
+ }
961
+ /**
962
+ * Writes a file into the user's Drive.
963
+ *
964
+ * Needs one of the Drive write permissions on their connection. A read-only
965
+ * grant is refused by Google, and the error names the missing permission
966
+ * rather than reporting a failed upload - one is fixed with a checkbox and
967
+ * the other sends somebody looking for a bug.
968
+ */
969
+ async saveToDrive(params, options) {
970
+ return this.#http.postBytes(
971
+ "/api/v1/google/drive/files",
972
+ params.bytes,
973
+ params.contentType ?? "application/octet-stream",
974
+ {
975
+ name: params.name,
976
+ ...params.folderId !== void 0 ? { folderId: params.folderId } : {}
977
+ },
978
+ options
979
+ );
980
+ }
981
+ /**
982
+ * Recent messages - senders, subjects and a one-line preview, never bodies.
983
+ *
984
+ * `query` is Gmail's own syntax passed through as written: `from:priya`,
985
+ * `has:attachment`, `newer_than:7d`. It selects within the connected mailbox
986
+ * and cannot reach another one.
987
+ */
988
+ async searchMail(params = {}, options) {
989
+ return this.#http.get(
990
+ "/api/v1/google/mail",
991
+ {
992
+ ...params.query !== void 0 ? { query: params.query } : {},
993
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
994
+ },
995
+ options
996
+ );
997
+ }
998
+ /** One message, with its body and the names of what is attached. */
999
+ async readMail(messageId, options) {
1000
+ return this.#http.get(
1001
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}`,
1002
+ void 0,
1003
+ options
1004
+ );
1005
+ }
1006
+ /**
1007
+ * The bytes of one attachment.
1008
+ *
1009
+ * Separate from `readMail` so listing a mailbox never drags attachments
1010
+ * across the network: a message with a 40 MB deck should not cost 40 MB to
1011
+ * summarise.
1012
+ */
1013
+ async downloadAttachment(messageId, attachmentId, options) {
1014
+ return this.#http.getBytes(
1015
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`,
1016
+ void 0,
1017
+ options
1018
+ );
1019
+ }
1020
+ /** Sends as the connected account. Needs the send permission. */
1021
+ async sendMail(params, options) {
1022
+ return this.#http.post("/api/v1/google/mail/send", params, options);
1023
+ }
1024
+ /** People in the user's contacts. Omit the query to list them. */
1025
+ async contacts(params = {}, options) {
1026
+ return this.#http.get(
1027
+ "/api/v1/google/contacts",
1028
+ {
1029
+ ...params.query !== void 0 ? { query: params.query } : {},
1030
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1031
+ },
1032
+ options
1033
+ );
1034
+ }
1035
+ };
891
1036
  var Integrations = class {
892
1037
  #http;
893
1038
  constructor(http) {
@@ -1018,6 +1163,8 @@ var PersistMemory = class {
1018
1163
  conflicts;
1019
1164
  conversations;
1020
1165
  integrations;
1166
+ /** Drive, mail and contacts on the user's connected Google account. */
1167
+ google;
1021
1168
  health;
1022
1169
  agent;
1023
1170
  #http;
@@ -1034,6 +1181,7 @@ var PersistMemory = class {
1034
1181
  this.conflicts = new Conflicts(this.#http);
1035
1182
  this.conversations = new Conversations(this.#http);
1036
1183
  this.integrations = new Integrations(this.#http);
1184
+ this.google = new Google(this.#http);
1037
1185
  this.health = new Health(this.#http);
1038
1186
  this.agent = new Agent(this.#http);
1039
1187
  }
@@ -1422,6 +1570,12 @@ var HELP = `
1422
1570
  list spaces your Spaces
1423
1571
  get memory <id> one memory, in full
1424
1572
 
1573
+ drive [name] search your Google Drive
1574
+ drive get <id> [--out path] download one file here
1575
+ drive put <file> [--name n] save a file into Drive
1576
+ mail [search] recent mail \u2014 from:priya, has:attachment
1577
+ mail read <id> one message, with its body
1578
+
1425
1579
  status is the service healthy
1426
1580
  requests file requests waiting for you to approve
1427
1581
  requests get <id> write a finished one to a file here
@@ -1568,8 +1722,8 @@ async function startLoopback(options = {}) {
1568
1722
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
1569
1723
  let resolveCallback;
1570
1724
  let rejectCallback;
1571
- const received = new Promise((resolve7, reject) => {
1572
- resolveCallback = resolve7;
1725
+ const received = new Promise((resolve8, reject) => {
1726
+ resolveCallback = resolve8;
1573
1727
  rejectCallback = reject;
1574
1728
  });
1575
1729
  const server = createServer((request, response) => {
@@ -1595,9 +1749,9 @@ async function startLoopback(options = {}) {
1595
1749
  response.end(donePage(callback));
1596
1750
  resolveCallback?.(callback);
1597
1751
  });
1598
- await new Promise((resolve7, reject) => {
1752
+ await new Promise((resolve8, reject) => {
1599
1753
  server.once("error", reject);
1600
- server.listen(0, "127.0.0.1", resolve7);
1754
+ server.listen(0, "127.0.0.1", resolve8);
1601
1755
  });
1602
1756
  const address = server.address();
1603
1757
  if (address === null || typeof address === "string") {
@@ -1859,7 +2013,7 @@ function safeEqual(a, b) {
1859
2013
  }
1860
2014
  async function openBrowser(url) {
1861
2015
  const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1862
- await new Promise((resolve7, reject) => {
2016
+ await new Promise((resolve8, reject) => {
1863
2017
  const child = spawn(command, args, {
1864
2018
  stdio: "ignore",
1865
2019
  // Detached so closing the terminal does not close the browser, and so
@@ -1868,7 +2022,7 @@ async function openBrowser(url) {
1868
2022
  });
1869
2023
  child.once("error", reject);
1870
2024
  child.unref();
1871
- resolve7();
2025
+ resolve8();
1872
2026
  });
1873
2027
  }
1874
2028
  async function describe(response) {
@@ -1930,13 +2084,13 @@ async function currentCredential(resolved, deps) {
1930
2084
  // src/context.ts
1931
2085
  import { createInterface } from "node:readline";
1932
2086
  async function askOnTty(prompt) {
1933
- return new Promise((resolve7) => {
2087
+ return new Promise((resolve8) => {
1934
2088
  const readline = createInterface({ input: process.stdin, output: process.stdout });
1935
2089
  readline.question(prompt, (answer3) => {
1936
2090
  readline.close();
1937
- resolve7(answer3.trim());
2091
+ resolve8(answer3.trim());
1938
2092
  });
1939
- readline.once("close", () => resolve7(""));
2093
+ readline.once("close", () => resolve8(""));
1940
2094
  });
1941
2095
  }
1942
2096
  var ETX = "";
@@ -1945,13 +2099,13 @@ var BACKSPACE = "\b";
1945
2099
  async function readSecretFromTty(prompt) {
1946
2100
  const input = process.stdin;
1947
2101
  if (!input.isTTY) {
1948
- return new Promise((resolve7) => {
2102
+ return new Promise((resolve8) => {
1949
2103
  const readline = createInterface({ input });
1950
2104
  readline.once("line", (line) => {
1951
2105
  readline.close();
1952
- resolve7(line.trim());
2106
+ resolve8(line.trim());
1953
2107
  });
1954
- readline.once("close", () => resolve7(""));
2108
+ readline.once("close", () => resolve8(""));
1955
2109
  });
1956
2110
  }
1957
2111
  process.stdout.write(prompt);
@@ -1959,14 +2113,14 @@ async function readSecretFromTty(prompt) {
1959
2113
  input.setRawMode?.(true);
1960
2114
  input.resume();
1961
2115
  input.setEncoding("utf8");
1962
- return new Promise((resolve7) => {
2116
+ return new Promise((resolve8) => {
1963
2117
  let value = "";
1964
2118
  const finish2 = () => {
1965
2119
  input.removeListener("data", onData);
1966
2120
  input.setRawMode?.(previouslyRaw);
1967
2121
  input.pause();
1968
2122
  process.stdout.write("\n");
1969
- resolve7(value.trim());
2123
+ resolve8(value.trim());
1970
2124
  };
1971
2125
  const onData = (chunk) => {
1972
2126
  for (const character of chunk) {
@@ -2265,7 +2419,7 @@ async function agentCommand(context) {
2265
2419
  body: JSON.stringify(body)
2266
2420
  });
2267
2421
  let complaint;
2268
- const complain = (message2) => {
2422
+ const complain2 = (message2) => {
2269
2423
  if (complaint === message2) return;
2270
2424
  complaint = message2;
2271
2425
  context.error(` ${message2}`);
@@ -2280,7 +2434,7 @@ async function agentCommand(context) {
2280
2434
  try {
2281
2435
  const beat = await call("heartbeat", { hostname: name, platform: process.platform });
2282
2436
  if (!beat.ok) {
2283
- complain(await said(beat, "the service refused this machine"));
2437
+ complain2(await said(beat, "the service refused this machine"));
2284
2438
  } else {
2285
2439
  working();
2286
2440
  const state = await beat.json();
@@ -2295,7 +2449,7 @@ async function agentCommand(context) {
2295
2449
  }
2296
2450
  const claimed = await call("claim", { hostname: name, limit: 5 });
2297
2451
  if (!claimed.ok) {
2298
- complain(await said(claimed, "could not pick up work"));
2452
+ complain2(await said(claimed, "could not pick up work"));
2299
2453
  } else {
2300
2454
  const { items } = await claimed.json();
2301
2455
  for (const request of items) {
@@ -2320,9 +2474,172 @@ async function agentCommand(context) {
2320
2474
  return 0;
2321
2475
  }
2322
2476
 
2477
+ // src/commands/google.ts
2478
+ import { writeFileSync as writeFileSync4 } from "node:fs";
2479
+ import { basename, resolve as resolve4 } from "node:path";
2480
+ import { readFileSync as readFileSync5 } from "node:fs";
2481
+ async function callApi(context, path, init = {}) {
2482
+ const credential = context.resolved.credential;
2483
+ if (!credential) {
2484
+ context.error("Sign in first: pm auth login");
2485
+ return void 0;
2486
+ }
2487
+ const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
2488
+ return fetch(`${apiUrl}${path}`, {
2489
+ ...init,
2490
+ headers: {
2491
+ authorization: `Bearer ${credential.token}`,
2492
+ ...init.headers ?? {}
2493
+ }
2494
+ });
2495
+ }
2496
+ async function complain(context, response) {
2497
+ const body = await response.json().catch(() => void 0);
2498
+ context.error(body?.error?.message ?? `That failed (${response.status}).`);
2499
+ return response.status === 409 ? 3 : 1;
2500
+ }
2501
+ async function driveCommand(context) {
2502
+ const [, noun, ...rest] = context.args.words;
2503
+ if (noun === "get") return driveGet(context, rest.join(" ").trim());
2504
+ if (noun === "put" || noun === "save") return drivePut(context, rest.join(" ").trim());
2505
+ const query = [noun, ...rest].filter(Boolean).join(" ").trim();
2506
+ const response = await callApi(
2507
+ context,
2508
+ `/api/v1/google/drive/files?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
2509
+ );
2510
+ if (!response) return 1;
2511
+ if (!response.ok) return complain(context, response);
2512
+ const { data } = await response.json();
2513
+ if (data.length === 0) {
2514
+ context.print(query ? `Nothing in Drive matches "${query}".` : "That Drive is empty.");
2515
+ return 0;
2516
+ }
2517
+ if (context.flags.output === "json") {
2518
+ context.print(JSON.stringify(data, void 0, 2));
2519
+ return 0;
2520
+ }
2521
+ for (const file of data) {
2522
+ context.print(` ${file.name}`);
2523
+ context.print(
2524
+ ` ${file.id}${file.size ? ` \xB7 ${Math.round(file.size / 1024)} KB` : ""}${file.modifiedTime ? ` \xB7 ${file.modifiedTime.slice(0, 10)}` : ""}`
2525
+ );
2526
+ }
2527
+ context.print("");
2528
+ context.print("Fetch one with: pm drive get <id>");
2529
+ return 0;
2530
+ }
2531
+ async function driveGet(context, fileId) {
2532
+ if (!fileId) {
2533
+ context.error("Say which file: pm drive get <id>");
2534
+ return 2;
2535
+ }
2536
+ const response = await callApi(
2537
+ context,
2538
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`
2539
+ );
2540
+ if (!response) return 1;
2541
+ if (!response.ok) return complain(context, response);
2542
+ const disposition = response.headers.get("content-disposition") ?? "";
2543
+ const named = /filename="([^"]+)"/.exec(disposition)?.[1];
2544
+ const out = stringFlag(context.args, "out");
2545
+ const target = resolve4(out ?? basename(named ?? fileId));
2546
+ writeFileSync4(target, Buffer.from(await response.arrayBuffer()));
2547
+ context.print(target);
2548
+ return 0;
2549
+ }
2550
+ async function drivePut(context, path) {
2551
+ if (!path) {
2552
+ context.error("Say which file: pm drive put ./notes.md");
2553
+ return 2;
2554
+ }
2555
+ let bytes;
2556
+ try {
2557
+ bytes = readFileSync5(resolve4(path));
2558
+ } catch {
2559
+ context.error(`Cannot read ${path}.`);
2560
+ return 1;
2561
+ }
2562
+ const name = stringFlag(context.args, "name") ?? basename(path);
2563
+ const response = await callApi(
2564
+ context,
2565
+ `/api/v1/google/drive/files?name=${encodeURIComponent(name)}`,
2566
+ {
2567
+ method: "POST",
2568
+ // The bytes as bytes. Base64 in JSON would be a third larger and would
2569
+ // make the limit somebody was told about stop matching the one they hit.
2570
+ headers: { "content-type": "application/octet-stream" },
2571
+ body: new Uint8Array(bytes)
2572
+ }
2573
+ );
2574
+ if (!response) return 1;
2575
+ if (!response.ok) return complain(context, response);
2576
+ const saved = await response.json();
2577
+ context.print(`Saved "${saved.name}" to Drive.${saved.link ? ` ${saved.link}` : ""}`);
2578
+ return 0;
2579
+ }
2580
+ async function mailCommand(context) {
2581
+ const [, noun, ...rest] = context.args.words;
2582
+ if (noun === "read" || noun === "get") {
2583
+ const messageId = rest.join(" ").trim();
2584
+ if (!messageId) {
2585
+ context.error("Say which message: pm mail read <id>");
2586
+ return 2;
2587
+ }
2588
+ const response2 = await callApi(
2589
+ context,
2590
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}`
2591
+ );
2592
+ if (!response2) return 1;
2593
+ if (!response2.ok) return complain(context, response2);
2594
+ const message2 = await response2.json();
2595
+ if (context.flags.output === "json") {
2596
+ context.print(JSON.stringify(message2, void 0, 2));
2597
+ return 0;
2598
+ }
2599
+ context.print(`From: ${message2.from ?? "unknown"}`);
2600
+ context.print(`Subject: ${message2.subject ?? "(none)"}`);
2601
+ if (message2.date) context.print(`Date: ${message2.date}`);
2602
+ context.print("");
2603
+ context.print(message2.body);
2604
+ if (message2.attachments.length > 0) {
2605
+ context.print("");
2606
+ context.print("Attached:");
2607
+ for (const one of message2.attachments) context.print(` ${one.filename} (${one.mimeType})`);
2608
+ }
2609
+ return 0;
2610
+ }
2611
+ const query = [noun, ...rest].filter(Boolean).join(" ").trim();
2612
+ const response = await callApi(
2613
+ context,
2614
+ `/api/v1/google/mail?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
2615
+ );
2616
+ if (!response) return 1;
2617
+ if (!response.ok) return complain(context, response);
2618
+ const { data } = await response.json();
2619
+ if (data.length === 0) {
2620
+ context.print(query ? `No mail matches "${query}".` : "Nothing in that mailbox.");
2621
+ return 0;
2622
+ }
2623
+ if (context.flags.output === "json") {
2624
+ context.print(JSON.stringify(data, void 0, 2));
2625
+ return 0;
2626
+ }
2627
+ for (const message2 of data) {
2628
+ const marks = [message2.unread ? "unread" : "", message2.hasAttachments ? "attachment" : ""].filter(Boolean).join(", ");
2629
+ context.print(` ${message2.subject ?? "(no subject)"}${marks ? ` [${marks}]` : ""}`);
2630
+ context.print(
2631
+ ` ${message2.from ?? "unknown"}${message2.date ? ` \xB7 ${message2.date.slice(0, 10)}` : ""}`
2632
+ );
2633
+ context.print(` ${message2.id}`);
2634
+ }
2635
+ context.print("");
2636
+ context.print("Read one with: pm mail read <id>");
2637
+ return 0;
2638
+ }
2639
+
2323
2640
  // src/commands/requests.ts
2324
- import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "node:fs";
2325
- import { resolve as resolve4 } from "node:path";
2641
+ import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
2642
+ import { resolve as resolve5 } from "node:path";
2326
2643
  async function requestsCommand(context) {
2327
2644
  if (context.args.words[1] === "get") return collectCommand(context);
2328
2645
  const credential = context.resolved.credential;
@@ -2390,18 +2707,18 @@ async function collectCommand(context) {
2390
2707
  return 1;
2391
2708
  }
2392
2709
  const name = stringFlag(context.args, "output", "o") ?? filename;
2393
- const target = resolve4(name);
2710
+ const target = resolve5(name);
2394
2711
  if (existsSync4(target)) {
2395
2712
  context.error(`${target} already exists. Pass --output to write somewhere else.`);
2396
2713
  return 1;
2397
2714
  }
2398
- writeFileSync4(target, new Uint8Array(await file.arrayBuffer()));
2715
+ writeFileSync5(target, new Uint8Array(await file.arrayBuffer()));
2399
2716
  context.print(`Wrote ${target}`);
2400
2717
  return 0;
2401
2718
  }
2402
2719
 
2403
2720
  // src/workspace.ts
2404
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
2721
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
2405
2722
  import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
2406
2723
  var WORKSPACE_FILE = ".persistmemory.json";
2407
2724
  function findWorkspace(from = process.cwd()) {
@@ -2419,7 +2736,7 @@ function findWorkspace(from = process.cwd()) {
2419
2736
  }
2420
2737
  function readWorkspace(file) {
2421
2738
  try {
2422
- const parsed = JSON.parse(readFileSync5(file, "utf8"));
2739
+ const parsed = JSON.parse(readFileSync6(file, "utf8"));
2423
2740
  const space = parsed.space;
2424
2741
  if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
2425
2742
  return { space: { id: space.id, name: space.name } };
@@ -2431,7 +2748,7 @@ function readWorkspace(file) {
2431
2748
  }
2432
2749
  function writeWorkspace(dir, config) {
2433
2750
  const file = join5(dir, WORKSPACE_FILE);
2434
- writeFileSync5(file, `${JSON.stringify(config, null, 2)}
2751
+ writeFileSync6(file, `${JSON.stringify(config, null, 2)}
2435
2752
  `, "utf8");
2436
2753
  return file;
2437
2754
  }
@@ -2702,7 +3019,7 @@ function message(error) {
2702
3019
  // src/commands/maintain.ts
2703
3020
  import { existsSync as existsSync6, rmSync } from "node:fs";
2704
3021
  import { spawnSync } from "node:child_process";
2705
- import { dirname as dirname4, resolve as resolve6 } from "node:path";
3022
+ import { dirname as dirname4, resolve as resolve7 } from "node:path";
2706
3023
  import { fileURLToPath } from "node:url";
2707
3024
  async function updateCommand(context) {
2708
3025
  const manager = installer();
@@ -2773,7 +3090,7 @@ async function deleteCommand(context) {
2773
3090
  return 0;
2774
3091
  }
2775
3092
  function removeEverything(context) {
2776
- const dir = resolve6(context.paths.dir);
3093
+ const dir = resolve7(context.paths.dir);
2777
3094
  if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
2778
3095
  context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
2779
3096
  return;
@@ -2785,7 +3102,7 @@ function installer() {
2785
3102
  }
2786
3103
  function processPath() {
2787
3104
  try {
2788
- return resolve6(dirname4(fileURLToPath(import.meta.url)));
3105
+ return resolve7(dirname4(fileURLToPath(import.meta.url)));
2789
3106
  } catch {
2790
3107
  return process.argv[1] ?? "";
2791
3108
  }
@@ -2797,7 +3114,7 @@ import { randomUUID } from "node:crypto";
2797
3114
  import { relative as relative2 } from "node:path";
2798
3115
 
2799
3116
  // src/events.ts
2800
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "node:fs";
3117
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
2801
3118
  import { join as join6 } from "node:path";
2802
3119
  function openSessionLog(paths, id) {
2803
3120
  const directory = join6(paths.dir, "sessions");
@@ -2815,7 +3132,7 @@ function openSessionLog(paths, id) {
2815
3132
  },
2816
3133
  read() {
2817
3134
  if (!existsSync7(path)) return [];
2818
- return readFileSync6(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
3135
+ return readFileSync7(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
2819
3136
  try {
2820
3137
  return [JSON.parse(line)];
2821
3138
  } catch {
@@ -2892,9 +3209,9 @@ async function sessionCommand(context) {
2892
3209
  context.print(` Ask anything. /help for commands, /exit to leave.
2893
3210
  `);
2894
3211
  const readline = createInterface2({ input: process.stdin, output: process.stdout });
2895
- const ask = (prompt) => new Promise((resolve7) => {
2896
- readline.question(prompt, resolve7);
2897
- readline.once("close", () => resolve7(void 0));
3212
+ const ask = (prompt) => new Promise((resolve8) => {
3213
+ readline.question(prompt, resolve8);
3214
+ readline.once("close", () => resolve8(void 0));
2898
3215
  });
2899
3216
  const root = process.cwd();
2900
3217
  let running = true;
@@ -3078,7 +3395,7 @@ async function write2(args) {
3078
3395
  }
3079
3396
 
3080
3397
  // src/commands/memory.ts
3081
- import { readFileSync as readFileSync7 } from "node:fs";
3398
+ import { readFileSync as readFileSync8 } from "node:fs";
3082
3399
 
3083
3400
  // src/spaces.ts
3084
3401
  async function spacesFor(context, client, env = process.env) {
@@ -3142,7 +3459,7 @@ async function rememberCommand(context) {
3142
3459
  let text;
3143
3460
  if (file) {
3144
3461
  try {
3145
- text = readFileSync7(file, "utf8");
3462
+ text = readFileSync8(file, "utf8");
3146
3463
  } catch {
3147
3464
  context.error(`Could not read ${file}.`);
3148
3465
  return 1;
@@ -3450,6 +3767,18 @@ async function dispatch(context) {
3450
3767
  return statusCommand(context);
3451
3768
  case "requests":
3452
3769
  return requestsCommand(context);
3770
+ /*
3771
+ Google, through the API rather than through Google.
3772
+
3773
+ A laptop holds a bearer token and no database connection, so the
3774
+ credential it can prove is the one the API accepts. Google's own tokens
3775
+ never leave the deployment — which is what stops a stolen
3776
+ `~/.persistmemory` from being a stolen mailbox.
3777
+ */
3778
+ case "drive":
3779
+ return driveCommand(context);
3780
+ case "mail":
3781
+ return mailCommand(context);
3453
3782
  /**
3454
3783
  * `pm <verb> <noun>`, the grammar the Harness CLI uses.
3455
3784
  *