@tumnel/codex 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { z as z2 } from "zod";
8
8
  import { createHash, randomBytes } from "crypto";
9
9
  import { createReadStream } from "fs";
10
10
  import { homedir } from "os";
11
- import { join } from "path";
11
+ import { dirname, join } from "path";
12
12
  import { readFile, readdir, stat } from "fs/promises";
13
13
  import { z } from "zod";
14
14
  var sessionMetaSchema = z.object({
@@ -22,8 +22,10 @@ var sessionMetaSchema = z.object({
22
22
  }).passthrough()
23
23
  });
24
24
  function defaultSessionsDir(environment = process.env) {
25
- const root = environment.CODEX_HOME ?? join(homedir(), ".codex");
26
- return join(root, "sessions");
25
+ return join(defaultCodexHome(environment), "sessions");
26
+ }
27
+ function defaultCodexHome(environment = process.env) {
28
+ return environment.CODEX_HOME ?? join(homedir(), ".codex");
27
29
  }
28
30
  function projectId(directory) {
29
31
  const hash = createHash("sha256").update(directory.toLowerCase()).digest("base64url").slice(0, 12);
@@ -78,6 +80,87 @@ async function readRolloutTitle(path, fallback) {
78
80
  }
79
81
  return title;
80
82
  }
83
+ async function readSessionIndex(path) {
84
+ const result = /* @__PURE__ */ new Map();
85
+ let source;
86
+ try {
87
+ source = await readFile(path, "utf8");
88
+ } catch {
89
+ return result;
90
+ }
91
+ for (const line of source.split(/\r?\n/)) {
92
+ if (!line.trim()) continue;
93
+ try {
94
+ const value = JSON.parse(line);
95
+ const id = stringValue(value.id);
96
+ if (!id) continue;
97
+ result.set(id, {
98
+ thread_name: stringValue(value.thread_name) ?? void 0,
99
+ updated_at: stringValue(value.updated_at) ?? void 0
100
+ });
101
+ } catch {
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ async function readGlobalProjects(path) {
107
+ let source;
108
+ try {
109
+ source = await readFile(path, "utf8");
110
+ } catch {
111
+ return [];
112
+ }
113
+ try {
114
+ const root = asRecord(JSON.parse(source));
115
+ const projects = asRecord(root?.["local-projects"]);
116
+ const order = Array.isArray(root?.["project-order"]) ? root?.["project-order"].filter((value) => typeof value === "string") : [];
117
+ const orderById = new Map(order.map((id, index) => [id, index]));
118
+ const result = [];
119
+ for (const [id, value] of Object.entries(projects ?? {})) {
120
+ const project = asRecord(value);
121
+ const name = stringValue(project?.name) ?? void 0;
122
+ const roots = Array.isArray(project?.rootPaths) ? project.rootPaths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
123
+ if (!roots.length) continue;
124
+ const created = timestampValue(project?.createdAt, 0);
125
+ const updated = timestampValue(project?.updatedAt, created);
126
+ for (const directory of roots) {
127
+ result.push({
128
+ id,
129
+ name,
130
+ directory,
131
+ created,
132
+ updated,
133
+ order: orderById.get(id) ?? Number.MAX_SAFE_INTEGER
134
+ });
135
+ }
136
+ }
137
+ return result.sort((left, right) => left.order - right.order || left.created - right.created);
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
142
+ async function readGlobalSessionAssignments(path) {
143
+ let source;
144
+ try {
145
+ source = await readFile(path, "utf8");
146
+ } catch {
147
+ return /* @__PURE__ */ new Map();
148
+ }
149
+ try {
150
+ const root = asRecord(JSON.parse(source));
151
+ const assignments = asRecord(root?.["thread-project-assignments"]);
152
+ const result = /* @__PURE__ */ new Map();
153
+ for (const [threadId, value] of Object.entries(assignments ?? {})) {
154
+ const assignment = asRecord(value);
155
+ const projectId2 = stringValue(assignment?.projectId);
156
+ const projectKind = stringValue(assignment?.projectKind);
157
+ if (projectId2 && (!projectKind || projectKind === "local")) result.set(threadId, projectId2);
158
+ }
159
+ return result;
160
+ } catch {
161
+ return /* @__PURE__ */ new Map();
162
+ }
163
+ }
81
164
  async function walkRollouts(dir) {
82
165
  let entries;
83
166
  try {
@@ -287,8 +370,9 @@ async function readRolloutHistory(path, sessionId) {
287
370
  }
288
371
  return { messages, title, updated };
289
372
  }
290
- async function discoverSessions(sessionsDir) {
373
+ async function discoverSessions(sessionsDir, sessionIndexPath = join(dirname(sessionsDir), "session_index.jsonl"), projectRootsById = /* @__PURE__ */ new Map(), projectAssignments = /* @__PURE__ */ new Map()) {
291
374
  const rollouts = await walkRollouts(sessionsDir);
375
+ const index = await readSessionIndex(sessionIndexPath);
292
376
  const sessions = [];
293
377
  for (const path of rollouts) {
294
378
  const line = await readFirstLine(path);
@@ -303,7 +387,11 @@ async function discoverSessions(sessionsDir) {
303
387
  if (!result.success) continue;
304
388
  const meta = result.data.payload;
305
389
  const id = meta.session_id ?? meta.id;
306
- if (!id || !meta.cwd) continue;
390
+ if (!id) continue;
391
+ const workingDirectory = meta.cwd;
392
+ const assignedProjectId = projectAssignments.get(id);
393
+ const directory = (assignedProjectId ? projectRootsById.get(assignedProjectId) : void 0) ?? workingDirectory;
394
+ if (!directory) continue;
307
395
  const created = meta.timestamp ? Date.parse(meta.timestamp) : NaN;
308
396
  let mtime = created;
309
397
  try {
@@ -312,13 +400,16 @@ async function discoverSessions(sessionsDir) {
312
400
  } catch {
313
401
  mtime = Number.isFinite(created) ? created : 0;
314
402
  }
403
+ const indexed = index.get(id);
404
+ const indexedUpdated = indexed?.updated_at ? Date.parse(indexed.updated_at) : NaN;
315
405
  sessions.push({
316
406
  id,
317
- directory: meta.cwd,
407
+ directory,
408
+ ...workingDirectory && workingDirectory !== directory ? { workingDirectory } : {},
318
409
  version: meta.cli_version ?? "unknown",
319
410
  created: Number.isFinite(created) ? created : mtime,
320
- updated: mtime,
321
- title: await readRolloutTitle(path, "(untitled session)"),
411
+ updated: Number.isFinite(indexedUpdated) ? Math.max(mtime, indexedUpdated) : mtime,
412
+ title: await readRolloutTitle(path, indexed?.thread_name ?? "(untitled session)"),
322
413
  rolloutPath: path
323
414
  });
324
415
  }
@@ -334,12 +425,19 @@ var CodexSessionStore = class {
334
425
  at: 0,
335
426
  byDirectory: /* @__PURE__ */ new Map()
336
427
  };
428
+ projectCache = {
429
+ at: 0,
430
+ projects: [],
431
+ rootsById: /* @__PURE__ */ new Map(),
432
+ assignments: /* @__PURE__ */ new Map()
433
+ };
337
434
  create(input) {
338
435
  const now = Date.now();
339
436
  const record = {
340
437
  id: input.id,
341
438
  projectID: projectId(input.directory),
342
439
  directory: input.directory,
440
+ workingDirectory: input.workingDirectory,
343
441
  title: input.title ?? "(untitled session)",
344
442
  version: input.version,
345
443
  created: now,
@@ -375,7 +473,13 @@ var CodexSessionStore = class {
375
473
  async refreshDiscovery() {
376
474
  const now = Date.now();
377
475
  if (this.discoveryCache.at !== 0 && now - this.discoveryCache.at <= 3e4) return;
378
- const discovered = await discoverSessions(this.sessionsDir);
476
+ await this.refreshProjectCatalog(now);
477
+ const discovered = await discoverSessions(
478
+ this.sessionsDir,
479
+ join(dirname(this.sessionsDir), "session_index.jsonl"),
480
+ this.projectCache.rootsById,
481
+ this.projectCache.assignments
482
+ );
379
483
  const byDirectory = /* @__PURE__ */ new Map();
380
484
  for (const session of discovered) {
381
485
  const list = byDirectory.get(session.directory) ?? [];
@@ -389,6 +493,24 @@ var CodexSessionStore = class {
389
493
  await this.refreshDiscovery();
390
494
  return directory ? [...this.discoveryCache.byDirectory.entries()].filter(([dir]) => samePath(dir, directory)).flatMap(([, sessions]) => sessions) : [...this.discoveryCache.byDirectory.values()].flat();
391
495
  }
496
+ async discoverProjects(directory) {
497
+ const now = Date.now();
498
+ await this.refreshProjectCatalog(now);
499
+ return directory ? this.projectCache.projects.filter((project) => samePath(project.directory, directory)) : [...this.projectCache.projects];
500
+ }
501
+ async refreshProjectCatalog(now = Date.now()) {
502
+ if (this.projectCache.at !== 0 && now - this.projectCache.at <= 3e4) return;
503
+ const globalStatePath = join(dirname(this.sessionsDir), ".codex-global-state.json");
504
+ const projects = await readGlobalProjects(globalStatePath);
505
+ const rootsById = /* @__PURE__ */ new Map();
506
+ for (const project of projects) {
507
+ if (!rootsById.has(project.id)) rootsById.set(project.id, project.directory);
508
+ }
509
+ this.projectCache.projects = projects;
510
+ this.projectCache.rootsById = rootsById;
511
+ this.projectCache.assignments = await readGlobalSessionAssignments(globalStatePath);
512
+ this.projectCache.at = now;
513
+ }
392
514
  async discover(directory) {
393
515
  const matching = await this.discoverAll(directory);
394
516
  const liveThreadIds = new Set(
@@ -758,13 +880,14 @@ function ensureThread(codex, record, config, model) {
758
880
  const selectedModel = model ?? record.model ?? config.model;
759
881
  if (record.thread && record.model === selectedModel) return record.thread;
760
882
  record.model = selectedModel;
883
+ const workingDirectory = record.workingDirectory ?? record.directory;
761
884
  if (record.codexThreadId) {
762
885
  record.thread = codex.resumeThread(
763
886
  record.codexThreadId,
764
- threadOptions(config, record.directory, selectedModel)
887
+ threadOptions(config, workingDirectory, selectedModel)
765
888
  );
766
889
  } else {
767
- record.thread = codex.startThread(threadOptions(config, record.directory, selectedModel));
890
+ record.thread = codex.startThread(threadOptions(config, workingDirectory, selectedModel));
768
891
  }
769
892
  return record.thread;
770
893
  }
@@ -874,25 +997,42 @@ function createCodexAdapter(options) {
874
997
  case "project.list": {
875
998
  const params = projectListSchema.parse(rawParams);
876
999
  const directory = params.directory || void 0;
1000
+ const catalogProjects = await sessions.discoverProjects(directory);
877
1001
  const records = sessions.list(directory);
878
1002
  const discovered = await sessions.discoverAll(directory);
879
1003
  const directories = /* @__PURE__ */ new Map();
1004
+ for (const project of catalogProjects) {
1005
+ directories.set(project.directory, {
1006
+ created: project.created,
1007
+ name: project.name,
1008
+ order: project.order
1009
+ });
1010
+ }
880
1011
  for (const record of records) {
881
1012
  const existing = directories.get(record.directory);
882
- if (existing === void 0 || record.created < existing) {
883
- directories.set(record.directory, record.created);
1013
+ if (existing === void 0 || record.created < existing.created) {
1014
+ directories.set(record.directory, {
1015
+ created: record.created,
1016
+ name: existing?.name,
1017
+ order: existing?.order ?? Number.MAX_SAFE_INTEGER
1018
+ });
884
1019
  }
885
1020
  }
886
1021
  for (const session of discovered) {
887
1022
  const existing = directories.get(session.directory);
888
- if (existing === void 0 || session.created < existing) {
889
- directories.set(session.directory, session.created);
1023
+ if (existing === void 0 || session.created < existing.created) {
1024
+ directories.set(session.directory, {
1025
+ created: session.created,
1026
+ name: existing?.name,
1027
+ order: existing?.order ?? Number.MAX_SAFE_INTEGER
1028
+ });
890
1029
  }
891
1030
  }
892
- return [...directories.entries()].map(([worktree, created]) => ({
1031
+ return [...directories.entries()].sort(([, left], [, right]) => left.order - right.order || left.created - right.created).map(([worktree, project]) => ({
893
1032
  id: projectId(worktree),
1033
+ ...project.name ? { name: project.name } : {},
894
1034
  worktree,
895
- time: { created }
1035
+ time: { created: project.created }
896
1036
  }));
897
1037
  }
898
1038
  case "session.list": {
@@ -963,6 +1103,7 @@ function createCodexAdapter(options) {
963
1103
  record = sessions.create({
964
1104
  id: params.sessionId,
965
1105
  directory: session.directory,
1106
+ workingDirectory: session.workingDirectory,
966
1107
  title: history?.title ?? session.title,
967
1108
  version: session.version,
968
1109
  codexThreadId: session.id,
@@ -1245,7 +1386,7 @@ import {
1245
1386
  verify
1246
1387
  } from "crypto";
1247
1388
  import { homedir as homedir2 } from "os";
1248
- import { dirname, join as join2 } from "path";
1389
+ import { dirname as dirname2, join as join2 } from "path";
1249
1390
  import { mkdir, open, readFile as readFile2 } from "fs/promises";
1250
1391
  import { z as z4 } from "zod";
1251
1392
  var DEVICE_PROOF_PREFIX = "tumnel-device-proof-v1";
@@ -1305,7 +1446,7 @@ async function readIdentity(path) {
1305
1446
  return validateKeyPair(identitySchema.parse(JSON.parse(contents)));
1306
1447
  }
1307
1448
  async function persistNewIdentity(path, identity) {
1308
- await mkdir(dirname(path), { recursive: true, mode: 448 });
1449
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
1309
1450
  const handle = await open(path, "wx", 384);
1310
1451
  try {
1311
1452
  await handle.writeFile(`${JSON.stringify(identity, null, 2)}
@@ -1502,7 +1643,7 @@ var TumnelBridgeClient = class {
1502
1643
  version: BRIDGE_PROTOCOL_VERSION,
1503
1644
  type: "device.hello",
1504
1645
  clientId: this.identity.deviceId,
1505
- agentVersion: "0.1.1",
1646
+ agentVersion: "0.1.3",
1506
1647
  connectorId: "codex"
1507
1648
  };
1508
1649
  socket.send(encodeBridgeMessage(hello));