@getbourdon/participants 0.2.2 → 0.2.4

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.
Files changed (48) hide show
  1. package/README.md +7 -1
  2. package/dist/base.d.ts +109 -0
  3. package/dist/base.js +88 -0
  4. package/dist/index.cjs +171 -82
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +117 -4
  7. package/dist/index.d.ts +42 -970
  8. package/dist/index.js +53 -6509
  9. package/dist/index.js.map +1 -1
  10. package/dist/network-base.d.ts +108 -0
  11. package/dist/network-base.js +248 -0
  12. package/dist/readers/cascade.d.ts +49 -0
  13. package/dist/readers/cascade.js +428 -0
  14. package/dist/readers/claude-code-automations.d.ts +62 -0
  15. package/dist/readers/claude-code-automations.js +693 -0
  16. package/dist/readers/claude-code.d.ts +50 -0
  17. package/dist/readers/claude-code.js +583 -0
  18. package/dist/readers/claude-desktop-code.d.ts +54 -0
  19. package/dist/readers/claude-desktop-code.js +526 -0
  20. package/dist/readers/claude-desktop-cowork.d.ts +47 -0
  21. package/dist/readers/claude-desktop-cowork.js +659 -0
  22. package/dist/readers/codex-automations.d.ts +55 -0
  23. package/dist/readers/codex-automations.js +519 -0
  24. package/dist/readers/codex-sessions.d.ts +88 -0
  25. package/dist/readers/codex-sessions.js +194 -0
  26. package/dist/readers/copilot-automations.d.ts +64 -0
  27. package/dist/readers/copilot-automations.js +530 -0
  28. package/dist/readers/copilot-cli.d.ts +45 -0
  29. package/dist/readers/copilot-cli.js +416 -0
  30. package/dist/readers/copilot-vscode.d.ts +54 -0
  31. package/dist/readers/copilot-vscode.js +553 -0
  32. package/dist/readers/copilot.d.ts +59 -0
  33. package/dist/readers/copilot.js +413 -0
  34. package/dist/readers/cursor-automations.d.ts +57 -0
  35. package/dist/readers/cursor-automations.js +685 -0
  36. package/dist/readers/cursor.d.ts +50 -0
  37. package/dist/readers/cursor.js +670 -0
  38. package/dist/readers/github-copilot.d.ts +34 -0
  39. package/dist/readers/github-copilot.js +242 -0
  40. package/dist/readers/hermes.d.ts +37 -0
  41. package/dist/readers/hermes.js +426 -0
  42. package/dist/readers/openclaw.d.ts +78 -0
  43. package/dist/readers/openclaw.js +386 -0
  44. package/dist/registry.d.ts +39 -0
  45. package/dist/registry.js +98 -0
  46. package/dist/sqlite-base.d.ts +65 -0
  47. package/dist/sqlite-base.js +163 -0
  48. package/package.json +5 -5
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Codex automations participant — publishes read-only Codex background/automation
3
+ * run memory into Bourdon as L5 evidence. Port of
4
+ * `participants/codex_automations.py`.
5
+ *
6
+ * Store convention (NOT SQLite, NOT network): a CONVENTION-FILE directory tree at
7
+ * `~/.codex/automations/<id>/`, each holding an `automation.toml` config and an
8
+ * optional `memory.md` run log. The reader parses the TOML config and extracts
9
+ * dated "runs" from the markdown memory, normalizing them into L5 sessions +
10
+ * entities (the automation itself, plus inferred projects and signal classes).
11
+ *
12
+ * Source-resolution order for the automations dir:
13
+ * 1. explicit `automationsDir` constructor arg
14
+ * 2. `codexHome` constructor arg → `<codexHome>/automations`
15
+ * 3. `CODEX_HOME` env-var → `$CODEX_HOME/automations`
16
+ * 4. `~/.codex/automations` (default convention path)
17
+ *
18
+ * Privacy: every native string routes through the redaction SSOT
19
+ * (`redactText(v, 180)` == the oracle's `_safe_native_memory_text`); the
20
+ * visibility policy filters PRIVATE entities before emission. Defensive
21
+ * throughout — a missing dir raises {@link ParticipantDiscoveryError} from
22
+ * `discover()`, malformed TOML/markdown degrades to empty rather than raising,
23
+ * and `healthCheck` never throws.
24
+ *
25
+ * NOTE: does NOT depend on the full codex reader — only the redaction helper it
26
+ * imports (`_safe_native_memory_text`, reproduced inline as {@link safeNativeMemoryText}).
27
+ */
28
+ import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
29
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
30
+ /** Return the default Codex automations directory (source-resolution order). */
31
+ export declare function defaultCodexAutomationsDir(codexHome?: string): string;
32
+ /**
33
+ * External participant for Codex automation memory artifacts. Reads
34
+ * `~/.codex/automations/<id>/{automation.toml,memory.md}` and normalizes dated
35
+ * runs into an L5 manifest.
36
+ */
37
+ export declare class CodexAutomationsParticipant implements BourdonParticipant {
38
+ agentId: string;
39
+ agentType: string;
40
+ private readonly automationsDir;
41
+ private readonly policy;
42
+ /**
43
+ * Conventional Codex automations dir (`~/.codex/automations`). Provided for
44
+ * protocol uniformity — the setup wizard wires the parent `codex` participant
45
+ * and skips `-automations` sub-surfaces rather than detecting this one.
46
+ */
47
+ static defaultNativePath(home?: string): string;
48
+ constructor(automationsDir?: string, codexHome?: string);
49
+ get nativePath(): string;
50
+ discover(): AgentStore;
51
+ exportSessions(since?: Date, limit?: number): SessionModel[];
52
+ exportL5(since?: Date): L5ManifestModel;
53
+ healthCheck(): HealthStatus;
54
+ private runs;
55
+ }
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Codex automations participant — publishes read-only Codex background/automation
3
+ * run memory into Bourdon as L5 evidence. Port of
4
+ * `participants/codex_automations.py`.
5
+ *
6
+ * Store convention (NOT SQLite, NOT network): a CONVENTION-FILE directory tree at
7
+ * `~/.codex/automations/<id>/`, each holding an `automation.toml` config and an
8
+ * optional `memory.md` run log. The reader parses the TOML config and extracts
9
+ * dated "runs" from the markdown memory, normalizing them into L5 sessions +
10
+ * entities (the automation itself, plus inferred projects and signal classes).
11
+ *
12
+ * Source-resolution order for the automations dir:
13
+ * 1. explicit `automationsDir` constructor arg
14
+ * 2. `codexHome` constructor arg → `<codexHome>/automations`
15
+ * 3. `CODEX_HOME` env-var → `$CODEX_HOME/automations`
16
+ * 4. `~/.codex/automations` (default convention path)
17
+ *
18
+ * Privacy: every native string routes through the redaction SSOT
19
+ * (`redactText(v, 180)` == the oracle's `_safe_native_memory_text`); the
20
+ * visibility policy filters PRIVATE entities before emission. Defensive
21
+ * throughout — a missing dir raises {@link ParticipantDiscoveryError} from
22
+ * `discover()`, malformed TOML/markdown degrades to empty rather than raising,
23
+ * and `healthCheck` never throws.
24
+ *
25
+ * NOTE: does NOT depend on the full codex reader — only the redaction helper it
26
+ * imports (`_safe_native_memory_text`, reproduced inline as {@link safeNativeMemoryText}).
27
+ */
28
+ import { readFileSync, readdirSync, statSync } from "node:fs";
29
+ import { homedir, hostname } from "node:os";
30
+ import { basename, dirname, join } from "node:path";
31
+ import { parse as tomlParse } from "smol-toml";
32
+ import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
33
+ import { redactText } from "@getbourdon/redaction";
34
+ import { ParticipantDiscoveryError, SPEC_VERSION, loadProjectHints, } from "../base.js";
35
+ // -- Constants -----------------------------------------------------------------
36
+ const AGENT_ID = "codex-automations";
37
+ const AGENT_TYPE = "other";
38
+ const ROLE_NARRATIVE = "Publishes read-only Codex automation run memory into Bourdon so background " +
39
+ "monitors, digests, and recurring checks are visible alongside interactive " +
40
+ "agent sessions.";
41
+ const DEFAULT_POLICY = makeVisibilityPolicy({
42
+ default: Visibility.TEAM,
43
+ private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
44
+ team_tags: ["codex-automation", "automation", "workspace"],
45
+ });
46
+ const AUTOMATIONS_DIR_NAME = "automations";
47
+ const AUTOMATION_TOML = "automation.toml";
48
+ const MEMORY_MD = "memory.md";
49
+ const MAX_MEMORY_CHARS = 160_000;
50
+ const MAX_KEY_ACTIONS_PER_RUN = 6;
51
+ const MAX_KEY_ACTION_CHARS = 280;
52
+ const RUN_HEADER_RE = /^(\d{4}-\d{2}-\d{2})(?:\b|$)(.*)$/;
53
+ // Product-neutral defaults only: this product plus the agent surfaces Bourdon
54
+ // federates. A deployment adds its own project roster via the
55
+ // `BOURDON_PROJECT_HINTS` env var (comma-separated) — see `loadProjectHints`.
56
+ const DEFAULT_PROJECT_HINTS = ["Bourdon", "Cursor", "Copilot"];
57
+ const SIGNAL_PATTERNS = [
58
+ ["human-dashboard-action", /\b(human|dashboard|manual)\b/i],
59
+ ["release-gate", /\b(release|store|app review|play console|testflight)\b/i],
60
+ ["billing-drift", /\b(billing|stripe|revenuecat|iap|subscription)\b/i],
61
+ ["memory-coverage-gap", /\b(memory|l5|manifest|federated|bourdon)\b/i],
62
+ ["launch-decision", /\b(launch|go-live|pricing|prod|production)\b/i],
63
+ ];
64
+ /** Minimal logger so a degraded parse is visible without pulling in a dep. */
65
+ const logger = {
66
+ warn(msg) {
67
+ // eslint-disable-next-line no-console
68
+ console.warn(`[bourdon.participants] ${msg}`);
69
+ },
70
+ };
71
+ // -- fs helpers ----------------------------------------------------------------
72
+ function homeDir() {
73
+ // os.homedir() can throw under some sandboxes; stay defensive so import +
74
+ // construction never crash even when HOME points at an unreadable path.
75
+ try {
76
+ return homedir();
77
+ }
78
+ catch {
79
+ return process.env.HOME || process.env.USERPROFILE || ".";
80
+ }
81
+ }
82
+ function isDir(p) {
83
+ try {
84
+ return statSync(p).isDirectory();
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ function isFile(p) {
91
+ try {
92
+ return statSync(p).isFile();
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ // -- Path resolution -----------------------------------------------------------
99
+ /** Return the default Codex automations directory (source-resolution order). */
100
+ export function defaultCodexAutomationsDir(codexHome) {
101
+ if (codexHome !== undefined && codexHome !== null) {
102
+ return join(codexHome, AUTOMATIONS_DIR_NAME);
103
+ }
104
+ const env = process.env.CODEX_HOME;
105
+ if (env)
106
+ return join(env, AUTOMATIONS_DIR_NAME);
107
+ return join(homeDir(), ".codex", AUTOMATIONS_DIR_NAME);
108
+ }
109
+ // -- Redaction -----------------------------------------------------------------
110
+ /**
111
+ * Thin wrapper over the redaction SSOT (the oracle's `_safe_native_memory_text`,
112
+ * `redact_text(value, limit=180)`).
113
+ */
114
+ function safeNativeMemoryText(value) {
115
+ return redactText(value, 180);
116
+ }
117
+ // -- Coercion helpers (mirror Python str() / truthiness) -----------------------
118
+ /** Mirror Python `str(a or b)`: use `a` when truthy, else `b`, then stringify. */
119
+ function pyStrOr(a, b) {
120
+ return String(a ? a : b);
121
+ }
122
+ // -- TOML config ---------------------------------------------------------------
123
+ /** Read + parse an automation.toml. Returns {} on any I/O or parse error. */
124
+ function readAutomationToml(path) {
125
+ let text;
126
+ try {
127
+ text = readFileSync(path, "utf8");
128
+ }
129
+ catch (exc) {
130
+ logger.warn(`CodexAutomationsParticipant: cannot parse ${path}: ${String(exc)}`);
131
+ return {};
132
+ }
133
+ try {
134
+ const parsed = tomlParse(text);
135
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
136
+ ? parsed
137
+ : {};
138
+ }
139
+ catch (exc) {
140
+ logger.warn(`CodexAutomationsParticipant: cannot parse ${path}: ${String(exc)}`);
141
+ return {};
142
+ }
143
+ }
144
+ /** Build an AutomationConfig from a toml path. Returns null on an empty id. */
145
+ function buildConfig(tomlPath) {
146
+ const raw = readAutomationToml(tomlPath);
147
+ const parentName = basename(dirname(tomlPath));
148
+ const automationId = pyStrOr(raw["id"], parentName).trim();
149
+ if (!automationId)
150
+ return null;
151
+ const name = pyStrOr(raw["name"], automationId).trim();
152
+ const status = pyStrOr(raw["status"], "UNKNOWN").trim().toUpperCase();
153
+ const rrule = pyStrOr(raw["rrule"], "").trim();
154
+ const kind = pyStrOr(raw["kind"], "").trim();
155
+ const cwdsRaw = raw["cwds"];
156
+ const cwds = Array.isArray(cwdsRaw)
157
+ ? cwdsRaw.filter((cwd) => typeof cwd === "string" && cwd.trim() !== "")
158
+ : [];
159
+ const memoryPath = join(dirname(tomlPath), MEMORY_MD);
160
+ return {
161
+ automationId,
162
+ name,
163
+ status,
164
+ rrule,
165
+ kind,
166
+ cwds,
167
+ path: tomlPath,
168
+ memoryPath: isFile(memoryPath) ? memoryPath : null,
169
+ };
170
+ }
171
+ /**
172
+ * Enumerate the `<id>/automation.toml` files, sorted (mirrors Python's sorted-glob).
173
+ * The varying path segment is the immediate sub-directory, so sorting the
174
+ * resulting toml paths reproduces the oracle's ordering.
175
+ */
176
+ function iterConfigs(automationsDir) {
177
+ if (!isDir(automationsDir))
178
+ return [];
179
+ let entries;
180
+ try {
181
+ entries = readdirSync(automationsDir);
182
+ }
183
+ catch {
184
+ return [];
185
+ }
186
+ const tomlPaths = [];
187
+ for (const entry of entries) {
188
+ const tomlPath = join(automationsDir, entry, AUTOMATION_TOML);
189
+ if (isFile(tomlPath))
190
+ tomlPaths.push(tomlPath);
191
+ }
192
+ tomlPaths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
193
+ const configs = [];
194
+ for (const tomlPath of tomlPaths) {
195
+ const config = buildConfig(tomlPath);
196
+ if (config !== null)
197
+ configs.push(config);
198
+ }
199
+ return configs;
200
+ }
201
+ // -- Memory extraction ---------------------------------------------------------
202
+ /** Read the memory file, returning at most the last MAX_MEMORY_CHARS characters. */
203
+ function readMemoryText(path) {
204
+ if (path === null)
205
+ return "";
206
+ let text;
207
+ try {
208
+ text = readFileSync(path, "utf8");
209
+ }
210
+ catch (exc) {
211
+ logger.warn(`CodexAutomationsParticipant: cannot read ${path}: ${String(exc)}`);
212
+ return "";
213
+ }
214
+ return text.length > MAX_MEMORY_CHARS ? text.slice(-MAX_MEMORY_CHARS) : text;
215
+ }
216
+ /** Split into lines the way Python's str.splitlines() does (best-effort). */
217
+ function splitLines(text) {
218
+ // Python splitlines includes ASCII record separators; this explicit set preserves parity.
219
+ // eslint-disable-next-line no-control-regex
220
+ return text.split(/\r\n|[\n\r\u000B\u000C\u001C\u001D\u001E\u0085\u2028\u2029]/u);
221
+ }
222
+ /** Python `str.rstrip()` — strip trailing whitespace only. */
223
+ function rstrip(s) {
224
+ return s.replace(/\s+$/, "");
225
+ }
226
+ /** Python `" ".join(value.split())` — collapse all whitespace runs. */
227
+ function collapseWhitespace(value) {
228
+ return value
229
+ .split(/\s+/)
230
+ .filter((p) => p.length > 0)
231
+ .join(" ");
232
+ }
233
+ /** Port of `_bounded`: collapse whitespace then cap with a `...` ellipsis. */
234
+ function bounded(value, limit) {
235
+ const normalized = collapseWhitespace(value);
236
+ if (normalized.length <= limit)
237
+ return normalized;
238
+ return rstrip(normalized.slice(0, limit - 1)) + "...";
239
+ }
240
+ function actionsFromLines(lines) {
241
+ const actions = [];
242
+ for (const line of lines) {
243
+ let cleaned = line.trim();
244
+ if (!cleaned)
245
+ continue;
246
+ cleaned = (cleaned.startsWith("- ") ? cleaned.slice(2) : cleaned).trim();
247
+ const lowered = cleaned.toLowerCase();
248
+ if (!cleaned || lowered.startsWith("runtime"))
249
+ continue;
250
+ if (lowered === "first run" || lowered === "follow-up")
251
+ continue;
252
+ if (lowered.startsWith("run:"))
253
+ cleaned = cleaned.slice(4).trim();
254
+ const safe = bounded(safeNativeMemoryText(cleaned), MAX_KEY_ACTION_CHARS);
255
+ if (safe && !actions.includes(safe))
256
+ actions.push(safe);
257
+ if (actions.length >= MAX_KEY_ACTIONS_PER_RUN)
258
+ break;
259
+ }
260
+ return actions;
261
+ }
262
+ function titleFromActions(config, actions) {
263
+ if (actions.length === 0)
264
+ return config.name;
265
+ const first = actions[0];
266
+ const prefix = `${config.name}: `;
267
+ if (first.startsWith(prefix))
268
+ return bounded(first, 120);
269
+ return bounded(prefix + first, 120);
270
+ }
271
+ function inferProjects(text) {
272
+ const projects = [];
273
+ const lowered = text.toLowerCase();
274
+ const seen = new Set();
275
+ for (const project of loadProjectHints(DEFAULT_PROJECT_HINTS)) {
276
+ const key = project.toLowerCase();
277
+ if (lowered.includes(key) && !seen.has(key)) {
278
+ projects.push(project);
279
+ seen.add(key);
280
+ }
281
+ }
282
+ return projects;
283
+ }
284
+ function inferSignals(text) {
285
+ const signals = [];
286
+ for (const [name, pattern] of SIGNAL_PATTERNS) {
287
+ if (pattern.test(text))
288
+ signals.push(name);
289
+ }
290
+ return signals;
291
+ }
292
+ function extractMemoryRuns(config) {
293
+ const text = readMemoryText(config.memoryPath);
294
+ if (!text)
295
+ return [];
296
+ const chunks = [];
297
+ let currentDate = "";
298
+ let currentLines = [];
299
+ for (const rawLine of splitLines(text)) {
300
+ const line = rstrip(rawLine);
301
+ const match = RUN_HEADER_RE.exec(line.trim());
302
+ if (match) {
303
+ if (currentDate)
304
+ chunks.push([currentDate, currentLines]);
305
+ currentDate = match[1];
306
+ const suffix = stripChars(match[2].trim(), " -:—");
307
+ currentLines = suffix ? [suffix] : [];
308
+ continue;
309
+ }
310
+ if (currentDate)
311
+ currentLines.push(line);
312
+ }
313
+ if (currentDate)
314
+ chunks.push([currentDate, currentLines]);
315
+ const runs = [];
316
+ for (const [runDate, lines] of chunks) {
317
+ const actions = actionsFromLines(lines);
318
+ if (actions.length === 0)
319
+ continue;
320
+ const body = actions.join(" ");
321
+ const title = titleFromActions(config, actions);
322
+ runs.push({
323
+ automation: config,
324
+ date: runDate,
325
+ title,
326
+ keyActions: actions,
327
+ projects: inferProjects(body),
328
+ signals: inferSignals(body),
329
+ });
330
+ }
331
+ return runs;
332
+ }
333
+ /** Python `str.strip(chars)` — strip any of `chars` from both ends. */
334
+ function stripChars(s, chars) {
335
+ let start = 0;
336
+ let end = s.length;
337
+ while (start < end && chars.includes(s[start]))
338
+ start++;
339
+ while (end > start && chars.includes(s[end - 1]))
340
+ end--;
341
+ return s.slice(start, end);
342
+ }
343
+ // -- L5 assembly ---------------------------------------------------------------
344
+ function sessionFromRun(run) {
345
+ const config = run.automation;
346
+ const keyActions = [
347
+ `automation_id: ${config.automationId}`,
348
+ `run: ${run.title}`,
349
+ ...run.keyActions,
350
+ ];
351
+ const filesTouched = [config.path];
352
+ if (config.memoryPath !== null)
353
+ filesTouched.push(config.memoryPath);
354
+ return makeSession({
355
+ date: run.date,
356
+ cwd: config.cwds.length > 0 ? config.cwds[0] : dirname(config.path),
357
+ project_focus: [...run.projects],
358
+ key_actions: keyActions.slice(0, MAX_KEY_ACTIONS_PER_RUN + 2),
359
+ files_touched: filesTouched,
360
+ visibility: Visibility.TEAM,
361
+ });
362
+ }
363
+ function entitiesFromConfigsAndRuns(configs, runs) {
364
+ const byName = new Map();
365
+ for (const config of configs) {
366
+ byName.set(config.automationId, makeEntity({
367
+ name: config.automationId,
368
+ type: "automation",
369
+ summary: bounded(`Codex automation '${config.name}' (${config.status}). ` +
370
+ `Schedule: ${config.rrule || "unspecified"}.`, 260),
371
+ last_touched: undefined,
372
+ tags: ["codex-automation", "automation", config.status.toLowerCase()],
373
+ visibility: Visibility.TEAM,
374
+ }));
375
+ }
376
+ for (const run of runs) {
377
+ for (const project of run.projects) {
378
+ if (!byName.has(project)) {
379
+ byName.set(project, makeEntity({
380
+ name: project,
381
+ type: "project",
382
+ summary: "Project mentioned by Codex automation run memory.",
383
+ last_touched: run.date,
384
+ tags: ["codex-automation", "automation-evidence"],
385
+ visibility: Visibility.TEAM,
386
+ }));
387
+ }
388
+ }
389
+ for (const signal of run.signals) {
390
+ if (!byName.has(signal)) {
391
+ byName.set(signal, makeEntity({
392
+ name: signal,
393
+ type: "automation-signal",
394
+ summary: "Signal class inferred from Codex automation run memory.",
395
+ last_touched: run.date,
396
+ tags: ["codex-automation", "automation-signal"],
397
+ visibility: Visibility.TEAM,
398
+ }));
399
+ }
400
+ }
401
+ }
402
+ return [...byName.values()];
403
+ }
404
+ // -- Participant ---------------------------------------------------------------
405
+ /**
406
+ * External participant for Codex automation memory artifacts. Reads
407
+ * `~/.codex/automations/<id>/{automation.toml,memory.md}` and normalizes dated
408
+ * runs into an L5 manifest.
409
+ */
410
+ export class CodexAutomationsParticipant {
411
+ agentId = AGENT_ID;
412
+ agentType = AGENT_TYPE;
413
+ automationsDir;
414
+ policy;
415
+ /**
416
+ * Conventional Codex automations dir (`~/.codex/automations`). Provided for
417
+ * protocol uniformity — the setup wizard wires the parent `codex` participant
418
+ * and skips `-automations` sub-surfaces rather than detecting this one.
419
+ */
420
+ static defaultNativePath(home) {
421
+ return defaultCodexAutomationsDir(home);
422
+ }
423
+ constructor(automationsDir, codexHome) {
424
+ this.automationsDir = automationsDir ?? defaultCodexAutomationsDir(codexHome);
425
+ this.policy = DEFAULT_POLICY;
426
+ }
427
+ get nativePath() {
428
+ return this.automationsDir;
429
+ }
430
+ discover() {
431
+ if (!isDir(this.automationsDir)) {
432
+ throw new ParticipantDiscoveryError(`Codex automations directory not found at ${this.automationsDir}.`);
433
+ }
434
+ const configs = iterConfigs(this.automationsDir);
435
+ return {
436
+ path: this.automationsDir,
437
+ version: "unknown",
438
+ metadata: {
439
+ automations: configs.length,
440
+ with_memory: configs.filter((c) => c.memoryPath !== null).length,
441
+ },
442
+ };
443
+ }
444
+ exportSessions(since, limit = 100) {
445
+ const sessions = this.runs(undefined, since).map(sessionFromRun);
446
+ sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
447
+ return sessions.slice(0, limit);
448
+ }
449
+ exportL5(since) {
450
+ const configs = iterConfigs(this.automationsDir);
451
+ const runs = this.runs(configs, since);
452
+ const sessions = runs.map(sessionFromRun);
453
+ sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
454
+ const entities = entitiesFromConfigsAndRuns(configs, runs);
455
+ const visibleEntities = filterForFederation(entities, this.policy);
456
+ return makeManifest({
457
+ spec_version: SPEC_VERSION,
458
+ agent: makeAgentInfo({
459
+ id: AGENT_ID,
460
+ type: AGENT_TYPE,
461
+ instance: hostname(),
462
+ spec_version_compat: SPEC_VERSION,
463
+ role_narrative: ROLE_NARRATIVE,
464
+ }),
465
+ last_updated: new Date().toISOString(),
466
+ capabilities: ["codex-automation-memory", "run-summary-publication"],
467
+ recent_sessions: sessions,
468
+ known_entities: visibleEntities,
469
+ visibility_policy: this.policy,
470
+ });
471
+ }
472
+ healthCheck() {
473
+ if (!isDir(this.automationsDir)) {
474
+ return {
475
+ status: "blocked",
476
+ reason: `Codex automations directory not found at ${this.automationsDir}.`,
477
+ details: { automations_dir: this.automationsDir },
478
+ proposedFix: "Create Codex automations or pass --automations-dir.",
479
+ };
480
+ }
481
+ const configs = iterConfigs(this.automationsDir);
482
+ const runs = this.runs(configs);
483
+ const hasConfigs = configs.length > 0;
484
+ return {
485
+ status: hasConfigs ? "ok" : "degraded",
486
+ reason: hasConfigs ? undefined : "No automation.toml files found.",
487
+ details: {
488
+ automations_dir: this.automationsDir,
489
+ automation_count: configs.length,
490
+ memory_files: configs.filter((c) => c.memoryPath !== null).length,
491
+ runs_extracted: runs.length,
492
+ active_automations: configs.filter((c) => c.status === "ACTIVE").length,
493
+ },
494
+ proposedFix: hasConfigs ? undefined : "Add Codex automation.toml files.",
495
+ };
496
+ }
497
+ // -- Internal ---------------------------------------------------------------
498
+ runs(configs, since) {
499
+ const runCutoff = since ? since.toISOString().slice(0, 10) : null;
500
+ const runs = [];
501
+ for (const config of configs ?? iterConfigs(this.automationsDir)) {
502
+ for (const run of extractMemoryRuns(config)) {
503
+ if (runCutoff && run.date < runCutoff)
504
+ continue;
505
+ runs.push(run);
506
+ }
507
+ }
508
+ runs.sort((a, b) => {
509
+ if (a.date !== b.date)
510
+ return a.date < b.date ? 1 : -1;
511
+ const ai = a.automation.automationId;
512
+ const bi = b.automation.automationId;
513
+ if (ai !== bi)
514
+ return ai < bi ? 1 : -1;
515
+ return 0;
516
+ });
517
+ return runs;
518
+ }
519
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Codex session-index title lookup — narrow port of the three
3
+ * `session_thread_titles` helpers in `participants/codex.py`.
4
+ *
5
+ * SCOPE (deliberately tiny): this is NOT the Codex participant/reader (the
6
+ * ~2.6k-line turn-compiler still lands in a follow-on slice). It ports ONLY the
7
+ * surface the tray/presence join needs — a `session_id -> human thread title`
8
+ * map read from `~/.codex/session_index.jsonl`.
9
+ *
10
+ * Why only the index: `session_thread_titles()` runs on EVERY tray refresh, so
11
+ * it must never touch rollout files. One small JSONL read, newest-first, done.
12
+ * It covers Codex CLI sessions AND ChatGPT-app threads — the app's Work and
13
+ * Codex modes share `~/.codex`, so `thread_name` is the human title for both.
14
+ *
15
+ * PRIVACY: titles are raw user text. Callers that EXPORT them MUST route them
16
+ * through the audited redaction pipeline (`redactField` in
17
+ * `mcp-server/src/agents-export.ts`, == Python's `_redact_field`), exactly like
18
+ * every other emitted string. This module deliberately does NOT redact — the
19
+ * redaction happens at the single audited egress point so it can't be
20
+ * double-applied or silently skipped.
21
+ *
22
+ * Best-effort by construction: a missing dir, an unreadable file, or a corrupt
23
+ * line degrades to fewer/no titles. Nothing here throws.
24
+ */
25
+ /** Default number of index records considered (newest-first). */
26
+ export declare const DEFAULT_TITLE_LIMIT = 200;
27
+ /** Default number of threads on the tray codex card (newest-first). */
28
+ export declare const DEFAULT_RECENT_THREAD_LIMIT = 5;
29
+ /**
30
+ * Locate `~/.codex/` (the primary Codex store), or null.
31
+ *
32
+ * Resolution order, mirroring `_resolve_codex_home()` exactly:
33
+ * 1. `$CODEX_HOME` — **only if that directory already exists**
34
+ * 2. `~/.codex` — only if it exists
35
+ * 3. `null`
36
+ *
37
+ * ⚠ FOOTGUN (preserved for parity, do not "fix" without changing Python too):
38
+ * step 1 is a silent fallback, not a pin. If `$CODEX_HOME` points at a path that
39
+ * does not exist yet, the env var is IGNORED and this returns the developer's
40
+ * REAL `~/.codex`. A sandbox/test that sets `CODEX_HOME` to a temp path without
41
+ * `mkdir`-ing it first looks isolated but reads live data. Always create the
42
+ * directory before setting the variable.
43
+ */
44
+ export declare function resolveCodexHome(baseHome?: string | null): string | null;
45
+ /**
46
+ * Read `session_index.jsonl`, newest-first by `updated_at`.
47
+ *
48
+ * Port of `_parse_session_index()`. Per-line JSON parse: a malformed line is
49
+ * WARNED and SKIPPED — it never aborts the scan (one bad append must not blind
50
+ * the tray to every other session). An unreadable file warns and yields `[]`.
51
+ * Only records that are objects with a string `id` are kept.
52
+ */
53
+ export declare function parseSessionIndex(path: string, limit?: number | null): Record<string, unknown>[];
54
+ /**
55
+ * Map full Codex session id → human thread title, newest-first.
56
+ *
57
+ * Port of `session_thread_titles()`. Reads ONLY `session_index.jsonl` — no
58
+ * rollout I/O, because this runs on every tray refresh. A missing/malformed
59
+ * index yields `{}`.
60
+ *
61
+ * NEWEST WINS: entries are already sorted newest-first and the insert is a
62
+ * `setdefault` equivalent, so the freshest `thread_name` for a repeated id is
63
+ * the one kept.
64
+ *
65
+ * Titles are RAW USER TEXT — see the module note; the exporter redacts.
66
+ */
67
+ export declare function sessionThreadTitles(codexHome?: string | null, opts?: {
68
+ limit?: number;
69
+ }): Record<string, string>;
70
+ /** One row of the tray codex card. NOTE: `instance`, never a full session id. */
71
+ export interface RecentThread {
72
+ instance: string;
73
+ title: string | null;
74
+ updated_at: string | null;
75
+ }
76
+ /**
77
+ * Newest-first `[{instance, title, updated_at}]` for the tray codex card.
78
+ *
79
+ * Port of `recent_session_threads()`. Independent of PRESENCE: ChatGPT-app
80
+ * threads fire no presence hooks, so this is the only way they surface.
81
+ *
82
+ * Emits only the 8-char `instance` prefix — parity with `presence.summarize`:
83
+ * a full session id never leaves the local read path. Titles are RAW USER TEXT;
84
+ * the exporter redacts them (see `sessionThreadTitles`).
85
+ */
86
+ export declare function recentSessionThreads(codexHome?: string | null, opts?: {
87
+ limit?: number;
88
+ }): RecentThread[];