@kibitzsh/kibitz 0.0.3 → 0.0.4

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.
@@ -1,866 +1,905 @@
1
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
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
34
17
  });
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
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
316
24
  };
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);
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SessionWatcher = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const events_1 = require("events");
41
+ const claude_1 = require("./parsers/claude");
42
+ const codex_1 = require("./parsers/codex");
43
+ class SessionWatcher extends events_1.EventEmitter {
44
+ watched = new Map();
45
+ scanInterval = null;
46
+ claudeIdeLocks = new Map();
47
+ sessionProjectNames = new Map(); // sessionId → projectName from meta
48
+ static ACTIVE_SESSION_WINDOW_MS = 5 * 60 * 1000;
49
+ constructor() {
50
+ super();
51
+ }
52
+ start() {
53
+ this.scan();
54
+ this.scanInterval = setInterval(() => this.scan(), 15_000);
55
+ }
56
+ stop() {
57
+ if (this.scanInterval) {
58
+ clearInterval(this.scanInterval);
59
+ this.scanInterval = null;
321
60
  }
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);
61
+ for (const w of this.watched.values()) {
62
+ w.watcher?.close();
63
+ }
64
+ this.watched.clear();
65
+ }
66
+ getActiveSessions() {
67
+ const now = Date.now();
68
+ const sessionsByKey = new Map();
69
+ for (const w of this.watched.values()) {
70
+ if (w.ignore)
71
+ continue;
72
+ this.reconcileSessionTitle(w);
73
+ if (w.ignore)
74
+ continue;
75
+ try {
76
+ const stat = fs.statSync(w.filePath);
77
+ if (now - stat.mtimeMs > SessionWatcher.ACTIVE_SESSION_WINDOW_MS)
78
+ continue;
79
+ const session = {
80
+ id: w.sessionId,
81
+ projectName: w.projectName,
82
+ sessionTitle: w.sessionTitle,
83
+ agent: w.agent,
84
+ source: this.detectSource(w),
85
+ filePath: w.filePath,
86
+ lastActivity: stat.mtimeMs,
87
+ };
88
+ const key = `${session.agent}:${session.id.toLowerCase()}`;
89
+ const existing = sessionsByKey.get(key);
90
+ if (!existing || existing.lastActivity < session.lastActivity) {
91
+ sessionsByKey.set(key, session);
92
+ }
93
+ }
94
+ catch { /* file gone */ }
95
+ }
96
+ return Array.from(sessionsByKey.values()).sort((a, b) => b.lastActivity - a.lastActivity);
97
+ }
98
+ scan() {
99
+ this.loadIdeLocks();
100
+ this.scanClaude();
101
+ this.scanCodex();
102
+ this.pruneStale();
103
+ }
104
+ scanClaude() {
105
+ const claudeDir = path.join(os.homedir(), '.claude', 'projects');
106
+ if (!fs.existsSync(claudeDir))
107
+ return;
108
+ let projectDirs;
340
109
  try {
341
- return fs.statSync(full).isDirectory();
342
- } catch {
343
- return false;
110
+ projectDirs = fs.readdirSync(claudeDir).filter(d => {
111
+ const full = path.join(claudeDir, d);
112
+ try {
113
+ return fs.statSync(full).isDirectory();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ });
119
+ }
120
+ catch {
121
+ return;
122
+ }
123
+ const now = Date.now();
124
+ for (const dir of projectDirs) {
125
+ // Skip the dedicated dir Kibitz uses for its own claude -p commentary sessions
126
+ // (~/.kibitz-sessions → encodes as ...-.kibitz-sessions in ~/.claude/projects/).
127
+ if (dir.endsWith('-.kibitz-sessions'))
128
+ continue;
129
+ const dirPath = path.join(claudeDir, dir);
130
+ let files;
131
+ try {
132
+ files = fs.readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
133
+ }
134
+ catch {
135
+ continue;
136
+ }
137
+ for (const file of files) {
138
+ const filePath = path.join(dirPath, file);
139
+ try {
140
+ const stat = fs.statSync(filePath);
141
+ if (now - stat.mtimeMs > SessionWatcher.ACTIVE_SESSION_WINDOW_MS)
142
+ continue; // skip stale
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ if (!this.watched.has(filePath)) {
148
+ const projectName = extractProjectName(dir);
149
+ this.watchFile(filePath, 'claude', projectName);
150
+ }
151
+ }
344
152
  }
345
- });
346
- } catch {
347
- return;
348
153
  }
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);
154
+ scanCodex() {
155
+ const currentTime = Date.now();
156
+ for (const sessionsDir of codexSessionDirs(2)) {
157
+ if (!fs.existsSync(sessionsDir))
158
+ continue;
159
+ let files;
160
+ try {
161
+ files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
162
+ }
163
+ catch {
164
+ continue;
165
+ }
166
+ for (const file of files) {
167
+ const filePath = path.join(sessionsDir, file);
168
+ try {
169
+ const stat = fs.statSync(filePath);
170
+ if (currentTime - stat.mtimeMs > SessionWatcher.ACTIVE_SESSION_WINDOW_MS)
171
+ continue;
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ if (!this.watched.has(filePath)) {
177
+ this.watchFile(filePath, 'codex', 'codex');
178
+ }
179
+ }
180
+ }
181
+ }
182
+ watchFile(filePath, agent, projectName) {
183
+ let offset;
360
184
  try {
361
- const stat = fs.statSync(filePath);
362
- if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
363
- } catch {
364
- continue;
185
+ const stat = fs.statSync(filePath);
186
+ offset = stat.size; // start from current end, only read new content
365
187
  }
366
- if (!this.watched.has(filePath)) {
367
- const projectName = extractProjectName(dir);
368
- this.watchFile(filePath, "claude", projectName);
188
+ catch {
189
+ return;
369
190
  }
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);
191
+ let resolvedProjectName = projectName;
192
+ if (agent === 'codex') {
193
+ const codexProject = extractCodexProjectName(filePath);
194
+ if (codexProject)
195
+ resolvedProjectName = codexProject;
196
+ }
197
+ const sessionId = extractSessionIdFromLog(filePath, agent, deriveSessionId(filePath, agent));
198
+ const ignore = (agent === 'codex' && isKibitzInternalCodexSession(filePath))
199
+ || (agent === 'claude' && isKibitzInternalClaudeSession(filePath));
200
+ const sessionTitle = extractSessionTitle(filePath, agent, sessionId);
201
+ const entry = {
202
+ filePath,
203
+ sessionId,
204
+ offset,
205
+ agent,
206
+ ignore,
207
+ watcher: null,
208
+ projectName: resolvedProjectName,
209
+ sessionTitle,
210
+ };
385
211
  try {
386
- const stat = fs.statSync(filePath);
387
- if (currentTime - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
388
- } catch {
389
- continue;
212
+ entry.watcher = fs.watch(filePath, () => {
213
+ this.readNewLines(entry);
214
+ });
390
215
  }
391
- if (!this.watched.has(filePath)) {
392
- this.watchFile(filePath, "codex", "codex");
216
+ catch {
217
+ // fs.watch failed, fall back to polling via scan interval
393
218
  }
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 {
219
+ this.watched.set(filePath, entry);
432
220
  }
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;
221
+ readNewLines(entry) {
222
+ if (entry.ignore) {
223
+ try {
224
+ const stat = fs.statSync(entry.filePath);
225
+ entry.offset = stat.size;
226
+ }
227
+ catch { /* file gone */ }
228
+ return;
229
+ }
230
+ let stat;
231
+ try {
232
+ stat = fs.statSync(entry.filePath);
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ this.reconcileSessionTitle(entry);
238
+ if (stat.size <= entry.offset)
239
+ return;
240
+ const fd = fs.openSync(entry.filePath, 'r');
241
+ const buf = Buffer.alloc(stat.size - entry.offset);
242
+ fs.readSync(fd, buf, 0, buf.length, entry.offset);
243
+ fs.closeSync(fd);
244
+ entry.offset = stat.size;
245
+ const chunk = buf.toString('utf8');
246
+ const lines = chunk.split('\n').filter(l => l.trim());
247
+ for (const line of lines) {
248
+ if (entry.agent === 'codex' && isKibitzInternalCodexLine(line)) {
249
+ entry.ignore = true;
250
+ break;
251
+ }
252
+ if (entry.agent === 'claude' && isKibitzInternalClaudeUserLine(line)) {
253
+ entry.ignore = true;
254
+ break;
255
+ }
256
+ if (!entry.sessionTitle) {
257
+ if (entry.agent === 'codex') {
258
+ const title = extractCodexSessionTitle(entry.filePath, entry.sessionId);
259
+ if (title)
260
+ entry.sessionTitle = title;
261
+ }
262
+ }
263
+ if (!entry.sessionTitle) {
264
+ const title = extractSessionTitleFromLine(line, entry.agent);
265
+ if (title)
266
+ entry.sessionTitle = title;
267
+ }
268
+ let events;
269
+ if (entry.agent === 'claude') {
270
+ events = (0, claude_1.parseClaudeLine)(line, entry.filePath);
271
+ }
272
+ else {
273
+ events = (0, codex_1.parseCodexLine)(line, entry.filePath);
274
+ }
275
+ for (const event of events) {
276
+ const normalizedEventSessionId = normalizeSessionId(event.sessionId, entry.agent);
277
+ if (normalizedEventSessionId && normalizedEventSessionId !== entry.sessionId) {
278
+ entry.sessionId = normalizedEventSessionId;
279
+ }
280
+ event.sessionId = entry.sessionId;
281
+ // Override source detection
282
+ event.source = this.detectSource(entry);
283
+ event.sessionTitle = entry.sessionTitle || fallbackSessionTitle(entry.projectName, entry.agent);
284
+ // Update project name from meta events
285
+ if (event.type === 'meta' && event.details.cwd) {
286
+ const cwd = String(event.details.cwd);
287
+ const name = cwd.split('/').pop() || cwd.split('\\').pop() || 'unknown';
288
+ this.sessionProjectNames.set(event.sessionId, name);
289
+ entry.projectName = name;
290
+ event.projectName = name;
291
+ }
292
+ else if (this.sessionProjectNames.has(event.sessionId)) {
293
+ event.projectName = this.sessionProjectNames.get(event.sessionId);
294
+ entry.projectName = event.projectName;
295
+ }
296
+ this.emit('event', event);
297
+ }
298
+ }
508
299
  }
509
- if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
510
- entry.sessionTitle = void 0;
300
+ reconcileSessionTitle(entry) {
301
+ if (entry.agent === 'codex') {
302
+ const threadTitle = getCodexThreadTitle(entry.sessionId);
303
+ if (threadTitle) {
304
+ if (threadTitle !== entry.sessionTitle)
305
+ entry.sessionTitle = threadTitle;
306
+ return;
307
+ }
308
+ }
309
+ // For Claude sessions without a real title (no title or noise), re-check whether
310
+ // this is a Kibitz internal session. This catches the timing race where the file
311
+ // was empty when watchFile was first called, so isKibitzInternalClaudeSession()
312
+ // returned false at that point. Once the user message is written, this will detect it.
313
+ if (entry.agent === 'claude' && (!entry.sessionTitle || isNoiseSessionTitle(entry.sessionTitle))) {
314
+ if (isKibitzInternalClaudeSession(entry.filePath)) {
315
+ entry.ignore = true;
316
+ entry.sessionTitle = undefined;
317
+ return;
318
+ }
319
+ }
320
+ // Avoid persisting prompt/instruction text as a session label (both agents).
321
+ if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
322
+ entry.sessionTitle = undefined;
323
+ }
511
324
  }
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) {
325
+ loadIdeLocks() {
326
+ const ideDir = path.join(os.homedir(), '.claude', 'ide');
327
+ if (!fs.existsSync(ideDir))
328
+ return;
329
+ this.claudeIdeLocks.clear();
520
330
  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
- }
331
+ const files = fs.readdirSync(ideDir).filter(f => f.endsWith('.lock'));
332
+ for (const file of files) {
333
+ try {
334
+ const content = fs.readFileSync(path.join(ideDir, file), 'utf8');
335
+ const lock = JSON.parse(content);
336
+ this.claudeIdeLocks.set(file, {
337
+ pid: lock.pid,
338
+ workspaceFolders: lock.workspaceFolders || [],
339
+ });
340
+ }
341
+ catch { /* corrupt lock */ }
342
+ }
343
+ }
344
+ catch { /* can't read ide dir */ }
345
+ }
346
+ detectSource(entry) {
347
+ if (entry.agent === 'codex')
348
+ return 'cli';
349
+ // If there are IDE lock files, assume VS Code sessions
350
+ return this.claudeIdeLocks.size > 0 ? 'vscode' : 'cli';
351
+ }
352
+ pruneStale() {
353
+ const now = Date.now();
354
+ for (const [filePath, entry] of this.watched) {
355
+ try {
356
+ const stat = fs.statSync(filePath);
357
+ if (now - stat.mtimeMs > SessionWatcher.ACTIVE_SESSION_WINDOW_MS) {
358
+ entry.watcher?.close();
359
+ this.watched.delete(filePath);
360
+ }
361
+ }
362
+ catch {
363
+ entry.watcher?.close();
364
+ this.watched.delete(filePath);
365
+ }
366
+ }
550
367
  }
551
- }
552
- };
368
+ }
369
+ exports.SessionWatcher = SessionWatcher;
553
370
  function extractSessionTitle(filePath, agent, sessionId) {
554
- return agent === "claude" ? extractClaudeSessionTitle(filePath) : extractCodexSessionTitle(filePath, sessionId);
371
+ return agent === 'claude'
372
+ ? extractClaudeSessionTitle(filePath)
373
+ : extractCodexSessionTitle(filePath, sessionId);
555
374
  }
