@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.
- package/README.md +11 -8
- package/dist/cli/index.js +2676 -341
- package/dist/core/commentary.js +929 -602
- package/dist/core/platform-support.js +127 -155
- package/dist/core/session-dispatch.js +391 -379
- package/dist/core/watcher.js +852 -829
- package/package.json +5 -3
package/dist/core/watcher.js
CHANGED
|
@@ -1,905 +1,928 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
agent,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
220
325
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
368
|
+
if (!this.watched.has(filePath)) {
|
|
369
|
+
const projectName = extractProjectName(dir);
|
|
370
|
+
this.watchFile(filePath, "claude", projectName);
|
|
323
371
|
}
|
|
372
|
+
}
|
|
324
373
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
459
|
-
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
return void 0;
|
|
460
640
|
}
|
|
461
641
|
function extractCodexSessionTitle(filePath, sessionId) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
499
|
-
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
return void 0;
|
|
500
671
|
}
|
|
501
672
|
function extractCodexProjectName(filePath) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
522
|
-
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
return void 0;
|
|
523
690
|
}
|
|
524
691
|
function isKibitzInternalCodexSession(filePath) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
564
717
|
}
|
|
565
718
|
function isKibitzInternalClaudeUserLine(line) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
583
|
-
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
584
734
|
}
|
|
585
735
|
function isKibitzInternalCodexLine(line) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
611
751
|
}
|
|
612
752
|
function looksLikeKibitzGeneratedPrompt(text) {
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
652
|
-
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
return false;
|
|
653
786
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
787
|
+
var codexThreadTitlesCache = {
|
|
788
|
+
mtimeMs: -1,
|
|
789
|
+
titles: /* @__PURE__ */ new Map(),
|
|
790
|
+
order: []
|
|
658
791
|
};
|
|
659
792
|
function readCodexThreadTitles() {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return normalizedFallback;
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
return normalizedFallback;
|
|
743
856
|
}
|
|
744
857
|
function pickSessionTitle(raw) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
return `${agent} session`;
|
|
921
|
+
const project = (projectName || "").trim();
|
|
922
|
+
if (project) return project;
|
|
923
|
+
return `${agent} session`;
|
|
904
924
|
}
|
|
905
|
-
|
|
925
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
926
|
+
0 && (module.exports = {
|
|
927
|
+
SessionWatcher
|
|
928
|
+
});
|