codex-session-manager 0.1.0

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.
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractMessageText = extractMessageText;
7
+ exports.extractPreviewFromLine = extractPreviewFromLine;
8
+ exports.readMessagePreviews = readMessagePreviews;
9
+ const node_fs_1 = require("node:fs");
10
+ const node_readline_1 = __importDefault(require("node:readline"));
11
+ function normalizeWhitespace(text) {
12
+ return text.replace(/\s+/g, " ").trim();
13
+ }
14
+ function extractMessageText(payload) {
15
+ if (!payload || payload.type !== "message") {
16
+ return "";
17
+ }
18
+ const content = payload.content;
19
+ if (!Array.isArray(content)) {
20
+ return "";
21
+ }
22
+ const parts = [];
23
+ for (const part of content) {
24
+ if (!part || typeof part !== "object") {
25
+ continue;
26
+ }
27
+ const text = typeof part.text === "string"
28
+ ? part.text
29
+ : typeof part.input_text === "string"
30
+ ? part.input_text
31
+ : typeof part.output_text === "string"
32
+ ? part.output_text
33
+ : "";
34
+ if (text) {
35
+ parts.push(text);
36
+ }
37
+ }
38
+ return normalizeWhitespace(parts.join(" "));
39
+ }
40
+ function extractPreviewFromLine(line) {
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(line);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ if (!parsed.payload || typeof parsed.payload !== "object") {
49
+ return null;
50
+ }
51
+ const text = extractMessageText(parsed.payload);
52
+ if (!text) {
53
+ return null;
54
+ }
55
+ return text;
56
+ }
57
+ function truncate(text, maxChars) {
58
+ if (text.length <= maxChars) {
59
+ return text;
60
+ }
61
+ if (maxChars <= 3) {
62
+ return text.slice(0, maxChars);
63
+ }
64
+ return `${text.slice(0, maxChars - 3)}...`;
65
+ }
66
+ async function readMessagePreviews(filePath, maxChars) {
67
+ const stream = (0, node_fs_1.createReadStream)(filePath, { encoding: "utf8" });
68
+ const rl = node_readline_1.default.createInterface({ input: stream, crlfDelay: Infinity });
69
+ let first = null;
70
+ let last = null;
71
+ try {
72
+ for await (const line of rl) {
73
+ const preview = extractPreviewFromLine(line);
74
+ if (!preview) {
75
+ continue;
76
+ }
77
+ if (!first) {
78
+ first = preview;
79
+ }
80
+ last = preview;
81
+ }
82
+ }
83
+ finally {
84
+ rl.close();
85
+ stream.destroy();
86
+ }
87
+ if (!first) {
88
+ return null;
89
+ }
90
+ const safeLast = last ?? first;
91
+ return {
92
+ first: truncate(first, maxChars),
93
+ last: truncate(safeLast, maxChars),
94
+ };
95
+ }
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDefaultPaths = getDefaultPaths;
7
+ exports.loadSessions = loadSessions;
8
+ exports.filterSessions = filterSessions;
9
+ exports.parseTagsInput = parseTagsInput;
10
+ exports.sortSessionsByDate = sortSessionsByDate;
11
+ exports.updateSessionMetadata = updateSessionMetadata;
12
+ exports.setArchiveStatus = setArchiveStatus;
13
+ const promises_1 = __importDefault(require("node:fs/promises"));
14
+ const node_fs_1 = require("node:fs");
15
+ const node_os_1 = __importDefault(require("node:os"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const node_readline_1 = __importDefault(require("node:readline"));
18
+ function getDefaultPaths() {
19
+ const codexDir = node_path_1.default.join(node_os_1.default.homedir(), ".codex");
20
+ return {
21
+ codexDir,
22
+ sessionsDir: node_path_1.default.join(codexDir, "sessions"),
23
+ archivedDir: node_path_1.default.join(codexDir, "archived_sessions"),
24
+ };
25
+ }
26
+ async function collectJsonlFiles(dir) {
27
+ const results = [];
28
+ let entries;
29
+ try {
30
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
31
+ }
32
+ catch (error) {
33
+ const err = error;
34
+ if (err.code === "ENOENT") {
35
+ return results;
36
+ }
37
+ throw err;
38
+ }
39
+ for (const entry of entries) {
40
+ const fullPath = node_path_1.default.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ results.push(...(await collectJsonlFiles(fullPath)));
43
+ continue;
44
+ }
45
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
46
+ results.push(fullPath);
47
+ }
48
+ }
49
+ return results;
50
+ }
51
+ async function readFirstLine(filePath) {
52
+ const stream = (0, node_fs_1.createReadStream)(filePath, { encoding: "utf8" });
53
+ const rl = node_readline_1.default.createInterface({ input: stream, crlfDelay: Infinity });
54
+ return new Promise((resolve, reject) => {
55
+ let done = false;
56
+ rl.on("line", (line) => {
57
+ if (done) {
58
+ return;
59
+ }
60
+ done = true;
61
+ rl.close();
62
+ stream.destroy();
63
+ resolve(line);
64
+ });
65
+ rl.on("close", () => {
66
+ if (!done) {
67
+ resolve(null);
68
+ }
69
+ });
70
+ rl.on("error", (err) => {
71
+ if (!done) {
72
+ reject(err);
73
+ }
74
+ });
75
+ stream.on("error", (err) => {
76
+ if (!done) {
77
+ reject(err);
78
+ }
79
+ });
80
+ });
81
+ }
82
+ function normalizeTags(value) {
83
+ if (value == null) {
84
+ return [];
85
+ }
86
+ let raw = [];
87
+ if (Array.isArray(value)) {
88
+ raw = value.filter((item) => typeof item === "string");
89
+ }
90
+ else if (typeof value === "string") {
91
+ raw = value.split(/[\s,]+/);
92
+ }
93
+ return uniqueTags(raw.map((tag) => tag.trim()).filter(Boolean));
94
+ }
95
+ function uniqueTags(tags) {
96
+ const seen = new Set();
97
+ const result = [];
98
+ for (const tag of tags) {
99
+ const key = tag.toLowerCase();
100
+ if (seen.has(key)) {
101
+ continue;
102
+ }
103
+ seen.add(key);
104
+ result.push(tag);
105
+ }
106
+ return result;
107
+ }
108
+ function parseTimestamp(value) {
109
+ if (typeof value !== "string") {
110
+ return null;
111
+ }
112
+ const parsed = Date.parse(value);
113
+ if (Number.isNaN(parsed)) {
114
+ return null;
115
+ }
116
+ return new Date(parsed);
117
+ }
118
+ function parseDateFromFilename(fileName) {
119
+ const match = fileName.match(/(20\d{2})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
120
+ if (!match) {
121
+ return null;
122
+ }
123
+ const [, year, month, day, hour, minute, second] = match;
124
+ return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
125
+ }
126
+ function formatDateLabel(date) {
127
+ if (!date || Number.isNaN(date.valueOf())) {
128
+ return undefined;
129
+ }
130
+ return date.toISOString().slice(0, 10);
131
+ }
132
+ function pickTitle(payload) {
133
+ const title = typeof payload.title === "string" ? payload.title.trim() : "";
134
+ if (title) {
135
+ return title;
136
+ }
137
+ const name = typeof payload.name === "string" ? payload.name.trim() : "";
138
+ if (name) {
139
+ return name;
140
+ }
141
+ return undefined;
142
+ }
143
+ function deriveDisplayName(payload, fileName) {
144
+ const title = pickTitle(payload);
145
+ if (title) {
146
+ return title;
147
+ }
148
+ const cwd = typeof payload.cwd === "string" ? payload.cwd.trim() : "";
149
+ if (cwd) {
150
+ return node_path_1.default.basename(cwd);
151
+ }
152
+ return fileName.replace(/\.jsonl$/, "");
153
+ }
154
+ function parseGit(payload) {
155
+ const git = payload.git;
156
+ if (!git || typeof git !== "object") {
157
+ return undefined;
158
+ }
159
+ const repositoryUrl = typeof git.repository_url === "string" ? git.repository_url : undefined;
160
+ const branch = typeof git.branch === "string" ? git.branch : undefined;
161
+ const commitHash = typeof git.commit_hash === "string" ? git.commit_hash : undefined;
162
+ if (!repositoryUrl && !branch && !commitHash) {
163
+ return undefined;
164
+ }
165
+ return { repositoryUrl, branch, commitHash };
166
+ }
167
+ async function parseSessionFile(filePath, archived) {
168
+ const firstLine = await readFirstLine(filePath);
169
+ if (!firstLine) {
170
+ return null;
171
+ }
172
+ let meta;
173
+ try {
174
+ meta = JSON.parse(firstLine);
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ if (meta.type !== "session_meta" || !meta.payload) {
180
+ return null;
181
+ }
182
+ const payload = meta.payload;
183
+ const timestamp = typeof payload.timestamp === "string" ? payload.timestamp : undefined;
184
+ const fileName = node_path_1.default.basename(filePath);
185
+ const stat = await promises_1.default.stat(filePath);
186
+ const sortDate = parseTimestamp(timestamp) ||
187
+ parseDateFromFilename(fileName) ||
188
+ stat.mtime ||
189
+ null;
190
+ return {
191
+ id: typeof payload.id === "string" ? payload.id : undefined,
192
+ filePath,
193
+ fileName,
194
+ archived,
195
+ cwd: typeof payload.cwd === "string" ? payload.cwd : undefined,
196
+ title: pickTitle(payload),
197
+ tags: normalizeTags(payload.tags),
198
+ timestamp,
199
+ dateLabel: formatDateLabel(sortDate),
200
+ displayName: deriveDisplayName(payload, fileName),
201
+ sortKey: sortDate ? sortDate.getTime() : 0,
202
+ originator: typeof payload.originator === "string" ? payload.originator : undefined,
203
+ cliVersion: typeof payload.cli_version === "string" ? payload.cli_version : undefined,
204
+ source: typeof payload.source === "string" ? payload.source : undefined,
205
+ modelProvider: typeof payload.model_provider === "string"
206
+ ? payload.model_provider
207
+ : undefined,
208
+ git: parseGit(payload),
209
+ };
210
+ }
211
+ async function loadSessions(paths) {
212
+ const [activeFiles, archivedFiles] = await Promise.all([
213
+ collectJsonlFiles(paths.sessionsDir),
214
+ collectJsonlFiles(paths.archivedDir),
215
+ ]);
216
+ const sessions = [];
217
+ for (const filePath of activeFiles) {
218
+ const session = await parseSessionFile(filePath, false);
219
+ if (session) {
220
+ sessions.push(session);
221
+ }
222
+ }
223
+ for (const filePath of archivedFiles) {
224
+ const session = await parseSessionFile(filePath, true);
225
+ if (session) {
226
+ sessions.push(session);
227
+ }
228
+ }
229
+ return sessions;
230
+ }
231
+ function filterSessions(sessions, query, archiveFilter) {
232
+ const trimmed = query.trim().toLowerCase();
233
+ return sessions.filter((session) => {
234
+ if (archiveFilter === "archived" && !session.archived) {
235
+ return false;
236
+ }
237
+ if (archiveFilter === "active" && session.archived) {
238
+ return false;
239
+ }
240
+ if (!trimmed) {
241
+ return true;
242
+ }
243
+ const name = session.displayName.toLowerCase();
244
+ const tags = session.tags.join(" ").toLowerCase();
245
+ return name.includes(trimmed) || tags.includes(trimmed);
246
+ });
247
+ }
248
+ function parseTagsInput(input) {
249
+ if (!input.trim()) {
250
+ return [];
251
+ }
252
+ return uniqueTags(input
253
+ .split(/[\s,]+/)
254
+ .map((tag) => tag.trim())
255
+ .filter(Boolean));
256
+ }
257
+ function sortSessionsByDate(sessions, order) {
258
+ const sorted = [...sessions];
259
+ if (order === "asc") {
260
+ sorted.sort((a, b) => a.sortKey - b.sortKey);
261
+ return sorted;
262
+ }
263
+ sorted.sort((a, b) => b.sortKey - a.sortKey);
264
+ return sorted;
265
+ }
266
+ async function updateSessionMetadata(filePath, updates) {
267
+ const content = await promises_1.default.readFile(filePath, "utf8");
268
+ const lines = content.split("\n");
269
+ if (!lines[0]) {
270
+ throw new Error("Session file is empty.");
271
+ }
272
+ let meta;
273
+ try {
274
+ meta = JSON.parse(lines[0]);
275
+ }
276
+ catch {
277
+ throw new Error("Failed to parse session metadata.");
278
+ }
279
+ if (meta.type !== "session_meta" || !meta.payload) {
280
+ throw new Error("First line is not session metadata.");
281
+ }
282
+ const payload = meta.payload;
283
+ if (updates.title !== undefined) {
284
+ const cleaned = updates.title.trim();
285
+ if (cleaned) {
286
+ payload.title = cleaned;
287
+ payload.name = cleaned;
288
+ }
289
+ else {
290
+ delete payload.title;
291
+ delete payload.name;
292
+ }
293
+ }
294
+ if (updates.tags !== undefined) {
295
+ const cleanedTags = uniqueTags(updates.tags.map((tag) => tag.trim()).filter(Boolean));
296
+ if (cleanedTags.length) {
297
+ payload.tags = cleanedTags;
298
+ }
299
+ else {
300
+ delete payload.tags;
301
+ }
302
+ }
303
+ meta.payload = payload;
304
+ lines[0] = JSON.stringify(meta);
305
+ await promises_1.default.writeFile(filePath, lines.join("\n"), "utf8");
306
+ }
307
+ async function moveFile(source, destination) {
308
+ try {
309
+ await promises_1.default.rename(source, destination);
310
+ }
311
+ catch (error) {
312
+ const err = error;
313
+ if (err.code !== "EXDEV") {
314
+ throw err;
315
+ }
316
+ await promises_1.default.copyFile(source, destination);
317
+ await promises_1.default.unlink(source);
318
+ }
319
+ }
320
+ function resolveActivePath(fileName, timestamp, fallbackTime, paths) {
321
+ const date = parseTimestamp(timestamp) || parseDateFromFilename(fileName) || fallbackTime;
322
+ const dateLabel = formatDateLabel(date);
323
+ if (!dateLabel) {
324
+ throw new Error("Unable to determine session date.");
325
+ }
326
+ const [year, month, day] = dateLabel.split("-");
327
+ return node_path_1.default.join(paths.sessionsDir, year, month, day, fileName);
328
+ }
329
+ async function setArchiveStatus(session, targetArchived, paths) {
330
+ if (session.archived === targetArchived) {
331
+ return session.filePath;
332
+ }
333
+ if (targetArchived) {
334
+ const destination = node_path_1.default.join(paths.archivedDir, session.fileName);
335
+ await promises_1.default.mkdir(paths.archivedDir, { recursive: true });
336
+ try {
337
+ await promises_1.default.access(destination);
338
+ throw new Error("Archived session already exists.");
339
+ }
340
+ catch (error) {
341
+ const err = error;
342
+ if (err.code !== "ENOENT") {
343
+ throw err;
344
+ }
345
+ }
346
+ await moveFile(session.filePath, destination);
347
+ return destination;
348
+ }
349
+ const stat = await promises_1.default.stat(session.filePath);
350
+ const destination = resolveActivePath(session.fileName, session.timestamp, stat.mtime, paths);
351
+ await promises_1.default.mkdir(node_path_1.default.dirname(destination), { recursive: true });
352
+ try {
353
+ await promises_1.default.access(destination);
354
+ throw new Error("Active session already exists.");
355
+ }
356
+ catch (error) {
357
+ const err = error;
358
+ if (err.code !== "ENOENT") {
359
+ throw err;
360
+ }
361
+ }
362
+ await moveFile(session.filePath, destination);
363
+ return destination;
364
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTagIndex = buildTagIndex;
4
+ exports.getTagFragment = getTagFragment;
5
+ exports.getTagSuggestions = getTagSuggestions;
6
+ exports.applyTagSuggestion = applyTagSuggestion;
7
+ const session_store_1 = require("./session-store");
8
+ function buildTagIndex(sessions) {
9
+ const seen = new Set();
10
+ const result = [];
11
+ for (const session of sessions) {
12
+ for (const tag of session.tags) {
13
+ const key = tag.toLowerCase();
14
+ if (seen.has(key)) {
15
+ continue;
16
+ }
17
+ seen.add(key);
18
+ result.push(tag);
19
+ }
20
+ }
21
+ result.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
22
+ return result;
23
+ }
24
+ function getTagFragment(input) {
25
+ const match = input.match(/^(.*?)([^,\s]*)$/);
26
+ if (!match) {
27
+ return { prefix: input, fragment: "" };
28
+ }
29
+ return { prefix: match[1], fragment: match[2] };
30
+ }
31
+ function getTagSuggestions(input, allTags, limit) {
32
+ if (!allTags.length) {
33
+ return [];
34
+ }
35
+ const { fragment } = getTagFragment(input);
36
+ const fragmentLower = fragment.toLowerCase();
37
+ const used = (0, session_store_1.parseTagsInput)(input);
38
+ const usedSet = new Set(used.map((tag) => tag.toLowerCase()));
39
+ if (fragmentLower) {
40
+ usedSet.delete(fragmentLower);
41
+ }
42
+ const matches = allTags.filter((tag) => {
43
+ const lower = tag.toLowerCase();
44
+ if (usedSet.has(lower)) {
45
+ return false;
46
+ }
47
+ if (!fragmentLower) {
48
+ return true;
49
+ }
50
+ return lower.startsWith(fragmentLower);
51
+ });
52
+ matches.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
53
+ return matches.slice(0, limit);
54
+ }
55
+ function applyTagSuggestion(input, suggestion) {
56
+ const { prefix } = getTagFragment(input);
57
+ return `${prefix}${suggestion}`;
58
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_test_1 = __importDefault(require("node:test"));
11
+ const preview_1 = require("../src/preview");
12
+ (0, node_test_1.default)("extractMessageText joins message parts", () => {
13
+ const text = (0, preview_1.extractMessageText)({
14
+ type: "message",
15
+ content: [{ text: "Hello" }, { input_text: "world" }],
16
+ });
17
+ strict_1.default.equal(text, "Hello world");
18
+ });
19
+ (0, node_test_1.default)("extractPreviewFromLine returns message preview", () => {
20
+ const line = JSON.stringify({
21
+ payload: {
22
+ type: "message",
23
+ content: [{ text: "Preview text" }],
24
+ },
25
+ });
26
+ strict_1.default.equal((0, preview_1.extractPreviewFromLine)(line), "Preview text");
27
+ });
28
+ (0, node_test_1.default)("readMessagePreviews returns first and last messages", async () => {
29
+ const dir = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-preview-"));
30
+ const filePath = node_path_1.default.join(dir, "session.jsonl");
31
+ const lines = [
32
+ JSON.stringify({ type: "session_meta", payload: { id: "1" } }),
33
+ JSON.stringify({ payload: { type: "event", content: [] } }),
34
+ JSON.stringify({ payload: { type: "message", content: [{ text: "Hello world" }] } }),
35
+ JSON.stringify({ payload: { type: "message", content: [{ text: "Later message" }] } }),
36
+ ];
37
+ await (0, promises_1.writeFile)(filePath, `${lines.join("\n")}\n`, "utf8");
38
+ const previews = await (0, preview_1.readMessagePreviews)(filePath, 20);
39
+ strict_1.default.deepEqual(previews, {
40
+ first: "Hello world",
41
+ last: "Later message",
42
+ });
43
+ });
44
+ (0, node_test_1.default)("readMessagePreviews returns same first and last for single message", async () => {
45
+ const dir = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-preview-single-"));
46
+ const filePath = node_path_1.default.join(dir, "session.jsonl");
47
+ const lines = [
48
+ JSON.stringify({ type: "session_meta", payload: { id: "1" } }),
49
+ JSON.stringify({ payload: { type: "message", content: [{ text: "Only message" }] } }),
50
+ ];
51
+ await (0, promises_1.writeFile)(filePath, `${lines.join("\n")}\n`, "utf8");
52
+ const previews = await (0, preview_1.readMessagePreviews)(filePath, 50);
53
+ strict_1.default.deepEqual(previews, {
54
+ first: "Only message",
55
+ last: "Only message",
56
+ });
57
+ });
58
+ (0, node_test_1.default)("readMessagePreviews returns null when no messages", async () => {
59
+ const dir = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-preview-empty-"));
60
+ const filePath = node_path_1.default.join(dir, "session.jsonl");
61
+ const lines = [
62
+ JSON.stringify({ type: "session_meta", payload: { id: "1" } }),
63
+ JSON.stringify({ payload: { type: "event", content: [] } }),
64
+ ];
65
+ await (0, promises_1.writeFile)(filePath, `${lines.join("\n")}\n`, "utf8");
66
+ const previews = await (0, preview_1.readMessagePreviews)(filePath, 50);
67
+ strict_1.default.equal(previews, null);
68
+ });