@getbourdon/participants 0.1.1 → 0.2.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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Cascade (Windsurf) participant — convention-file reader. Port of
3
+ * `participants/cascade.py`.
4
+ *
5
+ * Cascade's internal state is not exposed on the filesystem in a standardized
6
+ * format (like cloud Copilot). Instead this participant uses a CONVENTION FILE:
7
+ * Cascade maintains a structured memory file at `~/.cascade-bourdon/memory.md`
8
+ * with YAML front-matter carrying `entities` + `sessions`. Cascade owns that
9
+ * projection explicitly (it writes what it knows at session end); the
10
+ * participant normalizes the front-matter into an L5 manifest for federation.
11
+ *
12
+ * Privacy: entity/session summaries pass through the redaction SSOT; the
13
+ * visibility policy (private_tags) filters PRIVATE entities before emission.
14
+ */
15
+ import { type L5ManifestModel, type SessionModel, type VisibilityPolicyModel } from "@getbourdon/l5";
16
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
17
+ /** Return the default Cascade-Bourdon convention directory (`~/.cascade-bourdon`). */
18
+ export declare function defaultCascadeDir(): string;
19
+ /** Return the default path to the Cascade memory file. */
20
+ export declare function defaultCascadeMemoryPath(): string;
21
+ /**
22
+ * Create a starter memory.md in the Cascade-Bourdon convention directory.
23
+ *
24
+ * @param cascadeDir Override the convention directory (default `~/.cascade-bourdon`).
25
+ * @param force Overwrite an existing file; otherwise throws.
26
+ * @returns the path to the created memory file.
27
+ */
28
+ export declare function initMemoryFile(cascadeDir?: string, force?: boolean): string;
29
+ /**
30
+ * Convention-based Bourdon participant for Cascade (Windsurf). Reads structured
31
+ * memory from `~/.cascade-bourdon/memory.md` and normalizes it into L5.
32
+ */
33
+ export declare class CascadeParticipant implements BourdonParticipant {
34
+ agentId: string;
35
+ agentType: string;
36
+ displayName: string;
37
+ private readonly dir;
38
+ private readonly policy;
39
+ static defaultNativePath(home?: string): string;
40
+ constructor(cascadeDir?: string, policy?: VisibilityPolicyModel);
41
+ /** Path to the Cascade-Bourdon convention directory. */
42
+ get nativePath(): string;
43
+ private memoryPath;
44
+ private readFrontmatter;
45
+ discover(): AgentStore;
46
+ exportL5(since?: Date): L5ManifestModel;
47
+ exportSessions(since?: Date, limit?: number): SessionModel[];
48
+ healthCheck(): HealthStatus;
49
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Cascade (Windsurf) participant — convention-file reader. Port of
3
+ * `participants/cascade.py`.
4
+ *
5
+ * Cascade's internal state is not exposed on the filesystem in a standardized
6
+ * format (like cloud Copilot). Instead this participant uses a CONVENTION FILE:
7
+ * Cascade maintains a structured memory file at `~/.cascade-bourdon/memory.md`
8
+ * with YAML front-matter carrying `entities` + `sessions`. Cascade owns that
9
+ * projection explicitly (it writes what it knows at session end); the
10
+ * participant normalizes the front-matter into an L5 manifest for federation.
11
+ *
12
+ * Privacy: entity/session summaries pass through the redaction SSOT; the
13
+ * visibility policy (private_tags) filters PRIVATE entities before emission.
14
+ */
15
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { 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 { REDACTED, SENSITIVE_PATTERNS, redactText } from "@getbourdon/redaction";
21
+ import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
22
+ // -- Constants -----------------------------------------------------------------
23
+ const AGENT_ID = "cascade";
24
+ const AGENT_TYPE = "code-assistant";
25
+ const DISPLAY_NAME = "Cascade (Windsurf)";
26
+ const ROLE_NARRATIVE = "Agentic AI coding assistant embedded in Windsurf IDE. " +
27
+ "Operates with multi-step planning, tool use (file editing, terminal, " +
28
+ "browser preview, code search), persistent memory, and workspace-level " +
29
+ "context awareness. Specializes in pair-programming workflows with " +
30
+ "concurrent read-plan-execute cycles.";
31
+ const CONVENTION_DIR_NAME = ".cascade-bourdon";
32
+ const MEMORY_FILENAME = "memory.md";
33
+ const DEFAULT_POLICY = makeVisibilityPolicy({
34
+ default: Visibility.PUBLIC,
35
+ private_tags: ["personal", "credential", "financial", "secret", "private"],
36
+ });
37
+ /**
38
+ * The native-memory sensitive-pattern set, extended with Cascade-specific
39
+ * triggers (`secret`, `sk_test_*`). A match anywhere drops the WHOLE string to
40
+ * the redaction sentinel (see {@link scrubCredential}). All non-global so they
41
+ * are safe to reuse across `.test()`.
42
+ */
43
+ const CASCADE_SENSITIVE_PATTERNS = [
44
+ ...SENSITIVE_PATTERNS,
45
+ /\bsecret\b/i,
46
+ /sk[_-]test[_-]/i,
47
+ ];
48
+ /** Minimal logger so a malformed-frontmatter warning is visible without a dep. */
49
+ const logger = {
50
+ warn(msg) {
51
+ // eslint-disable-next-line no-console
52
+ console.warn(`[bourdon.participants] ${msg}`);
53
+ },
54
+ };
55
+ // -- Path helpers --------------------------------------------------------------
56
+ function homeDir() {
57
+ // os.homedir() can throw under some sandboxes; stay defensive so import +
58
+ // construction never crash even when HOME points at an unreadable path.
59
+ try {
60
+ return homedir();
61
+ }
62
+ catch {
63
+ return process.env.HOME || process.env.USERPROFILE || ".";
64
+ }
65
+ }
66
+ function isDir(p) {
67
+ try {
68
+ return statSync(p).isDirectory();
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function isFile(p) {
75
+ try {
76
+ return statSync(p).isFile();
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /** Return the default Cascade-Bourdon convention directory (`~/.cascade-bourdon`). */
83
+ export function defaultCascadeDir() {
84
+ return join(homeDir(), CONVENTION_DIR_NAME);
85
+ }
86
+ /** Return the default path to the Cascade memory file. */
87
+ export function defaultCascadeMemoryPath() {
88
+ return join(defaultCascadeDir(), MEMORY_FILENAME);
89
+ }
90
+ // -- Front-matter --------------------------------------------------------------
91
+ /**
92
+ * Extract YAML front-matter from a `---` fenced block. Returns an empty object
93
+ * when the text has no valid front-matter. On YAML parse failure logs at WARNING
94
+ * (with the source path when provided) and treats it as no-frontmatter.
95
+ *
96
+ * NOTE: this is the Cascade-specific fence parse (`startswith("---")` +
97
+ * `find("---", 3)`), distinct from the claude-code reader's `---\n` variant —
98
+ * preserved faithfully from the oracle.
99
+ */
100
+ function parseFrontmatter(text, source) {
101
+ if (!text.startsWith("---"))
102
+ return {};
103
+ const end = text.indexOf("---", 3);
104
+ if (end === -1)
105
+ return {};
106
+ const yamlBlock = text.slice(3, end).trim();
107
+ if (!yamlBlock)
108
+ return {};
109
+ let data;
110
+ try {
111
+ data = yamlParse(yamlBlock);
112
+ }
113
+ catch (exc) {
114
+ const where = source ? ` in ${source}` : "";
115
+ const detail = String(exc).replace(/\n/g, " ").slice(0, 200);
116
+ logger.warn(`CascadeParticipant: malformed YAML frontmatter${where}; ` +
117
+ `treating as no-frontmatter (${detail})`);
118
+ return {};
119
+ }
120
+ return typeof data === "object" && data !== null && !Array.isArray(data)
121
+ ? data
122
+ : {};
123
+ }
124
+ // -- Redaction -----------------------------------------------------------------
125
+ /**
126
+ * Redact + truncate native-memory text. Same semantics as the codex reader's
127
+ * `_safe_native_memory_text` (the redaction SSOT), extended with Cascade-
128
+ * specific patterns (`secret`, `sk_test_*`): if any match, drop the whole string
129
+ * to the redaction sentinel; otherwise scrub + cap via {@link redactText}.
130
+ */
131
+ function scrubCredential(text) {
132
+ if (!text)
133
+ return text;
134
+ if (CASCADE_SENSITIVE_PATTERNS.some((p) => p.test(text))) {
135
+ return REDACTED;
136
+ }
137
+ return redactText(text);
138
+ }
139
+ // -- Builders ------------------------------------------------------------------
140
+ /** Python `list(x or [])`: copy a list verbatim, char-split a string, else []. */
141
+ function asList(v) {
142
+ if (!v)
143
+ return [];
144
+ if (Array.isArray(v))
145
+ return v;
146
+ if (typeof v === "string")
147
+ return Array.from(v);
148
+ return [];
149
+ }
150
+ function isRecord(v) {
151
+ return typeof v === "object" && v !== null && !Array.isArray(v);
152
+ }
153
+ /** Build an Entity from a raw front-matter dict entry. Returns null on invalid. */
154
+ function buildEntity(raw) {
155
+ if (!isRecord(raw))
156
+ return null;
157
+ const name = raw["name"];
158
+ if (typeof name !== "string" || !name.trim())
159
+ return null;
160
+ let summary;
161
+ const rawSummary = raw["summary"];
162
+ if (typeof rawSummary === "string")
163
+ summary = scrubCredential(rawSummary);
164
+ return makeEntity({
165
+ name: name.trim(),
166
+ type: typeof raw["type"] === "string" ? raw["type"] : undefined,
167
+ summary,
168
+ aliases: asList(raw["aliases"]),
169
+ tags: asList(raw["tags"]),
170
+ last_touched: raw["last_touched"] ? String(raw["last_touched"]) : undefined,
171
+ valid_from: raw["valid_from"] ? String(raw["valid_from"]) : undefined,
172
+ valid_to: raw["valid_to"] ? String(raw["valid_to"]) : undefined,
173
+ visibility: undefined,
174
+ });
175
+ }
176
+ /** Build a Session from a raw front-matter dict entry. Returns null on invalid. */
177
+ function buildSession(raw) {
178
+ if (!isRecord(raw))
179
+ return null;
180
+ const dateVal = raw["date"];
181
+ if (!dateVal)
182
+ return null;
183
+ // Normalize datetime strings to date-only (YYYY-MM-DD).
184
+ const dateStr = String(dateVal).slice(0, 10);
185
+ return makeSession({
186
+ date: dateStr,
187
+ cwd: raw["cwd"] == null ? undefined : raw["cwd"],
188
+ key_actions: asList(raw["key_actions"]),
189
+ files_touched: asList(raw["files_touched"]),
190
+ project_focus: asList(raw["project_focus"]),
191
+ visibility: undefined,
192
+ });
193
+ }
194
+ /**
195
+ * Mirror `datetime.fromisoformat(date)` on a date-only string for the `since`
196
+ * filter. Returns the UTC midnight Date, or null on a ValueError-equivalent
197
+ * (the caller keeps the session when this is null, matching `except ValueError`).
198
+ */
199
+ function parseSessionDate(date) {
200
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
201
+ return null;
202
+ const ms = Date.parse(`${date}T00:00:00Z`);
203
+ return Number.isNaN(ms) ? null : new Date(ms);
204
+ }
205
+ function buildSessions(rawSessions, since) {
206
+ const list = Array.isArray(rawSessions) ? rawSessions : [];
207
+ const sessions = [];
208
+ for (const raw of list) {
209
+ const session = buildSession(raw);
210
+ if (session === null)
211
+ continue;
212
+ if (since !== undefined) {
213
+ const d = parseSessionDate(session.date);
214
+ if (d !== null && d.getTime() < since.getTime())
215
+ continue;
216
+ }
217
+ sessions.push(session);
218
+ }
219
+ return sessions;
220
+ }
221
+ // -- Diagnostics ---------------------------------------------------------------
222
+ /** Diagnostic inspection of the Cascade memory file: presence + content stats. */
223
+ function inspectCascadeMemory(cascadeDir) {
224
+ const memoryPath = join(cascadeDir, MEMORY_FILENAME);
225
+ if (!isFile(memoryPath))
226
+ return { present: false, error: "missing" };
227
+ let text;
228
+ try {
229
+ text = readFileSync(memoryPath, "utf8");
230
+ }
231
+ catch (e) {
232
+ return { present: true, readable: false, error: String(e) };
233
+ }
234
+ const data = parseFrontmatter(text, memoryPath);
235
+ if (Object.keys(data).length === 0) {
236
+ return {
237
+ present: true,
238
+ readable: true,
239
+ frontmatter_valid: false,
240
+ entity_count: 0,
241
+ session_count: 0,
242
+ };
243
+ }
244
+ const entities = data["entities"];
245
+ const sessions = data["sessions"];
246
+ return {
247
+ present: true,
248
+ readable: true,
249
+ frontmatter_valid: true,
250
+ entity_count: Array.isArray(entities) ? entities.length : 0,
251
+ session_count: Array.isArray(sessions) ? sessions.length : 0,
252
+ };
253
+ }
254
+ // -- Init helper ---------------------------------------------------------------
255
+ const MEMORY_TEMPLATE = `---
256
+ entities:
257
+ - name: Example Project
258
+ type: project
259
+ summary: Replace with real project summaries
260
+ tags: [project]
261
+ sessions:
262
+ - date: "{today}"
263
+ cwd: /path/to/workspace
264
+ key_actions:
265
+ - Initialized Cascade Bourdon memory
266
+ files_touched: []
267
+ project_focus: []
268
+ ---
269
+
270
+ # Cascade Bourdon Memory
271
+
272
+ This file is maintained by Cascade (Windsurf) for cross-agent memory federation.
273
+ Edit the YAML front-matter to update entities and sessions.
274
+ Cascade will update this file at session end when instructed.
275
+ `;
276
+ /**
277
+ * Create a starter memory.md in the Cascade-Bourdon convention directory.
278
+ *
279
+ * @param cascadeDir Override the convention directory (default `~/.cascade-bourdon`).
280
+ * @param force Overwrite an existing file; otherwise throws.
281
+ * @returns the path to the created memory file.
282
+ */
283
+ export function initMemoryFile(cascadeDir, force = false) {
284
+ const targetDir = cascadeDir ?? defaultCascadeDir();
285
+ mkdirSync(targetDir, { recursive: true });
286
+ const memoryPath = join(targetDir, MEMORY_FILENAME);
287
+ if (isFile(memoryPath) && !force) {
288
+ throw new Error(`Memory file already exists: ${memoryPath}. Use force=True to overwrite.`);
289
+ }
290
+ const today = new Date().toISOString().slice(0, 10);
291
+ const content = MEMORY_TEMPLATE.replace("{today}", today);
292
+ writeFileSync(memoryPath, content, "utf8");
293
+ return memoryPath;
294
+ }
295
+ // -- Participant ---------------------------------------------------------------
296
+ /**
297
+ * Convention-based Bourdon participant for Cascade (Windsurf). Reads structured
298
+ * memory from `~/.cascade-bourdon/memory.md` and normalizes it into L5.
299
+ */
300
+ export class CascadeParticipant {
301
+ agentId = AGENT_ID;
302
+ agentType = AGENT_TYPE;
303
+ displayName = DISPLAY_NAME;
304
+ dir;
305
+ policy;
306
+ static defaultNativePath(home) {
307
+ if (home !== undefined)
308
+ return join(home, CONVENTION_DIR_NAME);
309
+ return defaultCascadeDir();
310
+ }
311
+ constructor(cascadeDir, policy) {
312
+ this.dir = cascadeDir ?? defaultCascadeDir();
313
+ this.policy = policy ?? DEFAULT_POLICY;
314
+ }
315
+ /** Path to the Cascade-Bourdon convention directory. */
316
+ get nativePath() {
317
+ return this.dir;
318
+ }
319
+ memoryPath() {
320
+ return join(this.dir, MEMORY_FILENAME);
321
+ }
322
+ readFrontmatter() {
323
+ const path = this.memoryPath();
324
+ if (!isFile(path))
325
+ return {};
326
+ let text;
327
+ try {
328
+ text = readFileSync(path, "utf8");
329
+ }
330
+ catch {
331
+ return {};
332
+ }
333
+ return parseFrontmatter(text, path);
334
+ }
335
+ discover() {
336
+ if (!isDir(this.dir)) {
337
+ throw new ParticipantDiscoveryError(`Cascade-Bourdon directory not found: ${this.dir}`);
338
+ }
339
+ const memoryPresent = isFile(this.memoryPath());
340
+ return {
341
+ path: this.dir,
342
+ version: "unknown",
343
+ metadata: {
344
+ memory_file: this.memoryPath(),
345
+ memory_file_present: memoryPresent,
346
+ },
347
+ };
348
+ }
349
+ exportL5(since) {
350
+ const data = this.readFrontmatter();
351
+ // Build entities.
352
+ const rawEntities = data["entities"];
353
+ const entityList = Array.isArray(rawEntities) ? rawEntities : [];
354
+ const entities = [];
355
+ for (const raw of entityList) {
356
+ const entity = buildEntity(raw);
357
+ if (entity !== null)
358
+ entities.push(entity);
359
+ }
360
+ // Build sessions (since-filtered).
361
+ const sessions = buildSessions(data["sessions"], since);
362
+ // Apply visibility policy — filter out PRIVATE entities before emission.
363
+ const visibleEntities = filterForFederation(entities, this.policy);
364
+ return makeManifest({
365
+ spec_version: SPEC_VERSION,
366
+ agent: makeAgentInfo({
367
+ id: AGENT_ID,
368
+ type: AGENT_TYPE,
369
+ role_narrative: ROLE_NARRATIVE,
370
+ }),
371
+ last_updated: new Date().toISOString(),
372
+ known_entities: visibleEntities,
373
+ recent_sessions: sessions,
374
+ capabilities: ["chat", "code-editing", "terminal", "planning", "search"],
375
+ });
376
+ }
377
+ exportSessions(since, limit) {
378
+ const data = this.readFrontmatter();
379
+ const sessions = buildSessions(data["sessions"], since);
380
+ sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
381
+ if (limit !== undefined)
382
+ return sessions.slice(0, limit);
383
+ return sessions;
384
+ }
385
+ healthCheck() {
386
+ if (!isDir(this.dir)) {
387
+ return {
388
+ status: "blocked",
389
+ reason: "Cascade-Bourdon directory not found",
390
+ details: { expected_path: this.dir },
391
+ proposedFix: "Run `bourdon cascade init` to create the convention directory + starter memory.md.",
392
+ };
393
+ }
394
+ const report = inspectCascadeMemory(this.dir);
395
+ if (!report["present"]) {
396
+ return {
397
+ status: "degraded",
398
+ reason: "Memory file not found; run `bourdon cascade init` to create it",
399
+ details: report,
400
+ proposedFix: "Run `bourdon cascade init` to write the starter memory.md template.",
401
+ };
402
+ }
403
+ if (!report["readable"]) {
404
+ return {
405
+ status: "degraded",
406
+ reason: `Memory file not readable: ${String(report["error"])}`,
407
+ details: report,
408
+ proposedFix: `Check filesystem permissions on ${join(this.dir, "memory.md")} ` +
409
+ "(should be readable by your user).",
410
+ };
411
+ }
412
+ if (!report["frontmatter_valid"]) {
413
+ return {
414
+ status: "degraded",
415
+ reason: "Memory file has no valid YAML front-matter",
416
+ details: report,
417
+ proposedFix: `Inspect ${join(this.dir, "memory.md")} -- the opening and closing ` +
418
+ "`---` fences must wrap a valid YAML block. Run " +
419
+ "`bourdon cascade init --force` to reset to the template " +
420
+ "(WARNING: this overwrites existing content).",
421
+ };
422
+ }
423
+ return {
424
+ status: "ok",
425
+ details: report,
426
+ };
427
+ }
428
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * GitHub Copilot CLI participant — normalize the `copilot` terminal agent's
3
+ * SQLite session store into L5. Port of `participants/copilot_cli.py`.
4
+ *
5
+ * Copilot CLI stores session history in a SQLite database at
6
+ * `~/.copilot/session-store.db` (override `$COPILOT_CLI_HOME`). This is distinct
7
+ * from:
8
+ * - the VS Code Copilot Chat extension (`github-copilot` network reader)
9
+ * - the `~/.copilot-bourdon/memory.md` convention-file participant (`copilot`)
10
+ * - GitHub-embedded Copilot (no local state)
11
+ *
12
+ * The schema (schema_version 4) carries `sessions` / `turns` / `checkpoints`
13
+ * plus `session_files` / `session_refs` / `dynamic_context_items`. The reader
14
+ * opens the DB read-only (`mode=ro` via {@link tryOpenReadonly}) so a live
15
+ * writer's lock can never block the export, probes each table defensively (a
16
+ * missing table degrades to "no rows" rather than raising), redacts every native
17
+ * string, applies the visibility filter BEFORE emission, and is deterministic.
18
+ * `healthCheck` never throws.
19
+ */
20
+ import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
21
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
22
+ /**
23
+ * Conventional `~/.copilot/` directory. Respects the `COPILOT_CLI_HOME`
24
+ * environment-variable override (mirrors `default_copilot_cli_dir`).
25
+ */
26
+ export declare function defaultCopilotCliDir(): string;
27
+ /**
28
+ * External participant for GitHub Copilot CLI (`~/.copilot/session-store.db`).
29
+ * Reads the SQLite database read-only (never locks the live db).
30
+ */
31
+ export declare class CopilotCliParticipant implements BourdonParticipant {
32
+ agentId: string;
33
+ agentType: string;
34
+ displayName: string;
35
+ nativePath: string;
36
+ private readonly copilotDir;
37
+ /** Conventional `~/.copilot` dir used by the setup wizard's detection. */
38
+ static defaultNativePath(home?: string): string;
39
+ constructor(copilotDir?: string);
40
+ discover(): AgentStore;
41
+ exportSessions(since?: Date, limit?: number): SessionModel[];
42
+ exportL5(since?: Date): L5ManifestModel;
43
+ healthCheck(): HealthStatus;
44
+ private emptyManifest;
45
+ }