@kibitzsh/kibitz 0.0.4 → 0.0.6

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,905 +1,928 @@
1
1
  "use strict";
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;
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
17
34
  });
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;
24
- };
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;
60
- }
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;
109
- try {
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
- }
152
- }
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
+ }
153
123
  }
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
- }
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
+ }
181
139
  }
182
- watchFile(filePath, agent, projectName) {
183
- let offset;
184
- try {
185
- const stat = fs.statSync(filePath);
186
- offset = stat.size; // start from current end, only read new content
187
- }
188
- catch {
189
- return;
190
- }
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,
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({
203
221
  sessionId,
204
- offset,
205
- agent,
206
- ignore,
207
- watcher: null,
208
- projectName: resolvedProjectName,
209
- sessionTitle,
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.reconcileSessionTitle(w);
305
+ if (w.ignore) continue;
306
+ try {
307
+ const stat = fs.statSync(w.filePath);
308
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
309
+ const session = {
310
+ id: w.sessionId,
311
+ projectName: w.projectName,
312
+ sessionTitle: w.sessionTitle,
313
+ agent: w.agent,
314
+ source: this.detectSource(w),
315
+ filePath: w.filePath,
316
+ lastActivity: stat.mtimeMs
210
317
  };
211
- try {
212
- entry.watcher = fs.watch(filePath, () => {
213
- this.readNewLines(entry);
214
- });
215
- }
216
- catch {
217
- // fs.watch failed, fall back to polling via scan interval
318
+ const key = `${session.agent}:${session.id.toLowerCase()}`;
319
+ const existing = sessionsByKey.get(key);
320
+ if (!existing || existing.lastActivity < session.lastActivity) {
321
+ sessionsByKey.set(key, session);
218
322
  }
219
- this.watched.set(filePath, entry);
323
+ } catch {
324
+ }
220
325
  }
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;
326
+ return Array.from(sessionsByKey.values()).sort((a, b) => b.lastActivity - a.lastActivity);
327
+ }
328
+ scan() {
329
+ this.loadIdeLocks();
330
+ this.scanClaude();
331
+ this.scanCodex();
332
+ this.pruneStale();
333
+ }
334
+ scanClaude() {
335
+ const claudeDir = path.join(os.homedir(), ".claude", "projects");
336
+ if (!fs.existsSync(claudeDir)) return;
337
+ let projectDirs;
338
+ try {
339
+ projectDirs = fs.readdirSync(claudeDir).filter((d) => {
340
+ const full = path.join(claudeDir, d);
231
341
  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
- }
342
+ return fs.statSync(full).isDirectory();
343
+ } catch {
344
+ return false;
298
345
  }
346
+ });
347
+ } catch {
348
+ return;
299
349
  }
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
- }
350
+ const now = Date.now();
351
+ for (const dir of projectDirs) {
352
+ if (dir.endsWith("-.kibitz-sessions")) continue;
353
+ const dirPath = path.join(claudeDir, dir);
354
+ let files;
355
+ try {
356
+ files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
357
+ } catch {
358
+ continue;
359
+ }
360
+ for (const file of files) {
361
+ const filePath = path.join(dirPath, file);
362
+ try {
363
+ const stat = fs.statSync(filePath);
364
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
365
+ } catch {
366
+ continue;
319
367
  }
320
- // Avoid persisting prompt/instruction text as a session label (both agents).
321
- if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
322
- entry.sessionTitle = undefined;
368
+ if (!this.watched.has(filePath)) {
369
+ const projectName = extractProjectName(dir);
370
+ this.watchFile(filePath, "claude", projectName);
323
371
  }
372
+ }
324
373
  }
