@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/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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1572
|
-
resolveCallback =
|
|
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((
|
|
1752
|
+
await new Promise((resolve8, reject) => {
|
|
1599
1753
|
server.once("error", reject);
|
|
1600
|
-
server.listen(0, "127.0.0.1",
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2091
|
+
resolve8(answer3.trim());
|
|
1938
2092
|
});
|
|
1939
|
-
readline.once("close", () =>
|
|
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((
|
|
2102
|
+
return new Promise((resolve8) => {
|
|
1949
2103
|
const readline = createInterface({ input });
|
|
1950
2104
|
readline.once("line", (line) => {
|
|
1951
2105
|
readline.close();
|
|
1952
|
-
|
|
2106
|
+
resolve8(line.trim());
|
|
1953
2107
|
});
|
|
1954
|
-
readline.once("close", () =>
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2325
|
-
import { resolve as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
2896
|
-
readline.question(prompt,
|
|
2897
|
-
readline.once("close", () =>
|
|
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
|
|
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 =
|
|
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
|
*
|