currentlybuilding 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/README.md +153 -0
- package/dist/currentlybuilding.mjs +2050 -0
- package/package.json +27 -0
|
@@ -0,0 +1,2050 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// currently.mjs
|
|
4
|
+
import {
|
|
5
|
+
readFileSync as readFileSync6,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
existsSync as existsSync10,
|
|
8
|
+
mkdirSync
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join as join9 } from "node:path";
|
|
11
|
+
import { homedir as homedir6 } from "node:os";
|
|
12
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
13
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
14
|
+
|
|
15
|
+
// ../lib/sessions/parse.ts
|
|
16
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
17
|
+
"Edit",
|
|
18
|
+
"Write",
|
|
19
|
+
"MultiEdit",
|
|
20
|
+
"NotebookEdit",
|
|
21
|
+
"Update"
|
|
22
|
+
]);
|
|
23
|
+
var FILE_PATH_KEYS = ["file_path", "filePath", "notebook_path", "path"];
|
|
24
|
+
var DECISION_CUES = /\b(chose|choose|chosen|decided|decid(?:e|ing)|instead of|rather than|trade[- ]?off|opted|opt for|going with|went with|will use|because|in favor of|preferred|settled on|the reason)\b/i;
|
|
25
|
+
function safeParseLine(line2) {
|
|
26
|
+
const trimmed = line2.trim();
|
|
27
|
+
if (!trimmed) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(trimmed);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function asRecord(value) {
|
|
35
|
+
return value && typeof value === "object" ? value : {};
|
|
36
|
+
}
|
|
37
|
+
function normalize(raw) {
|
|
38
|
+
if (raw.type !== "user" && raw.type !== "assistant") return null;
|
|
39
|
+
const role = raw.type;
|
|
40
|
+
const content = raw.message?.content;
|
|
41
|
+
const texts = [];
|
|
42
|
+
const toolCalls = [];
|
|
43
|
+
let hasToolResult = false;
|
|
44
|
+
if (typeof content === "string") {
|
|
45
|
+
if (content.trim()) texts.push(content);
|
|
46
|
+
} else if (Array.isArray(content)) {
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
const b = asRecord(block);
|
|
49
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
50
|
+
texts.push(b.text);
|
|
51
|
+
} else if (b.type === "tool_use" && typeof b.name === "string") {
|
|
52
|
+
toolCalls.push({ name: b.name, input: asRecord(b.input) });
|
|
53
|
+
} else if (b.type === "tool_result") {
|
|
54
|
+
hasToolResult = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
role,
|
|
60
|
+
texts,
|
|
61
|
+
toolCalls,
|
|
62
|
+
hasToolResult,
|
|
63
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : null,
|
|
64
|
+
cwd: typeof raw.cwd === "string" ? raw.cwd : null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var NOISE_PREFIXES = [
|
|
68
|
+
"<command-name>",
|
|
69
|
+
"<command-message>",
|
|
70
|
+
"<local-command",
|
|
71
|
+
"<system-reminder>",
|
|
72
|
+
"caveat:",
|
|
73
|
+
"[request interrupted",
|
|
74
|
+
"[the user",
|
|
75
|
+
"this session is being continued",
|
|
76
|
+
"api error"
|
|
77
|
+
];
|
|
78
|
+
function isSubstantiveUserText(text) {
|
|
79
|
+
const t = text.trim();
|
|
80
|
+
if (t.length < 3) return false;
|
|
81
|
+
const lower = t.toLowerCase();
|
|
82
|
+
if (t.startsWith("/")) return false;
|
|
83
|
+
if (NOISE_PREFIXES.some((p) => lower.startsWith(p))) return false;
|
|
84
|
+
if (t.startsWith("<") && t.includes(">")) return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function isSubstantiveAssistantText(text) {
|
|
88
|
+
const t = text.trim();
|
|
89
|
+
if (t.length < 12) return false;
|
|
90
|
+
if (t.startsWith("<")) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function basename(p) {
|
|
94
|
+
const cleaned = p.replace(/[\\/]+$/, "");
|
|
95
|
+
const parts = cleaned.split(/[\\/]/);
|
|
96
|
+
return parts[parts.length - 1] || cleaned;
|
|
97
|
+
}
|
|
98
|
+
function hash(input) {
|
|
99
|
+
let h = 5381;
|
|
100
|
+
for (let i = 0; i < input.length; i++) {
|
|
101
|
+
h = (h << 5) + h + input.charCodeAt(i) >>> 0;
|
|
102
|
+
}
|
|
103
|
+
return h.toString(16).padStart(8, "0");
|
|
104
|
+
}
|
|
105
|
+
function firstSentence(text) {
|
|
106
|
+
const trimmed = text.trim();
|
|
107
|
+
const match = trimmed.match(/^.*?[.!?](?:\s|$)/s);
|
|
108
|
+
return (match ? match[0] : trimmed).trim();
|
|
109
|
+
}
|
|
110
|
+
function clip(text, max) {
|
|
111
|
+
const t = text.trim().replace(/\s+/g, " ");
|
|
112
|
+
return t.length > max ? `${t.slice(0, max - 1).trimEnd()}\u2026` : t;
|
|
113
|
+
}
|
|
114
|
+
function extractFilePath(input) {
|
|
115
|
+
for (const key of FILE_PATH_KEYS) {
|
|
116
|
+
const v = input[key];
|
|
117
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function splitSentences(text) {
|
|
122
|
+
return text.replace(/\s+/g, " ").split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
123
|
+
}
|
|
124
|
+
function buildApproach(toolTally, filesTouched) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
const reads = toolTally.get("Read") ?? 0;
|
|
127
|
+
const searches = (toolTally.get("Grep") ?? 0) + (toolTally.get("Glob") ?? 0);
|
|
128
|
+
const edits = (toolTally.get("Edit") ?? 0) + (toolTally.get("MultiEdit") ?? 0) + (toolTally.get("Write") ?? 0) + (toolTally.get("NotebookEdit") ?? 0);
|
|
129
|
+
const commands = toolTally.get("Bash") ?? 0;
|
|
130
|
+
if (searches > 0) parts.push(`searched the codebase (${searches})`);
|
|
131
|
+
if (reads > 0) parts.push(`read ${reads} file${reads === 1 ? "" : "s"}`);
|
|
132
|
+
if (edits > 0) {
|
|
133
|
+
const fileCount = filesTouched.length;
|
|
134
|
+
parts.push(
|
|
135
|
+
`edited ${fileCount || edits} file${(fileCount || edits) === 1 ? "" : "s"}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (commands > 0)
|
|
139
|
+
parts.push(`ran ${commands} command${commands === 1 ? "" : "s"}`);
|
|
140
|
+
if (parts.length === 0) return "Worked through the problem in conversation.";
|
|
141
|
+
const joined = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
142
|
+
return `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`;
|
|
143
|
+
}
|
|
144
|
+
function extractDecisions(assistantTexts) {
|
|
145
|
+
const decisions = [];
|
|
146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const text of assistantTexts) {
|
|
148
|
+
for (const sentence of splitSentences(text)) {
|
|
149
|
+
if (sentence.length < 20 || sentence.length > 240) continue;
|
|
150
|
+
if (!DECISION_CUES.test(sentence)) continue;
|
|
151
|
+
const key = sentence.toLowerCase();
|
|
152
|
+
if (seen.has(key)) continue;
|
|
153
|
+
seen.add(key);
|
|
154
|
+
decisions.push(sentence);
|
|
155
|
+
if (decisions.length >= 5) return decisions;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return decisions;
|
|
159
|
+
}
|
|
160
|
+
function assembleSession(cs) {
|
|
161
|
+
const messages = cs.messages ?? [];
|
|
162
|
+
const timestamps = [];
|
|
163
|
+
for (const m of messages) {
|
|
164
|
+
if (m.timestamp) {
|
|
165
|
+
const t = Date.parse(m.timestamp);
|
|
166
|
+
if (Number.isFinite(t)) timestamps.push(t);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let problem = "";
|
|
170
|
+
for (const m of messages) {
|
|
171
|
+
if (m.role !== "user") continue;
|
|
172
|
+
const hit = (m.texts ?? []).find(isSubstantiveUserText);
|
|
173
|
+
if (hit) {
|
|
174
|
+
problem = clip(hit, 400);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const toolTally = /* @__PURE__ */ new Map();
|
|
179
|
+
const toolsUsed = [];
|
|
180
|
+
const filesTouched = [];
|
|
181
|
+
const filesSeen = /* @__PURE__ */ new Set();
|
|
182
|
+
const assistantTexts = [];
|
|
183
|
+
for (const m of messages) {
|
|
184
|
+
if (m.role === "assistant") {
|
|
185
|
+
for (const text of m.texts ?? []) {
|
|
186
|
+
if (isSubstantiveAssistantText(text)) assistantTexts.push(text);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const call of m.toolCalls ?? []) {
|
|
190
|
+
if (!toolTally.has(call.name)) toolsUsed.push(call.name);
|
|
191
|
+
toolTally.set(call.name, (toolTally.get(call.name) ?? 0) + 1);
|
|
192
|
+
if (WRITE_TOOLS.has(call.name)) {
|
|
193
|
+
const fp = extractFilePath(call.input);
|
|
194
|
+
if (fp && !filesSeen.has(fp)) {
|
|
195
|
+
filesSeen.add(fp);
|
|
196
|
+
filesTouched.push(fp);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const f of cs.filesTouched ?? []) {
|
|
202
|
+
if (f && !filesSeen.has(f)) {
|
|
203
|
+
filesSeen.add(f);
|
|
204
|
+
filesTouched.push(f);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const approach = buildApproach(toolTally, filesTouched);
|
|
208
|
+
const decisions = extractDecisions(assistantTexts);
|
|
209
|
+
let outcome = "";
|
|
210
|
+
for (let i = assistantTexts.length - 1; i >= 0; i--) {
|
|
211
|
+
outcome = clip(firstSentence(assistantTexts[i]) || assistantTexts[i], 400);
|
|
212
|
+
if (outcome) break;
|
|
213
|
+
}
|
|
214
|
+
const startedAt = timestamps.length > 0 ? new Date(Math.min(...timestamps)).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
215
|
+
const endedAt = timestamps.length > 0 ? new Date(Math.max(...timestamps)).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
216
|
+
const cwd = cs.cwd ?? "";
|
|
217
|
+
let sessionId = cs.sessionId ?? "";
|
|
218
|
+
if (!sessionId) {
|
|
219
|
+
sessionId = hash(cs.sourcePath ?? `${cwd}:${startedAt}:${messages.length}`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
sessionId,
|
|
223
|
+
cwd,
|
|
224
|
+
repo: cwd ? basename(cwd) : "workspace",
|
|
225
|
+
startedAt,
|
|
226
|
+
endedAt,
|
|
227
|
+
problem: problem || "No stated problem in this session.",
|
|
228
|
+
approach,
|
|
229
|
+
decisions,
|
|
230
|
+
outcome: outcome || "No closing summary in this session.",
|
|
231
|
+
filesTouched,
|
|
232
|
+
toolsUsed,
|
|
233
|
+
messageCount: messages.length,
|
|
234
|
+
...cs.tool ? { tool: cs.tool } : {}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function claudeJsonlToCommon(jsonl, sourcePath) {
|
|
238
|
+
const messages = [];
|
|
239
|
+
let cwd = "";
|
|
240
|
+
let sessionId = "";
|
|
241
|
+
for (const line2 of jsonl.split(/\r?\n/)) {
|
|
242
|
+
const raw = safeParseLine(line2);
|
|
243
|
+
if (!raw) continue;
|
|
244
|
+
if (!sessionId && typeof raw.sessionId === "string") sessionId = raw.sessionId;
|
|
245
|
+
if (!cwd && typeof raw.cwd === "string") cwd = raw.cwd;
|
|
246
|
+
const norm = normalize(raw);
|
|
247
|
+
if (norm) {
|
|
248
|
+
if (!cwd && norm.cwd) cwd = norm.cwd;
|
|
249
|
+
messages.push({
|
|
250
|
+
role: norm.role,
|
|
251
|
+
texts: norm.texts,
|
|
252
|
+
toolCalls: norm.toolCalls,
|
|
253
|
+
hasToolResult: norm.hasToolResult,
|
|
254
|
+
timestamp: norm.timestamp
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
tool: "claude-code",
|
|
260
|
+
sessionId: sessionId || void 0,
|
|
261
|
+
cwd: cwd || void 0,
|
|
262
|
+
messages,
|
|
263
|
+
sourcePath
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
var REFINE_MODEL = process.env.GROQ_MODEL || "llama-3.1-8b-instant";
|
|
267
|
+
async function refineWithGroq(session, opts = {}) {
|
|
268
|
+
if (opts.disabled || !process.env.GROQ_API_KEY) return session;
|
|
269
|
+
const system = 'You tighten a build-session summary written by a developer tool. Rewrite only the wording of problem, approach, and outcome to read like a sharp, concrete human engineer. Keep every fact. Invent nothing. No buzzwords, no em dashes. Reply with strict JSON: {"problem":string,"approach":string,"outcome":string}.';
|
|
270
|
+
const payload = {
|
|
271
|
+
problem: session.problem,
|
|
272
|
+
approach: session.approach,
|
|
273
|
+
outcome: session.outcome,
|
|
274
|
+
filesTouched: session.filesTouched.slice(0, 20),
|
|
275
|
+
toolsUsed: session.toolsUsed
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
const timer = setTimeout(() => controller.abort(), 2e4);
|
|
280
|
+
let text = "";
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: {
|
|
285
|
+
"content-type": "application/json",
|
|
286
|
+
authorization: `Bearer ${process.env.GROQ_API_KEY}`
|
|
287
|
+
},
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
model: opts.model ?? REFINE_MODEL,
|
|
290
|
+
max_tokens: 700,
|
|
291
|
+
temperature: 0.3,
|
|
292
|
+
response_format: { type: "json_object" },
|
|
293
|
+
messages: [
|
|
294
|
+
{ role: "system", content: system },
|
|
295
|
+
{ role: "user", content: JSON.stringify(payload) }
|
|
296
|
+
]
|
|
297
|
+
}),
|
|
298
|
+
signal: controller.signal
|
|
299
|
+
});
|
|
300
|
+
if (!res.ok) return session;
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
text = data.choices?.[0]?.message?.content ?? "";
|
|
303
|
+
} finally {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
}
|
|
306
|
+
const start = text.indexOf("{");
|
|
307
|
+
const end = text.lastIndexOf("}");
|
|
308
|
+
if (start === -1 || end <= start) return session;
|
|
309
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
310
|
+
return {
|
|
311
|
+
...session,
|
|
312
|
+
problem: typeof parsed.problem === "string" && parsed.problem.trim() ? clip(parsed.problem, 400) : session.problem,
|
|
313
|
+
approach: typeof parsed.approach === "string" && parsed.approach.trim() ? clip(parsed.approach, 400) : session.approach,
|
|
314
|
+
outcome: typeof parsed.outcome === "string" && parsed.outcome.trim() ? clip(parsed.outcome, 400) : session.outcome
|
|
315
|
+
};
|
|
316
|
+
} catch {
|
|
317
|
+
return session;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ../lib/sessions/redact.ts
|
|
322
|
+
var TAG = (type) => `[redacted:${type}]`;
|
|
323
|
+
var REDACTION_RULES = [
|
|
324
|
+
// Private key blocks (multi-line).
|
|
325
|
+
{
|
|
326
|
+
type: "private-key",
|
|
327
|
+
re: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
328
|
+
replace: () => TAG("private-key")
|
|
329
|
+
},
|
|
330
|
+
// GitHub personal-access / OAuth / app tokens.
|
|
331
|
+
{
|
|
332
|
+
type: "github-token",
|
|
333
|
+
re: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
|
|
334
|
+
replace: () => TAG("github-token")
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: "github-token",
|
|
338
|
+
re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
339
|
+
replace: () => TAG("github-token")
|
|
340
|
+
},
|
|
341
|
+
// AWS access key id.
|
|
342
|
+
{
|
|
343
|
+
type: "aws-key",
|
|
344
|
+
re: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[0-9A-Z]{16}\b/g,
|
|
345
|
+
replace: () => TAG("aws-key")
|
|
346
|
+
},
|
|
347
|
+
// Slack tokens.
|
|
348
|
+
{
|
|
349
|
+
type: "slack-token",
|
|
350
|
+
re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
351
|
+
replace: () => TAG("slack-token")
|
|
352
|
+
},
|
|
353
|
+
// Stripe live/test secret + restricted keys.
|
|
354
|
+
{
|
|
355
|
+
type: "stripe-key",
|
|
356
|
+
re: /\b[rsp]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
|
|
357
|
+
replace: () => TAG("stripe-key")
|
|
358
|
+
},
|
|
359
|
+
// Google API key.
|
|
360
|
+
{
|
|
361
|
+
type: "google-key",
|
|
362
|
+
re: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
363
|
+
replace: () => TAG("google-key")
|
|
364
|
+
},
|
|
365
|
+
// Supabase service / secret keys.
|
|
366
|
+
{
|
|
367
|
+
type: "supabase-key",
|
|
368
|
+
re: /\bsb(?:p|_secret|_publishable)?_[A-Za-z0-9]{20,}\b/g,
|
|
369
|
+
replace: () => TAG("supabase-key")
|
|
370
|
+
},
|
|
371
|
+
// OpenAI / Anthropic style secret keys (sk-…, sk-ant-…).
|
|
372
|
+
{
|
|
373
|
+
type: "api-key",
|
|
374
|
+
re: /\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}\b/g,
|
|
375
|
+
replace: () => TAG("api-key")
|
|
376
|
+
},
|
|
377
|
+
// JSON Web Tokens.
|
|
378
|
+
{
|
|
379
|
+
type: "jwt",
|
|
380
|
+
re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\b/g,
|
|
381
|
+
replace: () => TAG("jwt")
|
|
382
|
+
},
|
|
383
|
+
// Authorization: Bearer <token> (keep the scheme, drop the secret).
|
|
384
|
+
{
|
|
385
|
+
type: "bearer",
|
|
386
|
+
re: /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
387
|
+
replace: () => `Bearer ${TAG("bearer")}`
|
|
388
|
+
},
|
|
389
|
+
// Credentialed URLs: scheme://user:pass@host → keep scheme + host.
|
|
390
|
+
{
|
|
391
|
+
type: "connection-string",
|
|
392
|
+
re: /\b([a-z][a-z0-9+.-]*):\/\/([^\s:@/]+):([^\s:@/]+)@/gi,
|
|
393
|
+
replace: (_m, scheme) => `${scheme}://${TAG("connection-string")}@`
|
|
394
|
+
},
|
|
395
|
+
// .env-style secret assignment by name: KEY = value (keep the key, drop the value).
|
|
396
|
+
{
|
|
397
|
+
type: "env-secret",
|
|
398
|
+
re: /\b([A-Za-z][A-Za-z0-9_]*(?:SECRET|KEY|TOKEN|PASSWORD|PASSWD|PASS|CREDENTIAL|PRIVATE|API|ACCESS)[A-Za-z0-9_]*)(\s*[:=]\s*)(["']?)([^\s"']{3,})\3/gi,
|
|
399
|
+
replace: (_m, key, sep) => `${key}${sep}${TAG("env-secret")}`
|
|
400
|
+
},
|
|
401
|
+
// Generic secret assignment by keyword (password: …, token = …).
|
|
402
|
+
{
|
|
403
|
+
type: "env-secret",
|
|
404
|
+
re: /\b(password|passwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret|auth[_-]?token)(\s*[:=]\s*)(["']?)([^\s"']{4,})\3/gi,
|
|
405
|
+
replace: (_m, key, sep) => `${key}${sep}${TAG("env-secret")}`
|
|
406
|
+
},
|
|
407
|
+
// Long hex secret blobs (md5/sha/hex API secrets).
|
|
408
|
+
{
|
|
409
|
+
type: "hex-secret",
|
|
410
|
+
re: /\b[0-9a-fA-F]{32,}\b/g,
|
|
411
|
+
replace: () => TAG("hex-secret")
|
|
412
|
+
},
|
|
413
|
+
// Long base64 secret blobs. We deliberately exclude "/" from the character class:
|
|
414
|
+
// it is a base64 char but also a path separator, and matching it swallows ordinary
|
|
415
|
+
// file paths ("a/b/c/…"). Slash-bearing secrets are still caught by the assignment
|
|
416
|
+
// and connection-string rules above (where such values actually live).
|
|
417
|
+
{
|
|
418
|
+
type: "base64-secret",
|
|
419
|
+
re: /\b[A-Za-z0-9+]{40,}={0,2}\b/g,
|
|
420
|
+
replace: () => TAG("base64-secret")
|
|
421
|
+
},
|
|
422
|
+
// Email addresses.
|
|
423
|
+
{
|
|
424
|
+
type: "email",
|
|
425
|
+
re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
426
|
+
replace: () => TAG("email")
|
|
427
|
+
},
|
|
428
|
+
// Windows home path: C:\Users\<name>\… → strip the username.
|
|
429
|
+
{
|
|
430
|
+
type: "home-path",
|
|
431
|
+
re: /([A-Za-z]:\\Users\\)([^\\/\s"']+)/g,
|
|
432
|
+
replace: (_m, prefix) => `${prefix}${TAG("user")}`
|
|
433
|
+
},
|
|
434
|
+
// Unix home path: /home/<name>/ or /Users/<name>/ → strip the username.
|
|
435
|
+
{
|
|
436
|
+
type: "home-path",
|
|
437
|
+
re: /(\/(?:home|Users)\/)([^/\s"']+)/g,
|
|
438
|
+
replace: (_m, prefix) => `${prefix}${TAG("user")}`
|
|
439
|
+
}
|
|
440
|
+
];
|
|
441
|
+
function redact(input) {
|
|
442
|
+
if (!input) return { text: input ?? "", count: 0, byType: {} };
|
|
443
|
+
let text = input;
|
|
444
|
+
let count = 0;
|
|
445
|
+
const byType = {};
|
|
446
|
+
for (const rule of REDACTION_RULES) {
|
|
447
|
+
rule.re.lastIndex = 0;
|
|
448
|
+
text = text.replace(rule.re, (...args) => {
|
|
449
|
+
count++;
|
|
450
|
+
byType[rule.type] = (byType[rule.type] ?? 0) + 1;
|
|
451
|
+
const match = args[0];
|
|
452
|
+
const groups = args.slice(1, -2);
|
|
453
|
+
return rule.replace(match, ...groups);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return { text, count, byType };
|
|
457
|
+
}
|
|
458
|
+
function redactFields(obj, keys) {
|
|
459
|
+
const value = { ...obj };
|
|
460
|
+
let count = 0;
|
|
461
|
+
const byType = {};
|
|
462
|
+
const merge = (r) => {
|
|
463
|
+
count += r.count;
|
|
464
|
+
for (const [type, n] of Object.entries(r.byType)) {
|
|
465
|
+
byType[type] = (byType[type] ?? 0) + n;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
for (const key of keys) {
|
|
469
|
+
const v = value[key];
|
|
470
|
+
if (typeof v === "string") {
|
|
471
|
+
const r = redact(v);
|
|
472
|
+
value[key] = r.text;
|
|
473
|
+
merge(r);
|
|
474
|
+
} else if (Array.isArray(v)) {
|
|
475
|
+
const redactedArr = v.map((item) => {
|
|
476
|
+
if (typeof item !== "string") return item;
|
|
477
|
+
const r = redact(item);
|
|
478
|
+
merge(r);
|
|
479
|
+
return r.text;
|
|
480
|
+
});
|
|
481
|
+
value[key] = redactedArr;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { value, count, byType };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ../lib/sessions/to-events.ts
|
|
488
|
+
var DEFAULT_PAD_MS = 60 * 60 * 1e3;
|
|
489
|
+
var EXT_LANGUAGE = {
|
|
490
|
+
ts: "TypeScript",
|
|
491
|
+
tsx: "TypeScript",
|
|
492
|
+
js: "JavaScript",
|
|
493
|
+
jsx: "JavaScript",
|
|
494
|
+
mjs: "JavaScript",
|
|
495
|
+
cjs: "JavaScript",
|
|
496
|
+
py: "Python",
|
|
497
|
+
rb: "Ruby",
|
|
498
|
+
go: "Go",
|
|
499
|
+
rs: "Rust",
|
|
500
|
+
java: "Java",
|
|
501
|
+
kt: "Kotlin",
|
|
502
|
+
swift: "Swift",
|
|
503
|
+
c: "C",
|
|
504
|
+
h: "C",
|
|
505
|
+
cpp: "C++",
|
|
506
|
+
cc: "C++",
|
|
507
|
+
cs: "C#",
|
|
508
|
+
php: "PHP",
|
|
509
|
+
sql: "SQL",
|
|
510
|
+
sh: "Shell",
|
|
511
|
+
css: "CSS",
|
|
512
|
+
scss: "CSS",
|
|
513
|
+
html: "HTML",
|
|
514
|
+
md: "Markdown",
|
|
515
|
+
json: "JSON",
|
|
516
|
+
yml: "YAML",
|
|
517
|
+
yaml: "YAML",
|
|
518
|
+
toml: "TOML"
|
|
519
|
+
};
|
|
520
|
+
function slugify(repo) {
|
|
521
|
+
return repo.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
522
|
+
}
|
|
523
|
+
function basename2(p) {
|
|
524
|
+
const cleaned = p.replace(/[\\/]+$/, "");
|
|
525
|
+
const parts = cleaned.split(/[\\/]/);
|
|
526
|
+
return parts[parts.length - 1] || cleaned;
|
|
527
|
+
}
|
|
528
|
+
function shortSha(sha) {
|
|
529
|
+
return sha.replace(/[^0-9a-fA-F]/g, "").slice(0, 7) || sha.slice(0, 7);
|
|
530
|
+
}
|
|
531
|
+
function languagesFrom(filesTouched) {
|
|
532
|
+
const langs = /* @__PURE__ */ new Set();
|
|
533
|
+
for (const f of filesTouched) {
|
|
534
|
+
const ext = f.split(".").pop()?.toLowerCase() ?? "";
|
|
535
|
+
const lang = EXT_LANGUAGE[ext];
|
|
536
|
+
if (lang) langs.add(lang);
|
|
537
|
+
}
|
|
538
|
+
return Array.from(langs).sort();
|
|
539
|
+
}
|
|
540
|
+
function correlate(commit, session, windowStart, windowEnd) {
|
|
541
|
+
if (commit.committedAt) {
|
|
542
|
+
const t = Date.parse(commit.committedAt);
|
|
543
|
+
if (Number.isFinite(t) && t >= windowStart && t <= windowEnd) return true;
|
|
544
|
+
} else {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
const msg = commit.message.toLowerCase();
|
|
548
|
+
for (const f of session.filesTouched) {
|
|
549
|
+
const name = basename2(f).toLowerCase();
|
|
550
|
+
if (name.length >= 3 && msg.includes(name)) return true;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
function buildBody(session, matched) {
|
|
555
|
+
const lines = [];
|
|
556
|
+
lines.push(`Problem: ${session.problem}`);
|
|
557
|
+
lines.push(`Approach: ${session.approach}`);
|
|
558
|
+
if (session.decisions.length > 0) {
|
|
559
|
+
lines.push("Decisions:");
|
|
560
|
+
for (const d of session.decisions) lines.push(`- ${d}`);
|
|
561
|
+
}
|
|
562
|
+
lines.push(`Outcome: ${session.outcome}`);
|
|
563
|
+
const shas = matched.map((c) => shortSha(c.sha)).join(", ");
|
|
564
|
+
lines.push(
|
|
565
|
+
`Grounded in ${matched.length} commit${matched.length === 1 ? "" : "s"}: ${shas}`
|
|
566
|
+
);
|
|
567
|
+
return lines.join("\n");
|
|
568
|
+
}
|
|
569
|
+
function buildTitle(session) {
|
|
570
|
+
const source = session.outcome && !session.outcome.startsWith("No closing") ? session.outcome : session.problem;
|
|
571
|
+
const first = source.split(/(?<=[.!?])\s/)[0].trim();
|
|
572
|
+
const clipped = first.length > 140 ? `${first.slice(0, 139).trimEnd()}\u2026` : first;
|
|
573
|
+
return clipped || `Build session in ${session.repo}`;
|
|
574
|
+
}
|
|
575
|
+
function sessionToEvent(session, opts) {
|
|
576
|
+
const pad = opts.windowPadMs ?? DEFAULT_PAD_MS;
|
|
577
|
+
const start = Date.parse(session.startedAt);
|
|
578
|
+
const end = Date.parse(session.endedAt);
|
|
579
|
+
const windowStart = (Number.isFinite(start) ? start : 0) - pad;
|
|
580
|
+
const windowEnd = (Number.isFinite(end) ? end : Date.now()) + pad;
|
|
581
|
+
const matchedCommits = (opts.commits ?? []).filter(
|
|
582
|
+
(c) => correlate(c, session, windowStart, windowEnd)
|
|
583
|
+
);
|
|
584
|
+
if (matchedCommits.length === 0) {
|
|
585
|
+
return {
|
|
586
|
+
event: null,
|
|
587
|
+
grounded: false,
|
|
588
|
+
matchedCommits: [],
|
|
589
|
+
reason: "No git commits correlate to this session window; dropped like an ungrounded claim."
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const repo = opts.repo ?? session.repo;
|
|
593
|
+
const anchorCommit = [...matchedCommits].sort((a, b) => {
|
|
594
|
+
const ta = a.committedAt ? Date.parse(a.committedAt) : 0;
|
|
595
|
+
const tb = b.committedAt ? Date.parse(b.committedAt) : 0;
|
|
596
|
+
if (ta !== tb) return ta - tb;
|
|
597
|
+
return a.sha.localeCompare(b.sha);
|
|
598
|
+
})[0];
|
|
599
|
+
const anchor = shortSha(anchorCommit.sha) || session.sessionId.slice(0, 8);
|
|
600
|
+
const additions = matchedCommits.reduce(
|
|
601
|
+
(s, c) => s + (typeof c.additions === "number" ? c.additions : 0),
|
|
602
|
+
0
|
|
603
|
+
);
|
|
604
|
+
const deletions = matchedCommits.reduce(
|
|
605
|
+
(s, c) => s + (typeof c.deletions === "number" ? c.deletions : 0),
|
|
606
|
+
0
|
|
607
|
+
);
|
|
608
|
+
const hasStats = matchedCommits.some(
|
|
609
|
+
(c) => typeof c.additions === "number" || typeof c.deletions === "number"
|
|
610
|
+
);
|
|
611
|
+
const event = {
|
|
612
|
+
id: `session:${slugify(repo)}:${anchor}`,
|
|
613
|
+
userId: opts.userId,
|
|
614
|
+
sourceId: opts.sourceId,
|
|
615
|
+
type: "session",
|
|
616
|
+
repo,
|
|
617
|
+
title: buildTitle(session),
|
|
618
|
+
body: buildBody(session, matchedCommits),
|
|
619
|
+
url: null,
|
|
620
|
+
languages: languagesFrom(session.filesTouched),
|
|
621
|
+
additions: hasStats ? additions : null,
|
|
622
|
+
deletions: hasStats ? deletions : null,
|
|
623
|
+
occurredAt: Number.isFinite(end) ? new Date(end).toISOString() : session.endedAt
|
|
624
|
+
};
|
|
625
|
+
return {
|
|
626
|
+
event,
|
|
627
|
+
grounded: true,
|
|
628
|
+
matchedCommits,
|
|
629
|
+
reason: `Grounded to ${matchedCommits.length} commit${matchedCommits.length === 1 ? "" : "s"} in the session window.`
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ../lib/sessions/adapters/claude-code.ts
|
|
634
|
+
import { readFileSync, existsSync as existsSync2 } from "node:fs";
|
|
635
|
+
import { join as join2, basename as basename3 } from "node:path";
|
|
636
|
+
import { homedir as homedir2 } from "node:os";
|
|
637
|
+
|
|
638
|
+
// ../lib/sessions/adapters/paths.ts
|
|
639
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
640
|
+
import { join } from "node:path";
|
|
641
|
+
import { homedir } from "node:os";
|
|
642
|
+
function vscodeUserDirs(appName) {
|
|
643
|
+
const home = homedir();
|
|
644
|
+
const candidates = [];
|
|
645
|
+
if (process.platform === "win32") {
|
|
646
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
647
|
+
candidates.push(join(appData, appName, "User"));
|
|
648
|
+
} else if (process.platform === "darwin") {
|
|
649
|
+
candidates.push(join(home, "Library", "Application Support", appName, "User"));
|
|
650
|
+
} else {
|
|
651
|
+
const cfg = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
652
|
+
candidates.push(join(cfg, appName, "User"));
|
|
653
|
+
}
|
|
654
|
+
return candidates.filter((p) => existsSync(p));
|
|
655
|
+
}
|
|
656
|
+
function appSupportDirs(appName) {
|
|
657
|
+
const home = homedir();
|
|
658
|
+
const candidates = [];
|
|
659
|
+
if (process.platform === "win32") {
|
|
660
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
661
|
+
const localAppData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
662
|
+
candidates.push(join(appData, appName), join(localAppData, appName));
|
|
663
|
+
} else if (process.platform === "darwin") {
|
|
664
|
+
candidates.push(join(home, "Library", "Application Support", appName));
|
|
665
|
+
} else {
|
|
666
|
+
const dataHome = process.env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
667
|
+
const cfg = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
668
|
+
candidates.push(join(dataHome, appName), join(cfg, appName));
|
|
669
|
+
}
|
|
670
|
+
return candidates.filter((p) => existsSync(p));
|
|
671
|
+
}
|
|
672
|
+
function safeStat(path) {
|
|
673
|
+
try {
|
|
674
|
+
const st = statSync(path);
|
|
675
|
+
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
676
|
+
} catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function walkFiles(root, match, opts = {}) {
|
|
681
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
682
|
+
const skip = opts.skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
683
|
+
const out = [];
|
|
684
|
+
const walk = (dir, depth) => {
|
|
685
|
+
if (depth > maxDepth) return;
|
|
686
|
+
let entries;
|
|
687
|
+
try {
|
|
688
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
689
|
+
} catch {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
for (const e of entries) {
|
|
693
|
+
const full = join(dir, e.name);
|
|
694
|
+
if (e.isDirectory()) {
|
|
695
|
+
if (skip.has(e.name)) continue;
|
|
696
|
+
walk(full, depth + 1);
|
|
697
|
+
} else if (e.isFile() && match(e.name, full)) {
|
|
698
|
+
out.push(full);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
walk(root, 0);
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
706
|
+
"node_modules",
|
|
707
|
+
".git",
|
|
708
|
+
".next",
|
|
709
|
+
"dist",
|
|
710
|
+
"build",
|
|
711
|
+
"out",
|
|
712
|
+
".cache",
|
|
713
|
+
".venv",
|
|
714
|
+
"venv",
|
|
715
|
+
"__pycache__",
|
|
716
|
+
".turbo",
|
|
717
|
+
"vendor",
|
|
718
|
+
"target"
|
|
719
|
+
]);
|
|
720
|
+
function fileUriToPath(uri) {
|
|
721
|
+
if (!uri) return "";
|
|
722
|
+
let s = uri.trim();
|
|
723
|
+
if (!s.startsWith("file:")) {
|
|
724
|
+
return s;
|
|
725
|
+
}
|
|
726
|
+
s = s.replace(/^file:\/\//, "");
|
|
727
|
+
try {
|
|
728
|
+
s = decodeURIComponent(s);
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
const winDrive = s.match(/^\/([A-Za-z]:.*)$/);
|
|
732
|
+
if (winDrive) return winDrive[1].replace(/\//g, "\\");
|
|
733
|
+
return s;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ../lib/sessions/adapters/claude-code.ts
|
|
737
|
+
function projectsRoot() {
|
|
738
|
+
return join2(homedir2(), ".claude", "projects");
|
|
739
|
+
}
|
|
740
|
+
var claudeCodeAdapter = {
|
|
741
|
+
id: "claude-code",
|
|
742
|
+
displayName: "Claude Code",
|
|
743
|
+
kind: "node",
|
|
744
|
+
detect() {
|
|
745
|
+
const root = projectsRoot();
|
|
746
|
+
if (!existsSync2(root)) return null;
|
|
747
|
+
return [{ root, label: root }];
|
|
748
|
+
},
|
|
749
|
+
listSessions(store) {
|
|
750
|
+
const files = walkFiles(store.root, (name) => name.endsWith(".jsonl"), {
|
|
751
|
+
maxDepth: 4
|
|
752
|
+
});
|
|
753
|
+
const refs = [];
|
|
754
|
+
for (const path of files) {
|
|
755
|
+
const st = safeStat(path);
|
|
756
|
+
if (!st || st.size <= 0) continue;
|
|
757
|
+
refs.push({
|
|
758
|
+
id: `claude-code:${basename3(path, ".jsonl")}`,
|
|
759
|
+
locator: path,
|
|
760
|
+
store,
|
|
761
|
+
mtimeMs: st.mtimeMs,
|
|
762
|
+
size: st.size
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return refs;
|
|
766
|
+
},
|
|
767
|
+
parse(ref) {
|
|
768
|
+
try {
|
|
769
|
+
const jsonl = readFileSync(ref.locator, "utf8");
|
|
770
|
+
return claudeJsonlToCommon(jsonl, ref.locator);
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// ../lib/sessions/adapters/codex.ts
|
|
778
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
779
|
+
import { join as join3, basename as basename4 } from "node:path";
|
|
780
|
+
import { homedir as homedir3 } from "node:os";
|
|
781
|
+
function codexRoot() {
|
|
782
|
+
return join3(homedir3(), ".codex");
|
|
783
|
+
}
|
|
784
|
+
function asRecord2(v) {
|
|
785
|
+
return v && typeof v === "object" ? v : {};
|
|
786
|
+
}
|
|
787
|
+
function textFromContent(content) {
|
|
788
|
+
const out = [];
|
|
789
|
+
if (typeof content === "string") {
|
|
790
|
+
if (content.trim()) out.push(content);
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
if (Array.isArray(content)) {
|
|
794
|
+
for (const block of content) {
|
|
795
|
+
const b = asRecord2(block);
|
|
796
|
+
const t = b.text ?? b.input_text ?? b.output_text;
|
|
797
|
+
if (typeof t === "string" && t.trim()) out.push(t);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
801
|
+
}
|
|
802
|
+
function filesFromPatch(patch) {
|
|
803
|
+
const files = [];
|
|
804
|
+
const seen = /* @__PURE__ */ new Set();
|
|
805
|
+
const add = (p) => {
|
|
806
|
+
const f = p.trim();
|
|
807
|
+
if (f && f !== "/dev/null" && !seen.has(f)) {
|
|
808
|
+
seen.add(f);
|
|
809
|
+
files.push(f);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const re = /^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\+\+\+ [ab]\/(.+)$|^diff --git a\/(.+?) b\//gm;
|
|
813
|
+
let m;
|
|
814
|
+
while (m = re.exec(patch)) add(m[1] ?? m[2] ?? m[3] ?? "");
|
|
815
|
+
return files;
|
|
816
|
+
}
|
|
817
|
+
function mapToolCall(name, args, files) {
|
|
818
|
+
const lower = name.toLowerCase();
|
|
819
|
+
const argObj = typeof args === "string" ? safeJson(args) : asRecord2(args);
|
|
820
|
+
const input = { ...argObj };
|
|
821
|
+
if (lower.includes("patch") || lower.includes("edit") || lower.includes("write")) {
|
|
822
|
+
const patch = typeof argObj.input === "string" ? argObj.input : typeof argObj.patch === "string" ? argObj.patch : typeof args === "string" ? args : "";
|
|
823
|
+
for (const f of filesFromPatch(patch)) {
|
|
824
|
+
files.push(f);
|
|
825
|
+
if (!input.file_path) input.file_path = f;
|
|
826
|
+
}
|
|
827
|
+
return { name: "Edit", input };
|
|
828
|
+
}
|
|
829
|
+
if (lower.includes("shell") || lower.includes("exec") || lower.includes("bash") || lower.includes("command")) {
|
|
830
|
+
return { name: "Bash", input };
|
|
831
|
+
}
|
|
832
|
+
if (lower.includes("read") || lower.includes("cat")) return { name: "Read", input };
|
|
833
|
+
if (lower.includes("search") || lower.includes("grep")) return { name: "Grep", input };
|
|
834
|
+
return { name: name || "Tool", input };
|
|
835
|
+
}
|
|
836
|
+
function safeJson(s) {
|
|
837
|
+
try {
|
|
838
|
+
return asRecord2(JSON.parse(s));
|
|
839
|
+
} catch {
|
|
840
|
+
return {};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function codexJsonlToCommon(jsonl, sourcePath) {
|
|
844
|
+
const messages = [];
|
|
845
|
+
const files = [];
|
|
846
|
+
let cwd = "";
|
|
847
|
+
let sessionId = "";
|
|
848
|
+
let gitBranch = null;
|
|
849
|
+
for (const line2 of jsonl.split(/\r?\n/)) {
|
|
850
|
+
const t = line2.trim();
|
|
851
|
+
if (!t) continue;
|
|
852
|
+
let raw;
|
|
853
|
+
try {
|
|
854
|
+
raw = asRecord2(JSON.parse(t));
|
|
855
|
+
} catch {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const type = typeof raw.type === "string" ? raw.type : "";
|
|
859
|
+
const payload = raw.payload !== void 0 ? asRecord2(raw.payload) : raw;
|
|
860
|
+
const timestamp = typeof raw.timestamp === "string" ? raw.timestamp : typeof payload.timestamp === "string" ? payload.timestamp : null;
|
|
861
|
+
if (type === "session_meta" || payload.type === "session_meta" || payload.cwd || payload.id) {
|
|
862
|
+
if (!cwd && typeof payload.cwd === "string") cwd = payload.cwd;
|
|
863
|
+
if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
|
|
864
|
+
const git = asRecord2(payload.git);
|
|
865
|
+
if (!gitBranch && typeof git.branch === "string") gitBranch = git.branch;
|
|
866
|
+
}
|
|
867
|
+
if (!cwd && typeof raw.cwd === "string") cwd = raw.cwd;
|
|
868
|
+
if (!sessionId && typeof raw.id === "string") sessionId = raw.id;
|
|
869
|
+
const item = payload.type ? payload : raw;
|
|
870
|
+
const itemType = typeof item.type === "string" ? item.type : "";
|
|
871
|
+
const role = typeof item.role === "string" ? item.role : "";
|
|
872
|
+
if (itemType === "message" || role === "user" || role === "assistant") {
|
|
873
|
+
const r = role === "assistant" ? "assistant" : "user";
|
|
874
|
+
const texts = textFromContent(item.content);
|
|
875
|
+
if (texts.length) messages.push({ role: r, texts, timestamp });
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (itemType === "function_call" || itemType === "tool_call" || item.name) {
|
|
879
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
880
|
+
if (!name) continue;
|
|
881
|
+
const args = item.arguments ?? item.args ?? item.input;
|
|
882
|
+
const call = mapToolCall(name, args, files);
|
|
883
|
+
messages.push({ role: "assistant", toolCalls: [call], timestamp });
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
tool: "codex",
|
|
889
|
+
sessionId: sessionId || void 0,
|
|
890
|
+
cwd: cwd || void 0,
|
|
891
|
+
gitBranch,
|
|
892
|
+
messages,
|
|
893
|
+
filesTouched: files.length ? files : void 0,
|
|
894
|
+
sourcePath
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var codexAdapter = {
|
|
898
|
+
id: "codex",
|
|
899
|
+
displayName: "Codex CLI",
|
|
900
|
+
kind: "node",
|
|
901
|
+
detect() {
|
|
902
|
+
const root = codexRoot();
|
|
903
|
+
if (!existsSync3(root)) return null;
|
|
904
|
+
return [{ root, label: root }];
|
|
905
|
+
},
|
|
906
|
+
listSessions(store) {
|
|
907
|
+
const files = walkFiles(
|
|
908
|
+
store.root,
|
|
909
|
+
(name) => name.endsWith(".jsonl") && name !== "history.jsonl" && !name.startsWith("."),
|
|
910
|
+
{ maxDepth: 5 }
|
|
911
|
+
);
|
|
912
|
+
const refs = [];
|
|
913
|
+
for (const path of files) {
|
|
914
|
+
const st = safeStat(path);
|
|
915
|
+
if (!st || st.size <= 0) continue;
|
|
916
|
+
refs.push({
|
|
917
|
+
id: `codex:${basename4(path, ".jsonl")}`,
|
|
918
|
+
locator: path,
|
|
919
|
+
store,
|
|
920
|
+
mtimeMs: st.mtimeMs,
|
|
921
|
+
size: st.size
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
return refs;
|
|
925
|
+
},
|
|
926
|
+
parse(ref) {
|
|
927
|
+
try {
|
|
928
|
+
const jsonl = readFileSync2(ref.locator, "utf8");
|
|
929
|
+
return codexJsonlToCommon(jsonl, ref.locator);
|
|
930
|
+
} catch {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// ../lib/sessions/adapters/cursor.ts
|
|
937
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
938
|
+
import { join as join4 } from "node:path";
|
|
939
|
+
|
|
940
|
+
// ../lib/sessions/adapters/sqlite.ts
|
|
941
|
+
import { createRequire } from "node:module";
|
|
942
|
+
import { execFileSync } from "node:child_process";
|
|
943
|
+
var require2 = createRequire(import.meta.url);
|
|
944
|
+
function asText(value) {
|
|
945
|
+
if (value == null) return "";
|
|
946
|
+
if (typeof value === "string") return value;
|
|
947
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("utf8");
|
|
948
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
949
|
+
return String(value);
|
|
950
|
+
}
|
|
951
|
+
var cachedBackend;
|
|
952
|
+
function sqliteBackend() {
|
|
953
|
+
if (cachedBackend !== void 0) return cachedBackend;
|
|
954
|
+
try {
|
|
955
|
+
const mod = require2("node:sqlite");
|
|
956
|
+
if (mod && typeof mod.DatabaseSync === "function") {
|
|
957
|
+
cachedBackend = "node:sqlite";
|
|
958
|
+
return cachedBackend;
|
|
959
|
+
}
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
require2.resolve("better-sqlite3");
|
|
964
|
+
cachedBackend = "better-sqlite3";
|
|
965
|
+
return cachedBackend;
|
|
966
|
+
} catch {
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
execFileSync("sqlite3", ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
970
|
+
cachedBackend = "sqlite3-cli";
|
|
971
|
+
return cachedBackend;
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
cachedBackend = null;
|
|
975
|
+
return cachedBackend;
|
|
976
|
+
}
|
|
977
|
+
function sqliteAvailable() {
|
|
978
|
+
return sqliteBackend() !== null;
|
|
979
|
+
}
|
|
980
|
+
function openSqlite(file) {
|
|
981
|
+
const backend = sqliteBackend();
|
|
982
|
+
if (!backend) return null;
|
|
983
|
+
try {
|
|
984
|
+
if (backend === "node:sqlite") {
|
|
985
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
986
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
987
|
+
return {
|
|
988
|
+
backend,
|
|
989
|
+
all: (sql) => db.prepare(sql).all(),
|
|
990
|
+
close: () => {
|
|
991
|
+
try {
|
|
992
|
+
db.close();
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (backend === "better-sqlite3") {
|
|
999
|
+
const Database = require2("better-sqlite3");
|
|
1000
|
+
const db = new Database(file, { readonly: true, fileMustExist: true });
|
|
1001
|
+
return {
|
|
1002
|
+
backend,
|
|
1003
|
+
all: (sql) => db.prepare(sql).all(),
|
|
1004
|
+
close: () => {
|
|
1005
|
+
try {
|
|
1006
|
+
db.close();
|
|
1007
|
+
} catch {
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
backend,
|
|
1014
|
+
all: (sql) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const out = execFileSync("sqlite3", ["-readonly", "-json", file, sql], {
|
|
1017
|
+
encoding: "utf8",
|
|
1018
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1019
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1020
|
+
});
|
|
1021
|
+
const trimmed = out.trim();
|
|
1022
|
+
if (!trimmed) return [];
|
|
1023
|
+
const parsed = JSON.parse(trimmed);
|
|
1024
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1025
|
+
} catch {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
close: () => {
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
} catch {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ../lib/sessions/adapters/cursor.ts
|
|
1038
|
+
function asRecord3(v) {
|
|
1039
|
+
return v && typeof v === "object" ? v : {};
|
|
1040
|
+
}
|
|
1041
|
+
function safeParse(s) {
|
|
1042
|
+
try {
|
|
1043
|
+
return JSON.parse(s);
|
|
1044
|
+
} catch {
|
|
1045
|
+
return {};
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function cleanId(id) {
|
|
1049
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "");
|
|
1050
|
+
}
|
|
1051
|
+
function stateDbs() {
|
|
1052
|
+
const dbs = [];
|
|
1053
|
+
for (const userDir of vscodeUserDirs("Cursor")) {
|
|
1054
|
+
const db = join4(userDir, "globalStorage", "state.vscdb");
|
|
1055
|
+
if (existsSync4(db)) dbs.push(db);
|
|
1056
|
+
}
|
|
1057
|
+
return dbs;
|
|
1058
|
+
}
|
|
1059
|
+
function bubbleText(b) {
|
|
1060
|
+
const out = [];
|
|
1061
|
+
const push = (s) => {
|
|
1062
|
+
if (typeof s === "string" && s.trim()) out.push(s);
|
|
1063
|
+
};
|
|
1064
|
+
push(b.text);
|
|
1065
|
+
if (!out.length && typeof b.richText === "string") push(b.richText);
|
|
1066
|
+
return out;
|
|
1067
|
+
}
|
|
1068
|
+
var cursorAdapter = {
|
|
1069
|
+
id: "cursor",
|
|
1070
|
+
displayName: "Cursor",
|
|
1071
|
+
kind: "sqlite",
|
|
1072
|
+
detect() {
|
|
1073
|
+
const dbs = stateDbs();
|
|
1074
|
+
return dbs.length ? dbs.map((db) => ({ root: db, label: db })) : null;
|
|
1075
|
+
},
|
|
1076
|
+
listSessions(store) {
|
|
1077
|
+
const reader = openSqlite(store.root);
|
|
1078
|
+
if (!reader) return [];
|
|
1079
|
+
const refs = [];
|
|
1080
|
+
try {
|
|
1081
|
+
const rows = reader.all(
|
|
1082
|
+
"SELECT key FROM cursorDiskKV WHERE key LIKE 'composerData:%'"
|
|
1083
|
+
);
|
|
1084
|
+
const st = safeStat(store.root);
|
|
1085
|
+
for (const row of rows) {
|
|
1086
|
+
const id = cleanId(asText(row.key).slice("composerData:".length));
|
|
1087
|
+
if (!id) continue;
|
|
1088
|
+
refs.push({
|
|
1089
|
+
id: `cursor:${id}`,
|
|
1090
|
+
locator: id,
|
|
1091
|
+
store,
|
|
1092
|
+
mtimeMs: st?.mtimeMs ?? 0
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
} catch {
|
|
1096
|
+
} finally {
|
|
1097
|
+
reader.close();
|
|
1098
|
+
}
|
|
1099
|
+
return refs;
|
|
1100
|
+
},
|
|
1101
|
+
parse(ref) {
|
|
1102
|
+
const reader = openSqlite(ref.store.root);
|
|
1103
|
+
if (!reader) return null;
|
|
1104
|
+
const messages = [];
|
|
1105
|
+
let cwd = "";
|
|
1106
|
+
try {
|
|
1107
|
+
const composerId = cleanId(ref.locator);
|
|
1108
|
+
const metaRows = reader.all(
|
|
1109
|
+
`SELECT value FROM cursorDiskKV WHERE key = 'composerData:${composerId}'`
|
|
1110
|
+
);
|
|
1111
|
+
if (metaRows[0]) {
|
|
1112
|
+
const meta = asRecord3(safeParse(asText(metaRows[0].value)));
|
|
1113
|
+
if (typeof meta.cwd === "string") cwd = meta.cwd;
|
|
1114
|
+
}
|
|
1115
|
+
const bubbleRows = reader.all(
|
|
1116
|
+
`SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:${composerId}:%'`
|
|
1117
|
+
);
|
|
1118
|
+
for (const row of bubbleRows) {
|
|
1119
|
+
const b = asRecord3(safeParse(asText(row.value)));
|
|
1120
|
+
const texts = bubbleText(b);
|
|
1121
|
+
if (!texts.length) continue;
|
|
1122
|
+
const t = typeof b.type === "number" ? b.type : b.type === "user" ? 1 : 2;
|
|
1123
|
+
const r = t === 1 ? "user" : "assistant";
|
|
1124
|
+
messages.push({ role: r, texts, timestamp: null });
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
return null;
|
|
1128
|
+
} finally {
|
|
1129
|
+
reader.close();
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
tool: "cursor",
|
|
1133
|
+
sessionId: ref.locator,
|
|
1134
|
+
cwd: cwd || void 0,
|
|
1135
|
+
gitBranch: null,
|
|
1136
|
+
messages,
|
|
1137
|
+
sourcePath: ref.store.root
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// ../lib/sessions/adapters/copilot.ts
|
|
1143
|
+
import { readFileSync as readFileSync3, existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
1144
|
+
import { join as join5, basename as basename5 } from "node:path";
|
|
1145
|
+
function asRecord4(v) {
|
|
1146
|
+
return v && typeof v === "object" ? v : {};
|
|
1147
|
+
}
|
|
1148
|
+
function textFromParts(v) {
|
|
1149
|
+
const out = [];
|
|
1150
|
+
const push = (s) => {
|
|
1151
|
+
if (typeof s === "string" && s.trim()) out.push(s);
|
|
1152
|
+
};
|
|
1153
|
+
if (typeof v === "string") {
|
|
1154
|
+
push(v);
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(v)) {
|
|
1158
|
+
for (const p of v) {
|
|
1159
|
+
if (typeof p === "string") {
|
|
1160
|
+
push(p);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const r = asRecord4(p);
|
|
1164
|
+
push(r.text ?? r.value ?? r.content);
|
|
1165
|
+
}
|
|
1166
|
+
} else if (v && typeof v === "object") {
|
|
1167
|
+
const r = asRecord4(v);
|
|
1168
|
+
push(r.text ?? r.value ?? r.content);
|
|
1169
|
+
}
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1172
|
+
function workspaceCwd(wsDir) {
|
|
1173
|
+
try {
|
|
1174
|
+
const meta = asRecord4(
|
|
1175
|
+
JSON.parse(readFileSync3(join5(wsDir, "workspace.json"), "utf8"))
|
|
1176
|
+
);
|
|
1177
|
+
const folder = typeof meta.folder === "string" ? meta.folder : "";
|
|
1178
|
+
if (folder) return fileUriToPath(folder);
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
return "";
|
|
1182
|
+
}
|
|
1183
|
+
function copilotJsonToCommon(json, cwd, sourcePath) {
|
|
1184
|
+
const messages = [];
|
|
1185
|
+
let sessionId = "";
|
|
1186
|
+
let root = {};
|
|
1187
|
+
try {
|
|
1188
|
+
root = asRecord4(JSON.parse(json));
|
|
1189
|
+
} catch {
|
|
1190
|
+
return { tool: "copilot", cwd: cwd || void 0, gitBranch: null, messages, sourcePath };
|
|
1191
|
+
}
|
|
1192
|
+
if (typeof root.sessionId === "string") sessionId = root.sessionId;
|
|
1193
|
+
const requests = Array.isArray(root.requests) ? root.requests : [];
|
|
1194
|
+
for (const req of requests) {
|
|
1195
|
+
const rq = asRecord4(req);
|
|
1196
|
+
const message = asRecord4(rq.message);
|
|
1197
|
+
const userTexts = textFromParts(message.text ?? message.parts ?? rq.message);
|
|
1198
|
+
if (userTexts.length) messages.push({ role: "user", texts: userTexts, timestamp: null });
|
|
1199
|
+
const respTexts = textFromParts(rq.response ?? rq.result);
|
|
1200
|
+
if (respTexts.length) {
|
|
1201
|
+
messages.push({ role: "assistant", texts: respTexts, timestamp: null });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
tool: "copilot",
|
|
1206
|
+
sessionId: sessionId || void 0,
|
|
1207
|
+
cwd: cwd || void 0,
|
|
1208
|
+
gitBranch: null,
|
|
1209
|
+
messages,
|
|
1210
|
+
sourcePath
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
var copilotAdapter = {
|
|
1214
|
+
id: "copilot",
|
|
1215
|
+
displayName: "GitHub Copilot",
|
|
1216
|
+
kind: "node",
|
|
1217
|
+
detect() {
|
|
1218
|
+
const stores = [];
|
|
1219
|
+
for (const app of ["Code", "Code - Insiders"]) {
|
|
1220
|
+
for (const userDir of vscodeUserDirs(app)) {
|
|
1221
|
+
const wsRoot = join5(userDir, "workspaceStorage");
|
|
1222
|
+
if (existsSync5(wsRoot)) stores.push({ root: wsRoot, label: wsRoot });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return stores.length ? stores : null;
|
|
1226
|
+
},
|
|
1227
|
+
listSessions(store) {
|
|
1228
|
+
const refs = [];
|
|
1229
|
+
let hashes;
|
|
1230
|
+
try {
|
|
1231
|
+
hashes = readdirSync2(store.root);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return refs;
|
|
1234
|
+
}
|
|
1235
|
+
for (const hash2 of hashes) {
|
|
1236
|
+
const wsDir = join5(store.root, hash2);
|
|
1237
|
+
const chatDir = join5(wsDir, "chatSessions");
|
|
1238
|
+
if (!existsSync5(chatDir)) continue;
|
|
1239
|
+
const cwd = workspaceCwd(wsDir);
|
|
1240
|
+
let files;
|
|
1241
|
+
try {
|
|
1242
|
+
files = readdirSync2(chatDir);
|
|
1243
|
+
} catch {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
for (const name of files) {
|
|
1247
|
+
if (!name.endsWith(".json")) continue;
|
|
1248
|
+
const path = join5(chatDir, name);
|
|
1249
|
+
const st = safeStat(path);
|
|
1250
|
+
if (!st || st.size <= 0) continue;
|
|
1251
|
+
refs.push({
|
|
1252
|
+
id: `copilot:${basename5(name, ".json")}`,
|
|
1253
|
+
locator: path,
|
|
1254
|
+
store: { ...store, meta: { cwd } },
|
|
1255
|
+
mtimeMs: st.mtimeMs,
|
|
1256
|
+
size: st.size
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return refs;
|
|
1261
|
+
},
|
|
1262
|
+
parse(ref) {
|
|
1263
|
+
try {
|
|
1264
|
+
const cwd = typeof ref.store.meta?.cwd === "string" ? ref.store.meta.cwd : "";
|
|
1265
|
+
return copilotJsonToCommon(readFileSync3(ref.locator, "utf8"), cwd, ref.locator);
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// ../lib/sessions/adapters/windsurf.ts
|
|
1273
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1274
|
+
import { join as join6 } from "node:path";
|
|
1275
|
+
var windsurfAdapter = {
|
|
1276
|
+
id: "windsurf",
|
|
1277
|
+
displayName: "Windsurf",
|
|
1278
|
+
kind: "experimental",
|
|
1279
|
+
detect() {
|
|
1280
|
+
const stores = [];
|
|
1281
|
+
for (const userDir of vscodeUserDirs("Windsurf")) {
|
|
1282
|
+
const db = join6(userDir, "globalStorage", "state.vscdb");
|
|
1283
|
+
if (existsSync6(db)) stores.push({ root: db, label: db });
|
|
1284
|
+
else stores.push({ root: userDir, label: userDir });
|
|
1285
|
+
}
|
|
1286
|
+
return stores.length ? stores : null;
|
|
1287
|
+
},
|
|
1288
|
+
listSessions() {
|
|
1289
|
+
return [];
|
|
1290
|
+
},
|
|
1291
|
+
parse() {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
// ../lib/sessions/adapters/aider.ts
|
|
1297
|
+
import { readFileSync as readFileSync4, existsSync as existsSync7 } from "node:fs";
|
|
1298
|
+
import { join as join7, dirname } from "node:path";
|
|
1299
|
+
import { homedir as homedir4 } from "node:os";
|
|
1300
|
+
var HISTORY_FILE = ".aider.chat.history.md";
|
|
1301
|
+
function candidateRoots() {
|
|
1302
|
+
const home = homedir4();
|
|
1303
|
+
const roots = /* @__PURE__ */ new Set();
|
|
1304
|
+
try {
|
|
1305
|
+
roots.add(process.cwd());
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
for (const name of ["dev", "code", "src", "projects", "repos", "work", "workspace", "Documents", "git"]) {
|
|
1309
|
+
const p = join7(home, name);
|
|
1310
|
+
if (existsSync7(p)) roots.add(p);
|
|
1311
|
+
}
|
|
1312
|
+
return [...roots];
|
|
1313
|
+
}
|
|
1314
|
+
var SESSION_HEADER = /^#\s*aider chat started at\s*(.+)$/i;
|
|
1315
|
+
function headerToIso(stamp) {
|
|
1316
|
+
const t = stamp.trim();
|
|
1317
|
+
const m = t.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/);
|
|
1318
|
+
if (!m) {
|
|
1319
|
+
const p2 = Date.parse(t);
|
|
1320
|
+
return Number.isFinite(p2) ? new Date(p2).toISOString() : null;
|
|
1321
|
+
}
|
|
1322
|
+
const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
|
|
1323
|
+
const p = Date.parse(iso);
|
|
1324
|
+
return Number.isFinite(p) ? new Date(p).toISOString() : null;
|
|
1325
|
+
}
|
|
1326
|
+
function splitSessions(md) {
|
|
1327
|
+
const blocks = [];
|
|
1328
|
+
let current = null;
|
|
1329
|
+
for (const line2 of md.split(/\r?\n/)) {
|
|
1330
|
+
const h = line2.match(SESSION_HEADER);
|
|
1331
|
+
if (h) {
|
|
1332
|
+
if (current) blocks.push(current);
|
|
1333
|
+
current = { startIso: headerToIso(h[1]), lines: [] };
|
|
1334
|
+
} else if (current) {
|
|
1335
|
+
current.lines.push(line2);
|
|
1336
|
+
} else {
|
|
1337
|
+
current = { startIso: null, lines: [line2] };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (current) blocks.push(current);
|
|
1341
|
+
return blocks.filter((b) => b.lines.some((l) => l.trim()));
|
|
1342
|
+
}
|
|
1343
|
+
function aiderBlockToCommon(block, cwd, index, sourcePath) {
|
|
1344
|
+
const messages = [];
|
|
1345
|
+
const files = [];
|
|
1346
|
+
const filesSeen = /* @__PURE__ */ new Set();
|
|
1347
|
+
const ts = block.startIso;
|
|
1348
|
+
let userBuf = [];
|
|
1349
|
+
let asstBuf = [];
|
|
1350
|
+
const flushUser = () => {
|
|
1351
|
+
if (userBuf.length) {
|
|
1352
|
+
messages.push({ role: "user", texts: [userBuf.join("\n").trim()], timestamp: ts });
|
|
1353
|
+
userBuf = [];
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
const flushAsst = () => {
|
|
1357
|
+
if (asstBuf.length) {
|
|
1358
|
+
const text = asstBuf.join("\n").trim();
|
|
1359
|
+
if (text) messages.push({ role: "assistant", texts: [text], timestamp: ts });
|
|
1360
|
+
asstBuf = [];
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
for (const line2 of block.lines) {
|
|
1364
|
+
const applied = line2.match(/Applied edit to\s+(.+?)\s*$/i);
|
|
1365
|
+
if (applied) {
|
|
1366
|
+
const f = applied[1].trim();
|
|
1367
|
+
if (f && !filesSeen.has(f)) {
|
|
1368
|
+
filesSeen.add(f);
|
|
1369
|
+
files.push(f);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const user = line2.match(/^####\s?(.*)$/);
|
|
1373
|
+
if (user) {
|
|
1374
|
+
flushAsst();
|
|
1375
|
+
userBuf.push(user[1]);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (/^>\s?/.test(line2)) {
|
|
1379
|
+
flushUser();
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
flushUser();
|
|
1383
|
+
asstBuf.push(line2);
|
|
1384
|
+
}
|
|
1385
|
+
flushUser();
|
|
1386
|
+
flushAsst();
|
|
1387
|
+
return {
|
|
1388
|
+
tool: "aider",
|
|
1389
|
+
sessionId: void 0,
|
|
1390
|
+
cwd,
|
|
1391
|
+
messages,
|
|
1392
|
+
filesTouched: files.length ? files : void 0,
|
|
1393
|
+
sourcePath: sourcePath ? `${sourcePath}#${index}` : void 0
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
var aiderAdapter = {
|
|
1397
|
+
id: "aider",
|
|
1398
|
+
displayName: "Aider",
|
|
1399
|
+
kind: "node",
|
|
1400
|
+
detect() {
|
|
1401
|
+
const found = [];
|
|
1402
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1403
|
+
for (const root of candidateRoots()) {
|
|
1404
|
+
const hits = walkFiles(root, (name) => name === HISTORY_FILE, { maxDepth: 4 });
|
|
1405
|
+
for (const hit of hits) {
|
|
1406
|
+
const repo = dirname(hit);
|
|
1407
|
+
if (seen.has(repo)) continue;
|
|
1408
|
+
seen.add(repo);
|
|
1409
|
+
found.push({ root: repo, label: repo, meta: { history: hit } });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return found.length ? found : null;
|
|
1413
|
+
},
|
|
1414
|
+
listSessions(store) {
|
|
1415
|
+
const history = store.meta?.history || join7(store.root, HISTORY_FILE);
|
|
1416
|
+
const st = safeStat(history);
|
|
1417
|
+
if (!st || st.size <= 0) return [];
|
|
1418
|
+
let md;
|
|
1419
|
+
try {
|
|
1420
|
+
md = readFileSync4(history, "utf8");
|
|
1421
|
+
} catch {
|
|
1422
|
+
return [];
|
|
1423
|
+
}
|
|
1424
|
+
const blocks = splitSessions(md);
|
|
1425
|
+
return blocks.map((_, i) => ({
|
|
1426
|
+
id: `aider:${store.root}:${i}`,
|
|
1427
|
+
locator: `${history}#${i}`,
|
|
1428
|
+
store,
|
|
1429
|
+
mtimeMs: st.mtimeMs,
|
|
1430
|
+
size: st.size,
|
|
1431
|
+
label: store.root
|
|
1432
|
+
}));
|
|
1433
|
+
},
|
|
1434
|
+
parse(ref) {
|
|
1435
|
+
const hashIdx = ref.locator.lastIndexOf("#");
|
|
1436
|
+
const path = hashIdx >= 0 ? ref.locator.slice(0, hashIdx) : ref.locator;
|
|
1437
|
+
const index = hashIdx >= 0 ? Number(ref.locator.slice(hashIdx + 1)) || 0 : 0;
|
|
1438
|
+
try {
|
|
1439
|
+
const md = readFileSync4(path, "utf8");
|
|
1440
|
+
const blocks = splitSessions(md);
|
|
1441
|
+
const block = blocks[index];
|
|
1442
|
+
if (!block) return null;
|
|
1443
|
+
return aiderBlockToCommon(block, ref.store.root, index, path);
|
|
1444
|
+
} catch {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
// ../lib/sessions/adapters/continue.ts
|
|
1451
|
+
import { readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
|
|
1452
|
+
import { join as join8, basename as basename6 } from "node:path";
|
|
1453
|
+
import { homedir as homedir5 } from "node:os";
|
|
1454
|
+
function sessionsRoot() {
|
|
1455
|
+
return join8(homedir5(), ".continue", "sessions");
|
|
1456
|
+
}
|
|
1457
|
+
function asRecord5(v) {
|
|
1458
|
+
return v && typeof v === "object" ? v : {};
|
|
1459
|
+
}
|
|
1460
|
+
function textFromContent2(content) {
|
|
1461
|
+
const out = [];
|
|
1462
|
+
const push = (s) => {
|
|
1463
|
+
if (typeof s === "string" && s.trim()) out.push(s);
|
|
1464
|
+
};
|
|
1465
|
+
if (typeof content === "string") {
|
|
1466
|
+
push(content);
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
if (Array.isArray(content)) {
|
|
1470
|
+
for (const block of content) {
|
|
1471
|
+
if (typeof block === "string") {
|
|
1472
|
+
push(block);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
const b = asRecord5(block);
|
|
1476
|
+
push(b.text ?? b.content);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return out;
|
|
1480
|
+
}
|
|
1481
|
+
function continueJsonToCommon(json, sourcePath) {
|
|
1482
|
+
const messages = [];
|
|
1483
|
+
let cwd = "";
|
|
1484
|
+
let sessionId = "";
|
|
1485
|
+
let root = {};
|
|
1486
|
+
try {
|
|
1487
|
+
root = asRecord5(JSON.parse(json));
|
|
1488
|
+
} catch {
|
|
1489
|
+
return { tool: "continue", gitBranch: null, messages, sourcePath };
|
|
1490
|
+
}
|
|
1491
|
+
if (typeof root.sessionId === "string") sessionId = root.sessionId;
|
|
1492
|
+
if (typeof root.workspaceDirectory === "string") cwd = root.workspaceDirectory;
|
|
1493
|
+
const history = Array.isArray(root.history) ? root.history : [];
|
|
1494
|
+
for (const entry of history) {
|
|
1495
|
+
const e = asRecord5(entry);
|
|
1496
|
+
const msg = e.message !== void 0 ? asRecord5(e.message) : e;
|
|
1497
|
+
const role = typeof msg.role === "string" ? msg.role : "";
|
|
1498
|
+
const texts = textFromContent2(msg.content);
|
|
1499
|
+
if (!texts.length) continue;
|
|
1500
|
+
const r = role === "assistant" || role === "system" ? "assistant" : "user";
|
|
1501
|
+
messages.push({ role: r, texts, timestamp: null });
|
|
1502
|
+
}
|
|
1503
|
+
return {
|
|
1504
|
+
tool: "continue",
|
|
1505
|
+
sessionId: sessionId || void 0,
|
|
1506
|
+
cwd: cwd || void 0,
|
|
1507
|
+
gitBranch: null,
|
|
1508
|
+
messages,
|
|
1509
|
+
sourcePath
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
var continueAdapter = {
|
|
1513
|
+
id: "continue",
|
|
1514
|
+
displayName: "Continue",
|
|
1515
|
+
kind: "node",
|
|
1516
|
+
detect() {
|
|
1517
|
+
const root = sessionsRoot();
|
|
1518
|
+
if (!existsSync8(root)) return null;
|
|
1519
|
+
return [{ root, label: root }];
|
|
1520
|
+
},
|
|
1521
|
+
listSessions(store) {
|
|
1522
|
+
let names;
|
|
1523
|
+
try {
|
|
1524
|
+
names = readdirSync3(store.root);
|
|
1525
|
+
} catch {
|
|
1526
|
+
return [];
|
|
1527
|
+
}
|
|
1528
|
+
const refs = [];
|
|
1529
|
+
for (const name of names) {
|
|
1530
|
+
if (!name.endsWith(".json") || name === "sessions.json") continue;
|
|
1531
|
+
const path = join8(store.root, name);
|
|
1532
|
+
const st = safeStat(path);
|
|
1533
|
+
if (!st || st.size <= 0) continue;
|
|
1534
|
+
refs.push({
|
|
1535
|
+
id: `continue:${basename6(name, ".json")}`,
|
|
1536
|
+
locator: path,
|
|
1537
|
+
store,
|
|
1538
|
+
mtimeMs: st.mtimeMs,
|
|
1539
|
+
size: st.size
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return refs;
|
|
1543
|
+
},
|
|
1544
|
+
parse(ref) {
|
|
1545
|
+
try {
|
|
1546
|
+
return continueJsonToCommon(readFileSync5(ref.locator, "utf8"), ref.locator);
|
|
1547
|
+
} catch {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// ../lib/sessions/adapters/zed.ts
|
|
1554
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1555
|
+
var zedAdapter = {
|
|
1556
|
+
id: "zed",
|
|
1557
|
+
displayName: "Zed",
|
|
1558
|
+
kind: "experimental",
|
|
1559
|
+
detect() {
|
|
1560
|
+
const stores = [];
|
|
1561
|
+
for (const dir of appSupportDirs("Zed")) {
|
|
1562
|
+
if (existsSync9(dir)) stores.push({ root: dir, label: dir });
|
|
1563
|
+
}
|
|
1564
|
+
return stores.length ? stores : null;
|
|
1565
|
+
},
|
|
1566
|
+
listSessions() {
|
|
1567
|
+
return [];
|
|
1568
|
+
},
|
|
1569
|
+
parse() {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// ../lib/sessions/adapters/index.ts
|
|
1575
|
+
var ADAPTERS = [
|
|
1576
|
+
claudeCodeAdapter,
|
|
1577
|
+
codexAdapter,
|
|
1578
|
+
cursorAdapter,
|
|
1579
|
+
copilotAdapter,
|
|
1580
|
+
windsurfAdapter,
|
|
1581
|
+
aiderAdapter,
|
|
1582
|
+
continueAdapter,
|
|
1583
|
+
zedAdapter
|
|
1584
|
+
];
|
|
1585
|
+
function discoverAllSessions(opts = {}) {
|
|
1586
|
+
const detected = [];
|
|
1587
|
+
const sessions = [];
|
|
1588
|
+
const notes = [];
|
|
1589
|
+
const haveSqlite = sqliteAvailable();
|
|
1590
|
+
for (const adapter of ADAPTERS) {
|
|
1591
|
+
if (opts.tool && adapter.id !== opts.tool) continue;
|
|
1592
|
+
let stores = null;
|
|
1593
|
+
try {
|
|
1594
|
+
stores = adapter.detect();
|
|
1595
|
+
} catch {
|
|
1596
|
+
stores = null;
|
|
1597
|
+
}
|
|
1598
|
+
if (!stores || stores.length === 0) continue;
|
|
1599
|
+
if (adapter.kind === "sqlite" && !haveSqlite) {
|
|
1600
|
+
detected.push({ adapter, stores, sessionCount: 0 });
|
|
1601
|
+
notes.push(
|
|
1602
|
+
`install better-sqlite3 to enable ${adapter.displayName} capture (npm i -g better-sqlite3)`
|
|
1603
|
+
);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
let refs = [];
|
|
1607
|
+
for (const store of stores) {
|
|
1608
|
+
try {
|
|
1609
|
+
refs.push(...adapter.listSessions(store));
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (adapter.kind === "experimental" && refs.length === 0) {
|
|
1614
|
+
detected.push({ adapter, stores, sessionCount: 0 });
|
|
1615
|
+
notes.push(`${adapter.displayName} capture is experimental; detected but not read yet`);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
detected.push({ adapter, stores, sessionCount: refs.length });
|
|
1619
|
+
for (const ref of refs) sessions.push({ adapter, ref });
|
|
1620
|
+
}
|
|
1621
|
+
sessions.sort((a, b) => b.ref.mtimeMs - a.ref.mtimeMs);
|
|
1622
|
+
return { detected, sessions, notes };
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// currently.mjs
|
|
1626
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
1627
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
1628
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
1629
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
1630
|
+
var line = () => console.log(dim("\u2500".repeat(64)));
|
|
1631
|
+
function parseArgs(argv) {
|
|
1632
|
+
const args = { _: [] };
|
|
1633
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1634
|
+
const a = argv[i];
|
|
1635
|
+
if (a === "--refine") args.refine = true;
|
|
1636
|
+
else if (a === "--all") args.all = true;
|
|
1637
|
+
else if (a === "--limit") args.limit = Number(argv[++i]);
|
|
1638
|
+
else if (a === "--url") args.url = argv[++i];
|
|
1639
|
+
else if (a === "--token") args.token = argv[++i];
|
|
1640
|
+
else if (a === "--interval") args.interval = Number(argv[++i]);
|
|
1641
|
+
else if (a === "--tool") args.tool = argv[++i];
|
|
1642
|
+
else if (a.startsWith("--tool=")) args.tool = a.slice(7);
|
|
1643
|
+
else if (a.startsWith("--url=")) args.url = a.slice(6);
|
|
1644
|
+
else if (a.startsWith("--token=")) args.token = a.slice(8);
|
|
1645
|
+
else if (a.startsWith("--limit=")) args.limit = Number(a.slice(8));
|
|
1646
|
+
else if (a.startsWith("--interval=")) args.interval = Number(a.slice(11));
|
|
1647
|
+
else args._.push(a);
|
|
1648
|
+
}
|
|
1649
|
+
return args;
|
|
1650
|
+
}
|
|
1651
|
+
function configDir() {
|
|
1652
|
+
return join9(homedir6(), ".currently");
|
|
1653
|
+
}
|
|
1654
|
+
function configPath() {
|
|
1655
|
+
return join9(configDir(), "config.json");
|
|
1656
|
+
}
|
|
1657
|
+
function statePath() {
|
|
1658
|
+
return join9(configDir(), "state.json");
|
|
1659
|
+
}
|
|
1660
|
+
function readJson(path, fallback) {
|
|
1661
|
+
try {
|
|
1662
|
+
if (!existsSync10(path)) return fallback;
|
|
1663
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1664
|
+
} catch {
|
|
1665
|
+
return fallback;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function writeJson(path, value) {
|
|
1669
|
+
mkdirSync(configDir(), { recursive: true });
|
|
1670
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
1671
|
+
}
|
|
1672
|
+
function readConfig() {
|
|
1673
|
+
return readJson(configPath(), {});
|
|
1674
|
+
}
|
|
1675
|
+
function readState() {
|
|
1676
|
+
return readJson(statePath(), { cursorMs: 0, syncedIds: [] });
|
|
1677
|
+
}
|
|
1678
|
+
function resolveToken(args) {
|
|
1679
|
+
return args.token || process.env.CURRENTLY_TOKEN || readConfig().token || null;
|
|
1680
|
+
}
|
|
1681
|
+
function resolveUrl(args) {
|
|
1682
|
+
return args.url || process.env.CURRENTLY_URL || readConfig().url || null;
|
|
1683
|
+
}
|
|
1684
|
+
function sourceIdFor(tool) {
|
|
1685
|
+
return tool === "claude-code" ? "claude_code:local" : `${tool}:local`;
|
|
1686
|
+
}
|
|
1687
|
+
function discoverSessions(toolFilter) {
|
|
1688
|
+
const { detected, sessions, notes } = discoverAllSessions(
|
|
1689
|
+
toolFilter ? { tool: toolFilter } : {}
|
|
1690
|
+
);
|
|
1691
|
+
const items = sessions.map(({ adapter, ref }) => ({
|
|
1692
|
+
adapter,
|
|
1693
|
+
ref,
|
|
1694
|
+
tool: adapter.id,
|
|
1695
|
+
toolName: adapter.displayName,
|
|
1696
|
+
path: ref.locator,
|
|
1697
|
+
mtimeMs: ref.mtimeMs,
|
|
1698
|
+
size: ref.size ?? 0
|
|
1699
|
+
}));
|
|
1700
|
+
return { detected, items, notes };
|
|
1701
|
+
}
|
|
1702
|
+
var RS = "";
|
|
1703
|
+
var FS = "";
|
|
1704
|
+
function isGitRepo(cwd) {
|
|
1705
|
+
try {
|
|
1706
|
+
execFileSync2("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], {
|
|
1707
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1708
|
+
});
|
|
1709
|
+
return true;
|
|
1710
|
+
} catch {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function commitsInWindow(cwd, startedAt, endedAt, padMs = 60 * 60 * 1e3) {
|
|
1715
|
+
if (!existsSync10(cwd) || !isGitRepo(cwd)) return { ok: false, commits: [] };
|
|
1716
|
+
const since = new Date(Date.parse(startedAt) - padMs).toISOString();
|
|
1717
|
+
const until = new Date(Date.parse(endedAt) + padMs).toISOString();
|
|
1718
|
+
let raw;
|
|
1719
|
+
try {
|
|
1720
|
+
raw = execFileSync2(
|
|
1721
|
+
"git",
|
|
1722
|
+
[
|
|
1723
|
+
"-C",
|
|
1724
|
+
cwd,
|
|
1725
|
+
"log",
|
|
1726
|
+
`--since=${since}`,
|
|
1727
|
+
`--until=${until}`,
|
|
1728
|
+
"--numstat",
|
|
1729
|
+
`--pretty=format:${RS}%H${FS}%s${FS}%cI`
|
|
1730
|
+
],
|
|
1731
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 20 * 1024 * 1024 }
|
|
1732
|
+
);
|
|
1733
|
+
} catch {
|
|
1734
|
+
return { ok: true, commits: [] };
|
|
1735
|
+
}
|
|
1736
|
+
const commits = [];
|
|
1737
|
+
for (const block of raw.split(RS)) {
|
|
1738
|
+
if (!block.trim()) continue;
|
|
1739
|
+
const [header, ...rest] = block.split("\n");
|
|
1740
|
+
const [sha, subject, date] = header.split(FS);
|
|
1741
|
+
if (!sha) continue;
|
|
1742
|
+
let additions = 0;
|
|
1743
|
+
let deletions = 0;
|
|
1744
|
+
let hasStats = false;
|
|
1745
|
+
for (const l of rest) {
|
|
1746
|
+
const m = l.match(/^(\d+|-)\t(\d+|-)\t/);
|
|
1747
|
+
if (!m) continue;
|
|
1748
|
+
hasStats = true;
|
|
1749
|
+
if (m[1] !== "-") additions += Number(m[1]);
|
|
1750
|
+
if (m[2] !== "-") deletions += Number(m[2]);
|
|
1751
|
+
}
|
|
1752
|
+
const cleanSubject = redact(subject ?? "").text;
|
|
1753
|
+
commits.push({
|
|
1754
|
+
sha,
|
|
1755
|
+
message: cleanSubject,
|
|
1756
|
+
committedAt: date,
|
|
1757
|
+
...hasStats ? { additions, deletions } : {}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
return { ok: true, commits };
|
|
1761
|
+
}
|
|
1762
|
+
var NARRATIVE_FIELDS = [
|
|
1763
|
+
"problem",
|
|
1764
|
+
"approach",
|
|
1765
|
+
"decisions",
|
|
1766
|
+
"outcome",
|
|
1767
|
+
"filesTouched",
|
|
1768
|
+
"cwd"
|
|
1769
|
+
];
|
|
1770
|
+
async function processSession(item, { refine }) {
|
|
1771
|
+
const common = item.adapter.parse(item.ref);
|
|
1772
|
+
if (!common) return null;
|
|
1773
|
+
let parsed = assembleSession(common);
|
|
1774
|
+
if (refine) parsed = await refineWithGroq(parsed);
|
|
1775
|
+
const { ok: repoOk, commits } = commitsInWindow(
|
|
1776
|
+
parsed.cwd,
|
|
1777
|
+
parsed.startedAt,
|
|
1778
|
+
parsed.endedAt
|
|
1779
|
+
);
|
|
1780
|
+
const { value: safe, count: redactions } = redactFields(parsed, NARRATIVE_FIELDS);
|
|
1781
|
+
const result = sessionToEvent(safe, {
|
|
1782
|
+
userId: "self",
|
|
1783
|
+
sourceId: sourceIdFor(item.tool),
|
|
1784
|
+
commits
|
|
1785
|
+
});
|
|
1786
|
+
return { file: item, parsed: safe, repoOk, commits, redactions, result, tool: item.toolName };
|
|
1787
|
+
}
|
|
1788
|
+
function printProof({ parsed, repoOk, commits, redactions, result, tool }) {
|
|
1789
|
+
line();
|
|
1790
|
+
console.log(
|
|
1791
|
+
bold(parsed.repo) + (tool ? dim(` [${tool}]`) : "") + dim(` ${parsed.startedAt} \u2192 ${parsed.endedAt}`)
|
|
1792
|
+
);
|
|
1793
|
+
console.log(dim(parsed.cwd));
|
|
1794
|
+
console.log();
|
|
1795
|
+
console.log(bold("Problem ") + parsed.problem);
|
|
1796
|
+
console.log(bold("Approach ") + parsed.approach);
|
|
1797
|
+
if (parsed.decisions.length) {
|
|
1798
|
+
console.log(bold("Decisions"));
|
|
1799
|
+
for (const d of parsed.decisions) console.log(" - " + d);
|
|
1800
|
+
}
|
|
1801
|
+
console.log(bold("Outcome ") + parsed.outcome);
|
|
1802
|
+
console.log();
|
|
1803
|
+
console.log(
|
|
1804
|
+
dim("files touched: ") + (parsed.filesTouched.join(", ") || "none")
|
|
1805
|
+
);
|
|
1806
|
+
console.log(dim("tools used: ") + (parsed.toolsUsed.join(", ") || "none"));
|
|
1807
|
+
console.log(
|
|
1808
|
+
dim("redactions: ") + (redactions > 0 ? yellow(`${redactions} secret/PII value(s) removed`) : "0")
|
|
1809
|
+
);
|
|
1810
|
+
if (!repoOk) {
|
|
1811
|
+
console.log(yellow("not a git repo: cannot ground this session, skipping."));
|
|
1812
|
+
}
|
|
1813
|
+
if (result.grounded && result.event) {
|
|
1814
|
+
console.log(
|
|
1815
|
+
green(`grounded: ${result.event.id}`) + dim(` (${commits.length} commit(s) in window)`)
|
|
1816
|
+
);
|
|
1817
|
+
for (const c of result.matchedCommits.slice(0, 5)) {
|
|
1818
|
+
console.log(dim(` \xB7 ${c.sha.slice(0, 7)} ${c.message}`));
|
|
1819
|
+
}
|
|
1820
|
+
} else {
|
|
1821
|
+
console.log(yellow(`dropped: ${result.reason}`));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function cmdSources(args) {
|
|
1825
|
+
const { detected, items, notes } = discoverSessions(args.tool);
|
|
1826
|
+
console.log(bold("AI tools detected on this machine"));
|
|
1827
|
+
line();
|
|
1828
|
+
if (detected.length === 0) {
|
|
1829
|
+
console.log(dim("None yet. Build with a supported tool and it shows up here."));
|
|
1830
|
+
}
|
|
1831
|
+
for (const d of detected) {
|
|
1832
|
+
const kind = d.adapter.kind === "sqlite" ? "sqlite" : d.adapter.kind === "experimental" ? "experimental" : "ready";
|
|
1833
|
+
console.log(
|
|
1834
|
+
`${green("\u2022")} ${bold(d.adapter.displayName.padEnd(18).slice(0, 18))} ${dim(String(d.sessionCount).padStart(4))} session(s) ${dim(kind)}`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
console.log(dim(`
|
|
1838
|
+
${items.length} session(s) total, newest first.`));
|
|
1839
|
+
for (const n of notes) console.log(yellow(" " + n));
|
|
1840
|
+
console.log(
|
|
1841
|
+
dim(
|
|
1842
|
+
"\nSupported: Claude Code, Codex, Cursor, GitHub Copilot, Windsurf, Aider, Continue, Zed."
|
|
1843
|
+
)
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
function cmdScan(args) {
|
|
1847
|
+
const { detected, items, notes } = discoverSessions(args.tool);
|
|
1848
|
+
if (items.length === 0 && detected.length === 0) {
|
|
1849
|
+
console.log("No AI-coding sessions found on this machine yet.");
|
|
1850
|
+
console.log(
|
|
1851
|
+
dim(
|
|
1852
|
+
"Build with Claude Code, Codex, Cursor, Copilot, Aider, or Continue, then run this again."
|
|
1853
|
+
)
|
|
1854
|
+
);
|
|
1855
|
+
for (const n of notes) console.log(yellow(" " + n));
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
console.log(bold(`Found ${items.length} session(s) across ${detected.length} tool(s):`));
|
|
1859
|
+
line();
|
|
1860
|
+
for (const f of items.slice(0, 40)) {
|
|
1861
|
+
let repo = "?";
|
|
1862
|
+
try {
|
|
1863
|
+
const common = f.adapter.parse(f.ref);
|
|
1864
|
+
if (common) repo = assembleSession(common).repo;
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1867
|
+
const when = new Date(f.mtimeMs).toISOString();
|
|
1868
|
+
console.log(
|
|
1869
|
+
`${bold(repo.padEnd(24).slice(0, 24))} ${dim(f.toolName.padEnd(16).slice(0, 16))} ${dim(when)}`
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
if (items.length > 40) console.log(dim(`\u2026 and ${items.length - 40} more`));
|
|
1873
|
+
for (const n of notes) console.log(yellow(" " + n));
|
|
1874
|
+
}
|
|
1875
|
+
async function processAndPrint(picked, totalCount, args, { headline }) {
|
|
1876
|
+
console.log(
|
|
1877
|
+
bold(headline) + dim(` (${picked.length} of ${totalCount} session(s), read-only, nothing sent)`)
|
|
1878
|
+
);
|
|
1879
|
+
const results = [];
|
|
1880
|
+
for (const f of picked) {
|
|
1881
|
+
const r = await processSession(f, { refine: args.refine });
|
|
1882
|
+
if (!r) continue;
|
|
1883
|
+
printProof(r);
|
|
1884
|
+
results.push(r);
|
|
1885
|
+
}
|
|
1886
|
+
line();
|
|
1887
|
+
const grounded = results.filter((r) => r.result.grounded).length;
|
|
1888
|
+
console.log(
|
|
1889
|
+
dim(
|
|
1890
|
+
`${grounded} of ${results.length} session(s) ground to real commits. Only grounded sessions become proof-of-work.`
|
|
1891
|
+
)
|
|
1892
|
+
);
|
|
1893
|
+
return results;
|
|
1894
|
+
}
|
|
1895
|
+
async function cmdPreview(args) {
|
|
1896
|
+
const { items } = discoverSessions(args.tool);
|
|
1897
|
+
if (items.length === 0) {
|
|
1898
|
+
console.log("No AI-coding sessions found. Run `currentlybuilding sources` to see detected tools.");
|
|
1899
|
+
return { results: [] };
|
|
1900
|
+
}
|
|
1901
|
+
const limit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 1;
|
|
1902
|
+
const picked = items.slice(0, limit);
|
|
1903
|
+
const results = await processAndPrint(picked, items.length, args, {
|
|
1904
|
+
headline: "Proof-of-work preview"
|
|
1905
|
+
});
|
|
1906
|
+
return { results };
|
|
1907
|
+
}
|
|
1908
|
+
function cmdConnect(args) {
|
|
1909
|
+
const token = args._[1] || args.token || process.env.CURRENTLY_TOKEN;
|
|
1910
|
+
if (!token) {
|
|
1911
|
+
console.log("Paste the token from your dashboard's Connect your editor card, e.g.");
|
|
1912
|
+
console.log(dim(" currentlybuilding connect cur_xxx --url http://localhost:3000"));
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const config = readConfig();
|
|
1916
|
+
config.token = token;
|
|
1917
|
+
const url = args.url || config.url;
|
|
1918
|
+
if (url) config.url = url;
|
|
1919
|
+
writeJson(configPath(), config);
|
|
1920
|
+
console.log(green("Saved. This machine is now connected to Currently."));
|
|
1921
|
+
console.log(dim(` token stored in ${configPath()}`));
|
|
1922
|
+
if (url) console.log(dim(` app url: ${url}`));
|
|
1923
|
+
console.log("Next: run " + bold("currentlybuilding sync") + " to send your grounded sessions.");
|
|
1924
|
+
}
|
|
1925
|
+
async function cmdSync(args) {
|
|
1926
|
+
const url = resolveUrl(args);
|
|
1927
|
+
const token = resolveToken(args);
|
|
1928
|
+
if (!url) {
|
|
1929
|
+
console.log("Missing app url. Pass --url, set CURRENTLY_URL, or save it with `connect`.");
|
|
1930
|
+
console.log(dim(" currentlybuilding sync --url http://localhost:3000"));
|
|
1931
|
+
return { sent: 0 };
|
|
1932
|
+
}
|
|
1933
|
+
if (!token) {
|
|
1934
|
+
console.log(
|
|
1935
|
+
yellow("No token found. Generate one in your dashboard, then run `currentlybuilding connect <token>`.")
|
|
1936
|
+
);
|
|
1937
|
+
console.log(dim(" (demo apps accept sync without a token; real ones require it.)"));
|
|
1938
|
+
}
|
|
1939
|
+
const { items: files } = discoverSessions(args.tool);
|
|
1940
|
+
if (files.length === 0) {
|
|
1941
|
+
console.log("No AI-coding sessions found. Run `currentlybuilding sources` to see detected tools.");
|
|
1942
|
+
return { sent: 0 };
|
|
1943
|
+
}
|
|
1944
|
+
const state = readState();
|
|
1945
|
+
const cursorMs = args.all ? 0 : Number(state.cursorMs) || 0;
|
|
1946
|
+
let fresh = files.filter((f) => f.mtimeMs > cursorMs);
|
|
1947
|
+
if (Number.isFinite(args.limit) && args.limit > 0) fresh = fresh.slice(0, args.limit);
|
|
1948
|
+
if (fresh.length === 0) {
|
|
1949
|
+
console.log(green("Up to date. No new sessions since the last sync."));
|
|
1950
|
+
console.log(dim(" Run with --all to reprocess everything."));
|
|
1951
|
+
return { sent: 0 };
|
|
1952
|
+
}
|
|
1953
|
+
const results = await processAndPrint(fresh, files.length, args, {
|
|
1954
|
+
headline: "Syncing new sessions"
|
|
1955
|
+
});
|
|
1956
|
+
const events = results.filter((r) => r.result.grounded && r.result.event).map((r) => {
|
|
1957
|
+
const { userId, ...rest } = r.result.event;
|
|
1958
|
+
return rest;
|
|
1959
|
+
});
|
|
1960
|
+
const maxMs = Math.max(cursorMs, ...fresh.map((f) => f.mtimeMs));
|
|
1961
|
+
if (events.length === 0) {
|
|
1962
|
+
console.log(yellow("Nothing grounded, so nothing to send. Ship a commit and retry."));
|
|
1963
|
+
writeJson(statePath(), { ...state, cursorMs: maxMs });
|
|
1964
|
+
return { sent: 0 };
|
|
1965
|
+
}
|
|
1966
|
+
const endpoint = url.replace(/\/+$/, "") + "/api/sessions";
|
|
1967
|
+
console.log();
|
|
1968
|
+
console.log(bold(`Sending ${events.length} grounded session event(s) to ${endpoint}`));
|
|
1969
|
+
try {
|
|
1970
|
+
const res = await fetch(endpoint, {
|
|
1971
|
+
method: "POST",
|
|
1972
|
+
headers: {
|
|
1973
|
+
"content-type": "application/json",
|
|
1974
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
1975
|
+
},
|
|
1976
|
+
body: JSON.stringify({ events })
|
|
1977
|
+
});
|
|
1978
|
+
const text = await res.text();
|
|
1979
|
+
let json;
|
|
1980
|
+
try {
|
|
1981
|
+
json = JSON.parse(text);
|
|
1982
|
+
} catch {
|
|
1983
|
+
json = { raw: text };
|
|
1984
|
+
}
|
|
1985
|
+
if (res.status === 401) {
|
|
1986
|
+
console.log(
|
|
1987
|
+
yellow("Unauthorized. Your token is missing or invalid.")
|
|
1988
|
+
);
|
|
1989
|
+
console.log(dim(" Generate a fresh one in your dashboard and run `currentlybuilding connect <token>`."));
|
|
1990
|
+
return { sent: 0 };
|
|
1991
|
+
}
|
|
1992
|
+
if (!res.ok) {
|
|
1993
|
+
console.log(yellow(`Server responded ${res.status}: ${JSON.stringify(json)}`));
|
|
1994
|
+
return { sent: 0 };
|
|
1995
|
+
}
|
|
1996
|
+
const c = json.counts ?? {};
|
|
1997
|
+
console.log(
|
|
1998
|
+
green(
|
|
1999
|
+
`Done. ${c.draftsCreated ?? 0} draft(s) created` + (json.demoMode ? " (demo mode, not persisted)." : `, ${c.eventsPersisted ?? 0} event(s) stored.`)
|
|
2000
|
+
)
|
|
2001
|
+
);
|
|
2002
|
+
console.log(dim("Drafts are never auto-published. Approve them in your dashboard."));
|
|
2003
|
+
const syncedIds = Array.from(
|
|
2004
|
+
/* @__PURE__ */ new Set([...state.syncedIds ?? [], ...events.map((e) => e.id)])
|
|
2005
|
+
).slice(-500);
|
|
2006
|
+
writeJson(statePath(), { cursorMs: maxMs, syncedIds });
|
|
2007
|
+
return { sent: events.length };
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
console.log(yellow(`Could not reach ${endpoint}: ${String(err?.message ?? err)}`));
|
|
2010
|
+
return { sent: 0 };
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async function cmdWatch(args) {
|
|
2014
|
+
const minutes = Number.isFinite(args.interval) && args.interval > 0 ? args.interval : 5;
|
|
2015
|
+
console.log(
|
|
2016
|
+
bold("Watch mode") + dim(` syncing new sessions every ${minutes} minute(s). Ctrl+C to stop.`)
|
|
2017
|
+
);
|
|
2018
|
+
for (; ; ) {
|
|
2019
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2020
|
+
console.log(dim(`
|
|
2021
|
+
[${stamp}] checking for new sessions...`));
|
|
2022
|
+
try {
|
|
2023
|
+
await cmdSync(args);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
console.log(yellow(`Sync pass failed: ${String(err?.message ?? err)}`));
|
|
2026
|
+
}
|
|
2027
|
+
await sleep(minutes * 60 * 1e3);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
async function main() {
|
|
2031
|
+
const args = parseArgs(process.argv.slice(2));
|
|
2032
|
+
const cmd = args._[0] ?? "preview";
|
|
2033
|
+
try {
|
|
2034
|
+
if (cmd === "scan") cmdScan(args);
|
|
2035
|
+
else if (cmd === "sources") cmdSources(args);
|
|
2036
|
+
else if (cmd === "preview") await cmdPreview(args);
|
|
2037
|
+
else if (cmd === "sync") await cmdSync(args);
|
|
2038
|
+
else if (cmd === "connect" || cmd === "login") cmdConnect(args);
|
|
2039
|
+
else if (cmd === "watch") await cmdWatch(args);
|
|
2040
|
+
else {
|
|
2041
|
+
console.log(`Unknown command: ${cmd}`);
|
|
2042
|
+
console.log("Try: currentlybuilding connect | sources | scan | preview | sync | watch");
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
} catch (err) {
|
|
2046
|
+
console.error(yellow("Something went wrong:"), String(err?.message ?? err));
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
main();
|