@fclef819/cdx 0.1.0 → 0.1.1

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 +136 -31
  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,16 @@ 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 findCdxFile(startDir, verbose) {
15
19
  let dir = path.resolve(startDir);
16
20
  while (true) {
17
21
  const candidate = path.join(dir, CDX_FILENAME);
18
22
  if (fs.existsSync(candidate)) {
23
+ logVerbose(`Found .cdx at: ${candidate}`, verbose);
19
24
  return { dir, filePath: candidate };
20
25
  }
21
26
  const parent = path.dirname(dir);
@@ -46,16 +51,19 @@ function sanitizeLabel(label) {
46
51
  return label.replace(/[\t\n\r]+/g, " ").trim();
47
52
  }
48
53
 
49
- function appendEntry(filePath, entry) {
54
+ function appendEntry(filePath, entry, verbose) {
50
55
  const line = `${entry.uuid}\t${entry.label}\n`;
56
+ logVerbose(`Appending entry to ${filePath}`, verbose);
51
57
  fs.appendFileSync(filePath, line, "utf8");
52
58
  }
53
59
 
54
- function writeEntries(filePath, entries) {
60
+ function writeEntries(filePath, entries, verbose) {
55
61
  if (!entries.length) {
62
+ logVerbose(`Removing empty .cdx at ${filePath}`, verbose);
56
63
  if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
57
64
  return;
58
65
  }
66
+ logVerbose(`Writing ${entries.length} entries to ${filePath}`, verbose);
59
67
  const content = entries.map((e) => `${e.uuid}\t${e.label}`).join("\n") + "\n";
60
68
  fs.writeFileSync(filePath, content, "utf8");
61
69
  }
@@ -95,17 +103,18 @@ function readSessionIdFromFile(filePath) {
95
103
  return extractIdFromFilename(filePath);
96
104
  }
97
105
 
98
- function getLatestSessionSnapshot() {
106
+ function getLatestSessionSnapshot(verbose) {
99
107
  const files = [];
100
108
  collectSessionFiles(SESSIONS_DIR, files);
101
109
  if (!files.length) return null;
102
110
  files.sort((a, b) => a.mtimeMs - b.mtimeMs);
103
111
  const latest = files[files.length - 1];
104
112
  const id = readSessionIdFromFile(latest.path);
113
+ logVerbose(`Latest session file: ${latest.path}`, verbose);
105
114
  return { id, mtimeMs: latest.mtimeMs, path: latest.path };
106
115
  }
107
116
 
108
- function getLastHistorySessionId() {
117
+ function getLastHistorySessionId(verbose) {
109
118
  if (!fs.existsSync(HISTORY_PATH)) return null;
110
119
  const lines = fs.readFileSync(HISTORY_PATH, "utf8").trim().split("\n");
111
120
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -116,10 +125,11 @@ function getLastHistorySessionId() {
116
125
  // ignore malformed lines
117
126
  }
118
127
  }
128
+ logVerbose("No session_id found in history.jsonl", verbose);
119
129
  return null;
120
130
  }
121
131
 
122
- function getNewestHistorySessionIdSince(previousId) {
132
+ function getNewestHistorySessionIdSince(previousId, verbose) {
123
133
  if (!fs.existsSync(HISTORY_PATH)) return null;
124
134
  const lines = fs.readFileSync(HISTORY_PATH, "utf8").trim().split("\n");
125
135
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -132,10 +142,12 @@ function getNewestHistorySessionIdSince(previousId) {
132
142
  // ignore malformed lines
133
143
  }
134
144
  }
145
+ logVerbose("No new session_id found in history.jsonl", verbose);
135
146
  return null;
136
147
  }
137
148
 
138
- function runCodex(args, cwd) {
149
+ function runCodex(args, cwd, verbose) {
150
+ logVerbose(`Running codex ${args.join(" ")}`.trim(), verbose);
139
151
  const check = spawnSync("codex", ["--version"], { stdio: "ignore" });
140
152
  if (check.error && check.error.code === "ENOENT") {
141
153
  console.error(
@@ -180,24 +192,36 @@ async function promptLabel() {
180
192
  return response.label;
181
193
  }
182
194
 
195
+ async function promptUuid() {
196
+ const response = await prompts({
197
+ type: "text",
198
+ name: "uuid",
199
+ message: "Session UUID",
200
+ validate: (value) => (value && value.trim() ? true : "UUID is required")
201
+ });
202
+ return response.uuid;
203
+ }
204
+
183
205
  async function runDefault(startDir, options) {
184
- const found = options.here ? null : findCdxFile(startDir);
206
+ const found = options.here ? null : findCdxFile(startDir, options.verbose);
185
207
  const workDir = found ? found.dir : startDir;
186
208
  const cdxPath = found ? found.filePath : path.join(startDir, CDX_FILENAME);
187
209
  const entries = loadEntries(found?.filePath);
188
210
 
189
211
  console.log(`.cdx: ${cdxPath}`);
190
- const selection = await selectSession(entries);
212
+ const selection = options.forceNew
213
+ ? { type: "new" }
214
+ : await selectSession(entries);
191
215
  if (!selection) return;
192
216
 
193
217
  if (selection.type === "new") {
194
218
  const labelInput = await promptLabel();
195
219
  if (!labelInput) return;
196
220
  const label = sanitizeLabel(labelInput);
197
- const previousHistoryId = getLastHistorySessionId();
198
- const previousSession = getLatestSessionSnapshot();
199
- runCodex([], workDir);
200
- const latestSession = getLatestSessionSnapshot();
221
+ const previousHistoryId = getLastHistorySessionId(options.verbose);
222
+ const previousSession = getLatestSessionSnapshot(options.verbose);
223
+ runCodex([], workDir, options.verbose);
224
+ const latestSession = getLatestSessionSnapshot(options.verbose);
201
225
  let newId = null;
202
226
  if (
203
227
  latestSession &&
@@ -208,31 +232,28 @@ async function runDefault(startDir, options) {
208
232
  ) {
209
233
  newId = latestSession.id;
210
234
  } else if (previousHistoryId) {
211
- newId = getNewestHistorySessionIdSince(previousHistoryId);
235
+ newId = getNewestHistorySessionIdSince(previousHistoryId, options.verbose);
212
236
  }
213
237
  if (!newId) {
214
238
  console.error("Could not determine new session UUID; not updating .cdx.");
215
239
  return;
216
240
  }
217
- appendEntry(cdxPath, { uuid: newId, label });
241
+ appendEntry(cdxPath, { uuid: newId, label }, options.verbose);
218
242
  return;
219
243
  }
220
244
 
221
245
  if (selection.type === "resume") {
222
- runCodex(["resume", selection.entry.uuid], workDir);
246
+ runCodex(["resume", selection.entry.uuid], workDir, options.verbose);
223
247
  }
224
248
  }
225
249
 
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;
250
+ async function runRemove(startDir, verbose) {
251
+ const found = findCdxFile(startDir, verbose);
230
252
  if (!found) {
231
- if (!fs.existsSync(targetPath)) {
232
- console.log("No .cdx file found.");
233
- return;
234
- }
253
+ console.log("No .cdx file found.");
254
+ return;
235
255
  }
256
+ const targetPath = found.filePath;
236
257
  const entries = loadEntries(targetPath);
237
258
  if (!entries.length) {
238
259
  console.log("No sessions to remove.");
@@ -252,7 +273,48 @@ async function runRemove(startDir, options) {
252
273
 
253
274
  if (!response.selection) return;
254
275
  const remaining = entries.filter((entry) => entry.uuid !== response.selection);
255
- writeEntries(targetPath, remaining);
276
+ writeEntries(targetPath, remaining, verbose);
277
+ }
278
+
279
+ function printAddHelp() {
280
+ console.log(`Usage:
281
+ cdx add <uuid> <label>
282
+ cdx add <uuid>
283
+ cdx add`);
284
+ }
285
+
286
+ function runInit(startDir, verbose) {
287
+ const cdxPath = path.join(startDir, CDX_FILENAME);
288
+ if (fs.existsSync(cdxPath)) {
289
+ console.log(".cdx already exists in the current directory.");
290
+ return;
291
+ }
292
+ fs.writeFileSync(cdxPath, "", "utf8");
293
+ logVerbose(`Created .cdx at ${cdxPath}`, verbose);
294
+ }
295
+
296
+ async function runAdd(startDir, args, verbose) {
297
+ if (args.length > 3 || (args.length === 2 && args[1] === "")) {
298
+ printAddHelp();
299
+ return;
300
+ }
301
+
302
+ const found = findCdxFile(startDir, verbose);
303
+ const cdxPath = found ? found.filePath : path.join(startDir, CDX_FILENAME);
304
+ let uuid = args[1];
305
+ let label = args[2];
306
+
307
+ if (!uuid) {
308
+ uuid = await promptUuid();
309
+ }
310
+ if (!uuid) return;
311
+
312
+ if (!label) {
313
+ label = await promptLabel();
314
+ }
315
+ if (!label) return;
316
+
317
+ appendEntry(cdxPath, { uuid: uuid.trim(), label: sanitizeLabel(label) }, verbose);
256
318
  }
257
319
 
258
320
  function printHelp() {
@@ -261,16 +323,37 @@ function printHelp() {
261
323
  Usage:
262
324
  cdx
263
325
  cdx here
326
+ cdx new
327
+ cdx new here
328
+ cdx here new
264
329
  cdx rm
265
- cdx rm here
266
- cdx here rm
330
+ cdx init
331
+ cdx add <uuid> <label>
332
+ cdx add <uuid>
333
+ cdx add
334
+ cdx -V
335
+ cdx --version
336
+ cdx -v
337
+ cdx --verbose
267
338
 
268
339
  Notes:
269
340
  - "here" skips parent directory search and uses .cdx in the current directory
270
341
  - the selected .cdx path is shown before session selection
342
+ - use -h, --help, or help to show this message
343
+ - use -v or --verbose to show debug logs
271
344
  `);
272
345
  }
273
346
 
347
+ function printVersion() {
348
+ try {
349
+ const pkgPath = path.join(__dirname, "..", "package.json");
350
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
351
+ console.log(pkg.version || "unknown");
352
+ } catch {
353
+ console.log("unknown");
354
+ }
355
+ }
356
+
274
357
  async function main() {
275
358
  const args = process.argv.slice(2);
276
359
  const subcommand = args[0];
@@ -280,23 +363,45 @@ async function main() {
280
363
  args.includes("-h") ||
281
364
  args.includes("--help") ||
282
365
  args.includes("help");
366
+ const wantsVersion = args.includes("-V") || args.includes("--version");
367
+ const verbose = args.includes("-v") || args.includes("--verbose");
283
368
 
284
369
  if (wantsHelp) {
285
370
  printHelp();
286
371
  return;
287
372
  }
288
373
 
289
- if (subcommand === "rm" || (here && args.includes("rm"))) {
290
- await runRemove(startDir, { here });
374
+ if (wantsVersion) {
375
+ printVersion();
376
+ return;
377
+ }
378
+
379
+ if (subcommand === "rm") {
380
+ await runRemove(startDir, verbose);
381
+ return;
382
+ }
383
+
384
+ if (subcommand === "add") {
385
+ await runAdd(startDir, args, verbose);
386
+ return;
387
+ }
388
+
389
+ if (subcommand === "init") {
390
+ runInit(startDir, verbose);
391
+ return;
392
+ }
393
+
394
+ if (subcommand === "new" || (here && args.includes("new"))) {
395
+ await runDefault(startDir, { here, forceNew: true, verbose });
291
396
  return;
292
397
  }
293
398
 
294
399
  if (subcommand === "here" || here) {
295
- await runDefault(startDir, { here: true });
400
+ await runDefault(startDir, { here: true, verbose });
296
401
  return;
297
402
  }
298
403
 
299
- await runDefault(startDir, { here: false });
404
+ await runDefault(startDir, { here: false, verbose });
300
405
  }
301
406
 
302
407
  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.1",
4
4
  "description": "Codex session wrapper",
5
5
  "keywords": [
6
6
  "codex",