@getbourdon/participants 0.2.3 → 0.3.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/LICENSE +1 -1
- package/LICENSE_FAQ.md +1 -1
- package/dist/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.cjs +12 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +42 -1083
- package/dist/index.js +53 -6590
- package/dist/index.js.map +1 -1
- package/dist/network-base.d.ts +108 -0
- package/dist/network-base.js +248 -0
- package/dist/readers/cascade.d.ts +49 -0
- package/dist/readers/cascade.js +428 -0
- package/dist/readers/claude-code-automations.d.ts +62 -0
- package/dist/readers/claude-code-automations.js +693 -0
- package/dist/readers/claude-code.d.ts +50 -0
- package/dist/readers/claude-code.js +590 -0
- package/dist/readers/claude-desktop-code.d.ts +54 -0
- package/dist/readers/claude-desktop-code.js +526 -0
- package/dist/readers/claude-desktop-cowork.d.ts +47 -0
- package/dist/readers/claude-desktop-cowork.js +659 -0
- package/dist/readers/codex-automations.d.ts +55 -0
- package/dist/readers/codex-automations.js +519 -0
- package/dist/readers/codex-sessions.d.ts +88 -0
- package/dist/readers/codex-sessions.js +194 -0
- package/dist/readers/copilot-automations.d.ts +64 -0
- package/dist/readers/copilot-automations.js +530 -0
- package/dist/readers/copilot-cli.d.ts +45 -0
- package/dist/readers/copilot-cli.js +416 -0
- package/dist/readers/copilot-vscode.d.ts +54 -0
- package/dist/readers/copilot-vscode.js +553 -0
- package/dist/readers/copilot.d.ts +59 -0
- package/dist/readers/copilot.js +413 -0
- package/dist/readers/cursor-automations.d.ts +57 -0
- package/dist/readers/cursor-automations.js +685 -0
- package/dist/readers/cursor.d.ts +50 -0
- package/dist/readers/cursor.js +670 -0
- package/dist/readers/github-copilot.d.ts +34 -0
- package/dist/readers/github-copilot.js +242 -0
- package/dist/readers/hermes.d.ts +37 -0
- package/dist/readers/hermes.js +426 -0
- package/dist/readers/openclaw.d.ts +78 -0
- package/dist/readers/openclaw.js +386 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +98 -0
- package/dist/sqlite-base.d.ts +65 -0
- package/dist/sqlite-base.js +163 -0
- package/package.json +6 -6
|
@@ -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[];
|