@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +2662 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/core/commentary.d.ts +69 -0
  8. package/dist/core/commentary.d.ts.map +1 -0
  9. package/dist/core/commentary.js +1041 -0
  10. package/dist/core/commentary.js.map +1 -0
  11. package/dist/core/parsers/claude.d.ts +3 -0
  12. package/dist/core/parsers/claude.d.ts.map +1 -0
  13. package/dist/core/parsers/claude.js +124 -0
  14. package/dist/core/parsers/claude.js.map +1 -0
  15. package/dist/core/parsers/codex.d.ts +3 -0
  16. package/dist/core/parsers/codex.d.ts.map +1 -0
  17. package/dist/core/parsers/codex.js +133 -0
  18. package/dist/core/parsers/codex.js.map +1 -0
  19. package/dist/core/platform-support.d.ts +17 -0
  20. package/dist/core/platform-support.d.ts.map +1 -0
  21. package/dist/core/platform-support.js +146 -0
  22. package/dist/core/platform-support.js.map +1 -0
  23. package/dist/core/providers/anthropic.d.ts +15 -0
  24. package/dist/core/providers/anthropic.d.ts.map +1 -0
  25. package/dist/core/providers/anthropic.js +236 -0
  26. package/dist/core/providers/anthropic.js.map +1 -0
  27. package/dist/core/providers/openai.d.ts +16 -0
  28. package/dist/core/providers/openai.d.ts.map +1 -0
  29. package/dist/core/providers/openai.js +154 -0
  30. package/dist/core/providers/openai.js.map +1 -0
  31. package/dist/core/session-dispatch.d.ts +28 -0
  32. package/dist/core/session-dispatch.d.ts.map +1 -0
  33. package/dist/core/session-dispatch.js +453 -0
  34. package/dist/core/session-dispatch.js.map +1 -0
  35. package/dist/core/types.d.ts +78 -0
  36. package/dist/core/types.d.ts.map +1 -0
  37. package/dist/core/types.js +22 -0
  38. package/dist/core/types.js.map +1 -0
  39. package/dist/core/watcher.d.ts +23 -0
  40. package/dist/core/watcher.d.ts.map +1 -0
  41. package/dist/core/watcher.js +866 -0
  42. package/dist/core/watcher.js.map +1 -0
  43. package/package.json +74 -0
