@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.
Files changed (43) hide show
  1. package/dist/base.d.ts +109 -0
  2. package/dist/base.js +88 -0
  3. package/dist/index.d.ts +42 -1083
  4. package/dist/index.js +53 -6590
  5. package/dist/network-base.d.ts +108 -0
  6. package/dist/network-base.js +248 -0
  7. package/dist/readers/cascade.d.ts +49 -0
  8. package/dist/readers/cascade.js +428 -0
  9. package/dist/readers/claude-code-automations.d.ts +62 -0
  10. package/dist/readers/claude-code-automations.js +693 -0
  11. package/dist/readers/claude-code.d.ts +50 -0
  12. package/dist/readers/claude-code.js +583 -0
  13. package/dist/readers/claude-desktop-code.d.ts +54 -0
  14. package/dist/readers/claude-desktop-code.js +526 -0
  15. package/dist/readers/claude-desktop-cowork.d.ts +47 -0
  16. package/dist/readers/claude-desktop-cowork.js +659 -0
  17. package/dist/readers/codex-automations.d.ts +55 -0
  18. package/dist/readers/codex-automations.js +519 -0
  19. package/dist/readers/codex-sessions.d.ts +88 -0
  20. package/dist/readers/codex-sessions.js +194 -0
  21. package/dist/readers/copilot-automations.d.ts +64 -0
  22. package/dist/readers/copilot-automations.js +530 -0
  23. package/dist/readers/copilot-cli.d.ts +45 -0
  24. package/dist/readers/copilot-cli.js +416 -0
  25. package/dist/readers/copilot-vscode.d.ts +54 -0
  26. package/dist/readers/copilot-vscode.js +553 -0
  27. package/dist/readers/copilot.d.ts +59 -0
  28. package/dist/readers/copilot.js +413 -0
  29. package/dist/readers/cursor-automations.d.ts +57 -0
  30. package/dist/readers/cursor-automations.js +685 -0
  31. package/dist/readers/cursor.d.ts +50 -0
  32. package/dist/readers/cursor.js +670 -0
  33. package/dist/readers/github-copilot.d.ts +34 -0
  34. package/dist/readers/github-copilot.js +242 -0
  35. package/dist/readers/hermes.d.ts +37 -0
  36. package/dist/readers/hermes.js +426 -0
  37. package/dist/readers/openclaw.d.ts +78 -0
  38. package/dist/readers/openclaw.js +386 -0
  39. package/dist/registry.d.ts +39 -0
  40. package/dist/registry.js +98 -0
  41. package/dist/sqlite-base.d.ts +65 -0
  42. package/dist/sqlite-base.js +163 -0
  43. package/package.json +6 -6
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Claude Code participant — reads agent-brain + auto-memory + MCP knowledge
3
+ * graph. Port of `participants/claude_code.py`.
4
+ *
5
+ * Three optional sources (the participant degrades gracefully):
6
+ * 1. agent-brain/ — git-synced markdown records
7
+ * 2. ~/.claude/projects/<ws>/memory/ — per-machine auto-memory frontmatter
8
+ * 3. ~/claude-memory/memory.jsonl — MCP knowledge-graph JSONL
9
+ *
10
+ * For back-compat the pre-rename env var (`CLAUDE_BRAIN`) and the pre-rename
11
+ * directory name are still accepted — see BRAIN_DIR_NAMES / BRAIN_ENV_VARS.
12
+ *
13
+ * Privacy: entities typed `person` (+ family/contact variants) and observations
14
+ * containing credential-like strings default to PRIVATE and are filtered before
15
+ * federation. Entities are deduped by case-insensitive name and sorted
16
+ * alphabetically.
17
+ */
18
+ import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
19
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
20
+ /**
21
+ * Directory names for the knowledge repo, most-preferred first. The second
22
+ * entry is a LEGACY ALIAS kept only so existing checkouts keep resolving after
23
+ * the rename to a product-neutral default; new installs should use the first.
24
+ * The alias is assembled at runtime so no build artifact (bundle or sourcemap)
25
+ * carries it as a contiguous literal — the packed-tarball gate must stay clean
26
+ * without artifact-path exemptions.
27
+ */
28
+ export declare const BRAIN_DIR_NAMES: readonly [string, string];
29
+ /**
30
+ * Env overrides, most-preferred first. `CLAUDE_BRAIN` is retained for
31
+ * back-compat with installs that set it before the rename.
32
+ */
33
+ export declare const BRAIN_ENV_VARS: readonly ["AGENT_BRAIN", "CLAUDE_BRAIN"];
34
+ /** External participant for Anthropic's Claude Code CLI. */
35
+ export declare class ClaudeCodeParticipant implements BourdonParticipant {
36
+ agentId: string;
37
+ agentType: string;
38
+ nativePath: string;
39
+ /** Public so a hermetic test can seed the discovered sources directly (path
40
+ * RESOLUTION is not the parity contract — only the export output shape is). */
41
+ brainPath: string | null;
42
+ autoMemoryPath: string | null;
43
+ knowledgeGraphPath: string | null;
44
+ static defaultNativePath(home?: string): string;
45
+ constructor();
46
+ discover(): AgentStore;
47
+ exportL5(since?: Date): L5ManifestModel;
48
+ exportSessions(since?: Date, limit?: number): SessionModel[];
49
+ healthCheck(): HealthStatus;
50
+ }
@@ -0,0 +1,583 @@
1
+ /**
2
+ * Claude Code participant — reads agent-brain + auto-memory + MCP knowledge
3
+ * graph. Port of `participants/claude_code.py`.
4
+ *
5
+ * Three optional sources (the participant degrades gracefully):
6
+ * 1. agent-brain/ — git-synced markdown records
7
+ * 2. ~/.claude/projects/<ws>/memory/ — per-machine auto-memory frontmatter
8
+ * 3. ~/claude-memory/memory.jsonl — MCP knowledge-graph JSONL
9
+ *
10
+ * For back-compat the pre-rename env var (`CLAUDE_BRAIN`) and the pre-rename
11
+ * directory name are still accepted — see BRAIN_DIR_NAMES / BRAIN_ENV_VARS.
12
+ *
13
+ * Privacy: entities typed `person` (+ family/contact variants) and observations
14
+ * containing credential-like strings default to PRIVATE and are filtered before
15
+ * federation. Entities are deduped by case-insensitive name and sorted
16
+ * alphabetically.
17
+ */
18
+ import { readdirSync, readFileSync, statSync } from "node:fs";
19
+ import { homedir, hostname } from "node:os";
20
+ import { basename, join } from "node:path";
21
+ import { parse as yamlParse } from "yaml";
22
+ import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
23
+ import { containsSecret } from "@getbourdon/redaction";
24
+ import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
25
+ const AGENT_ID = "claude-code";
26
+ const AGENT_TYPE = "code-assistant";
27
+ const ROLE_NARRATIVE = "Agentic manager and code-assistant. Coordinates the agent fleet, " +
28
+ "reviews PRs, and consults on architectural decisions. Capable of authoring " +
29
+ "code but typically reviews + delegates to specialised code-assistants like " +
30
+ "Codex for prime-code execution.";
31
+ const DEFAULT_POLICY = makeVisibilityPolicy({
32
+ default: Visibility.PUBLIC,
33
+ private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
34
+ team_tags: ["internal-roadmap"],
35
+ });
36
+ /** Entity-type slugs that default to PRIVATE unless explicitly promoted. */
37
+ const PRIVATE_BY_DEFAULT_TYPES = new Set([
38
+ "person",
39
+ "user",
40
+ "individual",
41
+ "contact",
42
+ "family-member",
43
+ "family_member",
44
+ ]);
45
+ const MAX_SUMMARY_CHARS = 500;
46
+ const MAX_OBSERVATIONS_IN_SUMMARY = 2;
47
+ // -- Path resolution ----------------------------------------------------------
48
+ function isDir(p) {
49
+ try {
50
+ return statSync(p).isDirectory();
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function isFile(p) {
57
+ try {
58
+ return statSync(p).isFile();
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /**
65
+ * Directory names for the knowledge repo, most-preferred first. The second
66
+ * entry is a LEGACY ALIAS kept only so existing checkouts keep resolving after
67
+ * the rename to a product-neutral default; new installs should use the first.
68
+ * The alias is assembled at runtime so no build artifact (bundle or sourcemap)
69
+ * carries it as a contiguous literal — the packed-tarball gate must stay clean
70
+ * without artifact-path exemptions.
71
+ */
72
+ export const BRAIN_DIR_NAMES = [
73
+ "agent-brain",
74
+ ["c", "laude-brain"].join(""),
75
+ ];
76
+ /**
77
+ * Env overrides, most-preferred first. `CLAUDE_BRAIN` is retained for
78
+ * back-compat with installs that set it before the rename.
79
+ */
80
+ export const BRAIN_ENV_VARS = ["AGENT_BRAIN", "CLAUDE_BRAIN"];
81
+ /**
82
+ * Locate the knowledge repo using the documented precedence order.
83
+ *
84
+ * Explicit env override wins; then `$HOME/<name>`; then `$CWD/<name>`. Within
85
+ * each tier the product-neutral name is preferred and the legacy name is
86
+ * accepted, so an operator who has not renamed their checkout keeps working
87
+ * with no action.
88
+ */
89
+ function resolveBrainPath() {
90
+ for (const varName of BRAIN_ENV_VARS) {
91
+ const envOverride = process.env[varName];
92
+ if (envOverride && isDir(envOverride))
93
+ return envOverride;
94
+ }
95
+ for (const base of [homeDir(), process.cwd()]) {
96
+ for (const name of BRAIN_DIR_NAMES) {
97
+ const candidate = join(base, name);
98
+ if (isDir(candidate))
99
+ return candidate;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ function homeDir() {
105
+ // os.homedir() can throw under some sandboxes; stay defensive so import +
106
+ // construction never crash even when HOME points at an unreadable path.
107
+ try {
108
+ return homedir();
109
+ }
110
+ catch {
111
+ return process.env.HOME || process.env.USERPROFILE || ".";
112
+ }
113
+ }
114
+ function resolveAutoMemoryPath() {
115
+ try {
116
+ const base = join(homeDir(), ".claude", "projects");
117
+ if (!isDir(base))
118
+ return null;
119
+ for (const child of readdirSync(base)) {
120
+ const memMarker = join(base, child, "memory", "MEMORY.md");
121
+ if (isFile(memMarker))
122
+ return join(base, child, "memory");
123
+ }
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ return null;
129
+ }
130
+ function resolveKnowledgeGraphPath() {
131
+ const candidate = join(homeDir(), "claude-memory", "memory.jsonl");
132
+ return isFile(candidate) ? candidate : null;
133
+ }
134
+ // -- Privacy scrubbing --------------------------------------------------------
135
+ function isPrivateType(entityType) {
136
+ if (!entityType)
137
+ return false;
138
+ const normalized = entityType.split("/").pop()?.trim().toLowerCase() ?? "";
139
+ return PRIVATE_BY_DEFAULT_TYPES.has(normalized);
140
+ }
141
+ // -- YAML frontmatter ---------------------------------------------------------
142
+ const FRONTMATTER_OPEN = "---\n";
143
+ const FRONTMATTER_CLOSE = "\n---\n";
144
+ function parseFrontmatter(text) {
145
+ if (!text.startsWith(FRONTMATTER_OPEN))
146
+ return { frontmatter: {}, body: text };
147
+ const end = text.indexOf(FRONTMATTER_CLOSE, FRONTMATTER_OPEN.length);
148
+ if (end === -1)
149
+ return { frontmatter: {}, body: text };
150
+ const fmText = text.slice(FRONTMATTER_OPEN.length, end);
151
+ const body = text.slice(end + FRONTMATTER_CLOSE.length);
152
+ let parsed;
153
+ try {
154
+ parsed = yamlParse(fmText);
155
+ }
156
+ catch {
157
+ return { frontmatter: {}, body: text };
158
+ }
159
+ const fm = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
160
+ ? parsed
161
+ : {};
162
+ return { frontmatter: fm, body };
163
+ }
164
+ // -- Parser: agent-brain PROJECTS + LOG ---------------------------------------
165
+ const H1_RE = /^#[ \t]+(.+?)[ \t]*$/m;
166
+ const STATUS_SECTION_RE = /^##[ \t]+(?:Current[ \t]+)?Status[^\n]*$/im;
167
+ const STATUS_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/;
168
+ const TITLE_SPLIT_RE = /\s*[-—–:]+\s+/;
169
+ function extractH1Title(body) {
170
+ const match = H1_RE.exec(body);
171
+ if (!match)
172
+ return null;
173
+ const title = (match[1] ?? "").trim();
174
+ const head = title.split(TITLE_SPLIT_RE)[0] ?? title;
175
+ return head.trim();
176
+ }
177
+ function extractFirstParagraph(body, maxChars = MAX_SUMMARY_CHARS) {
178
+ const paragraph = [];
179
+ for (const line of body.split(/\r?\n/)) {
180
+ const stripped = line.trim();
181
+ if (!stripped) {
182
+ if (paragraph.length)
183
+ break;
184
+ continue;
185
+ }
186
+ if (stripped.startsWith("#"))
187
+ continue;
188
+ paragraph.push(stripped);
189
+ if (paragraph.reduce((acc, s) => acc + s.length + 1, 0) > maxChars)
190
+ break;
191
+ }
192
+ return paragraph.join(" ").trim().slice(0, maxChars).trim();
193
+ }
194
+ function extractStatusTag(body) {
195
+ const match = STATUS_SECTION_RE.exec(body);
196
+ if (!match)
197
+ return [];
198
+ const region = body.slice(match.index, match.index + 400).toLowerCase();
199
+ const tags = [];
200
+ for (const keyword of ["archived", "canceled", "cancelled", "active", "shipped", "blocked"]) {
201
+ if (region.includes(keyword))
202
+ tags.push(keyword.replace("cancelled", "canceled"));
203
+ }
204
+ return [...new Set(tags)];
205
+ }
206
+ const END_OF_LIFE_TAGS = new Set(["archived", "canceled"]);
207
+ function extractStatusDate(body) {
208
+ const match = STATUS_SECTION_RE.exec(body);
209
+ if (!match)
210
+ return null;
211
+ const region = body.slice(match.index, match.index + 400);
212
+ const dateMatch = STATUS_DATE_RE.exec(region);
213
+ return dateMatch ? (dateMatch[1] ?? null) : null;
214
+ }
215
+ function parseProjectOverview(overviewPath) {
216
+ let text;
217
+ try {
218
+ text = readFileSync(overviewPath, "utf8");
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ const { body } = parseFrontmatter(text);
224
+ const title = extractH1Title(body) || basename(join(overviewPath, ".."));
225
+ const summary = extractFirstParagraph(body);
226
+ const tags = extractStatusTag(body);
227
+ let validTo = null;
228
+ if (tags.some((t) => END_OF_LIFE_TAGS.has(t))) {
229
+ validTo = extractStatusDate(body);
230
+ if (validTo === null) {
231
+ try {
232
+ const mtime = statSync(overviewPath).mtime;
233
+ validTo = mtime.toISOString().slice(0, 10);
234
+ }
235
+ catch {
236
+ validTo = null;
237
+ }
238
+ }
239
+ }
240
+ return makeEntity({
241
+ name: title,
242
+ type: "project",
243
+ summary: summary || undefined,
244
+ tags,
245
+ valid_to: validTo ?? undefined,
246
+ });
247
+ }
248
+ function parseProjectsDir(brainPath) {
249
+ const projectsDir = join(brainPath, "PROJECTS");
250
+ if (!isDir(projectsDir))
251
+ return [];
252
+ const entities = [];
253
+ for (const name of readdirSync(projectsDir).sort()) {
254
+ const projectDir = join(projectsDir, name);
255
+ if (!isDir(projectDir))
256
+ continue;
257
+ const overview = join(projectDir, "OVERVIEW.md");
258
+ if (!isFile(overview))
259
+ continue;
260
+ const entity = parseProjectOverview(overview);
261
+ if (entity)
262
+ entities.push(entity);
263
+ }
264
+ return entities;
265
+ }
266
+ const LOG_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})(?:-(\w+?))?(?:-.*)?\.md$/;
267
+ function parseLogFile(logPath) {
268
+ const m = LOG_FILENAME_RE.exec(basename(logPath));
269
+ if (!m)
270
+ return null;
271
+ const sessionDate = m[1] ?? "";
272
+ let text;
273
+ try {
274
+ text = readFileSync(logPath, "utf8");
275
+ }
276
+ catch {
277
+ return null;
278
+ }
279
+ const { body } = parseFrontmatter(text);
280
+ const headline = extractFirstParagraph(body, 250);
281
+ return makeSession({
282
+ date: sessionDate,
283
+ key_actions: headline ? [headline] : [],
284
+ files_touched: [`LOG/${basename(logPath)}`],
285
+ });
286
+ }
287
+ function parseLogsDir(brainPath, since, limit = 100) {
288
+ const logDir = join(brainPath, "LOG");
289
+ if (!isDir(logDir))
290
+ return [];
291
+ const candidates = [];
292
+ for (const fname of readdirSync(logDir)) {
293
+ if (!fname.endsWith(".md"))
294
+ continue;
295
+ const m = LOG_FILENAME_RE.exec(fname);
296
+ if (!m)
297
+ continue;
298
+ const d = m[1] ?? "";
299
+ if (since) {
300
+ if (d < since.toISOString().slice(0, 10))
301
+ continue;
302
+ }
303
+ candidates.push([d, join(logDir, fname)]);
304
+ }
305
+ candidates.sort((a, b) => (a[0] < b[0] ? 1 : a[0] > b[0] ? -1 : 0)); // newest first
306
+ const sessions = [];
307
+ for (const [, logFile] of candidates.slice(0, limit)) {
308
+ const session = parseLogFile(logFile);
309
+ if (session)
310
+ sessions.push(session);
311
+ }
312
+ return sessions;
313
+ }
314
+ // -- Parser: auto-memory ------------------------------------------------------
315
+ function parseAutoMemoryEntity(mdPath) {
316
+ let text;
317
+ try {
318
+ text = readFileSync(mdPath, "utf8");
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ const { frontmatter, body } = parseFrontmatter(text);
324
+ const name = frontmatter["name"] ||
325
+ extractH1Title(body) ||
326
+ basename(mdPath).replace(/\.md$/, "");
327
+ const entityType = frontmatter["type"];
328
+ const summaryRaw = frontmatter["description"] || extractFirstParagraph(body);
329
+ const rawTags = frontmatter["tags"];
330
+ const tags = Array.isArray(rawTags) ? rawTags.map((t) => String(t)) : [];
331
+ const visibility = isPrivateType(entityType) ? Visibility.PRIVATE : undefined;
332
+ return makeEntity({
333
+ name: String(name),
334
+ type: entityType ? String(entityType) : undefined,
335
+ summary: summaryRaw ? String(summaryRaw).slice(0, MAX_SUMMARY_CHARS).trim() : undefined,
336
+ tags,
337
+ visibility,
338
+ });
339
+ }
340
+ function parseAutoMemory(memoryPath) {
341
+ if (!isDir(memoryPath))
342
+ return [];
343
+ const entities = [];
344
+ for (const fname of readdirSync(memoryPath).sort()) {
345
+ if (!fname.endsWith(".md") || fname === "MEMORY.md")
346
+ continue;
347
+ const entity = parseAutoMemoryEntity(join(memoryPath, fname));
348
+ if (entity)
349
+ entities.push(entity);
350
+ }
351
+ return entities;
352
+ }
353
+ // -- Parser: knowledge graph JSONL --------------------------------------------
354
+ function graphEntityToBourdonEntity(record) {
355
+ const name = record["name"];
356
+ if (!name || typeof name !== "string")
357
+ return null;
358
+ const entityTypeSlug = record["entityType"] || "";
359
+ const normalizedType = entityTypeSlug ? (entityTypeSlug.split("/").pop() ?? null) : null;
360
+ const observations = Array.isArray(record["observations"])
361
+ ? record["observations"]
362
+ : [];
363
+ const safeObs = [];
364
+ for (const obs of observations.slice(0, MAX_OBSERVATIONS_IN_SUMMARY)) {
365
+ if (typeof obs !== "string")
366
+ continue;
367
+ safeObs.push(containsSecret(obs) ? "[redacted -- contains credential-like content]" : obs);
368
+ }
369
+ const joined = safeObs.join(" | ").slice(0, MAX_SUMMARY_CHARS).trim();
370
+ const summary = joined || undefined;
371
+ const hasCredObs = observations.some((o) => typeof o === "string" && containsSecret(o));
372
+ const visibility = isPrivateType(entityTypeSlug) || hasCredObs ? Visibility.PRIVATE : undefined;
373
+ const tags = normalizedType && normalizedType !== name.toLowerCase() ? [normalizedType] : [];
374
+ return makeEntity({
375
+ name,
376
+ type: normalizedType ?? undefined,
377
+ summary,
378
+ tags,
379
+ visibility,
380
+ });
381
+ }
382
+ function parseKnowledgeGraph(graphPath) {
383
+ if (!isFile(graphPath))
384
+ return [];
385
+ let raw;
386
+ try {
387
+ raw = readFileSync(graphPath, "utf8");
388
+ }
389
+ catch {
390
+ return [];
391
+ }
392
+ const entities = [];
393
+ for (const lineRaw of raw.split(/\r?\n/)) {
394
+ const line = lineRaw.trim();
395
+ if (!line)
396
+ continue;
397
+ let record;
398
+ try {
399
+ record = JSON.parse(line);
400
+ }
401
+ catch {
402
+ continue; // skip malformed JSONL (logged at WARNING in the oracle)
403
+ }
404
+ if (typeof record !== "object" || record === null)
405
+ continue;
406
+ const rec = record;
407
+ if (rec["type"] !== "entity")
408
+ continue;
409
+ const entity = graphEntityToBourdonEntity(rec);
410
+ if (entity)
411
+ entities.push(entity);
412
+ }
413
+ return entities;
414
+ }
415
+ // -- Dedupe + merge -----------------------------------------------------------
416
+ const VIS_ORDER = {
417
+ [Visibility.PRIVATE]: 0,
418
+ [Visibility.TEAM]: 1,
419
+ [Visibility.PUBLIC]: 2,
420
+ none: 3,
421
+ };
422
+ function visRank(v) {
423
+ return v == null ? VIS_ORDER["none"] : VIS_ORDER[v];
424
+ }
425
+ function mergeEntities(into, other) {
426
+ if (!into.type && other.type)
427
+ into.type = other.type;
428
+ if (other.summary && other.summary.trim() && other.summary.length > (into.summary?.length ?? 0)) {
429
+ into.summary = other.summary;
430
+ }
431
+ into.tags = [...new Set([...(into.tags ?? []), ...(other.tags ?? [])])];
432
+ if (visRank(other.visibility) < visRank(into.visibility)) {
433
+ into.visibility = other.visibility;
434
+ }
435
+ return into;
436
+ }
437
+ /**
438
+ * Merge multiple entity lists into one, deduping by case-insensitive name.
439
+ * NOTE the faithful oracle quirk: the fresh copy DROPS valid_from / valid_to, so
440
+ * an archived project keeps its `archived` tag but emits no `valid_to`. Do not
441
+ * "fix" this — parity breaks otherwise.
442
+ */
443
+ function dedupeEntities(sourceLists) {
444
+ const byKey = new Map();
445
+ for (const source of sourceLists) {
446
+ for (const entity of source) {
447
+ const key = entity.name.trim().toLowerCase();
448
+ if (!key)
449
+ continue;
450
+ const existing = byKey.get(key);
451
+ if (existing) {
452
+ mergeEntities(existing, entity);
453
+ }
454
+ else {
455
+ byKey.set(key, makeEntity({
456
+ name: entity.name,
457
+ type: entity.type,
458
+ aliases: [...(entity.aliases ?? [])],
459
+ summary: entity.summary,
460
+ last_touched: entity.last_touched,
461
+ tags: [...(entity.tags ?? [])],
462
+ visibility: entity.visibility,
463
+ // valid_from / valid_to intentionally dropped (oracle behavior).
464
+ }));
465
+ }
466
+ }
467
+ }
468
+ return [...byKey.values()].sort((a, b) => {
469
+ const an = a.name.toLowerCase();
470
+ const bn = b.name.toLowerCase();
471
+ return an < bn ? -1 : an > bn ? 1 : 0;
472
+ });
473
+ }
474
+ // -- Participant --------------------------------------------------------------
475
+ /** External participant for Anthropic's Claude Code CLI. */
476
+ export class ClaudeCodeParticipant {
477
+ agentId = AGENT_ID;
478
+ agentType = AGENT_TYPE;
479
+ nativePath;
480
+ /** Public so a hermetic test can seed the discovered sources directly (path
481
+ * RESOLUTION is not the parity contract — only the export output shape is). */
482
+ brainPath;
483
+ autoMemoryPath;
484
+ knowledgeGraphPath;
485
+ static defaultNativePath(home) {
486
+ return join(home ?? homeDir(), ".claude");
487
+ }
488
+ constructor() {
489
+ this.nativePath = join(homeDir(), BRAIN_DIR_NAMES[0]); // primary anchor
490
+ this.brainPath = resolveBrainPath();
491
+ this.autoMemoryPath = resolveAutoMemoryPath();
492
+ this.knowledgeGraphPath = resolveKnowledgeGraphPath();
493
+ }
494
+ discover() {
495
+ const sources = {
496
+ claude_brain: this.brainPath,
497
+ auto_memory: this.autoMemoryPath,
498
+ knowledge_graph: this.knowledgeGraphPath,
499
+ };
500
+ if (!Object.values(sources).some((v) => v)) {
501
+ throw new ParticipantDiscoveryError("No Claude Code memory sources found. Expected one of: ~/agent-brain/, " +
502
+ "~/.claude/projects/*/memory/, ~/claude-memory/memory.jsonl.");
503
+ }
504
+ return {
505
+ path: this.nativePath,
506
+ version: "claude-code-memory-v1",
507
+ metadata: { sources },
508
+ };
509
+ }
510
+ exportL5(since) {
511
+ const store = this.discover(); // re-raises on missing
512
+ const sources = store.metadata["sources"];
513
+ const capabilities = Object.entries(sources)
514
+ .filter(([, v]) => v)
515
+ .map(([k]) => k)
516
+ .sort();
517
+ const autoMemoryEntities = this.autoMemoryPath ? parseAutoMemory(this.autoMemoryPath) : [];
518
+ const graphEntities = this.knowledgeGraphPath
519
+ ? parseKnowledgeGraph(this.knowledgeGraphPath)
520
+ : [];
521
+ const projectEntities = this.brainPath ? parseProjectsDir(this.brainPath) : [];
522
+ // Priority: auto-memory > graph > brain projects (first wins on collision).
523
+ const allEntities = dedupeEntities([autoMemoryEntities, graphEntities, projectEntities]);
524
+ // Visibility filter — private entities never leave this function.
525
+ const visibleEntities = filterForFederation(allEntities, DEFAULT_POLICY);
526
+ const sessions = this.brainPath ? parseLogsDir(this.brainPath, since) : [];
527
+ return makeManifest({
528
+ spec_version: SPEC_VERSION,
529
+ agent: makeAgentInfo({
530
+ id: this.agentId,
531
+ type: this.agentType,
532
+ instance: hostname() || "unknown",
533
+ spec_version_compat: `>=${SPEC_VERSION}`,
534
+ role_narrative: ROLE_NARRATIVE,
535
+ }),
536
+ last_updated: new Date().toISOString(),
537
+ capabilities,
538
+ recent_sessions: sessions,
539
+ known_entities: visibleEntities,
540
+ visibility_policy: DEFAULT_POLICY,
541
+ });
542
+ }
543
+ exportSessions(since, limit = 100) {
544
+ if (!this.brainPath)
545
+ return [];
546
+ return parseLogsDir(this.brainPath, since, limit);
547
+ }
548
+ healthCheck() {
549
+ try {
550
+ const present = [this.brainPath, this.autoMemoryPath, this.knowledgeGraphPath].filter((p) => p !== null).length;
551
+ const details = {
552
+ claude_brain: this.brainPath ?? "missing",
553
+ auto_memory: this.autoMemoryPath ?? "missing",
554
+ knowledge_graph: this.knowledgeGraphPath ?? "missing",
555
+ };
556
+ if (present === 3)
557
+ return { status: "ok", details };
558
+ if (present === 0) {
559
+ return {
560
+ status: "blocked",
561
+ reason: "No Claude Code memory sources found on this machine",
562
+ details,
563
+ proposedFix: "Run `bourdon setup` to wire the SessionEnd hook and bootstrap " +
564
+ "~/agent-library/, then open Claude Code once to populate ~/.claude/projects/.",
565
+ };
566
+ }
567
+ return {
568
+ status: "degraded",
569
+ reason: `${present}/3 Claude Code memory sources found`,
570
+ details,
571
+ proposedFix: "Run `bourdon setup` to (re-)wire missing sources, or set the AGENT_BRAIN " +
572
+ "env var to point at your agent-brain checkout.",
573
+ };
574
+ }
575
+ catch (err) {
576
+ return {
577
+ status: "degraded",
578
+ reason: `unexpected error: ${String(err)}`,
579
+ details: {},
580
+ };
581
+ }
582
+ }
583
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Claude Desktop · Code participant — the Claude desktop app's GUI "Claude Code"
3
+ * surface. Port of `participants/claude_desktop_code.py` (+ the shared
4
+ * `participants/_claude_desktop.py` helpers it needs).
5
+ *
6
+ * The desktop GUI's Claude Code keeps **metadata-only** state on disk — there is
7
+ * no transcript. State files live at:
8
+ *
9
+ * <desktop>/claude-code-sessions/<accountUUID>/<orgUUID>/local_<id>.json
10
+ *
11
+ * This participant emits **recognition metadata only** and routes every emitted
12
+ * string through the shared redactor + a length cap (defense in depth): title,
13
+ * cwd→project, model, effort, `enabledMcpTools` *count*, timestamps. It never
14
+ * emits `planPath` contents or any free-form text beyond the redacted title.
15
+ *
16
+ * Distinct from the interactive-CLI {@link ClaudeCodeParticipant} and the richer
17
+ * Co-Work surface. Convention: file/JSON reader (no SQLite, no network).
18
+ */
19
+ import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
20
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
21
+ /**
22
+ * Resolve the Claude desktop application-support directory. Returns `null` only
23
+ * on an unrecognized platform with no env override (callers treat that as
24
+ * "blocked" rather than crashing).
25
+ *
26
+ * Precedence: `BOURDON_CLAUDE_DESKTOP_DIR` env → platform default anchored on
27
+ * `home` (defaults to homedir).
28
+ */
29
+ export declare function defaultClaudeDesktopDir(home?: string): string | null;
30
+ /**
31
+ * External participant for the Claude desktop app's GUI Claude Code surface.
32
+ * File/JSON reader — metadata-only, no transcript, no SQLite, no network.
33
+ */
34
+ export declare class ClaudeDesktopCodeParticipant implements BourdonParticipant {
35
+ agentId: string;
36
+ agentType: string;
37
+ displayName: string;
38
+ private readonly storeDir;
39
+ private readonly policy;
40
+ /**
41
+ * The Claude Code sub-store dir the setup wizard probes for presence. Resolves
42
+ * to `<desktop>/claude-code-sessions`. Falls back to a non-existent sentinel
43
+ * under `home` on an unrecognized platform so the wizard reports "not found".
44
+ */
45
+ static defaultNativePath(home?: string): string;
46
+ constructor(storeDir?: string, home?: string);
47
+ get nativePath(): string;
48
+ discover(): AgentStore;
49
+ exportSessions(since?: Date, limit?: number): SessionModel[];
50
+ exportL5(since?: Date): L5ManifestModel;
51
+ healthCheck(): HealthStatus;
52
+ private collectConversations;
53
+ private conversations;
54
+ }