@kibitzsh/kibitz 0.0.3
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 +21 -0
- package/README.md +200 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2662 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/commentary.d.ts +69 -0
- package/dist/core/commentary.d.ts.map +1 -0
- package/dist/core/commentary.js +1041 -0
- package/dist/core/commentary.js.map +1 -0
- package/dist/core/parsers/claude.d.ts +3 -0
- package/dist/core/parsers/claude.d.ts.map +1 -0
- package/dist/core/parsers/claude.js +124 -0
- package/dist/core/parsers/claude.js.map +1 -0
- package/dist/core/parsers/codex.d.ts +3 -0
- package/dist/core/parsers/codex.d.ts.map +1 -0
- package/dist/core/parsers/codex.js +133 -0
- package/dist/core/parsers/codex.js.map +1 -0
- package/dist/core/platform-support.d.ts +17 -0
- package/dist/core/platform-support.d.ts.map +1 -0
- package/dist/core/platform-support.js +146 -0
- package/dist/core/platform-support.js.map +1 -0
- package/dist/core/providers/anthropic.d.ts +15 -0
- package/dist/core/providers/anthropic.d.ts.map +1 -0
- package/dist/core/providers/anthropic.js +236 -0
- package/dist/core/providers/anthropic.js.map +1 -0
- package/dist/core/providers/openai.d.ts +16 -0
- package/dist/core/providers/openai.d.ts.map +1 -0
- package/dist/core/providers/openai.js +154 -0
- package/dist/core/providers/openai.js.map +1 -0
- package/dist/core/session-dispatch.d.ts +28 -0
- package/dist/core/session-dispatch.d.ts.map +1 -0
- package/dist/core/session-dispatch.js +453 -0
- package/dist/core/session-dispatch.js.map +1 -0
- package/dist/core/types.d.ts +78 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/watcher.d.ts +23 -0
- package/dist/core/watcher.d.ts.map +1 -0
- package/dist/core/watcher.js +866 -0
- package/dist/core/watcher.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,2662 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
var readline = __toESM(require("readline"));
|
|
28
|
+
|
|
29
|
+
// src/core/watcher.ts
|
|
30
|
+
var fs = __toESM(require("fs"));
|
|
31
|
+
var path = __toESM(require("path"));
|
|
32
|
+
var os = __toESM(require("os"));
|
|
33
|
+
var import_events = require("events");
|
|
34
|
+
|
|
35
|
+
// src/core/parsers/claude.ts
|
|
36
|
+
function summarizeToolUse(name, input) {
|
|
37
|
+
switch (name) {
|
|
38
|
+
case "Bash":
|
|
39
|
+
return `Running: ${truncate(String(input.command || ""), 80)}`;
|
|
40
|
+
case "Read":
|
|
41
|
+
return `Reading ${shortPath(String(input.file_path || ""))}`;
|
|
42
|
+
case "Write":
|
|
43
|
+
return `Writing ${shortPath(String(input.file_path || ""))}`;
|
|
44
|
+
case "Edit":
|
|
45
|
+
return `Editing ${shortPath(String(input.file_path || ""))}`;
|
|
46
|
+
case "Grep":
|
|
47
|
+
return `Searching for "${truncate(String(input.pattern || ""), 40)}"`;
|
|
48
|
+
case "Glob":
|
|
49
|
+
return `Finding files: ${truncate(String(input.pattern || ""), 40)}`;
|
|
50
|
+
case "Task":
|
|
51
|
+
return `Spawning agent: ${truncate(String(input.description || ""), 60)}`;
|
|
52
|
+
case "TodoWrite":
|
|
53
|
+
return "Updating task list";
|
|
54
|
+
case "WebSearch":
|
|
55
|
+
return `Web search: "${truncate(String(input.query || ""), 50)}"`;
|
|
56
|
+
case "WebFetch":
|
|
57
|
+
return `Fetching: ${truncate(String(input.url || ""), 60)}`;
|
|
58
|
+
default:
|
|
59
|
+
return `Using tool: ${name}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function shortPath(p) {
|
|
63
|
+
const parts = String(p || "").split(/[\\/]+/).filter(Boolean);
|
|
64
|
+
if (parts.length <= 3) return String(p || "");
|
|
65
|
+
return ".../" + parts.slice(-2).join("/");
|
|
66
|
+
}
|
|
67
|
+
function truncate(s, max) {
|
|
68
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
69
|
+
}
|
|
70
|
+
function projectNameFromCwd(cwd) {
|
|
71
|
+
const parts = String(cwd || "").split(/[\\/]+/).filter(Boolean);
|
|
72
|
+
return parts[parts.length - 1] || "unknown";
|
|
73
|
+
}
|
|
74
|
+
function parseClaudeLine(line, filePath) {
|
|
75
|
+
let obj;
|
|
76
|
+
try {
|
|
77
|
+
obj = JSON.parse(line);
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const sessionId = obj.sessionId || sessionIdFromFilePath(filePath);
|
|
82
|
+
const projectName = obj.cwd ? projectNameFromCwd(obj.cwd) : projectFromFilePath(filePath);
|
|
83
|
+
const timestamp = obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now();
|
|
84
|
+
if (obj.type === "queue-operation" || obj.type === "file-history-snapshot" || obj.type === "progress") {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const events = [];
|
|
88
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
89
|
+
for (const block of obj.message.content) {
|
|
90
|
+
if (block.type === "tool_use" && block.name && block.input) {
|
|
91
|
+
events.push({
|
|
92
|
+
sessionId,
|
|
93
|
+
projectName,
|
|
94
|
+
agent: "claude",
|
|
95
|
+
source: "cli",
|
|
96
|
+
// will be overridden by watcher
|
|
97
|
+
timestamp,
|
|
98
|
+
type: "tool_call",
|
|
99
|
+
summary: summarizeToolUse(block.name, block.input),
|
|
100
|
+
details: { tool: block.name, input: block.input }
|
|
101
|
+
});
|
|
102
|
+
} else if (block.type === "text" && block.text) {
|
|
103
|
+
const text = block.text.trim();
|
|
104
|
+
if (text.length > 0) {
|
|
105
|
+
events.push({
|
|
106
|
+
sessionId,
|
|
107
|
+
projectName,
|
|
108
|
+
agent: "claude",
|
|
109
|
+
source: "cli",
|
|
110
|
+
timestamp,
|
|
111
|
+
type: "message",
|
|
112
|
+
summary: truncate(text, 120),
|
|
113
|
+
details: { text }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (obj.type === "user" && obj.message?.content) {
|
|
120
|
+
for (const block of obj.message.content) {
|
|
121
|
+
if (block.type === "tool_result") {
|
|
122
|
+
events.push({
|
|
123
|
+
sessionId,
|
|
124
|
+
projectName,
|
|
125
|
+
agent: "claude",
|
|
126
|
+
source: "cli",
|
|
127
|
+
timestamp,
|
|
128
|
+
type: "tool_result",
|
|
129
|
+
summary: "Tool completed",
|
|
130
|
+
details: { tool_use_id: block.tool_use_id }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return events;
|
|
136
|
+
}
|
|
137
|
+
function projectFromFilePath(filePath) {
|
|
138
|
+
const segments = String(filePath || "").split(/[\\/]+/).filter(Boolean);
|
|
139
|
+
const projectDir = segments.length >= 2 ? segments[segments.length - 2] : "";
|
|
140
|
+
const projectSegments = projectDir.split("-").filter(Boolean);
|
|
141
|
+
return projectSegments[projectSegments.length - 1] || "unknown";
|
|
142
|
+
}
|
|
143
|
+
function sessionIdFromFilePath(filePath) {
|
|
144
|
+
return String(filePath || "").split(/[\\/]+/).filter(Boolean).pop()?.replace(/\.jsonl$/i, "") || "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/core/parsers/codex.ts
|
|
148
|
+
function truncate2(s, max) {
|
|
149
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
150
|
+
}
|
|
151
|
+
function projectNameFromCwd2(cwd) {
|
|
152
|
+
const parts = String(cwd || "").split(/[\\/]+/).filter(Boolean);
|
|
153
|
+
return parts[parts.length - 1] || "unknown";
|
|
154
|
+
}
|
|
155
|
+
function parseCodexLine(line, filePath) {
|
|
156
|
+
let obj;
|
|
157
|
+
try {
|
|
158
|
+
obj = JSON.parse(line);
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const timestamp = obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now();
|
|
163
|
+
const sessionId = sessionIdFromFilePath2(filePath);
|
|
164
|
+
const events = [];
|
|
165
|
+
if (obj.type === "session_meta" && obj.payload) {
|
|
166
|
+
const cwd = obj.payload.cwd || "";
|
|
167
|
+
events.push({
|
|
168
|
+
sessionId,
|
|
169
|
+
projectName: projectNameFromCwd2(cwd),
|
|
170
|
+
agent: "codex",
|
|
171
|
+
source: "cli",
|
|
172
|
+
timestamp,
|
|
173
|
+
type: "meta",
|
|
174
|
+
summary: `Codex session started (${obj.payload.model_provider || "unknown"}, v${obj.payload.cli_version || "?"})`,
|
|
175
|
+
details: { cwd, provider: obj.payload.model_provider, version: obj.payload.cli_version }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (obj.type === "response_item" && obj.payload) {
|
|
179
|
+
const p = obj.payload;
|
|
180
|
+
if (p.type === "function_call" && p.name) {
|
|
181
|
+
let args = {};
|
|
182
|
+
try {
|
|
183
|
+
args = JSON.parse(p.arguments || "{}");
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
const summary = summarizeCodexTool(p.name, args);
|
|
187
|
+
events.push({
|
|
188
|
+
sessionId,
|
|
189
|
+
projectName: projectFromFilePath2(filePath),
|
|
190
|
+
agent: "codex",
|
|
191
|
+
source: "cli",
|
|
192
|
+
timestamp,
|
|
193
|
+
type: "tool_call",
|
|
194
|
+
summary,
|
|
195
|
+
details: { tool: p.name, input: args }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (p.type === "function_call_output") {
|
|
199
|
+
events.push({
|
|
200
|
+
sessionId,
|
|
201
|
+
projectName: projectFromFilePath2(filePath),
|
|
202
|
+
agent: "codex",
|
|
203
|
+
source: "cli",
|
|
204
|
+
timestamp,
|
|
205
|
+
type: "tool_result",
|
|
206
|
+
summary: `Tool completed: ${truncate2(p.output || "", 60)}`,
|
|
207
|
+
details: { output: p.output, call_id: p.call_id }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (p.role === "assistant" && p.content) {
|
|
211
|
+
for (const block of p.content) {
|
|
212
|
+
const text = block.text || block.input_text || "";
|
|
213
|
+
if (text.trim()) {
|
|
214
|
+
events.push({
|
|
215
|
+
sessionId,
|
|
216
|
+
projectName: projectFromFilePath2(filePath),
|
|
217
|
+
agent: "codex",
|
|
218
|
+
source: "cli",
|
|
219
|
+
timestamp,
|
|
220
|
+
type: "message",
|
|
221
|
+
summary: truncate2(text.trim(), 120),
|
|
222
|
+
details: { text }
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (obj.type === "event_msg" && obj.payload) {
|
|
229
|
+
if (obj.payload.type === "task_started") {
|
|
230
|
+
events.push({
|
|
231
|
+
sessionId,
|
|
232
|
+
projectName: projectFromFilePath2(filePath),
|
|
233
|
+
agent: "codex",
|
|
234
|
+
source: "cli",
|
|
235
|
+
timestamp,
|
|
236
|
+
type: "meta",
|
|
237
|
+
summary: "Codex task started",
|
|
238
|
+
details: { turn_id: obj.payload.turn_id }
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return events;
|
|
243
|
+
}
|
|
244
|
+
function summarizeCodexTool(name, args) {
|
|
245
|
+
if (name === "shell" || name === "run_command") {
|
|
246
|
+
return `Running: ${truncate2(String(args.command || args.cmd || ""), 80)}`;
|
|
247
|
+
}
|
|
248
|
+
if (name === "read_file" || name === "file_read") {
|
|
249
|
+
return `Reading ${truncate2(String(args.path || args.file_path || ""), 60)}`;
|
|
250
|
+
}
|
|
251
|
+
if (name === "write_file" || name === "file_write") {
|
|
252
|
+
return `Writing ${truncate2(String(args.path || args.file_path || ""), 60)}`;
|
|
253
|
+
}
|
|
254
|
+
if (name === "edit_file" || name === "apply_diff") {
|
|
255
|
+
return `Editing ${truncate2(String(args.path || args.file_path || ""), 60)}`;
|
|
256
|
+
}
|
|
257
|
+
return `Using tool: ${name}`;
|
|
258
|
+
}
|
|
259
|
+
function sessionIdFromFilePath2(filePath) {
|
|
260
|
+
const basename3 = String(filePath || "").split(/[\\/]+/).filter(Boolean).pop()?.replace(/\.jsonl$/i, "") || "";
|
|
261
|
+
const match = basename3.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
|
|
262
|
+
return match ? match[1] : basename3;
|
|
263
|
+
}
|
|
264
|
+
function projectFromFilePath2(filePath) {
|
|
265
|
+
return "codex";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/core/watcher.ts
|
|
269
|
+
var SessionWatcher = class _SessionWatcher extends import_events.EventEmitter {
|
|
270
|
+
watched = /* @__PURE__ */ new Map();
|
|
271
|
+
scanInterval = null;
|
|
272
|
+
claudeIdeLocks = /* @__PURE__ */ new Map();
|
|
273
|
+
sessionProjectNames = /* @__PURE__ */ new Map();
|
|
274
|
+
// sessionId → projectName from meta
|
|
275
|
+
static ACTIVE_SESSION_WINDOW_MS = 5 * 60 * 1e3;
|
|
276
|
+
constructor() {
|
|
277
|
+
super();
|
|
278
|
+
}
|
|
279
|
+
start() {
|
|
280
|
+
this.scan();
|
|
281
|
+
this.scanInterval = setInterval(() => this.scan(), 15e3);
|
|
282
|
+
}
|
|
283
|
+
stop() {
|
|
284
|
+
if (this.scanInterval) {
|
|
285
|
+
clearInterval(this.scanInterval);
|
|
286
|
+
this.scanInterval = null;
|
|
287
|
+
}
|
|
288
|
+
for (const w of this.watched.values()) {
|
|
289
|
+
w.watcher?.close();
|
|
290
|
+
}
|
|
291
|
+
this.watched.clear();
|
|
292
|
+
}
|
|
293
|
+
getActiveSessions() {
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
const sessionsByKey = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const w of this.watched.values()) {
|
|
297
|
+
if (w.ignore) continue;
|
|
298
|
+
this.reconcileCodexSessionTitle(w);
|
|
299
|
+
try {
|
|
300
|
+
const stat = fs.statSync(w.filePath);
|
|
301
|
+
if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
|
|
302
|
+
const session = {
|
|
303
|
+
id: w.sessionId,
|
|
304
|
+
projectName: w.projectName,
|
|
305
|
+
sessionTitle: w.sessionTitle,
|
|
306
|
+
agent: w.agent,
|
|
307
|
+
source: this.detectSource(w),
|
|
308
|
+
filePath: w.filePath,
|
|
309
|
+
lastActivity: stat.mtimeMs
|
|
310
|
+
};
|
|
311
|
+
const key = `${session.agent}:${session.id.toLowerCase()}`;
|
|
312
|
+
const existing = sessionsByKey.get(key);
|
|
313
|
+
if (!existing || existing.lastActivity < session.lastActivity) {
|
|
314
|
+
sessionsByKey.set(key, session);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return Array.from(sessionsByKey.values()).sort((a, b) => b.lastActivity - a.lastActivity);
|
|
320
|
+
}
|
|
321
|
+
scan() {
|
|
322
|
+
this.loadIdeLocks();
|
|
323
|
+
this.scanClaude();
|
|
324
|
+
this.scanCodex();
|
|
325
|
+
this.pruneStale();
|
|
326
|
+
}
|
|
327
|
+
scanClaude() {
|
|
328
|
+
const claudeDir = path.join(os.homedir(), ".claude", "projects");
|
|
329
|
+
if (!fs.existsSync(claudeDir)) return;
|
|
330
|
+
let projectDirs;
|
|
331
|
+
try {
|
|
332
|
+
projectDirs = fs.readdirSync(claudeDir).filter((d) => {
|
|
333
|
+
const full = path.join(claudeDir, d);
|
|
334
|
+
try {
|
|
335
|
+
return fs.statSync(full).isDirectory();
|
|
336
|
+
} catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
} catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
for (const dir of projectDirs) {
|
|
345
|
+
const dirPath = path.join(claudeDir, dir);
|
|
346
|
+
let files;
|
|
347
|
+
try {
|
|
348
|
+
files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
349
|
+
} catch {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
for (const file of files) {
|
|
353
|
+
const filePath = path.join(dirPath, file);
|
|
354
|
+
try {
|
|
355
|
+
const stat = fs.statSync(filePath);
|
|
356
|
+
if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
|
|
357
|
+
} catch {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!this.watched.has(filePath)) {
|
|
361
|
+
const projectName = extractProjectName(dir);
|
|
362
|
+
this.watchFile(filePath, "claude", projectName);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
scanCodex() {
|
|
368
|
+
const currentTime = Date.now();
|
|
369
|
+
for (const sessionsDir of codexSessionDirs(2)) {
|
|
370
|
+
if (!fs.existsSync(sessionsDir)) continue;
|
|
371
|
+
let files;
|
|
372
|
+
try {
|
|
373
|
+
files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
374
|
+
} catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
const filePath = path.join(sessionsDir, file);
|
|
379
|
+
try {
|
|
380
|
+
const stat = fs.statSync(filePath);
|
|
381
|
+
if (currentTime - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
|
|
382
|
+
} catch {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!this.watched.has(filePath)) {
|
|
386
|
+
this.watchFile(filePath, "codex", "codex");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
watchFile(filePath, agent, projectName) {
|
|
392
|
+
let offset;
|
|
393
|
+
try {
|
|
394
|
+
const stat = fs.statSync(filePath);
|
|
395
|
+
offset = stat.size;
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
let resolvedProjectName = projectName;
|
|
400
|
+
if (agent === "codex") {
|
|
401
|
+
const codexProject = extractCodexProjectName(filePath);
|
|
402
|
+
if (codexProject) resolvedProjectName = codexProject;
|
|
403
|
+
}
|
|
404
|
+
const sessionId = extractSessionIdFromLog(
|
|
405
|
+
filePath,
|
|
406
|
+
agent,
|
|
407
|
+
deriveSessionId(filePath, agent)
|
|
408
|
+
);
|
|
409
|
+
const ignore = agent === "codex" && isKibitzInternalCodexSession(filePath);
|
|
410
|
+
const sessionTitle = extractSessionTitle(filePath, agent, sessionId);
|
|
411
|
+
const entry = {
|
|
412
|
+
filePath,
|
|
413
|
+
sessionId,
|
|
414
|
+
offset,
|
|
415
|
+
agent,
|
|
416
|
+
ignore,
|
|
417
|
+
watcher: null,
|
|
418
|
+
projectName: resolvedProjectName,
|
|
419
|
+
sessionTitle
|
|
420
|
+
};
|
|
421
|
+
try {
|
|
422
|
+
entry.watcher = fs.watch(filePath, () => {
|
|
423
|
+
this.readNewLines(entry);
|
|
424
|
+
});
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
this.watched.set(filePath, entry);
|
|
428
|
+
}
|
|
429
|
+
readNewLines(entry) {
|
|
430
|
+
if (entry.ignore) {
|
|
431
|
+
try {
|
|
432
|
+
const stat2 = fs.statSync(entry.filePath);
|
|
433
|
+
entry.offset = stat2.size;
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
let stat;
|
|
439
|
+
try {
|
|
440
|
+
stat = fs.statSync(entry.filePath);
|
|
441
|
+
} catch {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.reconcileCodexSessionTitle(entry);
|
|
445
|
+
if (stat.size <= entry.offset) return;
|
|
446
|
+
const fd = fs.openSync(entry.filePath, "r");
|
|
447
|
+
const buf = Buffer.alloc(stat.size - entry.offset);
|
|
448
|
+
fs.readSync(fd, buf, 0, buf.length, entry.offset);
|
|
449
|
+
fs.closeSync(fd);
|
|
450
|
+
entry.offset = stat.size;
|
|
451
|
+
const chunk = buf.toString("utf8");
|
|
452
|
+
const lines = chunk.split("\n").filter((l) => l.trim());
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
if (entry.agent === "codex" && isKibitzInternalCodexLine(line)) {
|
|
455
|
+
entry.ignore = true;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (!entry.sessionTitle) {
|
|
459
|
+
if (entry.agent === "codex") {
|
|
460
|
+
const title = extractCodexSessionTitle(entry.filePath, entry.sessionId);
|
|
461
|
+
if (title) entry.sessionTitle = title;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!entry.sessionTitle) {
|
|
465
|
+
const title = extractSessionTitleFromLine(line, entry.agent);
|
|
466
|
+
if (title) entry.sessionTitle = title;
|
|
467
|
+
}
|
|
468
|
+
let events;
|
|
469
|
+
if (entry.agent === "claude") {
|
|
470
|
+
events = parseClaudeLine(line, entry.filePath);
|
|
471
|
+
} else {
|
|
472
|
+
events = parseCodexLine(line, entry.filePath);
|
|
473
|
+
}
|
|
474
|
+
for (const event of events) {
|
|
475
|
+
const normalizedEventSessionId = normalizeSessionId(event.sessionId, entry.agent);
|
|
476
|
+
if (normalizedEventSessionId && normalizedEventSessionId !== entry.sessionId) {
|
|
477
|
+
entry.sessionId = normalizedEventSessionId;
|
|
478
|
+
}
|
|
479
|
+
event.sessionId = entry.sessionId;
|
|
480
|
+
event.source = this.detectSource(entry);
|
|
481
|
+
event.sessionTitle = entry.sessionTitle || fallbackSessionTitle(entry.projectName, entry.agent);
|
|
482
|
+
if (event.type === "meta" && event.details.cwd) {
|
|
483
|
+
const cwd = String(event.details.cwd);
|
|
484
|
+
const name = cwd.split("/").pop() || cwd.split("\\").pop() || "unknown";
|
|
485
|
+
this.sessionProjectNames.set(event.sessionId, name);
|
|
486
|
+
entry.projectName = name;
|
|
487
|
+
event.projectName = name;
|
|
488
|
+
} else if (this.sessionProjectNames.has(event.sessionId)) {
|
|
489
|
+
event.projectName = this.sessionProjectNames.get(event.sessionId);
|
|
490
|
+
entry.projectName = event.projectName;
|
|
491
|
+
}
|
|
492
|
+
this.emit("event", event);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
reconcileCodexSessionTitle(entry) {
|
|
497
|
+
if (entry.agent !== "codex") return;
|
|
498
|
+
const threadTitle = getCodexThreadTitle(entry.sessionId);
|
|
499
|
+
if (threadTitle) {
|
|
500
|
+
if (threadTitle !== entry.sessionTitle) entry.sessionTitle = threadTitle;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
|
|
504
|
+
entry.sessionTitle = void 0;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
loadIdeLocks() {
|
|
508
|
+
const ideDir = path.join(os.homedir(), ".claude", "ide");
|
|
509
|
+
if (!fs.existsSync(ideDir)) return;
|
|
510
|
+
this.claudeIdeLocks.clear();
|
|
511
|
+
try {
|
|
512
|
+
const files = fs.readdirSync(ideDir).filter((f) => f.endsWith(".lock"));
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
try {
|
|
515
|
+
const content = fs.readFileSync(path.join(ideDir, file), "utf8");
|
|
516
|
+
const lock = JSON.parse(content);
|
|
517
|
+
this.claudeIdeLocks.set(file, {
|
|
518
|
+
pid: lock.pid,
|
|
519
|
+
workspaceFolders: lock.workspaceFolders || []
|
|
520
|
+
});
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
detectSource(entry) {
|
|
528
|
+
if (entry.agent === "codex") return "cli";
|
|
529
|
+
return this.claudeIdeLocks.size > 0 ? "vscode" : "cli";
|
|
530
|
+
}
|
|
531
|
+
pruneStale() {
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
for (const [filePath, entry] of this.watched) {
|
|
534
|
+
try {
|
|
535
|
+
const stat = fs.statSync(filePath);
|
|
536
|
+
if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) {
|
|
537
|
+
entry.watcher?.close();
|
|
538
|
+
this.watched.delete(filePath);
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
entry.watcher?.close();
|
|
542
|
+
this.watched.delete(filePath);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
function extractSessionTitle(filePath, agent, sessionId) {
|
|
548
|
+
return agent === "claude" ? extractClaudeSessionTitle(filePath) : extractCodexSessionTitle(filePath, sessionId);
|
|
549
|
+
}
|
|
550
|
+
function extractSessionTitleFromLine(line, agent) {
|
|
551
|
+
try {
|
|
552
|
+
const obj = JSON.parse(line);
|
|
553
|
+
if (agent === "claude") {
|
|
554
|
+
if (obj.type !== "user") return void 0;
|
|
555
|
+
const content = obj.message?.content;
|
|
556
|
+
if (typeof content === "string") return pickSessionTitle(content);
|
|
557
|
+
if (Array.isArray(content)) {
|
|
558
|
+
for (const block of content) {
|
|
559
|
+
if (typeof block?.text === "string") {
|
|
560
|
+
const title = pickSessionTitle(block.text);
|
|
561
|
+
if (title) return title;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return void 0;
|
|
566
|
+
}
|
|
567
|
+
if (obj.type === "session_meta") {
|
|
568
|
+
const explicitTitle = pickSessionTitle(String(
|
|
569
|
+
obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
|
|
570
|
+
));
|
|
571
|
+
if (explicitTitle) return explicitTitle;
|
|
572
|
+
}
|
|
573
|
+
if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
|
|
574
|
+
return pickSessionTitle(String(obj.payload.message || ""));
|
|
575
|
+
}
|
|
576
|
+
if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user") {
|
|
577
|
+
const contentBlocks = obj.payload.content;
|
|
578
|
+
if (Array.isArray(contentBlocks)) {
|
|
579
|
+
for (const block of contentBlocks) {
|
|
580
|
+
const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
|
|
581
|
+
const title = pickSessionTitle(text);
|
|
582
|
+
if (title) return title;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
return void 0;
|
|
589
|
+
}
|
|
590
|
+
function extractClaudeSessionTitle(filePath) {
|
|
591
|
+
try {
|
|
592
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
593
|
+
for (const line of content.split("\n")) {
|
|
594
|
+
if (!line.trim()) continue;
|
|
595
|
+
try {
|
|
596
|
+
const obj = JSON.parse(line);
|
|
597
|
+
if (obj.type === "user") {
|
|
598
|
+
const msg = obj.message;
|
|
599
|
+
if (!msg) continue;
|
|
600
|
+
const content2 = msg.content;
|
|
601
|
+
if (typeof content2 === "string" && content2.trim()) {
|
|
602
|
+
const title = pickSessionTitle(content2);
|
|
603
|
+
if (title) return title;
|
|
604
|
+
}
|
|
605
|
+
if (Array.isArray(content2)) {
|
|
606
|
+
for (const block of content2) {
|
|
607
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
|
608
|
+
const title = pickSessionTitle(block.text);
|
|
609
|
+
if (title) return title;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
function extractCodexSessionTitle(filePath, sessionId) {
|
|
622
|
+
const codexSessionId = sessionId || deriveSessionId(filePath, "codex");
|
|
623
|
+
const explicitThreadTitle = getCodexThreadTitle(codexSessionId);
|
|
624
|
+
if (explicitThreadTitle) return explicitThreadTitle;
|
|
625
|
+
const logDerivedTitle = extractCodexSessionTitleFromLog(filePath);
|
|
626
|
+
return logDerivedTitle || void 0;
|
|
627
|
+
}
|
|
628
|
+
function getCodexThreadTitle(sessionId) {
|
|
629
|
+
const normalizedSessionId = String(sessionId || "").trim().toLowerCase();
|
|
630
|
+
if (!normalizedSessionId) return void 0;
|
|
631
|
+
return readCodexThreadTitles().titles.get(normalizedSessionId);
|
|
632
|
+
}
|
|
633
|
+
function extractCodexSessionTitleFromLog(filePath) {
|
|
634
|
+
try {
|
|
635
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
636
|
+
for (const line of content.split("\n")) {
|
|
637
|
+
if (!line.trim()) continue;
|
|
638
|
+
try {
|
|
639
|
+
const obj = JSON.parse(line);
|
|
640
|
+
if (obj.type !== "session_meta") continue;
|
|
641
|
+
const explicitTitle = pickSessionTitle(String(
|
|
642
|
+
obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
|
|
643
|
+
));
|
|
644
|
+
if (explicitTitle) return explicitTitle;
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
return void 0;
|
|
651
|
+
}
|
|
652
|
+
function extractCodexProjectName(filePath) {
|
|
653
|
+
try {
|
|
654
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
655
|
+
for (const line of content.split("\n")) {
|
|
656
|
+
if (!line.trim()) continue;
|
|
657
|
+
try {
|
|
658
|
+
const obj = JSON.parse(line);
|
|
659
|
+
if (obj.type !== "session_meta") continue;
|
|
660
|
+
const cwd = typeof obj.payload?.cwd === "string" ? obj.payload.cwd : "";
|
|
661
|
+
if (!cwd) continue;
|
|
662
|
+
const name = cwd.split("/").pop() || cwd.split("\\").pop();
|
|
663
|
+
if (name && name.trim()) return name.trim();
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
function isKibitzInternalCodexSession(filePath) {
|
|
672
|
+
try {
|
|
673
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
674
|
+
const lines = content.split("\n");
|
|
675
|
+
const maxProbeLines = 50;
|
|
676
|
+
for (let i = 0; i < lines.length && i < maxProbeLines; i++) {
|
|
677
|
+
const line = lines[i];
|
|
678
|
+
if (!line.trim()) continue;
|
|
679
|
+
try {
|
|
680
|
+
const obj = JSON.parse(line);
|
|
681
|
+
if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
|
|
682
|
+
const msg = String(obj.payload?.message || "").toLowerCase();
|
|
683
|
+
if (looksLikeKibitzGeneratedPrompt(msg)) return true;
|
|
684
|
+
}
|
|
685
|
+
if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
|
|
686
|
+
for (const block of obj.payload.content) {
|
|
687
|
+
const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
|
|
688
|
+
if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
function isKibitzInternalCodexLine(line) {
|
|
699
|
+
try {
|
|
700
|
+
const obj = JSON.parse(line);
|
|
701
|
+
if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
|
|
702
|
+
const msg = String(obj.payload?.message || "").toLowerCase();
|
|
703
|
+
return looksLikeKibitzGeneratedPrompt(msg);
|
|
704
|
+
}
|
|
705
|
+
if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
|
|
706
|
+
for (const block of obj.payload.content) {
|
|
707
|
+
const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
|
|
708
|
+
if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
function looksLikeKibitzGeneratedPrompt(text) {
|
|
716
|
+
if (!text) return false;
|
|
717
|
+
return text.includes("you oversee ai coding agents. summarize what they did") && text.includes("format for this message:") && text.includes("tone preset:");
|
|
718
|
+
}
|
|
719
|
+
var codexThreadTitlesCache = {
|
|
720
|
+
mtimeMs: -1,
|
|
721
|
+
titles: /* @__PURE__ */ new Map(),
|
|
722
|
+
order: []
|
|
723
|
+
};
|
|
724
|
+
function readCodexThreadTitles() {
|
|
725
|
+
const statePath = path.join(os.homedir(), ".codex", ".codex-global-state.json");
|
|
726
|
+
try {
|
|
727
|
+
const stat = fs.statSync(statePath);
|
|
728
|
+
if (stat.mtimeMs === codexThreadTitlesCache.mtimeMs) return codexThreadTitlesCache;
|
|
729
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
730
|
+
const rawTitles = raw?.["thread-titles"]?.titles;
|
|
731
|
+
const rawOrder = raw?.["thread-titles"]?.order;
|
|
732
|
+
const titles = /* @__PURE__ */ new Map();
|
|
733
|
+
if (rawTitles && typeof rawTitles === "object" && !Array.isArray(rawTitles)) {
|
|
734
|
+
for (const [key, value] of Object.entries(rawTitles)) {
|
|
735
|
+
const sessionId = String(key || "").trim().toLowerCase();
|
|
736
|
+
if (!sessionId) continue;
|
|
737
|
+
const title = pickSessionTitle(String(value || ""));
|
|
738
|
+
if (title) titles.set(sessionId, title);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const order = Array.isArray(rawOrder) ? rawOrder.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean) : [];
|
|
742
|
+
codexThreadTitlesCache.mtimeMs = stat.mtimeMs;
|
|
743
|
+
codexThreadTitlesCache.titles = titles;
|
|
744
|
+
codexThreadTitlesCache.order = order;
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
return codexThreadTitlesCache;
|
|
748
|
+
}
|
|
749
|
+
function deriveSessionId(filePath, agent) {
|
|
750
|
+
const basename3 = path.basename(filePath, ".jsonl");
|
|
751
|
+
if (agent !== "codex") return basename3;
|
|
752
|
+
const match = basename3.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/i);
|
|
753
|
+
return match ? match[1].toLowerCase() : basename3.toLowerCase();
|
|
754
|
+
}
|
|
755
|
+
function normalizeSessionId(rawSessionId, agent) {
|
|
756
|
+
const value = String(rawSessionId || "").trim();
|
|
757
|
+
if (!value) return "";
|
|
758
|
+
if (agent === "codex") return value.toLowerCase();
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
761
|
+
function extractSessionIdFromLog(filePath, agent, fallback) {
|
|
762
|
+
const normalizedFallback = normalizeSessionId(fallback, agent);
|
|
763
|
+
if (agent !== "claude") return normalizedFallback;
|
|
764
|
+
try {
|
|
765
|
+
const stat = fs.statSync(filePath);
|
|
766
|
+
const length = Math.min(stat.size, 512 * 1024);
|
|
767
|
+
if (length <= 0) return normalizedFallback;
|
|
768
|
+
const fd = fs.openSync(filePath, "r");
|
|
769
|
+
try {
|
|
770
|
+
const buf = Buffer.alloc(length);
|
|
771
|
+
const offset = Math.max(0, stat.size - length);
|
|
772
|
+
fs.readSync(fd, buf, 0, length, offset);
|
|
773
|
+
const lines = buf.toString("utf8").split("\n").filter((line) => line.trim());
|
|
774
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
775
|
+
try {
|
|
776
|
+
const obj = JSON.parse(lines[i]);
|
|
777
|
+
const fromLine = normalizeSessionId(obj?.sessionId, agent);
|
|
778
|
+
if (fromLine) return fromLine;
|
|
779
|
+
} catch {
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
} finally {
|
|
783
|
+
fs.closeSync(fd);
|
|
784
|
+
}
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
return normalizedFallback;
|
|
788
|
+
}
|
|
789
|
+
function pickSessionTitle(raw) {
|
|
790
|
+
if (!raw.trim()) return void 0;
|
|
791
|
+
if (looksLikeKibitzGeneratedPrompt(raw.toLowerCase())) return void 0;
|
|
792
|
+
for (const line of raw.split("\n")) {
|
|
793
|
+
const trimmed = line.trim();
|
|
794
|
+
if (!trimmed) continue;
|
|
795
|
+
const cleaned = trimmed.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
796
|
+
if (!cleaned || isNoiseSessionTitle(cleaned)) continue;
|
|
797
|
+
return cleaned;
|
|
798
|
+
}
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
function isNoiseSessionTitle(text) {
|
|
802
|
+
const lower = text.toLowerCase();
|
|
803
|
+
const normalized = lower.replace(/^[#>*\-\s]+/, "").trim();
|
|
804
|
+
if (normalized.length < 8) return true;
|
|
805
|
+
if (looksLikeSessionIdentifier(normalized)) return true;
|
|
806
|
+
if (/^[a-z0-9+/_=-]{64,}$/.test(normalized)) return true;
|
|
807
|
+
if (normalized.startsWith("the user opened the file ") || normalized.includes("may or may not be related to the current task")) return true;
|
|
808
|
+
if (/^[\w][\w.\-]*\.[a-z]{2,6}:\s/i.test(normalized)) return true;
|
|
809
|
+
if (/^\d+\)\s+after deciding to use a skill\b/.test(normalized) || /^\d+\)\s+when `?skill\.md`? references\b/.test(normalized) || /^\d+\)\s+if `?skill\.md`? points\b/.test(normalized) || /^\d+\)\s+if `?scripts\/`? exist\b/.test(normalized) || /^\d+\)\s+if `?assets\/`? or templates exist\b/.test(normalized) || /^perform( a)? repository commit\b/.test(normalized)) return true;
|
|
810
|
+
if (normalized.startsWith("agents.md instructions for") || normalized.startsWith("a skill is a set of local instructions") || normalized.startsWith("researched how the feature works") || normalized.startsWith("context from my ide setup:") || normalized.startsWith("open tabs:") || normalized.startsWith("my request for codex:") || normalized.startsWith("available skills") || normalized.startsWith("how to use skills") || normalized.startsWith("never mention session ids") || normalized.startsWith("never write in first person") || normalized.startsWith("you oversee ai coding agents") || normalized.startsWith("plain language.") || normalized.startsWith("bold only key nouns") || normalized.startsWith("upper case for emotional") || normalized.startsWith("emoji are allowed") || normalized.startsWith("no filler.") || normalized.startsWith("don't repeat what previous commentary") || normalized.startsWith("rules:") || normalized.startsWith("format for this message:") || normalized.startsWith("tone preset:") || normalized.startsWith("additional user instruction:") || normalized.startsWith("skill-creator:") || normalized.startsWith("skill-installer:") || normalized.startsWith("- skill-") || normalized.startsWith("discovery:") || normalized.startsWith("trigger rules:") || normalized.startsWith("missing/blocked:") || normalized.startsWith("how to use a skill") || normalized.startsWith("context hygiene:") || normalized.startsWith("safety and fallback:") || normalized.startsWith("environment_context") || normalized.startsWith("/environment_context") || normalized.startsWith("permissions instructions") || normalized.startsWith("filesystem sandboxing defines") || normalized.startsWith("collaboration mode:") || normalized.startsWith("system instructions:") || normalized.startsWith("user request:") || normalized.startsWith("active goals") || normalized.startsWith("room memory") || normalized.startsWith("recent activity") || normalized.startsWith("room workers") || normalized.startsWith("room tasks") || normalized.startsWith("recent decisions") || normalized.startsWith("pending questions to keeper") || normalized.startsWith("execution settings") || normalized.startsWith("instructions") || normalized.startsWith("based on the current state") || normalized.startsWith("important: you must call at least one tool") || normalized.startsWith("respond only with a tool call") || normalized.startsWith("use bullet points.") || normalized.startsWith("write a single sentence.") || normalized.startsWith("start with a short upper case reaction") || normalized.startsWith("use numbered steps showing what the agent did in order") || normalized.startsWith("summarize in one sentence, then ask a pointed rhetorical question") || normalized.startsWith("use a compact markdown table with columns") || normalized.startsWith("use bullet points with emoji tags") || normalized.startsWith("use a scoreboard format with these labels") || normalized.startsWith("example:") || normalized.startsWith("session: ") || normalized.startsWith("actions (") || normalized.startsWith("previous commentary") || normalized.startsWith("summarize only this session") || normalized.startsWith("never mention ids/logs/prompts/traces") || normalized.startsWith(`never say "i", "i'll", "we"`)) return true;
|
|
811
|
+
if (normalized.includes("only key nouns") || normalized.includes("fixed the login page") || normalized.includes("edited auth middleware") || normalized.includes("max 2 per commentary") || normalized.includes("don't repeat what previous commentary already said")) return true;
|
|
812
|
+
if (normalized.startsWith("cwd:") || normalized.startsWith("shell:")) return true;
|
|
813
|
+
if (normalized.startsWith("your identity") || normalized.startsWith("room id:") || normalized.startsWith("worker id:")) return true;
|
|
814
|
+
if (normalized.startsWith("you are codex, a coding agent")) return true;
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
function looksLikeSessionIdentifier(text) {
|
|
818
|
+
if (/^[0-9a-f]{8}$/.test(text)) return true;
|
|
819
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(text)) return true;
|
|
820
|
+
if (/^session[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
|
|
821
|
+
if (/^turn[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
|
|
822
|
+
if (/^rollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?$/.test(text)) return true;
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
function extractProjectName(dirName) {
|
|
826
|
+
const parts = dirName.split("-").filter(Boolean);
|
|
827
|
+
return parts[parts.length - 1] || "unknown";
|
|
828
|
+
}
|
|
829
|
+
function codexSessionDirs(daysBack) {
|
|
830
|
+
const home = os.homedir();
|
|
831
|
+
const results = [];
|
|
832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
833
|
+
const now = /* @__PURE__ */ new Date();
|
|
834
|
+
const totalDays = Math.max(1, daysBack);
|
|
835
|
+
for (let offset = 0; offset < totalDays; offset++) {
|
|
836
|
+
const d = new Date(now);
|
|
837
|
+
d.setDate(now.getDate() - offset);
|
|
838
|
+
const dir = path.join(
|
|
839
|
+
home,
|
|
840
|
+
".codex",
|
|
841
|
+
"sessions",
|
|
842
|
+
String(d.getFullYear()),
|
|
843
|
+
String(d.getMonth() + 1).padStart(2, "0"),
|
|
844
|
+
String(d.getDate()).padStart(2, "0")
|
|
845
|
+
);
|
|
846
|
+
if (seen.has(dir)) continue;
|
|
847
|
+
seen.add(dir);
|
|
848
|
+
results.push(dir);
|
|
849
|
+
}
|
|
850
|
+
return results;
|
|
851
|
+
}
|
|
852
|
+
function fallbackSessionTitle(projectName, agent) {
|
|
853
|
+
const project = (projectName || "").trim();
|
|
854
|
+
if (project) return project;
|
|
855
|
+
return `${agent} session`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/core/commentary.ts
|
|
859
|
+
var import_events2 = require("events");
|
|
860
|
+
|
|
861
|
+
// src/core/types.ts
|
|
862
|
+
var COMMENTARY_STYLE_OPTIONS = [
|
|
863
|
+
{ id: "bullets", label: "Bullet list" },
|
|
864
|
+
{ id: "one-liner", label: "One-liner" },
|
|
865
|
+
{ id: "headline-context", label: "Headline + context" },
|
|
866
|
+
{ id: "numbered-progress", label: "Numbered progress" },
|
|
867
|
+
{ id: "short-question", label: "Short + question" },
|
|
868
|
+
{ id: "table", label: "Table" },
|
|
869
|
+
{ id: "emoji-bullets", label: "Emoji bullets" },
|
|
870
|
+
{ id: "scoreboard", label: "Scoreboard" }
|
|
871
|
+
];
|
|
872
|
+
var MODELS = [
|
|
873
|
+
{ id: "claude-opus-4-6", label: "Claude Opus", provider: "anthropic" },
|
|
874
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet", provider: "anthropic" },
|
|
875
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku", provider: "anthropic" },
|
|
876
|
+
{ id: "gpt-4o", label: "GPT-4o", provider: "openai" },
|
|
877
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini", provider: "openai" }
|
|
878
|
+
];
|
|
879
|
+
var DEFAULT_MODEL = "claude-opus-4-6";
|
|
880
|
+
|
|
881
|
+
// src/core/providers/anthropic.ts
|
|
882
|
+
var import_child_process = require("child_process");
|
|
883
|
+
var import_fs = require("fs");
|
|
884
|
+
var import_path = require("path");
|
|
885
|
+
var import_os = require("os");
|
|
886
|
+
var cachedClaudePath = null;
|
|
887
|
+
var PROVIDER_TIMEOUT_MS = 9e4;
|
|
888
|
+
function resolveNodeScript(cmdPath) {
|
|
889
|
+
if (process.platform !== "win32" || !cmdPath.endsWith(".cmd")) return null;
|
|
890
|
+
try {
|
|
891
|
+
const content = (0, import_fs.readFileSync)(cmdPath, "utf-8");
|
|
892
|
+
const match = content.match(/%dp0%\\(.+?\.js)/);
|
|
893
|
+
if (match) {
|
|
894
|
+
const script = (0, import_path.join)((0, import_path.dirname)(cmdPath), match[1]);
|
|
895
|
+
if ((0, import_fs.existsSync)(script)) return script;
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
function resolveClaudePath() {
|
|
902
|
+
if (cachedClaudePath) return cachedClaudePath;
|
|
903
|
+
const home = (0, import_os.homedir)();
|
|
904
|
+
const isWindows = process.platform === "win32";
|
|
905
|
+
const candidates = isWindows ? [
|
|
906
|
+
(0, import_path.join)(home, ".claude", "bin", "claude.exe"),
|
|
907
|
+
(0, import_path.join)(home, "AppData", "Local", "Programs", "claude-code", "claude.exe"),
|
|
908
|
+
(0, import_path.join)(home, "AppData", "Local", "Claude", "claude.exe"),
|
|
909
|
+
(0, import_path.join)(home, "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
910
|
+
"C:\\Program Files\\Claude\\claude.exe",
|
|
911
|
+
(0, import_path.join)(home, "AppData", "Roaming", "npm", "claude.cmd")
|
|
912
|
+
] : [
|
|
913
|
+
(0, import_path.join)(home, ".local", "bin", "claude"),
|
|
914
|
+
(0, import_path.join)(home, ".claude", "bin", "claude"),
|
|
915
|
+
"/usr/local/bin/claude",
|
|
916
|
+
"/usr/bin/claude",
|
|
917
|
+
"/snap/bin/claude",
|
|
918
|
+
"/opt/homebrew/bin/claude"
|
|
919
|
+
];
|
|
920
|
+
for (const p of candidates) {
|
|
921
|
+
if ((0, import_fs.existsSync)(p)) {
|
|
922
|
+
cachedClaudePath = p;
|
|
923
|
+
return p;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const env = { ...process.env };
|
|
927
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
928
|
+
delete env.CLAUDECODE;
|
|
929
|
+
if (isWindows) {
|
|
930
|
+
try {
|
|
931
|
+
const resolved = (0, import_child_process.execSync)("where claude", {
|
|
932
|
+
encoding: "utf-8",
|
|
933
|
+
env,
|
|
934
|
+
timeout: 5e3
|
|
935
|
+
}).trim().split("\n")[0].trim();
|
|
936
|
+
if (resolved && (0, import_fs.existsSync)(resolved)) {
|
|
937
|
+
cachedClaudePath = resolved;
|
|
938
|
+
return resolved;
|
|
939
|
+
}
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
const shells = ["/bin/zsh", "/bin/bash"];
|
|
944
|
+
for (const sh of shells) {
|
|
945
|
+
if (!(0, import_fs.existsSync)(sh)) continue;
|
|
946
|
+
try {
|
|
947
|
+
const resolved = (0, import_child_process.execSync)(`${sh} -lic 'which claude'`, {
|
|
948
|
+
encoding: "utf-8",
|
|
949
|
+
env,
|
|
950
|
+
timeout: 5e3
|
|
951
|
+
}).trim();
|
|
952
|
+
if (resolved && (0, import_fs.existsSync)(resolved)) {
|
|
953
|
+
cachedClaudePath = resolved;
|
|
954
|
+
return resolved;
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
var AnthropicProvider = class {
|
|
963
|
+
async generate(systemPrompt, userPrompt, _apiKey, model, onChunk) {
|
|
964
|
+
return new Promise((resolve, reject) => {
|
|
965
|
+
const claudePath = resolveClaudePath();
|
|
966
|
+
if (!claudePath) {
|
|
967
|
+
reject(new Error("Claude CLI not found. Install from https://docs.anthropic.com/en/docs/claude-code"));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const fullPrompt = `${systemPrompt}
|
|
971
|
+
|
|
972
|
+
${userPrompt}`;
|
|
973
|
+
const args = [
|
|
974
|
+
"-p",
|
|
975
|
+
fullPrompt,
|
|
976
|
+
"--output-format",
|
|
977
|
+
"stream-json",
|
|
978
|
+
"--verbose",
|
|
979
|
+
"--max-turns",
|
|
980
|
+
"1",
|
|
981
|
+
"--model",
|
|
982
|
+
model
|
|
983
|
+
];
|
|
984
|
+
const env = { ...process.env };
|
|
985
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
986
|
+
delete env.CLAUDECODE;
|
|
987
|
+
let proc;
|
|
988
|
+
try {
|
|
989
|
+
const nodeScript = resolveNodeScript(claudePath);
|
|
990
|
+
if (nodeScript) {
|
|
991
|
+
proc = (0, import_child_process.spawn)(process.execPath, [nodeScript, ...args], {
|
|
992
|
+
cwd: (0, import_os.homedir)(),
|
|
993
|
+
env,
|
|
994
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
995
|
+
windowsHide: true
|
|
996
|
+
});
|
|
997
|
+
} else {
|
|
998
|
+
proc = (0, import_child_process.spawn)(claudePath, args, {
|
|
999
|
+
cwd: (0, import_os.homedir)(),
|
|
1000
|
+
env,
|
|
1001
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1002
|
+
windowsHide: true,
|
|
1003
|
+
shell: process.platform === "win32"
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
reject(new Error(`Failed to spawn claude CLI: ${err instanceof Error ? err.message : String(err)}`));
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (!proc.stdout || !proc.stderr) {
|
|
1011
|
+
reject(new Error("Failed to create stdio pipes for Claude CLI"));
|
|
1012
|
+
try {
|
|
1013
|
+
proc.kill();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
let full = "";
|
|
1019
|
+
let buffer = "";
|
|
1020
|
+
let stderr = "";
|
|
1021
|
+
let timedOut = false;
|
|
1022
|
+
proc.stdout.on("data", (data) => {
|
|
1023
|
+
buffer += data.toString();
|
|
1024
|
+
const lines = buffer.split("\n");
|
|
1025
|
+
buffer = lines.pop() ?? "";
|
|
1026
|
+
for (const line of lines) {
|
|
1027
|
+
if (!line.trim()) continue;
|
|
1028
|
+
try {
|
|
1029
|
+
const event = JSON.parse(line);
|
|
1030
|
+
if (event.type === "assistant") {
|
|
1031
|
+
const content = event.message?.content;
|
|
1032
|
+
if (Array.isArray(content)) {
|
|
1033
|
+
for (const block of content) {
|
|
1034
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1035
|
+
full += block.text;
|
|
1036
|
+
onChunk(block.text);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (event.type === "result" && event.result) {
|
|
1042
|
+
if (!full) {
|
|
1043
|
+
full = String(event.result);
|
|
1044
|
+
onChunk(full);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
proc.stderr.on("data", (data) => {
|
|
1052
|
+
stderr += data.toString();
|
|
1053
|
+
});
|
|
1054
|
+
const timeout = setTimeout(() => {
|
|
1055
|
+
timedOut = true;
|
|
1056
|
+
try {
|
|
1057
|
+
proc.kill();
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}, PROVIDER_TIMEOUT_MS);
|
|
1061
|
+
proc.on("close", (code) => {
|
|
1062
|
+
clearTimeout(timeout);
|
|
1063
|
+
if (timedOut && !full) {
|
|
1064
|
+
reject(new Error(`Claude CLI timed out after ${PROVIDER_TIMEOUT_MS}ms`));
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (code !== 0 && !full) {
|
|
1068
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
1069
|
+
} else {
|
|
1070
|
+
resolve(full);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
proc.on("error", (err) => {
|
|
1074
|
+
clearTimeout(timeout);
|
|
1075
|
+
reject(new Error(`Claude CLI error: ${err.message}`));
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/core/providers/openai.ts
|
|
1082
|
+
var import_child_process2 = require("child_process");
|
|
1083
|
+
var import_fs2 = require("fs");
|
|
1084
|
+
var import_path2 = require("path");
|
|
1085
|
+
var import_os2 = require("os");
|
|
1086
|
+
var cachedCodexScript = null;
|
|
1087
|
+
var codexScriptResolved = false;
|
|
1088
|
+
var PROVIDER_TIMEOUT_MS2 = 9e4;
|
|
1089
|
+
function resolveCodexNodeScript() {
|
|
1090
|
+
if (codexScriptResolved) return cachedCodexScript;
|
|
1091
|
+
codexScriptResolved = true;
|
|
1092
|
+
if (process.platform !== "win32") return null;
|
|
1093
|
+
try {
|
|
1094
|
+
const cmdPath = (0, import_child_process2.execSync)("where codex.cmd", {
|
|
1095
|
+
encoding: "utf-8",
|
|
1096
|
+
timeout: 5e3,
|
|
1097
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1098
|
+
}).trim().split("\n")[0].trim();
|
|
1099
|
+
const content = (0, import_fs2.readFileSync)(cmdPath, "utf-8");
|
|
1100
|
+
const match = content.match(/%dp0%\\(.+?\.js)/);
|
|
1101
|
+
if (!match) return null;
|
|
1102
|
+
const script = (0, import_path2.join)((0, import_path2.dirname)(cmdPath), match[1]);
|
|
1103
|
+
if ((0, import_fs2.existsSync)(script)) {
|
|
1104
|
+
cachedCodexScript = script;
|
|
1105
|
+
return script;
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
var OpenAIProvider = class {
|
|
1112
|
+
async generate(systemPrompt, userPrompt, _apiKey, _model, onChunk) {
|
|
1113
|
+
return new Promise((resolve, reject) => {
|
|
1114
|
+
const fullPrompt = `${systemPrompt}
|
|
1115
|
+
|
|
1116
|
+
${userPrompt}`;
|
|
1117
|
+
const args = ["exec", "--json", "--skip-git-repo-check", fullPrompt];
|
|
1118
|
+
let proc;
|
|
1119
|
+
try {
|
|
1120
|
+
const nodeScript = resolveCodexNodeScript();
|
|
1121
|
+
if (nodeScript) {
|
|
1122
|
+
proc = (0, import_child_process2.spawn)(process.execPath, [nodeScript, ...args], {
|
|
1123
|
+
cwd: (0, import_os2.homedir)(),
|
|
1124
|
+
env: process.env,
|
|
1125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1126
|
+
windowsHide: true
|
|
1127
|
+
});
|
|
1128
|
+
} else {
|
|
1129
|
+
const codexCmd = process.platform === "win32" ? "codex.cmd" : "codex";
|
|
1130
|
+
proc = (0, import_child_process2.spawn)(codexCmd, args, {
|
|
1131
|
+
cwd: (0, import_os2.homedir)(),
|
|
1132
|
+
env: process.env,
|
|
1133
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1134
|
+
windowsHide: true,
|
|
1135
|
+
shell: process.platform === "win32"
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
reject(new Error(`Failed to spawn codex CLI: ${err instanceof Error ? err.message : String(err)}`));
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (!proc.stdout || !proc.stderr) {
|
|
1143
|
+
reject(new Error("Failed to create stdio pipes for codex CLI"));
|
|
1144
|
+
try {
|
|
1145
|
+
proc.kill();
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
let full = "";
|
|
1151
|
+
let buffer = "";
|
|
1152
|
+
let stderr = "";
|
|
1153
|
+
let timedOut = false;
|
|
1154
|
+
proc.stdout.on("data", (data) => {
|
|
1155
|
+
buffer += data.toString();
|
|
1156
|
+
const lines = buffer.split("\n");
|
|
1157
|
+
buffer = lines.pop() ?? "";
|
|
1158
|
+
for (const line of lines) {
|
|
1159
|
+
if (!line.trim()) continue;
|
|
1160
|
+
try {
|
|
1161
|
+
const event = JSON.parse(line);
|
|
1162
|
+
if (event.type === "item.completed" && event.item) {
|
|
1163
|
+
if (event.item.type === "agent_message" && typeof event.item.text === "string") {
|
|
1164
|
+
full += event.item.text;
|
|
1165
|
+
onChunk(event.item.text);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
proc.stderr.on("data", (data) => {
|
|
1173
|
+
stderr += data.toString();
|
|
1174
|
+
});
|
|
1175
|
+
const timeout = setTimeout(() => {
|
|
1176
|
+
timedOut = true;
|
|
1177
|
+
try {
|
|
1178
|
+
proc.kill();
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
}, PROVIDER_TIMEOUT_MS2);
|
|
1182
|
+
proc.on("close", (code) => {
|
|
1183
|
+
clearTimeout(timeout);
|
|
1184
|
+
if (timedOut && !full) {
|
|
1185
|
+
reject(new Error(`Codex CLI timed out after ${PROVIDER_TIMEOUT_MS2}ms`));
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (code !== 0 && !full) {
|
|
1189
|
+
reject(new Error(`Codex CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
1190
|
+
} else {
|
|
1191
|
+
resolve(full);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
proc.on("error", (err) => {
|
|
1195
|
+
clearTimeout(timeout);
|
|
1196
|
+
reject(new Error(`Codex CLI error: ${err.message}`));
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// src/core/commentary.ts
|
|
1203
|
+
var SYSTEM_PROMPT = `You oversee AI coding agents. Summarize what they did in plain language anyone can understand.
|
|
1204
|
+
|
|
1205
|
+
Rules:
|
|
1206
|
+
- Plain language. "Fixed the login page" not "Edited auth middleware".
|
|
1207
|
+
- **Bold** only key nouns, 1-2 words: **tests**, **deploy**, **production**. NEVER bold sentences.
|
|
1208
|
+
- UPPER CASE short reactions when it helps: GREAT FIND, NICE MOVE, RISK ALERT, SOLID CHECK, WATCH OUT.
|
|
1209
|
+
- Vary judgment wording across messages; avoid repeating one catchphrase.
|
|
1210
|
+
- Always judge execution direction using the session goal + concrete actions in this batch.
|
|
1211
|
+
- Close with a sentence that captures the current direction and momentum of the work.
|
|
1212
|
+
- Emoji are allowed when they add meaning (max 2 per commentary).
|
|
1213
|
+
- No filler. No "methodical", "surgical", "disciplined", "clean work". Facts and reactions only.
|
|
1214
|
+
- Don't repeat what previous commentary already said.
|
|
1215
|
+
- Never mention session IDs, logs, traces, prompts, JSONL files, or internal tooling.
|
|
1216
|
+
- Never write in first person ("I", "I'll", "we") or future tense plans.
|
|
1217
|
+
- Never use the word "verdict". Write narrative, not a ruling.`;
|
|
1218
|
+
var FORMAT_TEMPLATES = {
|
|
1219
|
+
bullets: `Use bullet points. Each bullet = one thing done or one observation.
|
|
1220
|
+
Close with a sentence on direction and momentum.
|
|
1221
|
+
Example:
|
|
1222
|
+
- Researched how the feature works
|
|
1223
|
+
- Rewrote the page with new layout
|
|
1224
|
+
- Shipped without **tests** \u2014 RISKY
|
|
1225
|
+
- Checked for errors before pushing`,
|
|
1226
|
+
"one-liner": `Write a single sentence. Punchy, complete, under 20 words.
|
|
1227
|
+
One closing sentence on where this is headed.
|
|
1228
|
+
Example: Agent investigated the bug, found the root cause, and fixed it \u2014 NICE WORK.
|
|
1229
|
+
Example: Still reading code after 30 actions \u2014 hasn't changed anything yet.`,
|
|
1230
|
+
"headline-context": `Start with a short UPPER CASE reaction (2-4 words), then one sentence of context.
|
|
1231
|
+
Close with a read on momentum.
|
|
1232
|
+
Example:
|
|
1233
|
+
SOLID APPROACH. Agent read the dependencies first, then made targeted changes across three files.
|
|
1234
|
+
Example:
|
|
1235
|
+
NOT GREAT. Pushed to **production** without running any tests \u2014 hope nothing breaks.`,
|
|
1236
|
+
"numbered-progress": `Use numbered steps showing what the agent did in order. Finish with a momentum line.
|
|
1237
|
+
Example:
|
|
1238
|
+
1. Read the existing code
|
|
1239
|
+
2. Made changes to the login flow
|
|
1240
|
+
3. Tested locally
|
|
1241
|
+
4. Pushed to **production**
|
|
1242
|
+
Proper process, nothing to flag.`,
|
|
1243
|
+
"short-question": `Summarize in one sentence, then ask a pointed rhetorical question.
|
|
1244
|
+
End with a one-sentence read on direction.
|
|
1245
|
+
Example:
|
|
1246
|
+
Agent rewrote the entire settings page and shipped it immediately. Did they test this at all?
|
|
1247
|
+
Example:
|
|
1248
|
+
Third time reading the same code. Lost, or just being thorough?`,
|
|
1249
|
+
table: `Use a compact markdown table with columns: Action | Why it mattered.
|
|
1250
|
+
Include 3-5 rows max, then one sentence on momentum.
|
|
1251
|
+
Example:
|
|
1252
|
+
| Action | Why it mattered |
|
|
1253
|
+
| --- | --- |
|
|
1254
|
+
| Ran tests | Verified changes before shipping |
|
|
1255
|
+
| Skipped lint | Could hide style regressions |
|
|
1256
|
+
Mostly careful, one loose end.`,
|
|
1257
|
+
"emoji-bullets": `Use bullet points with emoji tags to signal risk/quality.
|
|
1258
|
+
Use only these tags: \u2705, \u26A0\uFE0F, \u{1F525}, \u2744\uFE0F.
|
|
1259
|
+
Close with a direction sentence.
|
|
1260
|
+
Example:
|
|
1261
|
+
- \u2705 Fixed the failing migration script
|
|
1262
|
+
- \u26A0\uFE0F Pushed without rerunning integration tests
|
|
1263
|
+
- \u{1F525} Found the root cause in auth token parsing`,
|
|
1264
|
+
scoreboard: `Use a scoreboard format with these labels:
|
|
1265
|
+
Wins:
|
|
1266
|
+
Risks:
|
|
1267
|
+
Next:
|
|
1268
|
+
Each label should have 1-3 short bullets, then one line on where things stand.`
|
|
1269
|
+
};
|
|
1270
|
+
var DEFAULT_FORMAT_STYLE_IDS = COMMENTARY_STYLE_OPTIONS.map((option) => option.id);
|
|
1271
|
+
var PRESET_INSTRUCTIONS = {
|
|
1272
|
+
auto: "",
|
|
1273
|
+
"critical-coder": "Be a VERY CRITICAL coder using code terminology. Call out architectural or process flaws directly and sharply.",
|
|
1274
|
+
"precise-short": "Be precise and short. Keep the output compact and information-dense with minimal words.",
|
|
1275
|
+
emotional: "Be emotional and expressive. React strongly to good or risky behavior while staying factual.",
|
|
1276
|
+
newbie: "Explain for non-developers. Avoid jargon or explain it in plain words with simple cause/effect."
|
|
1277
|
+
};
|
|
1278
|
+
function normalizeFormatStyles(styleIds) {
|
|
1279
|
+
const requested = new Set(styleIds.map((styleId) => String(styleId || "").trim()));
|
|
1280
|
+
const normalized = COMMENTARY_STYLE_OPTIONS.map((option) => option.id).filter((styleId) => requested.has(styleId));
|
|
1281
|
+
return normalized.length > 0 ? normalized : DEFAULT_FORMAT_STYLE_IDS.slice();
|
|
1282
|
+
}
|
|
1283
|
+
function sameFormatStyles(a, b) {
|
|
1284
|
+
if (a.length !== b.length) return false;
|
|
1285
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
1286
|
+
if (a[i] !== b[i]) return false;
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
var SESSION_ID_PATTERNS = [
|
|
1291
|
+
/\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi,
|
|
1292
|
+
/\brollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?\b/gi,
|
|
1293
|
+
/\bsession[\s:_-]*[0-9a-f]{8,}\b/gi,
|
|
1294
|
+
/\bturn[\s:_-]*[0-9a-f]{8,}\b/gi
|
|
1295
|
+
];
|
|
1296
|
+
var MIN_BATCH_SIZE = 1;
|
|
1297
|
+
var MAX_BATCH_SIZE = 40;
|
|
1298
|
+
var IDLE_TIMEOUT_MS = 5e3;
|
|
1299
|
+
var MAX_WAIT_MS = 15e3;
|
|
1300
|
+
var TEST_COMMAND_PATTERNS = [
|
|
1301
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|test:[\w:-]+)\b/i,
|
|
1302
|
+
/\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test|mvn\s+test|gradle(?:w)?\s+test|rspec|phpunit)\b/i
|
|
1303
|
+
];
|
|
1304
|
+
var DEPLOY_COMMAND_PATTERNS = [
|
|
1305
|
+
/\b(?:deploy|release|publish)\b/i,
|
|
1306
|
+
/\b(?:kubectl\s+apply|helm\s+upgrade|terraform\s+apply|serverless\s+deploy|vercel\s+--prod|netlify\s+deploy|fly\s+deploy)\b/i
|
|
1307
|
+
];
|
|
1308
|
+
var ERROR_SIGNAL_PATTERN = /\b(?:error|failed|failure|exception|traceback|panic|denied|timeout|cannot|can't|unable)\b/i;
|
|
1309
|
+
var SECURITY_COMMAND_RULES = [
|
|
1310
|
+
{
|
|
1311
|
+
severity: "high",
|
|
1312
|
+
signal: "Remote script piped directly into a shell",
|
|
1313
|
+
pattern: /\b(?:curl|wget)\b[^\n|]{0,300}\|\s*(?:bash|sh|zsh|pwsh|powershell)\b/i
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
severity: "high",
|
|
1317
|
+
signal: "TLS verification explicitly disabled",
|
|
1318
|
+
pattern: /\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0\b/i
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
severity: "high",
|
|
1322
|
+
signal: "Destructive root-level delete command",
|
|
1323
|
+
pattern: /\brm\s+-rf\s+\/(?:\s|$)/i
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
severity: "medium",
|
|
1327
|
+
signal: "Insecure transport flag used in command",
|
|
1328
|
+
pattern: /\bcurl\b[^\n]*\s(?:--insecure|-k)\b/i
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
severity: "medium",
|
|
1332
|
+
signal: "Git hooks bypassed with --no-verify",
|
|
1333
|
+
pattern: /\bgit\b[^\n]*\s--no-verify\b/i
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
severity: "medium",
|
|
1337
|
+
signal: "SSH host key verification disabled",
|
|
1338
|
+
pattern: /\bStrictHostKeyChecking\s*=\s*no\b/i
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
severity: "medium",
|
|
1342
|
+
signal: "Overly broad file permissions (chmod 777)",
|
|
1343
|
+
pattern: /\bchmod\s+777\b/i
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
severity: "medium",
|
|
1347
|
+
signal: "Potential secret exposed in command arguments",
|
|
1348
|
+
pattern: /\b(?:api[_-]?key|token|password)\s*=\s*\S+/i
|
|
1349
|
+
}
|
|
1350
|
+
];
|
|
1351
|
+
var SECURITY_PATH_RULES = [
|
|
1352
|
+
{
|
|
1353
|
+
severity: "medium",
|
|
1354
|
+
signal: "Secret-related file touched",
|
|
1355
|
+
pattern: /(?:^|[\\/])(?:\.env(?:\.[^\\/]+)?|id_rsa|id_dsa|authorized_keys|known_hosts|credentials?|secrets?)(?:$|[\\/])/i
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
severity: "medium",
|
|
1359
|
+
signal: "Sensitive key/cert file touched",
|
|
1360
|
+
pattern: /\.(?:pem|key|p12|pfx)$/i
|
|
1361
|
+
}
|
|
1362
|
+
];
|
|
1363
|
+
var JUDGMENT_PHRASES = [
|
|
1364
|
+
"NOT IDEAL",
|
|
1365
|
+
"NOT GREAT",
|
|
1366
|
+
"WATCH OUT",
|
|
1367
|
+
"RISK ALERT",
|
|
1368
|
+
"GREAT FIND",
|
|
1369
|
+
"NICE MOVE",
|
|
1370
|
+
"SOLID CHECK"
|
|
1371
|
+
];
|
|
1372
|
+
function getDetailValue(details, ...keys) {
|
|
1373
|
+
for (const key of keys) {
|
|
1374
|
+
const value = details[key];
|
|
1375
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1376
|
+
}
|
|
1377
|
+
return "";
|
|
1378
|
+
}
|
|
1379
|
+
function extractCommand(event) {
|
|
1380
|
+
if (event.type !== "tool_call") return "";
|
|
1381
|
+
const details = event.details || {};
|
|
1382
|
+
const input = typeof details.input === "object" && details.input ? details.input : {};
|
|
1383
|
+
const direct = getDetailValue(details, "command", "cmd");
|
|
1384
|
+
if (direct) return direct;
|
|
1385
|
+
return getDetailValue(input, "command", "cmd");
|
|
1386
|
+
}
|
|
1387
|
+
function extractTouchedPath(event) {
|
|
1388
|
+
if (event.type !== "tool_call") return "";
|
|
1389
|
+
const details = event.details || {};
|
|
1390
|
+
const input = typeof details.input === "object" && details.input ? details.input : {};
|
|
1391
|
+
const direct = getDetailValue(details, "path", "file_path");
|
|
1392
|
+
if (direct) return direct;
|
|
1393
|
+
return getDetailValue(input, "path", "file_path");
|
|
1394
|
+
}
|
|
1395
|
+
function extractToolName(event) {
|
|
1396
|
+
const details = event.details || {};
|
|
1397
|
+
const value = details.tool;
|
|
1398
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
1399
|
+
}
|
|
1400
|
+
function pushUnique(list, value, maxItems = 4) {
|
|
1401
|
+
const normalized = sanitizePromptText(value);
|
|
1402
|
+
if (!normalized) return;
|
|
1403
|
+
if (list.includes(normalized)) return;
|
|
1404
|
+
if (list.length < maxItems) list.push(normalized);
|
|
1405
|
+
}
|
|
1406
|
+
function pushSecurityFinding(findings, finding) {
|
|
1407
|
+
const key = `${finding.severity}:${finding.signal}:${finding.evidence}`;
|
|
1408
|
+
const exists = findings.some((item) => `${item.severity}:${item.signal}:${item.evidence}` === key);
|
|
1409
|
+
if (!exists) findings.push(finding);
|
|
1410
|
+
}
|
|
1411
|
+
function detectSecurityFindings(findings, sourceText, rules) {
|
|
1412
|
+
const evidence = sanitizePromptText(sourceText).slice(0, 140);
|
|
1413
|
+
if (!evidence) return;
|
|
1414
|
+
for (const rule of rules) {
|
|
1415
|
+
if (!rule.pattern.test(sourceText)) continue;
|
|
1416
|
+
pushSecurityFinding(findings, {
|
|
1417
|
+
severity: rule.severity,
|
|
1418
|
+
signal: rule.signal,
|
|
1419
|
+
evidence
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function hasPattern(text, patterns) {
|
|
1424
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
1425
|
+
}
|
|
1426
|
+
function summarizeActivity(reads, writes, searches, commands, tests, errors) {
|
|
1427
|
+
return `reads=${reads}, writes=${writes}, searches=${searches}, commands=${commands}, tests=${tests}, errors=${errors}`;
|
|
1428
|
+
}
|
|
1429
|
+
function summarizeClosingEvidence(assessment) {
|
|
1430
|
+
if (assessment.progressSignals.length > 0) return assessment.progressSignals[0];
|
|
1431
|
+
if (assessment.driftSignals.length > 0) return assessment.driftSignals[0];
|
|
1432
|
+
return assessment.activitySummary;
|
|
1433
|
+
}
|
|
1434
|
+
function createClosingLine(assessment) {
|
|
1435
|
+
const evidence = summarizeClosingEvidence(assessment);
|
|
1436
|
+
const dir = assessment.direction === "on-track" ? "on track" : assessment.direction;
|
|
1437
|
+
const sec = assessment.security !== "clean" ? ` Security is ${assessment.security}.` : "";
|
|
1438
|
+
return `${evidence} \u2014 looks ${dir} with ${assessment.confidence} confidence.${sec}`;
|
|
1439
|
+
}
|
|
1440
|
+
function extractRecentlyUsedJudgments(recentCommentary) {
|
|
1441
|
+
const found = /* @__PURE__ */ new Set();
|
|
1442
|
+
for (const text of recentCommentary.slice(-5)) {
|
|
1443
|
+
const upper = String(text || "").toUpperCase();
|
|
1444
|
+
for (const phrase of JUDGMENT_PHRASES) {
|
|
1445
|
+
if (upper.includes(phrase)) found.add(phrase);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return Array.from(found).slice(0, 4);
|
|
1449
|
+
}
|
|
1450
|
+
function buildCommentaryAssessment(events, sessionTitle) {
|
|
1451
|
+
let reads = 0;
|
|
1452
|
+
let writes = 0;
|
|
1453
|
+
let searches = 0;
|
|
1454
|
+
let commands = 0;
|
|
1455
|
+
let tests = 0;
|
|
1456
|
+
let deploys = 0;
|
|
1457
|
+
let errors = 0;
|
|
1458
|
+
const touchedFiles = [];
|
|
1459
|
+
const progressSignals = [];
|
|
1460
|
+
const driftSignals = [];
|
|
1461
|
+
const securityFindings = [];
|
|
1462
|
+
for (const event of events) {
|
|
1463
|
+
const summary = sanitizePromptText(event.summary);
|
|
1464
|
+
const summaryLower = summary.toLowerCase();
|
|
1465
|
+
const toolName = extractToolName(event);
|
|
1466
|
+
const command = extractCommand(event);
|
|
1467
|
+
const touchedPath = extractTouchedPath(event);
|
|
1468
|
+
if (event.type === "tool_call") {
|
|
1469
|
+
if (summaryLower.startsWith("reading ") || toolName.includes("read")) reads += 1;
|
|
1470
|
+
if (summaryLower.startsWith("writing ") || summaryLower.startsWith("editing ") || toolName.includes("write") || toolName.includes("edit") || toolName.includes("apply_diff")) {
|
|
1471
|
+
writes += 1;
|
|
1472
|
+
}
|
|
1473
|
+
if (summaryLower.startsWith("searching ") || summaryLower.startsWith("finding files") || toolName.includes("grep") || toolName.includes("glob")) {
|
|
1474
|
+
searches += 1;
|
|
1475
|
+
}
|
|
1476
|
+
if (command) {
|
|
1477
|
+
commands += 1;
|
|
1478
|
+
if (hasPattern(command, TEST_COMMAND_PATTERNS)) tests += 1;
|
|
1479
|
+
if (hasPattern(command, DEPLOY_COMMAND_PATTERNS)) deploys += 1;
|
|
1480
|
+
detectSecurityFindings(securityFindings, command, SECURITY_COMMAND_RULES);
|
|
1481
|
+
}
|
|
1482
|
+
if (touchedPath) {
|
|
1483
|
+
pushUnique(touchedFiles, touchedPath, 3);
|
|
1484
|
+
detectSecurityFindings(securityFindings, touchedPath, SECURITY_PATH_RULES);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const output = typeof event.details?.output === "string" ? event.details.output : "";
|
|
1488
|
+
const combinedText = `${summary}
|
|
1489
|
+
${output}`;
|
|
1490
|
+
if ((event.type === "message" || event.type === "tool_result") && ERROR_SIGNAL_PATTERN.test(combinedText)) {
|
|
1491
|
+
errors += 1;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (writes > 0) pushUnique(progressSignals, `${writes} write/edit actions executed`);
|
|
1495
|
+
if (tests > 0) pushUnique(progressSignals, `${tests} test commands detected`);
|
|
1496
|
+
if (deploys > 0) pushUnique(progressSignals, `${deploys} deploy/release commands detected`);
|
|
1497
|
+
if (touchedFiles.length > 0) pushUnique(progressSignals, `touched files: ${touchedFiles.join(", ")}`);
|
|
1498
|
+
if (reads >= 4 && writes === 0) pushUnique(driftSignals, `${reads} reads with no code edits`);
|
|
1499
|
+
if (commands >= 6 && writes === 0 && tests === 0) {
|
|
1500
|
+
pushUnique(driftSignals, `${commands} commands executed without visible code/test progress`);
|
|
1501
|
+
}
|
|
1502
|
+
if (errors >= 2) pushUnique(driftSignals, `${errors} error/failure signals seen in outputs`);
|
|
1503
|
+
if (deploys > 0 && tests === 0) pushUnique(driftSignals, "deploy/release command seen without test evidence");
|
|
1504
|
+
if (progressSignals.length === 0 && events.length >= 3) {
|
|
1505
|
+
pushUnique(driftSignals, "no concrete progress signal captured in this batch");
|
|
1506
|
+
}
|
|
1507
|
+
let direction = "on-track";
|
|
1508
|
+
if (errors >= 3 && writes === 0 && tests === 0) {
|
|
1509
|
+
direction = "blocked";
|
|
1510
|
+
} else if (driftSignals.length > 0 && writes === 0 && tests === 0) {
|
|
1511
|
+
direction = "drifting";
|
|
1512
|
+
} else if (progressSignals.length === 0) {
|
|
1513
|
+
direction = "drifting";
|
|
1514
|
+
}
|
|
1515
|
+
const evidenceScore = writes + tests + deploys + (errors > 0 ? 1 : 0);
|
|
1516
|
+
const confidence = events.length >= 8 || evidenceScore >= 3 ? "high" : events.length >= 4 || evidenceScore >= 1 ? "medium" : "low";
|
|
1517
|
+
const security = securityFindings.some((finding) => finding.severity === "high") ? "alert" : securityFindings.length > 0 ? "watch" : "clean";
|
|
1518
|
+
if (sessionTitle) {
|
|
1519
|
+
pushUnique(progressSignals, `session goal/title: ${sessionTitle}`, 5);
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
direction,
|
|
1523
|
+
confidence,
|
|
1524
|
+
security,
|
|
1525
|
+
activitySummary: summarizeActivity(reads, writes, searches, commands, tests, errors),
|
|
1526
|
+
progressSignals,
|
|
1527
|
+
driftSignals,
|
|
1528
|
+
securityFindings: securityFindings.slice(0, 3)
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function applyAssessmentSignals(commentary, assessment) {
|
|
1532
|
+
let out = String(commentary || "").trim();
|
|
1533
|
+
if (!out) {
|
|
1534
|
+
out = "Agent completed actions in this session.";
|
|
1535
|
+
}
|
|
1536
|
+
const hasSecuritySignal = /\bsecurity\b|\bsecurity alert\b|\bsecurity watch\b/i.test(out);
|
|
1537
|
+
if (!hasSecuritySignal && assessment.security !== "clean") {
|
|
1538
|
+
const top = assessment.securityFindings[0];
|
|
1539
|
+
const label = assessment.security === "alert" ? "SECURITY ALERT" : "SECURITY WATCH";
|
|
1540
|
+
const reason = top ? `${top.signal} (${top.evidence})` : "Risky behavior detected in this batch.";
|
|
1541
|
+
out = `${label}: ${reason}
|
|
1542
|
+
${out}`;
|
|
1543
|
+
}
|
|
1544
|
+
const hasClosingLine = /\bverdict\b|\bon.?track\b|\bdrifting\b|\bblocked\b|\bmomentum\b/i.test(out);
|
|
1545
|
+
if (!hasClosingLine) {
|
|
1546
|
+
out = `${out}
|
|
1547
|
+
${createClosingLine(assessment)}`;
|
|
1548
|
+
}
|
|
1549
|
+
return out;
|
|
1550
|
+
}
|
|
1551
|
+
var CommentaryEngine = class extends import_events2.EventEmitter {
|
|
1552
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1553
|
+
flushQueue = [];
|
|
1554
|
+
queuedSessions = /* @__PURE__ */ new Set();
|
|
1555
|
+
generating = false;
|
|
1556
|
+
model = DEFAULT_MODEL;
|
|
1557
|
+
userFocus = "";
|
|
1558
|
+
preset = "auto";
|
|
1559
|
+
providers = {
|
|
1560
|
+
anthropic: new AnthropicProvider(),
|
|
1561
|
+
openai: new OpenAIProvider()
|
|
1562
|
+
};
|
|
1563
|
+
keyResolver;
|
|
1564
|
+
paused = false;
|
|
1565
|
+
enabledFormatStyleIds = DEFAULT_FORMAT_STYLE_IDS.slice();
|
|
1566
|
+
lastFormatStyleId = null;
|
|
1567
|
+
constructor(keyResolver) {
|
|
1568
|
+
super();
|
|
1569
|
+
this.keyResolver = keyResolver;
|
|
1570
|
+
}
|
|
1571
|
+
setModel(model) {
|
|
1572
|
+
this.model = model;
|
|
1573
|
+
this.emit("model-changed", model);
|
|
1574
|
+
}
|
|
1575
|
+
getModel() {
|
|
1576
|
+
return this.model;
|
|
1577
|
+
}
|
|
1578
|
+
setFocus(focus) {
|
|
1579
|
+
this.userFocus = focus;
|
|
1580
|
+
}
|
|
1581
|
+
getFocus() {
|
|
1582
|
+
return this.userFocus;
|
|
1583
|
+
}
|
|
1584
|
+
setPreset(preset) {
|
|
1585
|
+
const next = Object.prototype.hasOwnProperty.call(PRESET_INSTRUCTIONS, preset) ? preset : "auto";
|
|
1586
|
+
this.preset = next;
|
|
1587
|
+
this.emit("preset-changed", this.preset);
|
|
1588
|
+
}
|
|
1589
|
+
getPreset() {
|
|
1590
|
+
return this.preset;
|
|
1591
|
+
}
|
|
1592
|
+
setFormatStyles(styleIds) {
|
|
1593
|
+
const next = normalizeFormatStyles(Array.isArray(styleIds) ? styleIds : []);
|
|
1594
|
+
if (sameFormatStyles(next, this.enabledFormatStyleIds)) return;
|
|
1595
|
+
this.enabledFormatStyleIds = next;
|
|
1596
|
+
if (this.lastFormatStyleId && !this.enabledFormatStyleIds.includes(this.lastFormatStyleId)) {
|
|
1597
|
+
this.lastFormatStyleId = null;
|
|
1598
|
+
}
|
|
1599
|
+
this.emit("format-styles-changed", this.enabledFormatStyleIds.slice());
|
|
1600
|
+
}
|
|
1601
|
+
getFormatStyles() {
|
|
1602
|
+
return this.enabledFormatStyleIds.slice();
|
|
1603
|
+
}
|
|
1604
|
+
pause() {
|
|
1605
|
+
this.paused = true;
|
|
1606
|
+
}
|
|
1607
|
+
resume() {
|
|
1608
|
+
this.paused = false;
|
|
1609
|
+
}
|
|
1610
|
+
isPaused() {
|
|
1611
|
+
return this.paused;
|
|
1612
|
+
}
|
|
1613
|
+
addEvent(event) {
|
|
1614
|
+
if (this.paused) return;
|
|
1615
|
+
if (event.type === "tool_result" || event.type === "meta") return;
|
|
1616
|
+
const key = this.sessionKey(event);
|
|
1617
|
+
const state = this.ensureSessionState(key);
|
|
1618
|
+
state.events.push(event);
|
|
1619
|
+
if (state.events.length >= MAX_BATCH_SIZE) {
|
|
1620
|
+
this.requestFlush(key, true);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
1624
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, false), IDLE_TIMEOUT_MS);
|
|
1625
|
+
if (!state.maxTimer) {
|
|
1626
|
+
state.maxTimer = setTimeout(() => this.requestFlush(key, true), MAX_WAIT_MS);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
sessionKey(event) {
|
|
1630
|
+
return `${event.agent}:${event.sessionId}`;
|
|
1631
|
+
}
|
|
1632
|
+
ensureSessionState(key) {
|
|
1633
|
+
if (!this.sessions.has(key)) {
|
|
1634
|
+
this.sessions.set(key, {
|
|
1635
|
+
events: [],
|
|
1636
|
+
idleTimer: null,
|
|
1637
|
+
maxTimer: null,
|
|
1638
|
+
recentCommentary: []
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return this.sessions.get(key);
|
|
1642
|
+
}
|
|
1643
|
+
clearSessionTimers(state) {
|
|
1644
|
+
if (state.idleTimer) {
|
|
1645
|
+
clearTimeout(state.idleTimer);
|
|
1646
|
+
state.idleTimer = null;
|
|
1647
|
+
}
|
|
1648
|
+
if (state.maxTimer) {
|
|
1649
|
+
clearTimeout(state.maxTimer);
|
|
1650
|
+
state.maxTimer = null;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
requestFlush(key, force) {
|
|
1654
|
+
const state = this.sessions.get(key);
|
|
1655
|
+
if (!state) return;
|
|
1656
|
+
this.clearSessionTimers(state);
|
|
1657
|
+
if (state.events.length === 0) return;
|
|
1658
|
+
if (!force && state.events.length < MIN_BATCH_SIZE) {
|
|
1659
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
this.enqueueFlush(key);
|
|
1663
|
+
}
|
|
1664
|
+
enqueueFlush(key) {
|
|
1665
|
+
if (this.queuedSessions.has(key)) return;
|
|
1666
|
+
this.queuedSessions.add(key);
|
|
1667
|
+
this.flushQueue.push(key);
|
|
1668
|
+
this.processFlushQueue();
|
|
1669
|
+
}
|
|
1670
|
+
async processFlushQueue() {
|
|
1671
|
+
if (this.generating) return;
|
|
1672
|
+
while (this.flushQueue.length > 0) {
|
|
1673
|
+
const key = this.flushQueue.shift();
|
|
1674
|
+
this.queuedSessions.delete(key);
|
|
1675
|
+
const state = this.sessions.get(key);
|
|
1676
|
+
if (!state || state.events.length === 0) continue;
|
|
1677
|
+
const batch = state.events.splice(0);
|
|
1678
|
+
this.generating = true;
|
|
1679
|
+
try {
|
|
1680
|
+
await this.generateCommentary(batch, state);
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
this.emit("error", err);
|
|
1683
|
+
} finally {
|
|
1684
|
+
this.generating = false;
|
|
1685
|
+
}
|
|
1686
|
+
if (state.events.length > 0) {
|
|
1687
|
+
if (state.events.length >= MIN_BATCH_SIZE) {
|
|
1688
|
+
this.enqueueFlush(key);
|
|
1689
|
+
} else if (!state.idleTimer) {
|
|
1690
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
pickFormat() {
|
|
1696
|
+
const enabled = this.enabledFormatStyleIds.length > 0 ? this.enabledFormatStyleIds : DEFAULT_FORMAT_STYLE_IDS;
|
|
1697
|
+
const pool = this.lastFormatStyleId && enabled.length > 1 ? enabled.filter((styleId) => styleId !== this.lastFormatStyleId) : enabled;
|
|
1698
|
+
const pickedStyleId = pool[Math.floor(Math.random() * pool.length)];
|
|
1699
|
+
this.lastFormatStyleId = pickedStyleId;
|
|
1700
|
+
return FORMAT_TEMPLATES[pickedStyleId];
|
|
1701
|
+
}
|
|
1702
|
+
async generateCommentary(events, state) {
|
|
1703
|
+
if (events.length === 0) return;
|
|
1704
|
+
const modelConfig = MODELS.find((m) => m.id === this.model);
|
|
1705
|
+
if (!modelConfig) {
|
|
1706
|
+
this.emit("error", new Error(`Unknown model: ${this.model}`));
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const apiKey = "";
|
|
1710
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
1711
|
+
const latest = events[events.length - 1];
|
|
1712
|
+
const assessment = buildCommentaryAssessment(events, latest.sessionTitle);
|
|
1713
|
+
const userPrompt = this.buildUserPrompt(events, state.recentCommentary, assessment);
|
|
1714
|
+
const provider = this.providers[modelConfig.provider];
|
|
1715
|
+
const entry = {
|
|
1716
|
+
timestamp: Date.now(),
|
|
1717
|
+
sessionId: latest.sessionId,
|
|
1718
|
+
projectName: latest.projectName,
|
|
1719
|
+
sessionTitle: latest.sessionTitle,
|
|
1720
|
+
agent: latest.agent,
|
|
1721
|
+
source: latest.source,
|
|
1722
|
+
eventSummary: events.map((e) => sanitizePromptText(e.summary)).join(" \u2192 "),
|
|
1723
|
+
commentary: ""
|
|
1724
|
+
};
|
|
1725
|
+
this.emit("commentary-start", entry);
|
|
1726
|
+
try {
|
|
1727
|
+
let streamedRaw = "";
|
|
1728
|
+
const full = await provider.generate(
|
|
1729
|
+
systemPrompt,
|
|
1730
|
+
userPrompt,
|
|
1731
|
+
apiKey || "",
|
|
1732
|
+
this.model,
|
|
1733
|
+
(chunk) => {
|
|
1734
|
+
streamedRaw += chunk;
|
|
1735
|
+
}
|
|
1736
|
+
);
|
|
1737
|
+
const rawOutput = full || streamedRaw;
|
|
1738
|
+
entry.commentary = applyAssessmentSignals(
|
|
1739
|
+
sanitizeGeneratedCommentary(rawOutput),
|
|
1740
|
+
assessment
|
|
1741
|
+
);
|
|
1742
|
+
if (entry.commentary) {
|
|
1743
|
+
this.emit("commentary-chunk", { entry, chunk: entry.commentary });
|
|
1744
|
+
}
|
|
1745
|
+
this.emit("commentary-done", entry);
|
|
1746
|
+
state.recentCommentary.push(entry.commentary);
|
|
1747
|
+
if (state.recentCommentary.length > 5) {
|
|
1748
|
+
state.recentCommentary.shift();
|
|
1749
|
+
}
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
entry.commentary = "Commentary unavailable right now.";
|
|
1752
|
+
this.emit("commentary-chunk", { entry, chunk: entry.commentary });
|
|
1753
|
+
this.emit("commentary-done", entry);
|
|
1754
|
+
this.emit("error", err);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
buildSystemPrompt() {
|
|
1758
|
+
let prompt = SYSTEM_PROMPT;
|
|
1759
|
+
prompt += `
|
|
1760
|
+
|
|
1761
|
+
Format for this message:
|
|
1762
|
+
${this.pickFormat()}`;
|
|
1763
|
+
if (this.preset !== "auto") {
|
|
1764
|
+
prompt += `
|
|
1765
|
+
|
|
1766
|
+
Tone preset:
|
|
1767
|
+
${PRESET_INSTRUCTIONS[this.preset]}`;
|
|
1768
|
+
}
|
|
1769
|
+
if (this.userFocus.trim()) {
|
|
1770
|
+
prompt += `
|
|
1771
|
+
|
|
1772
|
+
Additional user instruction: ${this.userFocus}`;
|
|
1773
|
+
}
|
|
1774
|
+
return prompt;
|
|
1775
|
+
}
|
|
1776
|
+
buildUserPrompt(events, recentCommentary, assessment) {
|
|
1777
|
+
const latest = events[events.length - 1];
|
|
1778
|
+
const lines = events.map((e) => ` ${e.type}: ${sanitizePromptText(e.summary)}`);
|
|
1779
|
+
let prompt = `Session: ${latest.agent}/${latest.projectName}
|
|
1780
|
+
`;
|
|
1781
|
+
if (latest.sessionTitle) {
|
|
1782
|
+
prompt += `Session title: ${sanitizePromptText(latest.sessionTitle)}
|
|
1783
|
+
`;
|
|
1784
|
+
}
|
|
1785
|
+
prompt += `Actions (${events.length}):
|
|
1786
|
+
${lines.join("\n")}`;
|
|
1787
|
+
prompt += `
|
|
1788
|
+
|
|
1789
|
+
Direction context:
|
|
1790
|
+
`;
|
|
1791
|
+
prompt += `- Activity snapshot: ${assessment.activitySummary}
|
|
1792
|
+
`;
|
|
1793
|
+
if (assessment.progressSignals.length > 0) {
|
|
1794
|
+
prompt += `- Progress signals: ${assessment.progressSignals.join(" | ")}
|
|
1795
|
+
`;
|
|
1796
|
+
}
|
|
1797
|
+
if (assessment.driftSignals.length > 0) {
|
|
1798
|
+
prompt += `- Drift/block signals: ${assessment.driftSignals.join(" | ")}
|
|
1799
|
+
`;
|
|
1800
|
+
}
|
|
1801
|
+
prompt += `- Initial direction estimate from raw signals: ${assessment.direction} (${assessment.confidence} confidence)
|
|
1802
|
+
`;
|
|
1803
|
+
prompt += `
|
|
1804
|
+
Security auto-check:
|
|
1805
|
+
`;
|
|
1806
|
+
prompt += `- Security state: ${assessment.security}
|
|
1807
|
+
`;
|
|
1808
|
+
if (assessment.securityFindings.length > 0) {
|
|
1809
|
+
prompt += assessment.securityFindings.map((finding) => `- ${finding.severity.toUpperCase()}: ${finding.signal} (${finding.evidence})`).join("\n");
|
|
1810
|
+
prompt += "\n";
|
|
1811
|
+
} else {
|
|
1812
|
+
prompt += `- No explicit security flags in this batch.
|
|
1813
|
+
`;
|
|
1814
|
+
}
|
|
1815
|
+
if (recentCommentary.length > 0) {
|
|
1816
|
+
prompt += `
|
|
1817
|
+
|
|
1818
|
+
Previous commentary (don't repeat):
|
|
1819
|
+
`;
|
|
1820
|
+
prompt += recentCommentary.slice(-3).map((c2) => `- ${sanitizePromptText(c2).slice(0, 80)}...`).join("\n");
|
|
1821
|
+
prompt += `
|
|
1822
|
+
Use different judgment wording than these previous lines.`;
|
|
1823
|
+
}
|
|
1824
|
+
const recentlyUsed = extractRecentlyUsedJudgments(recentCommentary);
|
|
1825
|
+
if (recentlyUsed.length > 0) {
|
|
1826
|
+
prompt += `
|
|
1827
|
+
Avoid reusing these reaction phrases: ${recentlyUsed.join(", ")}.`;
|
|
1828
|
+
}
|
|
1829
|
+
prompt += `
|
|
1830
|
+
|
|
1831
|
+
Summarize only this session's actions. Plain language, with your reaction.`;
|
|
1832
|
+
prompt += `
|
|
1833
|
+
Close with one sentence about direction and momentum \u2014 weave in confidence and security naturally.`;
|
|
1834
|
+
prompt += `
|
|
1835
|
+
Never mention IDs/logs/prompts/traces or internal data-collection steps.`;
|
|
1836
|
+
prompt += `
|
|
1837
|
+
Never say "I", "I'll", "we", or any future plan.`;
|
|
1838
|
+
return prompt;
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
function redactSessionIdentifiers(text) {
|
|
1842
|
+
let out = text;
|
|
1843
|
+
for (const pattern of SESSION_ID_PATTERNS) {
|
|
1844
|
+
out = out.replace(pattern, "[session]");
|
|
1845
|
+
}
|
|
1846
|
+
return out;
|
|
1847
|
+
}
|
|
1848
|
+
function sanitizePromptText(text) {
|
|
1849
|
+
return redactSessionIdentifiers(String(text || "")).replace(/\s+/g, " ").trim();
|
|
1850
|
+
}
|
|
1851
|
+
function sanitizeGeneratedCommentary(text) {
|
|
1852
|
+
let out = redactSessionIdentifiers(String(text || ""));
|
|
1853
|
+
out = out.replace(/\blocal session logs?\b/gi, "recent actions");
|
|
1854
|
+
out = out.replace(/\bsession logs?\b/gi, "actions");
|
|
1855
|
+
out = out.replace(/\bsession id\b/gi, "session");
|
|
1856
|
+
out = out.replace(/\bverdict\s*:\s*/gi, "");
|
|
1857
|
+
const blockedLine = /\b(i['’]?ll|i will|we(?:'ll| will)?)\b.*\b(pull|fetch|read|inspect|scan|parse|load|check)\b.*\b(session|log|trace|jsonl|prompt|history)\b/i;
|
|
1858
|
+
const keptLines = out.split("\n").filter((line) => !blockedLine.test(line.trim()));
|
|
1859
|
+
out = keptLines.join("\n").trim();
|
|
1860
|
+
if (!out) {
|
|
1861
|
+
return "Agent completed actions in this session.";
|
|
1862
|
+
}
|
|
1863
|
+
return out;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/core/platform-support.ts
|
|
1867
|
+
var import_child_process3 = require("child_process");
|
|
1868
|
+
var fs2 = __toESM(require("fs"));
|
|
1869
|
+
var path2 = __toESM(require("path"));
|
|
1870
|
+
function getProviderCliCommand(provider, platform = process.platform) {
|
|
1871
|
+
return platform === "win32" ? `${provider}.cmd` : provider;
|
|
1872
|
+
}
|
|
1873
|
+
function resolveCmdNodeScript(cmdPath) {
|
|
1874
|
+
if (!String(cmdPath || "").toLowerCase().endsWith(".cmd")) return null;
|
|
1875
|
+
try {
|
|
1876
|
+
const content = fs2.readFileSync(cmdPath, "utf8");
|
|
1877
|
+
const match = content.match(/%dp0%\\(.+?\.js)/i);
|
|
1878
|
+
if (!match) return null;
|
|
1879
|
+
const scriptPath = path2.join(path2.dirname(cmdPath), match[1]);
|
|
1880
|
+
return fs2.existsSync(scriptPath) ? scriptPath : null;
|
|
1881
|
+
} catch {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
function inheritShellPath(platform = process.platform) {
|
|
1886
|
+
if (platform === "darwin") {
|
|
1887
|
+
inheritDarwinPath();
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (platform === "win32") {
|
|
1891
|
+
inheritNpmPrefixPath("win32");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
inheritNpmPrefixPath("linux");
|
|
1895
|
+
}
|
|
1896
|
+
function findCommandPath(command, platform = process.platform) {
|
|
1897
|
+
try {
|
|
1898
|
+
if (platform === "win32") {
|
|
1899
|
+
const out2 = (0, import_child_process3.execSync)(`where ${command}`, {
|
|
1900
|
+
encoding: "utf8",
|
|
1901
|
+
timeout: 5e3,
|
|
1902
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1903
|
+
}).trim();
|
|
1904
|
+
const first = out2.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1905
|
+
return first || void 0;
|
|
1906
|
+
}
|
|
1907
|
+
const out = (0, import_child_process3.execFileSync)("which", [command], {
|
|
1908
|
+
encoding: "utf8",
|
|
1909
|
+
timeout: 5e3,
|
|
1910
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1911
|
+
}).trim();
|
|
1912
|
+
return out || void 0;
|
|
1913
|
+
} catch {
|
|
1914
|
+
return void 0;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
function inheritDarwinPath() {
|
|
1918
|
+
const currentPath = process.env.PATH || "";
|
|
1919
|
+
const parts = new Set(currentPath.split(path2.delimiter).filter(Boolean));
|
|
1920
|
+
const candidateShells = [process.env.SHELL, "/bin/zsh", "/bin/bash"].map((value) => String(value || "").trim()).filter(Boolean).filter((value, index, arr) => arr.indexOf(value) === index);
|
|
1921
|
+
for (const shell of candidateShells) {
|
|
1922
|
+
if (!fs2.existsSync(shell)) continue;
|
|
1923
|
+
try {
|
|
1924
|
+
const shellPath = (0, import_child_process3.execFileSync)(shell, ["-lic", "echo $PATH"], {
|
|
1925
|
+
encoding: "utf8",
|
|
1926
|
+
timeout: 5e3,
|
|
1927
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1928
|
+
}).trim();
|
|
1929
|
+
if (!shellPath) continue;
|
|
1930
|
+
process.env.PATH = mergePath(currentPath, shellPath, parts);
|
|
1931
|
+
return;
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function inheritNpmPrefixPath(platform) {
|
|
1937
|
+
const npmCommand = platform === "win32" ? "npm.cmd" : "npm";
|
|
1938
|
+
try {
|
|
1939
|
+
const npmPrefix = (0, import_child_process3.execFileSync)(npmCommand, ["prefix", "-g"], {
|
|
1940
|
+
encoding: "utf8",
|
|
1941
|
+
timeout: 5e3,
|
|
1942
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1943
|
+
shell: platform === "win32"
|
|
1944
|
+
}).trim();
|
|
1945
|
+
if (!npmPrefix) return;
|
|
1946
|
+
const candidates = platform === "win32" ? [npmPrefix, path2.join(npmPrefix, "node_modules", ".bin")] : [path2.join(npmPrefix, "bin")];
|
|
1947
|
+
let nextPath = process.env.PATH || "";
|
|
1948
|
+
const parts = new Set(nextPath.split(path2.delimiter).filter(Boolean));
|
|
1949
|
+
for (const candidate of candidates) {
|
|
1950
|
+
if (!candidate || parts.has(candidate)) continue;
|
|
1951
|
+
nextPath = nextPath ? `${candidate}${path2.delimiter}${nextPath}` : candidate;
|
|
1952
|
+
parts.add(candidate);
|
|
1953
|
+
}
|
|
1954
|
+
process.env.PATH = nextPath;
|
|
1955
|
+
} catch {
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function mergePath(currentPath, shellPath, knownParts) {
|
|
1959
|
+
const additions = [];
|
|
1960
|
+
for (const candidate of shellPath.split(path2.delimiter).filter(Boolean)) {
|
|
1961
|
+
if (knownParts.has(candidate)) continue;
|
|
1962
|
+
knownParts.add(candidate);
|
|
1963
|
+
additions.push(candidate);
|
|
1964
|
+
}
|
|
1965
|
+
if (additions.length === 0) return currentPath;
|
|
1966
|
+
return currentPath ? `${currentPath}${path2.delimiter}${additions.join(path2.delimiter)}` : additions.join(path2.delimiter);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/core/session-dispatch.ts
|
|
1970
|
+
var import_events3 = require("events");
|
|
1971
|
+
var import_child_process4 = require("child_process");
|
|
1972
|
+
var fs3 = __toESM(require("fs"));
|
|
1973
|
+
var path3 = __toESM(require("path"));
|
|
1974
|
+
var SessionDispatchService = class extends import_events3.EventEmitter {
|
|
1975
|
+
constructor(options) {
|
|
1976
|
+
super();
|
|
1977
|
+
this.options = options;
|
|
1978
|
+
}
|
|
1979
|
+
async dispatch(request) {
|
|
1980
|
+
const prompt = String(request.prompt || "").trim();
|
|
1981
|
+
if (!prompt) {
|
|
1982
|
+
this.emitStatus("failed", request.target, "Prompt cannot be empty");
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
this.emitStatus("queued", request.target, `Queued for ${describeTarget(request.target)}`);
|
|
1986
|
+
if (request.target.kind === "existing") {
|
|
1987
|
+
await this.dispatchExistingSession(request.target, prompt);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const provider = request.target.kind === "new-codex" ? "codex" : "claude";
|
|
1991
|
+
this.emitStatus("started", request.target, `Starting new ${provider} session`);
|
|
1992
|
+
try {
|
|
1993
|
+
if (request.origin === "vscode") {
|
|
1994
|
+
if (!this.options.launchInteractiveSession) {
|
|
1995
|
+
throw new Error("Interactive launcher is not available in VS Code mode");
|
|
1996
|
+
}
|
|
1997
|
+
await this.options.launchInteractiveSession(provider, prompt);
|
|
1998
|
+
} else {
|
|
1999
|
+
await runInteractiveInTerminal(provider, prompt);
|
|
2000
|
+
}
|
|
2001
|
+
this.emitStatus("sent", request.target, `Started new ${provider} session`);
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
this.emitStatus("failed", request.target, normalizeError(error));
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
async dispatchExistingSession(target, prompt) {
|
|
2007
|
+
const targetSessionId = String(target.sessionId || "").trim().toLowerCase();
|
|
2008
|
+
const targetAgent = target.agent;
|
|
2009
|
+
if (!targetSessionId || targetAgent !== "claude" && targetAgent !== "codex") {
|
|
2010
|
+
this.emitStatus("failed", target, "Missing target session or provider");
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const active = this.options.getActiveSessions();
|
|
2014
|
+
const activeMatch = active.find((session) => {
|
|
2015
|
+
return session.agent === targetAgent && session.id.toLowerCase() === targetSessionId;
|
|
2016
|
+
});
|
|
2017
|
+
if (!activeMatch) {
|
|
2018
|
+
this.emitStatus("failed", target, `Selected ${describeProvider(targetAgent)} session is not active`);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const targetLabel = describeTarget(target);
|
|
2022
|
+
this.emitStatus("started", target, `Dispatching to ${targetLabel}`);
|
|
2023
|
+
try {
|
|
2024
|
+
const command = buildExistingDispatchCommand(target, prompt);
|
|
2025
|
+
const dispatchCwd = deriveDispatchCwdForSession(activeMatch);
|
|
2026
|
+
const beforeDispatch = captureSessionFileSnapshot(activeMatch.filePath);
|
|
2027
|
+
const handle = await startBackgroundCommand(command, { cwd: dispatchCwd });
|
|
2028
|
+
const dispatchOutcome = await runWithTimeout(
|
|
2029
|
+
waitForDispatchAcknowledgement(
|
|
2030
|
+
activeMatch.filePath,
|
|
2031
|
+
prompt,
|
|
2032
|
+
beforeDispatch,
|
|
2033
|
+
handle.completion
|
|
2034
|
+
),
|
|
2035
|
+
2e4,
|
|
2036
|
+
"Dispatch timed out waiting for target session update"
|
|
2037
|
+
);
|
|
2038
|
+
if (dispatchOutcome === "process-complete") {
|
|
2039
|
+
verifyExistingDispatchDelivery(activeMatch.filePath, prompt, beforeDispatch);
|
|
2040
|
+
}
|
|
2041
|
+
void handle.completion.catch(() => void 0);
|
|
2042
|
+
this.emitStatus("sent", target, `Prompt sent to ${targetLabel}`);
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
this.emitStatus("failed", target, normalizeError(error));
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
emitStatus(state, target, message) {
|
|
2048
|
+
const status = {
|
|
2049
|
+
state,
|
|
2050
|
+
message,
|
|
2051
|
+
target,
|
|
2052
|
+
timestamp: Date.now()
|
|
2053
|
+
};
|
|
2054
|
+
this.emit("status", status);
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
function buildExistingDispatchCommand(target, prompt, platform = process.platform) {
|
|
2058
|
+
if (target.kind !== "existing") {
|
|
2059
|
+
throw new Error(`Expected existing target, got "${target.kind}"`);
|
|
2060
|
+
}
|
|
2061
|
+
const sessionId = String(target.sessionId || "").trim();
|
|
2062
|
+
const agent = target.agent;
|
|
2063
|
+
if (!sessionId || agent !== "claude" && agent !== "codex") {
|
|
2064
|
+
throw new Error("Missing existing-session target details");
|
|
2065
|
+
}
|
|
2066
|
+
if (agent === "codex") {
|
|
2067
|
+
return {
|
|
2068
|
+
provider: "codex",
|
|
2069
|
+
command: getProviderCliCommand("codex", platform),
|
|
2070
|
+
args: ["exec", "resume", "--json", "--skip-git-repo-check", sessionId, prompt]
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
provider: "claude",
|
|
2075
|
+
command: getProviderCliCommand("claude", platform),
|
|
2076
|
+
args: ["-p", prompt, "--verbose", "--output-format", "stream-json", "--resume", sessionId]
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function buildInteractiveDispatchCommand(provider, prompt, platform = process.platform) {
|
|
2080
|
+
return {
|
|
2081
|
+
provider,
|
|
2082
|
+
command: getProviderCliCommand(provider, platform),
|
|
2083
|
+
args: [prompt]
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function resolveDispatchCommand(command, platform = process.platform) {
|
|
2087
|
+
const pathHint = findCommandPath(command.command, platform);
|
|
2088
|
+
if (!pathHint) {
|
|
2089
|
+
throw new Error(`CLI not found: ${command.command}`);
|
|
2090
|
+
}
|
|
2091
|
+
if (platform === "win32") {
|
|
2092
|
+
const nodeScript = resolveCmdNodeScript(pathHint);
|
|
2093
|
+
if (nodeScript) {
|
|
2094
|
+
return {
|
|
2095
|
+
command: process.execPath,
|
|
2096
|
+
args: [nodeScript, ...command.args],
|
|
2097
|
+
shell: false
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
if (pathHint.toLowerCase().endsWith(".cmd")) {
|
|
2101
|
+
return {
|
|
2102
|
+
command: pathHint,
|
|
2103
|
+
args: command.args,
|
|
2104
|
+
shell: true
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
command: pathHint,
|
|
2110
|
+
args: command.args,
|
|
2111
|
+
shell: false
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
async function waitForDispatchAcknowledgement(filePath, prompt, before, completion) {
|
|
2115
|
+
let completionState = 0;
|
|
2116
|
+
let completionError = null;
|
|
2117
|
+
completion.then(() => {
|
|
2118
|
+
completionState = 1;
|
|
2119
|
+
}).catch((error) => {
|
|
2120
|
+
completionState = 2;
|
|
2121
|
+
completionError = error instanceof Error ? error : new Error(String(error || "Dispatch failed"));
|
|
2122
|
+
});
|
|
2123
|
+
while (true) {
|
|
2124
|
+
if (completionState === 2) {
|
|
2125
|
+
throw completionError || new Error("Dispatch failed");
|
|
2126
|
+
}
|
|
2127
|
+
if (hasSessionUpdateWithPrompt(filePath, prompt, before)) {
|
|
2128
|
+
return "prompt-observed";
|
|
2129
|
+
}
|
|
2130
|
+
if (completionState === 1) {
|
|
2131
|
+
return "process-complete";
|
|
2132
|
+
}
|
|
2133
|
+
await sleepMs(120);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function startBackgroundCommand(command, options = {}) {
|
|
2137
|
+
const resolved = resolveDispatchCommand(command);
|
|
2138
|
+
return await new Promise((resolve, reject) => {
|
|
2139
|
+
const child = (0, import_child_process4.spawn)(resolved.command, resolved.args, {
|
|
2140
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2141
|
+
shell: resolved.shell,
|
|
2142
|
+
windowsHide: true,
|
|
2143
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
2144
|
+
});
|
|
2145
|
+
let stderr = "";
|
|
2146
|
+
let spawned = false;
|
|
2147
|
+
let launchSettled = false;
|
|
2148
|
+
const failLaunch = (error) => {
|
|
2149
|
+
if (launchSettled) return;
|
|
2150
|
+
launchSettled = true;
|
|
2151
|
+
reject(error);
|
|
2152
|
+
};
|
|
2153
|
+
const completion = new Promise((resolveCompletion, rejectCompletion) => {
|
|
2154
|
+
child.on("error", (error) => {
|
|
2155
|
+
if (!spawned) {
|
|
2156
|
+
failLaunch(error);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
rejectCompletion(error);
|
|
2160
|
+
});
|
|
2161
|
+
child.on("close", (code) => {
|
|
2162
|
+
if (code === 0) {
|
|
2163
|
+
resolveCompletion();
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const normalized = String(stderr || "").trim();
|
|
2167
|
+
if (looksLikeUnsupportedFlags(normalized)) {
|
|
2168
|
+
rejectCompletion(new Error("Provider CLI does not support required resume flags. Update the CLI version."));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
rejectCompletion(new Error(normalized || `Dispatch exited with code ${code}`));
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
child.stderr?.on("data", (data) => {
|
|
2175
|
+
stderr += data.toString();
|
|
2176
|
+
});
|
|
2177
|
+
child.on("spawn", () => {
|
|
2178
|
+
spawned = true;
|
|
2179
|
+
if (launchSettled) return;
|
|
2180
|
+
launchSettled = true;
|
|
2181
|
+
resolve({ completion });
|
|
2182
|
+
});
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
async function runInteractiveInTerminal(provider, prompt) {
|
|
2186
|
+
const command = buildInteractiveDispatchCommand(provider, prompt);
|
|
2187
|
+
const resolved = resolveDispatchCommand(command);
|
|
2188
|
+
await new Promise((resolve, reject) => {
|
|
2189
|
+
const child = (0, import_child_process4.spawn)(resolved.command, resolved.args, {
|
|
2190
|
+
stdio: "inherit",
|
|
2191
|
+
shell: resolved.shell,
|
|
2192
|
+
windowsHide: false
|
|
2193
|
+
});
|
|
2194
|
+
child.on("error", (error) => reject(error));
|
|
2195
|
+
child.on("close", (code) => {
|
|
2196
|
+
if (code === 0) resolve();
|
|
2197
|
+
else reject(new Error(`Interactive session exited with code ${code}`));
|
|
2198
|
+
});
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
function describeTarget(target) {
|
|
2202
|
+
if (target.kind === "new-codex") return "new codex session";
|
|
2203
|
+
if (target.kind === "new-claude") return "new claude session";
|
|
2204
|
+
const provider = describeProvider(target.agent);
|
|
2205
|
+
const project = cleanTargetLabel(target.projectName, 24);
|
|
2206
|
+
const sessionTitle = cleanTargetLabel(target.sessionTitle, 44);
|
|
2207
|
+
if (project && sessionTitle) return `${provider} session (${project} \u203A ${sessionTitle})`;
|
|
2208
|
+
if (sessionTitle) return `${provider} session (${sessionTitle})`;
|
|
2209
|
+
if (project) return `${provider} session (${project})`;
|
|
2210
|
+
return `${provider} session`;
|
|
2211
|
+
}
|
|
2212
|
+
function describeProvider(agent) {
|
|
2213
|
+
return String(agent || "").toLowerCase() === "claude" ? "Claude" : "Codex";
|
|
2214
|
+
}
|
|
2215
|
+
function cleanTargetLabel(value, max) {
|
|
2216
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
2217
|
+
if (!text) return "";
|
|
2218
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(text)) return "";
|
|
2219
|
+
if (text.length <= max) return text;
|
|
2220
|
+
if (max <= 3) return text.slice(0, max);
|
|
2221
|
+
return `${text.slice(0, max - 3).trimEnd()}...`;
|
|
2222
|
+
}
|
|
2223
|
+
function looksLikeUnsupportedFlags(stderr) {
|
|
2224
|
+
const normalized = String(stderr || "").toLowerCase();
|
|
2225
|
+
if (!normalized) return false;
|
|
2226
|
+
return normalized.includes("unknown option") || normalized.includes("unknown flag") || normalized.includes("unrecognized option") || normalized.includes("did you mean");
|
|
2227
|
+
}
|
|
2228
|
+
function normalizeError(error) {
|
|
2229
|
+
if (error instanceof Error && error.message) return error.message;
|
|
2230
|
+
return String(error || "Dispatch failed");
|
|
2231
|
+
}
|
|
2232
|
+
function captureSessionFileSnapshot(filePath) {
|
|
2233
|
+
try {
|
|
2234
|
+
const stat = fs3.statSync(filePath);
|
|
2235
|
+
return {
|
|
2236
|
+
exists: true,
|
|
2237
|
+
size: stat.size,
|
|
2238
|
+
mtimeMs: stat.mtimeMs
|
|
2239
|
+
};
|
|
2240
|
+
} catch {
|
|
2241
|
+
return {
|
|
2242
|
+
exists: false,
|
|
2243
|
+
size: 0,
|
|
2244
|
+
mtimeMs: 0
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
function verifyExistingDispatchDelivery(filePath, prompt, before) {
|
|
2249
|
+
if (!hasSessionFileChanged(filePath, before)) {
|
|
2250
|
+
throw new Error("Target session did not update after dispatch");
|
|
2251
|
+
}
|
|
2252
|
+
const promptSignature = firstPromptSignature(prompt);
|
|
2253
|
+
if (promptSignature.length < 4) return;
|
|
2254
|
+
const tail = readSessionTailSinceOffset(filePath, before.size);
|
|
2255
|
+
if (!tail) {
|
|
2256
|
+
throw new Error("Target session updated but prompt text was not found");
|
|
2257
|
+
}
|
|
2258
|
+
if (!tail.toLowerCase().includes(promptSignature.toLowerCase())) {
|
|
2259
|
+
throw new Error("Prompt text was not found in target session update");
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function hasSessionUpdateWithPrompt(filePath, prompt, before) {
|
|
2263
|
+
if (!hasSessionFileChanged(filePath, before)) return false;
|
|
2264
|
+
const promptSignature = firstPromptSignature(prompt);
|
|
2265
|
+
if (promptSignature.length < 4) return true;
|
|
2266
|
+
const tail = readSessionTailSinceOffset(filePath, before.size);
|
|
2267
|
+
if (!tail) return false;
|
|
2268
|
+
return tail.toLowerCase().includes(promptSignature.toLowerCase());
|
|
2269
|
+
}
|
|
2270
|
+
function hasSessionFileChanged(filePath, before) {
|
|
2271
|
+
const after = captureSessionFileSnapshot(filePath);
|
|
2272
|
+
if (!after.exists) {
|
|
2273
|
+
throw new Error("Target session file is not accessible after dispatch");
|
|
2274
|
+
}
|
|
2275
|
+
return after.size > before.size || after.mtimeMs > before.mtimeMs;
|
|
2276
|
+
}
|
|
2277
|
+
function firstPromptSignature(prompt) {
|
|
2278
|
+
const lines = String(prompt || "").split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
2279
|
+
const first = lines[0] || String(prompt || "").trim();
|
|
2280
|
+
return first.slice(0, 160);
|
|
2281
|
+
}
|
|
2282
|
+
function readSessionTailSinceOffset(filePath, previousSize) {
|
|
2283
|
+
try {
|
|
2284
|
+
const stat = fs3.statSync(filePath);
|
|
2285
|
+
const maxBytes = 1024 * 512;
|
|
2286
|
+
const start = Math.max(0, previousSize - 2048);
|
|
2287
|
+
const desiredStart = stat.size - start > maxBytes ? Math.max(0, stat.size - maxBytes) : start;
|
|
2288
|
+
const length = Math.max(0, stat.size - desiredStart);
|
|
2289
|
+
if (length === 0) return "";
|
|
2290
|
+
const fd = fs3.openSync(filePath, "r");
|
|
2291
|
+
try {
|
|
2292
|
+
const buf = Buffer.alloc(length);
|
|
2293
|
+
fs3.readSync(fd, buf, 0, length, desiredStart);
|
|
2294
|
+
return buf.toString("utf8");
|
|
2295
|
+
} finally {
|
|
2296
|
+
fs3.closeSync(fd);
|
|
2297
|
+
}
|
|
2298
|
+
} catch {
|
|
2299
|
+
return "";
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
async function runWithTimeout(promise, timeoutMs, message) {
|
|
2303
|
+
return await new Promise((resolve, reject) => {
|
|
2304
|
+
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
2305
|
+
promise.then((value) => {
|
|
2306
|
+
clearTimeout(timer);
|
|
2307
|
+
resolve(value);
|
|
2308
|
+
}).catch((error) => {
|
|
2309
|
+
clearTimeout(timer);
|
|
2310
|
+
reject(error);
|
|
2311
|
+
});
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
async function sleepMs(ms) {
|
|
2315
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2316
|
+
}
|
|
2317
|
+
function deriveDispatchCwdForSession(session) {
|
|
2318
|
+
if (session.agent !== "claude") return void 0;
|
|
2319
|
+
return decodeClaudeProjectPathFromSessionFile(session.filePath);
|
|
2320
|
+
}
|
|
2321
|
+
function decodeClaudeProjectPathFromSessionFile(filePath) {
|
|
2322
|
+
if (!filePath) return void 0;
|
|
2323
|
+
const projectDir = path3.basename(path3.dirname(filePath));
|
|
2324
|
+
if (!projectDir) return void 0;
|
|
2325
|
+
const parts = projectDir.split("-").filter(Boolean);
|
|
2326
|
+
if (parts.length === 0) return void 0;
|
|
2327
|
+
if (parts.length >= 2 && /^[A-Za-z]$/.test(parts[0])) {
|
|
2328
|
+
const windowsPath = `${parts[0]}:\\${parts.slice(1).join("\\")}`;
|
|
2329
|
+
if (fs3.existsSync(windowsPath)) return windowsPath;
|
|
2330
|
+
}
|
|
2331
|
+
const unixPath = `/${parts.join("/")}`;
|
|
2332
|
+
if (fs3.existsSync(unixPath)) return unixPath;
|
|
2333
|
+
return void 0;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/cli/index.ts
|
|
2337
|
+
var c = {
|
|
2338
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
2339
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
2340
|
+
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
2341
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
2342
|
+
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
2343
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
2344
|
+
};
|
|
2345
|
+
var noopKeyResolver = {
|
|
2346
|
+
async getKey(_provider) {
|
|
2347
|
+
return "subscription";
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
function parseArgs(args) {
|
|
2351
|
+
let model = DEFAULT_MODEL;
|
|
2352
|
+
let focus = "";
|
|
2353
|
+
let agent = null;
|
|
2354
|
+
for (let index = 0; index < args.length; index++) {
|
|
2355
|
+
const arg = args[index];
|
|
2356
|
+
if (arg === "--model" && args[index + 1]) {
|
|
2357
|
+
const value = args[++index];
|
|
2358
|
+
const found = MODELS.find((entry) => {
|
|
2359
|
+
return entry.id === value || entry.label.toLowerCase().includes(value.toLowerCase());
|
|
2360
|
+
});
|
|
2361
|
+
if (found) model = found.id;
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
if (arg === "--focus" && args[index + 1]) {
|
|
2365
|
+
focus = args[++index];
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
if (arg === "--agent" && args[index + 1]) {
|
|
2369
|
+
agent = args[++index];
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (arg === "--help" || arg === "-h") {
|
|
2373
|
+
printHelp();
|
|
2374
|
+
process.exit(0);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return { model, focus, agent };
|
|
2378
|
+
}
|
|
2379
|
+
function printHelp() {
|
|
2380
|
+
console.log(`
|
|
2381
|
+
${c.bold("KIBITZ")} \u2014 Live commentary + cross-session dispatch
|
|
2382
|
+
|
|
2383
|
+
${c.bold("Usage:")}
|
|
2384
|
+
kibitz [options]
|
|
2385
|
+
|
|
2386
|
+
${c.bold("Options:")}
|
|
2387
|
+
--model <name> Commentary model (opus, sonnet, haiku, gpt-4o, gpt-4o-mini)
|
|
2388
|
+
--focus <text> Focus instructions for commentary
|
|
2389
|
+
--agent <name> Filter events by agent (claude, codex)
|
|
2390
|
+
--help, -h Show this help
|
|
2391
|
+
|
|
2392
|
+
${c.bold("Slash commands:")}
|
|
2393
|
+
/help
|
|
2394
|
+
/pause
|
|
2395
|
+
/resume
|
|
2396
|
+
/focus <text>
|
|
2397
|
+
/model <id-or-label>
|
|
2398
|
+
/preset <id>
|
|
2399
|
+
/sessions
|
|
2400
|
+
/target <index|agent:sessionId|new-codex|new-claude>
|
|
2401
|
+
|
|
2402
|
+
${c.bold("Composer behavior:")}
|
|
2403
|
+
Plain text sends prompt to selected target.
|
|
2404
|
+
`);
|
|
2405
|
+
}
|
|
2406
|
+
function timeStr() {
|
|
2407
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
|
2408
|
+
hour: "2-digit",
|
|
2409
|
+
minute: "2-digit",
|
|
2410
|
+
second: "2-digit"
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function formatCommentary(text) {
|
|
2414
|
+
return String(text || "").replace(/\*\*(.+?)\*\*/g, (_, inner) => c.bold(inner)).replace(/`([^`]+)`/g, (_, code) => c.cyan(code)).replace(/^[-*] /gm, " \u2022 ");
|
|
2415
|
+
}
|
|
2416
|
+
function agentColor(agent) {
|
|
2417
|
+
return agent === "claude" ? c.yellow : c.green;
|
|
2418
|
+
}
|
|
2419
|
+
function visibleSessionName(session) {
|
|
2420
|
+
const title = String(session.sessionTitle || "").trim();
|
|
2421
|
+
if (title) return title;
|
|
2422
|
+
const project = String(session.projectName || "").trim();
|
|
2423
|
+
if (project) return project;
|
|
2424
|
+
return `${session.agent} session`;
|
|
2425
|
+
}
|
|
2426
|
+
function normalizeTarget(target) {
|
|
2427
|
+
if (target.kind === "new-claude") return { kind: "new-claude" };
|
|
2428
|
+
if (target.kind === "new-codex") return { kind: "new-codex" };
|
|
2429
|
+
return {
|
|
2430
|
+
kind: "existing",
|
|
2431
|
+
agent: target.agent,
|
|
2432
|
+
sessionId: String(target.sessionId || "").trim().toLowerCase(),
|
|
2433
|
+
projectName: target.projectName,
|
|
2434
|
+
sessionTitle: target.sessionTitle
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function parseTargetArg(rawArg, sessions) {
|
|
2438
|
+
const raw = String(rawArg || "").trim();
|
|
2439
|
+
if (!raw) return null;
|
|
2440
|
+
if (raw === "new-codex") return { kind: "new-codex" };
|
|
2441
|
+
if (raw === "new-claude") return { kind: "new-claude" };
|
|
2442
|
+
if (/^\d+$/.test(raw)) {
|
|
2443
|
+
const index = Number(raw);
|
|
2444
|
+
if (index < 1 || index > sessions.length) return null;
|
|
2445
|
+
const session = sessions[index - 1];
|
|
2446
|
+
return {
|
|
2447
|
+
kind: "existing",
|
|
2448
|
+
agent: session.agent,
|
|
2449
|
+
sessionId: String(session.id || "").trim().toLowerCase(),
|
|
2450
|
+
projectName: session.projectName,
|
|
2451
|
+
sessionTitle: session.sessionTitle
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
const match = raw.match(/^(claude|codex):(.+)$/i);
|
|
2455
|
+
if (!match) return null;
|
|
2456
|
+
const agent = match[1].toLowerCase();
|
|
2457
|
+
const sessionId = String(match[2] || "").trim().toLowerCase();
|
|
2458
|
+
if (!sessionId) return null;
|
|
2459
|
+
const found = sessions.find((session) => {
|
|
2460
|
+
return session.agent === agent && String(session.id || "").trim().toLowerCase() === sessionId;
|
|
2461
|
+
});
|
|
2462
|
+
return {
|
|
2463
|
+
kind: "existing",
|
|
2464
|
+
agent,
|
|
2465
|
+
sessionId,
|
|
2466
|
+
projectName: found?.projectName,
|
|
2467
|
+
sessionTitle: found?.sessionTitle
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function describeTarget2(target) {
|
|
2471
|
+
if (target.kind === "new-codex") return "new-codex";
|
|
2472
|
+
if (target.kind === "new-claude") return "new-claude";
|
|
2473
|
+
return `${target.agent || "unknown"}:${target.sessionId || "unknown"}`;
|
|
2474
|
+
}
|
|
2475
|
+
async function main() {
|
|
2476
|
+
inheritShellPath();
|
|
2477
|
+
const options = parseArgs(process.argv.slice(2));
|
|
2478
|
+
console.log(c.bold("\n KIBITZ") + c.dim(" \u2014 Live AI commentary + session dispatch\n"));
|
|
2479
|
+
console.log(c.dim(` Model: ${options.model}`));
|
|
2480
|
+
if (options.focus) console.log(c.dim(` Focus: ${options.focus}`));
|
|
2481
|
+
if (options.agent) console.log(c.dim(` Agent filter: ${options.agent}`));
|
|
2482
|
+
console.log(c.dim(" Watching for sessions... type /help for commands.\n"));
|
|
2483
|
+
const watcher = new SessionWatcher();
|
|
2484
|
+
const engine = new CommentaryEngine(noopKeyResolver);
|
|
2485
|
+
const dispatch = new SessionDispatchService({
|
|
2486
|
+
getActiveSessions: () => watcher.getActiveSessions()
|
|
2487
|
+
});
|
|
2488
|
+
engine.setModel(options.model);
|
|
2489
|
+
if (options.focus) engine.setFocus(options.focus);
|
|
2490
|
+
let selectedTarget = { kind: "new-codex" };
|
|
2491
|
+
function activeSessions() {
|
|
2492
|
+
return watcher.getActiveSessions().filter((session) => {
|
|
2493
|
+
return !options.agent || session.agent === options.agent;
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
function printSessions() {
|
|
2497
|
+
const sessions = activeSessions();
|
|
2498
|
+
if (sessions.length === 0) {
|
|
2499
|
+
console.log(` ${c.dim("No active sessions in watcher window.")}`);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
console.log(` ${c.bold("Active sessions:")}`);
|
|
2503
|
+
sessions.forEach((session, index) => {
|
|
2504
|
+
const marker = selectedTarget.kind === "existing" && selectedTarget.agent === session.agent && String(selectedTarget.sessionId || "").toLowerCase() === String(session.id || "").toLowerCase() ? c.green("*") : " ";
|
|
2505
|
+
const project = session.projectName ? ` project=${session.projectName}` : "";
|
|
2506
|
+
const title = visibleSessionName(session);
|
|
2507
|
+
console.log(` ${marker} [${index + 1}] ${session.agent}:${session.id} ${title}${project}`);
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
function printPromptLine() {
|
|
2511
|
+
const target = describeTarget2(selectedTarget);
|
|
2512
|
+
rl.setPrompt(c.dim(`kibitz[${target}]> `));
|
|
2513
|
+
rl.prompt();
|
|
2514
|
+
}
|
|
2515
|
+
watcher.on("event", (event) => {
|
|
2516
|
+
if (options.agent && event.agent !== options.agent) return;
|
|
2517
|
+
engine.addEvent(event);
|
|
2518
|
+
});
|
|
2519
|
+
engine.on("commentary-start", (entry) => {
|
|
2520
|
+
const color = agentColor(entry.agent);
|
|
2521
|
+
const badge = color(`${entry.agent}/${entry.projectName}`);
|
|
2522
|
+
const source = c.dim(`(${entry.source})`);
|
|
2523
|
+
console.log(`
|
|
2524
|
+
${c.dim(timeStr())} ${badge} ${source}`);
|
|
2525
|
+
console.log(` ${c.dim(entry.eventSummary)}`);
|
|
2526
|
+
process.stdout.write(" ");
|
|
2527
|
+
});
|
|
2528
|
+
engine.on("commentary-chunk", ({ chunk }) => {
|
|
2529
|
+
process.stdout.write(formatCommentary(chunk));
|
|
2530
|
+
});
|
|
2531
|
+
engine.on("commentary-done", () => {
|
|
2532
|
+
console.log("\n");
|
|
2533
|
+
printPromptLine();
|
|
2534
|
+
});
|
|
2535
|
+
engine.on("error", (error) => {
|
|
2536
|
+
console.log(`
|
|
2537
|
+
${c.red("Error:")} ${error.message}`);
|
|
2538
|
+
printPromptLine();
|
|
2539
|
+
});
|
|
2540
|
+
dispatch.on("status", (status) => {
|
|
2541
|
+
const label = status.state.toUpperCase();
|
|
2542
|
+
const color = status.state === "failed" ? c.red : status.state === "sent" ? c.green : c.cyan;
|
|
2543
|
+
console.log(`
|
|
2544
|
+
${color(label)} ${status.message}`);
|
|
2545
|
+
printPromptLine();
|
|
2546
|
+
});
|
|
2547
|
+
watcher.start();
|
|
2548
|
+
const rl = readline.createInterface({
|
|
2549
|
+
input: process.stdin,
|
|
2550
|
+
output: process.stdout,
|
|
2551
|
+
terminal: true,
|
|
2552
|
+
historySize: 1e3
|
|
2553
|
+
});
|
|
2554
|
+
async function handleSlash(raw) {
|
|
2555
|
+
const body = raw.slice(1).trim();
|
|
2556
|
+
const firstSpace = body.indexOf(" ");
|
|
2557
|
+
const command = (firstSpace === -1 ? body : body.slice(0, firstSpace)).toLowerCase();
|
|
2558
|
+
const args = firstSpace === -1 ? "" : body.slice(firstSpace + 1).trim();
|
|
2559
|
+
if (command === "help") {
|
|
2560
|
+
printHelp();
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (command === "pause") {
|
|
2564
|
+
engine.pause();
|
|
2565
|
+
console.log(` ${c.dim("Commentary paused")}`);
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (command === "resume") {
|
|
2569
|
+
engine.resume();
|
|
2570
|
+
console.log(` ${c.dim("Commentary resumed")}`);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
if (command === "focus") {
|
|
2574
|
+
if (!args) {
|
|
2575
|
+
console.log(` ${c.red("Usage: /focus <text>")}`);
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
engine.setFocus(args);
|
|
2579
|
+
console.log(` ${c.dim("Focus updated")}`);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
if (command === "model") {
|
|
2583
|
+
if (!args) {
|
|
2584
|
+
console.log(` ${c.red("Usage: /model <id-or-label>")}`);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const found = MODELS.find((model) => {
|
|
2588
|
+
const needle = args.toLowerCase();
|
|
2589
|
+
return model.id.toLowerCase() === needle || model.label.toLowerCase().includes(needle);
|
|
2590
|
+
});
|
|
2591
|
+
if (!found) {
|
|
2592
|
+
console.log(` ${c.red("Unknown model: " + args)}`);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
engine.setModel(found.id);
|
|
2596
|
+
console.log(` ${c.dim("Model set to " + found.label)}`);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (command === "preset") {
|
|
2600
|
+
if (!args) {
|
|
2601
|
+
console.log(` ${c.red("Usage: /preset <auto|critical-coder|precise-short|emotional|newbie>")}`);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
engine.setPreset(args);
|
|
2605
|
+
console.log(` ${c.dim("Preset set to " + engine.getPreset())}`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (command === "sessions") {
|
|
2609
|
+
printSessions();
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (command === "target") {
|
|
2613
|
+
if (!args) {
|
|
2614
|
+
console.log(` ${c.red("Usage: /target <index|agent:sessionId|new-codex|new-claude>")}`);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
const next = parseTargetArg(args, activeSessions());
|
|
2618
|
+
if (!next) {
|
|
2619
|
+
console.log(` ${c.red("Invalid target: " + args)}`);
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
selectedTarget = normalizeTarget(next);
|
|
2623
|
+
console.log(` ${c.dim("Target set to " + describeTarget2(selectedTarget))}`);
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
console.log(` ${c.red("Unknown command: /" + command)}`);
|
|
2627
|
+
}
|
|
2628
|
+
rl.on("line", async (line) => {
|
|
2629
|
+
const trimmed = String(line || "").trim();
|
|
2630
|
+
if (!trimmed) {
|
|
2631
|
+
printPromptLine();
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (trimmed.startsWith("/")) {
|
|
2635
|
+
await handleSlash(trimmed);
|
|
2636
|
+
printPromptLine();
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
const request = {
|
|
2640
|
+
target: selectedTarget,
|
|
2641
|
+
prompt: trimmed,
|
|
2642
|
+
origin: "cli"
|
|
2643
|
+
};
|
|
2644
|
+
await dispatch.dispatch(request);
|
|
2645
|
+
printPromptLine();
|
|
2646
|
+
});
|
|
2647
|
+
rl.on("close", () => {
|
|
2648
|
+
watcher.stop();
|
|
2649
|
+
console.log(c.dim("\n Kibitz out."));
|
|
2650
|
+
process.exit(0);
|
|
2651
|
+
});
|
|
2652
|
+
process.on("SIGINT", () => {
|
|
2653
|
+
watcher.stop();
|
|
2654
|
+
rl.close();
|
|
2655
|
+
});
|
|
2656
|
+
printPromptLine();
|
|
2657
|
+
}
|
|
2658
|
+
main().catch((error) => {
|
|
2659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2660
|
+
console.error(c.red(`Fatal: ${message}`));
|
|
2661
|
+
process.exit(1);
|
|
2662
|
+
});
|