@getbourdon/participants 0.2.3 → 0.3.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.
Files changed (48) hide show
  1. package/LICENSE +1 -1
  2. package/LICENSE_FAQ.md +1 -1
  3. package/dist/base.d.ts +109 -0
  4. package/dist/base.js +88 -0
  5. package/dist/index.cjs +12 -7
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts +42 -1083
  8. package/dist/index.js +53 -6590
  9. package/dist/index.js.map +1 -1
  10. package/dist/network-base.d.ts +108 -0
  11. package/dist/network-base.js +248 -0
  12. package/dist/readers/cascade.d.ts +49 -0
  13. package/dist/readers/cascade.js +428 -0
  14. package/dist/readers/claude-code-automations.d.ts +62 -0
  15. package/dist/readers/claude-code-automations.js +693 -0
  16. package/dist/readers/claude-code.d.ts +50 -0
  17. package/dist/readers/claude-code.js +590 -0
  18. package/dist/readers/claude-desktop-code.d.ts +54 -0
  19. package/dist/readers/claude-desktop-code.js +526 -0
  20. package/dist/readers/claude-desktop-cowork.d.ts +47 -0
  21. package/dist/readers/claude-desktop-cowork.js +659 -0
  22. package/dist/readers/codex-automations.d.ts +55 -0
  23. package/dist/readers/codex-automations.js +519 -0
  24. package/dist/readers/codex-sessions.d.ts +88 -0
  25. package/dist/readers/codex-sessions.js +194 -0
  26. package/dist/readers/copilot-automations.d.ts +64 -0
  27. package/dist/readers/copilot-automations.js +530 -0
  28. package/dist/readers/copilot-cli.d.ts +45 -0
  29. package/dist/readers/copilot-cli.js +416 -0
  30. package/dist/readers/copilot-vscode.d.ts +54 -0
  31. package/dist/readers/copilot-vscode.js +553 -0
  32. package/dist/readers/copilot.d.ts +59 -0
  33. package/dist/readers/copilot.js +413 -0
  34. package/dist/readers/cursor-automations.d.ts +57 -0
  35. package/dist/readers/cursor-automations.js +685 -0
  36. package/dist/readers/cursor.d.ts +50 -0
  37. package/dist/readers/cursor.js +670 -0
  38. package/dist/readers/github-copilot.d.ts +34 -0
  39. package/dist/readers/github-copilot.js +242 -0
  40. package/dist/readers/hermes.d.ts +37 -0
  41. package/dist/readers/hermes.js +426 -0
  42. package/dist/readers/openclaw.d.ts +78 -0
  43. package/dist/readers/openclaw.js +386 -0
  44. package/dist/registry.d.ts +39 -0
  45. package/dist/registry.js +98 -0
  46. package/dist/sqlite-base.d.ts +65 -0
  47. package/dist/sqlite-base.js +163 -0
  48. package/package.json +6 -6
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Codex session-index title lookup — narrow port of the three
3
+ * `session_thread_titles` helpers in `participants/codex.py`.
4
+ *
5
+ * SCOPE (deliberately tiny): this is NOT the Codex participant/reader (the
6
+ * ~2.6k-line turn-compiler still lands in a follow-on slice). It ports ONLY the
7
+ * surface the tray/presence join needs — a `session_id -> human thread title`
8
+ * map read from `~/.codex/session_index.jsonl`.
9
+ *
10
+ * Why only the index: `session_thread_titles()` runs on EVERY tray refresh, so
11
+ * it must never touch rollout files. One small JSONL read, newest-first, done.
12
+ * It covers Codex CLI sessions AND ChatGPT-app threads — the app's Work and
13
+ * Codex modes share `~/.codex`, so `thread_name` is the human title for both.
14
+ *
15
+ * PRIVACY: titles are raw user text. Callers that EXPORT them MUST route them
16
+ * through the audited redaction pipeline (`redactField` in
17
+ * `mcp-server/src/agents-export.ts`, == Python's `_redact_field`), exactly like
18
+ * every other emitted string. This module deliberately does NOT redact — the
19
+ * redaction happens at the single audited egress point so it can't be
20
+ * double-applied or silently skipped.
21
+ *
22
+ * Best-effort by construction: a missing dir, an unreadable file, or a corrupt
23
+ * line degrades to fewer/no titles. Nothing here throws.
24
+ */
25
+ import { readFileSync, statSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { join } from "node:path";
28
+ /** Default number of index records considered (newest-first). */
29
+ export const DEFAULT_TITLE_LIMIT = 200;
30
+ /** Default number of threads on the tray codex card (newest-first). */
31
+ export const DEFAULT_RECENT_THREAD_LIMIT = 5;
32
+ /** Collapse internal whitespace runs — mirrors Python `_normalize_text`. */
33
+ function normalizeText(value) {
34
+ return value.trim().replace(/\s+/g, " ");
35
+ }
36
+ function isDir(path) {
37
+ try {
38
+ return statSync(path).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Locate `~/.codex/` (the primary Codex store), or null.
46
+ *
47
+ * Resolution order, mirroring `_resolve_codex_home()` exactly:
48
+ * 1. `$CODEX_HOME` — **only if that directory already exists**
49
+ * 2. `~/.codex` — only if it exists
50
+ * 3. `null`
51
+ *
52
+ * ⚠ FOOTGUN (preserved for parity, do not "fix" without changing Python too):
53
+ * step 1 is a silent fallback, not a pin. If `$CODEX_HOME` points at a path that
54
+ * does not exist yet, the env var is IGNORED and this returns the developer's
55
+ * REAL `~/.codex`. A sandbox/test that sets `CODEX_HOME` to a temp path without
56
+ * `mkdir`-ing it first looks isolated but reads live data. Always create the
57
+ * directory before setting the variable.
58
+ */
59
+ export function resolveCodexHome(baseHome) {
60
+ const env = process.env.CODEX_HOME;
61
+ if (env) {
62
+ // Existence check FIRST — see the footgun note above.
63
+ if (isDir(env))
64
+ return env;
65
+ }
66
+ const home = baseHome ? baseHome : homedir();
67
+ const candidate = join(home, ".codex");
68
+ return isDir(candidate) ? candidate : null;
69
+ }
70
+ /**
71
+ * Read `session_index.jsonl`, newest-first by `updated_at`.
72
+ *
73
+ * Port of `_parse_session_index()`. Per-line JSON parse: a malformed line is
74
+ * WARNED and SKIPPED — it never aborts the scan (one bad append must not blind
75
+ * the tray to every other session). An unreadable file warns and yields `[]`.
76
+ * Only records that are objects with a string `id` are kept.
77
+ */
78
+ export function parseSessionIndex(path, limit) {
79
+ let text;
80
+ try {
81
+ if (!statSync(path).isFile())
82
+ return [];
83
+ text = readFileSync(path, "utf8");
84
+ }
85
+ catch (exc) {
86
+ // Covers both "missing" (Python's `is_file()` guard) and OSError-on-read.
87
+ if (existsQuiet(path))
88
+ console.warn(`Failed to read ${path}: ${String(exc)}`);
89
+ return [];
90
+ }
91
+ const entries = [];
92
+ const lines = text.split(/\r?\n/);
93
+ for (let i = 0; i < lines.length; i += 1) {
94
+ const line = lines[i].trim();
95
+ if (!line)
96
+ continue;
97
+ let record;
98
+ try {
99
+ record = JSON.parse(line);
100
+ }
101
+ catch (exc) {
102
+ console.warn(`Skipping malformed JSONL in ${path}:${i + 1} -- ${String(exc)}`);
103
+ continue;
104
+ }
105
+ if (record !== null &&
106
+ typeof record === "object" &&
107
+ !Array.isArray(record) &&
108
+ typeof record.id === "string") {
109
+ entries.push(record);
110
+ }
111
+ }
112
+ // Descending by `updated_at` (missing key sorts as ""), stable on ties —
113
+ // mirrors Python's `sort(key=..., reverse=True)` on a stable sort.
114
+ entries.sort((a, b) => {
115
+ const ua = String(a.updated_at ?? "");
116
+ const ub = String(b.updated_at ?? "");
117
+ return ua < ub ? 1 : ua > ub ? -1 : 0;
118
+ });
119
+ return limit !== undefined && limit !== null ? entries.slice(0, limit) : entries;
120
+ }
121
+ /** True if the path exists at all (distinguishes "missing" from "unreadable"). */
122
+ function existsQuiet(path) {
123
+ try {
124
+ statSync(path);
125
+ return true;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ /**
132
+ * Map full Codex session id → human thread title, newest-first.
133
+ *
134
+ * Port of `session_thread_titles()`. Reads ONLY `session_index.jsonl` — no
135
+ * rollout I/O, because this runs on every tray refresh. A missing/malformed
136
+ * index yields `{}`.
137
+ *
138
+ * NEWEST WINS: entries are already sorted newest-first and the insert is a
139
+ * `setdefault` equivalent, so the freshest `thread_name` for a repeated id is
140
+ * the one kept.
141
+ *
142
+ * Titles are RAW USER TEXT — see the module note; the exporter redacts.
143
+ */
144
+ export function sessionThreadTitles(codexHome, opts = {}) {
145
+ const limit = opts.limit ?? DEFAULT_TITLE_LIMIT;
146
+ const home = codexHome === undefined || codexHome === null ? resolveCodexHome() : codexHome;
147
+ if (home === null)
148
+ return {};
149
+ const titles = {};
150
+ for (const entry of parseSessionIndex(join(home, "session_index.jsonl"), limit)) {
151
+ const sid = entry.id;
152
+ const name = entry.thread_name;
153
+ if (typeof sid === "string" &&
154
+ sid &&
155
+ typeof name === "string" &&
156
+ name.trim() &&
157
+ !(sid in titles) // setdefault: first (== newest) wins
158
+ ) {
159
+ titles[sid] = normalizeText(name);
160
+ }
161
+ }
162
+ return titles;
163
+ }
164
+ /**
165
+ * Newest-first `[{instance, title, updated_at}]` for the tray codex card.
166
+ *
167
+ * Port of `recent_session_threads()`. Independent of PRESENCE: ChatGPT-app
168
+ * threads fire no presence hooks, so this is the only way they surface.
169
+ *
170
+ * Emits only the 8-char `instance` prefix — parity with `presence.summarize`:
171
+ * a full session id never leaves the local read path. Titles are RAW USER TEXT;
172
+ * the exporter redacts them (see `sessionThreadTitles`).
173
+ */
174
+ export function recentSessionThreads(codexHome, opts = {}) {
175
+ const limit = opts.limit ?? DEFAULT_RECENT_THREAD_LIMIT;
176
+ const home = codexHome === undefined || codexHome === null ? resolveCodexHome() : codexHome;
177
+ if (home === null)
178
+ return [];
179
+ const threads = [];
180
+ for (const entry of parseSessionIndex(join(home, "session_index.jsonl"), limit)) {
181
+ const sid = entry.id;
182
+ if (typeof sid !== "string" || !sid)
183
+ continue;
184
+ const name = entry.thread_name;
185
+ const title = typeof name === "string" && name.trim() ? normalizeText(name) : null;
186
+ const updated = String(entry.updated_at ?? "");
187
+ threads.push({
188
+ instance: sid.slice(0, 8),
189
+ title,
190
+ updated_at: updated || null,
191
+ });
192
+ }
193
+ return threads;
194
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * GitHub Copilot · Automations participant — convention-file reader. Port of
3
+ * `participants/copilot_automations.py`.
4
+ *
5
+ * GitHub Copilot can be triggered by GitHub Actions workflows, scheduled tasks,
6
+ * and event-driven automations (issue triage, PR review, etc.). These run on a
7
+ * separate memory surface from both the CLI agent and the VS Code extension.
8
+ * Copilot automations have no standardized local state dump, so — mirroring the
9
+ * codex-automations reader — this participant uses a CONVENTION directory:
10
+ *
11
+ * ~/.copilot-bourdon/automations/<automation-id>/
12
+ * automation.toml — id, name, status, trigger, kind, repos
13
+ * memory.md — dated bullet entries, one block per run
14
+ *
15
+ * Each `automation.toml` describes a recurring automation; each dated section of
16
+ * `memory.md` captures what happened in that run. Users or CI scripts maintain
17
+ * these files.
18
+ *
19
+ * Path resolution (matches the oracle):
20
+ * 1. `COPILOT_AUTOMATIONS_HOME` env-var override (verbatim, → /automations skipped:
21
+ * the override IS the automations dir).
22
+ * 2. `~/.copilot-bourdon/automations/` (default convention path).
23
+ *
24
+ * Defensive throughout: a missing automations directory raises
25
+ * {@link ParticipantDiscoveryError} from `discover()`; a malformed
26
+ * `automation.toml` or unreadable `memory.md` is logged and skipped rather than
27
+ * raising; `healthCheck` never throws.
28
+ */
29
+ import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
30
+ import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
31
+ /**
32
+ * Return the conventional `~/.copilot-bourdon/automations/` directory. Respects
33
+ * the `COPILOT_AUTOMATIONS_HOME` env-var override (returned verbatim — the
34
+ * override IS the automations dir, no `/automations` suffix is appended).
35
+ */
36
+ export declare function defaultCopilotAutomationsDir(copilotBourdonDir?: string | null): string;
37
+ /**
38
+ * Convention-based Bourdon participant for Copilot automation memory artifacts.
39
+ * Users or CI scripts maintain `automation.toml` + `memory.md` files at
40
+ * `~/.copilot-bourdon/automations/<id>/`.
41
+ */
42
+ export declare class CopilotAutomationsParticipant implements BourdonParticipant {
43
+ agentId: string;
44
+ agentType: string;
45
+ displayName: string;
46
+ private readonly automationsDir;
47
+ private readonly policy;
48
+ /** Conventional automations dir (`~/.copilot-bourdon/automations`). Does NOT
49
+ * consult `COPILOT_AUTOMATIONS_HOME` (matches Python `default_native_path`). */
50
+ static defaultNativePath(home?: string): string;
51
+ constructor(automationsDir?: string | null);
52
+ get nativePath(): string;
53
+ discover(): AgentStore;
54
+ exportSessions(since?: Date, limit?: number): SessionModel[];
55
+ exportL5(since?: Date): L5ManifestModel;
56
+ healthCheck(): HealthStatus;
57
+ private runs;
58
+ }
59
+ /**
60
+ * Create an automation scaffold at `<automationsDir>/<id>/`. Returns the path
61
+ * of the created automation directory. Throws when `automation.toml` already
62
+ * exists unless `force` is true.
63
+ */
64
+ export declare function initAutomation(automationsDir?: string | null, automationId?: string, name?: string, force?: boolean): string;