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