556
375
  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
- }
376
+ try {
377
+ const obj = JSON.parse(line);
378
+ if (agent === 'claude') {
379
+ if (obj.type !== 'user')
380
+ return undefined;
381
+ const content = obj.message?.content;
382
+ if (typeof content === 'string')
383
+ return pickSessionTitle(content);
384
+ if (Array.isArray(content)) {
385
+ for (const block of content) {
386
+ if (typeof block?.text === 'string') {
387
+ const title = pickSessionTitle(block.text);
388
+ if (title)
389
+ return title;
390
+ }
391
+ }
392
+ }
393
+ return undefined;
394
+ }
395
+ if (obj.type === 'session_meta') {
396
+ const explicitTitle = pickSessionTitle(String(obj.payload?.title
397
+ || obj.payload?.session_title
398
+ || obj.payload?.name
399
+ || obj.payload?.summary
400
+ || ''));
401
+ if (explicitTitle)
402
+ return explicitTitle;
403
+ }
404
+ if (obj.type === 'event_msg' && obj.payload?.type === 'user_message') {
405
+ return pickSessionTitle(String(obj.payload.message || ''));
406
+ }
407
+ if (obj.type === 'response_item' && obj.payload?.type === 'message' && obj.payload?.role === 'user') {
408
+ const contentBlocks = obj.payload.content;
409
+ if (Array.isArray(contentBlocks)) {
410
+ for (const block of contentBlocks) {
411
+ const text = typeof block?.text === 'string'
412
+ ? block.text
413
+ : typeof block?.input_text === 'string'
414
+ ? block.input_text
415
+ : '';
416
+ const title = pickSessionTitle(text);
417
+ if (title)
418
+ return title;
419
+ }
420
+ }
421
+ }
591
422
  }
