@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.
- package/README.md +7 -1
- package/dist/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.cjs +171 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -4
- package/dist/index.d.ts +42 -970
- package/dist/index.js +53 -6509
- package/dist/index.js.map +1 -1
- 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 +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
|
+
}
|