325
- loadIdeLocks() {
326
- const ideDir = path.join(os.homedir(), '.claude', 'ide');
327
- if (!fs.existsSync(ideDir))
328
- return;
329
- this.claudeIdeLocks.clear();
374
+ }
375
+ scanCodex() {
376
+ const currentTime = Date.now();
377
+ for (const sessionsDir of codexSessionDirs(2)) {
378
+ if (!fs.existsSync(sessionsDir)) continue;
379
+ let files;
380
+ try {
381
+ files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
382
+ } catch {
383
+ continue;
384
+ }
385
+ for (const file of files) {
386
+ const filePath = path.join(sessionsDir, file);
330
387
  try {
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
- }
388
+ const stat = fs.statSync(filePath);
389
+ if (currentTime - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) continue;
390
+ } catch {
391
+ continue;
343
392
  }
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
- }
393
+ if (!this.watched.has(filePath)) {
394
+ this.watchFile(filePath, "codex", "codex");
366
395
  }
396
+ }
367
397
  }
368
- }
369
- exports.SessionWatcher = SessionWatcher;
398
+ }
399
+ watchFile(filePath, agent, projectName) {
400
+ let offset;
401
+ try {
402
+ const stat = fs.statSync(filePath);
403
+ offset = stat.size;
404
+ } catch {
405
+ return;
406
+ }
407
+ let resolvedProjectName = projectName;
408
+ if (agent === "codex") {
409
+ const codexProject = extractCodexProjectName(filePath);
410
+ if (codexProject) resolvedProjectName = codexProject;
411
+ }
412
+ const sessionId = extractSessionIdFromLog(
413
+ filePath,
414
+ agent,
415
+ deriveSessionId(filePath, agent)
416
+ );
417
+ const ignore = agent === "codex" && isKibitzInternalCodexSession(filePath) || agent === "claude" && isKibitzInternalClaudeSession(filePath);
418
+ const sessionTitle = extractSessionTitle(filePath, agent, sessionId);
419
+ const entry = {
420
+ filePath,
421
+ sessionId,
422
+ offset,
423
+ agent,
424
+ ignore,
425
+ watcher: null,
426
+ projectName: resolvedProjectName,
427
+ sessionTitle
428
+ };
429
+ try {
430
+ entry.watcher = fs.watch(filePath, () => {
431
+ this.readNewLines(entry);
432
+ });
433
+ } catch {
434
+ }
435
+ this.watched.set(filePath, entry);
436
+ }
437
+ readNewLines(entry) {
438
+ if (entry.ignore) {
439
+ try {
440
+ const stat2 = fs.statSync(entry.filePath);
441
+ entry.offset = stat2.size;
442
+ } catch {
443
+ }
444
+ return;
445
+ }
446
+ let stat;
447
+ try {
448
+ stat = fs.statSync(entry.filePath);
449
+ } catch {
450
+ return;
451
+ }
452
+ this.reconcileSessionTitle(entry);
453
+ if (stat.size <= entry.offset) return;
454
+ const fd = fs.openSync(entry.filePath, "r");
455
+ const buf = Buffer.alloc(stat.size - entry.offset);
456
+ fs.readSync(fd, buf, 0, buf.length, entry.offset);
457
+ fs.closeSync(fd);
458
+ entry.offset = stat.size;
459
+ const chunk = buf.toString("utf8");
460
+ const lines = chunk.split("\n").filter((l) => l.trim());
461
+ for (const line of lines) {
462
+ if (entry.agent === "codex" && isKibitzInternalCodexLine(line)) {
463
+ entry.ignore = true;
464
+ break;
465
+ }
466
+ if (entry.agent === "claude" && isKibitzInternalClaudeUserLine(line)) {
467
+ entry.ignore = true;
468
+ break;
469
+ }
470
+ if (!entry.sessionTitle) {
471
+ if (entry.agent === "codex") {
472
+ const title = extractCodexSessionTitle(entry.filePath, entry.sessionId);
473
+ if (title) entry.sessionTitle = title;
474
+ }
475
+ }
476
+ if (!entry.sessionTitle) {
477
+ const title = extractSessionTitleFromLine(line, entry.agent);
478
+ if (title) entry.sessionTitle = title;
479
+ }
480
+ let events;
481
+ if (entry.agent === "claude") {
482
+ events = parseClaudeLine(line, entry.filePath);
483
+ } else {
484
+ events = parseCodexLine(line, entry.filePath);
485
+ }
486
+ for (const event of events) {
487
+ const normalizedEventSessionId = normalizeSessionId(event.sessionId, entry.agent);
488
+ if (normalizedEventSessionId && normalizedEventSessionId !== entry.sessionId) {
489
+ entry.sessionId = normalizedEventSessionId;
490
+ }
491
+ event.sessionId = entry.sessionId;
492
+ event.source = this.detectSource(entry);
493
+ event.sessionTitle = entry.sessionTitle || fallbackSessionTitle(entry.projectName, entry.agent);
494
+ if (event.type === "meta" && event.details.cwd) {
495
+ const cwd = String(event.details.cwd);
496
+ const name = cwd.split("/").pop() || cwd.split("\\").pop() || "unknown";
497
+ this.sessionProjectNames.set(event.sessionId, name);
498
+ entry.projectName = name;
499
+ event.projectName = name;
500
+ } else if (this.sessionProjectNames.has(event.sessionId)) {
501
+ event.projectName = this.sessionProjectNames.get(event.sessionId);
502
+ entry.projectName = event.projectName;
503
+ }
504
+ this.emit("event", event);
505
+ }
506
+ }
507
+ }
508
+ reconcileSessionTitle(entry) {
509
+ if (entry.agent === "codex") {
510
+ const threadTitle = getCodexThreadTitle(entry.sessionId);
511
+ if (threadTitle) {
512
+ if (threadTitle !== entry.sessionTitle) entry.sessionTitle = threadTitle;
513
+ return;
514
+ }
515
+ }
516
+ if (entry.agent === "claude" && (!entry.sessionTitle || isNoiseSessionTitle(entry.sessionTitle))) {
517
+ if (isKibitzInternalClaudeSession(entry.filePath)) {
518
+ entry.ignore = true;
519
+ entry.sessionTitle = void 0;
520
+ return;
521
+ }
522
+ }
523
+ if (entry.sessionTitle && isNoiseSessionTitle(entry.sessionTitle)) {
524
+ entry.sessionTitle = void 0;
525
+ }
526
+ }
527
+ loadIdeLocks() {
528
+ const ideDir = path.join(os.homedir(), ".claude", "ide");
529
+ if (!fs.existsSync(ideDir)) return;
530
+ this.claudeIdeLocks.clear();
531
+ try {
532
+ const files = fs.readdirSync(ideDir).filter((f) => f.endsWith(".lock"));
533
+ for (const file of files) {
534
+ try {
535
+ const content = fs.readFileSync(path.join(ideDir, file), "utf8");
536
+ const lock = JSON.parse(content);
537
+ this.claudeIdeLocks.set(file, {
538
+ pid: lock.pid,
539
+ workspaceFolders: lock.workspaceFolders || []
540
+ });
541
+ } catch {
542
+ }
543
+ }
544
+ } catch {
545
+ }
546
+ }
547
+ detectSource(entry) {
548
+ if (entry.agent === "codex") return "cli";
549
+ return this.claudeIdeLocks.size > 0 ? "vscode" : "cli";
550
+ }
551
+ pruneStale() {
552
+ const now = Date.now();
553
+ for (const [filePath, entry] of this.watched) {
554
+ try {
555
+ const stat = fs.statSync(filePath);
556
+ if (now - stat.mtimeMs > _SessionWatcher.ACTIVE_SESSION_WINDOW_MS) {
557
+ entry.watcher?.close();
558
+ this.watched.delete(filePath);
559
+ }
560
+ } catch {
561
+ entry.watcher?.close();
562
+ this.watched.delete(filePath);
563
+ }
564
+ }
565
+ }
566
+ };
370
567
  function extractSessionTitle(filePath, agent, sessionId) {
371
- return agent === 'claude'
372
- ? extractClaudeSessionTitle(filePath)
373
- : extractCodexSessionTitle(filePath, sessionId);
568
+ return agent === "claude" ? extractClaudeSessionTitle(filePath) : extractCodexSessionTitle(filePath, sessionId);
374
569
  }