@@ -0,0 +1,866 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/core/watcher.ts
31
+ var watcher_exports = {};
32
+ __export(watcher_exports, {
33
+ SessionWatcher: () => SessionWatcher
34
+ });
35
+ module.exports = __toCommonJS(watcher_exports);
36
+ var fs = __toESM(require("fs"));
37
+ var path = __toESM(require("path"));
38
+ var os = __toESM(require("os"));
39
+ var import_events = require("events");
40
+
41
+ // src/core/parsers/claude.ts
42
+ function summarizeToolUse(name, input) {
43
+ switch (name) {
44
+ case "Bash":
45
+ return `Running: ${truncate(String(input.command || ""), 80)}`;
46
+ case "Read":
47
+ return `Reading ${shortPath(String(input.file_path || ""))}`;
48
+ case "Write":
49
+ return `Writing ${shortPath(String(input.file_path || ""))}`;
50
+ case "Edit":
51
+ return `Editing ${shortPath(String(input.file_path || ""))}`;
52
+ case "Grep":
53
+ return `Searching for "${truncate(String(input.pattern || ""), 40)}"`;
54
+ case "Glob":
55
+ return `Finding files: ${truncate(String(input.pattern || ""), 40)}`;
56
+ case "Task":
57
+ return `Spawning agent: ${truncate(String(input.description || ""), 60)}`;
58
+ case "TodoWrite":
59
+ return "Updating task list";
60
+ case "WebSearch":
61
+ return `Web search: "${truncate(String(input.query || ""), 50)}"`;
62
+ case "WebFetch":
63
+ return `Fetching: ${truncate(String(input.url || ""), 60)}`;
64
+ default:
65
+ return `Using tool: ${name}`;
66
+ }
67
+ }
68
+ function shortPath(p) {
69
+ const parts = String(p || "").split(/[\\/]+/).filter(Boolean);
70
+ if (parts.length <= 3) return String(p || "");
71
+ return ".../" + parts.slice(-2).join("/");
72
+ }
73
+ function truncate(s, max) {
74
+ return s.length > max ? s.slice(0, max) + "..." : s;
75
+ }
76
+ function projectNameFromCwd(cwd) {
77
+ const parts = String(cwd || "").split(/[\\/]+/).filter(Boolean);
78
+ return parts[parts.length - 1] || "unknown";
79
+ }
80
+ function parseClaudeLine(line, filePath) {
81
+ let obj;
82
+ try {
83
+ obj = JSON.parse(line);
84
+ } catch {
85
+ return [];
86
+ }
87
+ const sessionId = obj.sessionId || sessionIdFromFilePath(filePath);
88
+ const projectName = obj.cwd ? projectNameFromCwd(obj.cwd) : projectFromFilePath(filePath);
89
+ const timestamp = obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now();
90
+ if (obj.type === "queue-operation" || obj.type === "file-history-snapshot" || obj.type === "progress") {
91
+ return [];
92
+ }
93
+ const events = [];
94
+ if (obj.type === "assistant" && obj.message?.content) {
95
+ for (const block of obj.message.content) {
96
+ if (block.type === "tool_use" && block.name && block.input) {
97
+ events.push({
98
+ sessionId,
99
+ projectName,
100
+ agent: "claude",
101
+ source: "cli",
102
+ // will be overridden by watcher
103
+ timestamp,
104
+ type: "tool_call",
105
+ summary: summarizeToolUse(block.name, block.input),
106
+ details: { tool: block.name, input: block.input }
107
+ });
108
+ } else if (block.type === "text" && block.text) {
109
+ const text = block.text.trim();
110
+ if (text.length > 0) {
111
+ events.push({
112
+ sessionId,
113
+ projectName,
114
+ agent: "claude",
115
+ source: "cli",
116
+ timestamp,
117
+ type: "message",
118
+ summary: truncate(text, 120),
119
+ details: { text }
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ if (obj.type === "user" && obj.message?.content) {
126
+ for (const block of obj.message.content) {
127
+ if (block.type === "tool_result") {
128
+ events.push({
129
+ sessionId,
130
+ projectName,
131
+ agent: "claude",
132
+ source: "cli",
133
+ timestamp,
134
+ type: "tool_result",
135
+ summary: "Tool completed",
136
+ details: { tool_use_id: block.tool_use_id }
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return events;
142
+ }
143
+ function projectFromFilePath(filePath) {
144
+ const segments = String(filePath || "").split(/[\\/]+/).filter(Boolean);
145
+ const projectDir = segments.length >= 2 ? segments[segments.length - 2] : "";
146
+ const projectSegments = projectDir.split("-").filter(Boolean);
147
+ return projectSegments[projectSegments.length - 1] || "unknown";
148
+ }
149
+ function sessionIdFromFilePath(filePath) {
150
+ return String(filePath || "").split(/[\\/]+/).filter(Boolean).pop()?.replace(/\.jsonl$/i, "") || "";
151
+ }
152
+
153
+ // src/core/parsers/codex.ts
154
+ function truncate2(s, max) {
155
+ return s.length > max ? s.slice(0, max) + "..." : s;
156
+ }
157
+ function projectNameFromCwd2(cwd) {
158
+ const parts = String(cwd || "").split(/[\\/]+/).filter(Boolean);
159
+ return parts[parts.length - 1] || "unknown";
160
+ }
161
+ function parseCodexLine(line, filePath) {
162
+ let obj;
163
+ try {
164
+ obj = JSON.parse(line);
165
+ } catch {
166
+ return [];
167
+ }
168
+ const timestamp = obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now();
169
+ const sessionId = sessionIdFromFilePath2(filePath);
170
+ const events = [];
171
+ if (obj.type === "session_meta" && obj.payload) {
172
+ const cwd = obj.payload.cwd || "";
173
+ events.push({
174
+ sessionId,
175
+ projectName: projectNameFromCwd2(cwd),
176
+ agent: "codex",
177
+ source: "cli",
178
+ timestamp,
179
+ type: "meta",
180
+ summary: `Codex session started (${obj.payload.model_provider || "unknown"}, v${obj.payload.cli_version || "?"})`,
181
+ details: { cwd, provider: obj.payload.model_provider, version: obj.payload.cli_version }
182
+ });
183
+ }
184
+ if (obj.type === "response_item" && obj.payload) {
185
+ const p = obj.payload;
186
+ if (p.type === "function_call" && p.name) {
187
+ let args = {};
188
+ try {
189
+ args = JSON.parse(p.arguments || "{}");
190
+ } catch {
191
+ }
192
+ const summary = summarizeCodexTool(p.name, args);
193
+ events.push({
194
+ sessionId,
195
+ projectName: projectFromFilePath2(filePath),
196
+ agent: "codex",
197
+ source: "cli",
198
+ timestamp,
199
+ type: "tool_call",
200
+ summary,
201
+ details: { tool: p.name, input: args }
202
+ });
203
+ }
204
+ if (p.type === "function_call_output") {
205
+ events.push({
206
+ sessionId,
207
+ projectName: projectFromFilePath2(filePath),
208
+ agent: "codex",
209
+ source: "cli",
210
+ timestamp,
211
+ type: "tool_result",
212
+ summary: `Tool completed: ${truncate2(p.output || "", 60)}`,
213
+ details: { output: p.output, call_id: p.call_id }
214
+ });
215
+ }
216
+ if (p.role === "assistant" && p.content) {
217
+ for (const block of p.content) {
218
+ const text = block.text || block.input_text || "";
219
+ if (text.trim()) {
220
+ events.push({
221
+ sessionId,
222
+ projectName: projectFromFilePath2(filePath),
223
+ agent: "codex",
224
+ source: "cli",
225
+ timestamp,
226
+ type: "message",
227
+ summary: truncate2(text.trim(), 120),
228
+ details: { text }
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ if (obj.type === "event_msg" && obj.payload) {
235
+ if (obj.payload.type === "task_started") {
236
+ events.push({
237
+ sessionId,
238
+ projectName: projectFromFilePath2(filePath),
239
+ agent: "codex",
240
+ source: "cli",
241
+ timestamp,
242
+ type: "meta",
243
+ summary: "Codex task started",
244
+ details: { turn_id: obj.payload.turn_id }
245
+ });
246
+ }
247
+ }
248
+ return events;
249
+ }
250
+ function summarizeCodexTool(name, args) {
251
+ if (name === "shell" || name === "run_command") {
252
+ return `Running: ${truncate2(String(args.command || args.cmd || ""), 80)}`;
253
+ }
254
+ if (name === "read_file" || name === "file_read") {
255
+ return `Reading ${truncate2(String(args.path || args.file_path || ""), 60)}`;
256
+ }
257
+ if (name === "write_file" || name === "file_write") {
258
+ return `Writing ${truncate2(String(args.path || args.file_path || ""), 60)}`;
259
+ }
260
+ if (name === "edit_file" || name === "apply_diff") {
261
+ return `Editing ${truncate2(String(args.path || args.file_path || ""), 60)}`;
262
+ }
263
+ return `Using tool: ${name}`;
264
+ }
265
+ function sessionIdFromFilePath2(filePath) {
266
+ const basename2 = String(filePath || "").split(/[\\/]+/).filter(Boolean).pop()?.replace(/\.jsonl$/i, "") || "";
267
+ const match = basename2.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
268
+ return match ? match[1] : basename2;
269
+ }
270
+ function projectFromFilePath2(filePath) {
271
+ return "codex";
272
+ }
273
+
274
+ // src/core/watcher.ts
275
+ var SessionWatcher = class _SessionWatcher extends import_events.EventEmitter {
276
+ watched = /* @__PURE__ */ new Map();
277
+ scanInterval = null;
278
+ claudeIdeLocks = /* @__PURE__ */ new Map();
279
+ sessionProjectNames = /* @__PURE__ */ new Map();
280
+ // sessionId → projectName from meta
281
+ static ACTIVE_SESSION_WINDOW_MS = 5 * 60 * 1e3;
282
+ constructor() {
283
+ super();
284
+ }
285
+ start() {
286
+ this.scan();
287
+ this.scanInterval = setInterval(() => this.scan(), 15e3);
288
+ }
289
+ stop() {
290
+ if (this.scanInterval) {
291
+ clearInterval(this.scanInterval);
292
+ this.scanInterval = null;
293
+ }
294
+ for (const w of this.watched.values()) {
295
+ w.watcher?.close();
296
+ }
297
+ this.watched.clear();
298
+ }
299
+ getActiveSessions() {
300
+ const now = Date.now();
301
+ const sessionsByKey = /* @__PURE__ */ new Map();
302
+ for (const w of this.watched.values()) {
303
+ if (w.ignore) continue;
304
+ this.reconcileCodexSessionTitle(w);
305
+ try {
306
+ const stat = fs.statSync(w.filePath);
307
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
308
+ const session = {
309
+ id: w.sessionId,
310
+ projectName: w.projectName,
311
+ sessionTitle: w.sessionTitle,
312
+ agent: w.agent,
313
+ source: this.detectSource(w),
314
+ filePath: w.filePath,
315
+ lastActivity: stat.mtimeMs
316
+ };
317
+ const key = `${session.agent}:${session.id.toLowerCase()}`;
318
+ const existing = sessionsByKey.get(key);
319
+ if (!existing || existing.lastActivity < session.lastActivity) {
320
+ sessionsByKey.set(key, session);
321
+ }
322
+ } catch {
323
+ }
324
+ }
325
+ return Array.from(sessionsByKey.values()).sort((a, b) => b.lastActivity - a.lastActivity);
326
+ }
327
+ scan() {
328
+ this.loadIdeLocks();
329
+ this.scanClaude();
330
+ this.scanCodex();
331
+ this.pruneStale();
332
+ }
333
+ scanClaude() {
334
+ const claudeDir = path.join(os.homedir(), ".claude", "projects");
335
+ if (!fs.existsSync(claudeDir)) return;
336
+ let projectDirs;
337
+ try {
338
+ projectDirs = fs.readdirSync(claudeDir).filter((d) => {
339
+ const full = path.join(claudeDir, d);
340
+ try {
341
+ return fs.statSync(full).isDirectory();
342
+ } catch {
343
+ return false;
344
+ }
345
+ });
346
+ } catch {
347
+ return;
348
+ }
349
+ const now = Date.now();
350
+ for (const dir of projectDirs) {
351
+ const dirPath = path.join(claudeDir, dir);
352
+ let files;
353
+ try {
354
+ files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
355
+ } catch {
356
+ continue;
357
+ }
358
+ for (const file of files) {
359
+ const filePath = path.join(dirPath, file);
360
+ try {
361
+ const stat = fs.statSync(filePath);
362
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
363
+ } catch {
364
+ continue;
365
+ }
366
+ if (!this.watched.has(filePath)) {
367
+ const projectName = extractProjectName(dir);
368
+ this.watchFile(filePath, "claude", projectName);
369
+ }
370
+ }
371
+ }
372
+ }
373
+ scanCodex() {
374
+ const currentTime = Date.now();
375
+ for (const sessionsDir of codexSessionDirs(2)) {
376
+ if (!fs.existsSync(sessionsDir)) continue;
377
+ let files;
378
+ try {
379
+ files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
380
+ } catch {
381
+ continue;
382
+ }
383
+ for (const file of files) {
384
+ const filePath = path.join(sessionsDir, file);
385
+ try {
386
+ const stat = fs.statSync(filePath);
387
+ if (currentTime - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
388
+ } catch {
389
+ continue;
390
+ }
391
+ if (!this.watched.has(filePath)) {
392
+ this.watchFile(filePath, "codex", "codex");
393
+ }
394
+ }
395
+ }
396
+ }
397
+ watchFile(filePath, agent, projectName) {
398
+ let offset;
399
+ try {
400
+ const stat = fs.statSync(filePath);
401
+ offset = stat.size;
402
+ } catch {
403
+ return;
404
+ }
405
+ let resolvedProjectName = projectName;
406
+ if (agent === "codex") {
407
+ const codexProject = extractCodexProjectName(filePath);
408
+ if (codexProject) resolvedProjectName = codexProject;
409
+ }
410
+ const sessionId = extractSessionIdFromLog(
411
+ filePath,
412
+ agent,
413
+ deriveSessionId(filePath, agent)
414
+ );
415
+ const ignore = agent === "codex" && isKibitzInternalCodexSession(filePath);
416
+ const sessionTitle = extractSessionTitle(filePath, agent, sessionId);
417
+ const entry = {
418
+ filePath,
419
+ sessionId,
420
+ offset,
421
+ agent,
422
+ ignore,
423
+ watcher: null,
424
+ projectName: resolvedProjectName,
425
+ sessionTitle
426
+ };
427
+ try {
428
+ entry.watcher = fs.watch(filePath, () => {
429
+ this.readNewLines(entry);
430
+ });
431
+ } catch {
432
+ }
433
+ this.watched.set(filePath, entry);
434
+ }
435
+ readNewLines(entry) {
436
+ if (entry.ignore) {
437
+ try {
438
+ const stat2 = fs.statSync(entry.filePath);
439
+ entry.offset = stat2.size;
440
+ } catch {
441
+ }
442
+ return;
443
+ }
444
+ let stat;
445
+ try {
446
+ stat = fs.statSync(entry.filePath);
447
+ } catch {
448
+ return;
449
+ }
450
+ this.reconcileCodexSessionTitle(entry);
451
+ if (stat.size <= entry.offset) return;
452
+ const fd = fs.openSync(entry.filePath, "r");
453
+ const buf = Buffer.alloc(stat.size - entry.offset);
454
+ fs.readSync(fd, buf, 0, buf.length, entry.offset);
455
+ fs.closeSync(fd);
456
+ entry.offset = stat.size;
457
+ const chunk = buf.toString("utf8");
458
+ const lines = chunk.split("\n").filter((l) => l.trim());
459
+ for (const line of lines) {
460
+ if (entry.agent === "codex" && isKibitzInternalCodexLine(line)) {
461
+ entry.ignore = true;
462
+ break;
463
+ }
464
+ if (!entry.sessionTitle) {
465
+ if (entry.agent === "codex") {
466
+ const title = extractCodexSessionTitle(entry.filePath, entry.sessionId);
467
+ if (title) entry.sessionTitle = title;
468
+ }
469
+ }
470
+ if (!entry.sessionTitle) {
471
+ const title = extractSessionTitleFromLine(line, entry.agent);
472
+ if (title) entry.sessionTitle = title;
473
+ }
474
+ let events;
475
+ if (entry.agent === "claude") {
476
+ events = parseClaudeLine(line, entry.filePath);
477
+ } else {
478
+ events = parseCodexLine(line, entry.filePath);
479
+ }
480
+ for (const event of events) {
481
+ const normalizedEventSessionId = normalizeSessionId(event.sessionId, entry.agent);
482
+ if (normalizedEventSessionId && normalizedEventSessionId !== entry.sessionId) {
483
+ entry.sessionId = normalizedEventSessionId;
484
+ }
485
+ event.sessionId = entry.sessionId;
486
+ event.source = this.detectSource(entry);
487
+ event.sessionTitle = entry.sessionTitle || fallbackSessionTitle(entry.projectName, entry.agent);
488
+ if (event.type === "meta" && event.details.cwd) {
489
+ const cwd = String(event.details.cwd);
490
+ const name = cwd.split("/").pop() || cwd.split("\\").pop() || "unknown";
491
+ this.sessionProjectNames.set(event.sessionId, name);
492
+ entry.projectName = name;
493
+ event.projectName = name;
494
+ } else if (this.sessionProjectNames.has(event.sessionId)) {
495
+ event.projectName = this.sessionProjectNames.get(event.sessionId);
496
+ entry.projectName = event.projectName;
497
+ }
498
+ this.emit("event", event);
499
+ }
500
+ }
501
+ }
502
+ reconcileCodexSessionTitle(entry) {
503
+ if (entry.agent !== "codex") return;
504
+ const threadTitle = getCodexThreadTitle(entry.sessionId);
505
+ if (threadTitle) {
506
+ if (threadTitle !== entry.sessionTitle) entry.sessionTitle = threadTitle;
507
+ return;
508
+ }
509
+ if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
510
+ entry.sessionTitle = void 0;
511
+ }
512
+ }
513
+ loadIdeLocks() {
514
+ const ideDir = path.join(os.homedir(), ".claude", "ide");
515
+ if (!fs.existsSync(ideDir)) return;
516
+ this.claudeIdeLocks.clear();
517
+ try {
518
+ const files = fs.readdirSync(ideDir).filter((f) => f.endsWith(".lock"));
519
+ for (const file of files) {
520
+ try {
521
+ const content = fs.readFileSync(path.join(ideDir, file), "utf8");
522
+ const lock = JSON.parse(content);
523
+ this.claudeIdeLocks.set(file, {
524
+ pid: lock.pid,
525
+ workspaceFolders: lock.workspaceFolders || []
526
+ });
527
+ } catch {
528
+ }
529
+ }
530
+ } catch {
531
+ }
532
+ }
533
+ detectSource(entry) {
534
+ if (entry.agent === "codex") return "cli";
535
+ return this.claudeIdeLocks.size > 0 ? "vscode" : "cli";
536
+ }
537
+ pruneStale() {
538
+ const now = Date.now();
539
+ for (const [filePath, entry] of this.watched) {
540
+ try {
541
+ const stat = fs.statSync(filePath);
542
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) {
543
+ entry.watcher?.close();
544
+ this.watched.delete(filePath);
545
+ }
546
+ } catch {
547
+ entry.watcher?.close();
548
+ this.watched.delete(filePath);
549
+ }
550
+ }
551
+ }
552
+ };
553
+ function extractSessionTitle(filePath, agent, sessionId) {
554
+ return agent === "claude" ? extractClaudeSessionTitle(filePath) : extractCodexSessionTitle(filePath, sessionId);
555
+ }
556
+ function extractSessionTitleFromLine(line, agent) {
557
+ try {
558
+ const obj = JSON.parse(line);
559
+ if (agent === "claude") {
560
+ if (obj.type !== "user") return void 0;
561
+ const content = obj.message?.content;
562
+ if (typeof content === "string") return pickSessionTitle(content);
563
+ if (Array.isArray(content)) {
564
+ for (const block of content) {
565
+ if (typeof block?.text === "string") {
566
+ const title = pickSessionTitle(block.text);
567
+ if (title) return title;
568
+ }
569
+ }
570
+ }
571
+ return void 0;
572
+ }
573
+ if (obj.type === "session_meta") {
574
+ const explicitTitle = pickSessionTitle(String(
575
+ obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
576
+ ));
577
+ if (explicitTitle) return explicitTitle;
578
+ }
579
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
580
+ return pickSessionTitle(String(obj.payload.message || ""));
581
+ }
582
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user") {
583
+ const contentBlocks = obj.payload.content;
584
+ if (Array.isArray(contentBlocks)) {
585
+ for (const block of contentBlocks) {
586
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
587
+ const title = pickSessionTitle(text);
588
+ if (title) return title;
589
+ }
590
+ }
591
+ }
592
+ } catch {
593
+ }
594
+ return void 0;
595
+ }
596
+ function extractClaudeSessionTitle(filePath) {
597
+ try {
598
+ const content = fs.readFileSync(filePath, "utf8");
599
+ for (const line of content.split("\n")) {
600
+ if (!line.trim()) continue;
601
+ try {
602
+ const obj = JSON.parse(line);
603
+ if (obj.type === "user") {
604
+ const msg = obj.message;
605
+ if (!msg) continue;
606
+ const content2 = msg.content;
607
+ if (typeof content2 === "string" && content2.trim()) {
608
+ const title = pickSessionTitle(content2);
609
+ if (title) return title;
610
+ }
611
+ if (Array.isArray(content2)) {
612
+ for (const block of content2) {
613
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
614
+ const title = pickSessionTitle(block.text);
615
+ if (title) return title;
616
+ }
617
+ }
618
+ }
619
+ }
620
+ } catch {
621
+ }
622
+ }
623
+ } catch {
624
+ }
625
+ return void 0;
626
+ }
627
+ function extractCodexSessionTitle(filePath, sessionId) {
628
+ const codexSessionId = sessionId || deriveSessionId(filePath, "codex");
629
+ const explicitThreadTitle = getCodexThreadTitle(codexSessionId);
630
+ if (explicitThreadTitle) return explicitThreadTitle;
631
+ const logDerivedTitle = extractCodexSessionTitleFromLog(filePath);
632
+ return logDerivedTitle || void 0;
633
+ }
634
+ function getCodexThreadTitle(sessionId) {
635
+ const normalizedSessionId = String(sessionId || "").trim().toLowerCase();
636
+ if (!normalizedSessionId) return void 0;
637
+ return readCodexThreadTitles().titles.get(normalizedSessionId);
638
+ }
639
+ function extractCodexSessionTitleFromLog(filePath) {
640
+ try {
641
+ const content = fs.readFileSync(filePath, "utf8");
642
+ for (const line of content.split("\n")) {
643
+ if (!line.trim()) continue;
644
+ try {
645
+ const obj = JSON.parse(line);
646
+ if (obj.type !== "session_meta") continue;
647
+ const explicitTitle = pickSessionTitle(String(
648
+ obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
649
+ ));
650
+ if (explicitTitle) return explicitTitle;
651
+ } catch {
652
+ }
653
+ }
654
+ } catch {
655
+ }
656
+ return void 0;
657
+ }
658
+ function extractCodexProjectName(filePath) {
659
+ try {
660
+ const content = fs.readFileSync(filePath, "utf8");
661
+ for (const line of content.split("\n")) {
662
+ if (!line.trim()) continue;
663
+ try {
664
+ const obj = JSON.parse(line);
665
+ if (obj.type !== "session_meta") continue;
666
+ const cwd = typeof obj.payload?.cwd === "string" ? obj.payload.cwd : "";
667
+ if (!cwd) continue;
668
+ const name = cwd.split("/").pop() || cwd.split("\\").pop();
669
+ if (name && name.trim()) return name.trim();
670
+ } catch {
671
+ }
672
+ }
673
+ } catch {
674
+ }
675
+ return void 0;
676
+ }
677
+ function isKibitzInternalCodexSession(filePath) {
678
+ try {
679
+ const content = fs.readFileSync(filePath, "utf8");
680
+ const lines = content.split("\n");
681
+ const maxProbeLines = 50;
682
+ for (let i = 0; i < lines.length && i < maxProbeLines; i++) {
683
+ const line = lines[i];
684
+ if (!line.trim()) continue;
685
+ try {
686
+ const obj = JSON.parse(line);
687
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
688
+ const msg = String(obj.payload?.message || "").toLowerCase();
689
+ if (looksLikeKibitzGeneratedPrompt(msg)) return true;
690
+ }
691
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
692
+ for (const block of obj.payload.content) {
693
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
694
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
695
+ }
696
+ }
697
+ } catch {
698
+ }
699
+ }
700
+ } catch {
701
+ }
702
+ return false;
703
+ }
704
+ function isKibitzInternalCodexLine(line) {
705
+ try {
706
+ const obj = JSON.parse(line);
707
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
708
+ const msg = String(obj.payload?.message || "").toLowerCase();
709
+ return looksLikeKibitzGeneratedPrompt(msg);
710
+ }
711
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
712
+ for (const block of obj.payload.content) {
713
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
714
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
715
+ }
716
+ }
717
+ } catch {
718
+ }
719
+ return false;
720
+ }
721
+ function looksLikeKibitzGeneratedPrompt(text) {
722
+ if (!text) return false;
723
+ return text.includes("you oversee ai coding agents. summarize what they did") && text.includes("format for this message:") && text.includes("tone preset:");
724
+ }
725
+ var codexThreadTitlesCache = {
726
+ mtimeMs: -1,
727
+ titles: /* @__PURE__ */ new Map(),
728
+ order: []
729
+ };
730
+ function readCodexThreadTitles() {
731
+ const statePath = path.join(os.homedir(), ".codex", ".codex-global-state.json");
732
+ try {
733
+ const stat = fs.statSync(statePath);
734
+ if (stat.mtimeMs === codexThreadTitlesCache.mtimeMs) return codexThreadTitlesCache;
735
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
736
+ const rawTitles = raw?.["thread-titles"]?.titles;
737
+ const rawOrder = raw?.["thread-titles"]?.order;
738
+ const titles = /* @__PURE__ */ new Map();
739
+ if (rawTitles && typeof rawTitles === "object" && !Array.isArray(rawTitles)) {
740
+ for (const [key, value] of Object.entries(rawTitles)) {
741
+ const sessionId = String(key || "").trim().toLowerCase();
742
+ if (!sessionId) continue;
743
+ const title = pickSessionTitle(String(value || ""));
744
+ if (title) titles.set(sessionId, title);
745
+ }
746
+ }
747
+ const order = Array.isArray(rawOrder) ? rawOrder.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean) : [];
748
+ codexThreadTitlesCache.mtimeMs = stat.mtimeMs;
749
+ codexThreadTitlesCache.titles = titles;
750
+ codexThreadTitlesCache.order = order;
751
+ } catch {
752
+ }
753
+ return codexThreadTitlesCache;
754
+ }
755
+ function deriveSessionId(filePath, agent) {
756
+ const basename2 = path.basename(filePath, ".jsonl");
757
+ if (agent !== "codex") return basename2;
758
+ const match = basename2.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/i);
759
+ return match ? match[1].toLowerCase() : basename2.toLowerCase();
760
+ }
761
+ function normalizeSessionId(rawSessionId, agent) {
762
+ const value = String(rawSessionId || "").trim();
763
+ if (!value) return "";
764
+ if (agent === "codex") return value.toLowerCase();
765
+ return value;
766
+ }
767
+ function extractSessionIdFromLog(filePath, agent, fallback) {
768
+ const normalizedFallback = normalizeSessionId(fallback, agent);
769
+ if (agent !== "claude") return normalizedFallback;
770
+ try {
771
+ const stat = fs.statSync(filePath);
772
+ const length = Math.min(stat.size, 512 * 1024);
773
+ if (length <= 0) return normalizedFallback;
774
+ const fd = fs.openSync(filePath, "r");
775
+ try {
776
+ const buf = Buffer.alloc(length);
777
+ const offset = Math.max(0, stat.size - length);
778
+ fs.readSync(fd, buf, 0, length, offset);
779
+ const lines = buf.toString("utf8").split("\n").filter((line) => line.trim());
780
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
781
+ try {
782
+ const obj = JSON.parse(lines[i]);
783
+ const fromLine = normalizeSessionId(obj?.sessionId, agent);
784
+ if (fromLine) return fromLine;
785
+ } catch {
786
+ }
787
+ }
788
+ } finally {
789
+ fs.closeSync(fd);
790
+ }
791
+ } catch {
792
+ }
793
+ return normalizedFallback;
794
+ }
795
+ function pickSessionTitle(raw) {
796
+ if (!raw.trim()) return void 0;
797
+ if (looksLikeKibitzGeneratedPrompt(raw.toLowerCase())) return void 0;
798
+ for (const line of raw.split("\n")) {
799
+ const trimmed = line.trim();
800
+ if (!trimmed) continue;
801
+ const cleaned = trimmed.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
802
+ if (!cleaned || isNoiseSessionTitle(cleaned)) continue;
803
+ return cleaned;
804
+ }
805
+ return void 0;
806
+ }
807
+ function isNoiseSessionTitle(text) {
808
+ const lower = text.toLowerCase();
809
+ const normalized = lower.replace(/^[#>*\-\s]+/, "").trim();
810
+ if (normalized.length < 8) return true;
811
+ if (looksLikeSessionIdentifier(normalized)) return true;
812
+ if (/^[a-z0-9+/_=-]{64,}$/.test(normalized)) return true;
813
+ if (normalized.startsWith("the user opened the file ") || normalized.includes("may or may not be related to the current task")) return true;
814
+ if (/^[\w][\w.\-]*\.[a-z]{2,6}:\s/i.test(normalized)) return true;
815
+ 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;
816
+ 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;
817
+ 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;
818
+ if (normalized.startsWith("cwd:") || normalized.startsWith("shell:")) return true;
819
+ if (normalized.startsWith("your identity") || normalized.startsWith("room id:") || normalized.startsWith("worker id:")) return true;
820
+ if (normalized.startsWith("you are codex, a coding agent")) return true;
821
+ return false;
822
+ }
823
+ function looksLikeSessionIdentifier(text) {
824
+ if (/^[0-9a-f]{8}$/.test(text)) return true;
825
+ if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(text)) return true;
826
+ if (/^session[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
827
+ if (/^turn[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
828
+ if (/^rollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?$/.test(text)) return true;
829
+ return false;
830
+ }
831
+ function extractProjectName(dirName) {
832
+ const parts = dirName.split("-").filter(Boolean);
833
+ return parts[parts.length - 1] || "unknown";
834
+ }
835
+ function codexSessionDirs(daysBack) {
836
+ const home = os.homedir();
837
+ const results = [];
838
+ const seen = /* @__PURE__ */ new Set();
839
+ const now = /* @__PURE__ */ new Date();
840
+ const totalDays = Math.max(1, daysBack);
841
+ for (let offset = 0; offset < totalDays; offset++) {
842
+ const d = new Date(now);
843
+ d.setDate(now.getDate() - offset);
844
+ const dir = path.join(
845
+ home,
846
+ ".codex",
847
+ "sessions",
848
+ String(d.getFullYear()),
849
+ String(d.getMonth() + 1).padStart(2, "0"),
850
+ String(d.getDate()).padStart(2, "0")
851
+ );
852
+ if (seen.has(dir)) continue;
853
+ seen.add(dir);
854
+ results.push(dir);
855
+ }
856
+ return results;
857
+ }
858
+ function fallbackSessionTitle(projectName, agent) {
859
+ const project = (projectName || "").trim();
860
+ if (project) return project;
861
+ return `${agent} session`;
862
+ }
863
+ // Annotate the CommonJS export names for ESM import in node:
864
+ 0 && (module.exports = {
865
+ SessionWatcher
866
+ });