592
- } catch {
593
- }
594
- return void 0;
423
+ catch { /* ignore malformed lines */ }
424
+ return undefined;
595
425
  }
596
426
  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
- }
427
+ try {
428
+ const content = fs.readFileSync(filePath, 'utf8');
429
+ for (const line of content.split('\n')) {
430
+ if (!line.trim())
431
+ continue;
432
+ try {
433
+ const obj = JSON.parse(line);
434
+ if (obj.type === 'user') {
435
+ const msg = obj.message;
436
+ if (!msg)
437
+ continue;
438
+ const content = msg.content;
439
+ if (typeof content === 'string' && content.trim()) {
440
+ const title = pickSessionTitle(content);
441
+ if (title)
442
+ return title;
443
+ }
444
+ if (Array.isArray(content)) {
445
+ for (const block of content) {
446
+ if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
447
+ const title = pickSessionTitle(block.text);
448
+ if (title)
449
+ return title;
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ catch { /* skip bad lines */ }
456
+ }
622
457
  }
623
- } catch {
624
- }
625
- return void 0;
458
+ catch { /* unreadable */ }
459
+ return undefined;
626
460
  }
627
461
  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;
462
+ const codexSessionId = sessionId || deriveSessionId(filePath, 'codex');
463
+ const explicitThreadTitle = getCodexThreadTitle(codexSessionId);
464
+ if (explicitThreadTitle)
465
+ return explicitThreadTitle;
466
+ const logDerivedTitle = extractCodexSessionTitleFromLog(filePath);
467
+ return logDerivedTitle || undefined;
633
468
  }
634
469
  function getCodexThreadTitle(sessionId) {
635
- const normalizedSessionId = String(sessionId || "").trim().toLowerCase();
636
- if (!normalizedSessionId) return void 0;
637
- return readCodexThreadTitles().titles.get(normalizedSessionId);
470
+ const normalizedSessionId = String(sessionId || '').trim().toLowerCase();
471
+ if (!normalizedSessionId)
472
+ return undefined;
473
+ return readCodexThreadTitles().titles.get(normalizedSessionId);
638
474
  }
639
475
  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
- }
476
+ try {
477
+ const content = fs.readFileSync(filePath, 'utf8');
478
+ // Some logs expose a first-class title in session metadata.
479
+ for (const line of content.split('\n')) {
480
+ if (!line.trim())
481
+ continue;
482
+ try {
483
+ const obj = JSON.parse(line);
484
+ if (obj.type !== 'session_meta')
485
+ continue;
486
+ const explicitTitle = pickSessionTitle(String(obj.payload?.title
487
+ || obj.payload?.session_title
488
+ || obj.payload?.name
489
+ || obj.payload?.summary
490
+ || ''));
491
+ if (explicitTitle)
492
+ return explicitTitle;
493
+ }
494
+ catch { /* skip bad lines */ }
495
+ }
496
+ // Do not derive Codex titles from raw user prompts. Prompt text leaks into labels.
653
497
  }