375
570
  function extractSessionTitleFromLine(line, agent) {
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
- }
571
+ try {
572
+ const obj = JSON.parse(line);
573
+ if (agent === "claude") {
574
+ if (obj.type !== "user") return void 0;
575
+ const content = obj.message?.content;
576
+ if (typeof content === "string") return pickSessionTitle(content);
577
+ if (Array.isArray(content)) {
578
+ for (const block of content) {
579
+ if (typeof block?.text === "string") {
580
+ const title = pickSessionTitle(block.text);
581
+ if (title) return title;
582
+ }
583
+ }
584
+ }
585
+ return void 0;
586
+ }
587
+ if (obj.type === "session_meta") {
588
+ const explicitTitle = pickSessionTitle(String(
589
+ obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
590
+ ));
591
+ if (explicitTitle) return explicitTitle;
592
+ }
593
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
594
+ return pickSessionTitle(String(obj.payload.message || ""));
422
595
  }
423
- catch { /* ignore malformed lines */ }
424
- return undefined;
596
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user") {
597
+ const contentBlocks = obj.payload.content;
598
+ if (Array.isArray(contentBlocks)) {
599
+ for (const block of contentBlocks) {
600
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
601
+ const title = pickSessionTitle(text);
602
+ if (title) return title;
603
+ }
604
+ }
605
+ }
606
+ } catch {
607
+ }
608
+ return void 0;
425
609
  }
