@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/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);
|
|
@@ -558,6 +590,58 @@ var Spaces = class {
|
|
|
558
590
|
async create(params, options) {
|
|
559
591
|
return this.#http.post("/api/v1/spaces", params, options);
|
|
560
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Deletes a Space. You must say what happens to what is in it.
|
|
595
|
+
*
|
|
596
|
+
* There is no default, here or in the API, and that is deliberate: "delete
|
|
597
|
+
* this Space" means the label to some people and everything inside it to
|
|
598
|
+
* others, and a client that guessed would destroy or keep somebody's
|
|
599
|
+
* material without being asked.
|
|
600
|
+
*
|
|
601
|
+
* `delete` never destroys a memory that is filed in another Space as well —
|
|
602
|
+
* that one is detached and left alone. `deleted` and `kept` come back so you
|
|
603
|
+
* can say what actually happened.
|
|
604
|
+
*/
|
|
605
|
+
async delete(id, params, options) {
|
|
606
|
+
return this.#http.delete(
|
|
607
|
+
`/api/v1/spaces/${encodeURIComponent(id)}`,
|
|
608
|
+
params,
|
|
609
|
+
options
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Merges Spaces into a NEW one, leaving every source exactly as it was.
|
|
614
|
+
*
|
|
615
|
+
* Additive, not destructive: a memory ends up in the sources AND the result,
|
|
616
|
+
* every existing search over a source returns what it did before, and
|
|
617
|
+
* undoing it is deleting the Space this returns. A memory in two sources is
|
|
618
|
+
* filed once.
|
|
619
|
+
*/
|
|
620
|
+
async merge(params, options) {
|
|
621
|
+
return this.#http.post(
|
|
622
|
+
"/api/v1/spaces/merge",
|
|
623
|
+
params,
|
|
624
|
+
options
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* The Space this account files into when a capture names none.
|
|
629
|
+
*
|
|
630
|
+
* `{}` — an object with no `space` — means there is no default, which is the
|
|
631
|
+
* normal state rather than a gap. It is also what comes back after the Space
|
|
632
|
+
* somebody chose has been deleted.
|
|
633
|
+
*/
|
|
634
|
+
async getDefault(options) {
|
|
635
|
+
return this.#http.get("/api/v1/spaces/default", void 0, options);
|
|
636
|
+
}
|
|
637
|
+
/** `null` clears it. Not the same as omitting it, which is why the type says so. */
|
|
638
|
+
async setDefault(spaceId, options) {
|
|
639
|
+
return this.#http.patch(
|
|
640
|
+
"/api/v1/spaces/default",
|
|
641
|
+
{ spaceId },
|
|
642
|
+
options
|
|
643
|
+
);
|
|
644
|
+
}
|
|
561
645
|
/** Renaming, retention, and archiving - `archived` is a field, not a verb. */
|
|
562
646
|
async update(id, params, options) {
|
|
563
647
|
return this.#http.patch(`/api/v1/spaces/${encodeURIComponent(id)}`, params, options);
|
|
@@ -838,6 +922,119 @@ var Conversations = class {
|
|
|
838
922
|
);
|
|
839
923
|
}
|
|
840
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
|
+
};
|
|
841
1038
|
var Integrations = class {
|
|
842
1039
|
#http;
|
|
843
1040
|
constructor(http) {
|
|
@@ -920,6 +1117,42 @@ var Health = class {
|
|
|
920
1117
|
return this.#http.get("/health/ready", void 0, options);
|
|
921
1118
|
}
|
|
922
1119
|
};
|
|
1120
|
+
var Agent = class {
|
|
1121
|
+
#http;
|
|
1122
|
+
constructor(http) {
|
|
1123
|
+
this.#http = http;
|
|
1124
|
+
}
|
|
1125
|
+
/** The row, including whether it finished and how large the result is. */
|
|
1126
|
+
async request(id, options) {
|
|
1127
|
+
return this.#http.get(
|
|
1128
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}`,
|
|
1129
|
+
void 0,
|
|
1130
|
+
options
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* A short-lived link to the bytes of a finished request.
|
|
1135
|
+
*
|
|
1136
|
+
* Returns the URL rather than the file, and that is a deliberate limit of
|
|
1137
|
+
* this package rather than an oversight. The transport under every other
|
|
1138
|
+
* method parses JSON, retries, and attaches the API key; none of those is
|
|
1139
|
+
* right for a hundred-megabyte binary body, and building a second request
|
|
1140
|
+
* path inside the SDK to serve one method is how a client ends up with two
|
|
1141
|
+
* retry policies that differ only during an outage. Fetch the URL with
|
|
1142
|
+
* whatever already streams in your runtime - it needs no credential, which
|
|
1143
|
+
* is the whole reason it is signed.
|
|
1144
|
+
*
|
|
1145
|
+
* Treat the URL as the file. It is a bearer credential for exactly one
|
|
1146
|
+
* object, it expires in minutes, and it should not be logged or stored.
|
|
1147
|
+
*/
|
|
1148
|
+
async downloadLink(id, options) {
|
|
1149
|
+
return this.#http.get(
|
|
1150
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
1151
|
+
void 0,
|
|
1152
|
+
options
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
923
1156
|
var PersistMemory = class {
|
|
924
1157
|
memories;
|
|
925
1158
|
search;
|
|
@@ -932,7 +1165,10 @@ var PersistMemory = class {
|
|
|
932
1165
|
conflicts;
|
|
933
1166
|
conversations;
|
|
934
1167
|
integrations;
|
|
1168
|
+
/** Drive, mail and contacts on the user's connected Google account. */
|
|
1169
|
+
google;
|
|
935
1170
|
health;
|
|
1171
|
+
agent;
|
|
936
1172
|
#http;
|
|
937
1173
|
constructor(options) {
|
|
938
1174
|
this.#http = new HttpClient(options);
|
|
@@ -947,7 +1183,9 @@ var PersistMemory = class {
|
|
|
947
1183
|
this.conflicts = new Conflicts(this.#http);
|
|
948
1184
|
this.conversations = new Conversations(this.#http);
|
|
949
1185
|
this.integrations = new Integrations(this.#http);
|
|
1186
|
+
this.google = new Google(this.#http);
|
|
950
1187
|
this.health = new Health(this.#http);
|
|
1188
|
+
this.agent = new Agent(this.#http);
|
|
951
1189
|
}
|
|
952
1190
|
/**
|
|
953
1191
|
* An escape hatch for an endpoint this package has not caught up with.
|
|
@@ -1289,7 +1527,7 @@ function shortDate(iso) {
|
|
|
1289
1527
|
}
|
|
1290
1528
|
|
|
1291
1529
|
// src/help.ts
|
|
1292
|
-
var VERSION = "0.1.
|
|
1530
|
+
var VERSION = "0.1.2";
|
|
1293
1531
|
var PACKAGE = "@persistmemory/cli";
|
|
1294
1532
|
var HELP = `
|
|
1295
1533
|
pm \u2014 PersistMemory from your terminal
|
|
@@ -1334,8 +1572,15 @@ var HELP = `
|
|
|
1334
1572
|
list spaces your Spaces
|
|
1335
1573
|
get memory <id> one memory, in full
|
|
1336
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
|
+
|
|
1337
1581
|
status is the service healthy
|
|
1338
1582
|
requests file requests waiting for you to approve
|
|
1583
|
+
requests get <id> write a finished one to a file here
|
|
1339
1584
|
|
|
1340
1585
|
update install the newest version
|
|
1341
1586
|
uninstall remove pm from this machine
|
|
@@ -1479,8 +1724,8 @@ async function startLoopback(options = {}) {
|
|
|
1479
1724
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
1480
1725
|
let resolveCallback;
|
|
1481
1726
|
let rejectCallback;
|
|
1482
|
-
const received = new Promise((
|
|
1483
|
-
resolveCallback =
|
|
1727
|
+
const received = new Promise((resolve8, reject) => {
|
|
1728
|
+
resolveCallback = resolve8;
|
|
1484
1729
|
rejectCallback = reject;
|
|
1485
1730
|
});
|
|
1486
1731
|
const server = createServer((request, response) => {
|
|
@@ -1506,9 +1751,9 @@ async function startLoopback(options = {}) {
|
|
|
1506
1751
|
response.end(donePage(callback));
|
|
1507
1752
|
resolveCallback?.(callback);
|
|
1508
1753
|
});
|
|
1509
|
-
await new Promise((
|
|
1754
|
+
await new Promise((resolve8, reject) => {
|
|
1510
1755
|
server.once("error", reject);
|
|
1511
|
-
server.listen(0, "127.0.0.1",
|
|
1756
|
+
server.listen(0, "127.0.0.1", resolve8);
|
|
1512
1757
|
});
|
|
1513
1758
|
const address = server.address();
|
|
1514
1759
|
if (address === null || typeof address === "string") {
|
|
@@ -1770,7 +2015,7 @@ function safeEqual(a, b) {
|
|
|
1770
2015
|
}
|
|
1771
2016
|
async function openBrowser(url) {
|
|
1772
2017
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1773
|
-
await new Promise((
|
|
2018
|
+
await new Promise((resolve8, reject) => {
|
|
1774
2019
|
const child = spawn(command, args, {
|
|
1775
2020
|
stdio: "ignore",
|
|
1776
2021
|
// Detached so closing the terminal does not close the browser, and so
|
|
@@ -1779,7 +2024,7 @@ async function openBrowser(url) {
|
|
|
1779
2024
|
});
|
|
1780
2025
|
child.once("error", reject);
|
|
1781
2026
|
child.unref();
|
|
1782
|
-
|
|
2027
|
+
resolve8();
|
|
1783
2028
|
});
|
|
1784
2029
|
}
|
|
1785
2030
|
async function describe(response) {
|
|
@@ -1841,13 +2086,13 @@ async function currentCredential(resolved, deps) {
|
|
|
1841
2086
|
// src/context.ts
|
|
1842
2087
|
import { createInterface } from "node:readline";
|
|
1843
2088
|
async function askOnTty(prompt) {
|
|
1844
|
-
return new Promise((
|
|
2089
|
+
return new Promise((resolve8) => {
|
|
1845
2090
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1846
2091
|
readline.question(prompt, (answer3) => {
|
|
1847
2092
|
readline.close();
|
|
1848
|
-
|
|
2093
|
+
resolve8(answer3.trim());
|
|
1849
2094
|
});
|
|
1850
|
-
readline.once("close", () =>
|
|
2095
|
+
readline.once("close", () => resolve8(""));
|
|
1851
2096
|
});
|
|
1852
2097
|
}
|
|
1853
2098
|
var ETX = "";
|
|
@@ -1856,13 +2101,13 @@ var BACKSPACE = "\b";
|
|
|
1856
2101
|
async function readSecretFromTty(prompt) {
|
|
1857
2102
|
const input = process.stdin;
|
|
1858
2103
|
if (!input.isTTY) {
|
|
1859
|
-
return new Promise((
|
|
2104
|
+
return new Promise((resolve8) => {
|
|
1860
2105
|
const readline = createInterface({ input });
|
|
1861
2106
|
readline.once("line", (line) => {
|
|
1862
2107
|
readline.close();
|
|
1863
|
-
|
|
2108
|
+
resolve8(line.trim());
|
|
1864
2109
|
});
|
|
1865
|
-
readline.once("close", () =>
|
|
2110
|
+
readline.once("close", () => resolve8(""));
|
|
1866
2111
|
});
|
|
1867
2112
|
}
|
|
1868
2113
|
process.stdout.write(prompt);
|
|
@@ -1870,14 +2115,14 @@ async function readSecretFromTty(prompt) {
|
|
|
1870
2115
|
input.setRawMode?.(true);
|
|
1871
2116
|
input.resume();
|
|
1872
2117
|
input.setEncoding("utf8");
|
|
1873
|
-
return new Promise((
|
|
2118
|
+
return new Promise((resolve8) => {
|
|
1874
2119
|
let value = "";
|
|
1875
2120
|
const finish2 = () => {
|
|
1876
2121
|
input.removeListener("data", onData);
|
|
1877
2122
|
input.setRawMode?.(previouslyRaw);
|
|
1878
2123
|
input.pause();
|
|
1879
2124
|
process.stdout.write("\n");
|
|
1880
|
-
|
|
2125
|
+
resolve8(value.trim());
|
|
1881
2126
|
};
|
|
1882
2127
|
const onData = (chunk) => {
|
|
1883
2128
|
for (const character of chunk) {
|
|
@@ -1914,8 +2159,8 @@ async function readStdin() {
|
|
|
1914
2159
|
// src/commands/agent.ts
|
|
1915
2160
|
import { hostname } from "node:os";
|
|
1916
2161
|
import { homedir as homedir2 } from "node:os";
|
|
1917
|
-
import { resolve as resolve3 } from "node:path";
|
|
1918
|
-
import { readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
2162
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
2163
|
+
import { readFileSync as readFileSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
1919
2164
|
|
|
1920
2165
|
// src/files.ts
|
|
1921
2166
|
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -1928,11 +2173,13 @@ var OutsideWorkspace = class extends Error {
|
|
|
1928
2173
|
};
|
|
1929
2174
|
var TooLarge = class extends Error {
|
|
1930
2175
|
constructor(path, bytes, limit) {
|
|
1931
|
-
|
|
2176
|
+
const say = (value) => value >= 1024 * 1024 ? `${(value / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(value / 1024)} KB`;
|
|
2177
|
+
super(`${path} is ${say(bytes)}, over the ${say(limit)} limit.`);
|
|
1932
2178
|
this.name = "TooLarge";
|
|
1933
2179
|
}
|
|
1934
2180
|
};
|
|
1935
2181
|
var MAX_READ_BYTES = 512 * 1024;
|
|
2182
|
+
var MAX_TRANSFER_BYTES = 100 * 1024 * 1024;
|
|
1936
2183
|
function realLocation(absolute) {
|
|
1937
2184
|
let existing = absolute;
|
|
1938
2185
|
const trailing = [];
|
|
@@ -2046,19 +2293,41 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2046
2293
|
return { ok: false, error: error instanceof Error ? error.message : "refused" };
|
|
2047
2294
|
}
|
|
2048
2295
|
let bytes;
|
|
2296
|
+
let filename = request.path.split("/").pop() ?? "file";
|
|
2297
|
+
if (request.kind === "list_dir") {
|
|
2298
|
+
try {
|
|
2299
|
+
const stats = statSync2(located);
|
|
2300
|
+
if (!stats.isDirectory()) return { ok: false, error: `${request.path} is not a folder.` };
|
|
2301
|
+
const entries = readdirSync(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_LISTED).map((entry) => {
|
|
2302
|
+
if (entry.isDirectory()) return `${entry.name}/`;
|
|
2303
|
+
try {
|
|
2304
|
+
return `${entry.name} ${sizeOf(join4(located, entry.name))}`;
|
|
2305
|
+
} catch {
|
|
2306
|
+
return entry.name;
|
|
2307
|
+
}
|
|
2308
|
+
}).sort();
|
|
2309
|
+
const listing = entries.length > 0 ? entries.join("\n") : "(empty)";
|
|
2310
|
+
bytes = Buffer.from(`${request.path}
|
|
2311
|
+
|
|
2312
|
+
${listing}
|
|
2313
|
+
`, "utf8");
|
|
2314
|
+
filename = `${request.path.split("/").filter(Boolean).pop() ?? "listing"}.txt`;
|
|
2315
|
+
const grant = await upload(apiUrl, token, filename, bytes);
|
|
2316
|
+
return grant;
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
return { ok: false, error: error instanceof Error ? error.message : "could not list it" };
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2049
2321
|
try {
|
|
2050
2322
|
const stats = statSync2(located);
|
|
2051
2323
|
if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
|
|
2052
|
-
if (stats.size > MAX_READ_BYTES) {
|
|
2053
|
-
return {
|
|
2054
|
-
ok: false,
|
|
2055
|
-
error: new TooLarge(request.path, stats.size, MAX_READ_BYTES).message
|
|
2056
|
-
};
|
|
2057
|
-
}
|
|
2058
2324
|
bytes = readFileSync4(located);
|
|
2059
2325
|
} catch (error) {
|
|
2060
2326
|
return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
|
|
2061
2327
|
}
|
|
2328
|
+
return upload(apiUrl, token, filename, bytes);
|
|
2329
|
+
}
|
|
2330
|
+
async function upload(apiUrl, token, filename, bytes) {
|
|
2062
2331
|
const grant = await fetch(`${apiUrl}/api/v1/agent/upload-url`, {
|
|
2063
2332
|
method: "POST",
|
|
2064
2333
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -2070,13 +2339,17 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2070
2339
|
// No content type is sent: this machine has a path, not a declaration.
|
|
2071
2340
|
// The server resolves it from the name against the one table that knows
|
|
2072
2341
|
// which types it can read, and tells us below what it decided.
|
|
2073
|
-
filename
|
|
2342
|
+
filename
|
|
2074
2343
|
})
|
|
2075
2344
|
});
|
|
2076
2345
|
if (!grant.ok) {
|
|
2077
2346
|
return { ok: false, error: await said(grant, "could not get an upload url") };
|
|
2078
2347
|
}
|
|
2079
|
-
const { uploadUrl, contentType } = await grant.json();
|
|
2348
|
+
const { uploadUrl, contentType, maxBytes } = await grant.json();
|
|
2349
|
+
const limit = maxBytes ?? MAX_TRANSFER_BYTES;
|
|
2350
|
+
if (bytes.length > limit) {
|
|
2351
|
+
return { ok: false, error: new TooLarge(filename, bytes.length, limit).message };
|
|
2352
|
+
}
|
|
2080
2353
|
const put = await fetch(uploadUrl, {
|
|
2081
2354
|
method: "PUT",
|
|
2082
2355
|
// The type the grant was signed for. Anything else is refused.
|
|
@@ -2088,6 +2361,13 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
2088
2361
|
if (!stored.attachToken) return { ok: false, error: "the upload returned no reference" };
|
|
2089
2362
|
return { ok: true, attachToken: stored.attachToken, bytes: bytes.length };
|
|
2090
2363
|
}
|
|
2364
|
+
var MAX_LISTED = 200;
|
|
2365
|
+
function sizeOf(path) {
|
|
2366
|
+
const size = statSync2(path).size;
|
|
2367
|
+
if (size < 1024) return `${size} B`;
|
|
2368
|
+
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
|
2369
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
2370
|
+
}
|
|
2091
2371
|
async function agentCommand(context) {
|
|
2092
2372
|
const credential = context.resolved.credential;
|
|
2093
2373
|
if (!credential) {
|
|
@@ -2141,7 +2421,7 @@ async function agentCommand(context) {
|
|
|
2141
2421
|
body: JSON.stringify(body)
|
|
2142
2422
|
});
|
|
2143
2423
|
let complaint;
|
|
2144
|
-
const
|
|
2424
|
+
const complain2 = (message2) => {
|
|
2145
2425
|
if (complaint === message2) return;
|
|
2146
2426
|
complaint = message2;
|
|
2147
2427
|
context.error(` ${message2}`);
|
|
@@ -2156,7 +2436,7 @@ async function agentCommand(context) {
|
|
|
2156
2436
|
try {
|
|
2157
2437
|
const beat = await call("heartbeat", { hostname: name, platform: process.platform });
|
|
2158
2438
|
if (!beat.ok) {
|
|
2159
|
-
|
|
2439
|
+
complain2(await said(beat, "the service refused this machine"));
|
|
2160
2440
|
} else {
|
|
2161
2441
|
working();
|
|
2162
2442
|
const state = await beat.json();
|
|
@@ -2171,7 +2451,7 @@ async function agentCommand(context) {
|
|
|
2171
2451
|
}
|
|
2172
2452
|
const claimed = await call("claim", { hostname: name, limit: 5 });
|
|
2173
2453
|
if (!claimed.ok) {
|
|
2174
|
-
|
|
2454
|
+
complain2(await said(claimed, "could not pick up work"));
|
|
2175
2455
|
} else {
|
|
2176
2456
|
const { items } = await claimed.json();
|
|
2177
2457
|
for (const request of items) {
|
|
@@ -2196,8 +2476,174 @@ async function agentCommand(context) {
|
|
|
2196
2476
|
return 0;
|
|
2197
2477
|
}
|
|
2198
2478
|
|
|
2479
|
+
// src/commands/google.ts
|
|
2480
|
+
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
2481
|
+
import { basename, resolve as resolve4 } from "node:path";
|
|
2482
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2483
|
+
async function callApi(context, path, init = {}) {
|
|
2484
|
+
const credential = context.resolved.credential;
|
|
2485
|
+
if (!credential) {
|
|
2486
|
+
context.error("Sign in first: pm auth login");
|
|
2487
|
+
return void 0;
|
|
2488
|
+
}
|
|
2489
|
+
const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
|
|
2490
|
+
return fetch(`${apiUrl}${path}`, {
|
|
2491
|
+
...init,
|
|
2492
|
+
headers: {
|
|
2493
|
+
authorization: `Bearer ${credential.token}`,
|
|
2494
|
+
...init.headers ?? {}
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
async function complain(context, response) {
|
|
2499
|
+
const body = await response.json().catch(() => void 0);
|
|
2500
|
+
context.error(body?.error?.message ?? `That failed (${response.status}).`);
|
|
2501
|
+
return response.status === 409 ? 3 : 1;
|
|
2502
|
+
}
|
|
2503
|
+
async function driveCommand(context) {
|
|
2504
|
+
const [, noun, ...rest] = context.args.words;
|
|
2505
|
+
if (noun === "get") return driveGet(context, rest.join(" ").trim());
|
|
2506
|
+
if (noun === "put" || noun === "save") return drivePut(context, rest.join(" ").trim());
|
|
2507
|
+
const query = [noun, ...rest].filter(Boolean).join(" ").trim();
|
|
2508
|
+
const response = await callApi(
|
|
2509
|
+
context,
|
|
2510
|
+
`/api/v1/google/drive/files?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
|
|
2511
|
+
);
|
|
2512
|
+
if (!response) return 1;
|
|
2513
|
+
if (!response.ok) return complain(context, response);
|
|
2514
|
+
const { data } = await response.json();
|
|
2515
|
+
if (data.length === 0) {
|
|
2516
|
+
context.print(query ? `Nothing in Drive matches "${query}".` : "That Drive is empty.");
|
|
2517
|
+
return 0;
|
|
2518
|
+
}
|
|
2519
|
+
if (context.flags.output === "json") {
|
|
2520
|
+
context.print(JSON.stringify(data, void 0, 2));
|
|
2521
|
+
return 0;
|
|
2522
|
+
}
|
|
2523
|
+
for (const file of data) {
|
|
2524
|
+
context.print(` ${file.name}`);
|
|
2525
|
+
context.print(
|
|
2526
|
+
` ${file.id}${file.size ? ` \xB7 ${Math.round(file.size / 1024)} KB` : ""}${file.modifiedTime ? ` \xB7 ${file.modifiedTime.slice(0, 10)}` : ""}`
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
context.print("");
|
|
2530
|
+
context.print("Fetch one with: pm drive get <id>");
|
|
2531
|
+
return 0;
|
|
2532
|
+
}
|
|
2533
|
+
async function driveGet(context, fileId) {
|
|
2534
|
+
if (!fileId) {
|
|
2535
|
+
context.error("Say which file: pm drive get <id>");
|
|
2536
|
+
return 2;
|
|
2537
|
+
}
|
|
2538
|
+
const response = await callApi(
|
|
2539
|
+
context,
|
|
2540
|
+
`/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`
|
|
2541
|
+
);
|
|
2542
|
+
if (!response) return 1;
|
|
2543
|
+
if (!response.ok) return complain(context, response);
|
|
2544
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
2545
|
+
const named = /filename="([^"]+)"/.exec(disposition)?.[1];
|
|
2546
|
+
const out = stringFlag(context.args, "out");
|
|
2547
|
+
const target = resolve4(out ?? basename(named ?? fileId));
|
|
2548
|
+
writeFileSync4(target, Buffer.from(await response.arrayBuffer()));
|
|
2549
|
+
context.print(target);
|
|
2550
|
+
return 0;
|
|
2551
|
+
}
|
|
2552
|
+
async function drivePut(context, path) {
|
|
2553
|
+
if (!path) {
|
|
2554
|
+
context.error("Say which file: pm drive put ./notes.md");
|
|
2555
|
+
return 2;
|
|
2556
|
+
}
|
|
2557
|
+
let bytes;
|
|
2558
|
+
try {
|
|
2559
|
+
bytes = readFileSync5(resolve4(path));
|
|
2560
|
+
} catch {
|
|
2561
|
+
context.error(`Cannot read ${path}.`);
|
|
2562
|
+
return 1;
|
|
2563
|
+
}
|
|
2564
|
+
const name = stringFlag(context.args, "name") ?? basename(path);
|
|
2565
|
+
const response = await callApi(
|
|
2566
|
+
context,
|
|
2567
|
+
`/api/v1/google/drive/files?name=${encodeURIComponent(name)}`,
|
|
2568
|
+
{
|
|
2569
|
+
method: "POST",
|
|
2570
|
+
// The bytes as bytes. Base64 in JSON would be a third larger and would
|
|
2571
|
+
// make the limit somebody was told about stop matching the one they hit.
|
|
2572
|
+
headers: { "content-type": "application/octet-stream" },
|
|
2573
|
+
body: new Uint8Array(bytes)
|
|
2574
|
+
}
|
|
2575
|
+
);
|
|
2576
|
+
if (!response) return 1;
|
|
2577
|
+
if (!response.ok) return complain(context, response);
|
|
2578
|
+
const saved = await response.json();
|
|
2579
|
+
context.print(`Saved "${saved.name}" to Drive.${saved.link ? ` ${saved.link}` : ""}`);
|
|
2580
|
+
return 0;
|
|
2581
|
+
}
|
|
2582
|
+
async function mailCommand(context) {
|
|
2583
|
+
const [, noun, ...rest] = context.args.words;
|
|
2584
|
+
if (noun === "read" || noun === "get") {
|
|
2585
|
+
const messageId = rest.join(" ").trim();
|
|
2586
|
+
if (!messageId) {
|
|
2587
|
+
context.error("Say which message: pm mail read <id>");
|
|
2588
|
+
return 2;
|
|
2589
|
+
}
|
|
2590
|
+
const response2 = await callApi(
|
|
2591
|
+
context,
|
|
2592
|
+
`/api/v1/google/mail/${encodeURIComponent(messageId)}`
|
|
2593
|
+
);
|
|
2594
|
+
if (!response2) return 1;
|
|
2595
|
+
if (!response2.ok) return complain(context, response2);
|
|
2596
|
+
const message2 = await response2.json();
|
|
2597
|
+
if (context.flags.output === "json") {
|
|
2598
|
+
context.print(JSON.stringify(message2, void 0, 2));
|
|
2599
|
+
return 0;
|
|
2600
|
+
}
|
|
2601
|
+
context.print(`From: ${message2.from ?? "unknown"}`);
|
|
2602
|
+
context.print(`Subject: ${message2.subject ?? "(none)"}`);
|
|
2603
|
+
if (message2.date) context.print(`Date: ${message2.date}`);
|
|
2604
|
+
context.print("");
|
|
2605
|
+
context.print(message2.body);
|
|
2606
|
+
if (message2.attachments.length > 0) {
|
|
2607
|
+
context.print("");
|
|
2608
|
+
context.print("Attached:");
|
|
2609
|
+
for (const one of message2.attachments) context.print(` ${one.filename} (${one.mimeType})`);
|
|
2610
|
+
}
|
|
2611
|
+
return 0;
|
|
2612
|
+
}
|
|
2613
|
+
const query = [noun, ...rest].filter(Boolean).join(" ").trim();
|
|
2614
|
+
const response = await callApi(
|
|
2615
|
+
context,
|
|
2616
|
+
`/api/v1/google/mail?limit=20${query ? `&query=${encodeURIComponent(query)}` : ""}`
|
|
2617
|
+
);
|
|
2618
|
+
if (!response) return 1;
|
|
2619
|
+
if (!response.ok) return complain(context, response);
|
|
2620
|
+
const { data } = await response.json();
|
|
2621
|
+
if (data.length === 0) {
|
|
2622
|
+
context.print(query ? `No mail matches "${query}".` : "Nothing in that mailbox.");
|
|
2623
|
+
return 0;
|
|
2624
|
+
}
|
|
2625
|
+
if (context.flags.output === "json") {
|
|
2626
|
+
context.print(JSON.stringify(data, void 0, 2));
|
|
2627
|
+
return 0;
|
|
2628
|
+
}
|
|
2629
|
+
for (const message2 of data) {
|
|
2630
|
+
const marks = [message2.unread ? "unread" : "", message2.hasAttachments ? "attachment" : ""].filter(Boolean).join(", ");
|
|
2631
|
+
context.print(` ${message2.subject ?? "(no subject)"}${marks ? ` [${marks}]` : ""}`);
|
|
2632
|
+
context.print(
|
|
2633
|
+
` ${message2.from ?? "unknown"}${message2.date ? ` \xB7 ${message2.date.slice(0, 10)}` : ""}`
|
|
2634
|
+
);
|
|
2635
|
+
context.print(` ${message2.id}`);
|
|
2636
|
+
}
|
|
2637
|
+
context.print("");
|
|
2638
|
+
context.print("Read one with: pm mail read <id>");
|
|
2639
|
+
return 0;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2199
2642
|
// src/commands/requests.ts
|
|
2643
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2644
|
+
import { resolve as resolve5 } from "node:path";
|
|
2200
2645
|
async function requestsCommand(context) {
|
|
2646
|
+
if (context.args.words[1] === "get") return collectCommand(context);
|
|
2201
2647
|
const credential = context.resolved.credential;
|
|
2202
2648
|
if (!credential) {
|
|
2203
2649
|
context.error("Sign in first: pm auth login");
|
|
@@ -2235,16 +2681,53 @@ async function requestsCommand(context) {
|
|
|
2235
2681
|
context.print("They cannot be approved from here \u2014 see `pm help requests`.");
|
|
2236
2682
|
return 0;
|
|
2237
2683
|
}
|
|
2684
|
+
async function collectCommand(context) {
|
|
2685
|
+
const credential = context.resolved.credential;
|
|
2686
|
+
if (!credential) {
|
|
2687
|
+
context.error("Sign in first: pm auth login");
|
|
2688
|
+
return 1;
|
|
2689
|
+
}
|
|
2690
|
+
const id = context.args.words[2];
|
|
2691
|
+
if (!id) {
|
|
2692
|
+
context.error("Which request? `pm requests` lists them with their ids.");
|
|
2693
|
+
return 2;
|
|
2694
|
+
}
|
|
2695
|
+
const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
|
|
2696
|
+
const link = await fetch(
|
|
2697
|
+
`${apiUrl}/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
2698
|
+
{ headers: { authorization: `Bearer ${credential.token}` } }
|
|
2699
|
+
);
|
|
2700
|
+
if (!link.ok) {
|
|
2701
|
+
const body = await link.json().catch(() => void 0);
|
|
2702
|
+
context.error(body?.error?.message ?? `Could not prepare that file (${link.status}).`);
|
|
2703
|
+
return 1;
|
|
2704
|
+
}
|
|
2705
|
+
const { downloadUrl, filename } = await link.json();
|
|
2706
|
+
const file = await fetch(downloadUrl);
|
|
2707
|
+
if (!file.ok) {
|
|
2708
|
+
context.error(`The download refused it (${file.status}). Links expire in minutes \u2014 try again.`);
|
|
2709
|
+
return 1;
|
|
2710
|
+
}
|
|
2711
|
+
const name = stringFlag(context.args, "output", "o") ?? filename;
|
|
2712
|
+
const target = resolve5(name);
|
|
2713
|
+
if (existsSync4(target)) {
|
|
2714
|
+
context.error(`${target} already exists. Pass --output to write somewhere else.`);
|
|
2715
|
+
return 1;
|
|
2716
|
+
}
|
|
2717
|
+
writeFileSync5(target, new Uint8Array(await file.arrayBuffer()));
|
|
2718
|
+
context.print(`Wrote ${target}`);
|
|
2719
|
+
return 0;
|
|
2720
|
+
}
|
|
2238
2721
|
|
|
2239
2722
|
// src/workspace.ts
|
|
2240
|
-
import { existsSync as
|
|
2241
|
-
import { dirname as dirname3, join as
|
|
2723
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2724
|
+
import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
|
|
2242
2725
|
var WORKSPACE_FILE = ".persistmemory.json";
|
|
2243
2726
|
function findWorkspace(from = process.cwd()) {
|
|
2244
2727
|
let dir = resolvePath(from);
|
|
2245
2728
|
for (; ; ) {
|
|
2246
|
-
const file =
|
|
2247
|
-
if (
|
|
2729
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2730
|
+
if (existsSync5(file)) {
|
|
2248
2731
|
const config = readWorkspace(file);
|
|
2249
2732
|
if (config) return { file, dir, config };
|
|
2250
2733
|
}
|
|
@@ -2255,7 +2738,7 @@ function findWorkspace(from = process.cwd()) {
|
|
|
2255
2738
|
}
|
|
2256
2739
|
function readWorkspace(file) {
|
|
2257
2740
|
try {
|
|
2258
|
-
const parsed = JSON.parse(
|
|
2741
|
+
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
2259
2742
|
const space = parsed.space;
|
|
2260
2743
|
if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
|
|
2261
2744
|
return { space: { id: space.id, name: space.name } };
|
|
@@ -2266,8 +2749,8 @@ function readWorkspace(file) {
|
|
|
2266
2749
|
}
|
|
2267
2750
|
}
|
|
2268
2751
|
function writeWorkspace(dir, config) {
|
|
2269
|
-
const file =
|
|
2270
|
-
|
|
2752
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2753
|
+
writeFileSync6(file, `${JSON.stringify(config, null, 2)}
|
|
2271
2754
|
`, "utf8");
|
|
2272
2755
|
return file;
|
|
2273
2756
|
}
|
|
@@ -2536,9 +3019,9 @@ function message(error) {
|
|
|
2536
3019
|
}
|
|
2537
3020
|
|
|
2538
3021
|
// src/commands/maintain.ts
|
|
2539
|
-
import { existsSync as
|
|
3022
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
2540
3023
|
import { spawnSync } from "node:child_process";
|
|
2541
|
-
import { dirname as dirname4, resolve as
|
|
3024
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
2542
3025
|
import { fileURLToPath } from "node:url";
|
|
2543
3026
|
async function updateCommand(context) {
|
|
2544
3027
|
const manager = installer();
|
|
@@ -2580,7 +3063,7 @@ Delete the file it runs from: ${processPath()}`
|
|
|
2580
3063
|
return 0;
|
|
2581
3064
|
}
|
|
2582
3065
|
async function deleteCommand(context) {
|
|
2583
|
-
if (!
|
|
3066
|
+
if (!existsSync6(context.paths.dir)) {
|
|
2584
3067
|
context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
|
|
2585
3068
|
return 0;
|
|
2586
3069
|
}
|
|
@@ -2609,7 +3092,7 @@ async function deleteCommand(context) {
|
|
|
2609
3092
|
return 0;
|
|
2610
3093
|
}
|
|
2611
3094
|
function removeEverything(context) {
|
|
2612
|
-
const dir =
|
|
3095
|
+
const dir = resolve7(context.paths.dir);
|
|
2613
3096
|
if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
|
|
2614
3097
|
context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
|
|
2615
3098
|
return;
|
|
@@ -2621,7 +3104,7 @@ function installer() {
|
|
|
2621
3104
|
}
|
|
2622
3105
|
function processPath() {
|
|
2623
3106
|
try {
|
|
2624
|
-
return
|
|
3107
|
+
return resolve7(dirname4(fileURLToPath(import.meta.url)));
|
|
2625
3108
|
} catch {
|
|
2626
3109
|
return process.argv[1] ?? "";
|
|
2627
3110
|
}
|
|
@@ -2633,12 +3116,12 @@ import { randomUUID } from "node:crypto";
|
|
|
2633
3116
|
import { relative as relative2 } from "node:path";
|
|
2634
3117
|
|
|
2635
3118
|
// src/events.ts
|
|
2636
|
-
import { appendFileSync, existsSync as
|
|
2637
|
-
import { join as
|
|
3119
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
3120
|
+
import { join as join6 } from "node:path";
|
|
2638
3121
|
function openSessionLog(paths, id) {
|
|
2639
|
-
const directory =
|
|
3122
|
+
const directory = join6(paths.dir, "sessions");
|
|
2640
3123
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
2641
|
-
const path =
|
|
3124
|
+
const path = join6(directory, `${id}.jsonl`);
|
|
2642
3125
|
return {
|
|
2643
3126
|
id,
|
|
2644
3127
|
path,
|
|
@@ -2650,8 +3133,8 @@ function openSessionLog(paths, id) {
|
|
|
2650
3133
|
}
|
|
2651
3134
|
},
|
|
2652
3135
|
read() {
|
|
2653
|
-
if (!
|
|
2654
|
-
return
|
|
3136
|
+
if (!existsSync7(path)) return [];
|
|
3137
|
+
return readFileSync7(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
2655
3138
|
try {
|
|
2656
3139
|
return [JSON.parse(line)];
|
|
2657
3140
|
} catch {
|
|
@@ -2728,9 +3211,9 @@ async function sessionCommand(context) {
|
|
|
2728
3211
|
context.print(` Ask anything. /help for commands, /exit to leave.
|
|
2729
3212
|
`);
|
|
2730
3213
|
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2731
|
-
const ask = (prompt) => new Promise((
|
|
2732
|
-
readline.question(prompt,
|
|
2733
|
-
readline.once("close", () =>
|
|
3214
|
+
const ask = (prompt) => new Promise((resolve8) => {
|
|
3215
|
+
readline.question(prompt, resolve8);
|
|
3216
|
+
readline.once("close", () => resolve8(void 0));
|
|
2734
3217
|
});
|
|
2735
3218
|
const root = process.cwd();
|
|
2736
3219
|
let running = true;
|
|
@@ -2914,7 +3397,7 @@ async function write2(args) {
|
|
|
2914
3397
|
}
|
|
2915
3398
|
|
|
2916
3399
|
// src/commands/memory.ts
|
|
2917
|
-
import { readFileSync as
|
|
3400
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2918
3401
|
|
|
2919
3402
|
// src/spaces.ts
|
|
2920
3403
|
async function spacesFor(context, client, env = process.env) {
|
|
@@ -2978,7 +3461,7 @@ async function rememberCommand(context) {
|
|
|
2978
3461
|
let text;
|
|
2979
3462
|
if (file) {
|
|
2980
3463
|
try {
|
|
2981
|
-
text =
|
|
3464
|
+
text = readFileSync8(file, "utf8");
|
|
2982
3465
|
} catch {
|
|
2983
3466
|
context.error(`Could not read ${file}.`);
|
|
2984
3467
|
return 1;
|
|
@@ -3286,6 +3769,18 @@ async function dispatch(context) {
|
|
|
3286
3769
|
return statusCommand(context);
|
|
3287
3770
|
case "requests":
|
|
3288
3771
|
return requestsCommand(context);
|
|
3772
|
+
/*
|
|
3773
|
+
Google, through the API rather than through Google.
|
|
3774
|
+
|
|
3775
|
+
A laptop holds a bearer token and no database connection, so the
|
|
3776
|
+
credential it can prove is the one the API accepts. Google's own tokens
|
|
3777
|
+
never leave the deployment — which is what stops a stolen
|
|
3778
|
+
`~/.persistmemory` from being a stolen mailbox.
|
|
3779
|
+
*/
|
|
3780
|
+
case "drive":
|
|
3781
|
+
return driveCommand(context);
|
|
3782
|
+
case "mail":
|
|
3783
|
+
return mailCommand(context);
|
|
3289
3784
|
/**
|
|
3290
3785
|
* `pm <verb> <noun>`, the grammar the Harness CLI uses.
|
|
3291
3786
|
*
|