654
- } catch {
655
- }
656
- return void 0;
498
+ catch { /* unreadable */ }
499
+ return undefined;
657
500
  }
658
501
  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
- }
502
+ try {
503
+ const content = fs.readFileSync(filePath, 'utf8');
504
+ for (const line of content.split('\n')) {
505
+ if (!line.trim())
506
+ continue;
507
+ try {
508
+ const obj = JSON.parse(line);
509
+ if (obj.type !== 'session_meta')
510
+ continue;
511
+ const cwd = typeof obj.payload?.cwd === 'string' ? obj.payload.cwd : '';
512
+ if (!cwd)
513
+ continue;
514
+ const name = cwd.split('/').pop() || cwd.split('\\').pop();
515
+ if (name && name.trim())
516
+ return name.trim();
517
+ }
518
+ catch { /* skip bad lines */ }
519
+ }
672
520
  }
673
- } catch {
674
- }
675
- return void 0;
521
+ catch { /* unreadable */ }
522
+ return undefined;
676
523
  }
677
524
  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 {
525
+ try {
526
+ const content = fs.readFileSync(filePath, 'utf8');
527
+ const lines = content.split('\n');
528
+ const maxProbeLines = 50;
529
+ for (let i = 0; i < lines.length && i < maxProbeLines; i++) {
530
+ const line = lines[i];
531
+ if (!line.trim())
532
+ continue;
533
+ try {
534
+ const obj = JSON.parse(line);
535
+ if (obj.type === 'event_msg' && obj.payload?.type === 'user_message') {
536
+ const msg = String(obj.payload?.message || '').toLowerCase();
537
+ if (looksLikeKibitzGeneratedPrompt(msg))
538
+ return true;
539
+ }
540
+ if (obj.type === 'response_item'
541
+ && obj.payload?.type === 'message'
542
+ && obj.payload?.role === 'user'
543
+ && Array.isArray(obj.payload?.content)) {
544
+ for (const block of obj.payload.content) {
545
+ const text = typeof block?.text === 'string'
546
+ ? block.text
547
+ : typeof block?.input_text === 'string'
548
+ ? block.input_text
549
+ : '';
550
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase()))
551
+ return true;
552
+ }
553
+ }
554
+ }
555
+ catch {
556
+ // ignore malformed lines
557
+ }
558
+ }
559
+ }
560
+ catch {
561
+ // unreadable session file
562
+ }
563
+ return false;
564
+ }
565
+ function isKibitzInternalClaudeUserLine(line) {
566
+ try {
686
567
  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
- }
568
+ if (obj.type !== 'user')
569
+ return false;
570
+ const c = obj.message?.content;
571
+ if (typeof c === 'string')
572
+ return looksLikeKibitzGeneratedPrompt(c.toLowerCase());
573
+ if (Array.isArray(c)) {
574
+ for (const block of c) {
575
+ if (block.type === 'text' && typeof block.text === 'string') {
576
+ if (looksLikeKibitzGeneratedPrompt(block.text.toLowerCase()))
577
+ return true;
578
+ }
579
+ }
580
+ }
699
581
  }
