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