426
610
  function extractClaudeSessionTitle(filePath) {
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
- }
611
+ try {
612
+ const content = fs.readFileSync(filePath, "utf8");
613
+ for (const line of content.split("\n")) {
614
+ if (!line.trim()) continue;
615
+ try {
616
+ const obj = JSON.parse(line);
617
+ if (obj.type === "user") {
618
+ const msg = obj.message;
619
+ if (!msg) continue;
620
+ const content2 = msg.content;
621
+ if (typeof content2 === "string" && content2.trim()) {
622
+ const title = pickSessionTitle(content2);
623
+ if (title) return title;
624
+ }
625
+ if (Array.isArray(content2)) {
626
+ for (const block of content2) {
627
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
628
+ const title = pickSessionTitle(block.text);
629
+ if (title) return title;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ } catch {
635
+ }
457
636
  }
458
- catch { /* unreadable */ }
459
- return undefined;
637
+ } catch {
638
+ }
639
+ return void 0;
460
640
  }
461
641
  function extractCodexSessionTitle(filePath, sessionId) {
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;
642
+ const codexSessionId = sessionId || deriveSessionId(filePath, "codex");
643
+ const explicitThreadTitle = getCodexThreadTitle(codexSessionId);
644
+ if (explicitThreadTitle) return explicitThreadTitle;
645
+ const logDerivedTitle = extractCodexSessionTitleFromLog(filePath);
646
+ return logDerivedTitle || void 0;
468
647
  }
469
648
  function getCodexThreadTitle(sessionId) {
470
- const normalizedSessionId = String(sessionId || '').trim().toLowerCase();
471
- if (!normalizedSessionId)
472
- return undefined;
473
- return readCodexThreadTitles().titles.get(normalizedSessionId);
649
+ const normalizedSessionId = String(sessionId || "").trim().toLowerCase();
650
+ if (!normalizedSessionId) return void 0;
651
+ return readCodexThreadTitles().titles.get(normalizedSessionId);
474
652
  }
475
653
  function extractCodexSessionTitleFromLog(filePath) {
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.
654
+ try {
655
+ const content = fs.readFileSync(filePath, "utf8");
656
+ for (const line of content.split("\n")) {
657
+ if (!line.trim()) continue;
658
+ try {
659
+ const obj = JSON.parse(line);
660
+ if (obj.type !== "session_meta") continue;
661
+ const explicitTitle = pickSessionTitle(String(
662
+ obj.payload?.title || obj.payload?.session_title || obj.payload?.name || obj.payload?.summary || ""
663
+ ));
664
+ if (explicitTitle) return explicitTitle;
665
+ } catch {
666
+ }
497
667
  }
498
- catch { /* unreadable */ }
499
- return undefined;
668
+ } catch {
669
+ }
670
+ return void 0;
500
671
  }
501
672
  function extractCodexProjectName(filePath) {
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
- }
673
+ try {
674
+ const content = fs.readFileSync(filePath, "utf8");
675
+ for (const line of content.split("\n")) {
676
+ if (!line.trim()) continue;
677
+ try {
678
+ const obj = JSON.parse(line);
679
+ if (obj.type !== "session_meta") continue;
680
+ const cwd = typeof obj.payload?.cwd === "string" ? obj.payload.cwd : "";
681
+ if (!cwd) continue;
682
+ const name = cwd.split("/").pop() || cwd.split("\\").pop();
683
+ if (name && name.trim()) return name.trim();
684
+ } catch {
685
+ }
520
686
  }
521
- catch { /* unreadable */ }
522
- return undefined;
687
+ } catch {
688
+ }
689
+ return void 0;
523
690
  }
524
691
  function isKibitzInternalCodexSession(filePath) {
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
692
+ try {
693
+ const content = fs.readFileSync(filePath, "utf8");
694
+ const lines = content.split("\n");
695
+ const maxProbeLines = 50;
696
+ for (let i = 0; i < lines.length && i < maxProbeLines; i++) {
697
+ const line = lines[i];
698
+ if (!line.trim()) continue;
699
+ try {
700
+ const obj = JSON.parse(line);
701
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
702
+ const msg = String(obj.payload?.message || "").toLowerCase();
703
+ if (looksLikeKibitzGeneratedPrompt(msg)) return true;
704
+ }
705
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
706
+ for (const block of obj.payload.content) {
707
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
708
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
709
+ }
710
+ }
711
+ } catch {
712
+ }
562
713
  }
563
- return false;
714
+ } catch {
715
+ }
716
+ return false;
564
717
  }