700
- } catch {
701
- }
702
- return false;
582
+ catch { /* ignore malformed */ }
583
+ return false;
703
584
  }
704
585
  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);
586
+ try {
587
+ const obj = JSON.parse(line);
588
+ if (obj.type === 'event_msg' && obj.payload?.type === 'user_message') {
589
+ const msg = String(obj.payload?.message || '').toLowerCase();
590
+ return looksLikeKibitzGeneratedPrompt(msg);
591
+ }
592
+ if (obj.type === 'response_item'
593
+ && obj.payload?.type === 'message'
594
+ && obj.payload?.role === 'user'
595
+ && Array.isArray(obj.payload?.content)) {
596
+ for (const block of obj.payload.content) {
597
+ const text = typeof block?.text === 'string'
598
+ ? block.text
599
+ : typeof block?.input_text === 'string'
600
+ ? block.input_text
601
+ : '';
602
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase()))
603
+ return true;
604
+ }
605
+ }
710
606
  }
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
- }
607
+ catch {
608
+ // ignore malformed lines
716
609
  }
717
- } catch {
718
- }
719
- return false;
610
+ return false;
720
611
  }
721
612
  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:");
613
+ if (!text)
614
+ return false;
615
+ // tone preset: is absent when preset=auto (the default), so don't require it
616
+ return text.includes('you oversee ai coding agents. summarize what they did')
617
+ && text.includes('format for this message:');
724
618
  }
