@getbourdon/participants 0.2.3 → 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.
- package/dist/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.d.ts +42 -1083
- package/dist/index.js +53 -6590
- 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 +583 -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,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code automations participant — publishes background-run memory as L5
|
|
3
|
+
* evidence. Port of `participants/claude_code_automations.py`.
|
|
4
|
+
*
|
|
5
|
+
* Convention-file reader over the `~/.claude/automations/<id>/` tree:
|
|
6
|
+
* automation.toml — id, name, status, schedule (rrule), kind, cwds
|
|
7
|
+
* memory.md — dated bullet entries, one block per run
|
|
8
|
+
*
|
|
9
|
+
* Each `automation.toml` becomes a known Entity (`type: automation`); each dated
|
|
10
|
+
* section of `memory.md` becomes a recent Session. Mirrors the Codex automation
|
|
11
|
+
* publisher — it covers the federation gap that the interactive-only
|
|
12
|
+
* `claude-code` participant leaves behind (automations whose work never touches
|
|
13
|
+
* agent-brain / auto-memory / the MCP knowledge graph).
|
|
14
|
+
*
|
|
15
|
+
* Privacy: every native string routes through the redaction SSOT
|
|
16
|
+
* (`redactText`, == the oracle's `_safe_native_memory_text`); the visibility
|
|
17
|
+
* policy filters PRIVATE entities before emission. `healthCheck` never throws.
|
|
18
|
+
*/
|
|
19
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
20
|
+
import { homedir, hostname } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
|
|
23
|
+
import { redactText } from "@getbourdon/redaction";
|
|
24
|
+
import { parse as parseToml } from "smol-toml";
|
|
25
|
+
import { ParticipantDiscoveryError, SPEC_VERSION, loadProjectHints, } from "../base.js";
|
|
26
|
+
// -- Constants -----------------------------------------------------------------
|
|
27
|
+
const AGENT_ID = "claude-code-automations";
|
|
28
|
+
const AGENT_TYPE = "other";
|
|
29
|
+
const ROLE_NARRATIVE = "Publishes read-only Claude Code automation run memory into Bourdon so " +
|
|
30
|
+
"scheduled /loop continuations, CronCreate jobs, GitHub Action runs of " +
|
|
31
|
+
"claude-code-action, and /schedule remote-routine summaries become " +
|
|
32
|
+
"visible alongside interactive Claude Code sessions.";
|
|
33
|
+
const DEFAULT_POLICY = makeVisibilityPolicy({
|
|
34
|
+
default: Visibility.TEAM,
|
|
35
|
+
private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
|
|
36
|
+
team_tags: ["claude-code-automation", "automation", "workspace"],
|
|
37
|
+
});
|
|
38
|
+
const AUTOMATIONS_DIR_NAME = "automations";
|
|
39
|
+
const AUTOMATION_TOML = "automation.toml";
|
|
40
|
+
const MEMORY_MD = "memory.md";
|
|
41
|
+
const MAX_MEMORY_CHARS = 160_000;
|
|
42
|
+
const MAX_KEY_ACTIONS_PER_RUN = 6;
|
|
43
|
+
const MAX_KEY_ACTION_CHARS = 280;
|
|
44
|
+
/** `^(\d{4}-\d{2}-\d{2})(?:\b|$)(.*)$` — anchored, non-global (safe to reuse). */
|
|
45
|
+
const RUN_HEADER_RE = /^(\d{4}-\d{2}-\d{2})(?:\b|$)(.*)$/;
|
|
46
|
+
const BULLET_RE = /^[-*]\s+(.*)$/;
|
|
47
|
+
/** Chars stripped from a run-header suffix (space, hyphen, colon, em-dash). */
|
|
48
|
+
const HEADER_SUFFIX_STRIP = " -:—";
|
|
49
|
+
// Product-neutral defaults only: this product plus the agent surfaces Bourdon
|
|
50
|
+
// federates. A deployment adds its own project roster via the
|
|
51
|
+
// `BOURDON_PROJECT_HINTS` env var (comma-separated) — see `loadProjectHints`.
|
|
52
|
+
const DEFAULT_PROJECT_HINTS = [
|
|
53
|
+
"Bourdon",
|
|
54
|
+
"Cursor",
|
|
55
|
+
"Copilot",
|
|
56
|
+
"Codex",
|
|
57
|
+
"Cascade",
|
|
58
|
+
];
|
|
59
|
+
/** Signal-class name → case-insensitive, NON-GLOBAL matcher. */
|
|
60
|
+
const SIGNAL_PATTERNS = [
|
|
61
|
+
["human-dashboard-action", /\b(human|dashboard|manual)\b/i],
|
|
62
|
+
["release-gate", /\b(release|store|app review|play console|testflight)\b/i],
|
|
63
|
+
["billing-drift", /\b(billing|stripe|revenuecat|iap|subscription)\b/i],
|
|
64
|
+
["memory-coverage-gap", /\b(memory|l5|manifest|federated|bourdon)\b/i],
|
|
65
|
+
["launch-decision", /\b(launch|go-live|pricing|prod|production)\b/i],
|
|
66
|
+
["ci-signal", /\b(github action|workflow run|ci failure|gh action)\b/i],
|
|
67
|
+
];
|
|
68
|
+
/** Minimal logger so a degraded read is visible without pulling in a dep. */
|
|
69
|
+
const logger = {
|
|
70
|
+
warn(msg) {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.warn(`[bourdon.participants] ${msg}`);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
// -- fs helpers ----------------------------------------------------------------
|
|
76
|
+
function homeDir() {
|
|
77
|
+
// os.homedir() can throw under some sandboxes; stay defensive so import +
|
|
78
|
+
// construction never crash even when HOME points at an unreadable path.
|
|
79
|
+
try {
|
|
80
|
+
return homedir();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return process.env.HOME || process.env.USERPROFILE || ".";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isDir(p) {
|
|
87
|
+
try {
|
|
88
|
+
return statSync(p).isDirectory();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isFile(p) {
|
|
95
|
+
try {
|
|
96
|
+
return statSync(p).isFile();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function exists(p) {
|
|
103
|
+
try {
|
|
104
|
+
statSync(p);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// -- Python-semantic string helpers -------------------------------------------
|
|
112
|
+
/** Python truthiness for the value kinds YAML/TOML yield here. */
|
|
113
|
+
function truthy(v) {
|
|
114
|
+
if (v === undefined || v === null || v === false || v === 0 || v === "")
|
|
115
|
+
return false;
|
|
116
|
+
if (Array.isArray(v))
|
|
117
|
+
return v.length > 0;
|
|
118
|
+
if (typeof v === "object")
|
|
119
|
+
return Object.keys(v).length > 0;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/** Mirror Python `str(value)` for the scalar types encountered here. */
|
|
123
|
+
function pyStr(v) {
|
|
124
|
+
if (v === true)
|
|
125
|
+
return "True";
|
|
126
|
+
if (v === false)
|
|
127
|
+
return "False";
|
|
128
|
+
if (v === null || v === undefined)
|
|
129
|
+
return "None";
|
|
130
|
+
return String(v);
|
|
131
|
+
}
|
|
132
|
+
/** Mirror `str(a or b)` — pick `a` if truthy else `b`, then `str()`. */
|
|
133
|
+
function strOr(a, b) {
|
|
134
|
+
return pyStr(truthy(a) ? a : b);
|
|
135
|
+
}
|
|
136
|
+
/** Python `str.rstrip()` — drop trailing whitespace. */
|
|
137
|
+
function rstrip(s) {
|
|
138
|
+
return s.replace(/\s+$/, "");
|
|
139
|
+
}
|
|
140
|
+
/** Python `str.strip(chars)` — strip any of `chars` from both ends. */
|
|
141
|
+
function stripChars(s, chars) {
|
|
142
|
+
let start = 0;
|
|
143
|
+
let end = s.length;
|
|
144
|
+
while (start < end && chars.includes(s[start]))
|
|
145
|
+
start++;
|
|
146
|
+
while (end > start && chars.includes(s[end - 1]))
|
|
147
|
+
end--;
|
|
148
|
+
return s.slice(start, end);
|
|
149
|
+
}
|
|
150
|
+
/** Approximate Python `str.splitlines()` (no trailing empty for a final EOL). */
|
|
151
|
+
function splitLines(s) {
|
|
152
|
+
const parts = s.split(/\r\n|\r|\n/);
|
|
153
|
+
if (parts.length > 0 && parts[parts.length - 1] === "")
|
|
154
|
+
parts.pop();
|
|
155
|
+
return parts;
|
|
156
|
+
}
|
|
157
|
+
// -- Path resolution -----------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Default Claude Code automations directory. Precedence:
|
|
160
|
+
* 1. explicit `claudeHome` argument (tests)
|
|
161
|
+
* 2. `CLAUDE_HOME` env var
|
|
162
|
+
* 3. `~/.claude/` (conventional per-user default)
|
|
163
|
+
*/
|
|
164
|
+
export function defaultClaudeCodeAutomationsDir(claudeHome) {
|
|
165
|
+
if (claudeHome !== undefined)
|
|
166
|
+
return join(claudeHome, AUTOMATIONS_DIR_NAME);
|
|
167
|
+
const env = process.env.CLAUDE_HOME;
|
|
168
|
+
if (env)
|
|
169
|
+
return join(env, AUTOMATIONS_DIR_NAME);
|
|
170
|
+
return join(homeDir(), ".claude", AUTOMATIONS_DIR_NAME);
|
|
171
|
+
}
|
|
172
|
+
// -- TOML config parsing -------------------------------------------------------
|
|
173
|
+
function readAutomationToml(path) {
|
|
174
|
+
let raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = parseToml(readFileSync(path, "utf8"));
|
|
177
|
+
}
|
|
178
|
+
catch (exc) {
|
|
179
|
+
logger.warn(`ClaudeCodeAutomationsParticipant: cannot parse ${path}: ${String(exc)}`);
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
183
|
+
? raw
|
|
184
|
+
: {};
|
|
185
|
+
}
|
|
186
|
+
function buildConfig(tomlPath) {
|
|
187
|
+
const raw = readAutomationToml(tomlPath);
|
|
188
|
+
const parent = dirname(tomlPath);
|
|
189
|
+
const parentName = basenameOf(parent);
|
|
190
|
+
const automationId = strOr(raw["id"], parentName).trim();
|
|
191
|
+
if (!automationId)
|
|
192
|
+
return null;
|
|
193
|
+
const name = strOr(raw["name"], automationId).trim();
|
|
194
|
+
const status = strOr(raw["status"], "UNKNOWN").trim().toUpperCase();
|
|
195
|
+
const rrule = strOr(raw["rrule"], "").trim();
|
|
196
|
+
const kind = strOr(raw["kind"], "").trim();
|
|
197
|
+
const cwdsRaw = truthy(raw["cwds"]) ? raw["cwds"] : [];
|
|
198
|
+
const cwds = [];
|
|
199
|
+
// Faithful to `for cwd in cwds_raw if isinstance(cwd, str) and cwd.strip()`:
|
|
200
|
+
// arrays iterate elements, a string iterates characters. Other (non-iterable)
|
|
201
|
+
// types yield no cwds here (the oracle would raise; see port notes).
|
|
202
|
+
if (Array.isArray(cwdsRaw)) {
|
|
203
|
+
for (const cwd of cwdsRaw) {
|
|
204
|
+
if (typeof cwd === "string" && cwd.trim())
|
|
205
|
+
cwds.push(cwd);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (typeof cwdsRaw === "string") {
|
|
209
|
+
for (const ch of cwdsRaw) {
|
|
210
|
+
if (ch.trim())
|
|
211
|
+
cwds.push(ch);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const memoryPath = join(parent, MEMORY_MD);
|
|
215
|
+
return {
|
|
216
|
+
automationId,
|
|
217
|
+
name,
|
|
218
|
+
status,
|
|
219
|
+
rrule,
|
|
220
|
+
kind,
|
|
221
|
+
cwds,
|
|
222
|
+
path: tomlPath,
|
|
223
|
+
memoryPath: isFile(memoryPath) ? memoryPath : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** `os.path.basename` that also handles a trailing separator like Path.name. */
|
|
227
|
+
function basenameOf(p) {
|
|
228
|
+
const cleaned = p.replace(/[/\\]+$/, "");
|
|
229
|
+
const idx = Math.max(cleaned.lastIndexOf("/"), cleaned.lastIndexOf("\\"));
|
|
230
|
+
return idx === -1 ? cleaned : cleaned.slice(idx + 1);
|
|
231
|
+
}
|
|
232
|
+
function iterConfigs(automationsDir) {
|
|
233
|
+
const configs = [];
|
|
234
|
+
if (!isDir(automationsDir))
|
|
235
|
+
return configs;
|
|
236
|
+
// Mirror `sorted(dir.glob("*/automation.toml"))`: one directory level deep,
|
|
237
|
+
// sorted by full path (== sorted by subdir name given the shared prefix).
|
|
238
|
+
const tomlPaths = [];
|
|
239
|
+
let entries;
|
|
240
|
+
try {
|
|
241
|
+
entries = readdirSync(automationsDir);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return configs;
|
|
245
|
+
}
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const sub = join(automationsDir, entry);
|
|
248
|
+
if (!isDir(sub))
|
|
249
|
+
continue;
|
|
250
|
+
const tomlPath = join(sub, AUTOMATION_TOML);
|
|
251
|
+
if (isFile(tomlPath))
|
|
252
|
+
tomlPaths.push(tomlPath);
|
|
253
|
+
}
|
|
254
|
+
tomlPaths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
255
|
+
for (const tomlPath of tomlPaths) {
|
|
256
|
+
const config = buildConfig(tomlPath);
|
|
257
|
+
if (config !== null)
|
|
258
|
+
configs.push(config);
|
|
259
|
+
}
|
|
260
|
+
return configs;
|
|
261
|
+
}
|
|
262
|
+
// -- memory.md → runs ----------------------------------------------------------
|
|
263
|
+
function readMemoryText(path) {
|
|
264
|
+
if (path === null)
|
|
265
|
+
return "";
|
|
266
|
+
let text;
|
|
267
|
+
try {
|
|
268
|
+
text = readFileSync(path, "utf8");
|
|
269
|
+
}
|
|
270
|
+
catch (exc) {
|
|
271
|
+
logger.warn(`ClaudeCodeAutomationsParticipant: cannot read ${path}: ${String(exc)}`);
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
return text.slice(-MAX_MEMORY_CHARS);
|
|
275
|
+
}
|
|
276
|
+
function extractMemoryRuns(config) {
|
|
277
|
+
const text = readMemoryText(config.memoryPath);
|
|
278
|
+
if (!text)
|
|
279
|
+
return [];
|
|
280
|
+
const chunks = [];
|
|
281
|
+
let currentDate = "";
|
|
282
|
+
let currentLines = [];
|
|
283
|
+
for (const rawLine of splitLines(text)) {
|
|
284
|
+
const line = rstrip(rawLine);
|
|
285
|
+
const match = RUN_HEADER_RE.exec(line.trim());
|
|
286
|
+
if (match) {
|
|
287
|
+
if (currentDate)
|
|
288
|
+
chunks.push([currentDate, currentLines]);
|
|
289
|
+
currentDate = match[1];
|
|
290
|
+
const suffix = stripChars(match[2] ?? "", HEADER_SUFFIX_STRIP);
|
|
291
|
+
currentLines = suffix ? [suffix] : [];
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (currentDate)
|
|
295
|
+
currentLines.push(line);
|
|
296
|
+
}
|
|
297
|
+
if (currentDate)
|
|
298
|
+
chunks.push([currentDate, currentLines]);
|
|
299
|
+
const runs = [];
|
|
300
|
+
for (const [runDate, lines] of chunks) {
|
|
301
|
+
const actions = actionsFromLines(lines);
|
|
302
|
+
if (actions.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
const body = actions.join(" ");
|
|
305
|
+
const title = titleFromActions(config, actions);
|
|
306
|
+
runs.push({
|
|
307
|
+
automation: config,
|
|
308
|
+
date: runDate,
|
|
309
|
+
title,
|
|
310
|
+
keyActions: actions,
|
|
311
|
+
projects: inferProjects(body),
|
|
312
|
+
signals: inferSignals(body),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return runs;
|
|
316
|
+
}
|
|
317
|
+
function actionsFromLines(lines) {
|
|
318
|
+
const actions = [];
|
|
319
|
+
for (const line of lines) {
|
|
320
|
+
let cleaned = line.trim();
|
|
321
|
+
if (!cleaned)
|
|
322
|
+
continue;
|
|
323
|
+
cleaned = removePrefix(cleaned, "- ").trim();
|
|
324
|
+
const lowered = cleaned.toLowerCase();
|
|
325
|
+
if (!cleaned || lowered.startsWith("runtime"))
|
|
326
|
+
continue;
|
|
327
|
+
if (lowered === "first run" || lowered === "follow-up")
|
|
328
|
+
continue;
|
|
329
|
+
if (lowered.startsWith("run:"))
|
|
330
|
+
cleaned = cleaned.slice(4).trim();
|
|
331
|
+
const safe = bounded(safeNativeMemoryText(cleaned), MAX_KEY_ACTION_CHARS);
|
|
332
|
+
if (safe && !actions.includes(safe))
|
|
333
|
+
actions.push(safe);
|
|
334
|
+
if (actions.length >= MAX_KEY_ACTIONS_PER_RUN)
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
return actions;
|
|
338
|
+
}
|
|
339
|
+
/** Python `str.removeprefix`. */
|
|
340
|
+
function removePrefix(s, prefix) {
|
|
341
|
+
return s.startsWith(prefix) ? s.slice(prefix.length) : s;
|
|
342
|
+
}
|
|
343
|
+
/** The oracle's `_safe_native_memory_text(v)` == `redactText(v, 180)`. */
|
|
344
|
+
function safeNativeMemoryText(value) {
|
|
345
|
+
return redactText(value, 180);
|
|
346
|
+
}
|
|
347
|
+
function titleFromActions(config, actions) {
|
|
348
|
+
if (actions.length === 0)
|
|
349
|
+
return config.name;
|
|
350
|
+
const first = actions[0];
|
|
351
|
+
const prefix = `${config.name}: `;
|
|
352
|
+
if (first.startsWith(prefix))
|
|
353
|
+
return bounded(first, 120);
|
|
354
|
+
return bounded(prefix + first, 120);
|
|
355
|
+
}
|
|
356
|
+
function inferProjects(text) {
|
|
357
|
+
const projects = [];
|
|
358
|
+
const lowered = text.toLowerCase();
|
|
359
|
+
const seen = new Set();
|
|
360
|
+
for (const project of loadProjectHints(DEFAULT_PROJECT_HINTS)) {
|
|
361
|
+
const key = project.toLowerCase();
|
|
362
|
+
if (lowered.includes(key) && !seen.has(key)) {
|
|
363
|
+
projects.push(project);
|
|
364
|
+
seen.add(key);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return projects;
|
|
368
|
+
}
|
|
369
|
+
function inferSignals(text) {
|
|
370
|
+
const signals = [];
|
|
371
|
+
for (const [name, pattern] of SIGNAL_PATTERNS) {
|
|
372
|
+
if (pattern.test(text))
|
|
373
|
+
signals.push(name);
|
|
374
|
+
}
|
|
375
|
+
return signals;
|
|
376
|
+
}
|
|
377
|
+
/** `" ".join(value.split())`, then cap at `limit` with a `...` tail (limit-1). */
|
|
378
|
+
function bounded(value, limit) {
|
|
379
|
+
const normalized = value.trim().replace(/\s+/g, " ");
|
|
380
|
+
if (normalized.length <= limit)
|
|
381
|
+
return normalized;
|
|
382
|
+
return rstrip(normalized.slice(0, limit - 1)) + "...";
|
|
383
|
+
}
|
|
384
|
+
// -- run → session / entities --------------------------------------------------
|
|
385
|
+
function sessionFromRun(run) {
|
|
386
|
+
const config = run.automation;
|
|
387
|
+
const keyActions = [
|
|
388
|
+
`automation_id: ${config.automationId}`,
|
|
389
|
+
`run: ${run.title}`,
|
|
390
|
+
...run.keyActions,
|
|
391
|
+
];
|
|
392
|
+
const filesTouched = [config.path];
|
|
393
|
+
if (config.memoryPath !== null)
|
|
394
|
+
filesTouched.push(config.memoryPath);
|
|
395
|
+
return makeSession({
|
|
396
|
+
date: run.date,
|
|
397
|
+
cwd: config.cwds.length > 0 ? config.cwds[0] : dirname(config.path),
|
|
398
|
+
project_focus: [...run.projects],
|
|
399
|
+
key_actions: keyActions.slice(0, MAX_KEY_ACTIONS_PER_RUN + 2),
|
|
400
|
+
files_touched: filesTouched,
|
|
401
|
+
visibility: Visibility.TEAM,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function entitiesFromConfigsAndRuns(configs, runs) {
|
|
405
|
+
// Insertion-ordered map (Python dict) — configs first, then run projects +
|
|
406
|
+
// signals via setdefault (first-wins).
|
|
407
|
+
const byName = new Map();
|
|
408
|
+
for (const config of configs) {
|
|
409
|
+
byName.set(config.automationId, makeEntity({
|
|
410
|
+
name: config.automationId,
|
|
411
|
+
type: "automation",
|
|
412
|
+
summary: bounded(`Claude Code automation '${config.name}' (${config.status}). ` +
|
|
413
|
+
`Schedule: ${config.rrule || "unspecified"}.`, 260),
|
|
414
|
+
last_touched: undefined,
|
|
415
|
+
tags: ["claude-code-automation", "automation", config.status.toLowerCase()],
|
|
416
|
+
visibility: Visibility.TEAM,
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
for (const run of runs) {
|
|
420
|
+
for (const project of run.projects) {
|
|
421
|
+
if (!byName.has(project)) {
|
|
422
|
+
byName.set(project, makeEntity({
|
|
423
|
+
name: project,
|
|
424
|
+
type: "project",
|
|
425
|
+
summary: "Project mentioned by Claude Code automation run memory.",
|
|
426
|
+
last_touched: run.date,
|
|
427
|
+
tags: ["claude-code-automation", "automation-evidence"],
|
|
428
|
+
visibility: Visibility.TEAM,
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const signal of run.signals) {
|
|
433
|
+
if (!byName.has(signal)) {
|
|
434
|
+
byName.set(signal, makeEntity({
|
|
435
|
+
name: signal,
|
|
436
|
+
type: "automation-signal",
|
|
437
|
+
summary: "Signal class inferred from Claude Code automation run memory.",
|
|
438
|
+
last_touched: run.date,
|
|
439
|
+
tags: ["claude-code-automation", "automation-signal"],
|
|
440
|
+
visibility: Visibility.TEAM,
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return [...byName.values()];
|
|
446
|
+
}
|
|
447
|
+
// -- memory.md section merge (ingestion utility) -------------------------------
|
|
448
|
+
/**
|
|
449
|
+
* Split memory.md text into `[date_header, bullets]` sections. Tolerates
|
|
450
|
+
* `2026-06-03`, `2026-06-03 -- subtitle`, and same-line `2026-06-03 run: ...`.
|
|
451
|
+
* Content before the first date header is discarded.
|
|
452
|
+
*/
|
|
453
|
+
function parseMemorySections(text) {
|
|
454
|
+
const sections = [];
|
|
455
|
+
let currentDate = "";
|
|
456
|
+
let currentBullets = [];
|
|
457
|
+
for (const rawLine of splitLines(text)) {
|
|
458
|
+
const line = rstrip(rawLine);
|
|
459
|
+
const match = RUN_HEADER_RE.exec(line.trim());
|
|
460
|
+
if (match) {
|
|
461
|
+
if (currentDate)
|
|
462
|
+
sections.push([currentDate, currentBullets]);
|
|
463
|
+
currentDate = match[1];
|
|
464
|
+
const suffix = stripChars(match[2] ?? "", HEADER_SUFFIX_STRIP);
|
|
465
|
+
currentBullets = [];
|
|
466
|
+
if (suffix)
|
|
467
|
+
currentBullets.push(suffix);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (!currentDate)
|
|
471
|
+
continue;
|
|
472
|
+
const bulletMatch = BULLET_RE.exec(line.trim());
|
|
473
|
+
if (bulletMatch)
|
|
474
|
+
currentBullets.push((bulletMatch[1] ?? "").trim());
|
|
475
|
+
}
|
|
476
|
+
if (currentDate)
|
|
477
|
+
sections.push([currentDate, currentBullets]);
|
|
478
|
+
return sections;
|
|
479
|
+
}
|
|
480
|
+
/** Render `[date, bullets]` sections back to memory.md form. */
|
|
481
|
+
function serializeSections(sections) {
|
|
482
|
+
const blocks = [];
|
|
483
|
+
for (const [dateStr, bullets] of sections) {
|
|
484
|
+
const lines = [dateStr, ...bullets.map((b) => `- ${b}`)];
|
|
485
|
+
blocks.push(lines.join("\n"));
|
|
486
|
+
}
|
|
487
|
+
return blocks.join("\n\n") + "\n";
|
|
488
|
+
}
|
|
489
|
+
const AUTOMATION_ID_RE = /^[A-Za-z0-9._-]+$/;
|
|
490
|
+
/**
|
|
491
|
+
* Merge an `automations/<id>/` tree from `sourceDir` into `destDir`. Copies new
|
|
492
|
+
* automations wholesale (stubbing automation.toml when absent) and merges
|
|
493
|
+
* memory.md bullets per-date (exact-string dedupe). Idempotent. Designed for
|
|
494
|
+
* ingesting GitHub Action workflow artifacts.
|
|
495
|
+
*/
|
|
496
|
+
export function mergeAutomationTree(sourceDir, destDir, defaultKind = "github-action") {
|
|
497
|
+
if (!isDir(sourceDir)) {
|
|
498
|
+
throw new Error(`merge source not found: ${sourceDir}`);
|
|
499
|
+
}
|
|
500
|
+
mkdirSync(destDir, { recursive: true });
|
|
501
|
+
let seen = 0;
|
|
502
|
+
let created = 0;
|
|
503
|
+
let bulletsAdded = 0;
|
|
504
|
+
let sectionsCreated = 0;
|
|
505
|
+
const skipped = [];
|
|
506
|
+
const srcEntries = readdirSync(sourceDir).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
507
|
+
for (const entry of srcEntries) {
|
|
508
|
+
const srcIdDir = join(sourceDir, entry);
|
|
509
|
+
if (!isDir(srcIdDir))
|
|
510
|
+
continue;
|
|
511
|
+
seen += 1;
|
|
512
|
+
const automationId = entry;
|
|
513
|
+
if (!AUTOMATION_ID_RE.test(automationId)) {
|
|
514
|
+
skipped.push(automationId);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const destIdDir = join(destDir, automationId);
|
|
518
|
+
const srcToml = join(srcIdDir, AUTOMATION_TOML);
|
|
519
|
+
const srcMemory = join(srcIdDir, MEMORY_MD);
|
|
520
|
+
if (!exists(destIdDir)) {
|
|
521
|
+
mkdirSync(destIdDir, { recursive: true });
|
|
522
|
+
created += 1;
|
|
523
|
+
if (isFile(srcToml)) {
|
|
524
|
+
writeFileSync(join(destIdDir, AUTOMATION_TOML), readFileSync(srcToml, "utf8"), "utf8");
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
writeFileSync(join(destIdDir, AUTOMATION_TOML), `version = 1\n` +
|
|
528
|
+
`id = "${automationId}"\n` +
|
|
529
|
+
`name = "${automationId}"\n` +
|
|
530
|
+
`status = "ACTIVE"\n` +
|
|
531
|
+
`kind = "${defaultKind}"\n` +
|
|
532
|
+
`rrule = ""\n` +
|
|
533
|
+
`cwds = []\n`, "utf8");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (!isFile(srcMemory))
|
|
537
|
+
continue;
|
|
538
|
+
const srcSections = parseMemorySections(readFileSync(srcMemory, "utf8"));
|
|
539
|
+
const destMemory = join(destIdDir, MEMORY_MD);
|
|
540
|
+
const destSections = isFile(destMemory)
|
|
541
|
+
? parseMemorySections(readFileSync(destMemory, "utf8"))
|
|
542
|
+
: [];
|
|
543
|
+
// `dict(dest_sections)` — last-wins on duplicate dates; the list refs are
|
|
544
|
+
// shared with destSections so appends via `existing` are reflected.
|
|
545
|
+
const destByDate = new Map();
|
|
546
|
+
for (const [dateStr, bullets] of destSections)
|
|
547
|
+
destByDate.set(dateStr, bullets);
|
|
548
|
+
for (const [dateStr, bullets] of srcSections) {
|
|
549
|
+
if (!destByDate.has(dateStr)) {
|
|
550
|
+
const fresh = [];
|
|
551
|
+
destByDate.set(dateStr, fresh);
|
|
552
|
+
destSections.push([dateStr, fresh]);
|
|
553
|
+
sectionsCreated += 1;
|
|
554
|
+
}
|
|
555
|
+
const existing = destByDate.get(dateStr);
|
|
556
|
+
for (const bullet of bullets) {
|
|
557
|
+
if (!existing.includes(bullet)) {
|
|
558
|
+
existing.push(bullet);
|
|
559
|
+
bulletsAdded += 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
destSections.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
564
|
+
writeFileSync(destMemory, serializeSections(destSections), "utf8");
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
automationsSeen: seen,
|
|
568
|
+
automationsCreated: created,
|
|
569
|
+
bulletsAdded,
|
|
570
|
+
sectionsCreated,
|
|
571
|
+
skipped,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// -- Participant ---------------------------------------------------------------
|
|
575
|
+
/** External participant for Claude Code automation memory artifacts. */
|
|
576
|
+
export class ClaudeCodeAutomationsParticipant {
|
|
577
|
+
agentId = AGENT_ID;
|
|
578
|
+
agentType = AGENT_TYPE;
|
|
579
|
+
nativePath;
|
|
580
|
+
automationsDir;
|
|
581
|
+
policy;
|
|
582
|
+
/**
|
|
583
|
+
* Conventional Claude Code automations dir (`~/.claude/automations`). Provided
|
|
584
|
+
* for protocol uniformity — the setup wizard wires the parent `claude-code`
|
|
585
|
+
* participant and skips `-automations` sub-surfaces.
|
|
586
|
+
*/
|
|
587
|
+
static defaultNativePath(home) {
|
|
588
|
+
return defaultClaudeCodeAutomationsDir(home);
|
|
589
|
+
}
|
|
590
|
+
constructor(automationsDir, claudeHome) {
|
|
591
|
+
this.automationsDir = automationsDir ?? defaultClaudeCodeAutomationsDir(claudeHome);
|
|
592
|
+
this.nativePath = this.automationsDir;
|
|
593
|
+
this.policy = DEFAULT_POLICY;
|
|
594
|
+
}
|
|
595
|
+
discover() {
|
|
596
|
+
if (!isDir(this.automationsDir)) {
|
|
597
|
+
throw new ParticipantDiscoveryError(`Claude Code automations directory not found at ${this.automationsDir}.`);
|
|
598
|
+
}
|
|
599
|
+
const configs = iterConfigs(this.automationsDir);
|
|
600
|
+
return {
|
|
601
|
+
path: this.automationsDir,
|
|
602
|
+
version: "unknown",
|
|
603
|
+
metadata: {
|
|
604
|
+
automations: configs.length,
|
|
605
|
+
with_memory: configs.filter((c) => c.memoryPath !== null).length,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
exportSessions(since, limit = 100) {
|
|
610
|
+
const sessions = this.runs(undefined, since).map(sessionFromRun);
|
|
611
|
+
sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
612
|
+
return sessions.slice(0, limit);
|
|
613
|
+
}
|
|
614
|
+
exportL5(since) {
|
|
615
|
+
if (!isDir(this.automationsDir)) {
|
|
616
|
+
throw new ParticipantDiscoveryError(`Claude Code automations directory not found at ${this.automationsDir}.`);
|
|
617
|
+
}
|
|
618
|
+
const configs = iterConfigs(this.automationsDir);
|
|
619
|
+
const runs = this.runs(configs, since);
|
|
620
|
+
const sessions = runs.map(sessionFromRun);
|
|
621
|
+
sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
622
|
+
const entities = entitiesFromConfigsAndRuns(configs, runs);
|
|
623
|
+
const visibleEntities = filterForFederation(entities, this.policy);
|
|
624
|
+
return makeManifest({
|
|
625
|
+
spec_version: SPEC_VERSION,
|
|
626
|
+
agent: makeAgentInfo({
|
|
627
|
+
id: AGENT_ID,
|
|
628
|
+
type: AGENT_TYPE,
|
|
629
|
+
instance: hostname(),
|
|
630
|
+
spec_version_compat: SPEC_VERSION,
|
|
631
|
+
role_narrative: ROLE_NARRATIVE,
|
|
632
|
+
}),
|
|
633
|
+
last_updated: new Date().toISOString(),
|
|
634
|
+
capabilities: ["claude-code-automation-memory", "run-summary-publication"],
|
|
635
|
+
recent_sessions: sessions,
|
|
636
|
+
known_entities: visibleEntities,
|
|
637
|
+
visibility_policy: this.policy,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
healthCheck() {
|
|
641
|
+
if (!isDir(this.automationsDir)) {
|
|
642
|
+
return {
|
|
643
|
+
status: "blocked",
|
|
644
|
+
reason: `Claude Code automations directory not found at ${this.automationsDir}.`,
|
|
645
|
+
details: { automations_dir: this.automationsDir },
|
|
646
|
+
proposedFix: "Create the automations directory and an automation.toml: " +
|
|
647
|
+
"mkdir -p ~/.claude/automations/<id> && " +
|
|
648
|
+
"$HOME/.claude/hooks/automation-memory-append.sh <id> '...'",
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const configs = iterConfigs(this.automationsDir);
|
|
652
|
+
const runs = this.runs(configs);
|
|
653
|
+
const hasConfigs = configs.length > 0;
|
|
654
|
+
const status = hasConfigs ? "ok" : "degraded";
|
|
655
|
+
const details = {
|
|
656
|
+
automations_dir: this.automationsDir,
|
|
657
|
+
automation_count: configs.length,
|
|
658
|
+
memory_files: configs.filter((c) => c.memoryPath !== null).length,
|
|
659
|
+
runs_extracted: runs.length,
|
|
660
|
+
active_automations: configs.filter((c) => c.status === "ACTIVE").length,
|
|
661
|
+
};
|
|
662
|
+
if (hasConfigs) {
|
|
663
|
+
return { status, details };
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
status,
|
|
667
|
+
reason: "No automation.toml files found.",
|
|
668
|
+
details,
|
|
669
|
+
proposedFix: "Add Claude Code automation.toml files.",
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
runs(configs, since) {
|
|
673
|
+
const runCutoff = since ? since.toISOString().slice(0, 10) : null;
|
|
674
|
+
const runs = [];
|
|
675
|
+
for (const config of configs ?? iterConfigs(this.automationsDir)) {
|
|
676
|
+
for (const run of extractMemoryRuns(config)) {
|
|
677
|
+
if (runCutoff && run.date < runCutoff)
|
|
678
|
+
continue;
|
|
679
|
+
runs.push(run);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
runs.sort((a, b) => {
|
|
683
|
+
if (a.date !== b.date)
|
|
684
|
+
return a.date < b.date ? 1 : -1;
|
|
685
|
+
const ai = a.automation.automationId;
|
|
686
|
+
const bi = b.automation.automationId;
|
|
687
|
+
if (ai !== bi)
|
|
688
|
+
return ai < bi ? 1 : -1;
|
|
689
|
+
return 0;
|
|
690
|
+
});
|
|
691
|
+
return runs;
|
|
692
|
+
}
|
|
693
|
+
}
|