565
718
  function isKibitzInternalClaudeUserLine(line) {
566
- try {
567
- const obj = JSON.parse(line);
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
- }
719
+ try {
720
+ const obj = JSON.parse(line);
721
+ if (obj.type !== "user") return false;
722
+ const c = obj.message?.content;
723
+ if (typeof c === "string") return looksLikeKibitzGeneratedPrompt(c.toLowerCase());
724
+ if (Array.isArray(c)) {
725
+ for (const block of c) {
726
+ if (block.type === "text" && typeof block.text === "string") {
727
+ if (looksLikeKibitzGeneratedPrompt(block.text.toLowerCase())) return true;
728
+ }
729
+ }
581
730
  }
582
- catch { /* ignore malformed */ }
583
- return false;
731
+ } catch {
732
+ }
733
+ return false;
584
734
  }
585
735
  function isKibitzInternalCodexLine(line) {
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
- }
736
+ try {
737
+ const obj = JSON.parse(line);
738
+ if (obj.type === "event_msg" && obj.payload?.type === "user_message") {
739
+ const msg = String(obj.payload?.message || "").toLowerCase();
740
+ return looksLikeKibitzGeneratedPrompt(msg);
606
741
  }
607
- catch {
608
- // ignore malformed lines
742
+ if (obj.type === "response_item" && obj.payload?.type === "message" && obj.payload?.role === "user" && Array.isArray(obj.payload?.content)) {
743
+ for (const block of obj.payload.content) {
744
+ const text = typeof block?.text === "string" ? block.text : typeof block?.input_text === "string" ? block.input_text : "";
745
+ if (looksLikeKibitzGeneratedPrompt(String(text).toLowerCase())) return true;
746
+ }
609
747
  }
610
- return false;
748
+ } catch {
749
+ }
750
+ return false;
611
751
  }
612
752
  function looksLikeKibitzGeneratedPrompt(text) {
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:');
753
+ if (!text) return false;
754
+ return text.includes("you oversee ai coding agents. summarize what they did") && text.includes("format for this message:");
618
755
  }
619
756
  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
- }
757
+ try {
758
+ const content = fs.readFileSync(filePath, "utf8");
759
+ const lines = content.split("\n");
760
+ for (let i = 0; i < lines.length && i < 20; i++) {
761
+ const line = lines[i];
762
+ if (!line.trim()) continue;
763
+ try {
764
+ const obj = JSON.parse(line);
765
+ if (obj.type !== "user") continue;
766
+ const msg = obj.message;
767
+ const c = msg?.content;
768
+ let text = "";
769
+ if (typeof c === "string") text = c;
770
+ else if (Array.isArray(c)) {
771
+ for (const block of c) {
772
+ if (block.type === "text") {
773
+ text = block.text;
774
+ break;
775
+ }
776
+ }
777
+ }
778
+ if (text) return looksLikeKibitzGeneratedPrompt(text.toLowerCase());
779
+ break;
780
+ } catch {
781
+ }
650
782
  }
651
- catch { /* unreadable */ }
652
- return false;
783
+ } catch {
784
+ }
785
+ return false;
653
786
  }