725
- var codexThreadTitlesCache = {
726
- mtimeMs: -1,
727
- titles: /* @__PURE__ */ new Map(),
728
- order: []
619
+ function isKibitzInternalClaudeSession(filePath) {
620
+ try {
621
+ const content = fs.readFileSync(filePath, 'utf8');
622
+ const lines = content.split('\n');
623
+ for (let i = 0; i < lines.length && i < 20; i++) {
624
+ const line = lines[i];
625
+ if (!line.trim())
626
+ continue;
627
+ try {
628
+ const obj = JSON.parse(line);
629
+ if (obj.type !== 'user')
630
+ continue;
631
+ const msg = obj.message;
632
+ const c = msg?.content;
633
+ let text = '';
634
+ if (typeof c === 'string')
635
+ text = c;
636
+ else if (Array.isArray(c)) {
637
+ for (const block of c) {
638
+ if (block.type === 'text') {
639
+ text = block.text;
640
+ break;
641
+ }
642
+ }
643
+ }
644
+ if (text)
645
+ return looksLikeKibitzGeneratedPrompt(text.toLowerCase());
646
+ break;
647
+ }
648
+ catch { /* ignore malformed */ }
649
+ }
650
+ }
651
+ catch { /* unreadable */ }
652
+ return false;
653
+ }
654
+ const codexThreadTitlesCache = {
655
+ mtimeMs: -1,
656
+ titles: new Map(),
657
+ order: [],
729
658
  };
730
659
  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
- }
660
+ const statePath = path.join(os.homedir(), '.codex', '.codex-global-state.json');
661
+ try {
662
+ const stat = fs.statSync(statePath);
663
+ if (stat.mtimeMs === codexThreadTitlesCache.mtimeMs)
664
+ return codexThreadTitlesCache;
665
+ const raw = JSON.parse(fs.readFileSync(statePath, 'utf8'));
666
+ const rawTitles = raw?.['thread-titles']?.titles;
667
+ const rawOrder = raw?.['thread-titles']?.order;
668
+ const titles = new Map();
669
+ if (rawTitles && typeof rawTitles === 'object' && !Array.isArray(rawTitles)) {
670
+ for (const [key, value] of Object.entries(rawTitles)) {
671
+ const sessionId = String(key || '').trim().toLowerCase();
672
+ if (!sessionId)
673
+ continue;
674
+ const title = pickSessionTitle(String(value || ''));
675
+ if (title)
676
+ titles.set(sessionId, title);
677
+ }
678
+ }
679
+ const order = Array.isArray(rawOrder)
680
+ ? rawOrder
681
+ .map((item) => String(item || '').trim().toLowerCase())
682
+ .filter(Boolean)
683
+ : [];
684
+ codexThreadTitlesCache.mtimeMs = stat.mtimeMs;
685
+ codexThreadTitlesCache.titles = titles;
686
+ codexThreadTitlesCache.order = order;
687
+ }
688
+ catch {
689
+ // Keep the last successful cache. If no cache exists, this remains empty.
746
690
  }
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;
691
+ return codexThreadTitlesCache;
754
692
  }
755
693
  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();
694
+ const basename = path.basename(filePath, '.jsonl');
695
+ if (agent !== 'codex')
696
+ return basename;
697
+ const match = basename.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/i);
698
+ return match ? match[1].toLowerCase() : basename.toLowerCase();
760
699
  }
761
700
  function normalizeSessionId(rawSessionId, agent) {
762
- const value = String(rawSessionId || "").trim();
763
- if (!value) return "";
764
- if (agent === "codex") return value.toLowerCase();
765
- return value;
701
+ const value = String(rawSessionId || '').trim();
702
+ if (!value)
703
+ return '';
704
+ if (agent === 'codex')
705
+ return value.toLowerCase();
706
+ return value;
766
707
  }
767
708
  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");
709
+ const normalizedFallback = normalizeSessionId(fallback, agent);
710
+ if (agent !== 'claude')
711
+ return normalizedFallback;
775
712
  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) {
713
+ const stat = fs.statSync(filePath);
714
+ const length = Math.min(stat.size, 512 * 1024);
715
+ if (length <= 0)
716
+ return normalizedFallback;
717
+ const fd = fs.openSync(filePath, 'r');
781
718
  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);
