@fclef819/cdx 0.1.0 → 0.1.2
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 +8 -1
- package/bin/cdx.js +232 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,9 +30,16 @@ Each line is:
|
|
|
30
30
|
|
|
31
31
|
- `cdx` to select or create a session
|
|
32
32
|
- `cdx here` to use `.cdx` from the current directory without parent search
|
|
33
|
+
- `cdx new` to create a new session without the selection UI
|
|
34
|
+
- `cdx new here` or `cdx here new` to create a new session using `.cdx` from the current directory
|
|
33
35
|
- `cdx rm` to remove a session from `.cdx`
|
|
34
|
-
- `cdx
|
|
36
|
+
- `cdx init` to create an empty `.cdx` in the current directory
|
|
37
|
+
- `cdx add <uuid> <label>` to add a session to `.cdx`
|
|
38
|
+
- `cdx add <uuid>` to add a session and prompt for the label
|
|
39
|
+
- `cdx add` to add a session and prompt for uuid and label
|
|
35
40
|
- `cdx -h`, `cdx --help`, or `cdx help` to show help
|
|
41
|
+
- `cdx -V` or `cdx --version` to show version
|
|
42
|
+
- `cdx -v` or `cdx --verbose` to show verbose logs
|
|
36
43
|
|
|
37
44
|
## Install (npm)
|
|
38
45
|
|
package/bin/cdx.js
CHANGED
|
@@ -11,11 +11,21 @@ const CODEX_HOME = path.join(process.env.HOME || "", ".codex");
|
|
|
11
11
|
const HISTORY_PATH = path.join(CODEX_HOME, "history.jsonl");
|
|
12
12
|
const SESSIONS_DIR = path.join(CODEX_HOME, "sessions");
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
function logVerbose(message, enabled) {
|
|
15
|
+
if (enabled) console.log(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapePowerShellArg(value) {
|
|
19
|
+
if (value === "") return "''";
|
|
20
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findCdxFile(startDir, verbose) {
|
|
15
24
|
let dir = path.resolve(startDir);
|
|
16
25
|
while (true) {
|
|
17
26
|
const candidate = path.join(dir, CDX_FILENAME);
|
|
18
27
|
if (fs.existsSync(candidate)) {
|
|
28
|
+
logVerbose(`Found .cdx at: ${candidate}`, verbose);
|
|
19
29
|
return { dir, filePath: candidate };
|
|
20
30
|
}
|
|
21
31
|
const parent = path.dirname(dir);
|
|
@@ -46,16 +56,19 @@ function sanitizeLabel(label) {
|
|
|
46
56
|
return label.replace(/[\t\n\r]+/g, " ").trim();
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
function appendEntry(filePath, entry) {
|
|
59
|
+
function appendEntry(filePath, entry, verbose) {
|
|
50
60
|
const line = `${entry.uuid}\t${entry.label}\n`;
|
|
61
|
+
logVerbose(`Appending entry to ${filePath}`, verbose);
|
|
51
62
|
fs.appendFileSync(filePath, line, "utf8");
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
function writeEntries(filePath, entries) {
|
|
65
|
+
function writeEntries(filePath, entries, verbose) {
|
|
55
66
|
if (!entries.length) {
|
|
67
|
+
logVerbose(`Removing empty .cdx at ${filePath}`, verbose);
|
|
56
68
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
57
69
|
return;
|
|
58
70
|
}
|
|
71
|
+
logVerbose(`Writing ${entries.length} entries to ${filePath}`, verbose);
|
|
59
72
|
const content = entries.map((e) => `${e.uuid}\t${e.label}`).join("\n") + "\n";
|
|
60
73
|
fs.writeFileSync(filePath, content, "utf8");
|
|
61
74
|
}
|
|
@@ -95,17 +108,18 @@ function readSessionIdFromFile(filePath) {
|
|
|
95
108
|
return extractIdFromFilename(filePath);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
function getLatestSessionSnapshot() {
|
|
111
|
+
function getLatestSessionSnapshot(verbose) {
|
|
99
112
|
const files = [];
|
|
100
113
|
collectSessionFiles(SESSIONS_DIR, files);
|
|
101
114
|
if (!files.length) return null;
|
|
102
115
|
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
103
116
|
const latest = files[files.length - 1];
|
|
104
117
|
const id = readSessionIdFromFile(latest.path);
|
|
118
|
+
logVerbose(`Latest session file: ${latest.path}`, verbose);
|
|
105
119
|
return { id, mtimeMs: latest.mtimeMs, path: latest.path };
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
function getLastHistorySessionId() {
|
|
122
|
+
function getLastHistorySessionId(verbose) {
|
|
109
123
|
if (!fs.existsSync(HISTORY_PATH)) return null;
|
|
110
124
|
const lines = fs.readFileSync(HISTORY_PATH, "utf8").trim().split("\n");
|
|
111
125
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -116,10 +130,11 @@ function getLastHistorySessionId() {
|
|
|
116
130
|
// ignore malformed lines
|
|
117
131
|
}
|
|
118
132
|
}
|
|
133
|
+
logVerbose("No session_id found in history.jsonl", verbose);
|
|
119
134
|
return null;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
function getNewestHistorySessionIdSince(previousId) {
|
|
137
|
+
function getNewestHistorySessionIdSince(previousId, verbose) {
|
|
123
138
|
if (!fs.existsSync(HISTORY_PATH)) return null;
|
|
124
139
|
const lines = fs.readFileSync(HISTORY_PATH, "utf8").trim().split("\n");
|
|
125
140
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -132,26 +147,72 @@ function getNewestHistorySessionIdSince(previousId) {
|
|
|
132
147
|
// ignore malformed lines
|
|
133
148
|
}
|
|
134
149
|
}
|
|
150
|
+
logVerbose("No new session_id found in history.jsonl", verbose);
|
|
135
151
|
return null;
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
function runCodex(args, cwd) {
|
|
139
|
-
|
|
140
|
-
|
|
154
|
+
function runCodex(args, cwd, verbose) {
|
|
155
|
+
logVerbose(`Running codex ${args.join(" ")}`.trim(), verbose);
|
|
156
|
+
const result = spawnSync("codex", args, { stdio: "inherit", cwd });
|
|
157
|
+
if (result.error) {
|
|
158
|
+
if (result.error.code === "ENOENT" && process.platform === "win32") {
|
|
159
|
+
logVerbose("codex not found directly; trying PowerShell fallback", verbose);
|
|
160
|
+
const psArgs = [
|
|
161
|
+
"-NoProfile",
|
|
162
|
+
"-Command",
|
|
163
|
+
["codex", ...args.map(escapePowerShellArg)].join(" ")
|
|
164
|
+
];
|
|
165
|
+
const psResult = spawnSync("powershell.exe", psArgs, {
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
cwd
|
|
168
|
+
});
|
|
169
|
+
if (!psResult.error) {
|
|
170
|
+
if (psResult.status !== 0) process.exit(psResult.status ?? 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
141
174
|
console.error(
|
|
142
175
|
"Codex CLI is not available. Please install it and ensure `codex` is on your PATH."
|
|
143
176
|
);
|
|
144
177
|
process.exit(1);
|
|
145
178
|
}
|
|
146
|
-
const result = spawnSync("codex", args, { stdio: "inherit", cwd });
|
|
147
|
-
if (result.error) {
|
|
148
|
-
console.error("Failed to run codex:", result.error.message);
|
|
149
|
-
process.exit(result.status ?? 1);
|
|
150
|
-
}
|
|
151
179
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
152
180
|
}
|
|
153
181
|
|
|
182
|
+
function printSessionList(entries) {
|
|
183
|
+
console.log("0: new");
|
|
184
|
+
entries.forEach((entry, index) => {
|
|
185
|
+
console.log(`${index + 1}: ${entry.label} (${entry.uuid})`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
async function selectSession(entries) {
|
|
190
|
+
if (!entries.length) {
|
|
191
|
+
return { type: "new" };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
printSessionList(entries);
|
|
195
|
+
while (true) {
|
|
196
|
+
const numberInput = await prompts({
|
|
197
|
+
type: "text",
|
|
198
|
+
name: "value",
|
|
199
|
+
message: "Select by number (Enter for list selection)",
|
|
200
|
+
validate: (value) => {
|
|
201
|
+
if (!value || !value.trim()) return true;
|
|
202
|
+
if (!/^\d+$/.test(value.trim())) return "Enter a number";
|
|
203
|
+
const index = Number.parseInt(value.trim(), 10);
|
|
204
|
+
if (index < 0 || index > entries.length) return "Out of range";
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!numberInput.value && numberInput.value !== "0") break;
|
|
210
|
+
const index = Number.parseInt(String(numberInput.value).trim(), 10);
|
|
211
|
+
if (Number.isNaN(index)) break;
|
|
212
|
+
if (index === 0) return { type: "new" };
|
|
213
|
+
return { type: "resume", entry: entries[index - 1] };
|
|
214
|
+
}
|
|
215
|
+
|
|
155
216
|
const choices = [
|
|
156
217
|
{ title: "new", value: { type: "new" } },
|
|
157
218
|
...entries.map((entry) => ({
|
|
@@ -180,24 +241,36 @@ async function promptLabel() {
|
|
|
180
241
|
return response.label;
|
|
181
242
|
}
|
|
182
243
|
|
|
244
|
+
async function promptUuid() {
|
|
245
|
+
const response = await prompts({
|
|
246
|
+
type: "text",
|
|
247
|
+
name: "uuid",
|
|
248
|
+
message: "Session UUID",
|
|
249
|
+
validate: (value) => (value && value.trim() ? true : "UUID is required")
|
|
250
|
+
});
|
|
251
|
+
return response.uuid;
|
|
252
|
+
}
|
|
253
|
+
|
|
183
254
|
async function runDefault(startDir, options) {
|
|
184
|
-
const found = options.here ? null : findCdxFile(startDir);
|
|
255
|
+
const found = options.here ? null : findCdxFile(startDir, options.verbose);
|
|
185
256
|
const workDir = found ? found.dir : startDir;
|
|
186
257
|
const cdxPath = found ? found.filePath : path.join(startDir, CDX_FILENAME);
|
|
187
258
|
const entries = loadEntries(found?.filePath);
|
|
188
259
|
|
|
189
260
|
console.log(`.cdx: ${cdxPath}`);
|
|
190
|
-
const selection =
|
|
261
|
+
const selection = options.forceNew
|
|
262
|
+
? { type: "new" }
|
|
263
|
+
: await selectSession(entries);
|
|
191
264
|
if (!selection) return;
|
|
192
265
|
|
|
193
266
|
if (selection.type === "new") {
|
|
194
267
|
const labelInput = await promptLabel();
|
|
195
268
|
if (!labelInput) return;
|
|
196
269
|
const label = sanitizeLabel(labelInput);
|
|
197
|
-
const previousHistoryId = getLastHistorySessionId();
|
|
198
|
-
const previousSession = getLatestSessionSnapshot();
|
|
199
|
-
runCodex([], workDir);
|
|
200
|
-
const latestSession = getLatestSessionSnapshot();
|
|
270
|
+
const previousHistoryId = getLastHistorySessionId(options.verbose);
|
|
271
|
+
const previousSession = getLatestSessionSnapshot(options.verbose);
|
|
272
|
+
runCodex([], workDir, options.verbose);
|
|
273
|
+
const latestSession = getLatestSessionSnapshot(options.verbose);
|
|
201
274
|
let newId = null;
|
|
202
275
|
if (
|
|
203
276
|
latestSession &&
|
|
@@ -208,31 +281,28 @@ async function runDefault(startDir, options) {
|
|
|
208
281
|
) {
|
|
209
282
|
newId = latestSession.id;
|
|
210
283
|
} else if (previousHistoryId) {
|
|
211
|
-
newId = getNewestHistorySessionIdSince(previousHistoryId);
|
|
284
|
+
newId = getNewestHistorySessionIdSince(previousHistoryId, options.verbose);
|
|
212
285
|
}
|
|
213
286
|
if (!newId) {
|
|
214
287
|
console.error("Could not determine new session UUID; not updating .cdx.");
|
|
215
288
|
return;
|
|
216
289
|
}
|
|
217
|
-
appendEntry(cdxPath, { uuid: newId, label });
|
|
290
|
+
appendEntry(cdxPath, { uuid: newId, label }, options.verbose);
|
|
218
291
|
return;
|
|
219
292
|
}
|
|
220
293
|
|
|
221
294
|
if (selection.type === "resume") {
|
|
222
|
-
runCodex(["resume", selection.entry.uuid], workDir);
|
|
295
|
+
runCodex(["resume", selection.entry.uuid], workDir, options.verbose);
|
|
223
296
|
}
|
|
224
297
|
}
|
|
225
298
|
|
|
226
|
-
async function runRemove(startDir,
|
|
227
|
-
const found =
|
|
228
|
-
const targetPath = found ? found.filePath : path.join(startDir, CDX_FILENAME);
|
|
229
|
-
const targetDir = found ? found.dir : startDir;
|
|
299
|
+
async function runRemove(startDir, verbose) {
|
|
300
|
+
const found = findCdxFile(startDir, verbose);
|
|
230
301
|
if (!found) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
302
|
+
console.log("No .cdx file found.");
|
|
303
|
+
return;
|
|
235
304
|
}
|
|
305
|
+
const targetPath = found.filePath;
|
|
236
306
|
const entries = loadEntries(targetPath);
|
|
237
307
|
if (!entries.length) {
|
|
238
308
|
console.log("No sessions to remove.");
|
|
@@ -240,19 +310,90 @@ async function runRemove(startDir, options) {
|
|
|
240
310
|
}
|
|
241
311
|
|
|
242
312
|
console.log(`.cdx: ${targetPath}`);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
name: "selection",
|
|
246
|
-
message: "Select a session to remove",
|
|
247
|
-
choices: entries.map((entry) => ({
|
|
248
|
-
title: `${entry.label} (${entry.uuid})`,
|
|
249
|
-
value: entry.uuid
|
|
250
|
-
}))
|
|
313
|
+
entries.forEach((entry, index) => {
|
|
314
|
+
console.log(`${index + 1}: ${entry.label} (${entry.uuid})`);
|
|
251
315
|
});
|
|
252
316
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
317
|
+
let selectedUuid = null;
|
|
318
|
+
while (true) {
|
|
319
|
+
const numberInput = await prompts({
|
|
320
|
+
type: "text",
|
|
321
|
+
name: "value",
|
|
322
|
+
message: "Select by number (Enter for list selection)",
|
|
323
|
+
validate: (value) => {
|
|
324
|
+
if (!value || !value.trim()) return true;
|
|
325
|
+
if (!/^\d+$/.test(value.trim())) return "Enter a number";
|
|
326
|
+
const index = Number.parseInt(value.trim(), 10);
|
|
327
|
+
if (index < 1 || index > entries.length) return "Out of range";
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!numberInput.value) break;
|
|
333
|
+
const index = Number.parseInt(String(numberInput.value).trim(), 10);
|
|
334
|
+
if (!Number.isNaN(index)) {
|
|
335
|
+
selectedUuid = entries[index - 1].uuid;
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!selectedUuid) {
|
|
341
|
+
const response = await prompts({
|
|
342
|
+
type: "select",
|
|
343
|
+
name: "selection",
|
|
344
|
+
message: "Select a session to remove",
|
|
345
|
+
choices: entries.map((entry) => ({
|
|
346
|
+
title: `${entry.label} (${entry.uuid})`,
|
|
347
|
+
value: entry.uuid
|
|
348
|
+
}))
|
|
349
|
+
});
|
|
350
|
+
if (!response.selection) return;
|
|
351
|
+
selectedUuid = response.selection;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const remaining = entries.filter((entry) => entry.uuid !== selectedUuid);
|
|
355
|
+
writeEntries(targetPath, remaining, verbose);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function printAddHelp() {
|
|
359
|
+
console.log(`Usage:
|
|
360
|
+
cdx add <uuid> <label>
|
|
361
|
+
cdx add <uuid>
|
|
362
|
+
cdx add`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function runInit(startDir, verbose) {
|
|
366
|
+
const cdxPath = path.join(startDir, CDX_FILENAME);
|
|
367
|
+
if (fs.existsSync(cdxPath)) {
|
|
368
|
+
console.log(".cdx already exists in the current directory.");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
fs.writeFileSync(cdxPath, "", "utf8");
|
|
372
|
+
logVerbose(`Created .cdx at ${cdxPath}`, verbose);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runAdd(startDir, args, verbose) {
|
|
376
|
+
if (args.length > 3 || (args.length === 2 && args[1] === "")) {
|
|
377
|
+
printAddHelp();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const found = findCdxFile(startDir, verbose);
|
|
382
|
+
const cdxPath = found ? found.filePath : path.join(startDir, CDX_FILENAME);
|
|
383
|
+
let uuid = args[1];
|
|
384
|
+
let label = args[2];
|
|
385
|
+
|
|
386
|
+
if (!uuid) {
|
|
387
|
+
uuid = await promptUuid();
|
|
388
|
+
}
|
|
389
|
+
if (!uuid) return;
|
|
390
|
+
|
|
391
|
+
if (!label) {
|
|
392
|
+
label = await promptLabel();
|
|
393
|
+
}
|
|
394
|
+
if (!label) return;
|
|
395
|
+
|
|
396
|
+
appendEntry(cdxPath, { uuid: uuid.trim(), label: sanitizeLabel(label) }, verbose);
|
|
256
397
|
}
|
|
257
398
|
|
|
258
399
|
function printHelp() {
|
|
@@ -261,16 +402,37 @@ function printHelp() {
|
|
|
261
402
|
Usage:
|
|
262
403
|
cdx
|
|
263
404
|
cdx here
|
|
405
|
+
cdx new
|
|
406
|
+
cdx new here
|
|
407
|
+
cdx here new
|
|
264
408
|
cdx rm
|
|
265
|
-
cdx
|
|
266
|
-
cdx
|
|
409
|
+
cdx init
|
|
410
|
+
cdx add <uuid> <label>
|
|
411
|
+
cdx add <uuid>
|
|
412
|
+
cdx add
|
|
413
|
+
cdx -V
|
|
414
|
+
cdx --version
|
|
415
|
+
cdx -v
|
|
416
|
+
cdx --verbose
|
|
267
417
|
|
|
268
418
|
Notes:
|
|
269
419
|
- "here" skips parent directory search and uses .cdx in the current directory
|
|
270
420
|
- the selected .cdx path is shown before session selection
|
|
421
|
+
- use -h, --help, or help to show this message
|
|
422
|
+
- use -v or --verbose to show debug logs
|
|
271
423
|
`);
|
|
272
424
|
}
|
|
273
425
|
|
|
426
|
+
function printVersion() {
|
|
427
|
+
try {
|
|
428
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
429
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
430
|
+
console.log(pkg.version || "unknown");
|
|
431
|
+
} catch {
|
|
432
|
+
console.log("unknown");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
274
436
|
async function main() {
|
|
275
437
|
const args = process.argv.slice(2);
|
|
276
438
|
const subcommand = args[0];
|
|
@@ -280,23 +442,45 @@ async function main() {
|
|
|
280
442
|
args.includes("-h") ||
|
|
281
443
|
args.includes("--help") ||
|
|
282
444
|
args.includes("help");
|
|
445
|
+
const wantsVersion = args.includes("-V") || args.includes("--version");
|
|
446
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
283
447
|
|
|
284
448
|
if (wantsHelp) {
|
|
285
449
|
printHelp();
|
|
286
450
|
return;
|
|
287
451
|
}
|
|
288
452
|
|
|
289
|
-
if (
|
|
290
|
-
|
|
453
|
+
if (wantsVersion) {
|
|
454
|
+
printVersion();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (subcommand === "rm") {
|
|
459
|
+
await runRemove(startDir, verbose);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (subcommand === "add") {
|
|
464
|
+
await runAdd(startDir, args, verbose);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (subcommand === "init") {
|
|
469
|
+
runInit(startDir, verbose);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (subcommand === "new" || (here && args.includes("new"))) {
|
|
474
|
+
await runDefault(startDir, { here, forceNew: true, verbose });
|
|
291
475
|
return;
|
|
292
476
|
}
|
|
293
477
|
|
|
294
478
|
if (subcommand === "here" || here) {
|
|
295
|
-
await runDefault(startDir, { here: true });
|
|
479
|
+
await runDefault(startDir, { here: true, verbose });
|
|
296
480
|
return;
|
|
297
481
|
}
|
|
298
482
|
|
|
299
|
-
await runDefault(startDir, { here: false });
|
|
483
|
+
await runDefault(startDir, { here: false, verbose });
|
|
300
484
|
}
|
|
301
485
|
|
|
302
486
|
main().catch((err) => {
|