@getbourdon/participants 0.2.0 → 0.2.2

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,413 +0,0 @@
1
- /**
2
- * GitHub Copilot (local convention) participant — reads the convention-based
3
- * memory file at `~/.copilot-bourdon/memory.md`. Port of `participants/copilot.py`.
4
- *
5
- * Copilot has no accessible local session index (its reasoning is cloud-side and
6
- * its native memory is not exposed on disk), so — like the Codex pre-distillation
7
- * fallback — this reader normalizes a user-maintained convention file into a
8
- * Bourdon L5 manifest. The file is YAML front-matter (structured `entities:` /
9
- * `sessions:`) followed by an optional freeform markdown body that Copilot Chat
10
- * reads as plain context but this reader ignores.
11
- *
12
- * NOT the cloud GitHub-Copilot network adapter ({@link GitHubCopilotParticipant})
13
- * and NOT the Copilot CLI SQLite reader.
14
- *
15
- * Paths checked (in order):
16
- * 1. `COPILOT_BOURDON_HOME` env-var override
17
- * 2. `~/.copilot-bourdon/` (default convention path)
18
- *
19
- * Defensive throughout: a missing convention directory raises
20
- * {@link ParticipantDiscoveryError} from `discover()`; malformed YAML degrades to
21
- * an empty manifest rather than raising; `healthCheck` never throws.
22
- */
23
- import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
24
- import { homedir, hostname } from "node:os";
25
- import { join } from "node:path";
26
- import { parse as yamlParse } from "yaml";
27
- import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
28
- import { redactText } from "@getbourdon/redaction";
29
- import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
30
- const AGENT_ID = "copilot";
31
- const AGENT_TYPE = "code-assistant";
32
- const DISPLAY_NAME = "GitHub Copilot";
33
- const ROLE_NARRATIVE = "Inline completion and chat assistant present across every IDE the team uses. " +
34
- "Works alongside the human at the keystroke level -- the ambient layer that " +
35
- "recognises what is being typed before a full turn is even formed. Bourdon " +
36
- "gives Copilot cross-session entity awareness it would otherwise lack entirely.";
37
- // Convention directory and file names. Override dir with COPILOT_BOURDON_HOME.
38
- const CONVENTION_DIR_NAME = ".copilot-bourdon";
39
- const MEMORY_FILENAME = "memory.md";
40
- // Front-matter delimiters (mirrors claude-code's parseFrontmatter conventions).
41
- const FRONTMATTER_OPEN = "---\n";
42
- const FRONTMATTER_CLOSE = "\n---\n";
43
- const DEFAULT_POLICY = makeVisibilityPolicy({
44
- default: Visibility.TEAM,
45
- private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
46
- team_tags: ["copilot-memory", "workspace", "copilot"],
47
- });
48
- /** Minimal logger so a degraded read is visible without pulling in a dep
49
- * (matches the registry's logging idiom). */
50
- const logger = {
51
- warn(msg) {
52
- // eslint-disable-next-line no-console
53
- console.warn(`[bourdon.participants] ${msg}`);
54
- },
55
- };
56
- /** Starter template written by `bourdon copilot init`. */
57
- const MEMORY_TEMPLATE = `---
58
- # Copilot Bourdon Memory
59
- # Edit this file to give Copilot cross-session entity awareness.
60
- # The YAML front-matter is parsed by \`bourdon copilot export\`.
61
- # The markdown body below the closing \`---\` is freeform context for Copilot Chat.
62
-
63
- entities: []
64
-
65
- sessions: []
66
- ---
67
-
68
- # Copilot notes
69
-
70
- Add project notes, preferences, or anything else you want Copilot Chat to
71
- remember here. This section is freeform -- the participant only reads the YAML
72
- front-matter above.
73
- `;
74
- // -- fs helpers ---------------------------------------------------------------
75
- function homeDir() {
76
- // os.homedir() can throw under some sandboxes; stay defensive so import +
77
- // construction never crash even when HOME points at an unreadable path.
78
- try {
79
- return homedir();
80
- }
81
- catch {
82
- return process.env.HOME || process.env.USERPROFILE || ".";
83
- }
84
- }
85
- function isDir(p) {
86
- try {
87
- return statSync(p).isDirectory();
88
- }
89
- catch {
90
- return false;
91
- }
92
- }
93
- function isFile(p) {
94
- try {
95
- return statSync(p).isFile();
96
- }
97
- catch {
98
- return false;
99
- }
100
- }
101
- // -- Path helpers -------------------------------------------------------------
102
- /**
103
- * Return the conventional `~/.copilot-bourdon/` directory path. Respects the
104
- * `COPILOT_BOURDON_HOME` env-var override (returned verbatim, even if absent;
105
- * the directory is not created here).
106
- */
107
- export function defaultCopilotBourdonDir() {
108
- const env = process.env.COPILOT_BOURDON_HOME;
109
- if (env)
110
- return env;
111
- return join(homeDir(), CONVENTION_DIR_NAME);
112
- }
113
- /** Return the path to the convention memory file. */
114
- export function defaultCopilotMemoryPath(copilotDir) {
115
- return join(copilotDir || defaultCopilotBourdonDir(), MEMORY_FILENAME);
116
- }
117
- // -- Coercion helpers (mirror Python str() / isinstance filtering) ------------
118
- /** Mirror Python `str(value)` for the value types YAML yields here. The `yaml`
119
- * lib parses timestamps into JS `Date`; Python's `yaml.safe_load` yields
120
- * `date`/`datetime` whose `str()` is ISO-ish, so a Date is rendered as its ISO
121
- * date (date-only callers slice to 10 anyway). */
122
- function coerceStr(v) {
123
- if (v instanceof Date)
124
- return v.toISOString().slice(0, 10);
125
- return String(v);
126
- }
127
- /** Mirror Python `str(raw.get(key) or "") or None`. */
128
- function strOrUndef(v) {
129
- const s = coerceStr(v || "");
130
- return s || undefined;
131
- }
132
- /** Mirror `[str(x) for x in raw if isinstance(x, str) and x.strip()]`. */
133
- function strList(v) {
134
- if (!Array.isArray(v))
135
- return [];
136
- return v.filter((x) => typeof x === "string" && x.trim() !== "");
137
- }
138
- function isRecord(v) {
139
- return typeof v === "object" && v !== null && !Array.isArray(v);
140
- }
141
- function asArray(v) {
142
- return Array.isArray(v) ? v : [];
143
- }
144
- // -- YAML frontmatter ---------------------------------------------------------
145
- /**
146
- * Extract and parse the YAML front-matter block from memory file text. Returns
147
- * an empty object when the file has no front-matter or when the YAML cannot be
148
- * parsed (logged at WARNING in the oracle; never raises here either).
149
- */
150
- function parseFrontmatter(text, source) {
151
- if (!text.startsWith(FRONTMATTER_OPEN))
152
- return {};
153
- const end = text.indexOf(FRONTMATTER_CLOSE, FRONTMATTER_OPEN.length);
154
- if (end === -1)
155
- return {};
156
- const fmText = text.slice(FRONTMATTER_OPEN.length, end);
157
- let parsed;
158
- try {
159
- parsed = yamlParse(fmText);
160
- }
161
- catch (err) {
162
- const where = source ? ` in ${source}` : "";
163
- const detail = String(err).replace(/\n/g, " ").slice(0, 200);
164
- logger.warn(`CopilotParticipant: malformed YAML frontmatter${where}; ` +
165
- `treating as no-frontmatter (${detail})`);
166
- return {};
167
- }
168
- return isRecord(parsed) ? parsed : {};
169
- }
170
- /**
171
- * Read and parse a copilot-bourdon memory file. Returns an empty object on any
172
- * I/O or parse error — the participant degrades to an empty manifest.
173
- */
174
- function readMemoryFile(path) {
175
- if (!isFile(path))
176
- return {};
177
- let text;
178
- try {
179
- text = readFileSync(path, "utf8");
180
- }
181
- catch (err) {
182
- logger.warn(`CopilotParticipant: cannot read ${path}: ${String(err)}`);
183
- return {};
184
- }
185
- return parseFrontmatter(text, path);
186
- }
187
- // -- Entity / Session converters ----------------------------------------------
188
- /** Convert a raw YAML entity record to a Bourdon Entity. Skips malformed
189
- * entries (oracle logs at DEBUG). Never raises. */
190
- function buildEntity(raw) {
191
- if (!isRecord(raw))
192
- return null;
193
- const name = raw["name"];
194
- if (typeof name !== "string" || name.trim() === "")
195
- return null;
196
- const summaryRaw = raw["summary"] || "";
197
- const summary = summaryRaw ? redactText(coerceStr(summaryRaw)) : undefined;
198
- return makeEntity({
199
- name: name.trim(),
200
- type: strOrUndef(raw["type"]),
201
- aliases: strList(raw["aliases"]),
202
- summary,
203
- last_touched: strOrUndef(raw["last_touched"]),
204
- tags: strList(raw["tags"]),
205
- valid_from: strOrUndef(raw["valid_from"]),
206
- valid_to: strOrUndef(raw["valid_to"]),
207
- });
208
- }
209
- /** Convert a raw YAML session record to a Bourdon Session. Skips malformed
210
- * entries (oracle logs at DEBUG). Never raises. */
211
- function buildSession(raw) {
212
- if (!isRecord(raw))
213
- return null;
214
- const dateVal = raw["date"];
215
- if (!dateVal)
216
- return null;
217
- const dateStr = coerceStr(dateVal).slice(0, 10); // keep YYYY-MM-DD prefix only
218
- return makeSession({
219
- date: dateStr,
220
- cwd: strOrUndef(raw["cwd"]),
221
- project_focus: strList(raw["project_focus"]),
222
- key_actions: strList(raw["key_actions"]),
223
- files_touched: strList(raw["files_touched"]),
224
- });
225
- }
226
- // -- Diagnostics --------------------------------------------------------------
227
- /** Diagnostic summary of the copilot-bourdon memory file (`bourdon copilot
228
- * doctor`). Never raises. */
229
- export function inspectCopilotMemory(copilotDir) {
230
- const memPath = defaultCopilotMemoryPath(copilotDir);
231
- const report = {
232
- path: memPath,
233
- present: isFile(memPath),
234
- readable: false,
235
- frontmatter_valid: false,
236
- entity_count: 0,
237
- session_count: 0,
238
- error: null,
239
- };
240
- if (!isFile(memPath)) {
241
- report["error"] = "missing";
242
- return report;
243
- }
244
- let text;
245
- try {
246
- text = readFileSync(memPath, "utf8");
247
- report["readable"] = true;
248
- }
249
- catch (err) {
250
- report["error"] = String(err);
251
- return report;
252
- }
253
- const data = parseFrontmatter(text, memPath);
254
- if (Object.keys(data).length > 0)
255
- report["frontmatter_valid"] = true;
256
- report["entity_count"] = asArray(data["entities"]).length;
257
- report["session_count"] = asArray(data["sessions"]).length;
258
- return report;
259
- }
260
- // -- Participant --------------------------------------------------------------
261
- /**
262
- * External participant for GitHub Copilot via `~/.copilot-bourdon/memory.md`.
263
- * Implements {@link BourdonParticipant} structurally.
264
- */
265
- export class CopilotParticipant {
266
- agentId = AGENT_ID;
267
- agentType = AGENT_TYPE;
268
- displayName = DISPLAY_NAME;
269
- nativePath;
270
- /** Conventional `~/.copilot-bourdon` dir used by the setup wizard's detection. */
271
- static defaultNativePath(home) {
272
- if (home !== undefined)
273
- return join(home, CONVENTION_DIR_NAME);
274
- return defaultCopilotBourdonDir();
275
- }
276
- copilotDir;
277
- constructor(copilotDir) {
278
- this.copilotDir = copilotDir ?? null;
279
- this.nativePath = this.copilotDir || defaultCopilotBourdonDir();
280
- }
281
- discover() {
282
- const path = this.copilotDir || defaultCopilotBourdonDir();
283
- if (!isDir(path)) {
284
- throw new ParticipantDiscoveryError(`Copilot convention directory not found at '${path}'. ` +
285
- "Run `bourdon copilot init` to create it with a starter template, " +
286
- "or create ~/.copilot-bourdon/memory.md manually.");
287
- }
288
- const memPath = join(path, MEMORY_FILENAME);
289
- return {
290
- path,
291
- version: "convention-v1",
292
- metadata: {
293
- memory_file: memPath,
294
- memory_file_present: isFile(memPath),
295
- },
296
- };
297
- }
298
- exportSessions(since, limit = 100) {
299
- const data = this.read();
300
- const sinceIso = since ? since.toISOString().slice(0, 10) : null;
301
- const sessions = [];
302
- for (const raw of asArray(data["sessions"])) {
303
- const session = buildSession(raw);
304
- if (session === null)
305
- continue;
306
- if (sinceIso && session.date && session.date < sinceIso)
307
- continue;
308
- sessions.push(session);
309
- if (sessions.length >= limit)
310
- break;
311
- }
312
- return sessions;
313
- }
314
- exportL5(since) {
315
- const data = this.read();
316
- const entities = [];
317
- for (const raw of asArray(data["entities"])) {
318
- const entity = buildEntity(raw);
319
- if (entity !== null)
320
- entities.push(entity);
321
- }
322
- const visibleEntities = filterForFederation(entities, DEFAULT_POLICY);
323
- const sinceIso = since ? since.toISOString().slice(0, 10) : null;
324
- const sessions = [];
325
- for (const raw of asArray(data["sessions"])) {
326
- const session = buildSession(raw);
327
- if (session === null)
328
- continue;
329
- if (sinceIso && session.date && session.date < sinceIso)
330
- continue;
331
- sessions.push(session);
332
- }
333
- return makeManifest({
334
- spec_version: SPEC_VERSION,
335
- agent: makeAgentInfo({
336
- id: AGENT_ID,
337
- type: AGENT_TYPE,
338
- instance: hostname() || "unknown",
339
- spec_version_compat: `>=${SPEC_VERSION}`,
340
- role_narrative: ROLE_NARRATIVE,
341
- }),
342
- last_updated: new Date().toISOString(),
343
- capabilities: ["inline-completion", "chat", "pr-review", "convention-memory"],
344
- recent_sessions: sessions,
345
- known_entities: visibleEntities,
346
- visibility_policy: DEFAULT_POLICY,
347
- });
348
- }
349
- healthCheck() {
350
- const path = this.copilotDir || defaultCopilotBourdonDir();
351
- if (!isDir(path)) {
352
- return {
353
- status: "blocked",
354
- reason: `Convention directory ${path} not found. ` +
355
- "Run `bourdon copilot init` to create it.",
356
- details: { expected_path: path },
357
- proposedFix: "Run `bourdon copilot init` to create the convention directory.",
358
- };
359
- }
360
- const memPath = join(path, MEMORY_FILENAME);
361
- if (!isFile(memPath)) {
362
- return {
363
- status: "degraded",
364
- reason: `Memory file ${memPath} not found. ` +
365
- "Run `bourdon copilot init` to write a starter template.",
366
- details: { expected_memory_file: memPath },
367
- proposedFix: "Run `bourdon copilot init` to write the starter memory.md template.",
368
- };
369
- }
370
- let data;
371
- try {
372
- data = this.read();
373
- }
374
- catch (err) {
375
- logger.warn(`CopilotParticipant health_check failed: ${String(err)}`);
376
- return {
377
- status: "degraded",
378
- reason: "Memory file present but could not be parsed.",
379
- details: { error: String(err) },
380
- proposedFix: `Inspect ${memPath} for malformed YAML front-matter. ` +
381
- "The opening and closing `---` fences must wrap a valid YAML " +
382
- "block. See docs/agent-integration-status.md for the schema.",
383
- };
384
- }
385
- return {
386
- status: "ok",
387
- details: {
388
- memory_file: memPath,
389
- entity_count: asArray(data["entities"]).length,
390
- session_count: asArray(data["sessions"]).length,
391
- },
392
- };
393
- }
394
- // -- Internal ---------------------------------------------------------------
395
- read() {
396
- return readMemoryFile(defaultCopilotMemoryPath(this.copilotDir));
397
- }
398
- }
399
- // -- Init helper --------------------------------------------------------------
400
- /**
401
- * Create `~/.copilot-bourdon/memory.md` with a starter template. Returns the
402
- * written path. Throws when the file already exists unless `force` is true.
403
- */
404
- export function initMemoryFile(copilotDir, force = false) {
405
- const targetDir = copilotDir || defaultCopilotBourdonDir();
406
- mkdirSync(targetDir, { recursive: true });
407
- const memPath = join(targetDir, MEMORY_FILENAME);
408
- if (isFile(memPath) && !force) {
409
- throw new Error(`${memPath} already exists. Pass --force to overwrite.`);
410
- }
411
- writeFileSync(memPath, MEMORY_TEMPLATE, "utf8");
412
- return memPath;
413
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Cursor participant — reads Cursor's (the AI-first IDE) SQLite-backed
3
- * workspace/composer state and normalizes it into L5. Port of
4
- * `participants/cursor.py` + the extraction helper `participants/_cursor_sqlite.py`.
5
- *
6
- * Cursor stores workspace state in `state.vscdb` SQLite DBs at platform-specific
7
- * paths (`~/.config/Cursor/`, `~/Library/Application Support/Cursor/`,
8
- * `%APPDATA%/Cursor/`). This reader discovers those stores, reads the `ItemTable`
9
- * key/value rows read-only, parses the composer/chat records into sessions +
10
- * project/topic entities, and merges curated `.cursor/memory/short-index.json`
11
- * entities alongside them.
12
- *
13
- * SQLite access: the Python oracle copies each DB (+ its `-wal`/`-shm`
14
- * companions) to a tmp file and opens it read-write so SQLite folds the WAL into
15
- * a consistent snapshot. The TS mirror instead leans on {@link tryOpenReadonly}
16
- * (`mode=ro`), which respects SQLite locking and reads the `-wal` sidecar of a
17
- * live writer — the same consistent view without the copy. Read-only,
18
- * deterministic, visibility-enforced-before-emission, redaction on every native
19
- * string; `healthCheck` never throws.
20
- */
21
- import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
22
- import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
23
- /**
24
- * Platform-specific Cursor data dir (the SQLite workspace state lives here),
25
- * `$CURSOR_DIR` override first. Returns null on an unrecognized platform (or
26
- * Windows without `%APPDATA%`). Mirrors `default_cursor_dir`.
27
- */
28
- export declare function defaultCursorDir(): string | null;
29
- /** External participant for Cursor's SQLite workspace state. */
30
- export declare class CursorParticipant implements BourdonParticipant {
31
- agentId: string;
32
- agentType: string;
33
- private readonly cursorDir;
34
- private readonly workspaceRoot;
35
- private readonly policy;
36
- /**
37
- * Conventional Cursor config dir (`~/.cursor`) used by the setup wizard. The
38
- * wizard probes `~/.cursor` for presence; the SQLite workspace state that
39
- * `exportL5` actually reads lives at {@link defaultCursorDir}. Detection and
40
- * extraction intentionally use different anchors.
41
- */
42
- static defaultNativePath(home?: string): string;
43
- constructor(cursorDir?: string | null, workspaceRoot?: string | null);
44
- get nativePath(): string;
45
- discover(): AgentStore;
46
- exportSessions(since?: Date, limit?: number): SessionModel[];
47
- exportL5(since?: Date): L5ManifestModel;
48
- healthCheck(): HealthStatus;
49
- private extract;
50
- }