@getbourdon/participants 0.2.2 → 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 (48) hide show
  1. package/README.md +7 -1
  2. package/dist/base.d.ts +109 -0
  3. package/dist/base.js +88 -0
  4. package/dist/index.cjs +171 -82
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +117 -4
  7. package/dist/index.d.ts +42 -970
  8. package/dist/index.js +53 -6509
  9. package/dist/index.js.map +1 -1
  10. package/dist/network-base.d.ts +108 -0
  11. package/dist/network-base.js +248 -0
  12. package/dist/readers/cascade.d.ts +49 -0
  13. package/dist/readers/cascade.js +428 -0
  14. package/dist/readers/claude-code-automations.d.ts +62 -0
  15. package/dist/readers/claude-code-automations.js +693 -0
  16. package/dist/readers/claude-code.d.ts +50 -0
  17. package/dist/readers/claude-code.js +583 -0
  18. package/dist/readers/claude-desktop-code.d.ts +54 -0
  19. package/dist/readers/claude-desktop-code.js +526 -0
  20. package/dist/readers/claude-desktop-cowork.d.ts +47 -0
  21. package/dist/readers/claude-desktop-cowork.js +659 -0
  22. package/dist/readers/codex-automations.d.ts +55 -0
  23. package/dist/readers/codex-automations.js +519 -0
  24. package/dist/readers/codex-sessions.d.ts +88 -0
  25. package/dist/readers/codex-sessions.js +194 -0
  26. package/dist/readers/copilot-automations.d.ts +64 -0
  27. package/dist/readers/copilot-automations.js +530 -0
  28. package/dist/readers/copilot-cli.d.ts +45 -0
  29. package/dist/readers/copilot-cli.js +416 -0
  30. package/dist/readers/copilot-vscode.d.ts +54 -0
  31. package/dist/readers/copilot-vscode.js +553 -0
  32. package/dist/readers/copilot.d.ts +59 -0
  33. package/dist/readers/copilot.js +413 -0
  34. package/dist/readers/cursor-automations.d.ts +57 -0
  35. package/dist/readers/cursor-automations.js +685 -0
  36. package/dist/readers/cursor.d.ts +50 -0
  37. package/dist/readers/cursor.js +670 -0
  38. package/dist/readers/github-copilot.d.ts +34 -0
  39. package/dist/readers/github-copilot.js +242 -0
  40. package/dist/readers/hermes.d.ts +37 -0
  41. package/dist/readers/hermes.js +426 -0
  42. package/dist/readers/openclaw.d.ts +78 -0
  43. package/dist/readers/openclaw.js +386 -0
  44. package/dist/registry.d.ts +39 -0
  45. package/dist/registry.js +98 -0
  46. package/dist/sqlite-base.d.ts +65 -0
  47. package/dist/sqlite-base.js +163 -0
  48. package/package.json +5 -5
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Claude Desktop · Co-Work participant — the richest of the two Claude-desktop
3
+ * surfaces. Port of `participants/claude_desktop_cowork.py` (+ the shared
4
+ * `participants/_claude_desktop.py` extraction helpers, inlined here).
5
+ *
6
+ * The desktop app stores each Co-Work / local-agent run as a state file plus a
7
+ * sibling audit transcript:
8
+ *
9
+ * <desktop>/local-agent-mode-sessions/<acct>/<org>/local_<id>.json (state)
10
+ * <desktop>/local-agent-mode-sessions/<acct>/<org>/local_<id>/audit.jsonl
11
+ *
12
+ * This participant emits **recognition metadata only**. From the state file it
13
+ * reads surface scalars (title, cwd, model, permission mode, timestamps,
14
+ * `enabledMcpTools` *count*, `userSelectedFolders` *basenames*). From the
15
+ * sibling `audit.jsonl` it reads ONLY the `system`/`init` capability *counts*
16
+ * and the `result` safe scalars (cost, turns, error flag). It NEVER reads
17
+ * conversation content. Every emitted string passes through the redaction SSOT
18
+ * + a length cap, and TEAM entities are visibility-filtered before emission.
19
+ *
20
+ * Storage kind: file/convention (glob + JSON + JSONL), so this reads like the
21
+ * claude-code / cascade file readers — no SQLite, no network.
22
+ */
23
+ import { readFileSync, readdirSync, statSync } from "node:fs";
24
+ import { hostname } from "node:os";
25
+ import { basename, dirname, join, parse as parsePath } from "node:path";
26
+ import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
27
+ import { redactText } from "@getbourdon/redaction";
28
+ import { ParticipantDiscoveryError, SPEC_VERSION, loadProjectHints, } from "../base.js";
29
+ // -- Constants -----------------------------------------------------------------
30
+ const AGENT_ID = "claude-desktop-cowork";
31
+ const AGENT_TYPE = "code-assistant";
32
+ const DISPLAY_NAME = "Claude Desktop · Co-Work";
33
+ const SURFACE_ENTITY_NAME = "Claude Desktop Co-Work";
34
+ const ROLE_NARRATIVE = "Claude desktop app, Co-Work / local-agent mode. Bourdon reads the " +
35
+ "per-run local state and audit transcript to surface recognition metadata " +
36
+ "-- title, project, model, turn/cost scalars, capability counts -- never " +
37
+ "conversation content -- so Co-Work runs are visible to other agents.";
38
+ const DEFAULT_POLICY = makeVisibilityPolicy({
39
+ default: Visibility.TEAM,
40
+ private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
41
+ team_tags: ["claude-desktop", "claude-desktop-cowork", "agent-surface", "workspace"],
42
+ });
43
+ // Environment override (tests + non-standard installs). Takes precedence over
44
+ // the platform default but not over an explicit `home` argument.
45
+ const DESKTOP_DIR_ENV = "BOURDON_CLAUDE_DESKTOP_DIR";
46
+ // Sub-store directory name under the desktop dir.
47
+ const COWORK_STORE = "local-agent-mode-sessions";
48
+ // State filename prefix (`local_<uuid>.json`).
49
+ const STATE_PREFIX = "local_";
50
+ const MAX_PROJECTS = 6;
51
+ const MAX_KEY_ACTIONS = 8;
52
+ // audit.jsonl can be large; only the (small) init + result lines are needed, so
53
+ // bound how much to scan before giving up.
54
+ const MAX_AUDIT_LINES = 50_000;
55
+ // Product-neutral defaults only: this product plus the agent surfaces Bourdon
56
+ // federates. A deployment adds its own project roster via the
57
+ // `BOURDON_PROJECT_HINTS` env var (comma-separated) — see `loadProjectHints`.
58
+ const DEFAULT_PROJECT_HINTS = [
59
+ "Bourdon",
60
+ "Cursor",
61
+ "Copilot",
62
+ "Codex",
63
+ "Cascade",
64
+ ];
65
+ /** Minimal logger so a malformed-file warning is visible without a dep. */
66
+ const logger = {
67
+ warn(msg) {
68
+ // eslint-disable-next-line no-console
69
+ console.warn(`[bourdon.participants] ${msg}`);
70
+ },
71
+ };
72
+ // -- fs helpers ----------------------------------------------------------------
73
+ function isDir(p) {
74
+ try {
75
+ return statSync(p).isDirectory();
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function isFile(p) {
82
+ try {
83
+ return statSync(p).isFile();
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function homeDir() {
90
+ return process.env.HOME || process.env.USERPROFILE || ".";
91
+ }
92
+ function isRecord(v) {
93
+ return typeof v === "object" && v !== null && !Array.isArray(v);
94
+ }
95
+ /** Current UTC time at seconds precision + `+00:00` offset — mirrors Python's
96
+ * `datetime.now(timezone.utc).isoformat(timespec="seconds")`. */
97
+ function isoSeconds() {
98
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "+00:00");
99
+ }
100
+ // -- Desktop dir resolution ----------------------------------------------------
101
+ /**
102
+ * Resolve the Claude desktop application-support directory.
103
+ *
104
+ * Precedence:
105
+ * 1. `BOURDON_CLAUDE_DESKTOP_DIR` env var.
106
+ * 2. Platform default, anchored on `home` (defaults to homedir):
107
+ * * macOS -- `<home>/Library/Application Support/Claude`
108
+ * * Windows -- `%APPDATA%/Claude` (falls back to `<home>/AppData/Roaming/Claude`)
109
+ * * Linux -- `<home>/.config/Claude`
110
+ *
111
+ * Returns `null` only on an unrecognized platform with no env override.
112
+ */
113
+ function defaultClaudeDesktopDir(home) {
114
+ const env = process.env[DESKTOP_DIR_ENV];
115
+ if (env)
116
+ return env;
117
+ const base = home || homeDir();
118
+ const plat = process.platform;
119
+ if (plat === "darwin") {
120
+ return join(base, "Library", "Application Support", "Claude");
121
+ }
122
+ if (plat.startsWith("win")) {
123
+ // When an explicit home is provided (tests), keep everything under it;
124
+ // otherwise honor %APPDATA%.
125
+ if (home === undefined) {
126
+ const appdata = process.env.APPDATA;
127
+ if (appdata)
128
+ return join(appdata, "Claude");
129
+ }
130
+ return join(base, "AppData", "Roaming", "Claude");
131
+ }
132
+ if (plat.startsWith("linux")) {
133
+ return join(base, ".config", "Claude");
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Return every `local_*.json` under `<store_dir>/<acct>/<org>/`, globbing across
139
+ * all account/org UUID directories. Returns `[]` (never throws) when the store
140
+ * dir is absent so health checks can distinguish "missing" from "empty".
141
+ */
142
+ function iterStateFiles(storeDir) {
143
+ if (!isDir(storeDir))
144
+ return [];
145
+ const out = [];
146
+ let accounts;
147
+ try {
148
+ accounts = readdirSync(storeDir);
149
+ }
150
+ catch {
151
+ return [];
152
+ }
153
+ for (const acct of accounts) {
154
+ const acctDir = join(storeDir, acct);
155
+ if (!isDir(acctDir))
156
+ continue;
157
+ let orgs;
158
+ try {
159
+ orgs = readdirSync(acctDir);
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ for (const org of orgs) {
165
+ const orgDir = join(acctDir, org);
166
+ if (!isDir(orgDir))
167
+ continue;
168
+ let entries;
169
+ try {
170
+ entries = readdirSync(orgDir);
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ for (const name of entries) {
176
+ if (name.startsWith(STATE_PREFIX) && name.endsWith(".json")) {
177
+ out.push(join(orgDir, name));
178
+ }
179
+ }
180
+ }
181
+ }
182
+ out.sort();
183
+ return out;
184
+ }
185
+ /**
186
+ * Read + parse one `local_*.json` state file. Returns the parsed object, or
187
+ * `null` on any read/parse failure or if the top-level JSON is not an object.
188
+ * Never throws -- malformed files are counted and skipped by callers.
189
+ */
190
+ function loadStateJson(path) {
191
+ let raw;
192
+ try {
193
+ raw = readFileSync(path, "utf8");
194
+ }
195
+ catch (exc) {
196
+ logger.warn(`claude-desktop: cannot read ${path}: ${String(exc)}`);
197
+ return null;
198
+ }
199
+ let data;
200
+ try {
201
+ data = JSON.parse(raw);
202
+ }
203
+ catch (exc) {
204
+ logger.warn(`claude-desktop: cannot parse ${path}: ${String(exc)}`);
205
+ return null;
206
+ }
207
+ return isRecord(data) ? data : null;
208
+ }
209
+ // -- Scalar/date helpers -------------------------------------------------------
210
+ /**
211
+ * Convert an epoch timestamp to a UTC `YYYY-MM-DD` string. Accepts ms (`> 1e12`)
212
+ * or seconds; ints, floats, and numeric strings are tolerated. Returns `""` on
213
+ * anything unparseable (bool is rejected explicitly).
214
+ */
215
+ function epochToDate(value) {
216
+ if (typeof value === "boolean")
217
+ return "";
218
+ let num = null;
219
+ if (typeof value === "number") {
220
+ num = value;
221
+ }
222
+ else if (typeof value === "string" && value.trim()) {
223
+ const n = Number(value.trim());
224
+ num = Number.isNaN(n) ? null : n;
225
+ }
226
+ if (num === null || Number.isNaN(num))
227
+ return "";
228
+ const seconds = num > 1e12 ? num / 1000 : num;
229
+ try {
230
+ const d = new Date(seconds * 1000);
231
+ if (Number.isNaN(d.getTime()))
232
+ return "";
233
+ return d.toISOString().slice(0, 10);
234
+ }
235
+ catch {
236
+ return "";
237
+ }
238
+ }
239
+ /** Pick a session date from `createdAt` then `lastActivityAt`. */
240
+ function sessionDate(state) {
241
+ for (const key of ["createdAt", "lastActivityAt"]) {
242
+ const parsed = epochToDate(state[key]);
243
+ if (parsed)
244
+ return parsed;
245
+ }
246
+ return "";
247
+ }
248
+ /** Redact credential-like text + strip links, then cap to `limit` chars. */
249
+ function bounded(value, limit = 280) {
250
+ return redactText(value, limit);
251
+ }
252
+ /** Coerce a scalar to a bounded, redacted display string ("" if empty). */
253
+ function safeLabel(value, limit = 280) {
254
+ if (value === null || value === undefined)
255
+ return "";
256
+ const text = String(value).trim();
257
+ if (!text)
258
+ return "";
259
+ return bounded(text, limit);
260
+ }
261
+ /**
262
+ * Count `true` values in an `enabledMcpTools` mapping. Keys are
263
+ * `"<serverUUID>:<tool>"` and are NOT emitted -- only the count leaks.
264
+ */
265
+ function countEnabledMcpTools(enabled) {
266
+ if (!isRecord(enabled))
267
+ return 0;
268
+ let n = 0;
269
+ for (const v of Object.values(enabled)) {
270
+ if (v === true)
271
+ n += 1;
272
+ }
273
+ return n;
274
+ }
275
+ function basenameOf(pathValue) {
276
+ if (typeof pathValue !== "string" || !pathValue.trim())
277
+ return "";
278
+ const name = basename(pathValue.trim()).trim();
279
+ return name && name !== "." && name !== "/" && name !== "\\" ? name : "";
280
+ }
281
+ /**
282
+ * Infer project names from cwd + user-selected folders (basenames only), plus a
283
+ * project-hint substring match. Order-preserving + case-insensitively deduped.
284
+ * No file contents and no full paths are emitted.
285
+ */
286
+ function inferProjects(state) {
287
+ const projects = [];
288
+ const seen = new Set();
289
+ const add = (name) => {
290
+ const cleaned = name.trim();
291
+ if (cleaned && !seen.has(cleaned.toLowerCase())) {
292
+ projects.push(cleaned);
293
+ seen.add(cleaned.toLowerCase());
294
+ }
295
+ };
296
+ const pathStrings = [];
297
+ const cwd = state["cwd"];
298
+ if (typeof cwd === "string" && cwd.trim())
299
+ pathStrings.push(cwd);
300
+ const folders = state["userSelectedFolders"];
301
+ if (Array.isArray(folders)) {
302
+ for (const f of folders) {
303
+ if (typeof f === "string" && f.trim())
304
+ pathStrings.push(f);
305
+ }
306
+ }
307
+ for (const raw of pathStrings) {
308
+ const base = basenameOf(raw);
309
+ if (base)
310
+ add(base);
311
+ }
312
+ const haystack = pathStrings.join(" ").toLowerCase();
313
+ for (const hint of loadProjectHints(DEFAULT_PROJECT_HINTS)) {
314
+ if (haystack.includes(hint.toLowerCase()))
315
+ add(hint);
316
+ }
317
+ return projects.slice(0, MAX_PROJECTS);
318
+ }
319
+ // -- audit.jsonl safe-scalar extraction ----------------------------------------
320
+ /** Sibling transcript dir: `local_<id>.json` -> `local_<id>/`. */
321
+ function runDirFor(statePath) {
322
+ return join(dirname(statePath), parsePath(statePath).name);
323
+ }
324
+ /**
325
+ * Pull safe scalars from one audit record into `out` (in place). Only the
326
+ * `system`/`init` and `result` record types contribute; every other record
327
+ * (user/assistant/etc.) is ignored -- their bodies are never touched.
328
+ */
329
+ function absorbAuditRecord(record, out) {
330
+ const recType = record["type"];
331
+ const subtype = record["subtype"];
332
+ if (recType === "system" && subtype === "init") {
333
+ // Counts only -- never the tool/skill/command names themselves.
334
+ const pairs = [
335
+ ["tools", "init_tools"],
336
+ ["mcp_servers", "init_mcp_servers"],
337
+ ["skills", "init_skills"],
338
+ ["plugins", "init_plugins"],
339
+ ["slash_commands", "init_slash_commands"],
340
+ ];
341
+ for (const [srcKey, dstKey] of pairs) {
342
+ const value = record[srcKey];
343
+ if (Array.isArray(value))
344
+ out[dstKey] = value.length;
345
+ }
346
+ const version = record["claude_code_version"];
347
+ if (version !== null && version !== undefined) {
348
+ out["claude_code_version"] = safeLabel(version, 40);
349
+ }
350
+ return;
351
+ }
352
+ if (recType === "result") {
353
+ // The `result` *text* field (free-form summary) is deliberately NOT read.
354
+ for (const key of ["total_cost_usd", "num_turns", "is_error", "duration_ms"]) {
355
+ if (key in record)
356
+ out[key] = record[key];
357
+ }
358
+ const stopReason = record["stop_reason"];
359
+ if (stopReason !== null && stopReason !== undefined) {
360
+ out["stop_reason"] = safeLabel(stopReason, 40);
361
+ }
362
+ }
363
+ }
364
+ /**
365
+ * Extract ONLY the safe scalars from a Co-Work `audit.jsonl` transcript. Reads
366
+ * line-by-line, tolerantly. A missing, unreadable, or locked file yields an
367
+ * empty object -- never an error. NEVER reads user/assistant message bodies.
368
+ */
369
+ function readAuditScalars(runDir) {
370
+ const auditPath = join(runDir, "audit.jsonl");
371
+ if (!isFile(auditPath))
372
+ return {};
373
+ const out = {};
374
+ let raw;
375
+ try {
376
+ raw = readFileSync(auditPath, "utf8");
377
+ }
378
+ catch (exc) {
379
+ logger.warn(`claude-desktop: cannot read ${auditPath}: ${String(exc)}`);
380
+ return out;
381
+ }
382
+ let lineNo = 0;
383
+ for (const rawLine of raw.split(/\r?\n/)) {
384
+ if (lineNo >= MAX_AUDIT_LINES)
385
+ break;
386
+ lineNo += 1;
387
+ const line = rawLine.trim();
388
+ if (!line || line[0] !== "{")
389
+ continue;
390
+ let record;
391
+ try {
392
+ record = JSON.parse(line);
393
+ }
394
+ catch {
395
+ continue;
396
+ }
397
+ if (!isRecord(record))
398
+ continue;
399
+ absorbAuditRecord(record, out);
400
+ }
401
+ return out;
402
+ }
403
+ function runFromState(statePath, state) {
404
+ const audit = readAuditScalars(runDirFor(statePath));
405
+ const title = safeLabel(state["title"], 160) || "(untitled run)";
406
+ const model = safeLabel(state["model"], 80);
407
+ const permissionMode = safeLabel(state["permissionMode"], 40);
408
+ const numTurnsRaw = audit["num_turns"];
409
+ const costRaw = audit["total_cost_usd"];
410
+ const isErrorRaw = audit["is_error"];
411
+ const initCounts = {};
412
+ for (const [k, v] of Object.entries(audit)) {
413
+ if (k.startsWith("init_") && typeof v === "number" && Number.isInteger(v)) {
414
+ initCounts[k] = v;
415
+ }
416
+ }
417
+ const stem = parsePath(statePath).name;
418
+ const sid = state["sessionId"];
419
+ return {
420
+ runId: String(sid || stem),
421
+ date: sessionDate(state),
422
+ cwd: safeLabel(state["cwd"], 300),
423
+ title,
424
+ model,
425
+ permissionMode,
426
+ isArchived: Boolean(state["isArchived"]),
427
+ mcpToolCount: countEnabledMcpTools(state["enabledMcpTools"]),
428
+ projects: inferProjects(state),
429
+ numTurns: typeof numTurnsRaw === "number" && Number.isInteger(numTurnsRaw) ? numTurnsRaw : null,
430
+ totalCostUsd: typeof costRaw === "number" ? costRaw : null,
431
+ isError: typeof isErrorRaw === "boolean" ? isErrorRaw : null,
432
+ initCounts,
433
+ };
434
+ }
435
+ /**
436
+ * Build the bounded, redacted key-action list for a run:
437
+ * `[title, "model: ...", "permission: ...", "<n> turns", "$<cost>", "error",
438
+ * "mcp-tools: <n>"]` -- all metadata, never content.
439
+ */
440
+ function keyActions(run) {
441
+ const actions = [bounded(run.title, 160)];
442
+ if (run.model)
443
+ actions.push(bounded(`model: ${run.model}`, 120));
444
+ if (run.permissionMode)
445
+ actions.push(bounded(`permission: ${run.permissionMode}`, 80));
446
+ if (run.numTurns !== null)
447
+ actions.push(`${run.numTurns} turns`);
448
+ if (run.totalCostUsd !== null)
449
+ actions.push(`$${run.totalCostUsd.toFixed(2)}`);
450
+ if (run.isError)
451
+ actions.push("error");
452
+ if (run.mcpToolCount)
453
+ actions.push(`mcp-tools: ${run.mcpToolCount}`);
454
+ return actions.slice(0, MAX_KEY_ACTIONS);
455
+ }
456
+ function sessionFromRun(run) {
457
+ return makeSession({
458
+ date: run.date,
459
+ cwd: run.cwd || undefined, // run.cwd or None -- privacy: never empty string
460
+ project_focus: [...run.projects],
461
+ key_actions: keyActions(run),
462
+ files_touched: [], // never list user files -- privacy
463
+ visibility: Visibility.TEAM,
464
+ });
465
+ }
466
+ /** Manifest-level capability *counts* only (no names). */
467
+ function capabilities(runs) {
468
+ const caps = [AGENT_ID];
469
+ const maxMcp = runs.length ? Math.max(...runs.map((r) => r.mcpToolCount)) : 0;
470
+ caps.push(`mcp-tools:${maxMcp}`);
471
+ const bestTools = runs.length ? Math.max(...runs.map((r) => r.initCounts["init_tools"] ?? 0)) : 0;
472
+ if (bestTools)
473
+ caps.push(`tools:${bestTools}`);
474
+ const bestSkills = runs.length
475
+ ? Math.max(...runs.map((r) => r.initCounts["init_skills"] ?? 0))
476
+ : 0;
477
+ if (bestSkills)
478
+ caps.push(`skills:${bestSkills}`);
479
+ return caps;
480
+ }
481
+ function entitiesFromRuns(runs) {
482
+ const lastSeen = runs.length
483
+ ? runs.map((r) => r.date).reduce((a, b) => (a > b ? a : b))
484
+ : null;
485
+ const entities = new Map();
486
+ entities.set(SURFACE_ENTITY_NAME, makeEntity({
487
+ name: SURFACE_ENTITY_NAME,
488
+ type: "agent-surface",
489
+ summary: bounded("Claude desktop app Co-Work / local-agent mode surface " +
490
+ "(metadata-only federation).", 260),
491
+ last_touched: lastSeen,
492
+ tags: ["claude-desktop", "claude-desktop-cowork", "agent-surface"],
493
+ visibility: Visibility.TEAM,
494
+ }));
495
+ for (const run of runs) {
496
+ for (const project of run.projects) {
497
+ if (!entities.has(project)) {
498
+ entities.set(project, makeEntity({
499
+ name: project,
500
+ type: "project",
501
+ summary: "Project inferred from a Claude Desktop Co-Work run cwd.",
502
+ last_touched: run.date || undefined, // run.date or None
503
+ tags: ["claude-desktop", "claude-desktop-cowork", "project"],
504
+ visibility: Visibility.TEAM,
505
+ }));
506
+ }
507
+ }
508
+ }
509
+ return [...entities.values()];
510
+ }
511
+ // -- Participant ---------------------------------------------------------------
512
+ /** External participant for the Claude desktop app's Co-Work surface. */
513
+ export class ClaudeDesktopCoworkParticipant {
514
+ agentId = AGENT_ID;
515
+ agentType = AGENT_TYPE;
516
+ displayName = DISPLAY_NAME;
517
+ storeDir;
518
+ policy;
519
+ /**
520
+ * The Co-Work sub-store dir the setup wizard probes for presence. Resolves to
521
+ * `<desktop>/local-agent-mode-sessions`. When the desktop dir cannot be
522
+ * resolved on this platform, falls back to a non-existent sentinel under
523
+ * `home` so the wizard reports "not found" rather than crashing.
524
+ */
525
+ static defaultNativePath(home) {
526
+ const desktop = defaultClaudeDesktopDir(home);
527
+ if (desktop === null)
528
+ return join(home || homeDir(), "Claude", COWORK_STORE);
529
+ return join(desktop, COWORK_STORE);
530
+ }
531
+ constructor(storeDir, home) {
532
+ this.storeDir = storeDir || ClaudeDesktopCoworkParticipant.defaultNativePath(home);
533
+ this.policy = DEFAULT_POLICY;
534
+ }
535
+ get nativePath() {
536
+ return this.storeDir;
537
+ }
538
+ // -- Protocol surface --------------------------------------------------------
539
+ discover() {
540
+ if (!isDir(this.storeDir)) {
541
+ throw new ParticipantDiscoveryError(`Claude Desktop Co-Work store not found at ${this.storeDir}.`);
542
+ }
543
+ const stateFiles = iterStateFiles(this.storeDir);
544
+ return {
545
+ path: this.storeDir,
546
+ version: "unknown",
547
+ metadata: { runs: stateFiles.length },
548
+ };
549
+ }
550
+ exportSessions(since, limit = 100) {
551
+ const runs = this.runs(since);
552
+ const sessions = runs.map(sessionFromRun);
553
+ sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
554
+ return sessions.slice(0, limit);
555
+ }
556
+ exportL5(since) {
557
+ if (!isDir(this.storeDir)) {
558
+ throw new ParticipantDiscoveryError(`Claude Desktop Co-Work store not found at ${this.storeDir}.`);
559
+ }
560
+ const runs = this.runs(since);
561
+ const sessions = runs.map(sessionFromRun);
562
+ sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
563
+ const entities = entitiesFromRuns(runs);
564
+ const visibleEntities = filterForFederation(entities, this.policy);
565
+ return makeManifest({
566
+ spec_version: SPEC_VERSION,
567
+ agent: makeAgentInfo({
568
+ id: AGENT_ID,
569
+ type: AGENT_TYPE,
570
+ instance: hostname(),
571
+ spec_version_compat: SPEC_VERSION,
572
+ role_narrative: ROLE_NARRATIVE,
573
+ }),
574
+ last_updated: isoSeconds(),
575
+ capabilities: capabilities(runs),
576
+ recent_sessions: sessions,
577
+ known_entities: visibleEntities,
578
+ visibility_policy: this.policy,
579
+ });
580
+ }
581
+ healthCheck() {
582
+ if (!isDir(this.storeDir)) {
583
+ return {
584
+ status: "blocked",
585
+ reason: `Claude Desktop Co-Work store not found at ${this.storeDir}.`,
586
+ details: { store_dir: this.storeDir },
587
+ proposedFix: "Install the Claude desktop app and run a Co-Work / local-agent " +
588
+ "session once. Set BOURDON_CLAUDE_DESKTOP_DIR if the app stores " +
589
+ "state in a non-standard location.",
590
+ };
591
+ }
592
+ let stateFiles;
593
+ let runs;
594
+ let malformed;
595
+ try {
596
+ stateFiles = iterStateFiles(this.storeDir);
597
+ [runs, malformed] = this.collectRuns();
598
+ }
599
+ catch (exc) {
600
+ logger.warn(`ClaudeDesktopCoworkParticipant health_check failed: ${String(exc)}`);
601
+ return {
602
+ status: "degraded",
603
+ reason: "Co-Work store present but extraction failed.",
604
+ details: { error: String(exc) },
605
+ proposedFix: "Close the Claude desktop app (its state files may be locked) and " +
606
+ "re-run `bourdon claude-desktop-cowork export`.",
607
+ };
608
+ }
609
+ if (stateFiles.length === 0) {
610
+ return {
611
+ status: "degraded",
612
+ reason: "No Co-Work runs found under the store directory.",
613
+ details: { store_dir: this.storeDir },
614
+ proposedFix: "Run a Co-Work / local-agent session in the Claude desktop app, " +
615
+ "then re-run `bourdon claude-desktop-cowork export`.",
616
+ };
617
+ }
618
+ return {
619
+ status: "ok",
620
+ details: {
621
+ store_dir: this.storeDir,
622
+ run_count: stateFiles.length,
623
+ runs_extracted: runs.length,
624
+ malformed_records: malformed,
625
+ runs_with_scalars: runs.filter((r) => r.numTurns !== null).length,
626
+ },
627
+ };
628
+ }
629
+ // -- Internal ----------------------------------------------------------------
630
+ collectRuns() {
631
+ const runs = [];
632
+ let malformed = 0;
633
+ for (const statePath of iterStateFiles(this.storeDir)) {
634
+ const state = loadStateJson(statePath);
635
+ if (state === null) {
636
+ malformed += 1;
637
+ continue;
638
+ }
639
+ runs.push(runFromState(statePath, state));
640
+ }
641
+ return [runs, malformed];
642
+ }
643
+ runs(since) {
644
+ const [runs] = this.collectRuns();
645
+ let filtered = runs;
646
+ if (since !== undefined) {
647
+ const cutoff = new Date(since.getTime()).toISOString().slice(0, 10);
648
+ filtered = runs.filter((run) => !run.date || run.date >= cutoff);
649
+ }
650
+ filtered.sort((a, b) => {
651
+ if (a.date !== b.date)
652
+ return a.date < b.date ? 1 : -1;
653
+ if (a.runId !== b.runId)
654
+ return a.runId < b.runId ? 1 : -1;
655
+ return 0;
656
+ });
657
+ return filtered;
658
+ }
659
+ }