@persistmemory/cli 0.5.0 → 0.7.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 +943 -285
- package/dist/bin.js.map +4 -4
- package/dist/index.js +943 -285
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ../sdk-js/dist/index.js
|
|
2
2
|
function encodeQuery(params) {
|
|
3
3
|
if (!params) return "";
|
|
4
4
|
const search = new URLSearchParams();
|
|
@@ -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);
|
|
@@ -615,17 +647,22 @@ var Spaces = class {
|
|
|
615
647
|
/**
|
|
616
648
|
* The memories filed in a Space.
|
|
617
649
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
650
|
+
* The cursor is PASSED. This fetch used to ignore the paginator's cursor
|
|
651
|
+
* on the stale belief that the endpoint had none — the server has minted
|
|
652
|
+
* `pagination.nextCursor` since it started paging, and its own comment
|
|
653
|
+
* says "both SDKs iterate by reading pagination". Ignoring it meant every
|
|
654
|
+
* page request was identical: the loop guard saw a non-advancing fetch and
|
|
655
|
+
* stopped silently, so `all()` returned the first page twice and dropped
|
|
656
|
+
* everything after it — duplicated AND truncated data, with no error.
|
|
623
657
|
*/
|
|
624
658
|
memories(id, params = {}, options) {
|
|
625
659
|
return new Paginated(
|
|
626
|
-
() => this.#http.get(
|
|
660
|
+
(cursor) => this.#http.get(
|
|
627
661
|
`/api/v1/spaces/${encodeURIComponent(id)}/memories`,
|
|
628
|
-
{
|
|
662
|
+
{
|
|
663
|
+
...params.limit !== void 0 ? { limit: params.limit } : {},
|
|
664
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
665
|
+
},
|
|
629
666
|
options
|
|
630
667
|
)
|
|
631
668
|
);
|
|
@@ -651,6 +688,94 @@ var Spaces = class {
|
|
|
651
688
|
options
|
|
652
689
|
);
|
|
653
690
|
}
|
|
691
|
+
/* ----------------------- who else can see it ----------------------- */
|
|
692
|
+
/**
|
|
693
|
+
* Who can see this Space, including invitations nobody has accepted.
|
|
694
|
+
*
|
|
695
|
+
* A DIFFERENT EDGE from `memories()` next door, and the difference is worth
|
|
696
|
+
* holding on to: that one maps a MEMORY to a Space, this one maps a PERSON
|
|
697
|
+
* to a Space. The server keeps them in two tables with two names for exactly
|
|
698
|
+
* that reason.
|
|
699
|
+
*
|
|
700
|
+
* Read `acceptedAt` before you render a row. An invitation grants nothing
|
|
701
|
+
* until it is accepted, so a list that draws invited and accepted people the
|
|
702
|
+
* same way tells its user somebody is reading their memories when nobody is.
|
|
703
|
+
*
|
|
704
|
+
* Paginated like every other list here. A Space has a handful of
|
|
705
|
+
* collaborators rather than thousands, so this will usually be one page -
|
|
706
|
+
* which costs a caller nothing and means the shape does not change if a
|
|
707
|
+
* Space ever has an organisation on it.
|
|
708
|
+
*/
|
|
709
|
+
collaborators(id, params = {}, options) {
|
|
710
|
+
return new Paginated(
|
|
711
|
+
(cursor) => this.#http.get(
|
|
712
|
+
`/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
|
|
713
|
+
{
|
|
714
|
+
...params.limit !== void 0 ? { limit: params.limit } : {},
|
|
715
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
716
|
+
},
|
|
717
|
+
options
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Offers somebody sight of a Space. Answers with the invitation.
|
|
723
|
+
*
|
|
724
|
+
* AN OFFER, NOT A GRANT, and the returned `acceptedAt` will be absent to
|
|
725
|
+
* prove it. The recipient has to accept before they can see anything, which
|
|
726
|
+
* is the property that keeps "nothing enters your memory without you" true
|
|
727
|
+
* even when somebody else starts the sharing. Do not tell your user their
|
|
728
|
+
* Space "has been shared" on the strength of a 2xx here.
|
|
729
|
+
*
|
|
730
|
+
* WHAT THEY GET IS THE WHOLE SPACE: every memory already filed in it and
|
|
731
|
+
* every memory that lands in it afterwards. There is no narrower grant, and
|
|
732
|
+
* `role` does not make one - it decides what they may do BESIDES read.
|
|
733
|
+
*
|
|
734
|
+
* Worth an idempotency key when a person is behind it. A double-clicked
|
|
735
|
+
* "share" is two invitations to the same address, and the second one is a
|
|
736
|
+
* second email arriving at somebody who has already been asked.
|
|
737
|
+
*/
|
|
738
|
+
async share(id, params, options) {
|
|
739
|
+
return this.#http.post(
|
|
740
|
+
`/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
|
|
741
|
+
params,
|
|
742
|
+
options
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Ends somebody's access, or withdraws an invitation they never accepted.
|
|
747
|
+
*
|
|
748
|
+
* Nothing was ever copied into their account - a collaborator SEES the
|
|
749
|
+
* owner's memories rather than holding a duplicate - so this is one write
|
|
750
|
+
* and not a cascade, and there is no orphaned copy left behind.
|
|
751
|
+
*
|
|
752
|
+
* A body on a DELETE, matching `removeMemories` above. The alternative is an
|
|
753
|
+
* address in a path segment, where every `.`, `+` and `@` is a chance for a
|
|
754
|
+
* proxy or a router to normalise somebody else's email into the one that
|
|
755
|
+
* gets revoked.
|
|
756
|
+
*/
|
|
757
|
+
async unshare(id, email, options) {
|
|
758
|
+
return this.#http.delete(
|
|
759
|
+
`/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
|
|
760
|
+
{ email },
|
|
761
|
+
options
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Changes what an existing collaborator may do. Never invites anybody.
|
|
766
|
+
*
|
|
767
|
+
* The quiet one. Moving somebody from `viewer` to `owner` sends no
|
|
768
|
+
* invitation and needs no acceptance, and afterwards they can share the
|
|
769
|
+
* Space onward and revoke the person who promoted them. Show your user what
|
|
770
|
+
* `owner` means before you send this, not after.
|
|
771
|
+
*/
|
|
772
|
+
async setRole(id, params, options) {
|
|
773
|
+
return this.#http.patch(
|
|
774
|
+
`/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
|
|
775
|
+
params,
|
|
776
|
+
options
|
|
777
|
+
);
|
|
778
|
+
}
|
|
654
779
|
};
|
|
655
780
|
var Sources = class {
|
|
656
781
|
#http;
|
|
@@ -888,6 +1013,119 @@ var Conversations = class {
|
|
|
888
1013
|
);
|
|
889
1014
|
}
|
|
890
1015
|
};
|
|
1016
|
+
var Google = class {
|
|
1017
|
+
#http;
|
|
1018
|
+
constructor(http) {
|
|
1019
|
+
this.#http = http;
|
|
1020
|
+
}
|
|
1021
|
+
/** Files by name, newest first. Omit the query for recently changed ones. */
|
|
1022
|
+
async searchDrive(params = {}, options) {
|
|
1023
|
+
return this.#http.get(
|
|
1024
|
+
"/api/v1/google/drive/files",
|
|
1025
|
+
{
|
|
1026
|
+
...params.query !== void 0 ? { query: params.query } : {},
|
|
1027
|
+
...params.limit !== void 0 ? { limit: params.limit } : {}
|
|
1028
|
+
},
|
|
1029
|
+
options
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
async getDriveFile(fileId, options) {
|
|
1033
|
+
return this.#http.get(
|
|
1034
|
+
`/api/v1/google/drive/files/${encodeURIComponent(fileId)}`,
|
|
1035
|
+
void 0,
|
|
1036
|
+
options
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The bytes of a Drive file.
|
|
1041
|
+
*
|
|
1042
|
+
* A Google Doc, Sheet or Slide holds no bytes of its own and is exported on
|
|
1043
|
+
* the way - a document as PDF, a spreadsheet as CSV - so `filename` comes
|
|
1044
|
+
* back describing what it BECAME. Writing it under the id instead produces a
|
|
1045
|
+
* file nothing will open.
|
|
1046
|
+
*/
|
|
1047
|
+
async downloadDriveFile(fileId, options) {
|
|
1048
|
+
return this.#http.getBytes(
|
|
1049
|
+
`/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`,
|
|
1050
|
+
void 0,
|
|
1051
|
+
options
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Writes a file into the user's Drive.
|
|
1056
|
+
*
|
|
1057
|
+
* Needs one of the Drive write permissions on their connection. A read-only
|
|
1058
|
+
* grant is refused by Google, and the error names the missing permission
|
|
1059
|
+
* rather than reporting a failed upload - one is fixed with a checkbox and
|
|
1060
|
+
* the other sends somebody looking for a bug.
|
|
1061
|
+
*/
|
|
1062
|
+
async saveToDrive(params, options) {
|
|
1063
|
+
return this.#http.postBytes(
|
|
1064
|
+
"/api/v1/google/drive/files",
|
|
1065
|
+
params.bytes,
|
|
1066
|
+
params.contentType ?? "application/octet-stream",
|
|
1067
|
+
{
|
|
1068
|
+
name: params.name,
|
|
1069
|
+
...params.folderId !== void 0 ? { folderId: params.folderId } : {}
|
|
1070
|
+
},
|
|
1071
|
+
options
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Recent messages - senders, subjects and a one-line preview, never bodies.
|
|
1076
|
+
*
|
|
1077
|
+
* `query` is Gmail's own syntax passed through as written: `from:priya`,
|
|
1078
|
+
* `has:attachment`, `newer_than:7d`. It selects within the connected mailbox
|
|
1079
|
+
* and cannot reach another one.
|
|
1080
|
+
*/
|
|
1081
|
+
async searchMail(params = {}, options) {
|
|
1082
|
+
return this.#http.get(
|
|
1083
|
+
"/api/v1/google/mail",
|
|
1084
|
+
{
|
|
1085
|
+
...params.query !== void 0 ? { query: params.query } : {},
|
|
1086
|
+
...params.limit !== void 0 ? { limit: params.limit } : {}
|
|
1087
|
+
},
|
|
1088
|
+
options
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
/** One message, with its body and the names of what is attached. */
|
|
1092
|
+
async readMail(messageId, options) {
|
|
1093
|
+
return this.#http.get(
|
|
1094
|
+
`/api/v1/google/mail/${encodeURIComponent(messageId)}`,
|
|
1095
|
+
void 0,
|
|
1096
|
+
options
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The bytes of one attachment.
|
|
1101
|
+
*
|
|
1102
|
+
* Separate from `readMail` so listing a mailbox never drags attachments
|
|
1103
|
+
* across the network: a message with a 40 MB deck should not cost 40 MB to
|
|
1104
|
+
* summarise.
|
|
1105
|
+
*/
|
|
1106
|
+
async downloadAttachment(messageId, attachmentId, options) {
|
|
1107
|
+
return this.#http.getBytes(
|
|
1108
|
+
`/api/v1/google/mail/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`,
|
|
1109
|
+
void 0,
|
|
1110
|
+
options
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
/** Sends as the connected account. Needs the send permission. */
|
|
1114
|
+
async sendMail(params, options) {
|
|
1115
|
+
return this.#http.post("/api/v1/google/mail/send", params, options);
|
|
1116
|
+
}
|
|
1117
|
+
/** People in the user's contacts. Omit the query to list them. */
|
|
1118
|
+
async contacts(params = {}, options) {
|
|
1119
|
+
return this.#http.get(
|
|
1120
|
+
"/api/v1/google/contacts",
|
|
1121
|
+
{
|
|
1122
|
+
...params.query !== void 0 ? { query: params.query } : {},
|
|
1123
|
+
...params.limit !== void 0 ? { limit: params.limit } : {}
|
|
1124
|
+
},
|
|
1125
|
+
options
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
891
1129
|
var Integrations = class {
|
|
892
1130
|
#http;
|
|
893
1131
|
constructor(http) {
|
|
@@ -1018,6 +1256,8 @@ var PersistMemory = class {
|
|
|
1018
1256
|
conflicts;
|
|
1019
1257
|
conversations;
|
|
1020
1258
|
integrations;
|
|
1259
|
+
/** Drive, mail and contacts on the user's connected Google account. */
|
|
1260
|
+
google;
|
|
1021
1261
|
health;
|
|
1022
1262
|
agent;
|
|
1023
1263
|
#http;
|
|
@@ -1034,6 +1274,7 @@ var PersistMemory = class {
|
|
|
1034
1274
|
this.conflicts = new Conflicts(this.#http);
|
|
1035
1275
|
this.conversations = new Conversations(this.#http);
|
|
1036
1276
|
this.integrations = new Integrations(this.#http);
|
|
1277
|
+
this.google = new Google(this.#http);
|
|
1037
1278
|
this.health = new Health(this.#http);
|
|
1038
1279
|
this.agent = new Agent(this.#http);
|
|
1039
1280
|
}
|
|
@@ -1291,6 +1532,23 @@ function maskToken(token) {
|
|
|
1291
1532
|
return `${token.slice(0, 4)}\u2026${token.slice(-4)}`;
|
|
1292
1533
|
}
|
|
1293
1534
|
|
|
1535
|
+
// src/display-safe.ts
|
|
1536
|
+
var NEUTRALISED = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u061c\u200b\u200e\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff\ufff9-\ufffb]|[\u{e0000}-\u{e007f}]/gu;
|
|
1537
|
+
var LINE_BREAKS = /\r\n|[\r\u2028\u2029]/g;
|
|
1538
|
+
function displaySafe(text) {
|
|
1539
|
+
return text.replace(LINE_BREAKS, "\n").replace(NEUTRALISED, "");
|
|
1540
|
+
}
|
|
1541
|
+
function jsonSafe(value) {
|
|
1542
|
+
return JSON.stringify(value, void 0, 2).replace(
|
|
1543
|
+
NEUTRALISED,
|
|
1544
|
+
(match) => (
|
|
1545
|
+
// Per UTF-16 unit, so a tags-block codepoint becomes its surrogate pair
|
|
1546
|
+
// rather than one escape JSON cannot represent.
|
|
1547
|
+
match.split("").map((unit) => "\\u" + unit.charCodeAt(0).toString(16).padStart(4, "0")).join("")
|
|
1548
|
+
)
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1294
1552
|
// src/output.ts
|
|
1295
1553
|
var OUTPUT_FORMATS = ["table", "json", "yaml", "csv", "tsv"];
|
|
1296
1554
|
function isOutputFormat(value) {
|
|
@@ -1299,7 +1557,7 @@ function isOutputFormat(value) {
|
|
|
1299
1557
|
function render(rows, columns, options) {
|
|
1300
1558
|
switch (options.format) {
|
|
1301
1559
|
case "json":
|
|
1302
|
-
return
|
|
1560
|
+
return jsonSafe(rows);
|
|
1303
1561
|
case "yaml":
|
|
1304
1562
|
return toYaml(rows);
|
|
1305
1563
|
case "csv":
|
|
@@ -1311,13 +1569,13 @@ function render(rows, columns, options) {
|
|
|
1311
1569
|
}
|
|
1312
1570
|
}
|
|
1313
1571
|
function renderOne(row, fields, options) {
|
|
1314
|
-
if (options.format === "json") return
|
|
1572
|
+
if (options.format === "json") return jsonSafe(row);
|
|
1315
1573
|
if (options.format === "yaml") return toYaml(row);
|
|
1316
1574
|
if (options.format === "csv" || options.format === "tsv") {
|
|
1317
1575
|
return delimited([row], fields, options.format === "csv" ? "," : " ");
|
|
1318
1576
|
}
|
|
1319
1577
|
const width = Math.max(...fields.map((field) => field.header.length));
|
|
1320
|
-
return fields.map((field) => `${field.header.padEnd(width)} ${field.value(row)}`).join("\n");
|
|
1578
|
+
return fields.map((field) => `${field.header.padEnd(width)} ${displaySafe(field.value(row))}`).join("\n");
|
|
1321
1579
|
}
|
|
1322
1580
|
function table(rows, columns, width = process.stdout.columns || 120) {
|
|
1323
1581
|
if (rows.length === 0) return "";
|
|
@@ -1377,7 +1635,8 @@ function isBlock(value) {
|
|
|
1377
1635
|
if (Array.isArray(value)) return value.length > 0;
|
|
1378
1636
|
return typeof value === "object" && value !== null && Object.keys(value).length > 0;
|
|
1379
1637
|
}
|
|
1380
|
-
function yamlString(
|
|
1638
|
+
function yamlString(text) {
|
|
1639
|
+
const value = displaySafe(text);
|
|
1381
1640
|
if (value === "") return '""';
|
|
1382
1641
|
if (value.includes("\n")) {
|
|
1383
1642
|
return `|-
|
|
@@ -1389,7 +1648,7 @@ ${value.split("\n").map((line) => ` ${line}`).join("\n")}`;
|
|
|
1389
1648
|
return ambiguous ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
1390
1649
|
}
|
|
1391
1650
|
function oneLine(value) {
|
|
1392
|
-
return value.replace(/\s*\n\s*/g, " ").trim();
|
|
1651
|
+
return displaySafe(value).replace(/\s*\n\s*/g, " ").trim();
|
|
1393
1652
|
}
|
|
1394
1653
|
function clip(value, width) {
|
|
1395
1654
|
if (value.length <= width) return value;
|
|
@@ -1402,7 +1661,7 @@ function shortDate(iso) {
|
|
|
1402
1661
|
}
|
|
1403
1662
|
|
|
1404
1663
|
// src/help.ts
|
|
1405
|
-
var VERSION = true ? "0.
|
|
1664
|
+
var VERSION = true ? "0.7.0" : versionFromManifest();
|
|
1406
1665
|
var PACKAGE = "@persistmemory/cli";
|
|
1407
1666
|
var HELP = `
|
|
1408
1667
|
pm \u2014 PersistMemory from your terminal
|
|
@@ -1440,28 +1699,31 @@ var HELP = `
|
|
|
1440
1699
|
to what is in it
|
|
1441
1700
|
spaces merge "A" "B" --name "C" a new Space holding both, originals kept
|
|
1442
1701
|
|
|
1702
|
+
spaces sharing "Acme" who can see it, and who has accepted
|
|
1703
|
+
spaces share "Acme" <email> --role viewer|editor
|
|
1704
|
+
offer somebody sight of EVERYTHING in it,
|
|
1705
|
+
now and later. You must say the role.
|
|
1706
|
+
They see nothing until they accept.
|
|
1707
|
+
spaces unshare "Acme" <email> end their access
|
|
1708
|
+
spaces role "Acme" <email> editor
|
|
1709
|
+
change what an existing collaborator may do
|
|
1710
|
+
|
|
1443
1711
|
remember <text> capture text
|
|
1444
1712
|
remember - capture whatever is piped in
|
|
1445
1713
|
remember --file <path> capture a file's contents
|
|
1446
1714
|
|
|
1447
1715
|
agent answer file and command requests from this
|
|
1448
|
-
machine. Reads
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1716
|
+
machine. Reads anywhere on it \u2014 you
|
|
1717
|
+
approve every request first, and see the
|
|
1718
|
+
exact path or command before you do.
|
|
1719
|
+
Stays in the foreground; background it
|
|
1720
|
+
and stop it later with:
|
|
1453
1721
|
nohup pm agent > ~/agent.log 2>&1 &
|
|
1454
1722
|
pkill -f "pm agent"
|
|
1455
|
-
agent --root <dir> [--root ...]
|
|
1723
|
+
agent --root <dir> [--root ...] narrow it to these folders, for this run
|
|
1456
1724
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
{
|
|
1460
|
-
"roots": ["~/code", "~/Documents"],
|
|
1461
|
-
"mode": "ask", ask | auto-read | plan
|
|
1462
|
-
"allow": ["swift"], programs this machine treats as reads
|
|
1463
|
-
"deny": ["rm"] programs it refuses, whatever is approved
|
|
1464
|
-
}
|
|
1725
|
+
A command that reaches the network, or that runs a language, is refused by
|
|
1726
|
+
this machine whatever anybody approves.
|
|
1465
1727
|
|
|
1466
1728
|
search <query> search your memory
|
|
1467
1729
|
list memories the most recent memories
|
|
@@ -1620,8 +1882,8 @@ async function startLoopback(options = {}) {
|
|
|
1620
1882
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
1621
1883
|
let resolveCallback;
|
|
1622
1884
|
let rejectCallback;
|
|
1623
|
-
const received = new Promise((
|
|
1624
|
-
resolveCallback =
|
|
1885
|
+
const received = new Promise((resolve8, reject) => {
|
|
1886
|
+
resolveCallback = resolve8;
|
|
1625
1887
|
rejectCallback = reject;
|
|
1626
1888
|
});
|
|
1627
1889
|
const server = createServer((request, response) => {
|
|
@@ -1657,9 +1919,9 @@ async function startLoopback(options = {}) {
|
|
|
1657
1919
|
response.end(donePage(callback));
|
|
1658
1920
|
resolveCallback?.(callback);
|
|
1659
1921
|
});
|
|
1660
|
-
await new Promise((
|
|
1922
|
+
await new Promise((resolve8, reject) => {
|
|
1661
1923
|
server.once("error", reject);
|
|
1662
|
-
server.listen(0, "127.0.0.1",
|
|
1924
|
+
server.listen(0, "127.0.0.1", resolve8);
|
|
1663
1925
|
});
|
|
1664
1926
|
const address = server.address();
|
|
1665
1927
|
if (address === null || typeof address === "string") {
|
|
@@ -1923,7 +2185,7 @@ function safeEqual(a, b) {
|
|
|
1923
2185
|
}
|
|
1924
2186
|
async function openBrowser(url) {
|
|
1925
2187
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1926
|
-
await new Promise((
|
|
2188
|
+
await new Promise((resolve8, reject) => {
|
|
1927
2189
|
const child = spawn(command, args, {
|
|
1928
2190
|
stdio: "ignore",
|
|
1929
2191
|
// Detached so closing the terminal does not close the browser, and so
|
|
@@ -1932,7 +2194,7 @@ async function openBrowser(url) {
|
|
|
1932
2194
|
});
|
|
1933
2195
|
child.once("error", reject);
|
|
1934
2196
|
child.unref();
|
|
1935
|
-
|
|
2197
|
+
resolve8();
|
|
1936
2198
|
});
|
|
1937
2199
|
}
|
|
1938
2200
|
async function describe(response) {
|
|
@@ -1994,16 +2256,16 @@ async function currentCredential(resolved, deps) {
|
|
|
1994
2256
|
// src/context.ts
|
|
1995
2257
|
import { createInterface } from "node:readline";
|
|
1996
2258
|
async function askOnTty(prompt, input = process.stdin, output = process.stdout) {
|
|
1997
|
-
return new Promise((
|
|
2259
|
+
return new Promise((resolve8) => {
|
|
1998
2260
|
const readline = createInterface({ input, output });
|
|
1999
2261
|
let answered = false;
|
|
2000
2262
|
readline.question(prompt, (answer3) => {
|
|
2001
2263
|
answered = true;
|
|
2002
|
-
|
|
2264
|
+
resolve8(answer3.trim());
|
|
2003
2265
|
readline.close();
|
|
2004
2266
|
});
|
|
2005
2267
|
readline.once("close", () => {
|
|
2006
|
-
if (!answered)
|
|
2268
|
+
if (!answered) resolve8("");
|
|
2007
2269
|
});
|
|
2008
2270
|
});
|
|
2009
2271
|
}
|
|
@@ -2013,16 +2275,16 @@ var BACKSPACE = "\b";
|
|
|
2013
2275
|
async function readSecretFromTty(prompt) {
|
|
2014
2276
|
const input = process.stdin;
|
|
2015
2277
|
if (!input.isTTY) {
|
|
2016
|
-
return new Promise((
|
|
2278
|
+
return new Promise((resolve8) => {
|
|
2017
2279
|
const readline = createInterface({ input });
|
|
2018
2280
|
let answered = false;
|
|
2019
2281
|
readline.once("line", (line) => {
|
|
2020
2282
|
answered = true;
|
|
2021
|
-
|
|
2283
|
+
resolve8(line.trim());
|
|
2022
2284
|
readline.close();
|
|
2023
2285
|
});
|
|
2024
2286
|
readline.once("close", () => {
|
|
2025
|
-
if (!answered)
|
|
2287
|
+
if (!answered) resolve8("");
|
|
2026
2288
|
});
|
|
2027
2289
|
});
|
|
2028
2290
|
}
|
|
@@ -2031,14 +2293,14 @@ async function readSecretFromTty(prompt) {
|
|
|
2031
2293
|
input.setRawMode?.(true);
|
|
2032
2294
|
input.resume();
|
|
2033
2295
|
input.setEncoding("utf8");
|
|
2034
|
-
return new Promise((
|
|
2296
|
+
return new Promise((resolve8) => {
|
|
2035
2297
|
let value = "";
|
|
2036
2298
|
const finish2 = () => {
|
|
2037
2299
|
input.removeListener("data", onData);
|
|
2038
2300
|
input.setRawMode?.(previouslyRaw);
|
|
2039
2301
|
input.pause();
|
|
2040
2302
|
process.stdout.write("\n");
|
|
2041
|
-
|
|
2303
|
+
resolve8(value.trim());
|
|
2042
2304
|
};
|
|
2043
2305
|
const onData = (chunk) => {
|
|
2044
2306
|
for (const character of chunk) {
|
|
@@ -2074,9 +2336,9 @@ async function readStdin() {
|
|
|
2074
2336
|
|
|
2075
2337
|
// src/commands/agent.ts
|
|
2076
2338
|
import { hostname } from "node:os";
|
|
2077
|
-
import { homedir as
|
|
2078
|
-
import { basename, join as
|
|
2079
|
-
import { existsSync as
|
|
2339
|
+
import { homedir as homedir2 } from "node:os";
|
|
2340
|
+
import { basename, join as join5, resolve as resolve3 } from "node:path";
|
|
2341
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2080
2342
|
|
|
2081
2343
|
// src/files.ts
|
|
2082
2344
|
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -2187,9 +2449,26 @@ ${head.join("\n")}`;
|
|
|
2187
2449
|
// src/commands/run-command.ts
|
|
2188
2450
|
import { spawn as spawn2 } from "node:child_process";
|
|
2189
2451
|
import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
2190
|
-
import { isAbsolute as isAbsolute2, join as join4, resolve as resolvePath } from "node:path";
|
|
2452
|
+
import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolvePath } from "node:path";
|
|
2191
2453
|
var CLASSES = new Map([
|
|
2192
|
-
|
|
2454
|
+
/*
|
|
2455
|
+
Reads. Report on the machine and change nothing.
|
|
2456
|
+
|
|
2457
|
+
Several of these are reads only until a particular flag is passed —
|
|
2458
|
+
`find -exec` runs any program, `sort -o` writes a file, `dmesg -C` empties
|
|
2459
|
+
the kernel buffer — and the name alone cannot see that. `FLAG_CHANGES_CLASS`
|
|
2460
|
+
below is where those come back out again; a name in this list is the
|
|
2461
|
+
starting point, not the verdict.
|
|
2462
|
+
|
|
2463
|
+
`journalctl`, `dmesg` and `zcat` are here because of what people actually
|
|
2464
|
+
ask for. "Check the logs on my server" is `journalctl -n 200 -u nginx`,
|
|
2465
|
+
`dmesg -T`, or `zcat` on a rotated `.gz`, and leaving all three in
|
|
2466
|
+
`unknown` bought nothing: every command already waits for a person, so the
|
|
2467
|
+
only thing an honest `read` label changes is that it stops being a lie.
|
|
2468
|
+
`zcat` earns it by only ever writing to stdout — `gunzip` and `gzip -d`
|
|
2469
|
+
delete their input, which is why neither is here — and the decompression
|
|
2470
|
+
bomb it can be handed is bounded by the output cap rather than by trust.
|
|
2471
|
+
*/
|
|
2193
2472
|
...[
|
|
2194
2473
|
"ls",
|
|
2195
2474
|
"cat",
|
|
@@ -2217,7 +2496,10 @@ var CLASSES = new Map([
|
|
|
2217
2496
|
"dirname",
|
|
2218
2497
|
"realpath",
|
|
2219
2498
|
"ps",
|
|
2220
|
-
"env"
|
|
2499
|
+
"env",
|
|
2500
|
+
"journalctl",
|
|
2501
|
+
"dmesg",
|
|
2502
|
+
"zcat"
|
|
2221
2503
|
].map((name) => [name, "read"]),
|
|
2222
2504
|
// Writes. Recoverable or not, they change the machine.
|
|
2223
2505
|
...[
|
|
@@ -2264,16 +2546,159 @@ var SUBCOMMAND_READS = /* @__PURE__ */ new Map([
|
|
|
2264
2546
|
"blame",
|
|
2265
2547
|
"shortlog",
|
|
2266
2548
|
"ls-files",
|
|
2267
|
-
"rev-parse"
|
|
2268
|
-
"config"
|
|
2549
|
+
"rev-parse"
|
|
2269
2550
|
])
|
|
2270
2551
|
],
|
|
2271
|
-
["npm", /* @__PURE__ */ new Set(["ls", "list", "view", "outdated", "why"
|
|
2272
|
-
["yarn", /* @__PURE__ */ new Set(["list", "why", "info"
|
|
2273
|
-
|
|
2552
|
+
["npm", /* @__PURE__ */ new Set(["ls", "list", "view", "outdated", "why"])],
|
|
2553
|
+
["yarn", /* @__PURE__ */ new Set(["list", "why", "info"])],
|
|
2554
|
+
/*
|
|
2555
|
+
`docker logs` STAYS a read, having been looked at again.
|
|
2556
|
+
|
|
2557
|
+
The case against it is real: every docker subcommand is a request to a
|
|
2558
|
+
daemon running as root, so "it only reads" is a statement about the
|
|
2559
|
+
subcommand and not about the socket it is sent down. The case for keeping
|
|
2560
|
+
it is that `logs` cannot start, stop or change anything — it prints what a
|
|
2561
|
+
container already wrote to its own stdout — and it is one of the three
|
|
2562
|
+
things anybody means by "check the logs on my server".
|
|
2563
|
+
|
|
2564
|
+
What actually needed fixing was not the label but the flags in front of
|
|
2565
|
+
it: see `REMOTE_FLAGS`. The residue this leaves is `docker --config=<dir>
|
|
2566
|
+
logs x`, which could load a CLI plugin from a directory the argv chose;
|
|
2567
|
+
`logs` is built in rather than a plugin, and a plugin has to be planted on
|
|
2568
|
+
the disk first, so it is left standing rather than papered over here.
|
|
2569
|
+
*/
|
|
2570
|
+
["docker", /* @__PURE__ */ new Set(["ps", "images", "logs", "inspect"])],
|
|
2571
|
+
/*
|
|
2572
|
+
`systemctl`, which was in no table at all and so was `unknown` whole.
|
|
2573
|
+
|
|
2574
|
+
It is the same shape as `git` and the reason this map exists: `systemctl
|
|
2575
|
+
status nginx` asks the manager a question over its bus, and `systemctl
|
|
2576
|
+
stop nginx` takes somebody's website down. One name, two powers, and the
|
|
2577
|
+
first non-flag word is the whole difference. Everything not listed —
|
|
2578
|
+
start, stop, restart, enable, mask, daemon-reload — lands in `write`,
|
|
2579
|
+
which is where it belongs and where it already effectively was.
|
|
2580
|
+
|
|
2581
|
+
`-H user@host` is not here because it is not a subcommand: it makes
|
|
2582
|
+
systemctl talk to ANOTHER machine over ssh, which is `REMOTE_FLAGS` below
|
|
2583
|
+
and a refusal, not a class.
|
|
2584
|
+
*/
|
|
2585
|
+
[
|
|
2586
|
+
"systemctl",
|
|
2587
|
+
/* @__PURE__ */ new Set([
|
|
2588
|
+
"status",
|
|
2589
|
+
"show",
|
|
2590
|
+
"cat",
|
|
2591
|
+
"list-units",
|
|
2592
|
+
"list-unit-files",
|
|
2593
|
+
"list-timers",
|
|
2594
|
+
"list-sockets",
|
|
2595
|
+
"list-dependencies",
|
|
2596
|
+
"list-jobs",
|
|
2597
|
+
"list-machines",
|
|
2598
|
+
"is-active",
|
|
2599
|
+
"is-enabled",
|
|
2600
|
+
"is-failed",
|
|
2601
|
+
"is-system-running",
|
|
2602
|
+
"get-default",
|
|
2603
|
+
"show-environment"
|
|
2604
|
+
])
|
|
2605
|
+
]
|
|
2606
|
+
]);
|
|
2607
|
+
var FLAG_CHANGES_CLASS = /* @__PURE__ */ new Map([
|
|
2608
|
+
[
|
|
2609
|
+
"find",
|
|
2610
|
+
new Map([
|
|
2611
|
+
// Runs anything, once per file found. A model that cannot get `bash`
|
|
2612
|
+
// past this file can get `find . -name x -exec bash {} ;` past it.
|
|
2613
|
+
...["-exec", "-execdir", "-ok", "-okdir"].map(
|
|
2614
|
+
(flag) => [flag, "interpreter"]
|
|
2615
|
+
),
|
|
2616
|
+
...["-delete", "-fprint", "-fprint0", "-fprintf", "-fls"].map(
|
|
2617
|
+
(flag) => [flag, "write"]
|
|
2618
|
+
)
|
|
2619
|
+
])
|
|
2620
|
+
],
|
|
2621
|
+
["sort", /* @__PURE__ */ new Map([["-o", "write"], ["--output", "write"]])],
|
|
2622
|
+
/*
|
|
2623
|
+
`fd`, `rg` and `tree` were left in the read table with no entry here, and
|
|
2624
|
+
the first two run arbitrary programs exactly the way `find -exec` does.
|
|
2625
|
+
|
|
2626
|
+
`fd -x` IS `find -exec` under a newer name, and `rg --pre` runs a program
|
|
2627
|
+
per file to decode it. Both were classified `read` and therefore ran
|
|
2628
|
+
automatically: `fd --exec curl http://…` reached the network, which is the
|
|
2629
|
+
one refusal nothing is supposed to override. They were refused only when
|
|
2630
|
+
the helper was spelled with a slash — `pathOutsideRoots` catching
|
|
2631
|
+
`/bin/sh` — so naming it `sh`, or putting it inside the roots, evaporated
|
|
2632
|
+
the refusal. That is not a boundary, it is a coincidence about spelling.
|
|
2633
|
+
|
|
2634
|
+
`tree -o` writes a file, the same shape as `sort -o`.
|
|
2635
|
+
*/
|
|
2636
|
+
[
|
|
2637
|
+
"fd",
|
|
2638
|
+
new Map(
|
|
2639
|
+
["-x", "--exec", "-X", "--exec-batch"].map(
|
|
2640
|
+
(flag) => [flag, "interpreter"]
|
|
2641
|
+
)
|
|
2642
|
+
)
|
|
2643
|
+
],
|
|
2644
|
+
[
|
|
2645
|
+
"rg",
|
|
2646
|
+
new Map(
|
|
2647
|
+
["--pre", "--hostname-bin"].map((flag) => [flag, "interpreter"])
|
|
2648
|
+
)
|
|
2649
|
+
],
|
|
2650
|
+
["tree", /* @__PURE__ */ new Map([["-o", "write"]])],
|
|
2651
|
+
[
|
|
2652
|
+
"journalctl",
|
|
2653
|
+
new Map(
|
|
2654
|
+
[
|
|
2655
|
+
"--vacuum-size",
|
|
2656
|
+
"--vacuum-time",
|
|
2657
|
+
"--vacuum-files",
|
|
2658
|
+
"--rotate",
|
|
2659
|
+
"--flush",
|
|
2660
|
+
"--sync",
|
|
2661
|
+
"--relinquish-var",
|
|
2662
|
+
"--smart-relinquish-var",
|
|
2663
|
+
"--setup-keys",
|
|
2664
|
+
"--update-catalog"
|
|
2665
|
+
].map((flag) => [flag, "write"])
|
|
2666
|
+
)
|
|
2667
|
+
],
|
|
2668
|
+
[
|
|
2669
|
+
"dmesg",
|
|
2670
|
+
new Map(
|
|
2671
|
+
// `-c` reads AND clears, which is the one that costs somebody the
|
|
2672
|
+
// evidence they were reading the log to find.
|
|
2673
|
+
[
|
|
2674
|
+
"-C",
|
|
2675
|
+
"--clear",
|
|
2676
|
+
"-c",
|
|
2677
|
+
"--read-clear",
|
|
2678
|
+
"-D",
|
|
2679
|
+
"--console-off",
|
|
2680
|
+
"-E",
|
|
2681
|
+
"--console-on",
|
|
2682
|
+
"-n",
|
|
2683
|
+
"--console-level"
|
|
2684
|
+
].map((flag) => [flag, "write"])
|
|
2685
|
+
)
|
|
2686
|
+
]
|
|
2687
|
+
]);
|
|
2688
|
+
var REMOTE_FLAGS = /* @__PURE__ */ new Map([
|
|
2689
|
+
["docker", /* @__PURE__ */ new Set(["-H", "--host", "--context"])],
|
|
2690
|
+
["systemctl", /* @__PURE__ */ new Set(["-H", "--host"])]
|
|
2691
|
+
]);
|
|
2692
|
+
var ENDLESS_FLAGS = /* @__PURE__ */ new Map([
|
|
2693
|
+
["tail", /* @__PURE__ */ new Set(["-f", "-F", "--follow"])],
|
|
2694
|
+
["journalctl", /* @__PURE__ */ new Set(["-f", "--follow"])],
|
|
2695
|
+
["dmesg", /* @__PURE__ */ new Set(["-w", "--follow", "-W", "--follow-new"])],
|
|
2696
|
+
["docker logs", /* @__PURE__ */ new Set(["-f", "--follow"])],
|
|
2697
|
+
["kubectl logs", /* @__PURE__ */ new Set(["-f", "--follow"])]
|
|
2274
2698
|
]);
|
|
2275
2699
|
var MAX_OUTPUT_BYTES = 256 * 1024;
|
|
2276
2700
|
var TIMEOUT_MS = 2e4;
|
|
2701
|
+
var AFTER_KILL_MS = 1e3;
|
|
2277
2702
|
function describe2(argv) {
|
|
2278
2703
|
return argv.join(" ");
|
|
2279
2704
|
}
|
|
@@ -2281,15 +2706,73 @@ function programOf(argv) {
|
|
|
2281
2706
|
const first = argv[0] ?? "";
|
|
2282
2707
|
return first.split("/").pop() ?? first;
|
|
2283
2708
|
}
|
|
2709
|
+
function subcommandOf(argv) {
|
|
2710
|
+
return argv.slice(1).find((one) => !one.startsWith("-"));
|
|
2711
|
+
}
|
|
2712
|
+
function flagsIn(argv) {
|
|
2713
|
+
const found = /* @__PURE__ */ new Set();
|
|
2714
|
+
for (const argument of argv.slice(1)) {
|
|
2715
|
+
if (argument === "--") break;
|
|
2716
|
+
if (argument === "-" || !argument.startsWith("-")) continue;
|
|
2717
|
+
const name = argument.split("=")[0] ?? argument;
|
|
2718
|
+
found.add(name);
|
|
2719
|
+
if (!name.startsWith("--")) {
|
|
2720
|
+
for (const letter of name.slice(1)) found.add(`-${letter}`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
return found;
|
|
2724
|
+
}
|
|
2725
|
+
function anyFlag(argv, flags) {
|
|
2726
|
+
if (!flags) return void 0;
|
|
2727
|
+
for (const flag of flagsIn(argv)) {
|
|
2728
|
+
if (flags.has(flag)) return flag;
|
|
2729
|
+
}
|
|
2730
|
+
return void 0;
|
|
2731
|
+
}
|
|
2732
|
+
function keysFor(argv) {
|
|
2733
|
+
const program = programOf(argv);
|
|
2734
|
+
const sub = subcommandOf(argv);
|
|
2735
|
+
return sub ? [program, `${program} ${sub}`] : [program];
|
|
2736
|
+
}
|
|
2737
|
+
function remoteFlag(argv) {
|
|
2738
|
+
return anyFlag(argv, REMOTE_FLAGS.get(programOf(argv)));
|
|
2739
|
+
}
|
|
2740
|
+
function endlessFlag(argv) {
|
|
2741
|
+
for (const key of keysFor(argv)) {
|
|
2742
|
+
const found = anyFlag(argv, ENDLESS_FLAGS.get(key));
|
|
2743
|
+
if (found) return found;
|
|
2744
|
+
}
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
function flagClass(argv) {
|
|
2748
|
+
for (const key of keysFor(argv)) {
|
|
2749
|
+
const table2 = FLAG_CHANGES_CLASS.get(key);
|
|
2750
|
+
if (!table2) continue;
|
|
2751
|
+
for (const flag of flagsIn(argv)) {
|
|
2752
|
+
const found = table2.get(flag);
|
|
2753
|
+
if (found) return found;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return void 0;
|
|
2757
|
+
}
|
|
2758
|
+
function runsAnotherProgram(argv) {
|
|
2759
|
+
if (programOf(argv) !== "env") return false;
|
|
2760
|
+
return argv.slice(1).some((one) => !one.startsWith("-") && !one.includes("="));
|
|
2761
|
+
}
|
|
2284
2762
|
function classify(argv, policy) {
|
|
2285
2763
|
const program = programOf(argv);
|
|
2764
|
+
if (remoteFlag(argv)) return "network";
|
|
2765
|
+
const imposed = flagClass(argv);
|
|
2766
|
+
if (imposed === "interpreter" || runsAnotherProgram(argv)) return "interpreter";
|
|
2767
|
+
const known = CLASSES.get(program);
|
|
2768
|
+
if (known === "network" || known === "interpreter") return known;
|
|
2286
2769
|
if (policy.allow.includes(program)) return "read";
|
|
2287
2770
|
const reads = SUBCOMMAND_READS.get(program);
|
|
2288
2771
|
if (reads) {
|
|
2289
|
-
const sub = argv
|
|
2772
|
+
const sub = subcommandOf(argv);
|
|
2290
2773
|
return sub && reads.has(sub) ? "read" : "write";
|
|
2291
2774
|
}
|
|
2292
|
-
return
|
|
2775
|
+
return imposed ?? known ?? "unknown";
|
|
2293
2776
|
}
|
|
2294
2777
|
function judge(argv, policy) {
|
|
2295
2778
|
const program = programOf(argv);
|
|
@@ -2306,21 +2789,32 @@ function judge(argv, policy) {
|
|
|
2306
2789
|
}
|
|
2307
2790
|
const commandClass = classify(argv, policy);
|
|
2308
2791
|
if (commandClass === "network") {
|
|
2792
|
+
const remote = remoteFlag(argv);
|
|
2309
2793
|
return {
|
|
2310
2794
|
commandClass,
|
|
2311
2795
|
automatic: false,
|
|
2312
|
-
refusal: `"${program}" can reach the network. This machine will not run it, and no approval enables it: a command that reads private files and can also send them is the one combination nobody can review by looking at it
|
|
2796
|
+
refusal: (remote ? `"${program} ${remote}" points at another machine, so it can reach the network. ` : `"${program}" can reach the network. `) + "This machine will not run it, and no approval enables it: a command that reads private files and can also send them is the one combination nobody can review by looking at it.",
|
|
2313
2797
|
reason: "reads private data and has a way out"
|
|
2314
2798
|
};
|
|
2315
2799
|
}
|
|
2316
2800
|
if (commandClass === "interpreter") {
|
|
2801
|
+
const language = CLASSES.get(program) === "interpreter";
|
|
2317
2802
|
return {
|
|
2318
2803
|
commandClass,
|
|
2319
2804
|
automatic: false,
|
|
2320
|
-
refusal: `"${program}" runs a language, which is every command at once. Ask for the specific command instead.`,
|
|
2805
|
+
refusal: language ? `"${program}" runs a language, which is every command at once. Ask for the specific command instead.` : `"${describe2(argv)}" runs a program of its own, which is every command at once. Ask for the specific command instead.`,
|
|
2321
2806
|
reason: "an interpreter is not one command"
|
|
2322
2807
|
};
|
|
2323
2808
|
}
|
|
2809
|
+
const endless = endlessFlag(argv);
|
|
2810
|
+
if (endless) {
|
|
2811
|
+
return {
|
|
2812
|
+
commandClass,
|
|
2813
|
+
automatic: false,
|
|
2814
|
+
refusal: `"${program} ${endless}" follows the log and never finishes, and nothing here streams \u2014 the reply is sent when the command exits. Ask for the end of the log instead: \`tail -n 500 <file>\`, \`journalctl -n 500 -u <unit>\`, \`docker logs --tail 500 <container>\`.`,
|
|
2815
|
+
reason: "a follow never produces an answer"
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2324
2818
|
const outside = pathOutsideRoots(argv, policy.roots);
|
|
2325
2819
|
if (outside) {
|
|
2326
2820
|
return {
|
|
@@ -2344,42 +2838,81 @@ function judge(argv, policy) {
|
|
|
2344
2838
|
};
|
|
2345
2839
|
}
|
|
2346
2840
|
function pathOutsideRoots(argv, roots) {
|
|
2841
|
+
const realRoots = roots.map((root) => realLocation(resolvePath(root)));
|
|
2347
2842
|
for (const argument of argv.slice(1)) {
|
|
2348
|
-
|
|
2349
|
-
|
|
2843
|
+
const attached = /^-[A-Za-z]([/~.].*)$/.exec(argument)?.[1];
|
|
2844
|
+
const value = argument.startsWith("-") ? argument.includes("=") ? argument.slice(argument.indexOf("=") + 1) : attached ?? "" : argument;
|
|
2845
|
+
if (argument.startsWith("-") && !argument.includes("=") && attached === void 0) continue;
|
|
2846
|
+
if (!value.startsWith("/") && !value.startsWith("~") && !value.startsWith(".") && !value.includes("/")) {
|
|
2350
2847
|
continue;
|
|
2351
2848
|
}
|
|
2352
|
-
const
|
|
2353
|
-
if (!
|
|
2849
|
+
const real = realLocation(expand(value, roots[0] ?? process.cwd()));
|
|
2850
|
+
if (!realRoots.some((root) => contains(root, real))) {
|
|
2354
2851
|
return argument;
|
|
2355
2852
|
}
|
|
2356
2853
|
}
|
|
2357
2854
|
return void 0;
|
|
2358
2855
|
}
|
|
2856
|
+
function contains(root, path) {
|
|
2857
|
+
const rel = relative2(root, path);
|
|
2858
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2859
|
+
}
|
|
2359
2860
|
function expand(argument, base) {
|
|
2360
2861
|
const home = process.env["HOME"] ?? "";
|
|
2361
2862
|
const withHome = argument.startsWith("~") ? join4(home, argument.slice(1)) : argument;
|
|
2362
2863
|
return isAbsolute2(withHome) ? resolvePath(withHome) : resolvePath(base, withHome);
|
|
2363
2864
|
}
|
|
2364
|
-
|
|
2865
|
+
function refusal(text) {
|
|
2866
|
+
return { ok: false, text, bytes: 0 };
|
|
2867
|
+
}
|
|
2868
|
+
async function runCommand(argv, policy, limits = {}) {
|
|
2869
|
+
const maxOutputBytes = limits.maxOutputBytes ?? MAX_OUTPUT_BYTES;
|
|
2870
|
+
const timeoutMs = limits.timeoutMs ?? TIMEOUT_MS;
|
|
2365
2871
|
const verdict = judge(argv, policy);
|
|
2366
|
-
if (verdict.refusal) return
|
|
2872
|
+
if (verdict.refusal) return refusal(verdict.refusal);
|
|
2367
2873
|
if (policy.mode === "plan") {
|
|
2368
|
-
return
|
|
2874
|
+
return refusal(`Plan mode: this machine did not run \`${describe2(argv)}\`.`);
|
|
2369
2875
|
}
|
|
2370
2876
|
const cwd = policy.roots[0];
|
|
2371
|
-
if (!cwd) return
|
|
2877
|
+
if (!cwd) return refusal("This machine has no folders it may read.");
|
|
2372
2878
|
try {
|
|
2373
|
-
if (!statSync2(cwd).isDirectory()) return
|
|
2879
|
+
if (!statSync2(cwd).isDirectory()) return refusal(`${cwd} is not a folder.`);
|
|
2374
2880
|
} catch {
|
|
2375
|
-
return
|
|
2881
|
+
return refusal(`${cwd} does not exist.`);
|
|
2376
2882
|
}
|
|
2377
|
-
return new Promise((
|
|
2883
|
+
return new Promise((resolve8) => {
|
|
2378
2884
|
const child = spawn2(argv[0], argv.slice(1), {
|
|
2379
2885
|
cwd,
|
|
2380
2886
|
// NO shell. With one, every character a model can produce is a character
|
|
2381
2887
|
// the shell can act on, and the argument list stops meaning anything.
|
|
2382
2888
|
shell: false,
|
|
2889
|
+
/*
|
|
2890
|
+
ITS OWN PROCESS GROUP, so the timeout can kill everything it started.
|
|
2891
|
+
|
|
2892
|
+
A signal to one pid stops one process. `zcat` is a shell wrapper around
|
|
2893
|
+
`gzip` on most systems, a script spawns what it likes, and killing the
|
|
2894
|
+
parent leaves the child running and holding the stdout pipe — which is
|
|
2895
|
+
what `close` waits for, so the promise below waited too, and the loop
|
|
2896
|
+
with it. A negative pid signals the group.
|
|
2897
|
+
|
|
2898
|
+
The cost is that this child no longer sees the Ctrl-C that stops the
|
|
2899
|
+
agent, which is the right way round: the loop finishes the request it
|
|
2900
|
+
is holding, and a half-killed command is not a better answer.
|
|
2901
|
+
|
|
2902
|
+
Not on Windows, which has no process groups to signal; there the
|
|
2903
|
+
backstop below is the whole guarantee.
|
|
2904
|
+
*/
|
|
2905
|
+
detached: process.platform !== "win32",
|
|
2906
|
+
/*
|
|
2907
|
+
NO STDIN, which is a bound as much as the timeout is.
|
|
2908
|
+
|
|
2909
|
+
The default is a pipe nobody ever writes to, so anything that reads
|
|
2910
|
+
standard input — `cat` with no file, `grep` with a pattern and no path,
|
|
2911
|
+
a program that stops to ask something — waited for the full twenty
|
|
2912
|
+
seconds and came back empty, indistinguishable from a hang. There is
|
|
2913
|
+
nobody at a keyboard here. Closed, so those read EOF and exit at once.
|
|
2914
|
+
*/
|
|
2915
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2383
2916
|
/*
|
|
2384
2917
|
A bare environment.
|
|
2385
2918
|
|
|
@@ -2393,126 +2926,87 @@ async function runCommand(argv, policy) {
|
|
|
2393
2926
|
LANG: process.env["LANG"] ?? "C"
|
|
2394
2927
|
}
|
|
2395
2928
|
});
|
|
2396
|
-
|
|
2397
|
-
|
|
2929
|
+
const started = Date.now();
|
|
2930
|
+
const chunks = [];
|
|
2931
|
+
let bytes = 0;
|
|
2932
|
+
let lastOutput;
|
|
2933
|
+
let stopped;
|
|
2934
|
+
let settled = false;
|
|
2935
|
+
let backstop;
|
|
2936
|
+
const kill = () => {
|
|
2937
|
+
try {
|
|
2938
|
+
if (child.pid !== void 0 && process.platform !== "win32") {
|
|
2939
|
+
process.kill(-child.pid, "SIGKILL");
|
|
2940
|
+
} else {
|
|
2941
|
+
child.kill("SIGKILL");
|
|
2942
|
+
}
|
|
2943
|
+
} catch {
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
const finished = (code) => {
|
|
2947
|
+
const elapsed = (Date.now() - started) / 1e3;
|
|
2948
|
+
const notes = [];
|
|
2949
|
+
if (stopped === "output") {
|
|
2950
|
+
notes.push(
|
|
2951
|
+
`[CUT OFF. This is the first ${bytes} bytes and this machine stopped the command there \u2014 there was more, and it is not below. For a log, ask for the end of it instead: \`tail -n 500 <file>\`, \`journalctl -n 500 -u <unit>\`.]`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
if (stopped === "time") {
|
|
2955
|
+
notes.push(
|
|
2956
|
+
`[STOPPED after ${elapsed.toFixed(1)}s. ` + (lastOutput === void 0 ? "It had produced nothing at all in that time" : `It had produced ${bytes} bytes, the last of them ${((lastOutput - started) / 1e3).toFixed(1)}s in`) + ", so this is a fragment of the answer rather than the answer.]"
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
if (code !== null && code !== 0) notes.push(`[exit code ${code}]`);
|
|
2960
|
+
const head = notes.length > 0 ? `${notes.join("\n")}
|
|
2961
|
+
|
|
2962
|
+
` : "";
|
|
2963
|
+
const body = bytes === 0 ? "(no output)\n" : Buffer.concat(chunks).toString("utf8");
|
|
2964
|
+
return {
|
|
2965
|
+
// See `ok` on CommandOutcome: a non-zero exit with something to say is
|
|
2966
|
+
// an answer, and a silent failure is a sentence.
|
|
2967
|
+
ok: bytes > 0 || code === 0,
|
|
2968
|
+
text: `$ ${describe2(argv)}
|
|
2969
|
+
|
|
2970
|
+
${head}${body}`,
|
|
2971
|
+
bytes,
|
|
2972
|
+
...code !== null ? { exitCode: code } : {},
|
|
2973
|
+
...stopped ? { stopped } : {}
|
|
2974
|
+
};
|
|
2975
|
+
};
|
|
2976
|
+
const settle = (outcome) => {
|
|
2977
|
+
if (settled) return;
|
|
2978
|
+
settled = true;
|
|
2979
|
+
clearTimeout(timer);
|
|
2980
|
+
if (backstop) clearTimeout(backstop);
|
|
2981
|
+
resolve8(outcome);
|
|
2982
|
+
};
|
|
2983
|
+
const stop = (why) => {
|
|
2984
|
+
if (stopped) return;
|
|
2985
|
+
stopped = why;
|
|
2986
|
+
kill();
|
|
2987
|
+
backstop = setTimeout(() => settle(finished(null)), AFTER_KILL_MS);
|
|
2988
|
+
};
|
|
2398
2989
|
const collect = (chunk) => {
|
|
2399
|
-
if (
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2990
|
+
if (stopped) return;
|
|
2991
|
+
lastOutput = Date.now();
|
|
2992
|
+
const room = maxOutputBytes - bytes;
|
|
2993
|
+
if (chunk.length > room) {
|
|
2994
|
+
chunks.push(chunk.subarray(0, room));
|
|
2995
|
+
bytes += room;
|
|
2996
|
+
stop("output");
|
|
2997
|
+
return;
|
|
2405
2998
|
}
|
|
2999
|
+
chunks.push(chunk);
|
|
3000
|
+
bytes += chunk.length;
|
|
2406
3001
|
};
|
|
2407
3002
|
child.stdout.on("data", collect);
|
|
2408
3003
|
child.stderr.on("data", collect);
|
|
2409
|
-
const timer = setTimeout(() =>
|
|
2410
|
-
child.on("error", (error) => {
|
|
2411
|
-
|
|
2412
|
-
resolve9({ ok: false, text: `Could not run it: ${error.message}` });
|
|
2413
|
-
});
|
|
2414
|
-
child.on("close", (code) => {
|
|
2415
|
-
clearTimeout(timer);
|
|
2416
|
-
const notes = [
|
|
2417
|
-
truncated ? `
|
|
2418
|
-
|
|
2419
|
-
[output cut off at ${MAX_OUTPUT_BYTES} bytes]` : "",
|
|
2420
|
-
code !== 0 && code !== null ? `
|
|
2421
|
-
|
|
2422
|
-
[exit code ${code}]` : ""
|
|
2423
|
-
].join("");
|
|
2424
|
-
resolve9({ ok: code === 0, text: `$ ${describe2(argv)}
|
|
2425
|
-
|
|
2426
|
-
${output}${notes}` });
|
|
2427
|
-
});
|
|
3004
|
+
const timer = setTimeout(() => stop("time"), timeoutMs);
|
|
3005
|
+
child.on("error", (error) => settle(refusal(`Could not run it: ${error.message}`)));
|
|
3006
|
+
child.on("close", (code) => settle(finished(code)));
|
|
2428
3007
|
});
|
|
2429
3008
|
}
|
|
2430
3009
|
|
|
2431
|
-
// src/commands/agent-config.ts
|
|
2432
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync, statSync as statSync3 } from "node:fs";
|
|
2433
|
-
import { homedir as homedir2 } from "node:os";
|
|
2434
|
-
import { join as join5, resolve as resolve3 } from "node:path";
|
|
2435
|
-
var NOT_MATERIAL = /* @__PURE__ */ new Set([
|
|
2436
|
-
"Library",
|
|
2437
|
-
"Applications",
|
|
2438
|
-
"System",
|
|
2439
|
-
"Public",
|
|
2440
|
-
"node_modules",
|
|
2441
|
-
"go",
|
|
2442
|
-
"Parallels",
|
|
2443
|
-
"VirtualBox VMs"
|
|
2444
|
-
]);
|
|
2445
|
-
function discoverRoots(home = homedir2(), list = (path) => {
|
|
2446
|
-
try {
|
|
2447
|
-
return readdirSync(path);
|
|
2448
|
-
} catch {
|
|
2449
|
-
return [];
|
|
2450
|
-
}
|
|
2451
|
-
}, isDirectory = (path) => {
|
|
2452
|
-
try {
|
|
2453
|
-
return statSync3(path).isDirectory();
|
|
2454
|
-
} catch {
|
|
2455
|
-
return false;
|
|
2456
|
-
}
|
|
2457
|
-
}) {
|
|
2458
|
-
return list(home).filter((name) => !name.startsWith(".")).filter((name) => !NOT_MATERIAL.has(name)).map((name) => join5(home, name)).filter(isDirectory).sort();
|
|
2459
|
-
}
|
|
2460
|
-
function expandHome(path, home = homedir2()) {
|
|
2461
|
-
return path.startsWith("~") ? resolve3(home, path.slice(1).replace(/^[/\\]/, "")) : resolve3(path);
|
|
2462
|
-
}
|
|
2463
|
-
var MODES = /* @__PURE__ */ new Set(["ask", "auto-read", "plan"]);
|
|
2464
|
-
function readAgentConfig(path, read2 = (at) => readFileSync5(at, "utf8"), exists = (at) => existsSync5(at), home = homedir2()) {
|
|
2465
|
-
if (!exists(path)) return {};
|
|
2466
|
-
let parsed;
|
|
2467
|
-
try {
|
|
2468
|
-
parsed = JSON.parse(read2(path));
|
|
2469
|
-
} catch (error) {
|
|
2470
|
-
return { error: `${path} is not valid JSON: ${error instanceof Error ? error.message : ""}` };
|
|
2471
|
-
}
|
|
2472
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2473
|
-
return { error: `${path} should hold an object, like {"roots": ["~/code"]}.` };
|
|
2474
|
-
}
|
|
2475
|
-
const held = parsed;
|
|
2476
|
-
const config = {};
|
|
2477
|
-
if (held["roots"] !== void 0) {
|
|
2478
|
-
if (!Array.isArray(held["roots"]) || held["roots"].some((one) => typeof one !== "string")) {
|
|
2479
|
-
return { error: `${path}: "roots" should be a list of folders.` };
|
|
2480
|
-
}
|
|
2481
|
-
config.roots = held["roots"].map((one) => expandHome(one, home));
|
|
2482
|
-
}
|
|
2483
|
-
if (held["mode"] !== void 0) {
|
|
2484
|
-
if (typeof held["mode"] !== "string" || !MODES.has(held["mode"])) {
|
|
2485
|
-
return { error: `${path}: "mode" should be "ask", "auto-read" or "plan".` };
|
|
2486
|
-
}
|
|
2487
|
-
config.mode = held["mode"];
|
|
2488
|
-
}
|
|
2489
|
-
for (const key of ["allow", "deny"]) {
|
|
2490
|
-
if (held[key] === void 0) continue;
|
|
2491
|
-
if (!Array.isArray(held[key]) || held[key].some((one) => typeof one !== "string")) {
|
|
2492
|
-
return { error: `${path}: "${key}" should be a list of program names.` };
|
|
2493
|
-
}
|
|
2494
|
-
config[key] = held[key];
|
|
2495
|
-
}
|
|
2496
|
-
return { config };
|
|
2497
|
-
}
|
|
2498
|
-
function resolveAgentConfig(args) {
|
|
2499
|
-
const roots = args.flagRoots && args.flagRoots.length > 0 ? args.flagRoots : args.stored?.roots && args.stored.roots.length > 0 ? args.stored.roots : args.discovered ?? [];
|
|
2500
|
-
return {
|
|
2501
|
-
roots,
|
|
2502
|
-
/*
|
|
2503
|
-
`ask` unless the owner said otherwise.
|
|
2504
|
-
|
|
2505
|
-
The mode decides how much happens with nobody watching, so the default
|
|
2506
|
-
is the one that does least. `auto-read` is a reasonable thing to choose
|
|
2507
|
-
and an unreasonable thing to be given.
|
|
2508
|
-
*/
|
|
2509
|
-
mode: args.stored?.mode ?? "ask",
|
|
2510
|
-
allow: args.stored?.allow ?? [],
|
|
2511
|
-
deny: args.stored?.deny ?? [],
|
|
2512
|
-
source: args.flagRoots && args.flagRoots.length > 0 ? "flags" : args.stored?.roots && args.stored.roots.length > 0 ? "config" : "discovered"
|
|
2513
|
-
};
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
3010
|
// src/commands/agent.ts
|
|
2517
3011
|
async function said(response, fallback) {
|
|
2518
3012
|
const body = await response.json().catch(() => void 0);
|
|
@@ -2520,7 +3014,7 @@ async function said(response, fallback) {
|
|
|
2520
3014
|
return message2 ?? `${fallback} (${response.status})`;
|
|
2521
3015
|
}
|
|
2522
3016
|
function locate(roots, requested) {
|
|
2523
|
-
const expanded = requested.startsWith("~") ?
|
|
3017
|
+
const expanded = requested.startsWith("~") ? resolve3(homedir2(), requested.slice(1).replace(/^[/\\]/, "")) : requested;
|
|
2524
3018
|
if (!expanded.includes("/") && !expanded.includes("\\")) {
|
|
2525
3019
|
const wanted2 = expanded.trim().toLowerCase();
|
|
2526
3020
|
const named = roots.find((root) => basename(root).toLowerCase() === wanted2);
|
|
@@ -2536,29 +3030,23 @@ function locate(roots, requested) {
|
|
|
2536
3030
|
throw new Error(`${requested} is not inside any allowed folder (${roots.join(", ")})`);
|
|
2537
3031
|
}
|
|
2538
3032
|
function uncontested(target) {
|
|
2539
|
-
if (!
|
|
3033
|
+
if (!existsSync5(target)) return target;
|
|
2540
3034
|
const dot = target.lastIndexOf(".");
|
|
2541
3035
|
const stem = dot > target.lastIndexOf("/") && dot !== -1 ? target.slice(0, dot) : target;
|
|
2542
3036
|
const extension = stem === target ? "" : target.slice(dot);
|
|
2543
3037
|
for (let n = 1; n < 1e3; n += 1) {
|
|
2544
3038
|
const candidate = `${stem} (${n})${extension}`;
|
|
2545
|
-
if (!
|
|
3039
|
+
if (!existsSync5(candidate)) return candidate;
|
|
2546
3040
|
}
|
|
2547
3041
|
throw new Error(`${target} and a thousand names beside it are taken.`);
|
|
2548
3042
|
}
|
|
2549
|
-
async function answer(context, apiUrl, token,
|
|
2550
|
-
const roots = settings.roots;
|
|
3043
|
+
async function answer(context, apiUrl, token, roots, request) {
|
|
2551
3044
|
let located;
|
|
2552
3045
|
if (request.kind === "run_command") {
|
|
2553
3046
|
if (!request.argv || request.argv.length === 0) {
|
|
2554
3047
|
return { ok: false, error: "No command was given." };
|
|
2555
3048
|
}
|
|
2556
|
-
const policy = {
|
|
2557
|
-
mode: settings.mode,
|
|
2558
|
-
allow: settings.allow,
|
|
2559
|
-
deny: settings.deny,
|
|
2560
|
-
roots
|
|
2561
|
-
};
|
|
3049
|
+
const policy = { mode: "ask", allow: [], deny: [], roots };
|
|
2562
3050
|
const outcome = await runCommand(request.argv, policy);
|
|
2563
3051
|
if (!outcome.ok) return { ok: false, error: outcome.text };
|
|
2564
3052
|
return upload(
|
|
@@ -2588,10 +3076,10 @@ async function answer(context, apiUrl, token, settings, request) {
|
|
|
2588
3076
|
}
|
|
2589
3077
|
try {
|
|
2590
3078
|
let target = located;
|
|
2591
|
-
if (
|
|
3079
|
+
if (existsSync5(located) && statSync3(located).isDirectory()) {
|
|
2592
3080
|
const disposition = fetched.headers.get("content-disposition") ?? "";
|
|
2593
3081
|
const named = /filename="([^"]+)"/.exec(disposition)?.[1];
|
|
2594
|
-
target =
|
|
3082
|
+
target = join5(located, basename(named ?? "file"));
|
|
2595
3083
|
}
|
|
2596
3084
|
target = uncontested(target);
|
|
2597
3085
|
writeFileSync4(target, downloaded, { flag: "wx" });
|
|
@@ -2614,21 +3102,9 @@ ${downloaded.length} bytes
|
|
|
2614
3102
|
let filename = request.path.split("/").pop() ?? "file";
|
|
2615
3103
|
if (request.kind === "list_dir") {
|
|
2616
3104
|
try {
|
|
2617
|
-
const stats =
|
|
3105
|
+
const stats = statSync3(located);
|
|
2618
3106
|
if (!stats.isDirectory()) return { ok: false, error: `${request.path} is not a folder.` };
|
|
2619
|
-
|
|
2620
|
-
if (entry.isDirectory()) return `${entry.name}/`;
|
|
2621
|
-
try {
|
|
2622
|
-
return `${entry.name} ${sizeOf(join6(located, entry.name))}`;
|
|
2623
|
-
} catch {
|
|
2624
|
-
return entry.name;
|
|
2625
|
-
}
|
|
2626
|
-
}).sort();
|
|
2627
|
-
const listing = entries.length > 0 ? entries.join("\n") : "(empty)";
|
|
2628
|
-
bytes = Buffer.from(`${request.path}
|
|
2629
|
-
|
|
2630
|
-
${listing}
|
|
2631
|
-
`, "utf8");
|
|
3107
|
+
bytes = Buffer.from(folderListing(request.path, located), "utf8");
|
|
2632
3108
|
filename = `${request.path.split("/").filter(Boolean).pop() ?? "listing"}.txt`;
|
|
2633
3109
|
const grant = await upload(apiUrl, token, filename, bytes);
|
|
2634
3110
|
return grant;
|
|
@@ -2637,9 +3113,9 @@ ${listing}
|
|
|
2637
3113
|
}
|
|
2638
3114
|
}
|
|
2639
3115
|
try {
|
|
2640
|
-
const stats =
|
|
3116
|
+
const stats = statSync3(located);
|
|
2641
3117
|
if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
|
|
2642
|
-
bytes =
|
|
3118
|
+
bytes = readFileSync5(located);
|
|
2643
3119
|
} catch (error) {
|
|
2644
3120
|
return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
|
|
2645
3121
|
}
|
|
@@ -2680,8 +3156,27 @@ async function upload(apiUrl, token, filename, bytes) {
|
|
|
2680
3156
|
return { ok: true, attachToken: stored.attachToken, bytes: bytes.length };
|
|
2681
3157
|
}
|
|
2682
3158
|
var MAX_LISTED = 200;
|
|
3159
|
+
function folderListing(requested, located) {
|
|
3160
|
+
const all = readdirSync(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name));
|
|
3161
|
+
const shown = all.slice(0, MAX_LISTED).map((entry) => {
|
|
3162
|
+
if (entry.isDirectory()) return `${entry.name}/`;
|
|
3163
|
+
try {
|
|
3164
|
+
return `${entry.name} ${sizeOf(join5(located, entry.name))}`;
|
|
3165
|
+
} catch {
|
|
3166
|
+
return entry.name;
|
|
3167
|
+
}
|
|
3168
|
+
});
|
|
3169
|
+
const listing = shown.length > 0 ? shown.join("\n") : "(empty)";
|
|
3170
|
+
const rest = all.length > shown.length ? `
|
|
3171
|
+
|
|
3172
|
+
[\u2026${all.length - shown.length} more entries not listed: this is the first ${MAX_LISTED} by name, not the whole folder]` : "";
|
|
3173
|
+
return `${requested}
|
|
3174
|
+
|
|
3175
|
+
${listing}${rest}
|
|
3176
|
+
`;
|
|
3177
|
+
}
|
|
2683
3178
|
function sizeOf(path) {
|
|
2684
|
-
const size =
|
|
3179
|
+
const size = statSync3(path).size;
|
|
2685
3180
|
if (size < 1024) return `${size} B`;
|
|
2686
3181
|
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
|
2687
3182
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -2693,41 +3188,16 @@ async function agentCommand(context) {
|
|
|
2693
3188
|
return 1;
|
|
2694
3189
|
}
|
|
2695
3190
|
const roots = (listFlag(context.args, "root") ?? []).map(
|
|
2696
|
-
(one) =>
|
|
3191
|
+
(one) => resolve3(one.startsWith("~") ? resolve3(homedir2(), one.slice(1).replace(/^[/\\]/, "")) : one)
|
|
2697
3192
|
);
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
if (stored.error) {
|
|
2701
|
-
context.error(stored.error);
|
|
2702
|
-
return 1;
|
|
2703
|
-
}
|
|
2704
|
-
const settings = resolveAgentConfig({
|
|
2705
|
-
flagRoots: roots,
|
|
2706
|
-
...stored.config ? { stored: stored.config } : {},
|
|
2707
|
-
discovered: discoverRoots()
|
|
2708
|
-
});
|
|
2709
|
-
if (settings.roots.length === 0) {
|
|
2710
|
-
context.error(
|
|
2711
|
-
"There is nothing in your home directory this machine would offer, so say which folders it may read from:"
|
|
2712
|
-
);
|
|
2713
|
-
context.error("");
|
|
2714
|
-
context.error(" pm agent --root ~/projects");
|
|
2715
|
-
context.error("");
|
|
2716
|
-
context.error(`Or write them once in ${configPath}:`);
|
|
2717
|
-
context.error("");
|
|
2718
|
-
context.error(' { "roots": ["~/projects"], "mode": "ask" }');
|
|
2719
|
-
return 1;
|
|
2720
|
-
}
|
|
2721
|
-
roots.length = 0;
|
|
2722
|
-
roots.push(...settings.roots);
|
|
2723
|
-
const shown = settings.roots.map((path) => path.replace(homedir3(), "~"));
|
|
2724
|
-
const where = settings.source === "flags" ? "" : settings.source === "config" ? ` (from ${configPath})` : " (found in your home directory)";
|
|
3193
|
+
const everywhere = roots.length === 0;
|
|
3194
|
+
if (everywhere) roots.push("/");
|
|
2725
3195
|
context.print(
|
|
2726
|
-
`Reading ${
|
|
3196
|
+
everywhere ? "Reading anywhere on this machine. You approve every request first, and see the exact path or command before you do. Narrow it with --root if you want to." : `Reading ${roots.map((path) => path.replace(homedir2(), "~")).join(", ")} \u2014 nothing outside them.`
|
|
2727
3197
|
);
|
|
2728
3198
|
for (const root of roots) {
|
|
2729
3199
|
try {
|
|
2730
|
-
if (!
|
|
3200
|
+
if (!statSync3(root).isDirectory()) {
|
|
2731
3201
|
context.error(`${root} is not a folder.`);
|
|
2732
3202
|
return 1;
|
|
2733
3203
|
}
|
|
@@ -2800,8 +3270,10 @@ async function agentCommand(context) {
|
|
|
2800
3270
|
} else {
|
|
2801
3271
|
const { items } = await claimed.json();
|
|
2802
3272
|
for (const request of items) {
|
|
2803
|
-
context.print(
|
|
2804
|
-
|
|
3273
|
+
context.print(
|
|
3274
|
+
request.kind === "run_command" ? `Running ${request.path}` : `Reading ${request.path}`
|
|
3275
|
+
);
|
|
3276
|
+
const outcome = await answer(context, apiUrl, await authorization(), roots, request);
|
|
2805
3277
|
const done = await call(
|
|
2806
3278
|
`complete/${encodeURIComponent(request.id)}`,
|
|
2807
3279
|
outcome.ok ? { result: { attachToken: outcome.attachToken } } : { error: outcome.error }
|
|
@@ -2823,8 +3295,8 @@ async function agentCommand(context) {
|
|
|
2823
3295
|
|
|
2824
3296
|
// src/commands/google.ts
|
|
2825
3297
|
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
2826
|
-
import { basename as basename2, resolve as
|
|
2827
|
-
import { readFileSync as
|
|
3298
|
+
import { basename as basename2, resolve as resolve4 } from "node:path";
|
|
3299
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2828
3300
|
async function callApi(context, path, init = {}) {
|
|
2829
3301
|
const credential = context.resolved.credential;
|
|
2830
3302
|
if (!credential) {
|
|
@@ -2889,7 +3361,7 @@ async function driveGet(context, fileId) {
|
|
|
2889
3361
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
2890
3362
|
const named = /filename="([^"]+)"/.exec(disposition)?.[1];
|
|
2891
3363
|
const out = stringFlag(context.args, "out");
|
|
2892
|
-
const target =
|
|
3364
|
+
const target = resolve4(out ?? basename2(named ?? fileId));
|
|
2893
3365
|
writeFileSync5(target, Buffer.from(await response.arrayBuffer()));
|
|
2894
3366
|
context.print(target);
|
|
2895
3367
|
return 0;
|
|
@@ -2901,7 +3373,7 @@ async function drivePut(context, path) {
|
|
|
2901
3373
|
}
|
|
2902
3374
|
let bytes;
|
|
2903
3375
|
try {
|
|
2904
|
-
bytes =
|
|
3376
|
+
bytes = readFileSync6(resolve4(path));
|
|
2905
3377
|
} catch {
|
|
2906
3378
|
context.error(`Cannot read ${path}.`);
|
|
2907
3379
|
return 1;
|
|
@@ -2985,8 +3457,8 @@ async function mailCommand(context) {
|
|
|
2985
3457
|
}
|
|
2986
3458
|
|
|
2987
3459
|
// src/commands/requests.ts
|
|
2988
|
-
import { existsSync as
|
|
2989
|
-
import { resolve as
|
|
3460
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3461
|
+
import { basename as basename3, resolve as resolve5 } from "node:path";
|
|
2990
3462
|
async function requestsCommand(context) {
|
|
2991
3463
|
if (context.args.words[1] === "get") return collectCommand(context);
|
|
2992
3464
|
const credential = context.resolved.credential;
|
|
@@ -3053,9 +3525,8 @@ async function collectCommand(context) {
|
|
|
3053
3525
|
context.error(`The download refused it (${file.status}). Links expire in minutes \u2014 try again.`);
|
|
3054
3526
|
return 1;
|
|
3055
3527
|
}
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
if (existsSync7(target)) {
|
|
3528
|
+
const target = downloadTarget(filename, stringFlag(context.args, "output", "o"));
|
|
3529
|
+
if (existsSync6(target)) {
|
|
3059
3530
|
context.error(`${target} already exists. Pass --output to write somewhere else.`);
|
|
3060
3531
|
return 1;
|
|
3061
3532
|
}
|
|
@@ -3063,16 +3534,19 @@ async function collectCommand(context) {
|
|
|
3063
3534
|
context.print(`Wrote ${target}`);
|
|
3064
3535
|
return 0;
|
|
3065
3536
|
}
|
|
3537
|
+
function downloadTarget(filename, output) {
|
|
3538
|
+
return resolve5(output ?? (basename3(filename) || "file"));
|
|
3539
|
+
}
|
|
3066
3540
|
|
|
3067
3541
|
// src/workspace.ts
|
|
3068
|
-
import { existsSync as
|
|
3069
|
-
import { dirname as dirname3, join as
|
|
3542
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3543
|
+
import { dirname as dirname3, join as join6, resolve as resolvePath2 } from "node:path";
|
|
3070
3544
|
var WORKSPACE_FILE = ".persistmemory.json";
|
|
3071
3545
|
function findWorkspace(from = process.cwd()) {
|
|
3072
3546
|
let dir = resolvePath2(from);
|
|
3073
3547
|
for (; ; ) {
|
|
3074
|
-
const file =
|
|
3075
|
-
if (
|
|
3548
|
+
const file = join6(dir, WORKSPACE_FILE);
|
|
3549
|
+
if (existsSync7(file)) {
|
|
3076
3550
|
const config = readWorkspace(file);
|
|
3077
3551
|
if (config) return { file, dir, config };
|
|
3078
3552
|
}
|
|
@@ -3083,7 +3557,7 @@ function findWorkspace(from = process.cwd()) {
|
|
|
3083
3557
|
}
|
|
3084
3558
|
function readWorkspace(file) {
|
|
3085
3559
|
try {
|
|
3086
|
-
const parsed = JSON.parse(
|
|
3560
|
+
const parsed = JSON.parse(readFileSync7(file, "utf8"));
|
|
3087
3561
|
const space = parsed.space;
|
|
3088
3562
|
if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
|
|
3089
3563
|
return { space: { id: space.id, name: space.name } };
|
|
@@ -3094,7 +3568,7 @@ function readWorkspace(file) {
|
|
|
3094
3568
|
}
|
|
3095
3569
|
}
|
|
3096
3570
|
function writeWorkspace(dir, config) {
|
|
3097
|
-
const file =
|
|
3571
|
+
const file = join6(dir, WORKSPACE_FILE);
|
|
3098
3572
|
writeFileSync7(file, `${JSON.stringify(config, null, 2)}
|
|
3099
3573
|
`, "utf8");
|
|
3100
3574
|
return file;
|
|
@@ -3364,9 +3838,9 @@ function message(error) {
|
|
|
3364
3838
|
}
|
|
3365
3839
|
|
|
3366
3840
|
// src/commands/maintain.ts
|
|
3367
|
-
import { existsSync as
|
|
3841
|
+
import { existsSync as existsSync8, rmSync } from "node:fs";
|
|
3368
3842
|
import { spawnSync } from "node:child_process";
|
|
3369
|
-
import { dirname as dirname4, resolve as
|
|
3843
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
3370
3844
|
import { fileURLToPath } from "node:url";
|
|
3371
3845
|
function installArgs(pkg = PACKAGE) {
|
|
3372
3846
|
return ["install", "-g", "--prefer-online", `${pkg}@latest`];
|
|
@@ -3430,7 +3904,7 @@ Delete the file it runs from: ${processPath()}`
|
|
|
3430
3904
|
return 0;
|
|
3431
3905
|
}
|
|
3432
3906
|
async function deleteCommand(context) {
|
|
3433
|
-
if (!
|
|
3907
|
+
if (!existsSync8(context.paths.dir)) {
|
|
3434
3908
|
context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
|
|
3435
3909
|
return 0;
|
|
3436
3910
|
}
|
|
@@ -3459,7 +3933,7 @@ async function deleteCommand(context) {
|
|
|
3459
3933
|
return 0;
|
|
3460
3934
|
}
|
|
3461
3935
|
function removeEverything(context) {
|
|
3462
|
-
const dir =
|
|
3936
|
+
const dir = resolve7(context.paths.dir);
|
|
3463
3937
|
if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
|
|
3464
3938
|
context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
|
|
3465
3939
|
return;
|
|
@@ -3471,7 +3945,7 @@ function installer() {
|
|
|
3471
3945
|
}
|
|
3472
3946
|
function processPath() {
|
|
3473
3947
|
try {
|
|
3474
|
-
return
|
|
3948
|
+
return resolve7(dirname4(fileURLToPath(import.meta.url)));
|
|
3475
3949
|
} catch {
|
|
3476
3950
|
return process.argv[1] ?? "";
|
|
3477
3951
|
}
|
|
@@ -3480,15 +3954,15 @@ function processPath() {
|
|
|
3480
3954
|
// src/commands/session.ts
|
|
3481
3955
|
import { createInterface as createInterface2 } from "node:readline";
|
|
3482
3956
|
import { randomUUID } from "node:crypto";
|
|
3483
|
-
import { relative as
|
|
3957
|
+
import { relative as relative3 } from "node:path";
|
|
3484
3958
|
|
|
3485
3959
|
// src/events.ts
|
|
3486
|
-
import { appendFileSync, existsSync as
|
|
3487
|
-
import { join as
|
|
3960
|
+
import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8 } from "node:fs";
|
|
3961
|
+
import { join as join7 } from "node:path";
|
|
3488
3962
|
function openSessionLog(paths, id) {
|
|
3489
|
-
const directory =
|
|
3963
|
+
const directory = join7(paths.dir, "sessions");
|
|
3490
3964
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
3491
|
-
const path =
|
|
3965
|
+
const path = join7(directory, `${id}.jsonl`);
|
|
3492
3966
|
return {
|
|
3493
3967
|
id,
|
|
3494
3968
|
path,
|
|
@@ -3500,8 +3974,8 @@ function openSessionLog(paths, id) {
|
|
|
3500
3974
|
}
|
|
3501
3975
|
},
|
|
3502
3976
|
read() {
|
|
3503
|
-
if (!
|
|
3504
|
-
return
|
|
3977
|
+
if (!existsSync9(path)) return [];
|
|
3978
|
+
return readFileSync8(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
3505
3979
|
try {
|
|
3506
3980
|
return [JSON.parse(line)];
|
|
3507
3981
|
} catch {
|
|
@@ -3578,9 +4052,9 @@ async function sessionCommand(context) {
|
|
|
3578
4052
|
context.print(` Ask anything. /help for commands, /exit to leave.
|
|
3579
4053
|
`);
|
|
3580
4054
|
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3581
|
-
const ask = (prompt) => new Promise((
|
|
3582
|
-
readline.question(prompt,
|
|
3583
|
-
readline.once("close", () =>
|
|
4055
|
+
const ask = (prompt) => new Promise((resolve8) => {
|
|
4056
|
+
readline.question(prompt, resolve8);
|
|
4057
|
+
readline.once("close", () => resolve8(void 0));
|
|
3584
4058
|
});
|
|
3585
4059
|
const root = process.cwd();
|
|
3586
4060
|
let running = true;
|
|
@@ -3652,11 +4126,11 @@ async function handleInput(args) {
|
|
|
3652
4126
|
path: file.path,
|
|
3653
4127
|
bytes: file.bytes
|
|
3654
4128
|
});
|
|
3655
|
-
context.print(` read ${
|
|
4129
|
+
context.print(` read ${relative3(root, file.path)} (${Math.round(file.bytes / 1024)} KB)`);
|
|
3656
4130
|
if (command === "capture") {
|
|
3657
4131
|
const client = await context.client();
|
|
3658
4132
|
const result = await client.memories.remember(
|
|
3659
|
-
{ text: file.text, title:
|
|
4133
|
+
{ text: file.text, title: relative3(root, file.path) },
|
|
3660
4134
|
{ idempotencyKey: `cli:capture:${file.path}:${file.bytes}` }
|
|
3661
4135
|
);
|
|
3662
4136
|
log.append({
|
|
@@ -3760,11 +4234,189 @@ async function write2(args) {
|
|
|
3760
4234
|
return;
|
|
3761
4235
|
}
|
|
3762
4236
|
commitWrite(proposed, true);
|
|
3763
|
-
context.print(` Wrote ${
|
|
4237
|
+
context.print(` Wrote ${relative3(root, proposed.path)}.`);
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
// src/commands/sharing.ts
|
|
4241
|
+
var collaboratorColumns = [
|
|
4242
|
+
{ header: "email", value: (one) => one.email },
|
|
4243
|
+
{ header: "name", value: (one) => one.name ?? "" },
|
|
4244
|
+
{ header: "role", value: (one) => one.role },
|
|
4245
|
+
// The column that matters. See `standing`.
|
|
4246
|
+
{ header: "access", value: (one) => standing(one) },
|
|
4247
|
+
{ header: "invited by", value: (one) => one.invitedBy ?? "" }
|
|
4248
|
+
];
|
|
4249
|
+
function standing(one) {
|
|
4250
|
+
return one.acceptedAt ? `yes, since ${one.acceptedAt.slice(0, 10)}` : "no \u2014 invitation not accepted";
|
|
4251
|
+
}
|
|
4252
|
+
function sharingArgs(context) {
|
|
4253
|
+
const words = context.args.words.slice(2);
|
|
4254
|
+
const at = words.findIndex((one) => one.includes("@"));
|
|
4255
|
+
const email = at >= 0 ? words[at] ?? "" : "";
|
|
4256
|
+
const named = at > 0 ? words.slice(0, at).join(" ") : "";
|
|
4257
|
+
const trailing = at >= 0 ? words[at + 1] : void 0;
|
|
4258
|
+
return { named, email, role: stringFlag(context.args, "role") ?? trailing };
|
|
4259
|
+
}
|
|
4260
|
+
var ROLES = ["viewer", "editor"];
|
|
4261
|
+
function isRole(value) {
|
|
4262
|
+
return value !== void 0 && ROLES.includes(value);
|
|
4263
|
+
}
|
|
4264
|
+
async function resolveSpace(context, named) {
|
|
4265
|
+
const client = await context.client();
|
|
4266
|
+
const { data } = await client.spaces.list({ limit: 200 }).first();
|
|
4267
|
+
if (named.startsWith("space_")) {
|
|
4268
|
+
const byId = data.find((one) => one.id === named);
|
|
4269
|
+
if (byId) return byId;
|
|
4270
|
+
context.error(`No Space with id ${named}. Run \`pm spaces list\` to see yours.`);
|
|
4271
|
+
return 1;
|
|
4272
|
+
}
|
|
4273
|
+
const wanted2 = named.trim().toLowerCase();
|
|
4274
|
+
const matches = data.filter((one) => one.name.trim().toLowerCase() === wanted2);
|
|
4275
|
+
const only = matches[0];
|
|
4276
|
+
if (matches.length === 1 && only) return only;
|
|
4277
|
+
if (matches.length === 0) {
|
|
4278
|
+
context.error(`No Space called "${named}". Run \`pm spaces list\` to see yours.`);
|
|
4279
|
+
return 1;
|
|
4280
|
+
}
|
|
4281
|
+
context.error(
|
|
4282
|
+
`More than one Space is called "${named}": ${matches.map((one) => one.id).join(", ")}.`
|
|
4283
|
+
);
|
|
4284
|
+
context.error("Name it by id \u2014 sharing the wrong one gives somebody the wrong memories.");
|
|
4285
|
+
return 1;
|
|
4286
|
+
}
|
|
4287
|
+
async function shareSpaceCommand(context) {
|
|
4288
|
+
const { named, email, role } = sharingArgs(context);
|
|
4289
|
+
if (named === "" || email === "") {
|
|
4290
|
+
context.error("Which Space, and who?");
|
|
4291
|
+
context.error("");
|
|
4292
|
+
context.error(' pm spaces share "Work" priya@example.com --role viewer');
|
|
4293
|
+
return 2;
|
|
4294
|
+
}
|
|
4295
|
+
if (!isRole(role)) {
|
|
4296
|
+
context.error("Say what they may do \u2014 there is no default:");
|
|
4297
|
+
context.error("");
|
|
4298
|
+
context.error(` pm spaces share "${named}" ${email} --role viewer read everything in it`);
|
|
4299
|
+
context.error(` pm spaces share "${named}" ${email} --role editor read it, and add to it`);
|
|
4300
|
+
if (role !== void 0) {
|
|
4301
|
+
context.error("");
|
|
4302
|
+
context.error(
|
|
4303
|
+
role.toLowerCase() === "owner" ? "A Space cannot be handed over. Its owner is whoever created it." : `Roles are viewer and editor. "${role}" is neither.`
|
|
4304
|
+
);
|
|
4305
|
+
}
|
|
4306
|
+
return 2;
|
|
4307
|
+
}
|
|
4308
|
+
const space = await resolveSpace(context, named);
|
|
4309
|
+
if (typeof space === "number") return space;
|
|
4310
|
+
const client = await context.client();
|
|
4311
|
+
const invitation = await client.spaces.share(
|
|
4312
|
+
space.id,
|
|
4313
|
+
{ email, role },
|
|
4314
|
+
{
|
|
4315
|
+
/*
|
|
4316
|
+
Derived from what is being shared, not random.
|
|
4317
|
+
|
|
4318
|
+
The SDK will not retry a POST without a key, and a random one per
|
|
4319
|
+
attempt would defeat the point: a share that timed out after the
|
|
4320
|
+
server recorded it would send a second invitation to a real person's
|
|
4321
|
+
inbox. Same Space, same address, one invitation.
|
|
4322
|
+
*/
|
|
4323
|
+
idempotencyKey: `cli:share:${space.id}:${email}`
|
|
4324
|
+
}
|
|
4325
|
+
);
|
|
4326
|
+
if (context.flags.output !== "table") {
|
|
4327
|
+
context.print(renderOne(invitation, collaboratorColumns, { format: context.flags.output }));
|
|
4328
|
+
return 0;
|
|
4329
|
+
}
|
|
4330
|
+
context.print(`Invited ${invitation.email} to "${space.name}" as ${invitation.role}.`);
|
|
4331
|
+
context.print("");
|
|
4332
|
+
context.print(
|
|
4333
|
+
`They will see everything filed in "${space.name}" \u2014 including memories added later.`
|
|
4334
|
+
);
|
|
4335
|
+
context.print("Nothing is shared yet: they have to accept the invitation first.");
|
|
4336
|
+
if (invitation.role === "owner") {
|
|
4337
|
+
context.print("");
|
|
4338
|
+
context.print("As an owner they can share it onward and revoke anybody, including you.");
|
|
4339
|
+
}
|
|
4340
|
+
context.print("");
|
|
4341
|
+
context.print(`Undo it with: pm spaces unshare "${space.name}" ${invitation.email}`);
|
|
4342
|
+
return 0;
|
|
4343
|
+
}
|
|
4344
|
+
async function unshareSpaceCommand(context) {
|
|
4345
|
+
const { named, email } = sharingArgs(context);
|
|
4346
|
+
if (named === "" || email === "") {
|
|
4347
|
+
context.error("Which Space, and who?");
|
|
4348
|
+
context.error("");
|
|
4349
|
+
context.error(' pm spaces unshare "Work" priya@example.com');
|
|
4350
|
+
return 2;
|
|
4351
|
+
}
|
|
4352
|
+
const space = await resolveSpace(context, named);
|
|
4353
|
+
if (typeof space === "number") return space;
|
|
4354
|
+
const client = await context.client();
|
|
4355
|
+
const { email: ended } = await client.spaces.unshare(space.id, email);
|
|
4356
|
+
if (context.flags.output !== "table") {
|
|
4357
|
+
context.print(JSON.stringify({ spaceId: space.id, email: ended }, void 0, 2));
|
|
4358
|
+
return 0;
|
|
4359
|
+
}
|
|
4360
|
+
context.print(`${ended} can no longer see "${space.name}".`);
|
|
4361
|
+
context.print("Nothing of it was ever copied into their account, so nothing of it remains.");
|
|
4362
|
+
return 0;
|
|
4363
|
+
}
|
|
4364
|
+
async function spaceRoleCommand(context) {
|
|
4365
|
+
const { named, email, role } = sharingArgs(context);
|
|
4366
|
+
if (named === "" || email === "" || !isRole(role)) {
|
|
4367
|
+
context.error("Which Space, who, and what to:");
|
|
4368
|
+
context.error("");
|
|
4369
|
+
context.error(' pm spaces role "Work" priya@example.com editor');
|
|
4370
|
+
context.error("");
|
|
4371
|
+
context.error(`Roles: ${ROLES.join(", ")}. This changes somebody who already has access \u2014`);
|
|
4372
|
+
context.error("use `pm spaces share` to invite a new person.");
|
|
4373
|
+
return 2;
|
|
4374
|
+
}
|
|
4375
|
+
const space = await resolveSpace(context, named);
|
|
4376
|
+
if (typeof space === "number") return space;
|
|
4377
|
+
const client = await context.client();
|
|
4378
|
+
const changed = await client.spaces.setRole(space.id, { email, role });
|
|
4379
|
+
if (context.flags.output !== "table") {
|
|
4380
|
+
context.print(renderOne(changed, collaboratorColumns, { format: context.flags.output }));
|
|
4381
|
+
return 0;
|
|
4382
|
+
}
|
|
4383
|
+
context.print(`${changed.email} is now ${changed.role} on "${space.name}".`);
|
|
4384
|
+
if (changed.role === "owner") {
|
|
4385
|
+
context.print("As an owner they can share it onward and revoke anybody, including you.");
|
|
4386
|
+
}
|
|
4387
|
+
return 0;
|
|
4388
|
+
}
|
|
4389
|
+
async function spaceSharingCommand(context) {
|
|
4390
|
+
const named = context.args.words.slice(2).join(" ").trim();
|
|
4391
|
+
if (named === "") {
|
|
4392
|
+
context.error('Which Space? Try `pm spaces sharing "Work"`.');
|
|
4393
|
+
return 2;
|
|
4394
|
+
}
|
|
4395
|
+
const space = await resolveSpace(context, named);
|
|
4396
|
+
if (typeof space === "number") return space;
|
|
4397
|
+
const client = await context.client();
|
|
4398
|
+
const { data } = await client.spaces.collaborators(space.id, { limit: 200 }).first();
|
|
4399
|
+
if (context.flags.output !== "table") {
|
|
4400
|
+
context.print(render(data, collaboratorColumns, { format: context.flags.output }));
|
|
4401
|
+
return 0;
|
|
4402
|
+
}
|
|
4403
|
+
if (data.length === 0) {
|
|
4404
|
+
context.print(`Nobody else can see "${space.name}". It has never been shared.`);
|
|
4405
|
+
return 0;
|
|
4406
|
+
}
|
|
4407
|
+
context.print(render(data, collaboratorColumns, { format: context.flags.output }));
|
|
4408
|
+
const waiting = data.filter((one) => one.acceptedAt === void 0).length;
|
|
4409
|
+
if (waiting > 0) {
|
|
4410
|
+
context.print("");
|
|
4411
|
+
context.print(
|
|
4412
|
+
`${waiting} ${waiting === 1 ? "invitation has" : "invitations have"} not been accepted. ${waiting === 1 ? "That person can" : "Those people can"} see nothing yet.`
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
return 0;
|
|
3764
4416
|
}
|
|
3765
4417
|
|
|
3766
4418
|
// src/commands/memory.ts
|
|
3767
|
-
import { readFileSync as
|
|
4419
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3768
4420
|
|
|
3769
4421
|
// src/spaces.ts
|
|
3770
4422
|
async function spacesFor(context, client, env = process.env) {
|
|
@@ -3828,7 +4480,7 @@ async function rememberCommand(context) {
|
|
|
3828
4480
|
let text;
|
|
3829
4481
|
if (file) {
|
|
3830
4482
|
try {
|
|
3831
|
-
text =
|
|
4483
|
+
text = readFileSync9(file, "utf8");
|
|
3832
4484
|
} catch {
|
|
3833
4485
|
context.error(`Could not read ${file}.`);
|
|
3834
4486
|
return 1;
|
|
@@ -4232,8 +4884,14 @@ async function dispatch(context) {
|
|
|
4232
4884
|
if (noun === "create" || noun === "new") return createSpaceCommand(context);
|
|
4233
4885
|
if (noun === "delete" || noun === "remove") return deleteSpaceCommand(context);
|
|
4234
4886
|
if (noun === "merge") return mergeSpacesCommand(context);
|
|
4887
|
+
if (noun === "share") return shareSpaceCommand(context);
|
|
4888
|
+
if (noun === "unshare") return unshareSpaceCommand(context);
|
|
4889
|
+
if (noun === "role") return spaceRoleCommand(context);
|
|
4890
|
+
if (noun === "sharing") return spaceSharingCommand(context);
|
|
4235
4891
|
if (noun === void 0 || noun === "list") return listSpacesCommand(context);
|
|
4236
|
-
context.error(
|
|
4892
|
+
context.error(
|
|
4893
|
+
`Cannot "pm spaces ${noun}". Try list, create, delete, merge, share, unshare, role or sharing.`
|
|
4894
|
+
);
|
|
4237
4895
|
return 2;
|
|
4238
4896
|
case "list":
|
|
4239
4897
|
if (noun === "memories" || noun === "memory") return listMemoriesCommand(context);
|