@persistmemory/cli 0.1.1 → 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 +554 -59
- package/dist/bin.js.map +4 -4
- package/dist/index.js +554 -59
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
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);
|
|
@@ -556,6 +588,58 @@ var Spaces = class {
|
|
|
556
588
|
async create(params, options) {
|
|
557
589
|
return this.#http.post("/api/v1/spaces", params, options);
|
|
558
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Deletes a Space. You must say what happens to what is in it.
|
|
593
|
+
*
|
|
594
|
+
* There is no default, here or in the API, and that is deliberate: "delete
|
|
595
|
+
* this Space" means the label to some people and everything inside it to
|
|
596
|
+
* others, and a client that guessed would destroy or keep somebody's
|
|
597
|
+
* material without being asked.
|
|
598
|
+
*
|
|
599
|
+
* `delete` never destroys a memory that is filed in another Space as well —
|
|
600
|
+
* that one is detached and left alone. `deleted` and `kept` come back so you
|
|
601
|
+
* can say what actually happened.
|
|
602
|
+
*/
|
|
603
|
+
async delete(id, params, options) {
|
|
604
|
+
return this.#http.delete(
|
|
605
|
+
`/api/v1/spaces/${encodeURIComponent(id)}`,
|
|
606
|
+
params,
|
|
607
|
+
options
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Merges Spaces into a NEW one, leaving every source exactly as it was.
|
|
612
|
+
*
|
|
613
|
+
* Additive, not destructive: a memory ends up in the sources AND the result,
|
|
614
|
+
* every existing search over a source returns what it did before, and
|
|
615
|
+
* undoing it is deleting the Space this returns. A memory in two sources is
|
|
616
|
+
* filed once.
|
|
617
|
+
*/
|
|
618
|
+
async merge(params, options) {
|
|
619
|
+
return this.#http.post(
|
|
620
|
+
"/api/v1/spaces/merge",
|
|
621
|
+
params,
|
|
622
|
+
options
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The Space this account files into when a capture names none.
|
|
627
|
+
*
|
|
628
|
+
* `{}` — an object with no `space` — means there is no default, which is the
|
|
629
|
+
* normal state rather than a gap. It is also what comes back after the Space
|
|
630
|
+
* somebody chose has been deleted.
|
|
631
|
+
*/
|
|
632
|
+
async getDefault(options) {
|
|
633
|
+
return this.#http.get("/api/v1/spaces/default", void 0, options);
|
|
634
|
+
}
|
|
635
|
+
/** `null` clears it. Not the same as omitting it, which is why the type says so. */
|
|
636
|
+
async setDefault(spaceId, options) {
|
|
637
|
+
return this.#http.patch(
|
|
638
|
+
"/api/v1/spaces/default",
|
|
639
|
+
{ spaceId },
|
|
640
|
+
options
|
|
641
|
+
);
|
|
642
|
+
}
|
|
559
643
|
/** Renaming, retention, and archiving - `archived` is a field, not a verb. */
|
|
560
644
|
async update(id, params, options) {
|
|
561
645
|
return this.#http.patch(`/api/v1/spaces/${encodeURIComponent(id)}`, params, options);
|
|
@@ -836,6 +920,119 @@ var Conversations = class {
|
|
|
836
920
|
);
|
|
837
921
|
}
|
|
838
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
|
+
};
|
|
839
1036
|
var Integrations = class {
|
|
840
1037
|
#http;
|
|
841
1038
|
constructor(http) {
|
|
@@ -918,6 +1115,42 @@ var Health = class {
|
|
|
918
1115
|
return this.#http.get("/health/ready", void 0, options);
|
|
919
1116
|
}
|
|
920
1117
|
};
|
|
1118
|
+
var Agent = class {
|
|
1119
|
+
#http;
|
|
1120
|
+
constructor(http) {
|
|
1121
|
+
this.#http = http;
|
|
1122
|
+
}
|
|
1123
|
+
/** The row, including whether it finished and how large the result is. */
|
|
1124
|
+
async request(id, options) {
|
|
1125
|
+
return this.#http.get(
|
|
1126
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}`,
|
|
1127
|
+
void 0,
|
|
1128
|
+
options
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* A short-lived link to the bytes of a finished request.
|
|
1133
|
+
*
|
|
1134
|
+
* Returns the URL rather than the file, and that is a deliberate limit of
|
|
1135
|
+
* this package rather than an oversight. The transport under every other
|
|
1136
|
+
* method parses JSON, retries, and attaches the API key; none of those is
|
|
1137
|
+
* right for a hundred-megabyte binary body, and building a second request
|
|
1138
|
+
* path inside the SDK to serve one method is how a client ends up with two
|
|
1139
|
+
* retry policies that differ only during an outage. Fetch the URL with
|
|
1140
|
+
* whatever already streams in your runtime - it needs no credential, which
|
|
1141
|
+
* is the whole reason it is signed.
|
|
1142
|
+
*
|
|
1143
|
+
* Treat the URL as the file. It is a bearer credential for exactly one
|
|
1144
|
+
* object, it expires in minutes, and it should not be logged or stored.
|
|
1145
|
+
*/
|
|
1146
|
+
async downloadLink(id, options) {
|
|
1147
|
+
return this.#http.get(
|
|
1148
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
1149
|
+
void 0,
|
|
1150
|
+
options
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
921
1154
|
var PersistMemory = class {
|
|
922
1155
|
memories;
|
|
923
1156
|
search;
|
|
@@ -930,7 +1163,10 @@ var PersistMemory = class {
|
|
|
930
1163
|
conflicts;
|
|
931
1164
|
conversations;
|
|
932
1165
|
integrations;
|
|
1166
|
+
/** Drive, mail and contacts on the user's connected Google account. */
|
|
1167
|
+
google;
|
|
933
1168
|
health;
|
|
1169
|
+
agent;
|
|
934
1170
|
#http;
|
|
935
1171
|
constructor(options) {
|
|
936
1172
|
this.#http = new HttpClient(options);
|
|
@@ -945,7 +1181,9 @@ var PersistMemory = class {
|
|
|
945
1181
|
this.conflicts = new Conflicts(this.#http);
|
|
946
1182
|
this.conversations = new Conversations(this.#http);
|
|
947
1183
|
this.integrations = new Integrations(this.#http);
|
|
1184
|
+
this.google = new Google(this.#http);
|
|
948
1185
|
this.health = new Health(this.#http);
|
|
1186
|
+
this.agent = new Agent(this.#http);
|
|
949
1187
|
}
|
|
950
1188
|
/**
|
|
951
1189
|
* An escape hatch for an endpoint this package has not caught up with.
|
|
@@ -1287,7 +1525,7 @@ function shortDate(iso) {
|
|
|
1287
1525
|
}
|
|
1288
1526
|
|
|
1289
1527
|
// src/help.ts
|
|
1290
|
-
var VERSION = "0.1.
|
|
1528
|
+
var VERSION = "0.1.2";
|
|
1291
1529
|
var PACKAGE = "@persistmemory/cli";
|
|
1292
1530
|
var HELP = `
|
|
1293
1531
|
pm \u2014 PersistMemory from your terminal
|
|
@@ -1332,8 +1570,15 @@ var HELP = `
|
|
|
1332
1570
|
list spaces your Spaces
|
|
1333
1571
|
get memory <id> one memory, in full
|
|
1334
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
|
+
|
|
1335
1579
|
status is the service healthy
|
|
1336
1580
|
requests file requests waiting for you to approve
|
|
1581
|
+
requests get <id> write a finished one to a file here
|
|
1337
1582
|
|
|
1338
1583
|
update install the newest version
|
|
1339
1584
|
uninstall remove pm from this machine
|
|
@@ -1477,8 +1722,8 @@ async function startLoopback(options = {}) {
|
|
|
1477
1722
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
1478
1723
|
let resolveCallback;
|
|
1479
1724
|
let rejectCallback;
|
|
1480
|
-
const received = new Promise((
|
|
1481
|
-
resolveCallback =
|
|
1725
|
+
const received = new Promise((resolve8, reject) => {
|
|
1726
|
+
resolveCallback = resolve8;
|
|
1482
1727
|
rejectCallback = reject;
|
|
1483
1728
|
});
|
|
1484
1729
|
const server = createServer((request, response) => {
|
|
@@ -1504,9 +1749,9 @@ async function startLoopback(options = {}) {
|
|
|
1504
1749
|
response.end(donePage(callback));
|
|
1505
1750
|
resolveCallback?.(callback);
|
|
1506
1751
|
});
|
|
1507
|
-
await new Promise((
|
|
1752
|
+
await new Promise((resolve8, reject) => {
|
|
1508
1753
|
server.once("error", reject);
|
|
1509
|
-
server.listen(0, "127.0.0.1",
|
|
1754
|
+
server.listen(0, "127.0.0.1", resolve8);
|
|
1510
1755
|
});
|
|
1511
1756
|
const address = server.address();
|
|
1512
1757
|
if (address === null || typeof address === "string") {
|
|
@@ -1768,7 +2013,7 @@ function safeEqual(a, b) {
|
|
|
1768
2013
|
}
|
|
1769
2014
|
async function openBrowser(url) {
|
|
1770
2015
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1771
|
-
await new Promise((
|
|
2016
|
+
await new Promise((resolve8, reject) => {
|
|
1772
2017
|
const child = spawn(command, args, {
|
|
1773
2018
|
stdio: "ignore",
|
|
1774
2019
|
// Detached so closing the terminal does not close the browser, and so
|
|
@@ -1777,7 +2022,7 @@ async function openBrowser(url) {
|
|
|
1777
2022
|
});
|
|
1778
2023
|
child.once("error", reject);
|
|
1779
2024
|
child.unref();
|
|
1780
|
-
|
|
2025
|
+
resolve8();
|
|
1781
2026
|
});
|
|
1782
2027
|
}
|
|
1783
2028
|
async function describe(response) {
|
|
@@ -1839,13 +2084,13 @@ async function currentCredential(resolved, deps) {
|
|
|
1839
2084
|
// src/context.ts
|
|
1840
2085
|
import { createInterface } from "node:readline";
|
|
1841
2086
|
async function askOnTty(prompt) {
|
|
1842
|
-
return new Promise((
|
|
2087
|
+
return new Promise((resolve8) => {
|
|
1843
2088
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1844
2089
|
readline.question(prompt, (answer3) => {
|
|
1845
2090
|
readline.close();
|
|
1846
|
-
|
|
2091
|
+
resolve8(answer3.trim());
|
|
1847
2092
|
});
|
|
1848
|
-
readline.once("close", () =>
|
|
2093
|
+
readline.once("close", () => resolve8(""));
|
|
1849
2094
|
});
|
|
1850
2095
|
}
|
|
1851
2096
|
var ETX = "";
|
|
@@ -1854,13 +2099,13 @@ var BACKSPACE = "\b";
|
|
|
1854
2099
|
async function readSecretFromTty(prompt) {
|
|
1855
2100
|
const input = process.stdin;
|
|
1856
2101
|
if (!input.isTTY) {
|
|
1857
|
-
return new Promise((
|
|
2102
|
+
return new Promise((resolve8) => {
|
|
1858
2103
|
const readline = createInterface({ input });
|
|
1859
2104
|
readline.once("line", (line) => {
|
|
1860
2105
|
readline.close();
|
|
1861
|
-
|
|
2106
|
+
resolve8(line.trim());
|
|
1862
2107
|
});
|
|
1863
|
-
readline.once("close", () =>
|
|
2108
|
+
readline.once("close", () => resolve8(""));
|
|
1864
2109
|
});
|
|
1865
2110
|
}
|
|
1866
2111
|
process.stdout.write(prompt);
|
|
@@ -1868,14 +2113,14 @@ async function readSecretFromTty(prompt) {
|
|
|
1868
2113
|
input.setRawMode?.(true);
|
|
1869
2114
|
input.resume();
|
|
1870
2115
|
input.setEncoding("utf8");
|
|
1871
|
-
return new Promise((
|
|
2116
|
+
return new Promise((resolve8) => {
|
|
1872
2117
|
let value = "";
|
|
1873
2118
|
const finish2 = () => {
|
|
1874
2119
|
input.removeListener("data", onData);
|
|
1875
2120
|
input.setRawMode?.(previouslyRaw);
|
|
1876
2121
|
input.pause();
|
|
1877
2122
|
process.stdout.write("\n");
|
|
1878
|
-
|
|
2123
|
+
resolve8(value.trim());
|
|
1879
2124
|
};
|
|
1880
2125
|
const onData = (chunk) => {
|
|
1881
2126
|
for (const character of chunk) {
|
|
@@ -1912,8 +2157,8 @@ async function readStdin() {
|
|
|
1912
2157
|
// src/commands/agent.ts
|
|
1913
2158
|
import { hostname } from "node:os";
|
|
1914
2159
|
import { homedir as homedir2 } from "node:os";
|
|
1915
|
-
import { resolve as resolve3 } from "node:path";
|
|
1916
|
-
import { readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
2160
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
2161
|
+
import { readFileSync as readFileSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
1917
2162
|
|
|
1918
2163
|
// src/files.ts
|
|
1919
2164
|
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -1926,11 +2171,13 @@ var OutsideWorkspace = class extends Error {
|
|
|
1926
2171
|
};
|
|
1927
2172
|
var TooLarge = class extends Error {
|
|
1928
2173
|
constructor(path, bytes, limit) {
|
|
1929
|
-
|
|
2174
|
+
const say = (value) => value >= 1024 * 1024 ? `${(value / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(value / 1024)} KB`;
|
|
2175
|
+
super(`${path} is ${say(bytes)}, over the ${say(limit)} limit.`);
|
|
1930
2176
|
this.name = "TooLarge";
|
|
1931
2177
|
}
|
|
1932
2178
|
};
|
|
1933
2179
|
var MAX_READ_BYTES = 512 * 1024;
|
|
2180
|
+
var MAX_TRANSFER_BYTES = 100 * 1024 * 1024;
|
|
1934
2181
|
function realLocation(absolute) {
|
|
1935
2182
|
let existing = absolute;
|
|
1936
2183
|
const trailing = [];
|
|
@@ -2044,19 +2291,41 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2044
2291
|
return { ok: false, error: error instanceof Error ? error.message : "refused" };
|
|
2045
2292
|
}
|
|
2046
2293
|
let bytes;
|
|
2294
|
+
let filename = request.path.split("/").pop() ?? "file";
|
|
2295
|
+
if (request.kind === "list_dir") {
|
|
2296
|
+
try {
|
|
2297
|
+
const stats = statSync2(located);
|
|
2298
|
+
if (!stats.isDirectory()) return { ok: false, error: `${request.path} is not a folder.` };
|
|
2299
|
+
const entries = readdirSync(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_LISTED).map((entry) => {
|
|
2300
|
+
if (entry.isDirectory()) return `${entry.name}/`;
|
|
2301
|
+
try {
|
|
2302
|
+
return `${entry.name} ${sizeOf(join4(located, entry.name))}`;
|
|
2303
|
+
} catch {
|
|
2304
|
+
return entry.name;
|
|
2305
|
+
}
|
|
2306
|
+
}).sort();
|
|
2307
|
+
const listing = entries.length > 0 ? entries.join("\n") : "(empty)";
|
|
2308
|
+
bytes = Buffer.from(`${request.path}
|
|
2309
|
+
|
|
2310
|
+
${listing}
|
|
2311
|
+
`, "utf8");
|
|
2312
|
+
filename = `${request.path.split("/").filter(Boolean).pop() ?? "listing"}.txt`;
|
|
2313
|
+
const grant = await upload(apiUrl, token, filename, bytes);
|
|
2314
|
+
return grant;
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
return { ok: false, error: error instanceof Error ? error.message : "could not list it" };
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2047
2319
|
try {
|
|
2048
2320
|
const stats = statSync2(located);
|
|
2049
2321
|
if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
|
|
2050
|
-
if (stats.size > MAX_READ_BYTES) {
|
|
2051
|
-
return {
|
|
2052
|
-
ok: false,
|
|
2053
|
-
error: new TooLarge(request.path, stats.size, MAX_READ_BYTES).message
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
2322
|
bytes = readFileSync4(located);
|
|
2057
2323
|
} catch (error) {
|
|
2058
2324
|
return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
|
|
2059
2325
|
}
|
|
2326
|
+
return upload(apiUrl, token, filename, bytes);
|
|
2327
|
+
}
|
|
2328
|
+
async function upload(apiUrl, token, filename, bytes) {
|
|
2060
2329
|
const grant = await fetch(`${apiUrl}/api/v1/agent/upload-url`, {
|
|
2061
2330
|
method: "POST",
|
|
2062
2331
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -2068,13 +2337,17 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2068
2337
|
// No content type is sent: this machine has a path, not a declaration.
|
|
2069
2338
|
// The server resolves it from the name against the one table that knows
|
|
2070
2339
|
// which types it can read, and tells us below what it decided.
|
|
2071
|
-
filename
|
|
2340
|
+
filename
|
|
2072
2341
|
})
|
|
2073
2342
|
});
|
|
2074
2343
|
if (!grant.ok) {
|
|
2075
2344
|
return { ok: false, error: await said(grant, "could not get an upload url") };
|
|
2076
2345
|
}
|
|
2077
|
-
const { uploadUrl, contentType } = await grant.json();
|
|
2346
|
+
const { uploadUrl, contentType, maxBytes } = await grant.json();
|
|
2347
|
+
const limit = maxBytes ?? MAX_TRANSFER_BYTES;
|
|
2348
|
+
if (bytes.length > limit) {
|
|
2349
|
+
return { ok: false, error: new TooLarge(filename, bytes.length, limit).message };
|
|
2350
|
+
}
|
|
2078
2351
|
const put = await fetch(uploadUrl, {
|
|
2079
2352
|
method: "PUT",
|
|
2080
2353
|
// The type the grant was signed for. Anything else is refused.
|
|
@@ -2086,6 +2359,13 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2086
2359
|
if (!stored.attachToken) return { ok: false, error: "the upload returned no reference" };
|
|
2087
2360
|
return { ok: true, attachToken: stored.attachToken, bytes: bytes.length };
|
|
2088
2361
|
}
|
|
2362
|
+
var MAX_LISTED = 200;
|
|
2363
|
+
function sizeOf(path) {
|
|
2364
|
+
const size = statSync2(path).size;
|
|
2365
|
+
if (size < 1024) return `${size} B`;
|
|
2366
|
+
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
|
2367
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
2368
|
+
}
|
|
2089
2369
|
async function agentCommand(context) {
|
|
2090
2370
|
const credential = context.resolved.credential;
|
|
2091
2371
|
if (!credential) {
|
|
@@ -2139,7 +2419,7 @@ async function agentCommand(context) {
|
|
|
2139
2419
|
body: JSON.stringify(body)
|
|
2140
2420
|
});
|
|
2141
2421
|
let complaint;
|
|
2142
|
-
const
|
|
2422
|
+
const complain2 = (message2) => {
|
|
2143
2423
|
if (complaint === message2) return;
|
|
2144
2424
|
complaint = message2;
|
|
2145
2425
|
context.error(` ${message2}`);
|
|
@@ -2154,7 +2434,7 @@ async function agentCommand(context) {
|
|
|
2154
2434
|
try {
|
|
2155
2435
|
const beat = await call("heartbeat", { hostname: name, platform: process.platform });
|
|
2156
2436
|
if (!beat.ok) {
|
|
2157
|
-
|
|
2437
|
+
complain2(await said(beat, "the service refused this machine"));
|
|
2158
2438
|
} else {
|
|
2159
2439
|
working();
|
|
2160
2440
|
const state = await beat.json();
|
|
@@ -2169,7 +2449,7 @@ async function agentCommand(context) {
|
|
|
2169
2449
|
}
|
|
2170
2450
|
const claimed = await call("claim", { hostname: name, limit: 5 });
|
|
2171
2451
|
if (!claimed.ok) {
|
|
2172
|
-
|
|
2452
|
+
complain2(await said(claimed, "could not pick up work"));
|
|
2173
2453
|
} else {
|
|
2174
2454
|
const { items } = await claimed.json();
|
|
2175
2455
|
for (const request of items) {
|
|
@@ -2194,8 +2474,174 @@ async function agentCommand(context) {
|
|
|
2194
2474
|
return 0;
|
|
2195
2475
|
}
|
|
2196
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
|
+
|
|
2197
2640
|
// src/commands/requests.ts
|
|
2641
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2642
|
+
import { resolve as resolve5 } from "node:path";
|
|
2198
2643
|
async function requestsCommand(context) {
|
|
2644
|
+
if (context.args.words[1] === "get") return collectCommand(context);
|
|
2199
2645
|
const credential = context.resolved.credential;
|
|
2200
2646
|
if (!credential) {
|
|
2201
2647
|
context.error("Sign in first: pm auth login");
|
|
@@ -2233,16 +2679,53 @@ async function requestsCommand(context) {
|
|
|
2233
2679
|
context.print("They cannot be approved from here \u2014 see `pm help requests`.");
|
|
2234
2680
|
return 0;
|
|
2235
2681
|
}
|
|
2682
|
+
async function collectCommand(context) {
|
|
2683
|
+
const credential = context.resolved.credential;
|
|
2684
|
+
if (!credential) {
|
|
2685
|
+
context.error("Sign in first: pm auth login");
|
|
2686
|
+
return 1;
|
|
2687
|
+
}
|
|
2688
|
+
const id = context.args.words[2];
|
|
2689
|
+
if (!id) {
|
|
2690
|
+
context.error("Which request? `pm requests` lists them with their ids.");
|
|
2691
|
+
return 2;
|
|
2692
|
+
}
|
|
2693
|
+
const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
|
|
2694
|
+
const link = await fetch(
|
|
2695
|
+
`${apiUrl}/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
2696
|
+
{ headers: { authorization: `Bearer ${credential.token}` } }
|
|
2697
|
+
);
|
|
2698
|
+
if (!link.ok) {
|
|
2699
|
+
const body = await link.json().catch(() => void 0);
|
|
2700
|
+
context.error(body?.error?.message ?? `Could not prepare that file (${link.status}).`);
|
|
2701
|
+
return 1;
|
|
2702
|
+
}
|
|
2703
|
+
const { downloadUrl, filename } = await link.json();
|
|
2704
|
+
const file = await fetch(downloadUrl);
|
|
2705
|
+
if (!file.ok) {
|
|
2706
|
+
context.error(`The download refused it (${file.status}). Links expire in minutes \u2014 try again.`);
|
|
2707
|
+
return 1;
|
|
2708
|
+
}
|
|
2709
|
+
const name = stringFlag(context.args, "output", "o") ?? filename;
|
|
2710
|
+
const target = resolve5(name);
|
|
2711
|
+
if (existsSync4(target)) {
|
|
2712
|
+
context.error(`${target} already exists. Pass --output to write somewhere else.`);
|
|
2713
|
+
return 1;
|
|
2714
|
+
}
|
|
2715
|
+
writeFileSync5(target, new Uint8Array(await file.arrayBuffer()));
|
|
2716
|
+
context.print(`Wrote ${target}`);
|
|
2717
|
+
return 0;
|
|
2718
|
+
}
|
|
2236
2719
|
|
|
2237
2720
|
// src/workspace.ts
|
|
2238
|
-
import { existsSync as
|
|
2239
|
-
import { dirname as dirname3, join as
|
|
2721
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2722
|
+
import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
|
|
2240
2723
|
var WORKSPACE_FILE = ".persistmemory.json";
|
|
2241
2724
|
function findWorkspace(from = process.cwd()) {
|
|
2242
2725
|
let dir = resolvePath(from);
|
|
2243
2726
|
for (; ; ) {
|
|
2244
|
-
const file =
|
|
2245
|
-
if (
|
|
2727
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2728
|
+
if (existsSync5(file)) {
|
|
2246
2729
|
const config = readWorkspace(file);
|
|
2247
2730
|
if (config) return { file, dir, config };
|
|
2248
2731
|
}
|
|
@@ -2253,7 +2736,7 @@ function findWorkspace(from = process.cwd()) {
|
|
|
2253
2736
|
}
|
|
2254
2737
|
function readWorkspace(file) {
|
|
2255
2738
|
try {
|
|
2256
|
-
const parsed = JSON.parse(
|
|
2739
|
+
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
2257
2740
|
const space = parsed.space;
|
|
2258
2741
|
if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
|
|
2259
2742
|
return { space: { id: space.id, name: space.name } };
|
|
@@ -2264,8 +2747,8 @@ function readWorkspace(file) {
|
|
|
2264
2747
|
}
|
|
2265
2748
|
}
|
|
2266
2749
|
function writeWorkspace(dir, config) {
|
|
2267
|
-
const file =
|
|
2268
|
-
|
|
2750
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2751
|
+
writeFileSync6(file, `${JSON.stringify(config, null, 2)}
|
|
2269
2752
|
`, "utf8");
|
|
2270
2753
|
return file;
|
|
2271
2754
|
}
|
|
@@ -2534,9 +3017,9 @@ function message(error) {
|
|
|
2534
3017
|
}
|
|
2535
3018
|
|
|
2536
3019
|
// src/commands/maintain.ts
|
|
2537
|
-
import { existsSync as
|
|
3020
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
2538
3021
|
import { spawnSync } from "node:child_process";
|
|
2539
|
-
import { dirname as dirname4, resolve as
|
|
3022
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
2540
3023
|
import { fileURLToPath } from "node:url";
|
|
2541
3024
|
async function updateCommand(context) {
|
|
2542
3025
|
const manager = installer();
|
|
@@ -2578,7 +3061,7 @@ Delete the file it runs from: ${processPath()}`
|
|
|
2578
3061
|
return 0;
|
|
2579
3062
|
}
|
|
2580
3063
|
async function deleteCommand(context) {
|
|
2581
|
-
if (!
|
|
3064
|
+
if (!existsSync6(context.paths.dir)) {
|
|
2582
3065
|
context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
|
|
2583
3066
|
return 0;
|
|
2584
3067
|
}
|
|
@@ -2607,7 +3090,7 @@ async function deleteCommand(context) {
|
|
|
2607
3090
|
return 0;
|
|
2608
3091
|
}
|
|
2609
3092
|
function removeEverything(context) {
|
|
2610
|
-
const dir =
|
|
3093
|
+
const dir = resolve7(context.paths.dir);
|
|
2611
3094
|
if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
|
|
2612
3095
|
context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
|
|
2613
3096
|
return;
|
|
@@ -2619,7 +3102,7 @@ function installer() {
|
|
|
2619
3102
|
}
|
|
2620
3103
|
function processPath() {
|
|
2621
3104
|
try {
|
|
2622
|
-
return
|
|
3105
|
+
return resolve7(dirname4(fileURLToPath(import.meta.url)));
|
|
2623
3106
|
} catch {
|
|
2624
3107
|
return process.argv[1] ?? "";
|
|
2625
3108
|
}
|
|
@@ -2631,12 +3114,12 @@ import { randomUUID } from "node:crypto";
|
|
|
2631
3114
|
import { relative as relative2 } from "node:path";
|
|
2632
3115
|
|
|
2633
3116
|
// src/events.ts
|
|
2634
|
-
import { appendFileSync, existsSync as
|
|
2635
|
-
import { join as
|
|
3117
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
3118
|
+
import { join as join6 } from "node:path";
|
|
2636
3119
|
function openSessionLog(paths, id) {
|
|
2637
|
-
const directory =
|
|
3120
|
+
const directory = join6(paths.dir, "sessions");
|
|
2638
3121
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
2639
|
-
const path =
|
|
3122
|
+
const path = join6(directory, `${id}.jsonl`);
|
|
2640
3123
|
return {
|
|
2641
3124
|
id,
|
|
2642
3125
|
path,
|
|
@@ -2648,8 +3131,8 @@ function openSessionLog(paths, id) {
|
|
|
2648
3131
|
}
|
|
2649
3132
|
},
|
|
2650
3133
|
read() {
|
|
2651
|
-
if (!
|
|
2652
|
-
return
|
|
3134
|
+
if (!existsSync7(path)) return [];
|
|
3135
|
+
return readFileSync7(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
2653
3136
|
try {
|
|
2654
3137
|
return [JSON.parse(line)];
|
|
2655
3138
|
} catch {
|
|
@@ -2726,9 +3209,9 @@ async function sessionCommand(context) {
|
|
|
2726
3209
|
context.print(` Ask anything. /help for commands, /exit to leave.
|
|
2727
3210
|
`);
|
|
2728
3211
|
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2729
|
-
const ask = (prompt) => new Promise((
|
|
2730
|
-
readline.question(prompt,
|
|
2731
|
-
readline.once("close", () =>
|
|
3212
|
+
const ask = (prompt) => new Promise((resolve8) => {
|
|
3213
|
+
readline.question(prompt, resolve8);
|
|
3214
|
+
readline.once("close", () => resolve8(void 0));
|
|
2732
3215
|
});
|
|
2733
3216
|
const root = process.cwd();
|
|
2734
3217
|
let running = true;
|
|
@@ -2912,7 +3395,7 @@ async function write2(args) {
|
|
|
2912
3395
|
}
|
|
2913
3396
|
|
|
2914
3397
|
// src/commands/memory.ts
|
|
2915
|
-
import { readFileSync as
|
|
3398
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2916
3399
|
|
|
2917
3400
|
// src/spaces.ts
|
|
2918
3401
|
async function spacesFor(context, client, env = process.env) {
|
|
@@ -2976,7 +3459,7 @@ async function rememberCommand(context) {
|
|
|
2976
3459
|
let text;
|
|
2977
3460
|
if (file) {
|
|
2978
3461
|
try {
|
|
2979
|
-
text =
|
|
3462
|
+
text = readFileSync8(file, "utf8");
|
|
2980
3463
|
} catch {
|
|
2981
3464
|
context.error(`Could not read ${file}.`);
|
|
2982
3465
|
return 1;
|
|
@@ -3284,6 +3767,18 @@ async function dispatch(context) {
|
|
|
3284
3767
|
return statusCommand(context);
|
|
3285
3768
|
case "requests":
|
|
3286
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);
|
|
3287
3782
|
/**
|
|
3288
3783
|
* `pm <verb> <noun>`, the grammar the Harness CLI uses.
|
|
3289
3784
|
*
|