654
- const codexThreadTitlesCache = {
655
- mtimeMs: -1,
656
- titles: new Map(),
657
- order: [],
787
+ var codexThreadTitlesCache = {
788
+ mtimeMs: -1,
789
+ titles: /* @__PURE__ */ new Map(),
790
+ order: []
658
791
  };
659
792
  function readCodexThreadTitles() {
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.
793
+ const statePath = path.join(os.homedir(), ".codex", ".codex-global-state.json");
794
+ try {
795
+ const stat = fs.statSync(statePath);
796
+ if (stat.mtimeMs === codexThreadTitlesCache.mtimeMs) return codexThreadTitlesCache;
797
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
798
+ const rawTitles = raw?.["thread-titles"]?.titles;
799
+ const rawOrder = raw?.["thread-titles"]?.order;
800
+ const titles = /* @__PURE__ */ new Map();
801
+ if (rawTitles && typeof rawTitles === "object" && !Array.isArray(rawTitles)) {
802
+ for (const [key, value] of Object.entries(rawTitles)) {
803
+ const sessionId = String(key || "").trim().toLowerCase();
804
+ if (!sessionId) continue;
805
+ const title = pickSessionTitle(String(value || ""));
806
+ if (title) titles.set(sessionId, title);
807
+ }
690
808
  }
691
- return codexThreadTitlesCache;
809
+ const order = Array.isArray(rawOrder) ? rawOrder.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean) : [];
810
+ codexThreadTitlesCache.mtimeMs = stat.mtimeMs;
811
+ codexThreadTitlesCache.titles = titles;
812
+ codexThreadTitlesCache.order = order;
813
+ } catch {
814
+ }
815
+ return codexThreadTitlesCache;
692
816
  }
693
817
  function deriveSessionId(filePath, agent) {
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();
818
+ const basename2 = path.basename(filePath, ".jsonl");
819
+ if (agent !== "codex") return basename2;
820
+ const match = basename2.match(/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/i);
821
+ return match ? match[1].toLowerCase() : basename2.toLowerCase();
699
822
  }
700
823
  function normalizeSessionId(rawSessionId, agent) {
701
- const value = String(rawSessionId || '').trim();
702
- if (!value)
703
- return '';
704
- if (agent === 'codex')
705
- return value.toLowerCase();
706
- return value;
824
+ const value = String(rawSessionId || "").trim();
825
+ if (!value) return "";
826
+ if (agent === "codex") return value.toLowerCase();
827
+ return value;
707
828
  }
708
829
  function extractSessionIdFromLog(filePath, agent, fallback) {
709
- const normalizedFallback = normalizeSessionId(fallback, agent);
710
- if (agent !== 'claude')
711
- return normalizedFallback;
830
+ const normalizedFallback = normalizeSessionId(fallback, agent);
831
+ if (agent !== "claude") return normalizedFallback;
832
+ try {
833
+ const stat = fs.statSync(filePath);
834
+ const length = Math.min(stat.size, 512 * 1024);
835
+ if (length <= 0) return normalizedFallback;
836
+ const fd = fs.openSync(filePath, "r");
712
837
  try {
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');
838
+ const buf = Buffer.alloc(length);
839
+ const offset = Math.max(0, stat.size - length);
840
+ fs.readSync(fd, buf, 0, length, offset);
841
+ const lines = buf.toString("utf8").split("\n").filter((line) => line.trim());
842
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
718
843
  try {
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
- }
844
+ const obj = JSON.parse(lines[i]);
845
+ const fromLine = normalizeSessionId(obj?.sessionId, agent);
846
+ if (fromLine) return fromLine;
847
+ } catch {
848
+ }
849
+ }
850
+ } finally {
851
+ fs.closeSync(fd);
738
852
  }