719
+ const buf = Buffer.alloc(length);
720
+ const offset = Math.max(0, stat.size - length);
721
+ fs.readSync(fd, buf, 0, length, offset);
722
+ const lines = buf.toString('utf8').split('\n').filter((line) => line.trim());
723
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
724
+ try {
725
+ const obj = JSON.parse(lines[i]);
726
+ const fromLine = normalizeSessionId(obj?.sessionId, agent);
727
+ if (fromLine)
728
+ return fromLine;
729
+ }
730
+ catch {
731
+ // ignore malformed line
732
+ }
733
+ }
734
+ }
735
+ finally {
736
+ fs.closeSync(fd);
737
+ }
790
738
  }
791
- } catch {
792
- }
793
- return normalizedFallback;
739
+ catch {
740
+ // ignore unreadable session log
741
+ }
742
+ return normalizedFallback;
794
743
  }
795
744
  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;
745
+ if (!raw.trim())
746
+ return undefined;
747
+ if (looksLikeKibitzGeneratedPrompt(raw.toLowerCase()))
748
+ return undefined;
749
+ for (const line of raw.split('\n')) {
750
+ const trimmed = line.trim();
751
+ if (!trimmed)
752
+ continue;
753
+ const cleaned = trimmed.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
754
+ if (!cleaned || isNoiseSessionTitle(cleaned))
755
+ continue;
756
+ return cleaned;
757
+ }
758
+ return undefined;
806
759
  }
807
760
  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;
761
+ const lower = text.toLowerCase();
762
+ const normalized = lower.replace(/^[#>*\-\s]+/, '').trim();
763
+ if (normalized.length < 8)
764
+ return true;
765
+ if (looksLikeSessionIdentifier(normalized))
766
+ return true;
767
+ // Drop opaque machine blobs (JWT/base64-like) that are never useful as titles.
768
+ if (/^[a-z0-9+/_=-]{64,}$/.test(normalized))
769
+ return true;
770
+ if (normalized.startsWith('the user opened the file ')
771
+ || normalized.includes('may or may not be related to the current task'))
772
+ return true;
773
+ // Drop file-reference lines like "logo.svg: media/logo.svg" or "file.ts: src/file.ts"
774
+ if (/^[\w][\w.\-]*\.[a-z]{2,6}:\s/i.test(normalized))
775
+ return true;
776
+ if (/^\d+\)\s+after deciding to use a skill\b/.test(normalized)
777
+ || /^\d+\)\s+when `?skill\.md`? references\b/.test(normalized)
778
+ || /^\d+\)\s+if `?skill\.md`? points\b/.test(normalized)
779
+ || /^\d+\)\s+if `?scripts\/`? exist\b/.test(normalized)
780
+ || /^\d+\)\s+if `?assets\/`? or templates exist\b/.test(normalized)
781
+ || /^perform( a)? repository commit\b/.test(normalized))
782
+ return true;
783
+ if (normalized.startsWith('agents.md instructions for')
784
+ || normalized.startsWith('a skill is a set of local instructions')
785
+ || normalized.startsWith('researched how the feature works')
786
+ || normalized.startsWith('context from my ide setup:')
787
+ || normalized.startsWith('open tabs:')
788
+ || normalized.startsWith('my request for codex:')
789
+ || normalized.startsWith('available skills')
790
+ || normalized.startsWith('how to use skills')
791
+ || normalized.startsWith('never mention session ids')
792
+ || normalized.startsWith('never write in first person')
793
+ || normalized.startsWith('you oversee ai coding agents')
794
+ || normalized.startsWith('plain language.')
795
+ || normalized.startsWith('bold only key nouns')
796
+ || normalized.startsWith('upper case')
797
+ || normalized.startsWith('emoji are allowed')
798
+ || normalized.startsWith('no filler.')
799
+ || normalized.startsWith("don't repeat what previous commentary")
800
+ || normalized.startsWith('rules:')
801
+ || normalized.startsWith('format for this message:')
802
+ || normalized.startsWith('tone preset:')
803
+ || normalized.startsWith('additional user instruction:')
804
+ || normalized.startsWith('skill-creator:')
805
+ || normalized.startsWith('skill-installer:')
806
+ || normalized.startsWith('- skill-')
807
+ || normalized.startsWith('discovery:')
808
+ || normalized.startsWith('trigger rules:')
809
+ || normalized.startsWith('missing/blocked:')
810
+ || normalized.startsWith('how to use a skill')
811
+ || normalized.startsWith('context hygiene:')
812
+ || normalized.startsWith('safety and fallback:')
813
+ || normalized.startsWith('environment_context')
814
+ || normalized.startsWith('/environment_context')
815
+ || normalized.startsWith('permissions instructions')
816
+ || normalized.startsWith('filesystem sandboxing defines')
817
+ || normalized.startsWith('collaboration mode:')
818
+ || normalized.startsWith('system instructions:')
819
+ || normalized.startsWith('user request:')
820
+ || normalized.startsWith('active goals')
821
+ || normalized.startsWith('room memory')
822
+ || normalized.startsWith('recent activity')
823
+ || normalized.startsWith('room workers')
824
+ || normalized.startsWith('room tasks')
825
+ || normalized.startsWith('recent decisions')
826
+ || normalized.startsWith('pending questions to keeper')
827
+ || normalized.startsWith('execution settings')
828
+ || normalized.startsWith('instructions')
829
+ || normalized.startsWith('based on the current state')
830
+ || normalized.startsWith('important: you must call at least one tool')
831
+ || normalized.startsWith('respond only with a tool call')
832
+ || normalized.startsWith('use bullet points.')
833
+ || normalized.startsWith('write a single sentence.')
834
+ || normalized.startsWith('start with a short upper case reaction')
835
+ || normalized.startsWith('use numbered steps showing what the agent did in order')
836
+ || normalized.startsWith('summarize in one sentence, then ask a pointed rhetorical question')
837
+ || normalized.startsWith('use a compact markdown table with columns')
838
+ || normalized.startsWith('use bullet points with emoji tags')
839
+ || normalized.startsWith('use a scoreboard format with these labels')
840
+ || normalized.startsWith('example:')
841
+ || normalized.startsWith('session: ')
842
+ || normalized.startsWith('actions (')
843
+ || normalized.startsWith('previous commentary')
844
+ || normalized.startsWith('summarize only this session')
845
+ || normalized.startsWith('never mention ids/logs/prompts/traces')
846
+ || normalized.startsWith('never say "i", "i\'ll", "we"'))
847
+ return true;
848
+ if (normalized.includes('only key nouns')
849
+ || normalized.includes('fixed the login page')
850
+ || normalized.includes('edited auth middleware')
851
+ || normalized.includes('max 2 per commentary')
852
+ || normalized.includes("don't repeat what previous commentary already said"))
853
+ return true;
854
+ if (normalized.startsWith('cwd:') || normalized.startsWith('shell:'))
855
+ return true;
856
+ if (normalized.startsWith('your identity')
857
+ || normalized.startsWith('room id:')
858
+ || normalized.startsWith('worker id:'))
859
+ return true;
860
+ if (normalized.startsWith('you are codex, a coding agent'))
861
+ return true;
862
+ return false;
822
863
  }
