@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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/bin/cdx.js +232 -48
  3. 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 rm here` or `cdx here rm` to remove a session from `.cdx` in the current directory
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 findCdxFile(startDir) {
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
- const check = spawnSync("codex", ["--version"], { stdio: "ignore" });
140
- if (check.error && check.error.code === "ENOENT") {
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 = await selectSession(entries);
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, options) {
227
- const found = options.here ? null : findCdxFile(startDir);
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
- if (!fs.existsSync(targetPath)) {
232
- console.log("No .cdx file found.");
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
- const response = await prompts({
244
- type: "select",
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
- if (!response.selection) return;
254
- const remaining = entries.filter((entry) => entry.uuid !== response.selection);
255
- writeEntries(targetPath, remaining);
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 rm here
266
- cdx here rm
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 (subcommand === "rm" || (here && args.includes("rm"))) {
290
- await runRemove(startDir, { here });
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fclef819/cdx",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Codex session wrapper",
5
5
  "keywords": [
6
6
  "codex",