@getbourdon/participants 0.1.1 → 0.2.1

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.
@@ -1,552 +0,0 @@
1
- /**
2
- * Claude Code participant — reads claude-brain + auto-memory + MCP knowledge
3
- * graph. Port of `participants/claude_code.py`.
4
- *
5
- * Three optional sources (the participant degrades gracefully):
6
- * 1. claude-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
- * Privacy: entities typed `person` (+ family/contact variants) and observations
11
- * containing credential-like strings default to PRIVATE and are filtered before
12
- * federation. Entities are deduped by case-insensitive name and sorted
13
- * alphabetically.
14
- */
15
- import { readdirSync, readFileSync, statSync } from "node:fs";
16
- import { homedir, hostname } from "node:os";
17
- import { basename, join } from "node:path";
18
- import { parse as yamlParse } from "yaml";
19
- import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
20
- import { containsSecret } from "@getbourdon/redaction";
21
- import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
22
- const AGENT_ID = "claude-code";
23
- const AGENT_TYPE = "code-assistant";
24
- const ROLE_NARRATIVE = "Agentic manager and code-assistant. Coordinates the RADLAB agent fleet, " +
25
- "reviews PRs, and consults on architectural decisions. Capable of authoring " +
26
- "code but typically reviews + delegates to specialised code-assistants like " +
27
- "Codex for prime-code execution.";
28
- const DEFAULT_POLICY = makeVisibilityPolicy({
29
- default: Visibility.PUBLIC,
30
- private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
31
- team_tags: ["internal-roadmap"],
32
- });
33
- /** Entity-type slugs that default to PRIVATE unless explicitly promoted. */
34
- const PRIVATE_BY_DEFAULT_TYPES = new Set([
35
- "person",
36
- "user",
37
- "individual",
38
- "contact",
39
- "family-member",
40
- "family_member",
41
- ]);
42
- const MAX_SUMMARY_CHARS = 500;
43
- const MAX_OBSERVATIONS_IN_SUMMARY = 2;
44
- // -- Path resolution ----------------------------------------------------------
45
- function isDir(p) {
46
- try {
47
- return statSync(p).isDirectory();
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
- function isFile(p) {
54
- try {
55
- return statSync(p).isFile();
56
- }
57
- catch {
58
- return false;
59
- }
60
- }
61
- function resolveClaudeBrainPath() {
62
- const envOverride = process.env.CLAUDE_BRAIN;
63
- if (envOverride && isDir(envOverride))
64
- return envOverride;
65
- const homeCandidate = join(homeDir(), "claude-brain");
66
- if (isDir(homeCandidate))
67
- return homeCandidate;
68
- const cwdCandidate = join(process.cwd(), "claude-brain");
69
- if (isDir(cwdCandidate))
70
- return cwdCandidate;
71
- return null;
72
- }
73
- function homeDir() {
74
- // os.homedir() can throw under some sandboxes; stay defensive so import +
75
- // construction never crash even when HOME points at an unreadable path.
76
- try {
77
- return homedir();
78
- }
79
- catch {
80
- return process.env.HOME || process.env.USERPROFILE || ".";
81
- }
82
- }
83
- function resolveAutoMemoryPath() {
84
- try {
85
- const base = join(homeDir(), ".claude", "projects");
86
- if (!isDir(base))
87
- return null;
88
- for (const child of readdirSync(base)) {
89
- const memMarker = join(base, child, "memory", "MEMORY.md");
90
- if (isFile(memMarker))
91
- return join(base, child, "memory");
92
- }
93
- }
94
- catch {
95
- return null;
96
- }
97
- return null;
98
- }
99
- function resolveKnowledgeGraphPath() {
100
- const candidate = join(homeDir(), "claude-memory", "memory.jsonl");
101
- return isFile(candidate) ? candidate : null;
102
- }
103
- // -- Privacy scrubbing --------------------------------------------------------
104
- function isPrivateType(entityType) {
105
- if (!entityType)
106
- return false;
107
- const normalized = entityType.split("/").pop()?.trim().toLowerCase() ?? "";
108
- return PRIVATE_BY_DEFAULT_TYPES.has(normalized);
109
- }
110
- // -- YAML frontmatter ---------------------------------------------------------
111
- const FRONTMATTER_OPEN = "---\n";
112
- const FRONTMATTER_CLOSE = "\n---\n";
113
- function parseFrontmatter(text) {
114
- if (!text.startsWith(FRONTMATTER_OPEN))
115
- return { frontmatter: {}, body: text };
116
- const end = text.indexOf(FRONTMATTER_CLOSE, FRONTMATTER_OPEN.length);
117
- if (end === -1)
118
- return { frontmatter: {}, body: text };
119
- const fmText = text.slice(FRONTMATTER_OPEN.length, end);
120
- const body = text.slice(end + FRONTMATTER_CLOSE.length);
121
- let parsed;
122
- try {
123
- parsed = yamlParse(fmText);
124
- }
125
- catch {
126
- return { frontmatter: {}, body: text };
127
- }
128
- const fm = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
129
- ? parsed
130
- : {};
131
- return { frontmatter: fm, body };
132
- }
133
- // -- Parser: claude-brain PROJECTS + LOG --------------------------------------
134
- const H1_RE = /^#[ \t]+(.+?)[ \t]*$/m;
135
- const STATUS_SECTION_RE = /^##[ \t]+(?:Current[ \t]+)?Status[^\n]*$/im;
136
- const STATUS_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/;
137
- const TITLE_SPLIT_RE = /\s*[-—–:]+\s+/;
138
- function extractH1Title(body) {
139
- const match = H1_RE.exec(body);
140
- if (!match)
141
- return null;
142
- const title = (match[1] ?? "").trim();
143
- const head = title.split(TITLE_SPLIT_RE)[0] ?? title;
144
- return head.trim();
145
- }
146
- function extractFirstParagraph(body, maxChars = MAX_SUMMARY_CHARS) {
147
- const paragraph = [];
148
- for (const line of body.split(/\r?\n/)) {
149
- const stripped = line.trim();
150
- if (!stripped) {
151
- if (paragraph.length)
152
- break;
153
- continue;
154
- }
155
- if (stripped.startsWith("#"))
156
- continue;
157
- paragraph.push(stripped);
158
- if (paragraph.reduce((acc, s) => acc + s.length + 1, 0) > maxChars)
159
- break;
160
- }
161
- return paragraph.join(" ").trim().slice(0, maxChars).trim();
162
- }
163
- function extractStatusTag(body) {
164
- const match = STATUS_SECTION_RE.exec(body);
165
- if (!match)
166
- return [];
167
- const region = body.slice(match.index, match.index + 400).toLowerCase();
168
- const tags = [];
169
- for (const keyword of ["archived", "canceled", "cancelled", "active", "shipped", "blocked"]) {
170
- if (region.includes(keyword))
171
- tags.push(keyword.replace("cancelled", "canceled"));
172
- }
173
- return [...new Set(tags)];
174
- }
175
- const END_OF_LIFE_TAGS = new Set(["archived", "canceled"]);
176
- function extractStatusDate(body) {
177
- const match = STATUS_SECTION_RE.exec(body);
178
- if (!match)
179
- return null;
180
- const region = body.slice(match.index, match.index + 400);
181
- const dateMatch = STATUS_DATE_RE.exec(region);
182
- return dateMatch ? (dateMatch[1] ?? null) : null;
183
- }
184
- function parseProjectOverview(overviewPath) {
185
- let text;
186
- try {
187
- text = readFileSync(overviewPath, "utf8");
188
- }
189
- catch {
190
- return null;
191
- }
192
- const { body } = parseFrontmatter(text);
193
- const title = extractH1Title(body) || basename(join(overviewPath, ".."));
194
- const summary = extractFirstParagraph(body);
195
- const tags = extractStatusTag(body);
196
- let validTo = null;
197
- if (tags.some((t) => END_OF_LIFE_TAGS.has(t))) {
198
- validTo = extractStatusDate(body);
199
- if (validTo === null) {
200
- try {
201
- const mtime = statSync(overviewPath).mtime;
202
- validTo = mtime.toISOString().slice(0, 10);
203
- }
204
- catch {
205
- validTo = null;
206
- }
207
- }
208
- }
209
- return makeEntity({
210
- name: title,
211
- type: "project",
212
- summary: summary || undefined,
213
- tags,
214
- valid_to: validTo ?? undefined,
215
- });
216
- }
217
- function parseProjectsDir(brainPath) {
218
- const projectsDir = join(brainPath, "PROJECTS");
219
- if (!isDir(projectsDir))
220
- return [];
221
- const entities = [];
222
- for (const name of readdirSync(projectsDir).sort()) {
223
- const projectDir = join(projectsDir, name);
224
- if (!isDir(projectDir))
225
- continue;
226
- const overview = join(projectDir, "OVERVIEW.md");
227
- if (!isFile(overview))
228
- continue;
229
- const entity = parseProjectOverview(overview);
230
- if (entity)
231
- entities.push(entity);
232
- }
233
- return entities;
234
- }
235
- const LOG_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})(?:-(\w+?))?(?:-.*)?\.md$/;
236
- function parseLogFile(logPath) {
237
- const m = LOG_FILENAME_RE.exec(basename(logPath));
238
- if (!m)
239
- return null;
240
- const sessionDate = m[1] ?? "";
241
- let text;
242
- try {
243
- text = readFileSync(logPath, "utf8");
244
- }
245
- catch {
246
- return null;
247
- }
248
- const { body } = parseFrontmatter(text);
249
- const headline = extractFirstParagraph(body, 250);
250
- return makeSession({
251
- date: sessionDate,
252
- key_actions: headline ? [headline] : [],
253
- files_touched: [`LOG/${basename(logPath)}`],
254
- });
255
- }
256
- function parseLogsDir(brainPath, since, limit = 100) {
257
- const logDir = join(brainPath, "LOG");
258
- if (!isDir(logDir))
259
- return [];
260
- const candidates = [];
261
- for (const fname of readdirSync(logDir)) {
262
- if (!fname.endsWith(".md"))
263
- continue;
264
- const m = LOG_FILENAME_RE.exec(fname);
265
- if (!m)
266
- continue;
267
- const d = m[1] ?? "";
268
- if (since) {
269
- if (d < since.toISOString().slice(0, 10))
270
- continue;
271
- }
272
- candidates.push([d, join(logDir, fname)]);
273
- }
274
- candidates.sort((a, b) => (a[0] < b[0] ? 1 : a[0] > b[0] ? -1 : 0)); // newest first
275
- const sessions = [];
276
- for (const [, logFile] of candidates.slice(0, limit)) {
277
- const session = parseLogFile(logFile);
278
- if (session)
279
- sessions.push(session);
280
- }
281
- return sessions;
282
- }
283
- // -- Parser: auto-memory ------------------------------------------------------
284
- function parseAutoMemoryEntity(mdPath) {
285
- let text;
286
- try {
287
- text = readFileSync(mdPath, "utf8");
288
- }
289
- catch {
290
- return null;
291
- }
292
- const { frontmatter, body } = parseFrontmatter(text);
293
- const name = frontmatter["name"] ||
294
- extractH1Title(body) ||
295
- basename(mdPath).replace(/\.md$/, "");
296
- const entityType = frontmatter["type"];
297
- const summaryRaw = frontmatter["description"] || extractFirstParagraph(body);
298
- const rawTags = frontmatter["tags"];
299
- const tags = Array.isArray(rawTags) ? rawTags.map((t) => String(t)) : [];
300
- const visibility = isPrivateType(entityType) ? Visibility.PRIVATE : undefined;
301
- return makeEntity({
302
- name: String(name),
303
- type: entityType ? String(entityType) : undefined,
304
- summary: summaryRaw ? String(summaryRaw).slice(0, MAX_SUMMARY_CHARS).trim() : undefined,
305
- tags,
306
- visibility,
307
- });
308
- }
309
- function parseAutoMemory(memoryPath) {
310
- if (!isDir(memoryPath))
311
- return [];
312
- const entities = [];
313
- for (const fname of readdirSync(memoryPath).sort()) {
314
- if (!fname.endsWith(".md") || fname === "MEMORY.md")
315
- continue;
316
- const entity = parseAutoMemoryEntity(join(memoryPath, fname));
317
- if (entity)
318
- entities.push(entity);
319
- }
320
- return entities;
321
- }
322
- // -- Parser: knowledge graph JSONL --------------------------------------------
323
- function graphEntityToBourdonEntity(record) {
324
- const name = record["name"];
325
- if (!name || typeof name !== "string")
326
- return null;
327
- const entityTypeSlug = record["entityType"] || "";
328
- const normalizedType = entityTypeSlug ? (entityTypeSlug.split("/").pop() ?? null) : null;
329
- const observations = Array.isArray(record["observations"])
330
- ? record["observations"]
331
- : [];
332
- const safeObs = [];
333
- for (const obs of observations.slice(0, MAX_OBSERVATIONS_IN_SUMMARY)) {
334
- if (typeof obs !== "string")
335
- continue;
336
- safeObs.push(containsSecret(obs) ? "[redacted -- contains credential-like content]" : obs);
337
- }
338
- const joined = safeObs.join(" | ").slice(0, MAX_SUMMARY_CHARS).trim();
339
- const summary = joined || undefined;
340
- const hasCredObs = observations.some((o) => typeof o === "string" && containsSecret(o));
341
- const visibility = isPrivateType(entityTypeSlug) || hasCredObs ? Visibility.PRIVATE : undefined;
342
- const tags = normalizedType && normalizedType !== name.toLowerCase() ? [normalizedType] : [];
343
- return makeEntity({
344
- name,
345
- type: normalizedType ?? undefined,
346
- summary,
347
- tags,
348
- visibility,
349
- });
350
- }
351
- function parseKnowledgeGraph(graphPath) {
352
- if (!isFile(graphPath))
353
- return [];
354
- let raw;
355
- try {
356
- raw = readFileSync(graphPath, "utf8");
357
- }
358
- catch {
359
- return [];
360
- }
361
- const entities = [];
362
- for (const lineRaw of raw.split(/\r?\n/)) {
363
- const line = lineRaw.trim();
364
- if (!line)
365
- continue;
366
- let record;
367
- try {
368
- record = JSON.parse(line);
369
- }
370
- catch {
371
- continue; // skip malformed JSONL (logged at WARNING in the oracle)
372
- }
373
- if (typeof record !== "object" || record === null)
374
- continue;
375
- const rec = record;
376
- if (rec["type"] !== "entity")
377
- continue;
378
- const entity = graphEntityToBourdonEntity(rec);
379
- if (entity)
380
- entities.push(entity);
381
- }
382
- return entities;
383
- }
384
- // -- Dedupe + merge -----------------------------------------------------------
385
- const VIS_ORDER = {
386
- [Visibility.PRIVATE]: 0,
387
- [Visibility.TEAM]: 1,
388
- [Visibility.PUBLIC]: 2,
389
- none: 3,
390
- };
391
- function visRank(v) {
392
- return v == null ? VIS_ORDER["none"] : VIS_ORDER[v];
393
- }
394
- function mergeEntities(into, other) {
395
- if (!into.type && other.type)
396
- into.type = other.type;
397
- if (other.summary && other.summary.trim() && other.summary.length > (into.summary?.length ?? 0)) {
398
- into.summary = other.summary;
399
- }
400
- into.tags = [...new Set([...(into.tags ?? []), ...(other.tags ?? [])])];
401
- if (visRank(other.visibility) < visRank(into.visibility)) {
402
- into.visibility = other.visibility;
403
- }
404
- return into;
405
- }
406
- /**
407
- * Merge multiple entity lists into one, deduping by case-insensitive name.
408
- * NOTE the faithful oracle quirk: the fresh copy DROPS valid_from / valid_to, so
409
- * an archived project keeps its `archived` tag but emits no `valid_to`. Do not
410
- * "fix" this — parity breaks otherwise.
411
- */
412
- function dedupeEntities(sourceLists) {
413
- const byKey = new Map();
414
- for (const source of sourceLists) {
415
- for (const entity of source) {
416
- const key = entity.name.trim().toLowerCase();
417
- if (!key)
418
- continue;
419
- const existing = byKey.get(key);
420
- if (existing) {
421
- mergeEntities(existing, entity);
422
- }
423
- else {
424
- byKey.set(key, makeEntity({
425
- name: entity.name,
426
- type: entity.type,
427
- aliases: [...(entity.aliases ?? [])],
428
- summary: entity.summary,
429
- last_touched: entity.last_touched,
430
- tags: [...(entity.tags ?? [])],
431
- visibility: entity.visibility,
432
- // valid_from / valid_to intentionally dropped (oracle behavior).
433
- }));
434
- }
435
- }
436
- }
437
- return [...byKey.values()].sort((a, b) => {
438
- const an = a.name.toLowerCase();
439
- const bn = b.name.toLowerCase();
440
- return an < bn ? -1 : an > bn ? 1 : 0;
441
- });
442
- }
443
- // -- Participant --------------------------------------------------------------
444
- /** External participant for Anthropic's Claude Code CLI. */
445
- export class ClaudeCodeParticipant {
446
- agentId = AGENT_ID;
447
- agentType = AGENT_TYPE;
448
- nativePath;
449
- /** Public so a hermetic test can seed the discovered sources directly (path
450
- * RESOLUTION is not the parity contract — only the export output shape is). */
451
- brainPath;
452
- autoMemoryPath;
453
- knowledgeGraphPath;
454
- static defaultNativePath(home) {
455
- return join(home ?? homeDir(), ".claude");
456
- }
457
- constructor() {
458
- this.nativePath = join(homeDir(), "claude-brain"); // primary anchor
459
- this.brainPath = resolveClaudeBrainPath();
460
- this.autoMemoryPath = resolveAutoMemoryPath();
461
- this.knowledgeGraphPath = resolveKnowledgeGraphPath();
462
- }
463
- discover() {
464
- const sources = {
465
- claude_brain: this.brainPath,
466
- auto_memory: this.autoMemoryPath,
467
- knowledge_graph: this.knowledgeGraphPath,
468
- };
469
- if (!Object.values(sources).some((v) => v)) {
470
- throw new ParticipantDiscoveryError("No Claude Code memory sources found. Expected one of: ~/claude-brain/, " +
471
- "~/.claude/projects/*/memory/, ~/claude-memory/memory.jsonl.");
472
- }
473
- return {
474
- path: this.nativePath,
475
- version: "claude-code-memory-v1",
476
- metadata: { sources },
477
- };
478
- }
479
- exportL5(since) {
480
- const store = this.discover(); // re-raises on missing
481
- const sources = store.metadata["sources"];
482
- const capabilities = Object.entries(sources)
483
- .filter(([, v]) => v)
484
- .map(([k]) => k)
485
- .sort();
486
- const autoMemoryEntities = this.autoMemoryPath ? parseAutoMemory(this.autoMemoryPath) : [];
487
- const graphEntities = this.knowledgeGraphPath
488
- ? parseKnowledgeGraph(this.knowledgeGraphPath)
489
- : [];
490
- const projectEntities = this.brainPath ? parseProjectsDir(this.brainPath) : [];
491
- // Priority: auto-memory > graph > brain projects (first wins on collision).
492
- const allEntities = dedupeEntities([autoMemoryEntities, graphEntities, projectEntities]);
493
- // Visibility filter — private entities never leave this function.
494
- const visibleEntities = filterForFederation(allEntities, DEFAULT_POLICY);
495
- const sessions = this.brainPath ? parseLogsDir(this.brainPath, since) : [];
496
- return makeManifest({
497
- spec_version: SPEC_VERSION,
498
- agent: makeAgentInfo({
499
- id: this.agentId,
500
- type: this.agentType,
501
- instance: hostname() || "unknown",
502
- spec_version_compat: `>=${SPEC_VERSION}`,
503
- role_narrative: ROLE_NARRATIVE,
504
- }),
505
- last_updated: new Date().toISOString(),
506
- capabilities,
507
- recent_sessions: sessions,
508
- known_entities: visibleEntities,
509
- visibility_policy: DEFAULT_POLICY,
510
- });
511
- }
512
- exportSessions(since, limit = 100) {
513
- if (!this.brainPath)
514
- return [];
515
- return parseLogsDir(this.brainPath, since, limit);
516
- }
517
- healthCheck() {
518
- try {
519
- const present = [this.brainPath, this.autoMemoryPath, this.knowledgeGraphPath].filter((p) => p !== null).length;
520
- const details = {
521
- claude_brain: this.brainPath ?? "missing",
522
- auto_memory: this.autoMemoryPath ?? "missing",
523
- knowledge_graph: this.knowledgeGraphPath ?? "missing",
524
- };
525
- if (present === 3)
526
- return { status: "ok", details };
527
- if (present === 0) {
528
- return {
529
- status: "blocked",
530
- reason: "No Claude Code memory sources found on this machine",
531
- details,
532
- proposedFix: "Run `bourdon setup` to wire the SessionEnd hook and bootstrap " +
533
- "~/agent-library/, then open Claude Code once to populate ~/.claude/projects/.",
534
- };
535
- }
536
- return {
537
- status: "degraded",
538
- reason: `${present}/3 Claude Code memory sources found`,
539
- details,
540
- proposedFix: "Run `bourdon setup` to (re-)wire missing sources, or set the CLAUDE_BRAIN " +
541
- "env var to point at your claude-brain checkout.",
542
- };
543
- }
544
- catch (err) {
545
- return {
546
- status: "degraded",
547
- reason: `unexpected error: ${String(err)}`,
548
- details: {},
549
- };
550
- }
551
- }
552
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * GitHub-embedded Copilot participant — the v0.3 network-adapter reference.
3
- * Port of `participants/github_copilot.py`.
4
- *
5
- * Cloud-side Copilot (PR review, `@copilot` comments) persists nothing on the
6
- * user's machine — its state lives on github.com. This adapter federates it by
7
- * reading the user's GitHub activity over the REST API. The
8
- * {@link NetworkParticipant} base provides the cache, degradation, and auth
9
- * boundary; this class provides only the GitHub-specific fetch + payload→L5.
10
- *
11
- * Auth: a token with `repo` + `read:user` scope, sourced from `$GITHUB_TOKEN` /
12
- * `$GH_TOKEN`, else `gh auth token`. Fetched lazily; never stored in the cache
13
- * or the manifest.
14
- */
15
- import { type L5ManifestModel } from "@getbourdon/l5";
16
- import { NetworkParticipant, type AuthProvider, type NetworkParticipantOptions } from "../network-base.js";
17
- /** Resolve a GitHub token: env var first, then the `gh` CLI keychain. */
18
- export declare function ghTokenProvider(): string | null;
19
- /** Federates cloud-side GitHub Copilot activity via the GitHub REST API. */
20
- export declare class GitHubCopilotParticipant extends NetworkParticipant {
21
- participantSlug: string;
22
- agentId: string;
23
- agentType: string;
24
- nativePath: string;
25
- displayName: string;
26
- private readonly maxResults;
27
- constructor(opts?: NetworkParticipantOptions & {
28
- maxResults?: number;
29
- });
30
- static defaultNativePath(): string;
31
- fetchPayload(token: string): Record<string, unknown>;
32
- payloadToL5(payload: Record<string, unknown>): L5ManifestModel;
33
- }
34
- export type { AuthProvider };