@agentcomms/resend 0.27.1 → 0.29.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/README.md +5 -0
- package/dist/cli.mjs +4 -4
- package/dist/index.d.mts +305 -1
- package/dist/index.mjs +2 -2
- package/dist/{send-BbyDPqGS.mjs → send-BC8FI-_F.mjs} +1588 -50
- package/dist/{server-stiYlGRn.mjs → server-7jk5cvX6.mjs} +3 -1
- package/dist/{stdio-entry-DXuzeDqK.mjs → stdio-entry-BvsR_B_w.mjs} +1 -1
- package/package.json +3 -3
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
|
|
2
|
-
import {
|
|
1
|
+
import { C as unknown, S as union$1, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { accessSync, constants, existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import { access, chmod, constants as constants$1, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
|
|
6
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
3
7
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
4
8
|
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
6
9
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
10
|
import "node:readline/promises";
|
|
8
11
|
import { styleText } from "node:util";
|
|
9
|
-
import { homedir, platform, userInfo } from "node:os";
|
|
10
|
-
import { accessSync, constants as constants$1, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
11
|
-
import { execFile, spawn } from "node:child_process";
|
|
12
12
|
import { gunzipSync } from "node:zlib";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
14
|
//#endregion
|
|
14
15
|
//#region src/version.ts
|
|
15
16
|
/** The package version, read from package.json at build time. */
|
|
16
|
-
const VERSION$2 = "0.
|
|
17
|
+
const VERSION$2 = "0.29.0";
|
|
17
18
|
//#endregion
|
|
18
19
|
//#region src/api/routes.ts
|
|
19
20
|
/**
|
|
@@ -201,8 +202,70 @@ const RESEND_CALLER = Object.freeze({
|
|
|
201
202
|
url: import.meta.url,
|
|
202
203
|
packageName: PACKAGE_NAME
|
|
203
204
|
});
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
Object.freeze(["on", "off"]);
|
|
206
|
+
/** Whether `value` is a retention a person can set: a whole number of days from 1 to 365. */
|
|
207
|
+
function isActivityRetention(value) {
|
|
208
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 365;
|
|
209
|
+
}
|
|
210
|
+
function isActivityRecordSetting(value) {
|
|
211
|
+
return value === "on" || value === "off";
|
|
212
|
+
}
|
|
213
|
+
/** The `activity` block as the file holds it, raw: an object, or nothing. */
|
|
214
|
+
function storedActivity(config) {
|
|
215
|
+
const stored = config.activity;
|
|
216
|
+
return typeof stored === "object" && stored !== null && !Array.isArray(stored) ? stored : null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The settings in force for `config`. A missing block, or a missing key, is the default. Anything else this release
|
|
220
|
+
* cannot read is named in `problems`, and read so that nothing is recorded and nothing is deleted: a word other than
|
|
221
|
+
* `on` or `off` as `off`; a retention that is not a whole number from 1 to 365 — out of range, a fraction, not a
|
|
222
|
+
* number — as 365 days with recording `off`; a block that is not an object as both.
|
|
223
|
+
*/
|
|
224
|
+
function readActivitySettings(config) {
|
|
225
|
+
const problems = [];
|
|
226
|
+
if (config.activity === void 0) return {
|
|
227
|
+
record: "on",
|
|
228
|
+
retentionDays: 30,
|
|
229
|
+
problems
|
|
230
|
+
};
|
|
231
|
+
const stored = storedActivity(config);
|
|
232
|
+
if (stored === null) {
|
|
233
|
+
problems.push(`activity in config.json is not an object, so recording is off and 365 days are kept`);
|
|
234
|
+
return {
|
|
235
|
+
record: "off",
|
|
236
|
+
retentionDays: 365,
|
|
237
|
+
problems
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
let record = "on";
|
|
241
|
+
if (stored.record !== void 0) {
|
|
242
|
+
if (isActivityRecordSetting(stored.record)) record = stored.record;
|
|
243
|
+
else {
|
|
244
|
+
record = "off";
|
|
245
|
+
problems.push(`activity.record is ${describe(stored.record)}, not on or off, so recording is off`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let retentionDays = 30;
|
|
249
|
+
const days = stored.retentionDays;
|
|
250
|
+
if (days !== void 0) {
|
|
251
|
+
if (isActivityRetention(days)) retentionDays = days;
|
|
252
|
+
else {
|
|
253
|
+
retentionDays = 365;
|
|
254
|
+
record = "off";
|
|
255
|
+
problems.push(`activity.retentionDays is ${describe(days)}, not a whole number from 1 to 365, so recording is off and 365 days are kept`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
record,
|
|
260
|
+
retentionDays,
|
|
261
|
+
problems
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** A stored value in a sentence: short, and never more than a few dozen characters of whatever the file held. */
|
|
265
|
+
function describe(value) {
|
|
266
|
+
const text = typeof value === "string" ? JSON.stringify(value) : String(JSON.stringify(value) ?? value);
|
|
267
|
+
return text.length > 40 ? `${text.slice(0, 39)}…` : text;
|
|
268
|
+
}
|
|
206
269
|
/**
|
|
207
270
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
208
271
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -402,22 +465,6 @@ function toCommsError(value) {
|
|
|
402
465
|
if (isCommsError(value)) return value;
|
|
403
466
|
return new CommsError("UNEXPECTED", value instanceof Error ? value.message : String(value), { cause: value });
|
|
404
467
|
}
|
|
405
|
-
function sha256Hex(data) {
|
|
406
|
-
return createHash("sha256").update(data).digest("hex");
|
|
407
|
-
}
|
|
408
|
-
/** Lower-cases the address part of `Name <addr>` or a bare address; display names are dropped. */
|
|
409
|
-
function normaliseAddress(value) {
|
|
410
|
-
return (/<([^<>]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
|
|
411
|
-
}
|
|
412
|
-
function collapseWhitespace(text) {
|
|
413
|
-
return text.replace(/\s+/g, " ").trim();
|
|
414
|
-
}
|
|
415
|
-
/** Deterministic JSON: keys sorted at every level, undefined members dropped. */
|
|
416
|
-
function canonicalJson$1(value) {
|
|
417
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
|
|
418
|
-
if (value && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson$1(v)}`).join(",")}}`;
|
|
419
|
-
return JSON.stringify(value);
|
|
420
|
-
}
|
|
421
468
|
/** Creates a directory (and parents) readable only by the current user. Tightens an existing one. */
|
|
422
469
|
async function ensurePrivateDir(path) {
|
|
423
470
|
await mkdir(path, {
|
|
@@ -535,6 +582,1267 @@ async function syncDirectory(path) {
|
|
|
535
582
|
await handle.close();
|
|
536
583
|
}
|
|
537
584
|
}
|
|
585
|
+
/** True when a path exists and is readable or writable by someone other than its owner (POSIX only). */
|
|
586
|
+
async function isGroupOrWorldAccessible(path) {
|
|
587
|
+
if (process.platform === "win32") return false;
|
|
588
|
+
return ((await stat(path)).mode & 63) !== 0;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* The activity log (design 2026-10-25-activity, §4 with S1–S5): one line for every call an agent session makes that
|
|
592
|
+
* reads or writes account data, tied to the session that made it — what it touched, never what it said.
|
|
593
|
+
*
|
|
594
|
+
* - **Where:** `<state>/activity/YYYY-MM-DD.jsonl` (the UTC day of the line), each file owner-only (600) in an
|
|
595
|
+
* owner-only folder (700), each line appended whole with `appendPrivateLine`, as the audit log's are. Daily files,
|
|
596
|
+
* so retention drops whole days (`prune`).
|
|
597
|
+
* - **What a line holds:** `ActivityLine`, and nothing else. Every line is rebuilt field by field from the fields a
|
|
598
|
+
* line has (`activityLineOf`), so a channel that hands over a whole message — body, text, HTML, attachment bytes —
|
|
599
|
+
* still writes only its id, subject and sender: the type says so, and the copy makes it so. Subjects and names are
|
|
600
|
+
* kept as received, cut to their bounds, and shown only inside the untrusted-content envelope.
|
|
601
|
+
* - **Never in the way:** `append` never throws into its caller. A line that cannot be written is said once per
|
|
602
|
+
* process on stderr, remembered for `doctor`, and the call it describes goes on (§4.6).
|
|
603
|
+
* - **The audit log is unchanged.** It stays small, content-free and shareable; this one is private.
|
|
604
|
+
*/
|
|
605
|
+
/** What the call did with account data: read it, wrote it, was told of it (an event delivery, S1), or approved it. */
|
|
606
|
+
const ACTIVITY_KINDS = Object.freeze([
|
|
607
|
+
"read",
|
|
608
|
+
"write",
|
|
609
|
+
"told",
|
|
610
|
+
"approval"
|
|
611
|
+
]);
|
|
612
|
+
/** How it ended: done, refused by a policy, an approval or a rule, or failed. */
|
|
613
|
+
const ACTIVITY_OUTCOMES = Object.freeze([
|
|
614
|
+
"ok",
|
|
615
|
+
"refused",
|
|
616
|
+
"failed"
|
|
617
|
+
]);
|
|
618
|
+
/** Which kind of process recorded it: an MCP server, a CLI call, or the event service's own work. */
|
|
619
|
+
const ACTIVITY_SURFACES = Object.freeze([
|
|
620
|
+
"mcp",
|
|
621
|
+
"cli",
|
|
622
|
+
"daemon"
|
|
623
|
+
]);
|
|
624
|
+
/**
|
|
625
|
+
* The most a line may take as written, its newline included: 16 KiB, well inside the one write `appendPrivateLine`
|
|
626
|
+
* makes of it, so a line is always whole or absent, whatever its items hold. Past it, `fitted` cuts it.
|
|
627
|
+
*/
|
|
628
|
+
const MAX_ACTIVITY_LINE_BYTES = 16384;
|
|
629
|
+
const BOUNDS = {
|
|
630
|
+
id: 200,
|
|
631
|
+
name: 200,
|
|
632
|
+
address: 320,
|
|
633
|
+
fileName: 255,
|
|
634
|
+
conversation: 200,
|
|
635
|
+
query: 500,
|
|
636
|
+
reason: 300,
|
|
637
|
+
word: 100,
|
|
638
|
+
alias: 128,
|
|
639
|
+
folder: 4096,
|
|
640
|
+
startedAt: 64,
|
|
641
|
+
version: 64
|
|
642
|
+
};
|
|
643
|
+
const MAX_RECIPIENTS$1 = 50;
|
|
644
|
+
const MAX_COUNTS = 10;
|
|
645
|
+
const SESSION_KEY = /^[0-9a-f]{16}$/;
|
|
646
|
+
const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
647
|
+
const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl$/;
|
|
648
|
+
const DAY_MS$1 = 864e5;
|
|
649
|
+
/** `value` cut to `max` characters — whole characters, never half a surrogate pair — ending in `…` when cut. */
|
|
650
|
+
function cut(value, max) {
|
|
651
|
+
if (typeof value !== "string") return void 0;
|
|
652
|
+
if (value.length <= max) return value;
|
|
653
|
+
const chars = Array.from(value.slice(0, max * 2 + 2));
|
|
654
|
+
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
655
|
+
}
|
|
656
|
+
function whole(value) {
|
|
657
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : void 0;
|
|
658
|
+
}
|
|
659
|
+
function isoDay(at) {
|
|
660
|
+
return new Date(at).toISOString().slice(0, 10);
|
|
661
|
+
}
|
|
662
|
+
function own$3(value) {
|
|
663
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
664
|
+
}
|
|
665
|
+
function itemOf$1(raw) {
|
|
666
|
+
const item = own$3(raw);
|
|
667
|
+
const id = cut(item?.id, BOUNDS.id);
|
|
668
|
+
if (item === void 0 || id === void 0) return null;
|
|
669
|
+
const sender = own$3(item.from);
|
|
670
|
+
const from = sender === void 0 ? void 0 : {
|
|
671
|
+
...typeof sender.name === "string" ? { name: cut(sender.name, BOUNDS.name) } : {},
|
|
672
|
+
...typeof sender.address === "string" ? { address: cut(sender.address, BOUNDS.address) } : {}
|
|
673
|
+
};
|
|
674
|
+
const size = whole(item.size);
|
|
675
|
+
return {
|
|
676
|
+
id,
|
|
677
|
+
...typeof item.subject === "string" ? { subject: cut(item.subject, 200) } : {},
|
|
678
|
+
...from !== void 0 && Object.keys(from).length > 0 ? { from } : {},
|
|
679
|
+
...typeof item.name === "string" ? { name: cut(item.name, BOUNDS.fileName) } : {},
|
|
680
|
+
...size !== void 0 ? { size } : {},
|
|
681
|
+
...typeof item.conversation === "string" ? { conversation: cut(item.conversation, BOUNDS.conversation) } : {}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function clientOf(raw) {
|
|
685
|
+
const client = own$3(raw);
|
|
686
|
+
const name = cut(client?.name, BOUNDS.word);
|
|
687
|
+
if (name === void 0 || name === "") return void 0;
|
|
688
|
+
const version = cut(client?.version, BOUNDS.version);
|
|
689
|
+
return {
|
|
690
|
+
name,
|
|
691
|
+
...version ? { version } : {}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** A parent in this exact key order — the order `sessionOfAncestor` finds it by. */
|
|
695
|
+
function parentOf(raw) {
|
|
696
|
+
const parent = own$3(raw);
|
|
697
|
+
const pid = whole(parent?.pid);
|
|
698
|
+
const startedAt = cut(parent?.startedAt, BOUNDS.startedAt);
|
|
699
|
+
if (pid === void 0 || startedAt === void 0) return void 0;
|
|
700
|
+
return {
|
|
701
|
+
pid,
|
|
702
|
+
startedAt,
|
|
703
|
+
name: cut(parent?.name, BOUNDS.word) ?? ""
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function sessionOf(raw) {
|
|
707
|
+
const session = own$3(raw) ?? {};
|
|
708
|
+
const key = typeof session.key === "string" && SESSION_KEY.test(session.key) ? session.key : null;
|
|
709
|
+
const surface = ACTIVITY_SURFACES.includes(session.surface) ? session.surface : "mcp";
|
|
710
|
+
const client = clientOf(session.client);
|
|
711
|
+
const parent = parentOf(session.parent);
|
|
712
|
+
const ancestors = Array.isArray(session.ancestors) ? session.ancestors.slice(0, 3).flatMap((name) => typeof name === "string" ? [cut(name, BOUNDS.word) ?? ""] : []) : void 0;
|
|
713
|
+
return {
|
|
714
|
+
key: key ?? randomKey(),
|
|
715
|
+
process: cut(session.process, BOUNDS.word) ?? PROCESS_ID,
|
|
716
|
+
pid: whole(session.pid) ?? 0,
|
|
717
|
+
...client === void 0 ? {} : { client },
|
|
718
|
+
folder: typeof session.folder === "string" ? cut(session.folder, BOUNDS.folder) ?? null : null,
|
|
719
|
+
...parent === void 0 ? {} : { parent },
|
|
720
|
+
...ancestors === void 0 ? {} : { ancestors },
|
|
721
|
+
grouped: key !== null && session.grouped === true,
|
|
722
|
+
surface
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* The line as it is written: every field copied from what a line has, each cut to its bound, in one fixed order.
|
|
727
|
+
* Anything else the input carries — a body, a text, an HTML part, bytes — is not copied, whatever it is called.
|
|
728
|
+
*/
|
|
729
|
+
function activityLineOf(input, now = /* @__PURE__ */ new Date()) {
|
|
730
|
+
return fitted(boundedLine(input, now));
|
|
731
|
+
}
|
|
732
|
+
/** How many bytes a line takes as written, its newline included. */
|
|
733
|
+
function bytesOf(line) {
|
|
734
|
+
return Buffer.byteLength(JSON.stringify(line)) + 1;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* The line, cut until it fits in `MAX_ACTIVITY_LINE_BYTES`, and marked `truncated` when it was: items from the end
|
|
738
|
+
* first — `count` keeps how many there were — then recipients from the end, then the longer words shortened, and
|
|
739
|
+
* last the words that are not the line's own dropped. Text a sender wrote can be many times its length as JSON (a
|
|
740
|
+
* control character is six bytes), so the bounds on each field alone do not bound the line.
|
|
741
|
+
*/
|
|
742
|
+
function fitted(line) {
|
|
743
|
+
if (bytesOf(line) <= 16384) return line;
|
|
744
|
+
const items = [...line.items ?? []];
|
|
745
|
+
const recipients = [...line.recipients ?? []];
|
|
746
|
+
let cutLine = {
|
|
747
|
+
...line,
|
|
748
|
+
truncated: true
|
|
749
|
+
};
|
|
750
|
+
const fits = () => bytesOf(cutLine) <= MAX_ACTIVITY_LINE_BYTES;
|
|
751
|
+
while (items.length > 0 && !fits()) {
|
|
752
|
+
items.pop();
|
|
753
|
+
cutLine = {
|
|
754
|
+
...cutLine,
|
|
755
|
+
items: [...items]
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
while (recipients.length > 0 && !fits()) {
|
|
759
|
+
recipients.pop();
|
|
760
|
+
cutLine = {
|
|
761
|
+
...cutLine,
|
|
762
|
+
recipients: [...recipients]
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (fits()) return cutLine;
|
|
766
|
+
const short = (text, max) => text === void 0 ? void 0 : cut(text, max);
|
|
767
|
+
const { ancestors: _ancestors, ...session } = cutLine.session;
|
|
768
|
+
cutLine = {
|
|
769
|
+
...cutLine,
|
|
770
|
+
channel: short(cutLine.channel, 40) ?? "",
|
|
771
|
+
operation: short(cutLine.operation, 64) ?? "",
|
|
772
|
+
account: {
|
|
773
|
+
id: short(cutLine.account.id, 64) ?? "",
|
|
774
|
+
alias: short(cutLine.account.alias, 64) ?? ""
|
|
775
|
+
},
|
|
776
|
+
session: {
|
|
777
|
+
...session,
|
|
778
|
+
folder: cutLine.session.folder === null ? null : short(cutLine.session.folder, 256) ?? null,
|
|
779
|
+
...session.client === void 0 ? {} : { client: {
|
|
780
|
+
name: short(session.client.name, 40) ?? "",
|
|
781
|
+
...session.client.version === void 0 ? {} : { version: short(session.client.version, 20) }
|
|
782
|
+
} },
|
|
783
|
+
...session.parent === void 0 ? {} : { parent: {
|
|
784
|
+
...session.parent,
|
|
785
|
+
name: short(session.parent.name, 40) ?? ""
|
|
786
|
+
} }
|
|
787
|
+
},
|
|
788
|
+
...cutLine.reason === void 0 ? {} : { reason: short(cutLine.reason, 100) },
|
|
789
|
+
...cutLine.query === void 0 ? {} : { query: short(cutLine.query, 100) },
|
|
790
|
+
...cutLine.told === void 0 ? {} : { told: {
|
|
791
|
+
rule: short(cutLine.told.rule, 60) ?? "",
|
|
792
|
+
target: short(cutLine.told.target, 60) ?? ""
|
|
793
|
+
} }
|
|
794
|
+
};
|
|
795
|
+
if (fits()) return cutLine;
|
|
796
|
+
const { reason: _reason, query: _query, told: _told, counts: _counts, ...rest } = cutLine;
|
|
797
|
+
return {
|
|
798
|
+
...rest,
|
|
799
|
+
truncated: true
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/** The line every field of it bounded, before the line as a whole is (`fitted`). */
|
|
803
|
+
function boundedLine(input, now) {
|
|
804
|
+
const raw = input;
|
|
805
|
+
const at = typeof raw.at === "string" && ISO_TIME.test(raw.at) ? raw.at : now.toISOString();
|
|
806
|
+
const account = own$3(raw.account);
|
|
807
|
+
const items = Array.isArray(raw.items) ? raw.items : void 0;
|
|
808
|
+
const kept = items?.slice(0, 50).flatMap((item) => itemOf$1(item) ?? []);
|
|
809
|
+
const given = whole(raw.count);
|
|
810
|
+
const count = items === void 0 ? given : Math.max(items.length, given ?? 0);
|
|
811
|
+
const counts = own$3(raw.counts);
|
|
812
|
+
const keptCounts = counts === void 0 ? void 0 : Object.fromEntries(Object.entries(counts).filter(([name, value]) => /^[a-z][A-Za-z]{0,31}$/.test(name) && whole(value) !== void 0).slice(0, MAX_COUNTS).map(([name, value]) => [name, whole(value)]));
|
|
813
|
+
const recipients = Array.isArray(raw.recipients) ? raw.recipients.slice(0, MAX_RECIPIENTS$1).flatMap((address) => typeof address === "string" ? [cut(address, BOUNDS.address)] : []) : void 0;
|
|
814
|
+
const told = own$3(raw.told);
|
|
815
|
+
const reason = cut(raw.reason, BOUNDS.reason);
|
|
816
|
+
const query = cut(raw.query, BOUNDS.query);
|
|
817
|
+
const approvalId = typeof raw.approvalId === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(raw.approvalId) ? raw.approvalId : void 0;
|
|
818
|
+
return {
|
|
819
|
+
at,
|
|
820
|
+
channel: cut(raw.channel, BOUNDS.word) ?? "",
|
|
821
|
+
account: {
|
|
822
|
+
id: cut(account?.id, BOUNDS.alias) ?? "",
|
|
823
|
+
alias: cut(account?.alias, BOUNDS.alias) ?? ""
|
|
824
|
+
},
|
|
825
|
+
operation: cut(raw.operation, BOUNDS.word) ?? "",
|
|
826
|
+
kind: ACTIVITY_KINDS.includes(raw.kind) ? raw.kind : "read",
|
|
827
|
+
outcome: ACTIVITY_OUTCOMES.includes(raw.outcome) ? raw.outcome : "ok",
|
|
828
|
+
...reason ? { reason } : {},
|
|
829
|
+
session: sessionOf(raw.session),
|
|
830
|
+
ms: whole(raw.ms) ?? 0,
|
|
831
|
+
...kept === void 0 ? {} : { items: kept },
|
|
832
|
+
...count === void 0 ? {} : { count },
|
|
833
|
+
...keptCounts === void 0 || Object.keys(keptCounts).length === 0 ? {} : { counts: keptCounts },
|
|
834
|
+
...query === void 0 ? {} : { query },
|
|
835
|
+
...recipients === void 0 ? {} : { recipients },
|
|
836
|
+
...approvalId === void 0 ? {} : { approvalId },
|
|
837
|
+
...told === void 0 ? {} : { told: {
|
|
838
|
+
rule: cut(told.rule, BOUNDS.name) ?? "",
|
|
839
|
+
target: cut(told.target, BOUNDS.name) ?? ""
|
|
840
|
+
} },
|
|
841
|
+
...raw.truncated === true ? { truncated: true } : {}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/** A line read back from a file: one this release can show, or null for anything else. */
|
|
845
|
+
function readLine(text) {
|
|
846
|
+
let raw;
|
|
847
|
+
try {
|
|
848
|
+
raw = JSON.parse(text);
|
|
849
|
+
} catch {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const line = own$3(raw);
|
|
853
|
+
if (line === void 0 || typeof line.at !== "string" || !ISO_TIME.test(line.at)) return null;
|
|
854
|
+
const session = own$3(line.session);
|
|
855
|
+
if (typeof session?.key !== "string" || !SESSION_KEY.test(session.key)) return null;
|
|
856
|
+
return activityLineOf(line);
|
|
857
|
+
}
|
|
858
|
+
function emptyKinds() {
|
|
859
|
+
return {
|
|
860
|
+
read: 0,
|
|
861
|
+
write: 0,
|
|
862
|
+
told: 0,
|
|
863
|
+
approval: 0
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function emptyOutcomes() {
|
|
867
|
+
return {
|
|
868
|
+
ok: 0,
|
|
869
|
+
refused: 0,
|
|
870
|
+
failed: 0
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function aggregateOf(line) {
|
|
874
|
+
const kind = emptyKinds();
|
|
875
|
+
kind[line.kind] += 1;
|
|
876
|
+
const outcome = emptyOutcomes();
|
|
877
|
+
outcome[line.outcome] += 1;
|
|
878
|
+
return {
|
|
879
|
+
key: line.session.key,
|
|
880
|
+
grouped: line.session.grouped,
|
|
881
|
+
surfaces: [line.session.surface],
|
|
882
|
+
primary: {
|
|
883
|
+
at: line.at,
|
|
884
|
+
cli: line.session.surface === "cli",
|
|
885
|
+
folder: line.session.folder,
|
|
886
|
+
client: line.session.client ?? null,
|
|
887
|
+
parent: line.session.parent ?? null
|
|
888
|
+
},
|
|
889
|
+
firstSeen: line.at,
|
|
890
|
+
lastSeen: line.at,
|
|
891
|
+
processes: [line.session.process],
|
|
892
|
+
channels: line.channel ? [line.channel] : [],
|
|
893
|
+
accounts: line.account.alias ? [line.account.alias] : [],
|
|
894
|
+
kind,
|
|
895
|
+
outcome
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function union(a, b) {
|
|
899
|
+
return [.../* @__PURE__ */ new Set([...a, ...b])];
|
|
900
|
+
}
|
|
901
|
+
/** Two aggregates of one session, added together. */
|
|
902
|
+
function merge(a, b) {
|
|
903
|
+
const first = (x, y) => x.cli !== y.cli ? x.cli ? y : x : x.at <= y.at ? x : y;
|
|
904
|
+
const primary = first(a.primary, b.primary);
|
|
905
|
+
const other = primary === a.primary ? b.primary : a.primary;
|
|
906
|
+
const kind = emptyKinds();
|
|
907
|
+
const outcome = emptyOutcomes();
|
|
908
|
+
for (const name of ACTIVITY_KINDS) kind[name] = a.kind[name] + b.kind[name];
|
|
909
|
+
for (const name of ACTIVITY_OUTCOMES) outcome[name] = a.outcome[name] + b.outcome[name];
|
|
910
|
+
return {
|
|
911
|
+
key: a.key,
|
|
912
|
+
grouped: a.grouped || b.grouped,
|
|
913
|
+
surfaces: union(a.surfaces, b.surfaces),
|
|
914
|
+
primary: {
|
|
915
|
+
...primary,
|
|
916
|
+
client: primary.client ?? other.client,
|
|
917
|
+
parent: primary.parent ?? other.parent
|
|
918
|
+
},
|
|
919
|
+
firstSeen: a.firstSeen <= b.firstSeen ? a.firstSeen : b.firstSeen,
|
|
920
|
+
lastSeen: a.lastSeen >= b.lastSeen ? a.lastSeen : b.lastSeen,
|
|
921
|
+
processes: union(a.processes, b.processes),
|
|
922
|
+
channels: union(a.channels, b.channels),
|
|
923
|
+
accounts: union(a.accounts, b.accounts),
|
|
924
|
+
kind,
|
|
925
|
+
outcome
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function add(groups, aggregate) {
|
|
929
|
+
const held = groups.get(aggregate.key);
|
|
930
|
+
groups.set(aggregate.key, held === void 0 ? aggregate : merge(held, aggregate));
|
|
931
|
+
}
|
|
932
|
+
function summaryOf$1(aggregate) {
|
|
933
|
+
return {
|
|
934
|
+
key: aggregate.key,
|
|
935
|
+
grouped: aggregate.grouped,
|
|
936
|
+
surfaces: [...aggregate.surfaces].sort(),
|
|
937
|
+
folder: aggregate.primary.folder,
|
|
938
|
+
client: aggregate.primary.client,
|
|
939
|
+
parent: aggregate.primary.parent,
|
|
940
|
+
firstSeen: aggregate.firstSeen,
|
|
941
|
+
lastSeen: aggregate.lastSeen,
|
|
942
|
+
processes: aggregate.processes.length,
|
|
943
|
+
channels: [...aggregate.channels].sort(),
|
|
944
|
+
accounts: [...aggregate.accounts].sort(),
|
|
945
|
+
counts: {
|
|
946
|
+
kind: { ...aggregate.kind },
|
|
947
|
+
outcome: { ...aggregate.outcome }
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** A day's aggregates read back from its index: trusted only as far as their shape, and dropped whole otherwise. */
|
|
952
|
+
function aggregatesOf(raw) {
|
|
953
|
+
if (!Array.isArray(raw)) return null;
|
|
954
|
+
const strings = (value) => Array.isArray(value) && value.every((each) => typeof each === "string");
|
|
955
|
+
for (const each of raw) {
|
|
956
|
+
const held = own$3(each);
|
|
957
|
+
if (held === void 0 || typeof held.key !== "string" || !SESSION_KEY.test(held.key) || typeof held.firstSeen !== "string" || typeof held.lastSeen !== "string" || own$3(held.primary) === void 0 || own$3(held.kind) === void 0 || own$3(held.outcome) === void 0 || !strings(held.processes) || !strings(held.channels) || !strings(held.accounts) || !strings(held.surfaces)) return null;
|
|
958
|
+
}
|
|
959
|
+
return raw;
|
|
960
|
+
}
|
|
961
|
+
const FAILURE_FILE = "activity-failure.json";
|
|
962
|
+
const INDEX_DIR = ".index";
|
|
963
|
+
/**
|
|
964
|
+
* Each day's server windows: one small line for each server process's window on that day — its key, parent and
|
|
965
|
+
* folder — so a CLI call finds the window it runs in without reading the day (`sessionOfAncestor`).
|
|
966
|
+
*/
|
|
967
|
+
const PARENTS_DIR = ".parents";
|
|
968
|
+
const DEFAULT_SHOW_LIMIT = 1e3;
|
|
969
|
+
const ON_BY_DEFAULT = Object.freeze({
|
|
970
|
+
record: "on",
|
|
971
|
+
retentionDays: 30,
|
|
972
|
+
problems: []
|
|
973
|
+
});
|
|
974
|
+
const UNREADABLE = Object.freeze({
|
|
975
|
+
record: "off",
|
|
976
|
+
retentionDays: 365,
|
|
977
|
+
problems: ["the configuration could not be read, so nothing is recorded"]
|
|
978
|
+
});
|
|
979
|
+
/** The once-a-process line, when the stderr default is used: one process, one line, however many logs it opens. */
|
|
980
|
+
let warnedOnStderr = false;
|
|
981
|
+
function codeOf$1(error) {
|
|
982
|
+
if (error instanceof CommsError) return error.code;
|
|
983
|
+
const code = error?.code;
|
|
984
|
+
return typeof code === "string" ? code : "error";
|
|
985
|
+
}
|
|
986
|
+
function messageOf$2(error) {
|
|
987
|
+
return cut(error instanceof Error ? error.message : String(error), BOUNDS.reason) ?? "unknown";
|
|
988
|
+
}
|
|
989
|
+
var ActivityLog = class {
|
|
990
|
+
directory;
|
|
991
|
+
#failureFile;
|
|
992
|
+
#now;
|
|
993
|
+
#settings;
|
|
994
|
+
#warn;
|
|
995
|
+
#failure = null;
|
|
996
|
+
#failureWrittenAt = 0;
|
|
997
|
+
#warned = false;
|
|
998
|
+
#prunedDay = null;
|
|
999
|
+
/** The days this process has made sure of a parents file for, and the windows it has noted there, by day. */
|
|
1000
|
+
#parentsReady = /* @__PURE__ */ new Set();
|
|
1001
|
+
#parentsNoted = /* @__PURE__ */ new Set();
|
|
1002
|
+
constructor(stateDir, options = {}) {
|
|
1003
|
+
this.directory = join(stateDir, "activity");
|
|
1004
|
+
this.#failureFile = join(stateDir, FAILURE_FILE);
|
|
1005
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1006
|
+
this.#settings = options.settings ?? (async () => ON_BY_DEFAULT);
|
|
1007
|
+
this.#warn = options.warn;
|
|
1008
|
+
}
|
|
1009
|
+
/** The last line this process could not write, or null. */
|
|
1010
|
+
get lastFailure() {
|
|
1011
|
+
return this.#failure;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Appends one line, unless recording is off. True when it was written. Never throws: a line that cannot be written
|
|
1015
|
+
* is said once per process (`warn`, stderr by default), recorded beside the log for `doctor`, and returns false.
|
|
1016
|
+
*
|
|
1017
|
+
* The first call a process makes on each day — this, or any read — first prunes days older than the retention in
|
|
1018
|
+
* force, whether or not recording is on: the log's daily maintenance, best effort.
|
|
1019
|
+
*/
|
|
1020
|
+
async append(input) {
|
|
1021
|
+
if ((await this.#maintained()).record !== "on") return false;
|
|
1022
|
+
try {
|
|
1023
|
+
const line = activityLineOf(input, this.#now());
|
|
1024
|
+
await appendPrivateLine(join(this.directory, `${line.at.slice(0, 10)}.jsonl`), JSON.stringify(line));
|
|
1025
|
+
await this.#noteParent(line);
|
|
1026
|
+
if (this.#failure !== null) {
|
|
1027
|
+
this.#failure = null;
|
|
1028
|
+
await rm(this.#failureFile, { force: true }).catch(() => void 0);
|
|
1029
|
+
}
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
await this.#failed(error);
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
async #failed(error) {
|
|
1037
|
+
const now = this.#now();
|
|
1038
|
+
const failure = {
|
|
1039
|
+
at: now.toISOString(),
|
|
1040
|
+
code: codeOf$1(error),
|
|
1041
|
+
message: messageOf$2(error),
|
|
1042
|
+
pid: process.pid
|
|
1043
|
+
};
|
|
1044
|
+
this.#failure = failure;
|
|
1045
|
+
const said = `agent-communications: activity is not being recorded: ${failure.message}`;
|
|
1046
|
+
if (this.#warn !== void 0) {
|
|
1047
|
+
if (!this.#warned) this.#warn(said);
|
|
1048
|
+
} else if (!warnedOnStderr) {
|
|
1049
|
+
warnedOnStderr = true;
|
|
1050
|
+
process.stderr.write(`${said}\n`);
|
|
1051
|
+
}
|
|
1052
|
+
this.#warned = true;
|
|
1053
|
+
if (now.getTime() - this.#failureWrittenAt < 6e4) return;
|
|
1054
|
+
this.#failureWrittenAt = now.getTime();
|
|
1055
|
+
await writeFileAtomic(this.#failureFile, `${JSON.stringify(failure)}\n`).catch(() => void 0);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* The day's parents file, made on a process's first line of the day — so a reader can tell a day with no window
|
|
1059
|
+
* from one an earlier build wrote, with no index — and a grouped server's window noted in it once a day. Best
|
|
1060
|
+
* effort: without it a CLI call reads the day itself.
|
|
1061
|
+
*/
|
|
1062
|
+
async #noteParent(line) {
|
|
1063
|
+
const day = line.at.slice(0, 10);
|
|
1064
|
+
const path = join(this.directory, PARENTS_DIR, `${day}.jsonl`);
|
|
1065
|
+
try {
|
|
1066
|
+
if (!this.#parentsReady.has(day)) {
|
|
1067
|
+
await ensurePrivateDir(join(this.directory, PARENTS_DIR));
|
|
1068
|
+
await (await open(path, "a", 384)).close();
|
|
1069
|
+
this.#parentsReady.add(day);
|
|
1070
|
+
}
|
|
1071
|
+
const { session } = line;
|
|
1072
|
+
if (session.surface !== "mcp" || !session.grouped || session.parent === void 0) return;
|
|
1073
|
+
const noted = `${day}\0${session.key}`;
|
|
1074
|
+
if (this.#parentsNoted.has(noted)) return;
|
|
1075
|
+
await appendPrivateLine(path, JSON.stringify({
|
|
1076
|
+
key: session.key,
|
|
1077
|
+
parent: session.parent,
|
|
1078
|
+
folder: session.folder
|
|
1079
|
+
}));
|
|
1080
|
+
this.#parentsNoted.add(noted);
|
|
1081
|
+
} catch {}
|
|
1082
|
+
}
|
|
1083
|
+
/** The windows noted on `day`, or null when the day has no parents file (an earlier build wrote it, or none). */
|
|
1084
|
+
async #parentsOf(day) {
|
|
1085
|
+
let text;
|
|
1086
|
+
try {
|
|
1087
|
+
text = await readFile(join(this.directory, PARENTS_DIR, `${day}.jsonl`), "utf8");
|
|
1088
|
+
} catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
return text.split("\n").flatMap((raw) => {
|
|
1092
|
+
if (!raw) return [];
|
|
1093
|
+
try {
|
|
1094
|
+
const held = own$3(JSON.parse(raw));
|
|
1095
|
+
const parent = parentOf(held?.parent);
|
|
1096
|
+
const key = held?.key;
|
|
1097
|
+
if (typeof key !== "string" || !SESSION_KEY.test(key) || parent === void 0) return [];
|
|
1098
|
+
return [{
|
|
1099
|
+
key,
|
|
1100
|
+
parent,
|
|
1101
|
+
folder: typeof held?.folder === "string" ? held.folder : null
|
|
1102
|
+
}];
|
|
1103
|
+
} catch {
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
/** The settings in force, read afresh; a reader that throws reads as `UNREADABLE` — off, and every day kept. */
|
|
1109
|
+
async #settingsNow() {
|
|
1110
|
+
try {
|
|
1111
|
+
return await this.#settings();
|
|
1112
|
+
} catch {
|
|
1113
|
+
return UNREADABLE;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* The settings in force, after the day's maintenance: the first call a process makes on a day — an append or a read,
|
|
1118
|
+
* recording on or off — deletes the days past the retention. Never throws.
|
|
1119
|
+
*/
|
|
1120
|
+
async #maintained() {
|
|
1121
|
+
const settings = await this.#settingsNow();
|
|
1122
|
+
const today = isoDay(this.#now().getTime());
|
|
1123
|
+
if (this.#prunedDay !== today) {
|
|
1124
|
+
this.#prunedDay = today;
|
|
1125
|
+
await this.prune(settings.retentionDays).catch(() => void 0);
|
|
1126
|
+
}
|
|
1127
|
+
return settings;
|
|
1128
|
+
}
|
|
1129
|
+
/** The first day the retention in force keeps: any day before it is never shown, deleted yet or not. */
|
|
1130
|
+
#cutoff(retentionDays) {
|
|
1131
|
+
return isoDay(this.#now().getTime() - retentionDays * DAY_MS$1);
|
|
1132
|
+
}
|
|
1133
|
+
/** The days held, oldest first. */
|
|
1134
|
+
async days() {
|
|
1135
|
+
let names;
|
|
1136
|
+
try {
|
|
1137
|
+
names = await readdir(this.directory);
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
const code = error.code;
|
|
1140
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
return names.flatMap((name) => DAY_FILE.exec(name)?.[1] ?? []).sort();
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Deletes every day whose last line is older than `retentionDays` days: a day goes once all of it is that old, so
|
|
1147
|
+
* every line is kept at least that long and at most a day longer. Whole days only; every other day is left whole.
|
|
1148
|
+
*/
|
|
1149
|
+
async prune(retentionDays) {
|
|
1150
|
+
if (!isActivityRetention(retentionDays)) throw new CommsError("USAGE", `the activity log keeps a whole number of days, 1 to 365, not ${String(retentionDays)}`);
|
|
1151
|
+
const cutoff = this.#cutoff(retentionDays);
|
|
1152
|
+
const deleted = [];
|
|
1153
|
+
for (const day of await this.days()) {
|
|
1154
|
+
if (day >= cutoff) continue;
|
|
1155
|
+
await rm(join(this.directory, `${day}.jsonl`), { force: true });
|
|
1156
|
+
await rm(join(this.directory, INDEX_DIR, `${day}.json`), { force: true }).catch(() => void 0);
|
|
1157
|
+
await rm(join(this.directory, PARENTS_DIR, `${day}.jsonl`), { force: true }).catch(() => void 0);
|
|
1158
|
+
deleted.push(day);
|
|
1159
|
+
}
|
|
1160
|
+
return { deleted };
|
|
1161
|
+
}
|
|
1162
|
+
async #lines(day) {
|
|
1163
|
+
const text = await this.#text(day);
|
|
1164
|
+
return text === null ? [] : text.split("\n").flatMap((raw) => raw ? readLine(raw) ?? [] : []);
|
|
1165
|
+
}
|
|
1166
|
+
async #text(day) {
|
|
1167
|
+
try {
|
|
1168
|
+
return await readFile(join(this.directory, `${day}.jsonl`), "utf8");
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (error.code === "ENOENT") return null;
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* A whole past day's sessions, from its index when the index was made from the file as it is now, else from the
|
|
1176
|
+
* file — and the index written again, best effort. Today's is never indexed: it is still being written.
|
|
1177
|
+
*/
|
|
1178
|
+
async #dayAggregates(day) {
|
|
1179
|
+
const file = join(this.directory, `${day}.jsonl`);
|
|
1180
|
+
const index = join(this.directory, INDEX_DIR, `${day}.json`);
|
|
1181
|
+
let mark;
|
|
1182
|
+
try {
|
|
1183
|
+
const info = await stat(file);
|
|
1184
|
+
mark = `${info.size}:${info.mtimeMs}`;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return [];
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const held = JSON.parse(await readFile(index, "utf8"));
|
|
1190
|
+
const sessions = held.version === 1 && held.mark === mark ? aggregatesOf(held.sessions) : null;
|
|
1191
|
+
if (sessions !== null) return sessions;
|
|
1192
|
+
} catch {}
|
|
1193
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const line of await this.#lines(day)) add(groups, aggregateOf(line));
|
|
1195
|
+
const sessions = [...groups.values()];
|
|
1196
|
+
await writeFileAtomic(index, `${JSON.stringify({
|
|
1197
|
+
version: 1,
|
|
1198
|
+
mark,
|
|
1199
|
+
sessions
|
|
1200
|
+
})}\n`).catch(() => void 0);
|
|
1201
|
+
return sessions;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Every session with a line at or after `since` (every day the retention keeps, when left out), newest first. A day
|
|
1205
|
+
* past the retention is never read, whether or not it has been deleted yet.
|
|
1206
|
+
*/
|
|
1207
|
+
async sessions(options = {}) {
|
|
1208
|
+
const since = options.since;
|
|
1209
|
+
const today = isoDay(this.#now().getTime());
|
|
1210
|
+
const cutoff = this.#cutoff((await this.#maintained()).retentionDays);
|
|
1211
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1212
|
+
for (const day of await this.days()) {
|
|
1213
|
+
if (day < cutoff) continue;
|
|
1214
|
+
if (since !== void 0 && `${day}T23:59:59.999Z` < since) continue;
|
|
1215
|
+
if ((since === void 0 || since <= `${day}T00:00:00.000Z`) && day < today) {
|
|
1216
|
+
for (const aggregate of await this.#dayAggregates(day)) add(groups, aggregate);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
for (const line of await this.#lines(day)) if (since === void 0 || line.at >= since) add(groups, aggregateOf(line));
|
|
1220
|
+
}
|
|
1221
|
+
return [...groups.values()].map(summaryOf$1).sort((a, b) => a.lastSeen === b.lastSeen ? a.key.localeCompare(b.key) : a.lastSeen < b.lastSeen ? 1 : -1);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* One session's lines, newest first, that match every filter given; and its summary over the time asked. Only
|
|
1225
|
+
* lines whose session key is exactly `key` are ever read into the answer, and none of a day past the retention.
|
|
1226
|
+
*/
|
|
1227
|
+
async show(key, filters = {}) {
|
|
1228
|
+
if (!SESSION_KEY.test(key)) throw new CommsError("USAGE", `"${cut(key, 40)}" is not a session key`);
|
|
1229
|
+
const cutoff = this.#cutoff((await this.#maintained()).retentionDays);
|
|
1230
|
+
const limit = filters.limit ?? DEFAULT_SHOW_LIMIT;
|
|
1231
|
+
const since = filters.since;
|
|
1232
|
+
const needle = `"key":"${key}"`;
|
|
1233
|
+
const lines = [];
|
|
1234
|
+
let matched = 0;
|
|
1235
|
+
let summary = null;
|
|
1236
|
+
const days = (await this.days()).filter((day) => day >= cutoff).reverse();
|
|
1237
|
+
for (const day of days) {
|
|
1238
|
+
if (since !== void 0 && `${day}T23:59:59.999Z` < since) break;
|
|
1239
|
+
const text = await this.#text(day);
|
|
1240
|
+
if (text === null) continue;
|
|
1241
|
+
const raws = text.split("\n");
|
|
1242
|
+
for (let index = raws.length - 1; index >= 0; index -= 1) {
|
|
1243
|
+
const raw = raws[index];
|
|
1244
|
+
if (!raw.includes(needle)) continue;
|
|
1245
|
+
const line = readLine(raw);
|
|
1246
|
+
if (line === null || line.session.key !== key) continue;
|
|
1247
|
+
if (since !== void 0 && line.at < since) continue;
|
|
1248
|
+
const aggregate = aggregateOf(line);
|
|
1249
|
+
summary = summary === null ? aggregate : merge(summary, aggregate);
|
|
1250
|
+
if (!matches(line, filters)) continue;
|
|
1251
|
+
lines.push(line);
|
|
1252
|
+
matched += 1;
|
|
1253
|
+
if (lines.length >= limit * 2) {
|
|
1254
|
+
lines.sort(newestFirst);
|
|
1255
|
+
lines.length = limit;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
lines.sort(newestFirst);
|
|
1260
|
+
if (lines.length > limit) lines.length = limit;
|
|
1261
|
+
return {
|
|
1262
|
+
lines,
|
|
1263
|
+
more: matched > limit,
|
|
1264
|
+
summary: summary === null ? null : summaryOf$1(summary)
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* The window a CLI call belongs to (S2): the session of a server process, today or yesterday, whose parent is one of
|
|
1269
|
+
* `ancestors` — the nearest first. Null when none is. Read from each day's parents file; a day with none — written
|
|
1270
|
+
* by an earlier build — is read whole, as before.
|
|
1271
|
+
*/
|
|
1272
|
+
async sessionOfAncestor(ancestors) {
|
|
1273
|
+
const now = this.#now().getTime();
|
|
1274
|
+
const noted = [];
|
|
1275
|
+
const texts = [];
|
|
1276
|
+
for (const day of [isoDay(now), isoDay(now - DAY_MS$1)]) {
|
|
1277
|
+
const parents = await this.#parentsOf(day);
|
|
1278
|
+
if (parents !== null) {
|
|
1279
|
+
noted.push(...parents);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const text = await this.#text(day).catch(() => null);
|
|
1283
|
+
if (text !== null) texts.push(text);
|
|
1284
|
+
}
|
|
1285
|
+
for (const ancestor of ancestors) {
|
|
1286
|
+
const found = noted.find((each) => each.parent.pid === ancestor.pid && each.parent.startedAt === ancestor.startedAt);
|
|
1287
|
+
if (found !== void 0) return found;
|
|
1288
|
+
const needle = `"parent":{"pid":${ancestor.pid},"startedAt":${JSON.stringify(ancestor.startedAt)}`;
|
|
1289
|
+
for (const text of texts) {
|
|
1290
|
+
let from = text.indexOf(needle);
|
|
1291
|
+
while (from !== -1) {
|
|
1292
|
+
const start = text.lastIndexOf("\n", from) + 1;
|
|
1293
|
+
const end = text.indexOf("\n", from);
|
|
1294
|
+
const line = readLine(text.slice(start, end === -1 ? text.length : end));
|
|
1295
|
+
const parent = line?.session.parent;
|
|
1296
|
+
if (line?.session.grouped && line.session.surface === "mcp" && parent?.pid === ancestor.pid && parent.startedAt === ancestor.startedAt) return {
|
|
1297
|
+
key: line.session.key,
|
|
1298
|
+
parent,
|
|
1299
|
+
folder: line.session.folder
|
|
1300
|
+
};
|
|
1301
|
+
from = text.indexOf(needle, from + needle.length);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
/** The last failure recorded by any process — the newer of the file beside the log and this process's own. */
|
|
1308
|
+
async readFailure() {
|
|
1309
|
+
let stored = null;
|
|
1310
|
+
try {
|
|
1311
|
+
const raw = own$3(JSON.parse(await readFile(this.#failureFile, "utf8")));
|
|
1312
|
+
if (raw !== void 0 && typeof raw.at === "string" && ISO_TIME.test(raw.at)) stored = {
|
|
1313
|
+
at: raw.at,
|
|
1314
|
+
code: cut(raw.code, BOUNDS.word) ?? "error",
|
|
1315
|
+
message: cut(raw.message, BOUNDS.reason) ?? "unknown",
|
|
1316
|
+
pid: whole(raw.pid) ?? 0
|
|
1317
|
+
};
|
|
1318
|
+
} catch {}
|
|
1319
|
+
const mine = this.#failure;
|
|
1320
|
+
if (mine === null) return stored;
|
|
1321
|
+
if (stored === null) return mine;
|
|
1322
|
+
return mine.at >= stored.at ? mine : stored;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* What `doctor` shows of the log. It creates nothing; like every read it first deletes the days past the retention,
|
|
1326
|
+
* once a day per process, so the days it lists past the retention are those that could not be deleted.
|
|
1327
|
+
*/
|
|
1328
|
+
async health() {
|
|
1329
|
+
await this.#maintained();
|
|
1330
|
+
const today = isoDay(this.#now().getTime());
|
|
1331
|
+
let exists = false;
|
|
1332
|
+
let isDirectory = true;
|
|
1333
|
+
try {
|
|
1334
|
+
const info = await stat(this.directory);
|
|
1335
|
+
exists = true;
|
|
1336
|
+
isDirectory = info.isDirectory();
|
|
1337
|
+
} catch {}
|
|
1338
|
+
const days = exists && isDirectory ? await this.days().catch(() => []) : [];
|
|
1339
|
+
let lastWritten = null;
|
|
1340
|
+
const newest = days.at(-1);
|
|
1341
|
+
if (newest !== void 0) lastWritten = await stat(join(this.directory, `${newest}.jsonl`)).then((info) => info.mtime.toISOString(), () => null);
|
|
1342
|
+
const state = join(this.directory, "..");
|
|
1343
|
+
const target = days.includes(today) ? join(this.directory, `${today}.jsonl`) : exists ? this.directory : existsSync(state) ? state : null;
|
|
1344
|
+
const writable = isDirectory && (target === null || await access(target, constants$1.W_OK).then(() => true, () => false));
|
|
1345
|
+
const loose = exists && isDirectory ? await isGroupOrWorldAccessible(this.directory).catch(() => false) : false;
|
|
1346
|
+
return {
|
|
1347
|
+
directory: this.directory,
|
|
1348
|
+
exists,
|
|
1349
|
+
isDirectory,
|
|
1350
|
+
private: !exists || !isDirectory || process.platform === "win32" ? null : !loose,
|
|
1351
|
+
writable,
|
|
1352
|
+
days,
|
|
1353
|
+
lastWritten,
|
|
1354
|
+
failure: await this.readFailure()
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
/** Newest first; lines of one time in the order they were met. */
|
|
1359
|
+
function newestFirst(a, b) {
|
|
1360
|
+
return a.at === b.at ? 0 : a.at < b.at ? 1 : -1;
|
|
1361
|
+
}
|
|
1362
|
+
function matches(line, filters) {
|
|
1363
|
+
if (filters.kind !== void 0 && line.kind !== filters.kind) return false;
|
|
1364
|
+
if (filters.outcome !== void 0 && line.outcome !== filters.outcome) return false;
|
|
1365
|
+
if (filters.channel !== void 0 && line.channel !== filters.channel) return false;
|
|
1366
|
+
if (filters.account !== void 0 && line.account.alias !== filters.account && line.account.id !== filters.account) return false;
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
/** `ps -o ppid=,lstart=,comm=` in the C locale: the parent, the start as five words, and the executable — last. */
|
|
1370
|
+
const PS_LINE = /^\s*(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{1,2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/;
|
|
1371
|
+
/** One process's line of `ps -o ppid=,lstart=,comm=`, or null for anything else. The name is the executable's own. */
|
|
1372
|
+
function parsePsLine(text) {
|
|
1373
|
+
const first = text.split("\n").find((row) => row.trim() !== "");
|
|
1374
|
+
const match = first === void 0 ? null : PS_LINE.exec(first);
|
|
1375
|
+
if (match === null) return null;
|
|
1376
|
+
const command = match[3];
|
|
1377
|
+
const name = command.includes("/") ? command.split("/").at(-1) ?? command : command;
|
|
1378
|
+
return {
|
|
1379
|
+
ppid: Number(match[1]),
|
|
1380
|
+
startedAt: match[2].replace(/\s+/g, " "),
|
|
1381
|
+
name
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
const PS_PROGRAMS = ["/bin/ps", "/usr/bin/ps"];
|
|
1385
|
+
/**
|
|
1386
|
+
* This machine's `ps`, by its full path, in the C locale so the start reads the same in every process, bounded by
|
|
1387
|
+
* `timeoutMs`. Never on Windows: no query there answers inside the bound (PowerShell alone takes longer to start),
|
|
1388
|
+
* so a Windows process is ungrouped.
|
|
1389
|
+
*/
|
|
1390
|
+
const psProbe = (pid, timeoutMs) => new Promise((resolve) => {
|
|
1391
|
+
const program = PS_PROGRAMS.find((path) => existsSync(path));
|
|
1392
|
+
if (process.platform === "win32" || program === void 0) {
|
|
1393
|
+
resolve(null);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
execFile(program, [
|
|
1398
|
+
"-o",
|
|
1399
|
+
"ppid=,lstart=,comm=",
|
|
1400
|
+
"-p",
|
|
1401
|
+
String(pid)
|
|
1402
|
+
], {
|
|
1403
|
+
timeout: Math.max(1, Math.round(timeoutMs)),
|
|
1404
|
+
env: {
|
|
1405
|
+
LC_ALL: "C",
|
|
1406
|
+
LANG: "C",
|
|
1407
|
+
PATH: "/bin:/usr/bin"
|
|
1408
|
+
},
|
|
1409
|
+
maxBuffer: 65536,
|
|
1410
|
+
windowsHide: true
|
|
1411
|
+
}, (error, stdout) => resolve(error ? null : parsePsLine(String(stdout)))).on("error", () => resolve(null));
|
|
1412
|
+
} catch {
|
|
1413
|
+
resolve(null);
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
function randomKey() {
|
|
1417
|
+
return randomBytes(8).toString("hex");
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* What a process has worked out of its session: its random ids, its background reads of its parent, and a CLI call's
|
|
1421
|
+
* identity. One per process (`sessionIdentity` keeps its own); a test makes its own.
|
|
1422
|
+
*/
|
|
1423
|
+
var SessionIdentityCache = class {
|
|
1424
|
+
/** A random id this process made when it started (§4.3). */
|
|
1425
|
+
processId = randomBytes(6).toString("hex");
|
|
1426
|
+
/** The key of this process's own session, when it is not grouped with others. */
|
|
1427
|
+
ownKey = randomKey();
|
|
1428
|
+
};
|
|
1429
|
+
/** Each cache's background reads, and its CLI identity by log: held here, so the class shows only its ids. */
|
|
1430
|
+
const READS = /* @__PURE__ */ new WeakMap();
|
|
1431
|
+
const CLI_IDENTITIES = /* @__PURE__ */ new WeakMap();
|
|
1432
|
+
function heldIn(map, cache) {
|
|
1433
|
+
let held = map.get(cache);
|
|
1434
|
+
if (held === void 0) {
|
|
1435
|
+
held = /* @__PURE__ */ new Map();
|
|
1436
|
+
map.set(cache, held);
|
|
1437
|
+
}
|
|
1438
|
+
return held;
|
|
1439
|
+
}
|
|
1440
|
+
const PROCESS_CACHE = new SessionIdentityCache();
|
|
1441
|
+
/** This process's own random id: a line handed in without one names this process. */
|
|
1442
|
+
const PROCESS_ID = PROCESS_CACHE.processId;
|
|
1443
|
+
/** `promise`, or null after `ms`. Its timer is not unref'd: whoever awaits it keeps the process alive till then. */
|
|
1444
|
+
function within$2(promise, ms) {
|
|
1445
|
+
return new Promise((resolve) => {
|
|
1446
|
+
const timer = setTimeout(() => resolve(null), Math.max(0, ms));
|
|
1447
|
+
promise.then((value) => {
|
|
1448
|
+
clearTimeout(timer);
|
|
1449
|
+
resolve(value);
|
|
1450
|
+
}, () => {
|
|
1451
|
+
clearTimeout(timer);
|
|
1452
|
+
resolve(null);
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
function folderOf(options) {
|
|
1457
|
+
try {
|
|
1458
|
+
return (options.cwd ?? (() => process.cwd()))();
|
|
1459
|
+
} catch {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async function readAncestry(options, depth, initIsParent, budgetMs) {
|
|
1464
|
+
const platform = options.platform ?? process.platform;
|
|
1465
|
+
const pid = options.pid ?? process.pid;
|
|
1466
|
+
const folder = folderOf(options);
|
|
1467
|
+
const chain = [];
|
|
1468
|
+
if (platform === "win32") return {
|
|
1469
|
+
pid,
|
|
1470
|
+
folder,
|
|
1471
|
+
chain,
|
|
1472
|
+
complete: true
|
|
1473
|
+
};
|
|
1474
|
+
const probe = options.probe ?? psProbe;
|
|
1475
|
+
const deadline = Date.now() + budgetMs;
|
|
1476
|
+
let next = options.ppid ?? process.ppid;
|
|
1477
|
+
while (chain.length < depth && (next > 1 || initIsParent && next === 1 && chain.length === 0)) {
|
|
1478
|
+
const left = deadline - Date.now();
|
|
1479
|
+
if (left <= 0) return {
|
|
1480
|
+
pid,
|
|
1481
|
+
folder,
|
|
1482
|
+
chain,
|
|
1483
|
+
complete: false
|
|
1484
|
+
};
|
|
1485
|
+
const facts = await within$2(probe(next, left), left);
|
|
1486
|
+
if (facts === null) return {
|
|
1487
|
+
pid,
|
|
1488
|
+
folder,
|
|
1489
|
+
chain,
|
|
1490
|
+
complete: false
|
|
1491
|
+
};
|
|
1492
|
+
chain.push({
|
|
1493
|
+
pid: next,
|
|
1494
|
+
startedAt: facts.startedAt,
|
|
1495
|
+
name: facts.name,
|
|
1496
|
+
ppid: facts.ppid
|
|
1497
|
+
});
|
|
1498
|
+
if (next === 1) break;
|
|
1499
|
+
next = facts.ppid;
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
pid,
|
|
1503
|
+
folder,
|
|
1504
|
+
chain,
|
|
1505
|
+
complete: true
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* The background read of this process's parent, begun if none is under way or kept: a read that finished is kept for
|
|
1510
|
+
* good, one under way is waited on, and one that ran out of time is begun again once a minute has passed.
|
|
1511
|
+
*/
|
|
1512
|
+
function ancestryRead(cache, options, initIsParent) {
|
|
1513
|
+
const id = initIsParent ? "daemon" : "server";
|
|
1514
|
+
const now = (options.now ?? Date.now)();
|
|
1515
|
+
const reads = heldIn(READS, cache);
|
|
1516
|
+
const held = reads.get(id);
|
|
1517
|
+
if (held !== void 0 && (held.settled === null || held.settled.complete || now - held.at < 6e4)) return held;
|
|
1518
|
+
const read = {
|
|
1519
|
+
at: now,
|
|
1520
|
+
settled: null,
|
|
1521
|
+
promise: readAncestry(options, 1, initIsParent, options.budgetMs ?? 2e3)
|
|
1522
|
+
};
|
|
1523
|
+
read.promise.then((ancestry) => {
|
|
1524
|
+
read.settled = ancestry;
|
|
1525
|
+
}, () => void 0);
|
|
1526
|
+
reads.set(id, read);
|
|
1527
|
+
return read;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Begins working out this process's session in the background, as an MCP server or the event service starts, so its
|
|
1531
|
+
* first calls find it done: a read of its parent of up to `SESSION_IDENTITY_BACKGROUND_MS`. Returns at once; never
|
|
1532
|
+
* throws. A CLI call reads its own, and is not warmed.
|
|
1533
|
+
*/
|
|
1534
|
+
function warmSessionIdentity(surface = "mcp", options = {}) {
|
|
1535
|
+
if (surface === "cli") return;
|
|
1536
|
+
try {
|
|
1537
|
+
ancestryRead(options.cache ?? PROCESS_CACHE, options, surface === "daemon");
|
|
1538
|
+
} catch {}
|
|
1539
|
+
}
|
|
1540
|
+
function windowKey(parent, folder, connection) {
|
|
1541
|
+
const named = connection === void 0 || connection === "" ? "" : `\0${connection}`;
|
|
1542
|
+
return createHash("sha256").update(`${parent.pid}\0${parent.startedAt}\0${folder}${named}`).digest("hex").slice(0, 16);
|
|
1543
|
+
}
|
|
1544
|
+
/** System folders, each with everything under it: no project is started there. */
|
|
1545
|
+
const POSIX_SYSTEM_TREES = [
|
|
1546
|
+
"/System",
|
|
1547
|
+
"/Library",
|
|
1548
|
+
"/Applications",
|
|
1549
|
+
"/usr",
|
|
1550
|
+
"/bin",
|
|
1551
|
+
"/sbin",
|
|
1552
|
+
"/etc",
|
|
1553
|
+
"/dev",
|
|
1554
|
+
"/proc",
|
|
1555
|
+
"/sys",
|
|
1556
|
+
"/boot"
|
|
1557
|
+
];
|
|
1558
|
+
/** Folders that hold everyone's projects, or anyone's temporary files — only the folder itself, not what is in it. */
|
|
1559
|
+
const POSIX_SHARED = [
|
|
1560
|
+
"/tmp",
|
|
1561
|
+
"/var",
|
|
1562
|
+
"/private",
|
|
1563
|
+
"/private/tmp",
|
|
1564
|
+
"/private/var",
|
|
1565
|
+
"/opt",
|
|
1566
|
+
"/Volumes",
|
|
1567
|
+
"/mnt",
|
|
1568
|
+
"/media"
|
|
1569
|
+
];
|
|
1570
|
+
const WINDOWS_SYSTEM_TREES = [
|
|
1571
|
+
"\\windows",
|
|
1572
|
+
"\\program files",
|
|
1573
|
+
"\\program files (x86)",
|
|
1574
|
+
"\\programdata"
|
|
1575
|
+
];
|
|
1576
|
+
/**
|
|
1577
|
+
* Whether a folder is one that cannot tell projects apart (S3): a filesystem's root, the home folder or a folder above
|
|
1578
|
+
* it, a system folder or anything in one, or a shared one such as `/tmp`. A client that starts every server from one
|
|
1579
|
+
* process there — a desktop app started in `/`, say — would otherwise make every project it serves one session.
|
|
1580
|
+
*/
|
|
1581
|
+
function sharedFolder(folder, home, platform = process.platform) {
|
|
1582
|
+
if (platform === "win32") {
|
|
1583
|
+
const plain = (path) => path.replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
|
|
1584
|
+
const at = plain(folder);
|
|
1585
|
+
const homeAt = plain(home);
|
|
1586
|
+
if (/^[a-z]:$/.test(at) || at === "") return true;
|
|
1587
|
+
if (homeAt !== "" && (at === homeAt || homeAt.startsWith(`${at}\\`))) return true;
|
|
1588
|
+
const rest = at.replace(/^[a-z]:/, "");
|
|
1589
|
+
return rest === "\\users" || WINDOWS_SYSTEM_TREES.some((tree) => rest === tree || rest.startsWith(`${tree}\\`));
|
|
1590
|
+
}
|
|
1591
|
+
const plain = (path) => (path.length > 1 ? path.replace(/\/+$/, "") : path) || "/";
|
|
1592
|
+
const at = plain(folder);
|
|
1593
|
+
const homeAt = plain(home);
|
|
1594
|
+
if (at === "/") return true;
|
|
1595
|
+
if (homeAt !== "/" && (at === homeAt || homeAt.startsWith(`${at}/`))) return true;
|
|
1596
|
+
if (POSIX_SHARED.includes(at)) return true;
|
|
1597
|
+
return POSIX_SYSTEM_TREES.some((tree) => at === tree || at.startsWith(`${tree}/`));
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Which session this process's calls belong to (S2): its id, its parent's id, start and name, its folder and the
|
|
1601
|
+
* session key. Never throws.
|
|
1602
|
+
*
|
|
1603
|
+
* - **An MCP server** (`mcp`) is grouped with every other server its parent started in the same folder: one agent
|
|
1604
|
+
* window. Its parent is read in the background — begun by `warmSessionIdentity` as the server starts, or by its first
|
|
1605
|
+
* call — for up to two seconds; a call waits for it about 200 ms and, when it is not done, goes on alone for now. A
|
|
1606
|
+
* read that ran out of time is not kept: one a minute or more later tries again. A parent that has gone, a folder
|
|
1607
|
+
* that cannot be read, and a folder that cannot tell projects apart (`sharedFolder`: `/`, the home folder, a system
|
|
1608
|
+
* folder) leave it ungrouped, with a key of its own.
|
|
1609
|
+
* - **A CLI call** (`cli`) reads up to three processes above it, within about 200 ms. When one of them is the parent
|
|
1610
|
+
* of a server process that recorded a line today (or yesterday) in `options.log`, the call joins that window's
|
|
1611
|
+
* session; otherwise it is a session of its own. Its `ancestors` name those processes either way.
|
|
1612
|
+
* - **The event service** (`daemon`) is grouped by its parent too, process 1 included, whatever its folder — it serves
|
|
1613
|
+
* no project — so it is one session since it started. It waits for the whole read: it records on a timer, and keeps
|
|
1614
|
+
* what it is given.
|
|
1615
|
+
* - **On Windows** no process is grouped: a query of a process's parent and start does not answer within the bound.
|
|
1616
|
+
*
|
|
1617
|
+
* `clientInfo` is what an MCP client said it is; it is stored as that, and nothing depends on it.
|
|
1618
|
+
*/
|
|
1619
|
+
async function sessionIdentity(surface, clientInfo, options = {}) {
|
|
1620
|
+
const seams = options.pid !== void 0 || options.ppid !== void 0 || options.cwd !== void 0 || options.probe !== void 0 || options.platform !== void 0 || options.budgetMs !== void 0 || options.waitMs !== void 0 || options.home !== void 0 || options.now !== void 0;
|
|
1621
|
+
const cache = options.cache ?? (seams ? null : PROCESS_CACHE);
|
|
1622
|
+
const client = clientOf(clientInfo);
|
|
1623
|
+
try {
|
|
1624
|
+
if (surface === "cli" && cache !== null) {
|
|
1625
|
+
const directory = options.log?.directory ?? "";
|
|
1626
|
+
const identities = heldIn(CLI_IDENTITIES, cache);
|
|
1627
|
+
let held = identities.get(directory);
|
|
1628
|
+
if (held === void 0) {
|
|
1629
|
+
held = identityOf$1(surface, options, cache);
|
|
1630
|
+
identities.set(directory, held);
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
...await held,
|
|
1634
|
+
...client === void 0 ? {} : { client }
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
...await identityOf$1(surface, options, cache),
|
|
1639
|
+
...client === void 0 ? {} : { client }
|
|
1640
|
+
};
|
|
1641
|
+
} catch {
|
|
1642
|
+
return {
|
|
1643
|
+
key: cache?.ownKey ?? randomKey(),
|
|
1644
|
+
process: cache?.processId ?? PROCESS_ID,
|
|
1645
|
+
pid: options.pid ?? process.pid,
|
|
1646
|
+
...client === void 0 ? {} : { client },
|
|
1647
|
+
folder: null,
|
|
1648
|
+
grouped: false,
|
|
1649
|
+
surface
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
async function identityOf$1(surface, options, cache) {
|
|
1654
|
+
const own = cache?.ownKey ?? randomKey();
|
|
1655
|
+
const processId = cache?.processId ?? randomBytes(6).toString("hex");
|
|
1656
|
+
const parentOfChain = (chain, index) => {
|
|
1657
|
+
const held = chain[index];
|
|
1658
|
+
return held === void 0 ? void 0 : {
|
|
1659
|
+
pid: held.pid,
|
|
1660
|
+
startedAt: held.startedAt,
|
|
1661
|
+
name: held.name
|
|
1662
|
+
};
|
|
1663
|
+
};
|
|
1664
|
+
if (surface === "cli") {
|
|
1665
|
+
const { pid, folder, chain } = await readAncestry(options, 3, false, options.budgetMs ?? 200);
|
|
1666
|
+
const base = {
|
|
1667
|
+
process: processId,
|
|
1668
|
+
pid,
|
|
1669
|
+
folder,
|
|
1670
|
+
surface
|
|
1671
|
+
};
|
|
1672
|
+
const ancestors = chain.map((held) => held.name);
|
|
1673
|
+
const parents = chain.map((_, index) => parentOfChain(chain, index));
|
|
1674
|
+
const joined = parents.length > 0 && options.log ? await options.log.sessionOfAncestor(parents) : null;
|
|
1675
|
+
if (joined !== null) return {
|
|
1676
|
+
key: joined.key,
|
|
1677
|
+
...base,
|
|
1678
|
+
parent: joined.parent,
|
|
1679
|
+
ancestors,
|
|
1680
|
+
grouped: true
|
|
1681
|
+
};
|
|
1682
|
+
const parent = parentOfChain(chain, 0);
|
|
1683
|
+
return {
|
|
1684
|
+
key: own,
|
|
1685
|
+
...base,
|
|
1686
|
+
...parent === void 0 ? {} : { parent },
|
|
1687
|
+
ancestors,
|
|
1688
|
+
grouped: false
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const initIsParent = surface === "daemon";
|
|
1692
|
+
let ancestry;
|
|
1693
|
+
if (cache === null) ancestry = await readAncestry(options, 1, initIsParent, options.budgetMs ?? 200);
|
|
1694
|
+
else {
|
|
1695
|
+
const read = ancestryRead(cache, options, initIsParent);
|
|
1696
|
+
const wait = initIsParent ? options.budgetMs ?? 2e3 : options.waitMs ?? 200;
|
|
1697
|
+
ancestry = read.settled ?? await within$2(read.promise, wait);
|
|
1698
|
+
}
|
|
1699
|
+
if (ancestry === null) return {
|
|
1700
|
+
key: own,
|
|
1701
|
+
process: processId,
|
|
1702
|
+
pid: options.pid ?? process.pid,
|
|
1703
|
+
folder: folderOf(options),
|
|
1704
|
+
grouped: false,
|
|
1705
|
+
surface
|
|
1706
|
+
};
|
|
1707
|
+
const { pid, folder, chain } = ancestry;
|
|
1708
|
+
const base = {
|
|
1709
|
+
process: processId,
|
|
1710
|
+
pid,
|
|
1711
|
+
folder,
|
|
1712
|
+
surface
|
|
1713
|
+
};
|
|
1714
|
+
const parent = parentOfChain(chain, 0);
|
|
1715
|
+
const home = options.home ?? homedir();
|
|
1716
|
+
const shared = !initIsParent && folder !== null && sharedFolder(folder, home, options.platform ?? process.platform);
|
|
1717
|
+
if (parent === void 0 || folder === null || shared) return {
|
|
1718
|
+
key: own,
|
|
1719
|
+
...base,
|
|
1720
|
+
...parent === void 0 ? {} : { parent },
|
|
1721
|
+
grouped: false
|
|
1722
|
+
};
|
|
1723
|
+
return {
|
|
1724
|
+
key: windowKey(parent, folder, options.connection),
|
|
1725
|
+
...base,
|
|
1726
|
+
parent,
|
|
1727
|
+
grouped: true
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Records one line in `core`'s activity log. Never throws, and never fails the call it describes: a line that cannot
|
|
1732
|
+
* be written is reported by the log itself (`ActivityLog.append`). Recording off writes nothing.
|
|
1733
|
+
*
|
|
1734
|
+
* For a line a channel builds itself — the event service's `told` lines (S1), say. Around an operation, use
|
|
1735
|
+
* `withActivity`, which times it and records how it ended.
|
|
1736
|
+
*/
|
|
1737
|
+
async function recordActivity(core, line) {
|
|
1738
|
+
try {
|
|
1739
|
+
await core.activity.append(line);
|
|
1740
|
+
} catch {}
|
|
1741
|
+
}
|
|
1742
|
+
/** The refusals a call can end in — a policy, an approval, a rule, a cap or a usage mistake — rather than a failure. */
|
|
1743
|
+
const REFUSED_CODES = /* @__PURE__ */ new Set([
|
|
1744
|
+
"APPROVAL_REQUIRED",
|
|
1745
|
+
"APPROVAL_PENDING",
|
|
1746
|
+
"APPROVAL_EXPIRED",
|
|
1747
|
+
"APPROVAL_VOID",
|
|
1748
|
+
"SEND_REFUSED",
|
|
1749
|
+
"POLICY_NEVER",
|
|
1750
|
+
"ACT_NOT_ALLOWED",
|
|
1751
|
+
"STANDING_REFUSED",
|
|
1752
|
+
"STANDING_LIMIT",
|
|
1753
|
+
"RATE_CAPPED",
|
|
1754
|
+
"LOOSENING_REFUSED",
|
|
1755
|
+
"USAGE",
|
|
1756
|
+
"WHATSAPP_VISIBILITY_SEAM_REQUIRED"
|
|
1757
|
+
]);
|
|
1758
|
+
function failureDetail(error) {
|
|
1759
|
+
const code = error instanceof CommsError ? error.code : "UNEXPECTED";
|
|
1760
|
+
return {
|
|
1761
|
+
outcome: REFUSED_CODES.has(code) ? "refused" : "failed",
|
|
1762
|
+
reason: `${code}: ${messageOf$2(error)}`
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
async function recordCall(context, operation, kind, ms, detail) {
|
|
1766
|
+
try {
|
|
1767
|
+
const clientInfo = typeof context.clientInfo === "function" ? context.clientInfo() : context.clientInfo;
|
|
1768
|
+
const session = context.session ?? await sessionIdentity(context.surface, clientInfo, {
|
|
1769
|
+
log: context.core.activity,
|
|
1770
|
+
connection: typeof context.connection === "function" ? context.connection() : context.connection
|
|
1771
|
+
});
|
|
1772
|
+
const query = detail.query ?? context.query;
|
|
1773
|
+
await recordActivity(context.core, {
|
|
1774
|
+
channel: context.channel,
|
|
1775
|
+
account: detail.account ?? context.account,
|
|
1776
|
+
operation,
|
|
1777
|
+
kind,
|
|
1778
|
+
outcome: detail.outcome ?? "ok",
|
|
1779
|
+
...detail.reason === void 0 ? {} : { reason: detail.reason },
|
|
1780
|
+
session,
|
|
1781
|
+
ms,
|
|
1782
|
+
...detail.items === void 0 ? {} : { items: detail.items },
|
|
1783
|
+
...detail.count === void 0 ? {} : { count: detail.count },
|
|
1784
|
+
...detail.counts === void 0 ? {} : { counts: detail.counts },
|
|
1785
|
+
...query === void 0 ? {} : { query },
|
|
1786
|
+
...detail.recipients === void 0 ? {} : { recipients: detail.recipients },
|
|
1787
|
+
...detail.approvalId === void 0 ? {} : { approvalId: detail.approvalId },
|
|
1788
|
+
...detail.told === void 0 ? {} : { told: detail.told }
|
|
1789
|
+
});
|
|
1790
|
+
} catch {}
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Runs one operation and records its line (§8): times `fn`, and when it returns records `describe(result)` as `ok`
|
|
1794
|
+
* (or the outcome `describe` names); when it throws, records `refused` for a policy's, an approval's or a rule's
|
|
1795
|
+
* refusal and `failed` for anything else, with the error's code and message as the reason, and throws the same error
|
|
1796
|
+
* on. What it returns, or throws, is exactly what `fn` did: recording never changes or fails the call, and a
|
|
1797
|
+
* `describe` that throws records the line without its details.
|
|
1798
|
+
*
|
|
1799
|
+
* `operationId` is the capability id in `capabilities.json` (`gmail.search`); `kind` is `read`, `write`, `told` or
|
|
1800
|
+
* `approval`.
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* const found = await withActivity(
|
|
1804
|
+
* { core, channel: 'gmail', surface: 'mcp', account: { id: inbox.id, alias }, clientInfo: () => server.getClientVersion() },
|
|
1805
|
+
* 'gmail.search',
|
|
1806
|
+
* 'read',
|
|
1807
|
+
* () => search(core, request),
|
|
1808
|
+
* (result) => ({ query: request.query, count: result.total, items: result.messages.map((m) => ({ id: m.id, subject: m.subject, from: m.from })) }),
|
|
1809
|
+
* );
|
|
1810
|
+
*/
|
|
1811
|
+
async function withActivity(context, operationId, kind, fn, describe) {
|
|
1812
|
+
const started = performance.now();
|
|
1813
|
+
let result;
|
|
1814
|
+
try {
|
|
1815
|
+
result = await fn();
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
await recordCall(context, operationId, kind, performance.now() - started, failureDetail(error));
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
const ms = performance.now() - started;
|
|
1821
|
+
let detail = {};
|
|
1822
|
+
try {
|
|
1823
|
+
detail = describe(result) ?? {};
|
|
1824
|
+
} catch {
|
|
1825
|
+
detail = {};
|
|
1826
|
+
}
|
|
1827
|
+
await recordCall(context, operationId, kind, ms, detail);
|
|
1828
|
+
return result;
|
|
1829
|
+
}
|
|
1830
|
+
function sha256Hex(data) {
|
|
1831
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1832
|
+
}
|
|
1833
|
+
/** Lower-cases the address part of `Name <addr>` or a bare address; display names are dropped. */
|
|
1834
|
+
function normaliseAddress(value) {
|
|
1835
|
+
return (/<([^<>]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
|
|
1836
|
+
}
|
|
1837
|
+
function collapseWhitespace(text) {
|
|
1838
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1839
|
+
}
|
|
1840
|
+
/** Deterministic JSON: keys sorted at every level, undefined members dropped. */
|
|
1841
|
+
function canonicalJson$1(value) {
|
|
1842
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
|
|
1843
|
+
if (value && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson$1(v)}`).join(",")}}`;
|
|
1844
|
+
return JSON.stringify(value);
|
|
1845
|
+
}
|
|
538
1846
|
/**
|
|
539
1847
|
* Opening `wx` failed because someone else holds the path — retry, rather than failing the command.
|
|
540
1848
|
*
|
|
@@ -3681,7 +4989,7 @@ async function launcherCanStart(runtimeDir, name) {
|
|
|
3681
4989
|
const cli = launcherCli(runtimeDir, name);
|
|
3682
4990
|
try {
|
|
3683
4991
|
if (!(await stat(cli)).isFile()) return false;
|
|
3684
|
-
await access(cli, constants
|
|
4992
|
+
await access(cli, constants.R_OK);
|
|
3685
4993
|
return true;
|
|
3686
4994
|
} catch {
|
|
3687
4995
|
return false;
|
|
@@ -3692,7 +5000,7 @@ function launcherCanStartSync(runtimeDir, name) {
|
|
|
3692
5000
|
const cli = launcherCli(runtimeDir, name);
|
|
3693
5001
|
try {
|
|
3694
5002
|
if (!statSync(cli).isFile()) return false;
|
|
3695
|
-
accessSync(cli, constants
|
|
5003
|
+
accessSync(cli, constants.R_OK);
|
|
3696
5004
|
return true;
|
|
3697
5005
|
} catch {
|
|
3698
5006
|
return false;
|
|
@@ -3981,7 +5289,7 @@ function nodeProgram(env, platform = process.platform) {
|
|
|
3981
5289
|
const candidate = join(directory, name);
|
|
3982
5290
|
try {
|
|
3983
5291
|
if (!statSync(candidate).isFile()) continue;
|
|
3984
|
-
accessSync(candidate, constants
|
|
5292
|
+
accessSync(candidate, constants.X_OK);
|
|
3985
5293
|
if (!sameRealPath(realpathSync(candidate), real, platform)) continue;
|
|
3986
5294
|
return sameRealPath(candidate, own, platform) ? own : candidate;
|
|
3987
5295
|
} catch {}
|
|
@@ -4237,8 +5545,8 @@ function managedShimStarting(context, binary, entry) {
|
|
|
4237
5545
|
if (recorded === void 0 || recorded.path !== path) return null;
|
|
4238
5546
|
if (!lstatSync(path).isFile() || sha256(readFileSync(path, "utf8")) !== recorded.digest) return null;
|
|
4239
5547
|
if (realpathSync(recorded.entry) !== realpathSync(entry)) return null;
|
|
4240
|
-
accessSync(recorded.node, constants
|
|
4241
|
-
accessSync(path, constants
|
|
5548
|
+
accessSync(recorded.node, constants.X_OK);
|
|
5549
|
+
accessSync(path, constants.X_OK);
|
|
4242
5550
|
return publicPath;
|
|
4243
5551
|
} catch {
|
|
4244
5552
|
return null;
|
|
@@ -4542,7 +5850,7 @@ async function executableIn(name, directories, env) {
|
|
|
4542
5850
|
for (const directory of paths) for (const extension of extensions) {
|
|
4543
5851
|
const candidate = join(directory, name + extension.toLowerCase());
|
|
4544
5852
|
try {
|
|
4545
|
-
await access(candidate, constants.X_OK);
|
|
5853
|
+
await access(candidate, constants$1.X_OK);
|
|
4546
5854
|
return candidate;
|
|
4547
5855
|
} catch {}
|
|
4548
5856
|
}
|
|
@@ -4745,7 +6053,7 @@ async function installManagedRuntime(context, product, version) {
|
|
|
4745
6053
|
async function findNpmCli() {
|
|
4746
6054
|
const candidates = [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
|
|
4747
6055
|
for (const candidate of candidates) try {
|
|
4748
|
-
await access(candidate, constants.R_OK);
|
|
6056
|
+
await access(candidate, constants$1.R_OK);
|
|
4749
6057
|
return resolve(candidate);
|
|
4750
6058
|
} catch {}
|
|
4751
6059
|
throw new CommsError("CONFIG", "npm could not be found next to this Node installation", { hint: "Install with `--launcher npx` instead, which runs the published package directly." });
|
|
@@ -4879,7 +6187,7 @@ async function localCliEntry(moduleUrl) {
|
|
|
4879
6187
|
join(here, "..", "cli.mjs")
|
|
4880
6188
|
];
|
|
4881
6189
|
for (const candidate of candidates) try {
|
|
4882
|
-
await access(candidate, constants.R_OK);
|
|
6190
|
+
await access(candidate, constants$1.R_OK);
|
|
4883
6191
|
return resolve(candidate);
|
|
4884
6192
|
} catch {}
|
|
4885
6193
|
throw new CommsError("UNEXPECTED", "cannot find this package's own command to register");
|
|
@@ -7127,14 +8435,14 @@ async function resolveInsideRoot(root, target) {
|
|
|
7127
8435
|
async function createUniqueFile(directory, filename) {
|
|
7128
8436
|
const extension = extname(filename);
|
|
7129
8437
|
const stem = extension ? filename.slice(0, -extension.length) : filename;
|
|
7130
|
-
const noFollow = process.platform === "win32" ? 0 : constants
|
|
8438
|
+
const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
|
|
7131
8439
|
for (let attempt = 1; attempt < 1e3; attempt += 1) {
|
|
7132
8440
|
const name = attempt === 1 ? filename : `${stem}-${attempt}${extension}`;
|
|
7133
8441
|
const path = join(directory, name);
|
|
7134
8442
|
try {
|
|
7135
8443
|
return {
|
|
7136
8444
|
path,
|
|
7137
|
-
handle: await open(path, constants
|
|
8445
|
+
handle: await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 384)
|
|
7138
8446
|
};
|
|
7139
8447
|
} catch (error) {
|
|
7140
8448
|
if (error.code === "EEXIST") continue;
|
|
@@ -7477,7 +8785,7 @@ const configV1Schema = looseObject({
|
|
|
7477
8785
|
version: literal$1(1),
|
|
7478
8786
|
...v1BodyShape
|
|
7479
8787
|
}).superRefine(checkV1Body);
|
|
7480
|
-
const servesSchema = union([literal$1("any"), looseObject({ domains: array(string().min(1)).min(1) })]);
|
|
8788
|
+
const servesSchema = union$1([literal$1("any"), looseObject({ domains: array(string().min(1)).min(1) })]);
|
|
7481
8789
|
const generationSchema = looseObject({
|
|
7482
8790
|
name: aliasSchema,
|
|
7483
8791
|
clientId: string().min(1),
|
|
@@ -7633,7 +8941,7 @@ const configV3Naming2Schema = looseObject({
|
|
|
7633
8941
|
...v2BodyShape,
|
|
7634
8942
|
...v3Shape
|
|
7635
8943
|
}).superRefine(checkV2Body);
|
|
7636
|
-
union([configV3Naming1Schema, configV3Naming2Schema]);
|
|
8944
|
+
union$1([configV3Naming1Schema, configV3Naming2Schema]);
|
|
7637
8945
|
function schemaFor(version, naming = 2) {
|
|
7638
8946
|
if (version === 3) return naming === 1 ? configV3Naming1Schema : configV3Naming2Schema;
|
|
7639
8947
|
return version === 2 ? configV2Schema : configV1Schema;
|
|
@@ -8440,6 +9748,10 @@ function classifyChange(before, after) {
|
|
|
8440
9748
|
const levels = behaviourLevels(behaviour.deltas);
|
|
8441
9749
|
loosen("behaviour.rules", levels.before, levels.after);
|
|
8442
9750
|
}
|
|
9751
|
+
const activityBefore = readActivitySettings(before);
|
|
9752
|
+
const activityAfter = readActivitySettings(after);
|
|
9753
|
+
if (activityBefore.record === "off" && activityAfter.record === "on") loosen("activity.record", activityBefore.record, activityAfter.record);
|
|
9754
|
+
if (activityAfter.retentionDays > activityBefore.retentionDays) loosen("activity.retentionDays", activityBefore.retentionDays, activityAfter.retentionDays);
|
|
8443
9755
|
return {
|
|
8444
9756
|
loosened: changes.map((change) => change.path),
|
|
8445
9757
|
changes
|
|
@@ -8523,7 +9835,7 @@ const ACCOUNT_SETTINGS = [
|
|
|
8523
9835
|
];
|
|
8524
9836
|
/**
|
|
8525
9837
|
* Every setting that differs between `before` and `after`, loosened or tightened: the defaults, the secret store,
|
|
8526
|
-
* the agent label's settings, the behaviour rules, and each mailbox's and account's own settings — including those of
|
|
9838
|
+
* the agent label's settings, the behaviour rules, the activity log's, and each mailbox's and account's own settings — including those of
|
|
8527
9839
|
* one added or removed.
|
|
8528
9840
|
*
|
|
8529
9841
|
* What a change approval binds, beside its loosenings. A preview shows the whole change — "sends approved by never;
|
|
@@ -8566,6 +9878,8 @@ function changedSettings(before, after) {
|
|
|
8566
9878
|
const labelAfter = storedAgentLabel(after);
|
|
8567
9879
|
for (const key of LABEL_SETTINGS) record(`agentLabel.${key}`, labelBefore[key], labelAfter[key]);
|
|
8568
9880
|
record("behaviour.rules", storedBehaviourRules(before), storedBehaviourRules(after));
|
|
9881
|
+
record("activity.record", storedActivity(before)?.record, storedActivity(after)?.record);
|
|
9882
|
+
record("activity.retentionDays", storedActivity(before)?.retentionDays, storedActivity(after)?.retentionDays);
|
|
8569
9883
|
return changes;
|
|
8570
9884
|
}
|
|
8571
9885
|
/** The behaviour rules as the file holds them — raw, ids and all, as `changedSettings` records every setting. */
|
|
@@ -12996,7 +14310,7 @@ function folding(platform) {
|
|
|
12996
14310
|
return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
|
|
12997
14311
|
}
|
|
12998
14312
|
/** The package version, read from package.json at build time. */
|
|
12999
|
-
const VERSION = "0.
|
|
14313
|
+
const VERSION = "0.29.0";
|
|
13000
14314
|
/**
|
|
13001
14315
|
* The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
|
|
13002
14316
|
*
|
|
@@ -13373,7 +14687,7 @@ function stableLauncher(dataDir, packageName, version, platform) {
|
|
|
13373
14687
|
const name = launcherName(packageName);
|
|
13374
14688
|
const { launcher } = launcherPaths(dataDir, name);
|
|
13375
14689
|
if (!statSync(launcher).isFile()) return null;
|
|
13376
|
-
accessSync(launcher, platform === "win32" ? constants
|
|
14690
|
+
accessSync(launcher, platform === "win32" ? constants.R_OK : constants.X_OK);
|
|
13377
14691
|
const pointer = readPointerSync(dataDir, name);
|
|
13378
14692
|
if (pointer === void 0 || !launcherCanStartSync(pointer.current, name)) return null;
|
|
13379
14693
|
const started = readPackageManifest(join(pointer.current, "node_modules", ...packageName.split("/")));
|
|
@@ -13546,7 +14860,7 @@ function checkedEntry(root, entry) {
|
|
|
13546
14860
|
ok: false,
|
|
13547
14861
|
why: `${entry} is not a file`
|
|
13548
14862
|
};
|
|
13549
|
-
accessSync(real, constants
|
|
14863
|
+
accessSync(real, constants.R_OK);
|
|
13550
14864
|
} catch {
|
|
13551
14865
|
return {
|
|
13552
14866
|
ok: false,
|
|
@@ -14319,7 +15633,9 @@ const SETTING_LABELS = {
|
|
|
14319
15633
|
"agentLabel.rules": "who each kind of message appears to come from",
|
|
14320
15634
|
"agentLabel.slackIcons": "the icon of the agent’s Slack posts",
|
|
14321
15635
|
"agentLabel.slackStyles": "how the agent’s Slack messages look",
|
|
14322
|
-
"behaviour.rules": "what an agent may do"
|
|
15636
|
+
"behaviour.rules": "what an agent may do",
|
|
15637
|
+
"activity.record": "recording what agent sessions read and send",
|
|
15638
|
+
"activity.retentionDays": "how many days the activity log keeps"
|
|
14323
15639
|
};
|
|
14324
15640
|
/**
|
|
14325
15641
|
* What the loosening means for the person, in their terms.
|
|
@@ -14351,6 +15667,8 @@ function meaning(field, after, scope) {
|
|
|
14351
15667
|
case "agentLabel.slackIcons": return "the app’s Slack posts will show this icon beside your name";
|
|
14352
15668
|
case "agentLabel.slackStyles": return "every agent message there goes out as you, labelled in its words, even where the app could post: it trades Slack’s own APP tag, which no one can fake, for words in the message";
|
|
14353
15669
|
case "behaviour.rules": return "where it allows more, an agent may start those acts; each one still waits for your yes";
|
|
15670
|
+
case "activity.record": return "every call an agent session makes to your accounts will be recorded on this machine again: the subjects and senders it read, what it searched for, what it sent and to whom — never a message body";
|
|
15671
|
+
case "activity.retentionDays": return "what agent sessions read and sent — subjects, senders, searches, recipients — will be kept on this machine for longer";
|
|
14354
15672
|
default: return "";
|
|
14355
15673
|
}
|
|
14356
15674
|
}
|
|
@@ -14843,7 +16161,7 @@ var FileSecretStore = class {
|
|
|
14843
16161
|
}
|
|
14844
16162
|
async get(ref) {
|
|
14845
16163
|
try {
|
|
14846
|
-
const handle = await open(this.#path(ref), constants
|
|
16164
|
+
const handle = await open(this.#path(ref), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
14847
16165
|
let raw;
|
|
14848
16166
|
try {
|
|
14849
16167
|
raw = await handle.readFile("utf8");
|
|
@@ -15284,7 +16602,7 @@ strictObject({
|
|
|
15284
16602
|
clientId: string().regex(GOOGLE_CLIENT_ID_PATTERN, "a Google client id is digits, a hyphen, then .apps.googleusercontent.com"),
|
|
15285
16603
|
clientSecret: string().regex(/^[\x20-\x7e]{1,256}$/, "the client secret is 1–256 printable ASCII characters"),
|
|
15286
16604
|
projectId: string().regex(/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, "a Google Cloud project id is 6–30 lowercase letters, digits or hyphens").optional(),
|
|
15287
|
-
serves: union([literal$1("any"), strictObject({ domains: array(string().regex(DOMAIN_PATTERN, "each domain is a lower-case host name")).min(1).max(50) })])
|
|
16605
|
+
serves: union$1([literal$1("any"), strictObject({ domains: array(string().regex(DOMAIN_PATTERN, "each domain is a lower-case host name")).min(1).max(50) })])
|
|
15288
16606
|
}).optional(),
|
|
15289
16607
|
slack: strictObject({
|
|
15290
16608
|
workspace: string().regex(SLACK_WORKSPACE_ID_PATTERN, "a Slack workspace id is T followed by capital letters or digits"),
|
|
@@ -15325,7 +16643,7 @@ function shownPath(path) {
|
|
|
15325
16643
|
async function readBounded(path, what) {
|
|
15326
16644
|
let handle;
|
|
15327
16645
|
try {
|
|
15328
|
-
handle = await open(path, constants
|
|
16646
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
15329
16647
|
} catch (error) {
|
|
15330
16648
|
return what.unopened(error);
|
|
15331
16649
|
}
|
|
@@ -16050,6 +17368,10 @@ function openCore(options = {}) {
|
|
|
16050
17368
|
plans: new PlanStore(paths.stateDir, now),
|
|
16051
17369
|
taint: new TaintStore(paths.stateDir, now),
|
|
16052
17370
|
audit,
|
|
17371
|
+
activity: new ActivityLog(paths.stateDir, {
|
|
17372
|
+
now,
|
|
17373
|
+
settings: async () => readActivitySettings(await config.load())
|
|
17374
|
+
}),
|
|
16053
17375
|
handoffs,
|
|
16054
17376
|
async secrets(kind) {
|
|
16055
17377
|
const chosen = kind ?? secretsStoreOf(await config.load());
|
|
@@ -16741,7 +18063,7 @@ async function corePackageRoot() {
|
|
|
16741
18063
|
}
|
|
16742
18064
|
throw new CommsError("UNEXPECTED", "cannot find the @agentcomms/core package this is running from");
|
|
16743
18065
|
}
|
|
16744
|
-
const exists = (path) => access(path, constants.R_OK).then(() => true, () => false);
|
|
18066
|
+
const exists = (path) => access(path, constants$1.R_OK).then(() => true, () => false);
|
|
16745
18067
|
/**
|
|
16746
18068
|
* A module URL inside a channel's package, for the `local` launcher, which registers a checkout's own code.
|
|
16747
18069
|
*
|
|
@@ -17191,7 +18513,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
17191
18513
|
};
|
|
17192
18514
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
17193
18515
|
packageName: serverFacts(request.channel).packageName,
|
|
17194
|
-
version: "0.
|
|
18516
|
+
version: "0.29.0"
|
|
17195
18517
|
};
|
|
17196
18518
|
const product = {
|
|
17197
18519
|
packageName,
|
|
@@ -19339,7 +20661,7 @@ function terminalUpdateHooks(core, env, options = {}) {
|
|
|
19339
20661
|
} };
|
|
19340
20662
|
}
|
|
19341
20663
|
//#endregion
|
|
19342
|
-
//#region ../core/dist/tool-arguments-
|
|
20664
|
+
//#region ../core/dist/tool-arguments-1XgEf3VM.mjs
|
|
19343
20665
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
19344
20666
|
const TARGET = "draft-2020-12";
|
|
19345
20667
|
/**
|
|
@@ -31782,6 +33104,11 @@ var ResendContext = class {
|
|
|
31782
33104
|
platform;
|
|
31783
33105
|
surface;
|
|
31784
33106
|
/**
|
|
33107
|
+
* What the MCP client said it is (`clientInfo`), for the activity log's session: set by the server once it exists,
|
|
33108
|
+
* read when a line is written, since a client says it only on connecting. Never set for the CLI.
|
|
33109
|
+
*/
|
|
33110
|
+
clientInfo = void 0;
|
|
33111
|
+
/**
|
|
31785
33112
|
* What every result, refusal and hint names for a person to run: Resend's own CLI as this installation runs it, or
|
|
31786
33113
|
* core's through its installed dependency (`own`, `core`), quoted for `platform` — or why there is none here.
|
|
31787
33114
|
*/
|
|
@@ -32973,6 +34300,98 @@ async function runDoctor(context, options = {}) {
|
|
|
32973
34300
|
};
|
|
32974
34301
|
}
|
|
32975
34302
|
//#endregion
|
|
34303
|
+
//#region src/activity.ts
|
|
34304
|
+
/**
|
|
34305
|
+
* Resend's half of the activity log (design 2026-10-25-activity §4.2, §8 Task 2a): every operation a capability row
|
|
34306
|
+
* names that reads or writes a Resend account records one line, through core's `withActivity`, from inside the
|
|
34307
|
+
* operation — so the command and the tool that run it record the same line.
|
|
34308
|
+
*
|
|
34309
|
+
* - **One line per call.** An operation another recorded operation calls on its way records nothing of its own; the
|
|
34310
|
+
* outer call is known by the async context it runs in, so two calls at once in one server are two lines.
|
|
34311
|
+
* - **What it touched, never what it said.** Ids, counts, subjects, senders' names and addresses, file names and
|
|
34312
|
+
* sizes, a send's recipients and its approval — never a body, a preview or bytes. A field a result carries inside the
|
|
34313
|
+
* untrusted-content envelope is taken out of it for the line (`plain`); the log wraps it again wherever it is shown.
|
|
34314
|
+
* - **Never in the way.** Working out the account never throws, and `withActivity` never changes what the call returns
|
|
34315
|
+
* or throws.
|
|
34316
|
+
*/
|
|
34317
|
+
/** Set for the length of a recorded call: an operation it calls on its way records nothing of its own. */
|
|
34318
|
+
const recording = new AsyncLocalStorage();
|
|
34319
|
+
/** The account a line is recorded against, as known before the call: its id and its name, or the name alone. */
|
|
34320
|
+
async function accountOf(context, name) {
|
|
34321
|
+
try {
|
|
34322
|
+
return {
|
|
34323
|
+
id: (await context.accounts.find(name))?.account.id ?? "",
|
|
34324
|
+
alias: name
|
|
34325
|
+
};
|
|
34326
|
+
} catch {
|
|
34327
|
+
return {
|
|
34328
|
+
id: "",
|
|
34329
|
+
alias: name
|
|
34330
|
+
};
|
|
34331
|
+
}
|
|
34332
|
+
}
|
|
34333
|
+
/**
|
|
34334
|
+
* Runs one Resend operation and records its line: `operation` is the capability id (`resend.emails.list`), `kind`
|
|
34335
|
+
* what it did, `account` the account's name as the call gives it. Inside another recorded call it only runs `fn`.
|
|
34336
|
+
*/
|
|
34337
|
+
async function recorded(context, operation, kind, account, fn, describe) {
|
|
34338
|
+
if (recording.getStore() === true) return fn();
|
|
34339
|
+
const named = await accountOf(context, account);
|
|
34340
|
+
return recording.run(true, () => withActivity({
|
|
34341
|
+
core: context.core,
|
|
34342
|
+
channel: "resend",
|
|
34343
|
+
surface: context.surface,
|
|
34344
|
+
account: named,
|
|
34345
|
+
clientInfo: () => context.clientInfo?.()
|
|
34346
|
+
}, operation, kind, fn, describe));
|
|
34347
|
+
}
|
|
34348
|
+
/** What a read answers when its key cannot read: a refusal that comes back as a result, recorded as one. */
|
|
34349
|
+
function unavailable(result) {
|
|
34350
|
+
return {
|
|
34351
|
+
outcome: "refused",
|
|
34352
|
+
reason: result.reason
|
|
34353
|
+
};
|
|
34354
|
+
}
|
|
34355
|
+
const ENVELOPE = new RegExp(`^<${UNTRUSTED_TAG} boundary="([A-Za-z0-9_-]+)"[^>\\n]*>\\n([\\s\\S]*)\\n</${UNTRUSTED_TAG} boundary="\\1">$`);
|
|
34356
|
+
/**
|
|
34357
|
+
* A field as its sender wrote it: the text inside its untrusted-content envelope when it is wrapped, the field itself
|
|
34358
|
+
* when it is bare (a plain address), and undefined for nothing at all.
|
|
34359
|
+
*/
|
|
34360
|
+
function plain(field) {
|
|
34361
|
+
if (typeof field !== "string" || field === "") return void 0;
|
|
34362
|
+
return ENVELOPE.exec(field)?.[2] ?? field;
|
|
34363
|
+
}
|
|
34364
|
+
/** A sender from a `Name <address>` field, bare or wrapped: the name only when there is one. */
|
|
34365
|
+
function senderOfField(field) {
|
|
34366
|
+
const text = plain(field);
|
|
34367
|
+
if (text === void 0) return void 0;
|
|
34368
|
+
const parsed = parseAddressList(text)[0];
|
|
34369
|
+
if (parsed === void 0) return { address: text };
|
|
34370
|
+
return {
|
|
34371
|
+
...parsed.name ? { name: parsed.name } : {},
|
|
34372
|
+
address: parsed.address
|
|
34373
|
+
};
|
|
34374
|
+
}
|
|
34375
|
+
/** A sender from its two fields, each bare or wrapped. Undefined when there is neither. */
|
|
34376
|
+
function senderOf(from) {
|
|
34377
|
+
const name = plain(from?.name ?? void 0);
|
|
34378
|
+
const address = plain(from?.address ?? void 0);
|
|
34379
|
+
if (name === void 0 && address === void 0) return void 0;
|
|
34380
|
+
return {
|
|
34381
|
+
...name === void 0 ? {} : { name },
|
|
34382
|
+
...address === void 0 ? {} : { address }
|
|
34383
|
+
};
|
|
34384
|
+
}
|
|
34385
|
+
/** One email touched: its id, and its subject and sender when it has them. */
|
|
34386
|
+
function itemOf(id, subject, from) {
|
|
34387
|
+
const text = plain(subject ?? void 0);
|
|
34388
|
+
return {
|
|
34389
|
+
id,
|
|
34390
|
+
...text === void 0 ? {} : { subject: text },
|
|
34391
|
+
...from === void 0 ? {} : { from }
|
|
34392
|
+
};
|
|
34393
|
+
}
|
|
34394
|
+
//#endregion
|
|
32976
34395
|
//#region src/compose/inbound.ts
|
|
32977
34396
|
/**
|
|
32978
34397
|
* Turning what arrived — or what the team sent — into what an agent may read.
|
|
@@ -33360,8 +34779,11 @@ function domainRow(entry) {
|
|
|
33360
34779
|
createdAt: text(entry.created_at)
|
|
33361
34780
|
};
|
|
33362
34781
|
}
|
|
33363
|
-
/** The team's domains; with `domain`, that one with the DNS records it needs. */
|
|
34782
|
+
/** The team's domains; with `domain`, that one with the DNS records it needs. Recorded by how many there are. */
|
|
33364
34783
|
async function listDomains(context, name, options = {}) {
|
|
34784
|
+
return recorded(context, "resend.domains", "read", name, () => domainsOf(context, name, options), (result) => result.available ? { count: result.domains.length } : unavailable(result));
|
|
34785
|
+
}
|
|
34786
|
+
async function domainsOf(context, name, options) {
|
|
33365
34787
|
return whenReadable(context, name, async (named, transport) => {
|
|
33366
34788
|
const domains = ((await resendRequest(transport, "GET", "/domains")).data ?? []).map(domainRow);
|
|
33367
34789
|
if (options.domain === void 0) return { domains };
|
|
@@ -33417,7 +34839,18 @@ function pageQuery(paging, fallback) {
|
|
|
33417
34839
|
after: cursorOf(paging.after)
|
|
33418
34840
|
};
|
|
33419
34841
|
}
|
|
34842
|
+
/** A sent email as its activity line keeps it: its id, its subject and who it was from. */
|
|
34843
|
+
function sentItem(row) {
|
|
34844
|
+
return itemOf(row.id, row.subject, senderOfField(row.from));
|
|
34845
|
+
}
|
|
34846
|
+
/** The emails the team sent, recorded by each one's id, subject and sender. */
|
|
33420
34847
|
async function listSentEmails(context, name, paging = {}) {
|
|
34848
|
+
return recorded(context, "resend.emails.list", "read", name, () => sentEmailsOf(context, name, paging), (result) => result.available ? {
|
|
34849
|
+
count: result.emails.length,
|
|
34850
|
+
items: result.emails.map(sentItem)
|
|
34851
|
+
} : unavailable(result));
|
|
34852
|
+
}
|
|
34853
|
+
async function sentEmailsOf(context, name, paging) {
|
|
33421
34854
|
const query = pageQuery(paging, 20);
|
|
33422
34855
|
return whenReadable(context, name, async (_named, transport) => {
|
|
33423
34856
|
const page = await resendRequest(transport, "GET", "/emails", { query });
|
|
@@ -33435,7 +34868,11 @@ async function listSentEmails(context, name, paging = {}) {
|
|
|
33435
34868
|
};
|
|
33436
34869
|
});
|
|
33437
34870
|
}
|
|
34871
|
+
/** One sent email, recorded by its id, subject and sender. */
|
|
33438
34872
|
async function showSentEmail(context, name, id) {
|
|
34873
|
+
return recorded(context, "resend.emails.show", "read", name, () => sentEmailOf(context, name, id), (result) => result.available ? { items: [sentItem(result.email)] } : unavailable(result));
|
|
34874
|
+
}
|
|
34875
|
+
async function sentEmailOf(context, name, id) {
|
|
33439
34876
|
const emailId = resendId(id, "email id");
|
|
33440
34877
|
return whenReadable(context, name, async (_named, transport) => {
|
|
33441
34878
|
const entry = await resendRequest(transport, "GET", `/emails/${emailId}`);
|
|
@@ -33516,7 +34953,14 @@ async function recordTaint(context, named, transport, collector, entries) {
|
|
|
33516
34953
|
internalDomains
|
|
33517
34954
|
});
|
|
33518
34955
|
}
|
|
34956
|
+
/** The emails the team received, recorded by each one's id, subject and sender. */
|
|
33519
34957
|
async function listReceived(context, name, paging = {}) {
|
|
34958
|
+
return recorded(context, "resend.received.list", "read", name, () => receivedOf(context, name, paging), (result) => result.available ? {
|
|
34959
|
+
count: result.emails.length,
|
|
34960
|
+
items: result.emails.map((email) => itemOf(email.id, email.subject, senderOf(email.from)))
|
|
34961
|
+
} : unavailable(result));
|
|
34962
|
+
}
|
|
34963
|
+
async function receivedOf(context, name, paging) {
|
|
33520
34964
|
const query = pageQuery(paging, 20);
|
|
33521
34965
|
return whenReadable(context, name, async (named, transport) => {
|
|
33522
34966
|
const page = await resendRequest(transport, "GET", "/emails/receiving", { query });
|
|
@@ -33552,7 +34996,16 @@ async function listReceived(context, name, paging = {}) {
|
|
|
33552
34996
|
};
|
|
33553
34997
|
});
|
|
33554
34998
|
}
|
|
34999
|
+
/** One received email, recorded by its id, subject and sender. */
|
|
33555
35000
|
async function showReceived(context, name, id) {
|
|
35001
|
+
return recorded(context, "resend.received.show", "read", name, () => receivedEmailOf(context, name, id), (result) => {
|
|
35002
|
+
if (!result.available) return unavailable(result);
|
|
35003
|
+
const email = result.email;
|
|
35004
|
+
const subject = typeof email.subject === "string" ? email.subject : void 0;
|
|
35005
|
+
return { items: [itemOf(String(email.id ?? ""), subject, senderOf(email.from))] };
|
|
35006
|
+
});
|
|
35007
|
+
}
|
|
35008
|
+
async function receivedEmailOf(context, name, id) {
|
|
33556
35009
|
const emailId = resendId(id, "email id");
|
|
33557
35010
|
return whenReadable(context, name, async (named, transport) => {
|
|
33558
35011
|
const entry = await resendRequest(transport, "GET", `/emails/receiving/${emailId}`);
|
|
@@ -33661,7 +35114,25 @@ async function downloadsRoot(context) {
|
|
|
33661
35114
|
* path is the sender's: see {@link storedName}. The names they gave, and the types they declared, come back as their
|
|
33662
35115
|
* own fields, wrapped where they are text; their addresses are recorded as tainted before the result is returned.
|
|
33663
35116
|
*/
|
|
35117
|
+
/** Saves a received email's attachments, recorded by each file's id, the name its sender gave it, and its size. */
|
|
33664
35118
|
async function downloadReceived(context, name, id, options = {}) {
|
|
35119
|
+
return recorded(context, "resend.received.download", "read", name, () => downloadReceivedOf(context, name, id, options), (result) => {
|
|
35120
|
+
if (!result.available) return unavailable(result);
|
|
35121
|
+
const items = result.files.map((file) => {
|
|
35122
|
+
const filename = plain(file.filename);
|
|
35123
|
+
return {
|
|
35124
|
+
id: file.attachmentId,
|
|
35125
|
+
...filename === void 0 ? {} : { name: filename },
|
|
35126
|
+
size: file.size
|
|
35127
|
+
};
|
|
35128
|
+
});
|
|
35129
|
+
return {
|
|
35130
|
+
items,
|
|
35131
|
+
counts: { saved: items.length }
|
|
35132
|
+
};
|
|
35133
|
+
});
|
|
35134
|
+
}
|
|
35135
|
+
async function downloadReceivedOf(context, name, id, options) {
|
|
33665
35136
|
const emailId = resendId(id, "email id");
|
|
33666
35137
|
const only = options.attachmentId === void 0 ? void 0 : resendId(options.attachmentId, "attachment id");
|
|
33667
35138
|
const subpath = relativeSubpath(options.out);
|
|
@@ -33733,7 +35204,11 @@ function dateOf(raw, name) {
|
|
|
33733
35204
|
if (typeof raw !== "string" || !DATE.test(raw.trim())) throw new CommsError("USAGE", `${name} must be a date like 2026-09-01, or an ISO 8601 time with a zone`);
|
|
33734
35205
|
return raw.trim();
|
|
33735
35206
|
}
|
|
35207
|
+
/** The team's sending totals, recorded only as asked: they name no email. */
|
|
33736
35208
|
async function getMetrics(context, name, range = {}) {
|
|
35209
|
+
return recorded(context, "resend.metrics", "read", name, () => metricsOf(context, name, range), (result) => result.available ? {} : unavailable(result));
|
|
35210
|
+
}
|
|
35211
|
+
async function metricsOf(context, name, range) {
|
|
33737
35212
|
const start = dateOf(range.start, "start");
|
|
33738
35213
|
const end = dateOf(range.end, "end");
|
|
33739
35214
|
return whenReadable(context, name, async (_named, transport) => {
|
|
@@ -33756,7 +35231,11 @@ const ORIGINS = [
|
|
|
33756
35231
|
"complaint",
|
|
33757
35232
|
"manual"
|
|
33758
35233
|
];
|
|
35234
|
+
/** The addresses Resend will not send to, recorded by how many there are — never the addresses. */
|
|
33759
35235
|
async function listSuppressions(context, name, options = {}) {
|
|
35236
|
+
return recorded(context, "resend.suppressions", "read", name, () => suppressionsOf(context, name, options), (result) => result.available ? { count: result.suppressions.length } : unavailable(result));
|
|
35237
|
+
}
|
|
35238
|
+
async function suppressionsOf(context, name, options) {
|
|
33760
35239
|
const query = pageQuery(options, 20);
|
|
33761
35240
|
if (options.origin !== void 0 && !ORIGINS.includes(String(options.origin))) throw new CommsError("USAGE", `"${String(options.origin)}" is not an origin: use bounce, complaint or manual`);
|
|
33762
35241
|
return whenReadable(context, name, async (_named, transport) => {
|
|
@@ -33790,7 +35269,14 @@ async function listSuppressions(context, name, options = {}) {
|
|
|
33790
35269
|
});
|
|
33791
35270
|
}
|
|
33792
35271
|
const SCHEDULED_PAGES = 3;
|
|
35272
|
+
/** The emails waiting to go, recorded by each one's id, subject and sender. */
|
|
33793
35273
|
async function listScheduled(context, name) {
|
|
35274
|
+
return recorded(context, "resend.scheduled.list", "read", name, () => scheduledOf(context, name), (result) => result.available ? {
|
|
35275
|
+
count: result.scheduled.length,
|
|
35276
|
+
items: result.scheduled.map(sentItem)
|
|
35277
|
+
} : unavailable(result));
|
|
35278
|
+
}
|
|
35279
|
+
async function scheduledOf(context, name) {
|
|
33794
35280
|
return whenReadable(context, name, async (named, transport) => {
|
|
33795
35281
|
const records = new SendRecords(context.core.paths.stateDir, context.now);
|
|
33796
35282
|
const envelope = {
|
|
@@ -33831,7 +35317,31 @@ async function listScheduled(context, name) {
|
|
|
33831
35317
|
function messageOf$1(error) {
|
|
33832
35318
|
return error instanceof Error ? error.message : String(error);
|
|
33833
35319
|
}
|
|
35320
|
+
/**
|
|
35321
|
+
* Cancelling one scheduled email, as a change the shared change flow plans and applies. Recorded in the activity log
|
|
35322
|
+
* as the write it is — the email's id, and how many recipients it had — when it is applied, or when the look before it
|
|
35323
|
+
* refuses; a call that only prepares the change's approval writes nothing to the account, and the approval store keeps
|
|
35324
|
+
* it.
|
|
35325
|
+
*/
|
|
33834
35326
|
function cancelScheduledChange(context, name, id) {
|
|
35327
|
+
const change = cancelChange(context, name, id);
|
|
35328
|
+
return {
|
|
35329
|
+
...change,
|
|
35330
|
+
plan: async (config) => {
|
|
35331
|
+
try {
|
|
35332
|
+
return await change.plan(config);
|
|
35333
|
+
} catch (error) {
|
|
35334
|
+
await recorded(context, "resend.scheduled.cancel", "write", name, () => Promise.reject(error), () => ({})).catch(() => void 0);
|
|
35335
|
+
throw error;
|
|
35336
|
+
}
|
|
35337
|
+
},
|
|
35338
|
+
apply: (consent, request) => recorded(context, "resend.scheduled.cancel", "write", name, () => change.apply(consent, request), (cancelled) => ({
|
|
35339
|
+
items: [{ id: cancelled.id }],
|
|
35340
|
+
counts: { recipients: cancelled.recipients }
|
|
35341
|
+
}))
|
|
35342
|
+
};
|
|
35343
|
+
}
|
|
35344
|
+
function cancelChange(context, name, id) {
|
|
33835
35345
|
const emailId = resendId(id, "email id");
|
|
33836
35346
|
const look = async () => {
|
|
33837
35347
|
const named = await context.accounts.require(name);
|
|
@@ -34326,6 +35836,21 @@ async function attachPolicy(context) {
|
|
|
34326
35836
|
* this exact content. Nothing is sent; preparing twice is free.
|
|
34327
35837
|
*/
|
|
34328
35838
|
async function prepareSend(context, name, input) {
|
|
35839
|
+
return recorded(context, "resend.send.prepare", "approval", name, () => prepareOneSend(context, name, input), (prepared) => ({
|
|
35840
|
+
approvalId: prepared.approvalId,
|
|
35841
|
+
items: [itemOf(prepared.approvalId, prepared.expect.subject)],
|
|
35842
|
+
recipients: activityRecipients(prepared.expect)
|
|
35843
|
+
}));
|
|
35844
|
+
}
|
|
35845
|
+
/** A send's recipients as its activity line keeps them: To, Cc and Bcc, as addresses. */
|
|
35846
|
+
function activityRecipients(expect) {
|
|
35847
|
+
return [
|
|
35848
|
+
...expect.to,
|
|
35849
|
+
...expect.cc,
|
|
35850
|
+
...expect.bcc
|
|
35851
|
+
].flatMap((address) => plain(address) ?? []);
|
|
35852
|
+
}
|
|
35853
|
+
async function prepareOneSend(context, name, input) {
|
|
34329
35854
|
const { legacyDrain } = await ensureSendEpochConfig(context.core, { now: context.now });
|
|
34330
35855
|
const named = await context.accounts.require(name);
|
|
34331
35856
|
requireSendMode(named, context.handoffs);
|
|
@@ -34583,6 +36108,13 @@ function acceptedSaid(withId, scheduledAt) {
|
|
|
34583
36108
|
* request, send — never retried, because a retried send may deliver twice.
|
|
34584
36109
|
*/
|
|
34585
36110
|
async function executeSend(context, name, options) {
|
|
36111
|
+
return recorded(context, "resend.send.execute", "write", name, () => sendApproved(context, name, options), (sent) => ({
|
|
36112
|
+
approvalId: sent.approvalId,
|
|
36113
|
+
items: [itemOf(sent.resendId ?? sent.approvalId, sent.subject)],
|
|
36114
|
+
recipients: activityRecipients(sent)
|
|
36115
|
+
}));
|
|
36116
|
+
}
|
|
36117
|
+
async function sendApproved(context, name, options) {
|
|
34586
36118
|
const { legacyDrain } = await ensureSendEpochConfig(context.core, { now: context.now });
|
|
34587
36119
|
const named = await context.accounts.require(name);
|
|
34588
36120
|
requireSendMode(named, context.handoffs);
|
|
@@ -34823,6 +36355,12 @@ function outcomeWords(outcome) {
|
|
|
34823
36355
|
* machine's own record of a cancellation Resend confirmed; without it, a cancellation is as Resend reports it.
|
|
34824
36356
|
*/
|
|
34825
36357
|
async function sendStatus(context, name, approvalId) {
|
|
36358
|
+
return recorded(context, "resend.send.status", "read", name, () => statusOf(context, name, approvalId), (status) => ({
|
|
36359
|
+
approvalId: status.approvalId,
|
|
36360
|
+
...status.resend === null ? {} : { items: [{ id: status.resend.id }] }
|
|
36361
|
+
}));
|
|
36362
|
+
}
|
|
36363
|
+
async function statusOf(context, name, approvalId) {
|
|
34826
36364
|
const named = await context.accounts.require(name);
|
|
34827
36365
|
const local = await new SendRecords(context.core.paths.stateDir, context.now).summary(named.account.id, approvalId);
|
|
34828
36366
|
const seen = await context.core.approvals.inspect(approvalId, {
|
|
@@ -35096,4 +36634,4 @@ async function revokeSendApproval(context, approvalId) {
|
|
|
35096
36634
|
});
|
|
35097
36635
|
}
|
|
35098
36636
|
//#endregion
|
|
35099
|
-
export { colorEnabled as $, removeAccountChange as A, waitCallOptions as At, strictToolArguments as B,
|
|
36637
|
+
export { colorEnabled as $, removeAccountChange as A, waitCallOptions as At, strictToolArguments as B, ROUTES as Bt, inspectKey as C, serverPruneChange as Ct, policyChange as D, truncateDisplay as Dt, policyApprovalRefusal as E, toCommsError as Et, CHANGE_POLICIES as F, PACKAGE_NAME as Ft, UPDATE_CHECK_CHILD_COMMAND as G, CommsError as H, MODES as I, RESEND_CALLER as It, approveRefusedHint as J, agentMarker as K, SEND_POLICIES as L, REFUSED as Lt, showAccount as M, warmSessionIdentity as Mt, resendRequest as N, wholeNumber as Nt, policyReport as O, updateNoticeAtTerminal as Ot, ResendContext as P, writeResult as Pt, cliHandoffs as Q, checkNewName as R, RESEND_API_ORIGIN as Rt, addAccountChange as S, serverInstallChange as St, modeOf as T, terminalUpdateHooks as Tt, EXIT_CODES as U, CHANNEL_SERVERS as V, VERSION$2 as Vt, PATH_OPTIONS as W, changeToolResult as X, canPrompt as Y, checkForUpdates as Z, showSentEmail as _, renderInstall as _t, revokeSendApproval as a, gatedChangeAtTerminal as at, readBody as b, runCommand as bt, downloadReceived as c, installExitStatus as ct, listReceived as d, newBoundary as dt, commandPathOf as et, listScheduled as f, openCore as ft, showReceived as g, renderApprovalWait as gt, resendId as h, refuseUnclaimedApproval as ht, prepareSend as i, gatedChange as it, sendPolicyOf as j, waitForApproval as jt, policyWanted as k, updateToolNotice as kt, getMetrics as l, kindOf as lt, listSuppressions as m, pathOverridesFromCliOptions as mt, executeSend as n, escapeForDisplay as nt, sendStatus as o, handoffSentence as ot, listSentEmails as p, paint as pt, approveChangeAtTerminal as q, finishSendApproval as r, exemptFromUpdateNotice as rt, cancelScheduledChange as s, handoffSentenceToFill as st, beginSendApproval as t, describeFenceReport as tt, listDomains as u, neutralise as ut, addressesOf as v, renderPrune as vt, listAccounts as w, stripInvisible as wt, runDoctor as x, runUpdateCheckChild as xt, attachmentRisks as y, resolvePaths as yt, keyPermissionOf as z, RESEND_DOWNLOAD_ORIGIN as zt };
|