@getbourdon/participants 0.1.0
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/LICENSE +92 -0
- package/LICENSE_FAQ.md +79 -0
- package/README.md +61 -0
- package/dist/base.d.ts +81 -0
- package/dist/base.js +52 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +33 -0
- package/dist/network-base.d.ts +108 -0
- package/dist/network-base.js +248 -0
- package/dist/readers/claude-code.d.ts +33 -0
- package/dist/readers/claude-code.js +552 -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/registry.d.ts +38 -0
- package/dist/registry.js +73 -0
- package/dist/sqlite-base.d.ts +65 -0
- package/dist/sqlite-base.js +163 -0
- package/package.json +51 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network-shaped participant base — port of `participants/_network_base.py`
|
|
3
|
+
* (v0.3 adapter contract). Same `exportL5` / `exportSessions` half as a file
|
|
4
|
+
* participant; only the discovery half differs (fetch from an authenticated
|
|
5
|
+
* API). The base provides the three guarantees so adapters don't reimplement:
|
|
6
|
+
*
|
|
7
|
+
* 1. Local TTL cache under `~/.cache/bourdon/<slug>/payload.json` storing ONLY
|
|
8
|
+
* `{fetched_at, payload}` — NEVER the token. A cache hit = zero network.
|
|
9
|
+
* 2. Graceful degradation: network down → serve the most recent cached payload
|
|
10
|
+
* (even expired) and report `degraded` (not silence).
|
|
11
|
+
* 3. Auth boundary: an injected lazy {@link AuthProvider} (env / keychain).
|
|
12
|
+
* An invalid token (401/403 non-ratelimit) ALWAYS propagates as `blocked`
|
|
13
|
+
* and is NEVER masked by stale cache; a missing token serves stale when a
|
|
14
|
+
* cache exists, else blocks; rate-limit degrades.
|
|
15
|
+
*
|
|
16
|
+
* A subclass sets `participantSlug` / `agentId` / `agentType` (+ `nativePath`)
|
|
17
|
+
* and implements `fetchPayload(token)` + `payloadToL5(payload)` (pure, no I/O).
|
|
18
|
+
*/
|
|
19
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { ParticipantError, } from "./base.js";
|
|
23
|
+
export const NETWORK_CONTRACT_VERSION = "0.3";
|
|
24
|
+
/** Default cache TTL — adapters override via `cacheTtlSeconds`. */
|
|
25
|
+
export const DEFAULT_CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
|
|
26
|
+
// -- Network-specific errors --------------------------------------------------
|
|
27
|
+
/** Transport failure / timeout / 5xx / rate-limit — recoverable via cache fallback. */
|
|
28
|
+
export class NetworkUnavailable extends ParticipantError {
|
|
29
|
+
}
|
|
30
|
+
/** 401/403 / missing credential — NOT recoverable via cache. The user must fix
|
|
31
|
+
* auth; surfaced as `blocked`, never silently served from stale cache (stale
|
|
32
|
+
* data would mask a broken token indefinitely). */
|
|
33
|
+
export class ParticipantAuthError extends ParticipantError {
|
|
34
|
+
}
|
|
35
|
+
/** Auth provider that reads a token from an environment variable. */
|
|
36
|
+
export function envAuthProvider(varName) {
|
|
37
|
+
return () => process.env[varName] || null;
|
|
38
|
+
}
|
|
39
|
+
// -- Cache --------------------------------------------------------------------
|
|
40
|
+
/** `$XDG_CACHE_HOME/bourdon` or `~/.cache/bourdon`. */
|
|
41
|
+
export function defaultCacheRoot() {
|
|
42
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
43
|
+
const base = xdg ? xdg : join(homedir(), ".cache");
|
|
44
|
+
return join(base, "bourdon");
|
|
45
|
+
}
|
|
46
|
+
/** A cached payload + the wall-clock time it was fetched (epoch seconds). */
|
|
47
|
+
export class CacheEntry {
|
|
48
|
+
fetchedAt;
|
|
49
|
+
payload;
|
|
50
|
+
constructor(fetchedAt, payload) {
|
|
51
|
+
this.fetchedAt = fetchedAt;
|
|
52
|
+
this.payload = payload;
|
|
53
|
+
}
|
|
54
|
+
get ageSeconds() {
|
|
55
|
+
return Math.max(0, Date.now() / 1000 - this.fetchedAt);
|
|
56
|
+
}
|
|
57
|
+
isFresh(ttlSeconds) {
|
|
58
|
+
return this.ageSeconds < ttlSeconds;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A tiny JSON file cache, one file per participant slug. Stores only
|
|
63
|
+
* `{fetched_at, payload}` — never the auth token. Read/write are best-effort: a
|
|
64
|
+
* corrupt or unwritable cache degrades to "no cache", never raises.
|
|
65
|
+
*/
|
|
66
|
+
export class PayloadCache {
|
|
67
|
+
slug;
|
|
68
|
+
root;
|
|
69
|
+
path;
|
|
70
|
+
constructor(slug, rootDir) {
|
|
71
|
+
this.slug = slug;
|
|
72
|
+
this.root = join(rootDir ?? defaultCacheRoot(), slug);
|
|
73
|
+
this.path = join(this.root, "payload.json");
|
|
74
|
+
}
|
|
75
|
+
read() {
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = JSON.parse(readFileSync(this.path, "utf8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (typeof raw !== "object" || raw === null || !("payload" in raw))
|
|
84
|
+
return null;
|
|
85
|
+
const obj = raw;
|
|
86
|
+
let fetchedAt = Number(obj["fetched_at"] ?? 0);
|
|
87
|
+
if (!Number.isFinite(fetchedAt))
|
|
88
|
+
fetchedAt = 0;
|
|
89
|
+
const payload = obj["payload"];
|
|
90
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
91
|
+
return null;
|
|
92
|
+
return new CacheEntry(fetchedAt, payload);
|
|
93
|
+
}
|
|
94
|
+
write(payload) {
|
|
95
|
+
try {
|
|
96
|
+
mkdirSync(this.root, { recursive: true });
|
|
97
|
+
const tmp = `${this.path}.tmp`;
|
|
98
|
+
writeFileSync(tmp, JSON.stringify({ fetched_at: Date.now() / 1000, payload }), "utf8");
|
|
99
|
+
renameSync(tmp, this.path); // atomic
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* best-effort; a write failure degrades to "no cache" */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// -- Base participant ---------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Base for participants whose native state lives behind an authenticated API.
|
|
109
|
+
* Caching, degradation, the auth boundary, and the
|
|
110
|
+
* `discover`/`exportL5`/`exportSessions`/`healthCheck` plumbing live here;
|
|
111
|
+
* subclasses provide only `fetchPayload` + `payloadToL5`.
|
|
112
|
+
*/
|
|
113
|
+
export class NetworkParticipant {
|
|
114
|
+
participantSlug = "network";
|
|
115
|
+
agentId = "network";
|
|
116
|
+
agentType = "other";
|
|
117
|
+
cacheTtlSeconds = DEFAULT_CACHE_TTL_SECONDS;
|
|
118
|
+
nativePath = "network://";
|
|
119
|
+
authProvider;
|
|
120
|
+
cacheRoot;
|
|
121
|
+
cacheInstance;
|
|
122
|
+
constructor(opts = {}) {
|
|
123
|
+
this.authProvider = opts.authProvider ?? null;
|
|
124
|
+
this.cacheRoot = opts.cacheRoot;
|
|
125
|
+
}
|
|
126
|
+
/** Lazily built so it reads the SUBCLASS's `participantSlug` — subclass field
|
|
127
|
+
* initializers run after `super()`, so building the cache in the constructor
|
|
128
|
+
* would capture the base "network" slug instead. */
|
|
129
|
+
get cache() {
|
|
130
|
+
if (!this.cacheInstance) {
|
|
131
|
+
this.cacheInstance = new PayloadCache(this.participantSlug, this.cacheRoot);
|
|
132
|
+
}
|
|
133
|
+
return this.cacheInstance;
|
|
134
|
+
}
|
|
135
|
+
// -- Core fetch-with-cache-and-degrade -------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Resolve (payload, source) where source is 'cache' | 'network' | 'stale-cache'.
|
|
138
|
+
* 1. Fresh cache hit → 'cache' (no network)
|
|
139
|
+
* 2. Miss/expired + token → 'network' (fetch + refresh cache)
|
|
140
|
+
* 3. Net fails + any cache → 'stale-cache' (serve last good payload)
|
|
141
|
+
* 4. Net fails + no cache → raise
|
|
142
|
+
* Auth errors are never swallowed — they propagate so health surfaces
|
|
143
|
+
* 'blocked' rather than serving stale data behind a dead token.
|
|
144
|
+
*/
|
|
145
|
+
getPayload() {
|
|
146
|
+
const cached = this.cache.read();
|
|
147
|
+
if (cached !== null && cached.isFresh(this.cacheTtlSeconds)) {
|
|
148
|
+
return { payload: cached.payload, source: "cache" };
|
|
149
|
+
}
|
|
150
|
+
const token = this.authProvider ? this.authProvider() : null;
|
|
151
|
+
if (!token) {
|
|
152
|
+
// No credential: if we have any cache, serve it stale; else blocked.
|
|
153
|
+
if (cached !== null)
|
|
154
|
+
return { payload: cached.payload, source: "stale-cache" };
|
|
155
|
+
throw new ParticipantAuthError(`${this.participantSlug}: no auth token available and no cache`);
|
|
156
|
+
}
|
|
157
|
+
let payload;
|
|
158
|
+
try {
|
|
159
|
+
payload = this.fetchPayload(token);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err instanceof ParticipantAuthError)
|
|
163
|
+
throw err; // never mask a bad token
|
|
164
|
+
if (err instanceof NetworkUnavailable) {
|
|
165
|
+
if (cached !== null)
|
|
166
|
+
return { payload: cached.payload, source: "stale-cache" };
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
this.cache.write(payload);
|
|
172
|
+
return { payload, source: "network" };
|
|
173
|
+
}
|
|
174
|
+
// -- Participant protocol ---------------------------------------------------
|
|
175
|
+
discover() {
|
|
176
|
+
const { source } = this.getPayload();
|
|
177
|
+
return {
|
|
178
|
+
path: this.nativePath,
|
|
179
|
+
version: `${this.participantSlug}-network-v1`,
|
|
180
|
+
metadata: { source, contract_version: NETWORK_CONTRACT_VERSION },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
exportL5(_since) {
|
|
184
|
+
const { payload } = this.getPayload();
|
|
185
|
+
return this.payloadToL5(payload);
|
|
186
|
+
}
|
|
187
|
+
exportSessions(since, limit = 100) {
|
|
188
|
+
const manifest = this.exportL5(since);
|
|
189
|
+
let sessions = manifest.recent_sessions ?? [];
|
|
190
|
+
if (since) {
|
|
191
|
+
const cutoff = since.toISOString().slice(0, 10);
|
|
192
|
+
sessions = sessions.filter((s) => (s.date || "") >= cutoff);
|
|
193
|
+
}
|
|
194
|
+
return sessions.slice(0, limit);
|
|
195
|
+
}
|
|
196
|
+
healthCheck() {
|
|
197
|
+
let source;
|
|
198
|
+
try {
|
|
199
|
+
source = this.getPayload().source;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof ParticipantAuthError) {
|
|
203
|
+
return {
|
|
204
|
+
status: "blocked",
|
|
205
|
+
reason: String(err.message),
|
|
206
|
+
details: { participant: this.participantSlug },
|
|
207
|
+
proposedFix: `Provide a valid credential for ${this.participantSlug} ` +
|
|
208
|
+
"(see the adapter docs for the auth path) and re-run `bourdon export-all`.",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (err instanceof NetworkUnavailable) {
|
|
212
|
+
return {
|
|
213
|
+
status: "degraded",
|
|
214
|
+
reason: `network unavailable and no cache: ${err.message}`,
|
|
215
|
+
details: { participant: this.participantSlug },
|
|
216
|
+
proposedFix: "Check connectivity, then re-run `bourdon export-all`.",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Defense in depth: health_check must NEVER raise. An adapter's
|
|
220
|
+
// fetchPayload should only raise the two contract errors; anything else is
|
|
221
|
+
// reported as blocked rather than propagated.
|
|
222
|
+
return {
|
|
223
|
+
status: "blocked",
|
|
224
|
+
reason: `unexpected error resolving payload: ${String(err)}`,
|
|
225
|
+
details: { participant: this.participantSlug },
|
|
226
|
+
proposedFix: `${this.participantSlug} raised an unexpected error during healthCheck; ` +
|
|
227
|
+
"its fetchPayload should only raise ParticipantAuthError / NetworkUnavailable.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const cached = this.cache.read();
|
|
231
|
+
const details = { participant: this.participantSlug, source };
|
|
232
|
+
if (cached !== null) {
|
|
233
|
+
details["cache_age_seconds"] = Math.round(cached.ageSeconds * 10) / 10;
|
|
234
|
+
details["cache_fetched_at"] = new Date(cached.fetchedAt * 1000)
|
|
235
|
+
.toISOString()
|
|
236
|
+
.replace(/\.\d{3}Z$/, "+00:00");
|
|
237
|
+
}
|
|
238
|
+
if (source === "stale-cache") {
|
|
239
|
+
return {
|
|
240
|
+
status: "degraded",
|
|
241
|
+
reason: "serving stale cache (network unavailable or no token)",
|
|
242
|
+
details,
|
|
243
|
+
proposedFix: "Restore connectivity / refresh the token to update.",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { status: "ok", details };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code participant — reads claude-brain + auto-memory + MCP knowledge
|
|
3
|
+
* graph. Port of `participants/claude_code.py`.
|
|
4
|
+
*
|
|
5
|
+
* Three optional sources (the participant degrades gracefully):
|
|
6
|
+
* 1. claude-brain/ — git-synced markdown records
|
|
7
|
+
* 2. ~/.claude/projects/<ws>/memory/ — per-machine auto-memory frontmatter
|
|
8
|
+
* 3. ~/claude-memory/memory.jsonl — MCP knowledge-graph JSONL
|
|
9
|
+
*
|
|
10
|
+
* Privacy: entities typed `person` (+ family/contact variants) and observations
|
|
11
|
+
* containing credential-like strings default to PRIVATE and are filtered before
|
|
12
|
+
* federation. Entities are deduped by case-insensitive name and sorted
|
|
13
|
+
* alphabetically.
|
|
14
|
+
*/
|
|
15
|
+
import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
|
|
16
|
+
import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
|
|
17
|
+
/** External participant for Anthropic's Claude Code CLI. */
|
|
18
|
+
export declare class ClaudeCodeParticipant implements BourdonParticipant {
|
|
19
|
+
agentId: string;
|
|
20
|
+
agentType: string;
|
|
21
|
+
nativePath: string;
|
|
22
|
+
/** Public so a hermetic test can seed the discovered sources directly (path
|
|
23
|
+
* RESOLUTION is not the parity contract — only the export output shape is). */
|
|
24
|
+
brainPath: string | null;
|
|
25
|
+
autoMemoryPath: string | null;
|
|
26
|
+
knowledgeGraphPath: string | null;
|
|
27
|
+
static defaultNativePath(home?: string): string;
|
|
28
|
+
constructor();
|
|
29
|
+
discover(): AgentStore;
|
|
30
|
+
exportL5(since?: Date): L5ManifestModel;
|
|
31
|
+
exportSessions(since?: Date, limit?: number): SessionModel[];
|
|
32
|
+
healthCheck(): HealthStatus;
|
|
33
|
+
}
|