@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 +370 -41
- package/dist/bin.js.map +4 -4
- package/dist/index.js +370 -41
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1574
|
-
resolveCallback =
|
|
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((
|
|
1754
|
+
await new Promise((resolve8, reject) => {
|
|
1601
1755
|
server.once("error", reject);
|
|
1602
|
-
server.listen(0, "127.0.0.1",
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2093
|
+
resolve8(answer3.trim());
|
|
1940
2094
|
});
|
|
1941
|
-
readline.once("close", () =>
|
|
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((
|
|
2104
|
+
return new Promise((resolve8) => {
|
|
1951
2105
|
const readline = createInterface({ input });
|
|
1952
2106
|
readline.once("line", (line) => {
|
|
1953
2107
|
readline.close();
|
|
1954
|
-
|
|
2108
|
+
resolve8(line.trim());
|
|
1955
2109
|
});
|
|
1956
|
-
readline.once("close", () =>
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2327
|
-
import { resolve as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
2898
|
-
readline.question(prompt,
|
|
2899
|
-
readline.once("close", () =>
|
|
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
|
|
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 =
|
|
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
|
*
|