@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.
- package/LICENSE +1 -1
- package/LICENSE_FAQ.md +1 -1
- package/dist/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.cjs +12 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +42 -1083
- package/dist/index.js +53 -6590
- 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 +590 -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,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot · VS Code participant — reads the GUI Copilot Chat extension's
|
|
3
|
+
* per-workspace transcripts + repo-scoped memory. Port of
|
|
4
|
+
* `participants/copilot_vscode.py`.
|
|
5
|
+
*
|
|
6
|
+
* VS Code's Copilot Chat extension stores per-workspace data at
|
|
7
|
+
* `<workspaceStorage>/<hash>/GitHub.copilot-chat/`:
|
|
8
|
+
* - `transcripts/<session-uuid>.jsonl` — event stream per chat session
|
|
9
|
+
* - `memory-tool/memories/**\/*.md` — per-repo memory (markdown bullets)
|
|
10
|
+
*
|
|
11
|
+
* This is a FILE/CONVENTION reader (not SQLite, not network): it walks the
|
|
12
|
+
* workspaceStorage tree read-only, parses JSONL transcripts + markdown memory
|
|
13
|
+
* files, aggregates across every workspace hash, redacts every native string,
|
|
14
|
+
* applies the visibility filter BEFORE emission, and is deterministic.
|
|
15
|
+
* `healthCheck` never throws.
|
|
16
|
+
*
|
|
17
|
+
* Distinct from:
|
|
18
|
+
* - `copilot` (convention-file at `~/.copilot-bourdon/memory.md`)
|
|
19
|
+
* - `copilot-cli` (terminal agent, `~/.copilot/session-store.db`, SQLite)
|
|
20
|
+
* - `github-copilot` (network reader — GitHub-embedded Copilot)
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
23
|
+
import { homedir, hostname, platform } from "node:os";
|
|
24
|
+
import { join, relative } from "node:path";
|
|
25
|
+
import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
|
|
26
|
+
import { redactText } from "@getbourdon/redaction";
|
|
27
|
+
import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
|
|
28
|
+
const AGENT_ID = "copilot-vscode";
|
|
29
|
+
const AGENT_TYPE = "code-assistant";
|
|
30
|
+
const DISPLAY_NAME = "GitHub Copilot · VS Code";
|
|
31
|
+
const ROLE_NARRATIVE = "VS Code integrated Copilot Chat — the GUI surface for inline completion, " +
|
|
32
|
+
"chat, plan mode, ask mode, and explore mode. Persists per-workspace " +
|
|
33
|
+
"transcripts and repo-scoped memory locally. The most widely-used Copilot " +
|
|
34
|
+
"surface by session count.";
|
|
35
|
+
const DEFAULT_POLICY = makeVisibilityPolicy({
|
|
36
|
+
default: Visibility.TEAM,
|
|
37
|
+
private_tags: ["personal", "financial", "credential", "health", "family", "legal"],
|
|
38
|
+
team_tags: ["copilot-vscode", "copilot", "vscode", "workspace"],
|
|
39
|
+
});
|
|
40
|
+
const COPILOT_CHAT_EXT = "GitHub.copilot-chat";
|
|
41
|
+
const TRANSCRIPTS_DIR = "transcripts";
|
|
42
|
+
const MEMORY_TOOL_DIR = "memory-tool";
|
|
43
|
+
const MAX_KEY_ACTIONS = 6;
|
|
44
|
+
const MAX_KEY_ACTION_CHARS = 280;
|
|
45
|
+
const MAX_SUMMARY_CHARS = 260;
|
|
46
|
+
const MAX_TRANSCRIPT_EVENTS = 500; // cap per file to avoid loading multi-MB transcripts fully
|
|
47
|
+
// -- fs helpers ---------------------------------------------------------------
|
|
48
|
+
function homeDir() {
|
|
49
|
+
// os.homedir() can throw under some sandboxes; stay defensive so import +
|
|
50
|
+
// construction never crash even when HOME points at an unreadable path.
|
|
51
|
+
try {
|
|
52
|
+
return homedir();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return process.env.HOME || process.env.USERPROFILE || ".";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isDir(p) {
|
|
59
|
+
try {
|
|
60
|
+
return statSync(p).isDirectory();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Redact secrets + clamp to the native-memory limit (`_safe_native_memory_text`). */
|
|
67
|
+
function safeNativeMemoryText(value, limit = 180) {
|
|
68
|
+
return redactText(value, limit);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Collapse whitespace and clamp with a single-char ellipsis (mirrors the
|
|
72
|
+
* oracle's `_bounded`). Distinct from {@link redactText}'s `...` clamp — this
|
|
73
|
+
* one uses `…` (U+2026) and does no redaction.
|
|
74
|
+
*/
|
|
75
|
+
function bounded(value, limit) {
|
|
76
|
+
const normalized = value.split(/\s+/).filter(Boolean).join(" ");
|
|
77
|
+
if (normalized.length <= limit)
|
|
78
|
+
return normalized;
|
|
79
|
+
return normalized.slice(0, limit - 1).replace(/\s+$/, "") + "…";
|
|
80
|
+
}
|
|
81
|
+
/** Python `str.splitlines()` — split on \r\n, \r, or \n. */
|
|
82
|
+
function splitLines(text) {
|
|
83
|
+
return text.split(/\r\n|\r|\n/);
|
|
84
|
+
}
|
|
85
|
+
// -- Path resolution ----------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Return the VS Code workspaceStorage path for this platform, or null when it
|
|
88
|
+
* cannot be resolved (Windows without `%APPDATA%`). Respects the
|
|
89
|
+
* `COPILOT_VSCODE_STORAGE` environment-variable override. Mirrors
|
|
90
|
+
* `default_vscode_workspace_storage_dir`.
|
|
91
|
+
*/
|
|
92
|
+
export function defaultVscodeWorkspaceStorageDir() {
|
|
93
|
+
const env = process.env.COPILOT_VSCODE_STORAGE;
|
|
94
|
+
if (env)
|
|
95
|
+
return env;
|
|
96
|
+
// Map Node's os.platform() onto Python's platform.system().
|
|
97
|
+
const system = platform();
|
|
98
|
+
if (system === "win32") {
|
|
99
|
+
const appdata = process.env.APPDATA;
|
|
100
|
+
if (appdata)
|
|
101
|
+
return join(appdata, "Code", "User", "workspaceStorage");
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (system === "darwin") {
|
|
105
|
+
return join(homeDir(), "Library", "Application Support", "Code", "User", "workspaceStorage");
|
|
106
|
+
}
|
|
107
|
+
// Linux / WSL — check the Windows path under /mnt/c if WSL, else native.
|
|
108
|
+
const wslPath = "/mnt/c/Users";
|
|
109
|
+
if (isDir(wslPath)) {
|
|
110
|
+
try {
|
|
111
|
+
for (const candidate of readdirSync(wslPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const ws = join(wslPath, candidate, "AppData", "Roaming", "Code", "User", "workspaceStorage");
|
|
114
|
+
if (isDir(ws))
|
|
115
|
+
return ws;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// fall through to native Linux
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const config = process.env.XDG_CONFIG_HOME || join(homeDir(), ".config");
|
|
127
|
+
return join(config, "Code", "User", "workspaceStorage");
|
|
128
|
+
}
|
|
129
|
+
/** Find all workspace hashes that contain Copilot Chat data. */
|
|
130
|
+
function findCopilotChatDirs(workspaceStorage) {
|
|
131
|
+
if (!isDir(workspaceStorage))
|
|
132
|
+
return [];
|
|
133
|
+
const dirs = [];
|
|
134
|
+
try {
|
|
135
|
+
for (const wsHash of readdirSync(workspaceStorage)) {
|
|
136
|
+
const chatDir = join(workspaceStorage, wsHash, COPILOT_CHAT_EXT);
|
|
137
|
+
if (isDir(chatDir))
|
|
138
|
+
dirs.push(chatDir);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (exc) {
|
|
142
|
+
logger.warn(`CopilotVscodeParticipant: cannot scan workspace storage: ${String(exc)}`);
|
|
143
|
+
}
|
|
144
|
+
return dirs;
|
|
145
|
+
}
|
|
146
|
+
/** Minimal logger so a scan warning is visible without a dep. */
|
|
147
|
+
const logger = {
|
|
148
|
+
warn(msg) {
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.warn(`[bourdon.participants] ${msg}`);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Parse a JSONL transcript file into a session summary. Returns null on any
|
|
155
|
+
* read/empty error. Never raises.
|
|
156
|
+
*/
|
|
157
|
+
function parseTranscript(path) {
|
|
158
|
+
let text;
|
|
159
|
+
try {
|
|
160
|
+
text = readFileSync(path, "utf8");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const lines = text.trim() ? splitLines(text.trim()) : [];
|
|
166
|
+
if (lines.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const info = {
|
|
169
|
+
transcript_path: path,
|
|
170
|
+
session_id: stem(path),
|
|
171
|
+
start_time: null,
|
|
172
|
+
producer: null,
|
|
173
|
+
copilot_version: null,
|
|
174
|
+
vscode_version: null,
|
|
175
|
+
user_messages: [],
|
|
176
|
+
turn_count: 0,
|
|
177
|
+
};
|
|
178
|
+
let turnCount = 0;
|
|
179
|
+
const userMsgs = [];
|
|
180
|
+
for (const line of lines.slice(0, MAX_TRANSCRIPT_EVENTS)) {
|
|
181
|
+
let event;
|
|
182
|
+
try {
|
|
183
|
+
event = JSON.parse(line);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (typeof event !== "object" || event === null)
|
|
189
|
+
continue;
|
|
190
|
+
const ev = event;
|
|
191
|
+
const eventType = ev["type"];
|
|
192
|
+
const dataRaw = ev["data"];
|
|
193
|
+
const data = typeof dataRaw === "object" && dataRaw !== null
|
|
194
|
+
? dataRaw
|
|
195
|
+
: {};
|
|
196
|
+
if (eventType === "session.start") {
|
|
197
|
+
info.start_time = data["startTime"] == null ? null : String(data["startTime"]);
|
|
198
|
+
info.producer = data["producer"] ?? null;
|
|
199
|
+
info.copilot_version = data["copilotVersion"] ?? null;
|
|
200
|
+
info.vscode_version = data["vscodeVersion"] ?? null;
|
|
201
|
+
}
|
|
202
|
+
else if (eventType === "user.message") {
|
|
203
|
+
const content = data["content"] ? String(data["content"]) : "";
|
|
204
|
+
if (content) {
|
|
205
|
+
// Truncate long messages but keep enough for entity extraction.
|
|
206
|
+
userMsgs.push(content.slice(0, 200));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (eventType === "assistant.turn_end") {
|
|
210
|
+
turnCount += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
info.user_messages = userMsgs.slice(0, 20);
|
|
214
|
+
info.turn_count = turnCount;
|
|
215
|
+
return info;
|
|
216
|
+
}
|
|
217
|
+
/** Basename minus final extension (Python `Path.stem`). */
|
|
218
|
+
function stem(p) {
|
|
219
|
+
const base = p.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "";
|
|
220
|
+
return base.replace(/\.[^.]+$/, "");
|
|
221
|
+
}
|
|
222
|
+
/** Recursively collect `*.md` files under `dir`, sorted for determinism. */
|
|
223
|
+
function walkMarkdown(dir) {
|
|
224
|
+
const out = [];
|
|
225
|
+
let entries;
|
|
226
|
+
try {
|
|
227
|
+
entries = readdirSync(dir).sort();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
for (const name of entries) {
|
|
233
|
+
const full = join(dir, name);
|
|
234
|
+
if (isDir(full)) {
|
|
235
|
+
out.push(...walkMarkdown(full));
|
|
236
|
+
}
|
|
237
|
+
else if (name.endsWith(".md")) {
|
|
238
|
+
out.push(full);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse memory-tool markdown files from a Copilot Chat workspace. Each file is a
|
|
245
|
+
* bulleted list of learned facts. Returns a list of {name, content, path, ...}.
|
|
246
|
+
*/
|
|
247
|
+
function parseMemoryFiles(chatDir) {
|
|
248
|
+
const memoryDir = join(chatDir, MEMORY_TOOL_DIR, "memories");
|
|
249
|
+
if (!isDir(memoryDir))
|
|
250
|
+
return [];
|
|
251
|
+
const memories = [];
|
|
252
|
+
for (const mdFile of walkMarkdown(memoryDir)) {
|
|
253
|
+
let text;
|
|
254
|
+
try {
|
|
255
|
+
text = readFileSync(mdFile, "utf8");
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!text.trim())
|
|
261
|
+
continue;
|
|
262
|
+
// Name from relative path (e.g. "repo/mobile-scaffold"), suffix stripped,
|
|
263
|
+
// backslashes normalized to forward slashes.
|
|
264
|
+
const rel = relative(memoryDir, mdFile);
|
|
265
|
+
const name = rel.replace(/\.[^./\\]*$/, "").replace(/\\/g, "/");
|
|
266
|
+
const bulletCount = text.split("\n- ").length - 1 + (text.startsWith("- ") ? 1 : 0);
|
|
267
|
+
memories.push({ name, content: text, path: mdFile, bullet_count: bulletCount });
|
|
268
|
+
}
|
|
269
|
+
return memories;
|
|
270
|
+
}
|
|
271
|
+
// -- Conversion to Bourdon types ----------------------------------------------
|
|
272
|
+
function sessionFromTranscript(info) {
|
|
273
|
+
const start = info.start_time ?? "";
|
|
274
|
+
const dateStr = start ? start.slice(0, 10) : "";
|
|
275
|
+
const keyActions = [];
|
|
276
|
+
const msgs = info.user_messages ?? [];
|
|
277
|
+
if (msgs.length > 0) {
|
|
278
|
+
keyActions.push(bounded(safeNativeMemoryText(String(msgs[0])), MAX_KEY_ACTION_CHARS));
|
|
279
|
+
}
|
|
280
|
+
if (info.turn_count)
|
|
281
|
+
keyActions.push(`turns: ${info.turn_count}`);
|
|
282
|
+
if (info.copilot_version)
|
|
283
|
+
keyActions.push(`copilot: ${String(info.copilot_version)}`);
|
|
284
|
+
if (info.producer)
|
|
285
|
+
keyActions.push(`mode: ${String(info.producer)}`);
|
|
286
|
+
return makeSession({
|
|
287
|
+
date: dateStr,
|
|
288
|
+
cwd: undefined,
|
|
289
|
+
project_focus: [],
|
|
290
|
+
key_actions: keyActions.slice(0, MAX_KEY_ACTIONS),
|
|
291
|
+
files_touched: [],
|
|
292
|
+
visibility: Visibility.TEAM,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function entitiesFromMemories(memories) {
|
|
296
|
+
const entities = [];
|
|
297
|
+
for (const mem of memories) {
|
|
298
|
+
const bullets = [];
|
|
299
|
+
for (const line of splitLines(mem.content)) {
|
|
300
|
+
const stripped = line.trim();
|
|
301
|
+
if (stripped.startsWith("- ")) {
|
|
302
|
+
bullets.push(stripped.slice(2).trim());
|
|
303
|
+
if (bullets.length === 3)
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let summary = bullets.length > 0 ? bullets.join("; ") : undefined;
|
|
308
|
+
if (summary)
|
|
309
|
+
summary = bounded(safeNativeMemoryText(summary), MAX_SUMMARY_CHARS);
|
|
310
|
+
entities.push(makeEntity({
|
|
311
|
+
name: mem.name,
|
|
312
|
+
type: "vscode-memory",
|
|
313
|
+
summary,
|
|
314
|
+
last_touched: undefined,
|
|
315
|
+
tags: ["copilot-vscode", "memory-tool", "repo-memory"],
|
|
316
|
+
visibility: Visibility.TEAM,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
return entities;
|
|
320
|
+
}
|
|
321
|
+
function entitiesFromTranscripts(transcripts) {
|
|
322
|
+
// Simple frequency-based extraction from first messages (insertion-ordered).
|
|
323
|
+
const topicCounts = new Map();
|
|
324
|
+
for (const info of transcripts) {
|
|
325
|
+
const msgs = info.user_messages ?? [];
|
|
326
|
+
if (msgs.length > 0) {
|
|
327
|
+
const first = String(msgs[0]).slice(0, 100);
|
|
328
|
+
const slashMatch = /^\/(\S+)/.exec(first);
|
|
329
|
+
if (slashMatch) {
|
|
330
|
+
const topic = `copilot-command:${slashMatch[1]}`;
|
|
331
|
+
topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// sorted(items, key=lambda x: -x[1])[:10] — stable, so ties keep insertion order.
|
|
336
|
+
const ordered = [...topicCounts.entries()]
|
|
337
|
+
.sort((a, b) => b[1] - a[1])
|
|
338
|
+
.slice(0, 10);
|
|
339
|
+
const entities = [];
|
|
340
|
+
for (const [topic, count] of ordered) {
|
|
341
|
+
entities.push(makeEntity({
|
|
342
|
+
name: topic,
|
|
343
|
+
type: "vscode-capability",
|
|
344
|
+
summary: `VS Code Copilot command used ${count} time(s).`,
|
|
345
|
+
tags: ["copilot-vscode", "capability"],
|
|
346
|
+
visibility: Visibility.TEAM,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
return entities;
|
|
350
|
+
}
|
|
351
|
+
// -- Participant --------------------------------------------------------------
|
|
352
|
+
/**
|
|
353
|
+
* External participant for GitHub Copilot in VS Code. Reads transcripts and
|
|
354
|
+
* memory-tool files from VS Code's workspaceStorage read-only, aggregating
|
|
355
|
+
* across all workspace hashes.
|
|
356
|
+
*/
|
|
357
|
+
export class CopilotVscodeParticipant {
|
|
358
|
+
agentId = AGENT_ID;
|
|
359
|
+
agentType = AGENT_TYPE;
|
|
360
|
+
displayName = DISPLAY_NAME;
|
|
361
|
+
workspaceStorage;
|
|
362
|
+
/** The workspaceStorage dir the setup wizard probes for presence. */
|
|
363
|
+
static defaultNativePath(home) {
|
|
364
|
+
if (home !== undefined) {
|
|
365
|
+
return join(home, ".config", "Code", "User", "workspaceStorage");
|
|
366
|
+
}
|
|
367
|
+
let ws;
|
|
368
|
+
try {
|
|
369
|
+
ws = defaultVscodeWorkspaceStorageDir();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
ws = null;
|
|
373
|
+
}
|
|
374
|
+
if (ws !== null)
|
|
375
|
+
return ws;
|
|
376
|
+
return join(homeDir(), ".config", "Code", "User", "workspaceStorage");
|
|
377
|
+
}
|
|
378
|
+
constructor(workspaceStorage) {
|
|
379
|
+
this.workspaceStorage = workspaceStorage ?? null;
|
|
380
|
+
}
|
|
381
|
+
get nativePath() {
|
|
382
|
+
return this.workspaceStorage ?? defaultVscodeWorkspaceStorageDir() ?? "";
|
|
383
|
+
}
|
|
384
|
+
resolveWs() {
|
|
385
|
+
return this.workspaceStorage ?? defaultVscodeWorkspaceStorageDir();
|
|
386
|
+
}
|
|
387
|
+
// -- Protocol surface -------------------------------------------------------
|
|
388
|
+
discover() {
|
|
389
|
+
const ws = this.resolveWs();
|
|
390
|
+
if (ws === null || !isDir(ws)) {
|
|
391
|
+
throw new ParticipantDiscoveryError(`VS Code workspaceStorage not found at ${ws ?? "None"}. ` +
|
|
392
|
+
"Install VS Code with the GitHub Copilot Chat extension.");
|
|
393
|
+
}
|
|
394
|
+
const chatDirs = findCopilotChatDirs(ws);
|
|
395
|
+
if (chatDirs.length === 0) {
|
|
396
|
+
throw new ParticipantDiscoveryError(`No GitHub.copilot-chat data found under ${ws}. ` +
|
|
397
|
+
"Open VS Code and use Copilot Chat at least once.");
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
path: ws,
|
|
401
|
+
version: "transcript-v1",
|
|
402
|
+
metadata: {
|
|
403
|
+
workspace_count: chatDirs.length,
|
|
404
|
+
workspace_storage_path: ws,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
exportSessions(since, limit = 100) {
|
|
409
|
+
const sinceIso = since ? since.toISOString().slice(0, 10) : null;
|
|
410
|
+
const transcripts = this.allTranscripts();
|
|
411
|
+
const sessions = [];
|
|
412
|
+
for (const info of transcripts) {
|
|
413
|
+
const session = sessionFromTranscript(info);
|
|
414
|
+
if (sinceIso && session.date && session.date < sinceIso)
|
|
415
|
+
continue;
|
|
416
|
+
sessions.push(session);
|
|
417
|
+
if (sessions.length >= limit)
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
return sessions;
|
|
421
|
+
}
|
|
422
|
+
exportL5(since) {
|
|
423
|
+
const transcripts = this.allTranscripts();
|
|
424
|
+
const memories = this.allMemories();
|
|
425
|
+
const sinceIso = since ? since.toISOString().slice(0, 10) : null;
|
|
426
|
+
const sessions = [];
|
|
427
|
+
for (const info of transcripts) {
|
|
428
|
+
const session = sessionFromTranscript(info);
|
|
429
|
+
if (sinceIso && session.date && session.date < sinceIso)
|
|
430
|
+
continue;
|
|
431
|
+
sessions.push(session);
|
|
432
|
+
}
|
|
433
|
+
const entities = [
|
|
434
|
+
...entitiesFromMemories(memories),
|
|
435
|
+
...entitiesFromTranscripts(transcripts),
|
|
436
|
+
];
|
|
437
|
+
const visibleEntities = filterForFederation(entities, DEFAULT_POLICY);
|
|
438
|
+
return makeManifest({
|
|
439
|
+
spec_version: SPEC_VERSION,
|
|
440
|
+
agent: makeAgentInfo({
|
|
441
|
+
id: AGENT_ID,
|
|
442
|
+
type: AGENT_TYPE,
|
|
443
|
+
instance: hostname() || "unknown",
|
|
444
|
+
spec_version_compat: `>=${SPEC_VERSION}`,
|
|
445
|
+
role_narrative: ROLE_NARRATIVE,
|
|
446
|
+
}),
|
|
447
|
+
last_updated: new Date().toISOString(),
|
|
448
|
+
capabilities: [
|
|
449
|
+
"inline-completion",
|
|
450
|
+
"chat",
|
|
451
|
+
"plan-mode",
|
|
452
|
+
"ask-mode",
|
|
453
|
+
"explore-mode",
|
|
454
|
+
"memory-tool",
|
|
455
|
+
"workspace-scoped",
|
|
456
|
+
],
|
|
457
|
+
recent_sessions: sessions,
|
|
458
|
+
known_entities: visibleEntities,
|
|
459
|
+
visibility_policy: DEFAULT_POLICY,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
healthCheck() {
|
|
463
|
+
try {
|
|
464
|
+
const ws = this.resolveWs();
|
|
465
|
+
if (ws === null || !isDir(ws)) {
|
|
466
|
+
return {
|
|
467
|
+
status: "blocked",
|
|
468
|
+
reason: `VS Code workspaceStorage not found at ${ws ?? "None"}.`,
|
|
469
|
+
details: { expected_path: ws ?? "None" },
|
|
470
|
+
proposedFix: "Install VS Code and the GitHub Copilot Chat extension. " +
|
|
471
|
+
"Set COPILOT_VSCODE_STORAGE if VS Code stores data elsewhere.",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const chatDirs = findCopilotChatDirs(ws);
|
|
475
|
+
if (chatDirs.length === 0) {
|
|
476
|
+
return {
|
|
477
|
+
status: "degraded",
|
|
478
|
+
reason: "No Copilot Chat data found in any workspace.",
|
|
479
|
+
details: { workspace_storage: ws },
|
|
480
|
+
proposedFix: "Open VS Code and start a Copilot Chat conversation, " +
|
|
481
|
+
"then re-run `bourdon copilot-vscode export`.",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const transcripts = this.allTranscripts();
|
|
485
|
+
const memories = this.allMemories();
|
|
486
|
+
return {
|
|
487
|
+
status: "ok",
|
|
488
|
+
details: {
|
|
489
|
+
workspace_storage: ws,
|
|
490
|
+
workspace_count: chatDirs.length,
|
|
491
|
+
transcript_count: transcripts.length,
|
|
492
|
+
memory_file_count: memories.length,
|
|
493
|
+
total_turns: transcripts.reduce((acc, t) => acc + (t.turn_count || 0), 0),
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
// healthCheck must NEVER raise (contract).
|
|
499
|
+
return {
|
|
500
|
+
status: "degraded",
|
|
501
|
+
reason: `unexpected error: ${String(err)}`,
|
|
502
|
+
details: {},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// -- Internal ---------------------------------------------------------------
|
|
507
|
+
/** Collect and parse all transcripts across all workspaces. */
|
|
508
|
+
allTranscripts() {
|
|
509
|
+
const ws = this.resolveWs();
|
|
510
|
+
if (ws === null)
|
|
511
|
+
return [];
|
|
512
|
+
const chatDirs = findCopilotChatDirs(ws);
|
|
513
|
+
const transcripts = [];
|
|
514
|
+
for (const chatDir of chatDirs) {
|
|
515
|
+
const transcriptDir = join(chatDir, TRANSCRIPTS_DIR);
|
|
516
|
+
if (!isDir(transcriptDir))
|
|
517
|
+
continue;
|
|
518
|
+
let files;
|
|
519
|
+
try {
|
|
520
|
+
files = readdirSync(transcriptDir).sort();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
for (const fname of files) {
|
|
526
|
+
if (!fname.endsWith(".jsonl"))
|
|
527
|
+
continue;
|
|
528
|
+
const info = parseTranscript(join(transcriptDir, fname));
|
|
529
|
+
if (info !== null)
|
|
530
|
+
transcripts.push(info);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Sort by start_time descending (missing → "").
|
|
534
|
+
transcripts.sort((a, b) => {
|
|
535
|
+
const av = a.start_time ?? "";
|
|
536
|
+
const bv = b.start_time ?? "";
|
|
537
|
+
return av < bv ? 1 : av > bv ? -1 : 0;
|
|
538
|
+
});
|
|
539
|
+
return transcripts;
|
|
540
|
+
}
|
|
541
|
+
/** Collect all memory-tool files across all workspaces. */
|
|
542
|
+
allMemories() {
|
|
543
|
+
const ws = this.resolveWs();
|
|
544
|
+
if (ws === null)
|
|
545
|
+
return [];
|
|
546
|
+
const chatDirs = findCopilotChatDirs(ws);
|
|
547
|
+
const memories = [];
|
|
548
|
+
for (const chatDir of chatDirs) {
|
|
549
|
+
memories.push(...parseMemoryFiles(chatDir));
|
|
550
|
+
}
|
|
551
|
+
return memories;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot (local convention) participant — reads the convention-based
|
|
3
|
+
* memory file at `~/.copilot-bourdon/memory.md`. Port of `participants/copilot.py`.
|
|
4
|
+
*
|
|
5
|
+
* Copilot has no accessible local session index (its reasoning is cloud-side and
|
|
6
|
+
* its native memory is not exposed on disk), so — like the Codex pre-distillation
|
|
7
|
+
* fallback — this reader normalizes a user-maintained convention file into a
|
|
8
|
+
* Bourdon L5 manifest. The file is YAML front-matter (structured `entities:` /
|
|
9
|
+
* `sessions:`) followed by an optional freeform markdown body that Copilot Chat
|
|
10
|
+
* reads as plain context but this reader ignores.
|
|
11
|
+
*
|
|
12
|
+
* NOT the cloud GitHub-Copilot network adapter ({@link GitHubCopilotParticipant})
|
|
13
|
+
* and NOT the Copilot CLI SQLite reader.
|
|
14
|
+
*
|
|
15
|
+
* Paths checked (in order):
|
|
16
|
+
* 1. `COPILOT_BOURDON_HOME` env-var override
|
|
17
|
+
* 2. `~/.copilot-bourdon/` (default convention path)
|
|
18
|
+
*
|
|
19
|
+
* Defensive throughout: a missing convention directory raises
|
|
20
|
+
* {@link ParticipantDiscoveryError} from `discover()`; malformed YAML degrades to
|
|
21
|
+
* an empty manifest rather than raising; `healthCheck` never throws.
|
|
22
|
+
*/
|
|
23
|
+
import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
|
|
24
|
+
import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
|
|
25
|
+
/**
|
|
26
|
+
* Return the conventional `~/.copilot-bourdon/` directory path. Respects the
|
|
27
|
+
* `COPILOT_BOURDON_HOME` env-var override (returned verbatim, even if absent;
|
|
28
|
+
* the directory is not created here).
|
|
29
|
+
*/
|
|
30
|
+
export declare function defaultCopilotBourdonDir(): string;
|
|
31
|
+
/** Return the path to the convention memory file. */
|
|
32
|
+
export declare function defaultCopilotMemoryPath(copilotDir?: string | null): string;
|
|
33
|
+
/** Diagnostic summary of the copilot-bourdon memory file (`bourdon copilot
|
|
34
|
+
* doctor`). Never raises. */
|
|
35
|
+
export declare function inspectCopilotMemory(copilotDir?: string | null): Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* External participant for GitHub Copilot via `~/.copilot-bourdon/memory.md`.
|
|
38
|
+
* Implements {@link BourdonParticipant} structurally.
|
|
39
|
+
*/
|
|
40
|
+
export declare class CopilotParticipant implements BourdonParticipant {
|
|
41
|
+
agentId: string;
|
|
42
|
+
agentType: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
nativePath: string;
|
|
45
|
+
/** Conventional `~/.copilot-bourdon` dir used by the setup wizard's detection. */
|
|
46
|
+
static defaultNativePath(home?: string): string;
|
|
47
|
+
private readonly copilotDir;
|
|
48
|
+
constructor(copilotDir?: string | null);
|
|
49
|
+
discover(): AgentStore;
|
|
50
|
+
exportSessions(since?: Date, limit?: number): SessionModel[];
|
|
51
|
+
exportL5(since?: Date): L5ManifestModel;
|
|
52
|
+
healthCheck(): HealthStatus;
|
|
53
|
+
private read;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create `~/.copilot-bourdon/memory.md` with a starter template. Returns the
|
|
57
|
+
* written path. Throws when the file already exists unless `force` is true.
|
|
58
|
+
*/
|
|
59
|
+
export declare function initMemoryFile(copilotDir?: string | null, force?: boolean): string;
|