739
- catch {
740
- // ignore unreadable session log
741
- }
742
- return normalizedFallback;
853
+ } catch {
854
+ }
855
+ return normalizedFallback;
743
856
  }
744
857
  function pickSessionTitle(raw) {
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;
858
+ if (!raw.trim()) return void 0;
859
+ if (looksLikeKibitzGeneratedPrompt(raw.toLowerCase())) return void 0;
860
+ for (const line of raw.split("\n")) {
861
+ const trimmed = line.trim();
862
+ if (!trimmed) continue;
863
+ const cleaned = trimmed.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
864
+ if (!cleaned || isNoiseSessionTitle(cleaned)) continue;
865
+ return cleaned;
866
+ }
867
+ return void 0;
759
868
  }
760
869
  function isNoiseSessionTitle(text) {
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;
870
+ const lower = text.toLowerCase();
871
+ const normalized = lower.replace(/^[#>*\-\s]+/, "").trim();
872
+ if (normalized.length < 8) return true;
873
+ if (looksLikeSessionIdentifier(normalized)) return true;
874
+ if (/^[a-z0-9+/_=-]{64,}$/.test(normalized)) return true;
875
+ if (normalized.startsWith("the user opened the file ") || normalized.includes("may or may not be related to the current task")) return true;
876
+ if (/^[\w][\w.\-]*\.[a-z]{2,6}:\s/i.test(normalized)) return true;
877
+ 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;
878
+ 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") || 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;
879
+ 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;
880
+ if (normalized.startsWith("cwd:") || normalized.startsWith("shell:")) return true;
881
+ if (normalized.startsWith("your identity") || normalized.startsWith("room id:") || normalized.startsWith("worker id:")) return true;
882
+ if (normalized.startsWith("you are codex, a coding agent")) return true;
883
+ return false;
863
884
  }
864
885
  function looksLikeSessionIdentifier(text) {
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;
886
+ if (/^[0-9a-f]{8}$/.test(text)) return true;
887
+ if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(text)) return true;
888
+ if (/^session[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
889
+ if (/^turn[\s:_-]*[0-9a-f]{8,}$/.test(text)) return true;
890
+ if (/^rollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?$/.test(text)) return true;
891
+ return false;
876
892
  }
877
893
  function extractProjectName(dirName) {
878
- // -Users-vasily-projects-room room
879
- const parts = dirName.split('-').filter(Boolean);
880
- return parts[parts.length - 1] || 'unknown';
894
+ const parts = dirName.split("-").filter(Boolean);
895
+ return parts[parts.length - 1] || "unknown";
881
896
  }
882
897
  function codexSessionDirs(daysBack) {
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;
898
+ const home = os.homedir();
899
+ const results = [];
900
+ const seen = /* @__PURE__ */ new Set();
901
+ const now = /* @__PURE__ */ new Date();
902
+ const totalDays = Math.max(1, daysBack);
903
+ for (let offset = 0; offset < totalDays; offset++) {
904
+ const d = new Date(now);
905
+ d.setDate(now.getDate() - offset);
906
+ const dir = path.join(
907
+ home,
908
+ ".codex",
909
+ "sessions",
910
+ String(d.getFullYear()),
911
+ String(d.getMonth() + 1).padStart(2, "0"),
912
+ String(d.getDate()).padStart(2, "0")
913
+ );
914
+ if (seen.has(dir)) continue;
915
+ seen.add(dir);
916
+ results.push(dir);
917
+ }
918
+ return results;
898
919
  }
899
920
  function fallbackSessionTitle(projectName, agent) {
900
- const project = (projectName || '').trim();
901
- if (project)
902
- return project;
903
- return `${agent} session`;
921
+ const project = (projectName || "").trim();
922
+ if (project) return project;
923
+ return `${agent} session`;
904
924
  }
905
- //# sourceMappingURL=watcher.js.map
925
+ // Annotate the CommonJS export names for ESM import in node:
926
+ 0 && (module.exports = {
927
+ SessionWatcher
928
+ });