823
864
  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;
865
+ if (/^[0-9a-f]{8}$/.test(text))
866
+ return true;
867
+ if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(text))
868
+ return true;
869
+ if (/^session[\s:_-]*[0-9a-f]{8,}$/.test(text))
870
+ return true;
871
+ if (/^turn[\s:_-]*[0-9a-f]{8,}$/.test(text))
872
+ return true;
873
+ if (/^rollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?$/.test(text))
874
+ return true;
875
+ return false;
830
876
  }
831
877
  function extractProjectName(dirName) {
832
- const parts = dirName.split("-").filter(Boolean);
833
- return parts[parts.length - 1] || "unknown";
878
+ // -Users-vasily-projects-room room
879
+ const parts = dirName.split('-').filter(Boolean);
880
+ return parts[parts.length - 1] || 'unknown';
834
881
  }
835
882
  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;
883
+ const home = os.homedir();
884
+ const results = [];
885
+ const seen = new Set();
886
+ const now = new Date();
887
+ const totalDays = Math.max(1, daysBack);
888
+ for (let offset = 0; offset < totalDays; offset++) {
889
+ const d = new Date(now);
890
+ d.setDate(now.getDate() - offset);
891
+ const dir = path.join(home, '.codex', 'sessions', String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0'));
892
+ if (seen.has(dir))
893
+ continue;
894
+ seen.add(dir);
895
+ results.push(dir);
896
+ }
897
+ return results;
857
898
  }
858
899
  function fallbackSessionTitle(projectName, agent) {
859
- const project = (projectName || "").trim();
860
- if (project) return project;
861
- return `${agent} session`;
900
+ const project = (projectName || '').trim();
901
+ if (project)
902
+ return project;
903
+ return `${agent} session`;
862
904
  }
863
- // Annotate the CommonJS export names for ESM import in node:
864
- 0 && (module.exports = {
865
- SessionWatcher
866
- });
905
+ //# sourceMappingURL=watcher.js.map