@getbourdon/participants 0.1.1 → 0.2.1
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/index.cjs +5011 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +577 -4
- package/dist/index.d.ts +970 -25
- package/dist/index.js +6509 -33
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/dist/base.d.ts +0 -81
- package/dist/base.js +0 -52
- package/dist/network-base.d.ts +0 -108
- package/dist/network-base.js +0 -248
- package/dist/readers/claude-code.d.ts +0 -33
- package/dist/readers/claude-code.js +0 -552
- package/dist/readers/github-copilot.d.ts +0 -34
- package/dist/readers/github-copilot.js +0 -242
- package/dist/readers/hermes.d.ts +0 -37
- package/dist/readers/hermes.js +0 -426
- package/dist/registry.d.ts +0 -38
- package/dist/registry.js +0 -73
- package/dist/sqlite-base.d.ts +0 -65
- package/dist/sqlite-base.js +0 -163
package/dist/readers/hermes.js
DELETED
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hermes participant — normalize Hermes Agent (Nous Research) memory + sessions
|
|
3
|
-
* into L5. Port of `participants/hermes.py`.
|
|
4
|
-
*
|
|
5
|
-
* Reads two surfaces under `~/.hermes/` (override `$HERMES_HOME`):
|
|
6
|
-
* * `state.db` — a SQLite store (`sessions` + `messages` tables). Recent
|
|
7
|
-
* non-archived sessions + project entities (from cwd basename) come from it.
|
|
8
|
-
* * `memories/{memory.md,user.md}` — durable cross-session facts (one
|
|
9
|
-
* Markdown bullet/line per memory). The highest-signal entities.
|
|
10
|
-
*
|
|
11
|
-
* Read-only (`mode=ro`), deterministic, visibility-enforced-before-emission,
|
|
12
|
-
* and redaction on every native string. `healthCheck` never throws; `discover`
|
|
13
|
-
* raises only {@link ParticipantDiscoveryError}.
|
|
14
|
-
*/
|
|
15
|
-
import { readFileSync, statSync } from "node:fs";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
|
|
19
|
-
import { redactText } from "@getbourdon/redaction";
|
|
20
|
-
import { CONTRACT_VERSION, ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
|
|
21
|
-
import { epochToIsoDate, friendlyLabel, projectKeyFromCwd, tableColumns, tableExists, tryOpenReadonly, } from "../sqlite-base.js";
|
|
22
|
-
const AGENT_ID = "hermes";
|
|
23
|
-
const AGENT_TYPE = "code-assistant";
|
|
24
|
-
const DISPLAY_NAME = "Hermes Agent";
|
|
25
|
-
const ROLE_NARRATIVE = "General-purpose tool-calling assistant (Nous Research). Operates across CLI, " +
|
|
26
|
-
"TUI, and messaging gateways (Slack/Telegram/Discord/WhatsApp); runs terminal, " +
|
|
27
|
-
"file, web, and delegation toolsets. Carries durable cross-session memory and a " +
|
|
28
|
-
"curated skill library. Federates session + memory context, not vendor account.";
|
|
29
|
-
const DEFAULT_SESSION_LIMIT = 100;
|
|
30
|
-
/** Hermes memory stores live as Markdown under `~/.hermes/memories/`. */
|
|
31
|
-
const MEMORY_FILENAMES = ["memory.md", "user.md"];
|
|
32
|
-
const DEFAULT_POLICY = makeVisibilityPolicy({
|
|
33
|
-
default: Visibility.TEAM,
|
|
34
|
-
private_tags: ["personal", "financial", "credential", "secret", "health", "family", "legal"],
|
|
35
|
-
team_tags: ["hermes-memory", "hermes-session", "hermes-project"],
|
|
36
|
-
});
|
|
37
|
-
/** cwd basenames that carry no project identity. */
|
|
38
|
-
const GENERIC_PROJECT_NAMES = new Set([
|
|
39
|
-
"",
|
|
40
|
-
"root",
|
|
41
|
-
"home",
|
|
42
|
-
"tmp",
|
|
43
|
-
"temp",
|
|
44
|
-
"desktop",
|
|
45
|
-
"documents",
|
|
46
|
-
"downloads",
|
|
47
|
-
"src",
|
|
48
|
-
]);
|
|
49
|
-
// -- Path resolution ----------------------------------------------------------
|
|
50
|
-
/**
|
|
51
|
-
* Conventional Hermes home used by the setup wizard's detection.
|
|
52
|
-
* Precedence: explicit `home` > `$HERMES_HOME` > `~/.hermes`.
|
|
53
|
-
*/
|
|
54
|
-
export function defaultNativePath(home) {
|
|
55
|
-
if (home)
|
|
56
|
-
return join(home, ".hermes");
|
|
57
|
-
const env = process.env.HERMES_HOME;
|
|
58
|
-
if (env)
|
|
59
|
-
return env;
|
|
60
|
-
return join(homedir(), ".hermes");
|
|
61
|
-
}
|
|
62
|
-
function isDir(p) {
|
|
63
|
-
try {
|
|
64
|
-
return statSync(p).isDirectory();
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function isFile(p) {
|
|
71
|
-
try {
|
|
72
|
-
return statSync(p).isFile();
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/** Redact secrets, collapse whitespace, and clamp length. */
|
|
79
|
-
function bounded(value, limit = 240) {
|
|
80
|
-
const cleaned = redactText((value || "").replace(/\s+/g, " ").trim(), limit);
|
|
81
|
-
return cleaned.trim();
|
|
82
|
-
}
|
|
83
|
-
function projectKey(cwd) {
|
|
84
|
-
return projectKeyFromCwd(cwd, GENERIC_PROJECT_NAMES);
|
|
85
|
-
}
|
|
86
|
-
// -- SQLite (read-only) -------------------------------------------------------
|
|
87
|
-
/** Pull recent, non-archived sessions newest-first. Never raises. */
|
|
88
|
-
function collectSessionRows(stateDb, limit = DEFAULT_SESSION_LIMIT) {
|
|
89
|
-
if (!isFile(stateDb))
|
|
90
|
-
return [];
|
|
91
|
-
const conn = tryOpenReadonly(stateDb);
|
|
92
|
-
if (!conn)
|
|
93
|
-
return [];
|
|
94
|
-
const out = [];
|
|
95
|
-
try {
|
|
96
|
-
if (!tableExists(conn, "sessions"))
|
|
97
|
-
return [];
|
|
98
|
-
// `archived` may be absent in older schemas — guard the column.
|
|
99
|
-
const cols = tableColumns(conn, "sessions");
|
|
100
|
-
const archivedClause = cols.has("archived") ? "WHERE COALESCE(archived, 0) = 0" : "";
|
|
101
|
-
const rows = conn
|
|
102
|
-
.prepare(`SELECT id, source, model, title, cwd, started_at, ended_at,
|
|
103
|
-
message_count, tool_call_count
|
|
104
|
-
FROM sessions
|
|
105
|
-
${archivedClause}
|
|
106
|
-
ORDER BY started_at DESC
|
|
107
|
-
LIMIT ?`)
|
|
108
|
-
.all(limit);
|
|
109
|
-
for (const r of rows) {
|
|
110
|
-
const day = epochToIsoDate(r["started_at"]);
|
|
111
|
-
if (!day)
|
|
112
|
-
continue;
|
|
113
|
-
out.push({
|
|
114
|
-
id: String(r["id"]),
|
|
115
|
-
source: String(r["source"] ?? "").toLowerCase(),
|
|
116
|
-
model: r["model"],
|
|
117
|
-
title: r["title"] ?? null,
|
|
118
|
-
cwd: r["cwd"] ?? null,
|
|
119
|
-
date: day,
|
|
120
|
-
messageCount: Number(r["message_count"] ?? 0) || 0,
|
|
121
|
-
toolCallCount: Number(r["tool_call_count"] ?? 0) || 0,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
/* degrade to whatever rows we gathered */
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
conn.close();
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
/** Distinct tool names used in a session, for key_actions evidence. */
|
|
134
|
-
function toolNamesForSession(conn, sessionId) {
|
|
135
|
-
if (!tableExists(conn, "messages"))
|
|
136
|
-
return [];
|
|
137
|
-
try {
|
|
138
|
-
const rows = conn
|
|
139
|
-
.prepare(`SELECT DISTINCT tool_name FROM messages
|
|
140
|
-
WHERE session_id = ? AND tool_name IS NOT NULL AND tool_name != ''`)
|
|
141
|
-
.all(sessionId);
|
|
142
|
-
const names = new Set();
|
|
143
|
-
for (const r of rows) {
|
|
144
|
-
if (r.tool_name)
|
|
145
|
-
names.add(r.tool_name);
|
|
146
|
-
}
|
|
147
|
-
return [...names].sort();
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// -- Memory parsing -----------------------------------------------------------
|
|
154
|
-
const BULLET_RE = /^\s*[-*]\s+(.*)$/;
|
|
155
|
-
/** Return memory entries (one per bullet / non-empty line). Never raises. */
|
|
156
|
-
function parseMemoryFile(path) {
|
|
157
|
-
let text;
|
|
158
|
-
try {
|
|
159
|
-
text = readFileSync(path, "utf8");
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
const entries = [];
|
|
165
|
-
for (const line of text.split(/\r?\n/)) {
|
|
166
|
-
const m = BULLET_RE.exec(line);
|
|
167
|
-
const candidate = m ? (m[1] ?? "") : line.trim();
|
|
168
|
-
if (candidate && !candidate.startsWith("#"))
|
|
169
|
-
entries.push(candidate);
|
|
170
|
-
}
|
|
171
|
-
return entries;
|
|
172
|
-
}
|
|
173
|
-
/** Map memory store name → list of entry strings. */
|
|
174
|
-
function collectMemoryEntries(home) {
|
|
175
|
-
const memDir = join(home, "memories");
|
|
176
|
-
const result = [];
|
|
177
|
-
if (!isDir(memDir))
|
|
178
|
-
return result;
|
|
179
|
-
for (const fname of MEMORY_FILENAMES) {
|
|
180
|
-
const path = join(memDir, fname);
|
|
181
|
-
if (isFile(path)) {
|
|
182
|
-
const entries = parseMemoryFile(path);
|
|
183
|
-
if (entries.length) {
|
|
184
|
-
const stem = fname.replace(/\.md$/, "");
|
|
185
|
-
result.push([stem, entries]);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
// -- Entity / session builders ------------------------------------------------
|
|
192
|
-
function buildSession(record, toolNames) {
|
|
193
|
-
const key = projectKey(record.cwd);
|
|
194
|
-
const focus = [];
|
|
195
|
-
if (key)
|
|
196
|
-
focus.push(key);
|
|
197
|
-
const keyActions = [];
|
|
198
|
-
if (record.title)
|
|
199
|
-
keyActions.push(bounded(record.title, 160));
|
|
200
|
-
if (toolNames.length) {
|
|
201
|
-
keyActions.push("Tools: " + toolNames.slice(0, 8).join(", "));
|
|
202
|
-
}
|
|
203
|
-
else if (record.toolCallCount) {
|
|
204
|
-
keyActions.push(`${record.toolCallCount} tool call(s)`);
|
|
205
|
-
}
|
|
206
|
-
return makeSession({
|
|
207
|
-
date: record.date,
|
|
208
|
-
cwd: record.cwd || undefined,
|
|
209
|
-
project_focus: focus,
|
|
210
|
-
key_actions: keyActions,
|
|
211
|
-
visibility: Visibility.TEAM,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
/** Turn a single memory line into a team-visibility entity. */
|
|
215
|
-
function memoryEntity(store, entry) {
|
|
216
|
-
const tag = store === "user" ? "hermes-user-memory" : "hermes-memory";
|
|
217
|
-
return makeEntity({
|
|
218
|
-
name: bounded(entry, 80),
|
|
219
|
-
type: store === "user" ? "preference" : "fact",
|
|
220
|
-
summary: bounded(entry, 240),
|
|
221
|
-
tags: [tag],
|
|
222
|
-
visibility: Visibility.TEAM,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
function projectEntities(records) {
|
|
226
|
-
const counts = new Map();
|
|
227
|
-
for (const r of records) {
|
|
228
|
-
const key = projectKey(r.cwd);
|
|
229
|
-
if (key)
|
|
230
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
231
|
-
}
|
|
232
|
-
const entities = [];
|
|
233
|
-
for (const [key, count] of counts) {
|
|
234
|
-
let last;
|
|
235
|
-
for (const r of records) {
|
|
236
|
-
if (projectKey(r.cwd) === key) {
|
|
237
|
-
if (last === undefined || r.date > last)
|
|
238
|
-
last = r.date;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
entities.push(makeEntity({
|
|
242
|
-
name: friendlyLabel(key),
|
|
243
|
-
type: "project",
|
|
244
|
-
summary: `Workspace observed across ${count} Hermes session(s).`,
|
|
245
|
-
aliases: [key],
|
|
246
|
-
last_touched: last,
|
|
247
|
-
tags: ["hermes-project"],
|
|
248
|
-
visibility: Visibility.TEAM,
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
return entities;
|
|
252
|
-
}
|
|
253
|
-
// -- Participant --------------------------------------------------------------
|
|
254
|
-
/** External participant for local Hermes Agent memory + sessions. */
|
|
255
|
-
export class HermesParticipant {
|
|
256
|
-
agentId = AGENT_ID;
|
|
257
|
-
agentType = AGENT_TYPE;
|
|
258
|
-
displayName = DISPLAY_NAME;
|
|
259
|
-
nativePath;
|
|
260
|
-
home;
|
|
261
|
-
static defaultNativePath(home) {
|
|
262
|
-
return defaultNativePath(home);
|
|
263
|
-
}
|
|
264
|
-
/** `hermesHome` is the `.hermes` directory itself (matching the Python ctor). */
|
|
265
|
-
constructor(hermesHome) {
|
|
266
|
-
if (hermesHome) {
|
|
267
|
-
this.home = hermesHome;
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
const path = defaultNativePath();
|
|
271
|
-
this.home = isDir(path) ? path : null;
|
|
272
|
-
}
|
|
273
|
-
this.nativePath = this.home ?? join(homedir(), ".hermes");
|
|
274
|
-
}
|
|
275
|
-
sources() {
|
|
276
|
-
const home = this.home;
|
|
277
|
-
const stateDb = home ? join(home, "state.db") : null;
|
|
278
|
-
const memDir = home ? join(home, "memories") : null;
|
|
279
|
-
const skillsDir = home ? join(home, "skills") : null;
|
|
280
|
-
return {
|
|
281
|
-
hermes_home: home,
|
|
282
|
-
state_db: stateDb && isFile(stateDb) ? stateDb : null,
|
|
283
|
-
memories_dir: memDir && isDir(memDir) ? memDir : null,
|
|
284
|
-
skills_dir: skillsDir && isDir(skillsDir) ? skillsDir : null,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
discover() {
|
|
288
|
-
const sources = this.sources();
|
|
289
|
-
if (!Object.values(sources).some((v) => v)) {
|
|
290
|
-
throw new ParticipantDiscoveryError("No Hermes memory sources found. Expected ~/.hermes/ (set $HERMES_HOME to override).");
|
|
291
|
-
}
|
|
292
|
-
if (!sources["state_db"] && !sources["memories_dir"]) {
|
|
293
|
-
throw new ParticipantDiscoveryError(`Hermes home '${this.nativePath}' exists but has no state.db or memories/ — ` +
|
|
294
|
-
"nothing to federate yet.");
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
path: this.nativePath,
|
|
298
|
-
version: "hermes-home-v1",
|
|
299
|
-
metadata: { sources },
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
exportSessions(since, limit = DEFAULT_SESSION_LIMIT) {
|
|
303
|
-
if (this.home === null)
|
|
304
|
-
return [];
|
|
305
|
-
const stateDb = join(this.home, "state.db");
|
|
306
|
-
const cutoff = since ? since.toISOString().slice(0, 10) : null;
|
|
307
|
-
const records = collectSessionRows(stateDb, limit);
|
|
308
|
-
if (!records.length)
|
|
309
|
-
return [];
|
|
310
|
-
const conn = tryOpenReadonly(stateDb);
|
|
311
|
-
const out = [];
|
|
312
|
-
try {
|
|
313
|
-
for (const rec of records) {
|
|
314
|
-
if (cutoff && rec.date < cutoff)
|
|
315
|
-
continue;
|
|
316
|
-
const tools = conn ? toolNamesForSession(conn, rec.id) : [];
|
|
317
|
-
out.push(buildSession(rec, tools));
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
finally {
|
|
321
|
-
if (conn)
|
|
322
|
-
conn.close();
|
|
323
|
-
}
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
exportL5(since) {
|
|
327
|
-
const store = this.discover();
|
|
328
|
-
const sources = (store.metadata["sources"] ?? {});
|
|
329
|
-
const capabilities = Object.entries(sources)
|
|
330
|
-
.filter(([k, v]) => v && k !== "hermes_home")
|
|
331
|
-
.map(([k]) => k)
|
|
332
|
-
.sort();
|
|
333
|
-
const home = this.home;
|
|
334
|
-
const records = home ? collectSessionRows(join(home, "state.db"), DEFAULT_SESSION_LIMIT) : [];
|
|
335
|
-
// Entities: project workspaces + memory-derived facts/preferences.
|
|
336
|
-
const entities = projectEntities(records);
|
|
337
|
-
const seen = new Set();
|
|
338
|
-
for (const e of entities) {
|
|
339
|
-
seen.add(`${e.type || "topic"}${e.name.toLowerCase()}`);
|
|
340
|
-
}
|
|
341
|
-
if (home) {
|
|
342
|
-
for (const [storeName, entries] of collectMemoryEntries(home)) {
|
|
343
|
-
for (const entry of entries) {
|
|
344
|
-
const ent = memoryEntity(storeName, entry);
|
|
345
|
-
const key = `${ent.type || "topic"}${ent.name.toLowerCase()}`;
|
|
346
|
-
if (seen.has(key))
|
|
347
|
-
continue;
|
|
348
|
-
seen.add(key);
|
|
349
|
-
entities.push(ent);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Visibility enforced here, before emission (contract requirement).
|
|
354
|
-
const visible = filterForFederation(entities, DEFAULT_POLICY);
|
|
355
|
-
const sessions = this.exportSessions(since ?? new Date(0), DEFAULT_SESSION_LIMIT);
|
|
356
|
-
return makeManifest({
|
|
357
|
-
spec_version: SPEC_VERSION,
|
|
358
|
-
agent: makeAgentInfo({
|
|
359
|
-
id: this.agentId,
|
|
360
|
-
type: this.agentType,
|
|
361
|
-
role_narrative: ROLE_NARRATIVE,
|
|
362
|
-
spec_version_compat: CONTRACT_VERSION,
|
|
363
|
-
}),
|
|
364
|
-
last_updated: new Date().toISOString(),
|
|
365
|
-
capabilities,
|
|
366
|
-
recent_sessions: sessions,
|
|
367
|
-
known_entities: visible,
|
|
368
|
-
visibility_policy: DEFAULT_POLICY,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
healthCheck() {
|
|
372
|
-
const details = {
|
|
373
|
-
hermes_home: this.nativePath,
|
|
374
|
-
state_db: "missing",
|
|
375
|
-
memories_dir: "missing",
|
|
376
|
-
skills_dir: "missing",
|
|
377
|
-
};
|
|
378
|
-
try {
|
|
379
|
-
if (this.home === null) {
|
|
380
|
-
return {
|
|
381
|
-
status: "blocked",
|
|
382
|
-
reason: "~/.hermes/ not found — Hermes Agent not installed here",
|
|
383
|
-
details,
|
|
384
|
-
proposedFix: "Install Hermes Agent and run a session once, then `bourdon export-all`.",
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
const sources = this.sources();
|
|
388
|
-
for (const key of ["state_db", "memories_dir", "skills_dir"]) {
|
|
389
|
-
if (sources[key])
|
|
390
|
-
details[key] = sources[key];
|
|
391
|
-
}
|
|
392
|
-
if (sources["state_db"]) {
|
|
393
|
-
const records = collectSessionRows(join(this.home, "state.db"), 1);
|
|
394
|
-
if (records.length)
|
|
395
|
-
return { status: "ok", details };
|
|
396
|
-
return {
|
|
397
|
-
status: "degraded",
|
|
398
|
-
reason: "state.db present but no readable sessions yet",
|
|
399
|
-
details,
|
|
400
|
-
proposedFix: "Run at least one Hermes session, then `bourdon export-all`.",
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
if (sources["memories_dir"]) {
|
|
404
|
-
return {
|
|
405
|
-
status: "degraded",
|
|
406
|
-
reason: "memories/ present but state.db missing — session history unavailable",
|
|
407
|
-
details,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
status: "blocked",
|
|
412
|
-
reason: "Hermes home has neither state.db nor memories/",
|
|
413
|
-
details,
|
|
414
|
-
proposedFix: "Run a Hermes session to populate ~/.hermes/state.db.",
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
catch (err) {
|
|
418
|
-
// healthCheck must NEVER raise (contract).
|
|
419
|
-
return {
|
|
420
|
-
status: "degraded",
|
|
421
|
-
reason: `unexpected error: ${String(err)}`,
|
|
422
|
-
details,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
package/dist/registry.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Participant discovery — a STATIC TS registry replacing Python's `pkgutil`
|
|
3
|
-
* package scan + entry-point duck-type (`participants/__init__.py`).
|
|
4
|
-
*
|
|
5
|
-
* Python scans the `participants/` package at runtime and duck-types each class
|
|
6
|
-
* by `_PARTICIPANT_MARKER_ATTRS`. A filesystem package scan does not survive
|
|
7
|
-
* bundling (`npx bourdon` is a single file), so the JS side declares an explicit
|
|
8
|
-
* {@link FIRST_PARTY} array. The three preserved invariants:
|
|
9
|
-
* - output sorted by `agentId`
|
|
10
|
-
* - first-wins dedupe on `agentId`
|
|
11
|
-
* - log-and-skip a participant that throws on construction (one broken reader
|
|
12
|
-
* must NOT abort discovery / crash the CLI)
|
|
13
|
-
*
|
|
14
|
-
* The marker-attr duck-typing collapses into the {@link BourdonParticipant}
|
|
15
|
-
* interface conformance (checked structurally via {@link isParticipant}).
|
|
16
|
-
*/
|
|
17
|
-
import { type BourdonParticipant } from "./base.js";
|
|
18
|
-
/** A zero-arg-constructible participant class. */
|
|
19
|
-
export type ParticipantCtor = new () => BourdonParticipant;
|
|
20
|
-
/**
|
|
21
|
-
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
22
|
-
* `agentId`. (cursor / codex / copilot_cli are the heavier SQLite readers of the
|
|
23
|
-
* follow-on slice; openclaw ships as the `@getbourdon/openclaw` plugin.)
|
|
24
|
-
*/
|
|
25
|
-
export declare const FIRST_PARTY: ParticipantCtor[];
|
|
26
|
-
/**
|
|
27
|
-
* Instantiate each registered participant, duck-type the contract, sort by
|
|
28
|
-
* `agentId`, and first-wins dedupe. A class that throws on construction is
|
|
29
|
-
* logged and skipped — never propagated.
|
|
30
|
-
*/
|
|
31
|
-
export declare function discoverParticipants(ctors?: ParticipantCtor[]): BourdonParticipant[];
|
|
32
|
-
/**
|
|
33
|
-
* Optional Layer 2: third-party plugins declared via a `package.json`
|
|
34
|
-
* `"bourdon": { participants: { ... } }` field. Gated behind `BOURDON_PLUGINS=1`
|
|
35
|
-
* and a no-op today (no external participant exists yet) — the resolver ships so
|
|
36
|
-
* the seam is wired, defaulted off so `npx bourdon` stays predictable.
|
|
37
|
-
*/
|
|
38
|
-
export declare function resolvePluginParticipants(): ParticipantCtor[];
|
package/dist/registry.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Participant discovery — a STATIC TS registry replacing Python's `pkgutil`
|
|
3
|
-
* package scan + entry-point duck-type (`participants/__init__.py`).
|
|
4
|
-
*
|
|
5
|
-
* Python scans the `participants/` package at runtime and duck-types each class
|
|
6
|
-
* by `_PARTICIPANT_MARKER_ATTRS`. A filesystem package scan does not survive
|
|
7
|
-
* bundling (`npx bourdon` is a single file), so the JS side declares an explicit
|
|
8
|
-
* {@link FIRST_PARTY} array. The three preserved invariants:
|
|
9
|
-
* - output sorted by `agentId`
|
|
10
|
-
* - first-wins dedupe on `agentId`
|
|
11
|
-
* - log-and-skip a participant that throws on construction (one broken reader
|
|
12
|
-
* must NOT abort discovery / crash the CLI)
|
|
13
|
-
*
|
|
14
|
-
* The marker-attr duck-typing collapses into the {@link BourdonParticipant}
|
|
15
|
-
* interface conformance (checked structurally via {@link isParticipant}).
|
|
16
|
-
*/
|
|
17
|
-
import { isParticipant } from "./base.js";
|
|
18
|
-
import { ClaudeCodeParticipant } from "./readers/claude-code.js";
|
|
19
|
-
import { GitHubCopilotParticipant } from "./readers/github-copilot.js";
|
|
20
|
-
import { HermesParticipant } from "./readers/hermes.js";
|
|
21
|
-
/**
|
|
22
|
-
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
23
|
-
* `agentId`. (cursor / codex / copilot_cli are the heavier SQLite readers of the
|
|
24
|
-
* follow-on slice; openclaw ships as the `@getbourdon/openclaw` plugin.)
|
|
25
|
-
*/
|
|
26
|
-
export const FIRST_PARTY = [
|
|
27
|
-
ClaudeCodeParticipant,
|
|
28
|
-
GitHubCopilotParticipant,
|
|
29
|
-
HermesParticipant,
|
|
30
|
-
];
|
|
31
|
-
/** Minimal logger so a skipped participant is visible without a dep. */
|
|
32
|
-
const logger = {
|
|
33
|
-
warn(msg) {
|
|
34
|
-
// eslint-disable-next-line no-console
|
|
35
|
-
console.warn(`[bourdon.participants] ${msg}`);
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Instantiate each registered participant, duck-type the contract, sort by
|
|
40
|
-
* `agentId`, and first-wins dedupe. A class that throws on construction is
|
|
41
|
-
* logged and skipped — never propagated.
|
|
42
|
-
*/
|
|
43
|
-
export function discoverParticipants(ctors = FIRST_PARTY) {
|
|
44
|
-
const found = new Map();
|
|
45
|
-
for (const Ctor of [...ctors, ...resolvePluginParticipants()]) {
|
|
46
|
-
let instance;
|
|
47
|
-
try {
|
|
48
|
-
instance = new Ctor();
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
logger.warn(`Skipping participant ${Ctor.name || "<anonymous>"}: construction failed: ${String(err)}`);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (!isParticipant(instance))
|
|
55
|
-
continue;
|
|
56
|
-
if (!found.has(instance.agentId))
|
|
57
|
-
found.set(instance.agentId, instance);
|
|
58
|
-
}
|
|
59
|
-
return [...found.values()].sort((a, b) => a.agentId < b.agentId ? -1 : a.agentId > b.agentId ? 1 : 0);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Optional Layer 2: third-party plugins declared via a `package.json`
|
|
63
|
-
* `"bourdon": { participants: { ... } }` field. Gated behind `BOURDON_PLUGINS=1`
|
|
64
|
-
* and a no-op today (no external participant exists yet) — the resolver ships so
|
|
65
|
-
* the seam is wired, defaulted off so `npx bourdon` stays predictable.
|
|
66
|
-
*/
|
|
67
|
-
export function resolvePluginParticipants() {
|
|
68
|
-
if (process.env.BOURDON_PLUGINS !== "1")
|
|
69
|
-
return [];
|
|
70
|
-
// No external participant package exists yet; the resolver is intentionally a
|
|
71
|
-
// no-op stub. When one ships, resolve its declared ctors here.
|
|
72
|
-
return [];
|
|
73
|
-
}
|
package/dist/sqlite-base.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared read-only SQLite + normalization helpers — port of
|
|
3
|
-
* `participants/_sqlite_base.py`.
|
|
4
|
-
*
|
|
5
|
-
* A SQLite-backed participant (hermes today; cursor/copilot_cli in the follow-on
|
|
6
|
-
* slice) reads a foreign agent's native DB read-only so a live writer's lock can
|
|
7
|
-
* never block the export, probes the schema defensively (native schemas drift
|
|
8
|
-
* across versions), and turns epoch timestamps + paths into the L5 shapes.
|
|
9
|
-
*
|
|
10
|
-
* `better-sqlite3` is a NATIVE addon. It is loaded lazily through `createRequire`
|
|
11
|
-
* inside a try/catch so this package — and the file/network readers — keep
|
|
12
|
-
* working even on a toolchain where the addon failed to build: in that case
|
|
13
|
-
* {@link sqliteAvailable} returns false and the sqlite readers degrade rather
|
|
14
|
-
* than crashing import. Nothing here applies visibility or builds manifests;
|
|
15
|
-
* that stays in the participant (per PARTICIPANT_CONTRACT.md).
|
|
16
|
-
*/
|
|
17
|
-
import type DatabaseT from "better-sqlite3";
|
|
18
|
-
/** A better-sqlite3 connection (re-exported type alias for reader signatures). */
|
|
19
|
-
export type SqliteDatabase = DatabaseT.Database;
|
|
20
|
-
/** True when the better-sqlite3 native addon loaded successfully. Tests gate
|
|
21
|
-
* the SQLite-reader assertions on this so a missing addon skips, not fails. */
|
|
22
|
-
export declare function sqliteAvailable(): boolean;
|
|
23
|
-
export interface OpenReadonlyOptions {
|
|
24
|
-
/** `immutable=1`: skip locks + ignore the WAL. Only for genuinely static
|
|
25
|
-
* snapshots; against a live writer it risks stale reads / SQLITE_CORRUPT. */
|
|
26
|
-
immutable?: boolean;
|
|
27
|
-
/** SQLite busy-timeout in milliseconds (Python's `timeout=2.0` seconds). */
|
|
28
|
-
timeoutMs?: number;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Open a SQLite DB read-only so a live writer is never disturbed. The default
|
|
32
|
-
* (`immutable` omitted) is the safe `mode=ro`: it respects SQLite locking and
|
|
33
|
-
* reads the `-wal` sidecar of a live, actively-writing agent. Raises on a
|
|
34
|
-
* genuine open failure; callers wanting the never-raises contract use
|
|
35
|
-
* {@link tryOpenReadonly}.
|
|
36
|
-
*/
|
|
37
|
-
export declare function openReadonly(dbPath: string, opts?: OpenReadonlyOptions): SqliteDatabase;
|
|
38
|
-
/**
|
|
39
|
-
* Like {@link openReadonly} but returns null instead of raising — for the
|
|
40
|
-
* export/health path where a missing or corrupt DB must degrade to "no rows".
|
|
41
|
-
*/
|
|
42
|
-
export declare function tryOpenReadonly(dbPath: string, opts?: OpenReadonlyOptions): SqliteDatabase | null;
|
|
43
|
-
/** True if `name` is a table in the connected DB (swallows errors → false). */
|
|
44
|
-
export declare function tableExists(db: SqliteDatabase, name: string): boolean;
|
|
45
|
-
/** Column names for `table` (empty set if absent / on error). PRAGMA cannot be
|
|
46
|
-
* parameterized, matching the Python f-string. */
|
|
47
|
-
export declare function tableColumns(db: SqliteDatabase, table: string): Set<string>;
|
|
48
|
-
/**
|
|
49
|
-
* Convert epoch seconds (number/string) to an ISO `YYYY-MM-DD` UTC date, or
|
|
50
|
-
* null for missing/unparseable values. Mirrors `epoch_to_iso_date`.
|
|
51
|
-
*/
|
|
52
|
-
export declare function epochToIsoDate(value: unknown): string | null;
|
|
53
|
-
/**
|
|
54
|
-
* Convert epoch seconds to a full ISO 8601 UTC datetime, or null. The offset is
|
|
55
|
-
* rendered `+00:00` (Python `datetime(...).isoformat()` style) rather than `Z`.
|
|
56
|
-
*/
|
|
57
|
-
export declare function epochToIsoDatetime(value: unknown): string | null;
|
|
58
|
-
/**
|
|
59
|
-
* Derive a lowercase project key from a working-directory path: the basename
|
|
60
|
-
* lowercased, or null when the cwd is empty or its basename is "generic"
|
|
61
|
-
* (carries no project identity). Mirrors `project_key_from_cwd`.
|
|
62
|
-
*/
|
|
63
|
-
export declare function projectKeyFromCwd(cwd: string | null | undefined, genericNames?: ReadonlySet<string>): string | null;
|
|
64
|
-
/** Turn a slug-ish key (`my-cool_project`) into a Title Case label. */
|
|
65
|
-
export declare function friendlyLabel(key: string): string;
|