@persistmemory/cli 0.1.2 → 0.2.1
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 +436 -49
- package/dist/bin.js.map +4 -4
- package/dist/index.js +436 -49
- 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
|
}
|
|
@@ -1377,7 +1525,7 @@ function shortDate(iso) {
|
|
|
1377
1525
|
}
|
|
1378
1526
|
|
|
1379
1527
|
// src/help.ts
|
|
1380
|
-
var VERSION = "0.1
|
|
1528
|
+
var VERSION = true ? "0.2.1" : versionFromManifest();
|
|
1381
1529
|
var PACKAGE = "@persistmemory/cli";
|
|
1382
1530
|
var HELP = `
|
|
1383
1531
|
pm \u2014 PersistMemory from your terminal
|
|
@@ -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) {
|
|
@@ -2003,8 +2157,8 @@ async function readStdin() {
|
|
|
2003
2157
|
// src/commands/agent.ts
|
|
2004
2158
|
import { hostname } from "node:os";
|
|
2005
2159
|
import { homedir as homedir2 } from "node:os";
|
|
2006
|
-
import { join as join4, resolve as resolve3 } from "node:path";
|
|
2007
|
-
import { readFileSync as readFileSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
2160
|
+
import { basename, join as join4, resolve as resolve3 } from "node:path";
|
|
2161
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2008
2162
|
|
|
2009
2163
|
// src/files.ts
|
|
2010
2164
|
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -2129,6 +2283,17 @@ function locate(roots, requested) {
|
|
|
2129
2283
|
}
|
|
2130
2284
|
throw new Error(`${requested} is not inside any allowed folder (${roots.join(", ")})`);
|
|
2131
2285
|
}
|
|
2286
|
+
function uncontested(target) {
|
|
2287
|
+
if (!existsSync4(target)) return target;
|
|
2288
|
+
const dot = target.lastIndexOf(".");
|
|
2289
|
+
const stem = dot > target.lastIndexOf("/") && dot !== -1 ? target.slice(0, dot) : target;
|
|
2290
|
+
const extension = stem === target ? "" : target.slice(dot);
|
|
2291
|
+
for (let n = 1; n < 1e3; n += 1) {
|
|
2292
|
+
const candidate = `${stem} (${n})${extension}`;
|
|
2293
|
+
if (!existsSync4(candidate)) return candidate;
|
|
2294
|
+
}
|
|
2295
|
+
throw new Error(`${target} and a thousand names beside it are taken.`);
|
|
2296
|
+
}
|
|
2132
2297
|
async function answer(context, apiUrl, token, roots, request) {
|
|
2133
2298
|
let located;
|
|
2134
2299
|
try {
|
|
@@ -2136,6 +2301,43 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2136
2301
|
} catch (error) {
|
|
2137
2302
|
return { ok: false, error: error instanceof Error ? error.message : "refused" };
|
|
2138
2303
|
}
|
|
2304
|
+
if (request.kind === "write_file") {
|
|
2305
|
+
if (!request.sourceUrl) return { ok: false, error: "There was nothing to write." };
|
|
2306
|
+
let downloaded;
|
|
2307
|
+
let fetched;
|
|
2308
|
+
try {
|
|
2309
|
+
fetched = await fetch(request.sourceUrl);
|
|
2310
|
+
if (!fetched.ok) {
|
|
2311
|
+
return { ok: false, error: await said(fetched, "The file could not be fetched") };
|
|
2312
|
+
}
|
|
2313
|
+
downloaded = Buffer.from(await fetched.arrayBuffer());
|
|
2314
|
+
} catch {
|
|
2315
|
+
return { ok: false, error: "The file could not be fetched from the service." };
|
|
2316
|
+
}
|
|
2317
|
+
try {
|
|
2318
|
+
let target = located;
|
|
2319
|
+
if (existsSync4(located) && statSync2(located).isDirectory()) {
|
|
2320
|
+
const disposition = fetched.headers.get("content-disposition") ?? "";
|
|
2321
|
+
const named = /filename="([^"]+)"/.exec(disposition)?.[1];
|
|
2322
|
+
target = join4(located, basename(named ?? "file"));
|
|
2323
|
+
}
|
|
2324
|
+
target = uncontested(target);
|
|
2325
|
+
writeFileSync4(target, downloaded, { flag: "wx" });
|
|
2326
|
+
return upload(
|
|
2327
|
+
apiUrl,
|
|
2328
|
+
token,
|
|
2329
|
+
"written.txt",
|
|
2330
|
+
Buffer.from(`Saved to ${target}
|
|
2331
|
+
${downloaded.length} bytes
|
|
2332
|
+
`, "utf8")
|
|
2333
|
+
);
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
return {
|
|
2336
|
+
ok: false,
|
|
2337
|
+
error: error instanceof Error ? error.message : "could not write it"
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2139
2341
|
let bytes;
|
|
2140
2342
|
let filename = request.path.split("/").pop() ?? "file";
|
|
2141
2343
|
if (request.kind === "list_dir") {
|
|
@@ -2265,7 +2467,7 @@ async function agentCommand(context) {
|
|
|
2265
2467
|
body: JSON.stringify(body)
|
|
2266
2468
|
});
|
|
2267
2469
|
let complaint;
|
|
2268
|
-
const
|
|
2470
|
+
const complain2 = (message2) => {
|
|
2269
2471
|
if (complaint === message2) return;
|
|
2270
2472
|
complaint = message2;
|
|
2271
2473
|
context.error(` ${message2}`);
|
|
@@ -2280,7 +2482,7 @@ async function agentCommand(context) {
|
|
|
2280
2482
|
try {
|
|
2281
2483
|
const beat = await call("heartbeat", { hostname: name, platform: process.platform });
|
|
2282
2484
|
if (!beat.ok) {
|
|
2283
|
-
|
|
2485
|
+
complain2(await said(beat, "the service refused this machine"));
|
|
2284
2486
|
} else {
|
|
2285
2487
|
working();
|
|
2286
2488
|
const state = await beat.json();
|
|
@@ -2295,7 +2497,7 @@ async function agentCommand(context) {
|
|
|
2295
2497
|
}
|
|
2296
2498
|
const claimed = await call("claim", { hostname: name, limit: 5 });
|
|
2297
2499
|
if (!claimed.ok) {
|
|
2298
|
-
|
|
2500
|
+
complain2(await said(claimed, "could not pick up work"));
|
|
2299
2501
|
} else {
|
|
2300
2502
|
const { items } = await claimed.json();
|
|
2301
2503
|
for (const request of items) {
|
|
@@ -2320,9 +2522,172 @@ async function agentCommand(context) {
|
|
|
2320
2522
|
return 0;
|
|
2321
2523
|
}
|
|
2322
2524
|
|
|
2525
|
+
// src/commands/google.ts
|
|
2526
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
2527
|
+
import { basename as basename2, resolve as resolve4 } from "node:path";
|
|
2528
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2529
|
+
async function callApi(context, path, init = {}) {
|
|
2530
|
+
const credential = context.resolved.credential;
|
|
2531
|
+
if (!credential) {
|
|
2532
|
+
context.error("Sign in first: pm auth login");
|
|
2533
|
+
return void 0;
|
|
2534
|
+
}
|
|
2535
|
+
const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
|
|
2536
|
+
return fetch(`${apiUrl}${path}`, {
|
|
2537
|
+
...init,
|
|
2538
|
+
headers: {
|
|
2539
|
+
authorization: `Bearer ${credential.token}`,
|
|
2540
|
+
...init.headers ?? {}
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
async function complain(context, response) {
|
|
2545
|
+
const body = await response.json().catch(() => void 0);
|
|
2546
|
+
context.error(body?.error?.message ?? `That failed (${response.status}).`);
|
|
2547
|
+
return response.status === 409 ? 3 : 1;
|
|
2548
|
+
}
|
|
2549
|
+
async function driveCommand(context) {
|
|
2550
|
+
const [, noun, ...rest] = context.args.words;
|
|
2551
|
+
if (noun === "get") return driveGet(context, rest.join(" ").trim());
|
|
2552
|
+
if (noun === "put" || noun === "save") return drivePut(context, rest.join(" ").trim());
|
|
2553
|
+
const query = [noun, ...rest].filter(Boolean).join(" ").trim();
|
|
2554
|
+
const response = await callApi(
|
|
2555
|
+
context,
|
|
2556
|
+
`/api/v1/google/drive/files?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
|
|
2557
|
+
);
|
|
2558
|
+
if (!response) return 1;
|
|
2559
|
+
if (!response.ok) return complain(context, response);
|
|
2560
|
+
const { data } = await response.json();
|
|
2561
|
+
if (data.length === 0) {
|
|
2562
|
+
context.print(query ? `Nothing in Drive matches "${query}".` : "That Drive is empty.");
|
|
2563
|
+
return 0;
|
|
2564
|
+
}
|
|
2565
|
+
if (context.flags.output === "json") {
|
|
2566
|
+
context.print(JSON.stringify(data, void 0, 2));
|
|
2567
|
+
return 0;
|
|
2568
|
+
}
|
|
2569
|
+
for (const file of data) {
|
|
2570
|
+
context.print(` ${file.name}`);
|
|
2571
|
+
context.print(
|
|
2572
|
+
` ${file.id}${file.size ? ` \xB7 ${Math.round(file.size / 1024)} KB` : ""}${file.modifiedTime ? ` \xB7 ${file.modifiedTime.slice(0, 10)}` : ""}`
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
context.print("");
|
|
2576
|
+
context.print("Fetch one with: pm drive get <id>");
|
|
2577
|
+
return 0;
|
|
2578
|
+
}
|
|
2579
|
+
async function driveGet(context, fileId) {
|
|
2580
|
+
if (!fileId) {
|
|
2581
|
+
context.error("Say which file: pm drive get <id>");
|
|
2582
|
+
return 2;
|
|
2583
|
+
}
|
|
2584
|
+
const response = await callApi(
|
|
2585
|
+
context,
|
|
2586
|
+
`/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`
|
|
2587
|
+
);
|
|
2588
|
+
if (!response) return 1;
|
|
2589
|
+
if (!response.ok) return complain(context, response);
|
|
2590
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
2591
|
+
const named = /filename="([^"]+)"/.exec(disposition)?.[1];
|
|
2592
|
+
const out = stringFlag(context.args, "out");
|
|
2593
|
+
const target = resolve4(out ?? basename2(named ?? fileId));
|
|
2594
|
+
writeFileSync5(target, Buffer.from(await response.arrayBuffer()));
|
|
2595
|
+
context.print(target);
|
|
2596
|
+
return 0;
|
|
2597
|
+
}
|
|
2598
|
+
async function drivePut(context, path) {
|
|
2599
|
+
if (!path) {
|
|
2600
|
+
context.error("Say which file: pm drive put ./notes.md");
|
|
2601
|
+
return 2;
|
|
2602
|
+
}
|
|
2603
|
+
let bytes;
|
|
2604
|
+
try {
|
|
2605
|
+
bytes = readFileSync5(resolve4(path));
|
|
2606
|
+
} catch {
|
|
2607
|
+
context.error(`Cannot read ${path}.`);
|
|
2608
|
+
return 1;
|
|
2609
|
+
}
|
|
2610
|
+
const name = stringFlag(context.args, "name") ?? basename2(path);
|
|
2611
|
+
const response = await callApi(
|
|
2612
|
+
context,
|
|
2613
|
+
`/api/v1/google/drive/files?name=${encodeURIComponent(name)}`,
|
|
2614
|
+
{
|
|
2615
|
+
method: "POST",
|
|
2616
|
+
// The bytes as bytes. Base64 in JSON would be a third larger and would
|
|
2617
|
+
// make the limit somebody was told about stop matching the one they hit.
|
|
2618
|
+
headers: { "content-type": "application/octet-stream" },
|
|
2619
|
+
body: new Uint8Array(bytes)
|
|
2620
|
+
}
|
|
2621
|
+
);
|
|
2622
|
+
if (!response) return 1;
|
|
2623
|
+
if (!response.ok) return complain(context, response);
|
|
2624
|
+
const saved = await response.json();
|
|
2625
|
+
context.print(`Saved "${saved.name}" to Drive.${saved.link ? ` ${saved.link}` : ""}`);
|
|
2626
|
+
return 0;
|
|
2627
|
+
}
|
|
2628
|
+
async function mailCommand(context) {
|
|
2629
|
+
const [, noun, ...rest] = context.args.words;
|
|
2630
|
+
if (noun === "read" || noun === "get") {
|
|
2631
|
+
const messageId = rest.join(" ").trim();
|
|
2632
|
+
if (!messageId) {
|
|
2633
|
+
context.error("Say which message: pm mail read <id>");
|
|
2634
|
+
return 2;
|
|
2635
|
+
}
|
|
2636
|
+
const response2 = await callApi(
|
|
2637
|
+
context,
|
|
2638
|
+
`/api/v1/google/mail/${encodeURIComponent(messageId)}`
|
|
2639
|
+
);
|
|
2640
|
+
if (!response2) return 1;
|
|
2641
|
+
if (!response2.ok) return complain(context, response2);
|
|
2642
|
+
const message2 = await response2.json();
|
|
2643
|
+
if (context.flags.output === "json") {
|
|
2644
|
+
context.print(JSON.stringify(message2, void 0, 2));
|
|
2645
|
+
return 0;
|
|
2646
|
+
}
|
|
2647
|
+
context.print(`From: ${message2.from ?? "unknown"}`);
|
|
2648
|
+
context.print(`Subject: ${message2.subject ?? "(none)"}`);
|
|
2649
|
+
if (message2.date) context.print(`Date: ${message2.date}`);
|
|
2650
|
+
context.print("");
|
|
2651
|
+
context.print(message2.body);
|
|
2652
|
+
if (message2.attachments.length > 0) {
|
|
2653
|
+
context.print("");
|
|
2654
|
+
context.print("Attached:");
|
|
2655
|
+
for (const one of message2.attachments) context.print(` ${one.filename} (${one.mimeType})`);
|
|
2656
|
+
}
|
|
2657
|
+
return 0;
|
|
2658
|
+
}
|
|
2659
|
+
const query = [noun, ...rest].filter(Boolean).join(" ").trim();
|
|
2660
|
+
const response = await callApi(
|
|
2661
|
+
context,
|
|
2662
|
+
`/api/v1/google/mail?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
|
|
2663
|
+
);
|
|
2664
|
+
if (!response) return 1;
|
|
2665
|
+
if (!response.ok) return complain(context, response);
|
|
2666
|
+
const { data } = await response.json();
|
|
2667
|
+
if (data.length === 0) {
|
|
2668
|
+
context.print(query ? `No mail matches "${query}".` : "Nothing in that mailbox.");
|
|
2669
|
+
return 0;
|
|
2670
|
+
}
|
|
2671
|
+
if (context.flags.output === "json") {
|
|
2672
|
+
context.print(JSON.stringify(data, void 0, 2));
|
|
2673
|
+
return 0;
|
|
2674
|
+
}
|
|
2675
|
+
for (const message2 of data) {
|
|
2676
|
+
const marks = [message2.unread ? "unread" : "", message2.hasAttachments ? "attachment" : ""].filter(Boolean).join(", ");
|
|
2677
|
+
context.print(` ${message2.subject ?? "(no subject)"}${marks ? ` [${marks}]` : ""}`);
|
|
2678
|
+
context.print(
|
|
2679
|
+
` ${message2.from ?? "unknown"}${message2.date ? ` \xB7 ${message2.date.slice(0, 10)}` : ""}`
|
|
2680
|
+
);
|
|
2681
|
+
context.print(` ${message2.id}`);
|
|
2682
|
+
}
|
|
2683
|
+
context.print("");
|
|
2684
|
+
context.print("Read one with: pm mail read <id>");
|
|
2685
|
+
return 0;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2323
2688
|
// src/commands/requests.ts
|
|
2324
|
-
import { existsSync as
|
|
2325
|
-
import { resolve as
|
|
2689
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2690
|
+
import { resolve as resolve5 } from "node:path";
|
|
2326
2691
|
async function requestsCommand(context) {
|
|
2327
2692
|
if (context.args.words[1] === "get") return collectCommand(context);
|
|
2328
2693
|
const credential = context.resolved.credential;
|
|
@@ -2390,25 +2755,25 @@ async function collectCommand(context) {
|
|
|
2390
2755
|
return 1;
|
|
2391
2756
|
}
|
|
2392
2757
|
const name = stringFlag(context.args, "output", "o") ?? filename;
|
|
2393
|
-
const target =
|
|
2394
|
-
if (
|
|
2758
|
+
const target = resolve5(name);
|
|
2759
|
+
if (existsSync5(target)) {
|
|
2395
2760
|
context.error(`${target} already exists. Pass --output to write somewhere else.`);
|
|
2396
2761
|
return 1;
|
|
2397
2762
|
}
|
|
2398
|
-
|
|
2763
|
+
writeFileSync6(target, new Uint8Array(await file.arrayBuffer()));
|
|
2399
2764
|
context.print(`Wrote ${target}`);
|
|
2400
2765
|
return 0;
|
|
2401
2766
|
}
|
|
2402
2767
|
|
|
2403
2768
|
// src/workspace.ts
|
|
2404
|
-
import { existsSync as
|
|
2769
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2405
2770
|
import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
|
|
2406
2771
|
var WORKSPACE_FILE = ".persistmemory.json";
|
|
2407
2772
|
function findWorkspace(from = process.cwd()) {
|
|
2408
2773
|
let dir = resolvePath(from);
|
|
2409
2774
|
for (; ; ) {
|
|
2410
2775
|
const file = join5(dir, WORKSPACE_FILE);
|
|
2411
|
-
if (
|
|
2776
|
+
if (existsSync6(file)) {
|
|
2412
2777
|
const config = readWorkspace(file);
|
|
2413
2778
|
if (config) return { file, dir, config };
|
|
2414
2779
|
}
|
|
@@ -2419,7 +2784,7 @@ function findWorkspace(from = process.cwd()) {
|
|
|
2419
2784
|
}
|
|
2420
2785
|
function readWorkspace(file) {
|
|
2421
2786
|
try {
|
|
2422
|
-
const parsed = JSON.parse(
|
|
2787
|
+
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
2423
2788
|
const space = parsed.space;
|
|
2424
2789
|
if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
|
|
2425
2790
|
return { space: { id: space.id, name: space.name } };
|
|
@@ -2431,7 +2796,7 @@ function readWorkspace(file) {
|
|
|
2431
2796
|
}
|
|
2432
2797
|
function writeWorkspace(dir, config) {
|
|
2433
2798
|
const file = join5(dir, WORKSPACE_FILE);
|
|
2434
|
-
|
|
2799
|
+
writeFileSync7(file, `${JSON.stringify(config, null, 2)}
|
|
2435
2800
|
`, "utf8");
|
|
2436
2801
|
return file;
|
|
2437
2802
|
}
|
|
@@ -2700,9 +3065,9 @@ function message(error) {
|
|
|
2700
3065
|
}
|
|
2701
3066
|
|
|
2702
3067
|
// src/commands/maintain.ts
|
|
2703
|
-
import { existsSync as
|
|
3068
|
+
import { existsSync as existsSync7, rmSync } from "node:fs";
|
|
2704
3069
|
import { spawnSync } from "node:child_process";
|
|
2705
|
-
import { dirname as dirname4, resolve as
|
|
3070
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
2706
3071
|
import { fileURLToPath } from "node:url";
|
|
2707
3072
|
async function updateCommand(context) {
|
|
2708
3073
|
const manager = installer();
|
|
@@ -2744,7 +3109,7 @@ Delete the file it runs from: ${processPath()}`
|
|
|
2744
3109
|
return 0;
|
|
2745
3110
|
}
|
|
2746
3111
|
async function deleteCommand(context) {
|
|
2747
|
-
if (!
|
|
3112
|
+
if (!existsSync7(context.paths.dir)) {
|
|
2748
3113
|
context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
|
|
2749
3114
|
return 0;
|
|
2750
3115
|
}
|
|
@@ -2773,7 +3138,7 @@ async function deleteCommand(context) {
|
|
|
2773
3138
|
return 0;
|
|
2774
3139
|
}
|
|
2775
3140
|
function removeEverything(context) {
|
|
2776
|
-
const dir =
|
|
3141
|
+
const dir = resolve7(context.paths.dir);
|
|
2777
3142
|
if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
|
|
2778
3143
|
context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
|
|
2779
3144
|
return;
|
|
@@ -2785,7 +3150,7 @@ function installer() {
|
|
|
2785
3150
|
}
|
|
2786
3151
|
function processPath() {
|
|
2787
3152
|
try {
|
|
2788
|
-
return
|
|
3153
|
+
return resolve7(dirname4(fileURLToPath(import.meta.url)));
|
|
2789
3154
|
} catch {
|
|
2790
3155
|
return process.argv[1] ?? "";
|
|
2791
3156
|
}
|
|
@@ -2797,7 +3162,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2797
3162
|
import { relative as relative2 } from "node:path";
|
|
2798
3163
|
|
|
2799
3164
|
// src/events.ts
|
|
2800
|
-
import { appendFileSync, existsSync as
|
|
3165
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
2801
3166
|
import { join as join6 } from "node:path";
|
|
2802
3167
|
function openSessionLog(paths, id) {
|
|
2803
3168
|
const directory = join6(paths.dir, "sessions");
|
|
@@ -2814,8 +3179,8 @@ function openSessionLog(paths, id) {
|
|
|
2814
3179
|
}
|
|
2815
3180
|
},
|
|
2816
3181
|
read() {
|
|
2817
|
-
if (!
|
|
2818
|
-
return
|
|
3182
|
+
if (!existsSync8(path)) return [];
|
|
3183
|
+
return readFileSync7(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
2819
3184
|
try {
|
|
2820
3185
|
return [JSON.parse(line)];
|
|
2821
3186
|
} catch {
|
|
@@ -2892,9 +3257,9 @@ async function sessionCommand(context) {
|
|
|
2892
3257
|
context.print(` Ask anything. /help for commands, /exit to leave.
|
|
2893
3258
|
`);
|
|
2894
3259
|
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2895
|
-
const ask = (prompt) => new Promise((
|
|
2896
|
-
readline.question(prompt,
|
|
2897
|
-
readline.once("close", () =>
|
|
3260
|
+
const ask = (prompt) => new Promise((resolve8) => {
|
|
3261
|
+
readline.question(prompt, resolve8);
|
|
3262
|
+
readline.once("close", () => resolve8(void 0));
|
|
2898
3263
|
});
|
|
2899
3264
|
const root = process.cwd();
|
|
2900
3265
|
let running = true;
|
|
@@ -3078,7 +3443,7 @@ async function write2(args) {
|
|
|
3078
3443
|
}
|
|
3079
3444
|
|
|
3080
3445
|
// src/commands/memory.ts
|
|
3081
|
-
import { readFileSync as
|
|
3446
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3082
3447
|
|
|
3083
3448
|
// src/spaces.ts
|
|
3084
3449
|
async function spacesFor(context, client, env = process.env) {
|
|
@@ -3142,7 +3507,7 @@ async function rememberCommand(context) {
|
|
|
3142
3507
|
let text;
|
|
3143
3508
|
if (file) {
|
|
3144
3509
|
try {
|
|
3145
|
-
text =
|
|
3510
|
+
text = readFileSync8(file, "utf8");
|
|
3146
3511
|
} catch {
|
|
3147
3512
|
context.error(`Could not read ${file}.`);
|
|
3148
3513
|
return 1;
|
|
@@ -3430,6 +3795,16 @@ async function dispatch(context) {
|
|
|
3430
3795
|
case "setup":
|
|
3431
3796
|
case "init":
|
|
3432
3797
|
return setupCommand(context);
|
|
3798
|
+
/*
|
|
3799
|
+
`pm version` as well as `--version`.
|
|
3800
|
+
|
|
3801
|
+
It is what people type, and answering "Unknown command" to somebody
|
|
3802
|
+
asking which version they are running — while a flag two characters away
|
|
3803
|
+
answers it — is a needless dead end.
|
|
3804
|
+
*/
|
|
3805
|
+
case "version":
|
|
3806
|
+
context.print(VERSION);
|
|
3807
|
+
return 0;
|
|
3433
3808
|
case "update":
|
|
3434
3809
|
case "upgrade":
|
|
3435
3810
|
return updateCommand(context);
|
|
@@ -3450,6 +3825,18 @@ async function dispatch(context) {
|
|
|
3450
3825
|
return statusCommand(context);
|
|
3451
3826
|
case "requests":
|
|
3452
3827
|
return requestsCommand(context);
|
|
3828
|
+
/*
|
|
3829
|
+
Google, through the API rather than through Google.
|
|
3830
|
+
|
|
3831
|
+
A laptop holds a bearer token and no database connection, so the
|
|
3832
|
+
credential it can prove is the one the API accepts. Google's own tokens
|
|
3833
|
+
never leave the deployment — which is what stops a stolen
|
|
3834
|
+
`~/.persistmemory` from being a stolen mailbox.
|
|
3835
|
+
*/
|
|
3836
|
+
case "drive":
|
|
3837
|
+
return driveCommand(context);
|
|
3838
|
+
case "mail":
|
|
3839
|
+
return mailCommand(context);
|
|
3453
3840
|
/**
|
|
3454
3841
|
* `pm <verb> <noun>`, the grammar the Harness CLI uses.
|
|
3455
3842
|
*
|