@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.
- package/dist/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.d.ts +42 -1083
- package/dist/index.js +53 -6590
- package/dist/network-base.d.ts +108 -0
- package/dist/network-base.js +248 -0
- package/dist/readers/cascade.d.ts +49 -0
- package/dist/readers/cascade.js +428 -0
- package/dist/readers/claude-code-automations.d.ts +62 -0
- package/dist/readers/claude-code-automations.js +693 -0
- package/dist/readers/claude-code.d.ts +50 -0
- package/dist/readers/claude-code.js +583 -0
- package/dist/readers/claude-desktop-code.d.ts +54 -0
- package/dist/readers/claude-desktop-code.js +526 -0
- package/dist/readers/claude-desktop-cowork.d.ts +47 -0
- package/dist/readers/claude-desktop-cowork.js +659 -0
- package/dist/readers/codex-automations.d.ts +55 -0
- package/dist/readers/codex-automations.js +519 -0
- package/dist/readers/codex-sessions.d.ts +88 -0
- package/dist/readers/codex-sessions.js +194 -0
- package/dist/readers/copilot-automations.d.ts +64 -0
- package/dist/readers/copilot-automations.js +530 -0
- package/dist/readers/copilot-cli.d.ts +45 -0
- package/dist/readers/copilot-cli.js +416 -0
- package/dist/readers/copilot-vscode.d.ts +54 -0
- package/dist/readers/copilot-vscode.js +553 -0
- package/dist/readers/copilot.d.ts +59 -0
- package/dist/readers/copilot.js +413 -0
- package/dist/readers/cursor-automations.d.ts +57 -0
- package/dist/readers/cursor-automations.js +685 -0
- package/dist/readers/cursor.d.ts +50 -0
- package/dist/readers/cursor.js +670 -0
- package/dist/readers/github-copilot.d.ts +34 -0
- package/dist/readers/github-copilot.js +242 -0
- package/dist/readers/hermes.d.ts +37 -0
- package/dist/readers/hermes.js +426 -0
- package/dist/readers/openclaw.d.ts +78 -0
- package/dist/readers/openclaw.js +386 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +98 -0
- package/dist/sqlite-base.d.ts +65 -0
- package/dist/sqlite-base.js +163 -0
- package/package.json +6 -6
|
@@ -0,0 +1,526 @@
|
|
|
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 { readdirSync, readFileSync, statSync } from "node:fs";
|
|
20
|
+
import { homedir, hostname } from "node:os";
|
|
21
|
+
import { basename, join, parse as parsePath } from "node:path";
|
|
22
|
+
import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
|
|
23
|
+
import { redactText } from "@getbourdon/redaction";
|
|
24
|
+
import { ParticipantDiscoveryError, SPEC_VERSION, loadProjectHints, } from "../base.js";
|
|
25
|
+
// -- Constants ----------------------------------------------------------------
|
|
26
|
+
const AGENT_ID = "claude-desktop-code";
|
|
27
|
+
const AGENT_TYPE = "code-assistant";
|
|
28
|
+
const DISPLAY_NAME = "Claude Desktop · Code";
|
|
29
|
+
const SURFACE_ENTITY_NAME = "Claude Desktop Code";
|
|
30
|
+
const ROLE_NARRATIVE = "Claude desktop app, GUI Claude Code. Bourdon reads the metadata-only " +
|
|
31
|
+
"per-conversation local state to surface recognition metadata -- title, " +
|
|
32
|
+
"project, model, effort, capability counts -- never conversation content " +
|
|
33
|
+
"-- so desktop Claude Code work is visible to other agents.";
|
|
34
|
+
// Environment override (tests + non-standard installs). Takes precedence over
|
|
35
|
+
// the platform default but not over an explicit `home` argument.
|
|
36
|
+
const DESKTOP_DIR_ENV = "BOURDON_CLAUDE_DESKTOP_DIR";
|
|
37
|
+
// Sub-store directory name under the desktop dir.
|
|
38
|
+
const CODE_STORE = "claude-code-sessions";
|
|
39
|
+
// State filename prefix shared by both desktop stores (`local_<uuid>.json`).
|
|
40
|
+
const STATE_PREFIX = "local_";
|
|
41
|
+
const STATE_FILE_RE = /^local_.*\.json$/;
|
|
42
|
+
// Caps -- keep emitted metadata small and bounded.
|
|
43
|
+
const MAX_KEY_ACTION_CHARS = 280;
|
|
44
|
+
const MAX_PROJECTS = 6;
|
|
45
|
+
const MAX_KEY_ACTIONS = 6;
|
|
46
|
+
// Product-neutral defaults only: this product plus the agent surfaces Bourdon
|
|
47
|
+
// federates. A deployment adds its own project roster via the
|
|
48
|
+
// `BOURDON_PROJECT_HINTS` env var (comma-separated) — see `loadProjectHints`.
|
|
49
|
+
const DEFAULT_PROJECT_HINTS = [
|
|
50
|
+
"Bourdon",
|
|
51
|
+
"Cursor",
|
|
52
|
+
"Copilot",
|
|
53
|
+
"Codex",
|
|
54
|
+
"Cascade",
|
|
55
|
+
];
|
|
56
|
+
const DEFAULT_POLICY = makeVisibilityPolicy({
|
|
57
|
+
default: Visibility.TEAM,
|
|
58
|
+
private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
|
|
59
|
+
team_tags: ["claude-desktop", "claude-desktop-code", "agent-surface", "workspace"],
|
|
60
|
+
});
|
|
61
|
+
// -- fs helpers ---------------------------------------------------------------
|
|
62
|
+
function isDir(p) {
|
|
63
|
+
try {
|
|
64
|
+
return statSync(p).isDirectory();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// -- Desktop-dir resolution (`_claude_desktop.default_claude_desktop_dir`) -----
|
|
71
|
+
function homeBase(home) {
|
|
72
|
+
// Mirror Python's `home or Path.home()`. os.homedir() can throw under some
|
|
73
|
+
// sandboxes; stay defensive so import + construction never crash.
|
|
74
|
+
if (home !== undefined)
|
|
75
|
+
return home;
|
|
76
|
+
try {
|
|
77
|
+
return homedir();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return process.env.HOME || process.env.USERPROFILE || ".";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the Claude desktop application-support directory. Returns `null` only
|
|
85
|
+
* on an unrecognized platform with no env override (callers treat that as
|
|
86
|
+
* "blocked" rather than crashing).
|
|
87
|
+
*
|
|
88
|
+
* Precedence: `BOURDON_CLAUDE_DESKTOP_DIR` env → platform default anchored on
|
|
89
|
+
* `home` (defaults to homedir).
|
|
90
|
+
*/
|
|
91
|
+
export function defaultClaudeDesktopDir(home) {
|
|
92
|
+
const env = process.env[DESKTOP_DIR_ENV];
|
|
93
|
+
if (env)
|
|
94
|
+
return env;
|
|
95
|
+
const base = homeBase(home);
|
|
96
|
+
if (process.platform === "darwin") {
|
|
97
|
+
return join(base, "Library", "Application Support", "Claude");
|
|
98
|
+
}
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
// When an explicit home is provided (tests), keep everything under it so the
|
|
101
|
+
// fake tree is self-contained; otherwise honor %APPDATA%.
|
|
102
|
+
if (home === undefined) {
|
|
103
|
+
const appdata = process.env.APPDATA;
|
|
104
|
+
if (appdata)
|
|
105
|
+
return join(appdata, "Claude");
|
|
106
|
+
}
|
|
107
|
+
return join(base, "AppData", "Roaming", "Claude");
|
|
108
|
+
}
|
|
109
|
+
if (process.platform === "linux") {
|
|
110
|
+
return join(base, ".config", "Claude");
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// -- State-file globbing (`_claude_desktop.iter_state_files`) ------------------
|
|
115
|
+
/**
|
|
116
|
+
* Return every `local_*.json` under `<store_dir>/<acct>/<org>/`, sorted by path.
|
|
117
|
+
* Never raises when the store dir is absent (returns `[]`) so health checks can
|
|
118
|
+
* distinguish "store missing" from "store empty".
|
|
119
|
+
*/
|
|
120
|
+
function iterStateFiles(storeDir) {
|
|
121
|
+
if (!isDir(storeDir))
|
|
122
|
+
return [];
|
|
123
|
+
const out = [];
|
|
124
|
+
let accts;
|
|
125
|
+
try {
|
|
126
|
+
accts = readdirSync(storeDir);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
for (const acct of accts) {
|
|
132
|
+
const acctDir = join(storeDir, acct);
|
|
133
|
+
if (!isDir(acctDir))
|
|
134
|
+
continue;
|
|
135
|
+
let orgs;
|
|
136
|
+
try {
|
|
137
|
+
orgs = readdirSync(acctDir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const org of orgs) {
|
|
143
|
+
const orgDir = join(acctDir, org);
|
|
144
|
+
if (!isDir(orgDir))
|
|
145
|
+
continue;
|
|
146
|
+
let names;
|
|
147
|
+
try {
|
|
148
|
+
names = readdirSync(orgDir);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const name of names) {
|
|
154
|
+
if (name.startsWith(STATE_PREFIX) && STATE_FILE_RE.test(name)) {
|
|
155
|
+
out.push(join(orgDir, name));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
out.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read + parse one `local_*.json` state file. Returns the parsed object, or
|
|
165
|
+
* `null` on any read/parse failure or if the top-level JSON is not an object.
|
|
166
|
+
* Never raises — malformed files are counted and skipped by callers.
|
|
167
|
+
*/
|
|
168
|
+
function loadStateJson(path) {
|
|
169
|
+
let raw;
|
|
170
|
+
try {
|
|
171
|
+
raw = readFileSync(path, "utf8");
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
let data;
|
|
177
|
+
try {
|
|
178
|
+
data = JSON.parse(raw);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
184
|
+
return null;
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
// -- Scalar helpers -----------------------------------------------------------
|
|
188
|
+
/**
|
|
189
|
+
* Convert an epoch timestamp to a UTC `YYYY-MM-DD` string. Accepts ms (`> 1e12`)
|
|
190
|
+
* or seconds; ints, floats, and numeric strings are tolerated. Returns `""` on
|
|
191
|
+
* anything unparseable (bools are rejected explicitly, as in Python).
|
|
192
|
+
*/
|
|
193
|
+
function epochToDate(value) {
|
|
194
|
+
if (typeof value === "boolean")
|
|
195
|
+
return ""; // bool is an int subclass in Py — reject
|
|
196
|
+
let num = null;
|
|
197
|
+
if (typeof value === "number") {
|
|
198
|
+
num = value;
|
|
199
|
+
}
|
|
200
|
+
else if (typeof value === "string" && value.trim()) {
|
|
201
|
+
const parsed = Number(value.trim());
|
|
202
|
+
num = Number.isNaN(parsed) ? null : parsed;
|
|
203
|
+
}
|
|
204
|
+
if (num === null || !Number.isFinite(num))
|
|
205
|
+
return "";
|
|
206
|
+
const seconds = num > 1e12 ? num / 1000 : num;
|
|
207
|
+
try {
|
|
208
|
+
const d = new Date(seconds * 1000);
|
|
209
|
+
const iso = d.toISOString();
|
|
210
|
+
return iso.slice(0, 10);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Pick a session date from `createdAt` then `lastActivityAt`. */
|
|
217
|
+
function sessionDate(state) {
|
|
218
|
+
for (const key of ["createdAt", "lastActivityAt"]) {
|
|
219
|
+
const parsed = epochToDate(state[key]);
|
|
220
|
+
if (parsed)
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Redact credential-like text + strip links, then cap to `limit` chars. Runs
|
|
227
|
+
* through the redaction SSOT first so a planted secret becomes the sentinel
|
|
228
|
+
* *before* truncation can split it. Mirrors `_safe_native_memory_text(v, limit)`.
|
|
229
|
+
*/
|
|
230
|
+
function bounded(value, limit = MAX_KEY_ACTION_CHARS) {
|
|
231
|
+
return redactText(value, limit);
|
|
232
|
+
}
|
|
233
|
+
/** Coerce a scalar to a bounded, redacted display string ("" if empty). */
|
|
234
|
+
function safeLabel(value, limit = MAX_KEY_ACTION_CHARS) {
|
|
235
|
+
if (value === null || value === undefined)
|
|
236
|
+
return "";
|
|
237
|
+
const text = String(value).trim();
|
|
238
|
+
if (!text)
|
|
239
|
+
return "";
|
|
240
|
+
return bounded(text, limit);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Count `true` values in an `enabledMcpTools` mapping. Keys are
|
|
244
|
+
* `"<serverUUID>:<tool>"` and are NOT emitted — only the count of enabled
|
|
245
|
+
* entries is surfaced, so no tool names or server identifiers leak.
|
|
246
|
+
*/
|
|
247
|
+
function countEnabledMcpTools(enabled) {
|
|
248
|
+
if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled))
|
|
249
|
+
return 0;
|
|
250
|
+
let count = 0;
|
|
251
|
+
for (const v of Object.values(enabled)) {
|
|
252
|
+
if (v === true)
|
|
253
|
+
count += 1;
|
|
254
|
+
}
|
|
255
|
+
return count;
|
|
256
|
+
}
|
|
257
|
+
function baseName(pathValue) {
|
|
258
|
+
if (typeof pathValue !== "string" || !pathValue.trim())
|
|
259
|
+
return "";
|
|
260
|
+
const name = basename(pathValue.trim()).trim();
|
|
261
|
+
return name && name !== "." && name !== "/" && name !== "\\" ? name : "";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Infer project names from cwd + user-selected folders (basenames only), plus a
|
|
265
|
+
* project-hint substring match. No file contents / no full paths — only
|
|
266
|
+
* directory basenames and recognized labels. Order-preserving + de-duplicated.
|
|
267
|
+
*/
|
|
268
|
+
function inferProjects(state) {
|
|
269
|
+
const projects = [];
|
|
270
|
+
const seen = new Set();
|
|
271
|
+
const add = (name) => {
|
|
272
|
+
const cleaned = name.trim();
|
|
273
|
+
if (cleaned && !seen.has(cleaned.toLowerCase())) {
|
|
274
|
+
projects.push(cleaned);
|
|
275
|
+
seen.add(cleaned.toLowerCase());
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const pathStrings = [];
|
|
279
|
+
const cwd = state["cwd"];
|
|
280
|
+
if (typeof cwd === "string" && cwd.trim())
|
|
281
|
+
pathStrings.push(cwd);
|
|
282
|
+
const folders = state["userSelectedFolders"];
|
|
283
|
+
if (Array.isArray(folders)) {
|
|
284
|
+
for (const f of folders) {
|
|
285
|
+
if (typeof f === "string" && f.trim())
|
|
286
|
+
pathStrings.push(f);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const rawPath of pathStrings) {
|
|
290
|
+
const base = baseName(rawPath);
|
|
291
|
+
if (base)
|
|
292
|
+
add(base);
|
|
293
|
+
}
|
|
294
|
+
const haystack = pathStrings.join(" ").toLowerCase();
|
|
295
|
+
for (const hint of loadProjectHints(DEFAULT_PROJECT_HINTS)) {
|
|
296
|
+
if (haystack.includes(hint.toLowerCase()))
|
|
297
|
+
add(hint);
|
|
298
|
+
}
|
|
299
|
+
return projects.slice(0, MAX_PROJECTS);
|
|
300
|
+
}
|
|
301
|
+
function conversationFromState(statePath, state) {
|
|
302
|
+
const sessionId = state["sessionId"];
|
|
303
|
+
const stem = parsePath(statePath).name;
|
|
304
|
+
return {
|
|
305
|
+
convId: String(sessionId || stem),
|
|
306
|
+
date: sessionDate(state),
|
|
307
|
+
cwd: safeLabel(state["cwd"], 300),
|
|
308
|
+
title: safeLabel(state["title"], 160) || "(untitled conversation)",
|
|
309
|
+
model: safeLabel(state["model"], 80),
|
|
310
|
+
effort: safeLabel(state["effort"], 40),
|
|
311
|
+
permissionMode: safeLabel(state["permissionMode"], 40),
|
|
312
|
+
isArchived: Boolean(state["isArchived"]),
|
|
313
|
+
mcpToolCount: countEnabledMcpTools(state["enabledMcpTools"]),
|
|
314
|
+
projects: inferProjects(state),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function keyActions(conv) {
|
|
318
|
+
const actions = [bounded(conv.title, 160)];
|
|
319
|
+
if (conv.model)
|
|
320
|
+
actions.push(bounded(`model: ${conv.model}`, 120));
|
|
321
|
+
if (conv.effort)
|
|
322
|
+
actions.push(bounded(`effort: ${conv.effort}`, 60));
|
|
323
|
+
if (conv.permissionMode)
|
|
324
|
+
actions.push(bounded(`permission: ${conv.permissionMode}`, 80));
|
|
325
|
+
if (conv.mcpToolCount)
|
|
326
|
+
actions.push(`mcp-tools: ${conv.mcpToolCount}`);
|
|
327
|
+
return actions.slice(0, MAX_KEY_ACTIONS);
|
|
328
|
+
}
|
|
329
|
+
function sessionFromConversation(conv) {
|
|
330
|
+
return makeSession({
|
|
331
|
+
date: conv.date,
|
|
332
|
+
cwd: conv.cwd || undefined,
|
|
333
|
+
project_focus: [...conv.projects],
|
|
334
|
+
key_actions: keyActions(conv),
|
|
335
|
+
files_touched: [], // never list user files -- privacy
|
|
336
|
+
visibility: Visibility.TEAM,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function capabilities(convs) {
|
|
340
|
+
let maxMcp = 0;
|
|
341
|
+
for (const conv of convs)
|
|
342
|
+
if (conv.mcpToolCount > maxMcp)
|
|
343
|
+
maxMcp = conv.mcpToolCount;
|
|
344
|
+
return [AGENT_ID, `mcp-tools:${maxMcp}`];
|
|
345
|
+
}
|
|
346
|
+
function entitiesFromConversations(convs) {
|
|
347
|
+
let lastSeen;
|
|
348
|
+
for (const conv of convs) {
|
|
349
|
+
if (lastSeen === undefined || conv.date > lastSeen)
|
|
350
|
+
lastSeen = conv.date;
|
|
351
|
+
}
|
|
352
|
+
const entities = new Map();
|
|
353
|
+
entities.set(SURFACE_ENTITY_NAME, makeEntity({
|
|
354
|
+
name: SURFACE_ENTITY_NAME,
|
|
355
|
+
type: "agent-surface",
|
|
356
|
+
summary: bounded("Claude desktop app GUI Claude Code surface (metadata-only federation).", 260),
|
|
357
|
+
last_touched: lastSeen,
|
|
358
|
+
tags: ["claude-desktop", "claude-desktop-code", "agent-surface"],
|
|
359
|
+
visibility: Visibility.TEAM,
|
|
360
|
+
}));
|
|
361
|
+
for (const conv of convs) {
|
|
362
|
+
for (const project of conv.projects) {
|
|
363
|
+
if (!entities.has(project)) {
|
|
364
|
+
entities.set(project, makeEntity({
|
|
365
|
+
name: project,
|
|
366
|
+
type: "project",
|
|
367
|
+
summary: "Project inferred from a Claude Desktop Code conversation cwd.",
|
|
368
|
+
last_touched: conv.date || undefined,
|
|
369
|
+
tags: ["claude-desktop", "claude-desktop-code", "project"],
|
|
370
|
+
visibility: Visibility.TEAM,
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return [...entities.values()];
|
|
376
|
+
}
|
|
377
|
+
// -- Participant --------------------------------------------------------------
|
|
378
|
+
/**
|
|
379
|
+
* External participant for the Claude desktop app's GUI Claude Code surface.
|
|
380
|
+
* File/JSON reader — metadata-only, no transcript, no SQLite, no network.
|
|
381
|
+
*/
|
|
382
|
+
export class ClaudeDesktopCodeParticipant {
|
|
383
|
+
agentId = AGENT_ID;
|
|
384
|
+
agentType = AGENT_TYPE;
|
|
385
|
+
displayName = DISPLAY_NAME;
|
|
386
|
+
storeDir;
|
|
387
|
+
policy;
|
|
388
|
+
/**
|
|
389
|
+
* The Claude Code sub-store dir the setup wizard probes for presence. Resolves
|
|
390
|
+
* to `<desktop>/claude-code-sessions`. Falls back to a non-existent sentinel
|
|
391
|
+
* under `home` on an unrecognized platform so the wizard reports "not found".
|
|
392
|
+
*/
|
|
393
|
+
static defaultNativePath(home) {
|
|
394
|
+
const desktop = defaultClaudeDesktopDir(home);
|
|
395
|
+
if (desktop === null)
|
|
396
|
+
return join(homeBase(home), "Claude", CODE_STORE);
|
|
397
|
+
return join(desktop, CODE_STORE);
|
|
398
|
+
}
|
|
399
|
+
constructor(storeDir, home) {
|
|
400
|
+
this.storeDir = storeDir ?? ClaudeDesktopCodeParticipant.defaultNativePath(home);
|
|
401
|
+
this.policy = DEFAULT_POLICY;
|
|
402
|
+
}
|
|
403
|
+
get nativePath() {
|
|
404
|
+
return this.storeDir;
|
|
405
|
+
}
|
|
406
|
+
discover() {
|
|
407
|
+
if (!isDir(this.storeDir)) {
|
|
408
|
+
throw new ParticipantDiscoveryError(`Claude Desktop Code store not found at ${this.storeDir}.`);
|
|
409
|
+
}
|
|
410
|
+
const stateFiles = iterStateFiles(this.storeDir);
|
|
411
|
+
return {
|
|
412
|
+
path: this.storeDir,
|
|
413
|
+
version: "unknown",
|
|
414
|
+
metadata: { conversations: stateFiles.length },
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
exportSessions(since, limit = 100) {
|
|
418
|
+
const convs = this.conversations(since);
|
|
419
|
+
const sessions = convs.map(sessionFromConversation);
|
|
420
|
+
sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
421
|
+
return sessions.slice(0, limit);
|
|
422
|
+
}
|
|
423
|
+
exportL5(since) {
|
|
424
|
+
if (!isDir(this.storeDir)) {
|
|
425
|
+
throw new ParticipantDiscoveryError(`Claude Desktop Code store not found at ${this.storeDir}.`);
|
|
426
|
+
}
|
|
427
|
+
const convs = this.conversations(since);
|
|
428
|
+
const sessions = convs.map(sessionFromConversation);
|
|
429
|
+
sessions.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
430
|
+
const entities = entitiesFromConversations(convs);
|
|
431
|
+
const visibleEntities = filterForFederation(entities, this.policy);
|
|
432
|
+
return makeManifest({
|
|
433
|
+
spec_version: SPEC_VERSION,
|
|
434
|
+
agent: makeAgentInfo({
|
|
435
|
+
id: AGENT_ID,
|
|
436
|
+
type: AGENT_TYPE,
|
|
437
|
+
instance: hostname(),
|
|
438
|
+
spec_version_compat: SPEC_VERSION,
|
|
439
|
+
role_narrative: ROLE_NARRATIVE,
|
|
440
|
+
}),
|
|
441
|
+
last_updated: new Date().toISOString(),
|
|
442
|
+
capabilities: capabilities(convs),
|
|
443
|
+
recent_sessions: sessions,
|
|
444
|
+
known_entities: visibleEntities,
|
|
445
|
+
visibility_policy: this.policy,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
healthCheck() {
|
|
449
|
+
if (!isDir(this.storeDir)) {
|
|
450
|
+
return {
|
|
451
|
+
status: "blocked",
|
|
452
|
+
reason: `Claude Desktop Code store not found at ${this.storeDir}.`,
|
|
453
|
+
details: { store_dir: this.storeDir },
|
|
454
|
+
proposedFix: "Install the Claude desktop app and open a Claude Code conversation " +
|
|
455
|
+
"once. Set BOURDON_CLAUDE_DESKTOP_DIR if the app stores state in a " +
|
|
456
|
+
"non-standard location.",
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
let stateFiles;
|
|
460
|
+
let convs;
|
|
461
|
+
let malformed;
|
|
462
|
+
try {
|
|
463
|
+
stateFiles = iterStateFiles(this.storeDir);
|
|
464
|
+
const collected = this.collectConversations();
|
|
465
|
+
convs = collected.convs;
|
|
466
|
+
malformed = collected.malformed;
|
|
467
|
+
}
|
|
468
|
+
catch (exc) {
|
|
469
|
+
return {
|
|
470
|
+
status: "degraded",
|
|
471
|
+
reason: "Code store present but extraction failed.",
|
|
472
|
+
details: { error: String(exc) },
|
|
473
|
+
proposedFix: "Close the Claude desktop app (its state files may be locked) and " +
|
|
474
|
+
"re-run `bourdon claude-desktop-code export`.",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (stateFiles.length === 0) {
|
|
478
|
+
return {
|
|
479
|
+
status: "degraded",
|
|
480
|
+
reason: "No Claude Code conversations found under the store directory.",
|
|
481
|
+
details: { store_dir: this.storeDir },
|
|
482
|
+
proposedFix: "Open a Claude Code conversation in the Claude desktop app, then " +
|
|
483
|
+
"re-run `bourdon claude-desktop-code export`.",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
status: "ok",
|
|
488
|
+
details: {
|
|
489
|
+
store_dir: this.storeDir,
|
|
490
|
+
conversation_count: stateFiles.length,
|
|
491
|
+
conversations_extracted: convs.length,
|
|
492
|
+
malformed_records: malformed,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
// -- Internal ---------------------------------------------------------------
|
|
497
|
+
collectConversations() {
|
|
498
|
+
const convs = [];
|
|
499
|
+
let malformed = 0;
|
|
500
|
+
for (const statePath of iterStateFiles(this.storeDir)) {
|
|
501
|
+
const state = loadStateJson(statePath);
|
|
502
|
+
if (state === null) {
|
|
503
|
+
malformed += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
convs.push(conversationFromState(statePath, state));
|
|
507
|
+
}
|
|
508
|
+
return { convs, malformed };
|
|
509
|
+
}
|
|
510
|
+
conversations(since) {
|
|
511
|
+
let { convs } = this.collectConversations();
|
|
512
|
+
if (since !== undefined) {
|
|
513
|
+
const cutoff = since.toISOString().slice(0, 10);
|
|
514
|
+
convs = convs.filter((conv) => !conv.date || conv.date >= cutoff);
|
|
515
|
+
}
|
|
516
|
+
// Reverse tuple sort: (date, convId) descending.
|
|
517
|
+
convs.sort((a, b) => {
|
|
518
|
+
if (a.date !== b.date)
|
|
519
|
+
return a.date < b.date ? 1 : -1;
|
|
520
|
+
if (a.convId !== b.convId)
|
|
521
|
+
return a.convId < b.convId ? 1 : -1;
|
|
522
|
+
return 0;
|
|
523
|
+
});
|
|
524
|
+
return convs;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
|
|
24
|
+
import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
|
|
25
|
+
/** External participant for the Claude desktop app's Co-Work surface. */
|
|
26
|
+
export declare class ClaudeDesktopCoworkParticipant implements BourdonParticipant {
|
|
27
|
+
agentId: string;
|
|
28
|
+
agentType: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
private readonly storeDir;
|
|
31
|
+
private readonly policy;
|
|
32
|
+
/**
|
|
33
|
+
* The Co-Work sub-store dir the setup wizard probes for presence. Resolves to
|
|
34
|
+
* `<desktop>/local-agent-mode-sessions`. When the desktop dir cannot be
|
|
35
|
+
* resolved on this platform, falls back to a non-existent sentinel under
|
|
36
|
+
* `home` so the wizard reports "not found" rather than crashing.
|
|
37
|
+
*/
|
|
38
|
+
static defaultNativePath(home?: string): string;
|
|
39
|
+
constructor(storeDir?: string, home?: string);
|
|
40
|
+
get nativePath(): string;
|
|
41
|
+
discover(): AgentStore;
|
|
42
|
+
exportSessions(since?: Date, limit?: number): SessionModel[];
|
|
43
|
+
exportL5(since?: Date): L5ManifestModel;
|
|
44
|
+
healthCheck(): HealthStatus;
|
|
45
|
+
private collectRuns;
|
|
46
|
+
private runs;
|
|
47
|
+
}
|