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