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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/bin/codex-session-manager.js +3 -0
- package/dist/src/cli.js +821 -0
- package/dist/src/preview.js +95 -0
- package/dist/src/session-store.js +364 -0
- package/dist/src/types.js +2 -0
- package/dist/src/ui-utils.js +58 -0
- package/dist/tests/preview.test.js +68 -0
- package/dist/tests/session-store.test.js +132 -0
- package/dist/tests/ui-utils.test.js +33 -0
- package/package.json +36 -0
|
@@ -0,0 +1,132 @@
|
|
|
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 session_store_1 = require("../src/session-store");
|
|
12
|
+
async function makeCodexPaths() {
|
|
13
|
+
const root = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-rename-"));
|
|
14
|
+
const sessionsDir = node_path_1.default.join(root, "sessions");
|
|
15
|
+
const archivedDir = node_path_1.default.join(root, "archived_sessions");
|
|
16
|
+
await (0, promises_1.mkdir)(sessionsDir, { recursive: true });
|
|
17
|
+
await (0, promises_1.mkdir)(archivedDir, { recursive: true });
|
|
18
|
+
return { codexDir: root, sessionsDir, archivedDir };
|
|
19
|
+
}
|
|
20
|
+
async function writeSessionFile(filePath, payload) {
|
|
21
|
+
const meta = {
|
|
22
|
+
timestamp: payload.timestamp ?? new Date().toISOString(),
|
|
23
|
+
type: "session_meta",
|
|
24
|
+
payload,
|
|
25
|
+
};
|
|
26
|
+
const lines = [JSON.stringify(meta), JSON.stringify({ type: "noop" })].join("\n");
|
|
27
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(filePath), { recursive: true });
|
|
28
|
+
await (0, promises_1.writeFile)(filePath, `${lines}\n`, "utf8");
|
|
29
|
+
}
|
|
30
|
+
(0, node_test_1.default)("loadSessions reads active and archived sessions", async () => {
|
|
31
|
+
const paths = await makeCodexPaths();
|
|
32
|
+
const activeFile = node_path_1.default.join(paths.sessionsDir, "2025", "11", "03", "rollout-2025-11-03T14-19-52-abc.jsonl");
|
|
33
|
+
const archivedFile = node_path_1.default.join(paths.archivedDir, "rollout-2025-11-04T10-10-10-def.jsonl");
|
|
34
|
+
await writeSessionFile(activeFile, {
|
|
35
|
+
id: "active",
|
|
36
|
+
timestamp: "2025-11-03T05:19:52.935Z",
|
|
37
|
+
cwd: "/tmp/project",
|
|
38
|
+
title: "Active Session",
|
|
39
|
+
tags: ["alpha", "beta"],
|
|
40
|
+
});
|
|
41
|
+
await writeSessionFile(archivedFile, {
|
|
42
|
+
id: "archived",
|
|
43
|
+
timestamp: "2025-11-04T05:19:52.935Z",
|
|
44
|
+
cwd: "/tmp/archived",
|
|
45
|
+
title: "Archived Session",
|
|
46
|
+
tags: "gamma, delta",
|
|
47
|
+
});
|
|
48
|
+
const sessions = await (0, session_store_1.loadSessions)(paths);
|
|
49
|
+
strict_1.default.equal(sessions.length, 2);
|
|
50
|
+
const archived = sessions.find((session) => session.archived);
|
|
51
|
+
const active = sessions.find((session) => !session.archived);
|
|
52
|
+
strict_1.default.equal(active?.displayName, "Active Session");
|
|
53
|
+
strict_1.default.deepEqual(active?.tags, ["alpha", "beta"]);
|
|
54
|
+
strict_1.default.equal(archived?.displayName, "Archived Session");
|
|
55
|
+
strict_1.default.deepEqual(archived?.tags, ["gamma", "delta"]);
|
|
56
|
+
const filtered = (0, session_store_1.filterSessions)(sessions, "alpha", "all");
|
|
57
|
+
strict_1.default.equal(filtered.length, 1);
|
|
58
|
+
strict_1.default.equal(filtered[0].id, "active");
|
|
59
|
+
});
|
|
60
|
+
(0, node_test_1.default)("filterSessions respects archive filter", async () => {
|
|
61
|
+
const paths = await makeCodexPaths();
|
|
62
|
+
const activeFile = node_path_1.default.join(paths.sessionsDir, "2025", "11", "08", "rollout-2025-11-08T10-10-10-active.jsonl");
|
|
63
|
+
const archivedFile = node_path_1.default.join(paths.archivedDir, "rollout-2025-11-09T10-10-10-archived.jsonl");
|
|
64
|
+
await writeSessionFile(activeFile, {
|
|
65
|
+
id: "active",
|
|
66
|
+
timestamp: "2025-11-08T05:19:52.935Z",
|
|
67
|
+
cwd: "/tmp/active",
|
|
68
|
+
});
|
|
69
|
+
await writeSessionFile(archivedFile, {
|
|
70
|
+
id: "archived",
|
|
71
|
+
timestamp: "2025-11-09T05:19:52.935Z",
|
|
72
|
+
cwd: "/tmp/archived",
|
|
73
|
+
});
|
|
74
|
+
const sessions = await (0, session_store_1.loadSessions)(paths);
|
|
75
|
+
strict_1.default.equal((0, session_store_1.filterSessions)(sessions, "", "active").length, 1);
|
|
76
|
+
strict_1.default.equal((0, session_store_1.filterSessions)(sessions, "", "archived").length, 1);
|
|
77
|
+
strict_1.default.equal((0, session_store_1.filterSessions)(sessions, "", "all").length, 2);
|
|
78
|
+
});
|
|
79
|
+
(0, node_test_1.default)("updateSessionMetadata writes title and tags", async () => {
|
|
80
|
+
const paths = await makeCodexPaths();
|
|
81
|
+
const filePath = node_path_1.default.join(paths.sessionsDir, "2025", "11", "05", "rollout-2025-11-05T10-10-10-xyz.jsonl");
|
|
82
|
+
await writeSessionFile(filePath, {
|
|
83
|
+
id: "update",
|
|
84
|
+
timestamp: "2025-11-05T05:19:52.935Z",
|
|
85
|
+
cwd: "/tmp/update",
|
|
86
|
+
});
|
|
87
|
+
await (0, session_store_1.updateSessionMetadata)(filePath, {
|
|
88
|
+
title: "New Name",
|
|
89
|
+
tags: ["Tag", "tag", ""],
|
|
90
|
+
});
|
|
91
|
+
const content = await (0, promises_1.readFile)(filePath, "utf8");
|
|
92
|
+
const meta = JSON.parse(content.split("\n")[0]);
|
|
93
|
+
strict_1.default.equal(meta.payload.title, "New Name");
|
|
94
|
+
strict_1.default.equal(meta.payload.name, "New Name");
|
|
95
|
+
strict_1.default.deepEqual(meta.payload.tags, ["Tag"]);
|
|
96
|
+
await (0, session_store_1.updateSessionMetadata)(filePath, { title: "", tags: [] });
|
|
97
|
+
const cleared = JSON.parse((await (0, promises_1.readFile)(filePath, "utf8")).split("\n")[0]);
|
|
98
|
+
strict_1.default.equal("title" in cleared.payload, false);
|
|
99
|
+
strict_1.default.equal("tags" in cleared.payload, false);
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.default)("parseTagsInput normalizes input", () => {
|
|
102
|
+
strict_1.default.deepEqual((0, session_store_1.parseTagsInput)("alpha, beta beta"), ["alpha", "beta"]);
|
|
103
|
+
strict_1.default.deepEqual((0, session_store_1.parseTagsInput)(""), []);
|
|
104
|
+
});
|
|
105
|
+
(0, node_test_1.default)("setArchiveStatus moves sessions", async () => {
|
|
106
|
+
const paths = await makeCodexPaths();
|
|
107
|
+
const filePath = node_path_1.default.join(paths.sessionsDir, "2025", "11", "07", "rollout-2025-11-07T10-10-10-move.jsonl");
|
|
108
|
+
await writeSessionFile(filePath, {
|
|
109
|
+
id: "move",
|
|
110
|
+
timestamp: "2025-11-07T05:19:52.935Z",
|
|
111
|
+
cwd: "/tmp/move",
|
|
112
|
+
});
|
|
113
|
+
const [session] = await (0, session_store_1.loadSessions)(paths);
|
|
114
|
+
strict_1.default.ok(session);
|
|
115
|
+
const archivedPath = await (0, session_store_1.setArchiveStatus)(session, true, paths);
|
|
116
|
+
await (0, promises_1.access)(archivedPath);
|
|
117
|
+
const [archivedSession] = (await (0, session_store_1.loadSessions)(paths)).filter((item) => item.archived);
|
|
118
|
+
strict_1.default.ok(archivedSession);
|
|
119
|
+
const restoredPath = await (0, session_store_1.setArchiveStatus)(archivedSession, false, paths);
|
|
120
|
+
await (0, promises_1.access)(restoredPath);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.default)("sortSessionsByDate orders sessions by sortKey", () => {
|
|
123
|
+
const sessions = [
|
|
124
|
+
{ filePath: "a", sortKey: 2 },
|
|
125
|
+
{ filePath: "b", sortKey: 1 },
|
|
126
|
+
{ filePath: "c", sortKey: 3 },
|
|
127
|
+
];
|
|
128
|
+
const asc = (0, session_store_1.sortSessionsByDate)(sessions, "asc");
|
|
129
|
+
strict_1.default.deepEqual(asc.map((session) => session.filePath), ["b", "a", "c"]);
|
|
130
|
+
const desc = (0, session_store_1.sortSessionsByDate)(sessions, "desc");
|
|
131
|
+
strict_1.default.deepEqual(desc.map((session) => session.filePath), ["c", "a", "b"]);
|
|
132
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
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 node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const ui_utils_1 = require("../src/ui-utils");
|
|
9
|
+
(0, node_test_1.default)("buildTagIndex dedupes and sorts", () => {
|
|
10
|
+
const sessions = [
|
|
11
|
+
{ filePath: "a", tags: ["Beta", "alpha"] },
|
|
12
|
+
{ filePath: "b", tags: ["alpha", "gamma"] },
|
|
13
|
+
];
|
|
14
|
+
const tags = (0, ui_utils_1.buildTagIndex)(sessions);
|
|
15
|
+
strict_1.default.deepEqual(tags, ["alpha", "Beta", "gamma"]);
|
|
16
|
+
});
|
|
17
|
+
(0, node_test_1.default)("getTagSuggestions matches fragment and excludes used", () => {
|
|
18
|
+
const suggestions = (0, ui_utils_1.getTagSuggestions)("alpha, b", ["beta", "bravo", "alpha"], 5);
|
|
19
|
+
strict_1.default.deepEqual(suggestions, ["beta", "bravo"]);
|
|
20
|
+
});
|
|
21
|
+
(0, node_test_1.default)("getTagSuggestions supports empty fragment and limit", () => {
|
|
22
|
+
const suggestions = (0, ui_utils_1.getTagSuggestions)("", ["beta", "alpha", "gamma"], 2);
|
|
23
|
+
strict_1.default.deepEqual(suggestions, ["alpha", "beta"]);
|
|
24
|
+
});
|
|
25
|
+
(0, node_test_1.default)("getTagFragment splits prefix and fragment", () => {
|
|
26
|
+
strict_1.default.deepEqual((0, ui_utils_1.getTagFragment)("alpha, be"), {
|
|
27
|
+
prefix: "alpha, ",
|
|
28
|
+
fragment: "be",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
(0, node_test_1.default)("applyTagSuggestion replaces fragment", () => {
|
|
32
|
+
strict_1.default.equal((0, ui_utils_1.applyTagSuggestion)("alpha, b", "beta"), "alpha, beta");
|
|
33
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-session-manager",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"description": "Simple TUI to rename, tag, and archive Codex CLI sessions.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
10
|
+
"lint": "eslint . --ext .ts",
|
|
11
|
+
"start": "node dist/src/cli.js",
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"test": "npm run build && node --test dist/tests/*.test.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"codex-session-manager": "bin/codex-session-manager.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
30
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
31
|
+
"@types/node": "^25.0.3",
|
|
32
|
+
"eslint": "^9.39.2",
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"typescript": "5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|