@tumnel/codex 0.1.3 → 0.1.5

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(
@@ -536,6 +658,11 @@ var questionRejectSchema = z2.object({
536
658
  requestId: z2.string().min(1).max(256),
537
659
  directory: directorySchema
538
660
  }).strict();
661
+ var CODEX_DEFAULT_MODEL_ID = "__codex_default__";
662
+ var CODEX_DEFAULT_MODEL_NAME = "Codex default";
663
+ function normalizeModelId(model) {
664
+ return model === CODEX_DEFAULT_MODEL_ID ? void 0 : model;
665
+ }
539
666
  var EventBus = class {
540
667
  queue = [];
541
668
  wake = null;
@@ -758,13 +885,14 @@ function ensureThread(codex, record, config, model) {
758
885
  const selectedModel = model ?? record.model ?? config.model;
759
886
  if (record.thread && record.model === selectedModel) return record.thread;
760
887
  record.model = selectedModel;
888
+ const workingDirectory = record.workingDirectory ?? record.directory;
761
889
  if (record.codexThreadId) {
762
890
  record.thread = codex.resumeThread(
763
891
  record.codexThreadId,
764
- threadOptions(config, record.directory, selectedModel)
892
+ threadOptions(config, workingDirectory, selectedModel)
765
893
  );
766
894
  } else {
767
- record.thread = codex.startThread(threadOptions(config, record.directory, selectedModel));
895
+ record.thread = codex.startThread(threadOptions(config, workingDirectory, selectedModel));
768
896
  }
769
897
  return record.thread;
770
898
  }
@@ -874,25 +1002,42 @@ function createCodexAdapter(options) {
874
1002
  case "project.list": {
875
1003
  const params = projectListSchema.parse(rawParams);
876
1004
  const directory = params.directory || void 0;
1005
+ const catalogProjects = await sessions.discoverProjects(directory);
877
1006
  const records = sessions.list(directory);
878
1007
  const discovered = await sessions.discoverAll(directory);
879
1008
  const directories = /* @__PURE__ */ new Map();
1009
+ for (const project of catalogProjects) {
1010
+ directories.set(project.directory, {
1011
+ created: project.created,
1012
+ name: project.name,
1013
+ order: project.order
1014
+ });
1015
+ }
880
1016
  for (const record of records) {
881
1017
  const existing = directories.get(record.directory);
882
- if (existing === void 0 || record.created < existing) {
883
- directories.set(record.directory, record.created);
1018
+ if (existing === void 0 || record.created < existing.created) {
1019
+ directories.set(record.directory, {
1020
+ created: record.created,
1021
+ name: existing?.name,
1022
+ order: existing?.order ?? Number.MAX_SAFE_INTEGER
1023
+ });
884
1024
  }
885
1025
  }
886
1026
  for (const session of discovered) {
887
1027
  const existing = directories.get(session.directory);
888
- if (existing === void 0 || session.created < existing) {
889
- directories.set(session.directory, session.created);
1028
+ if (existing === void 0 || session.created < existing.created) {
1029
+ directories.set(session.directory, {
1030
+ created: session.created,
1031
+ name: existing?.name,
1032
+ order: existing?.order ?? Number.MAX_SAFE_INTEGER
1033
+ });
890
1034
  }
891
1035
  }
892
- return [...directories.entries()].map(([worktree, created]) => ({
1036
+ return [...directories.entries()].sort(([, left], [, right]) => left.order - right.order || left.created - right.created).map(([worktree, project]) => ({
893
1037
  id: projectId(worktree),
1038
+ ...project.name ? { name: project.name } : {},
894
1039
  worktree,
895
- time: { created }
1040
+ time: { created: project.created }
896
1041
  }));
897
1042
  }
898
1043
  case "session.list": {
@@ -951,7 +1096,7 @@ function createCodexAdapter(options) {
951
1096
  }
952
1097
  case "session.prompt": {
953
1098
  const params = sessionPromptSchema.parse(rawParams);
954
- const selectedModel = params.model ? params.model.providerID === "codex" ? params.model.modelID : (() => {
1099
+ const selectedModel = params.model ? params.model.providerID === "codex" ? normalizeModelId(params.model.modelID) : (() => {
955
1100
  throw new Error(`Codex does not support provider ${params.model?.providerID}`);
956
1101
  })() : void 0;
957
1102
  let record = sessions.get(params.sessionId);
@@ -963,6 +1108,7 @@ function createCodexAdapter(options) {
963
1108
  record = sessions.create({
964
1109
  id: params.sessionId,
965
1110
  directory: session.directory,
1111
+ workingDirectory: session.workingDirectory,
966
1112
  title: history?.title ?? session.title,
967
1113
  version: session.version,
968
1114
  codexThreadId: session.id,
@@ -985,17 +1131,18 @@ function createCodexAdapter(options) {
985
1131
  }
986
1132
  case "provider.list": {
987
1133
  providerListSchema.parse(rawParams);
988
- if (!config.model) return toSafeJson({ default: {}, providers: [] });
1134
+ const modelID = config.model ?? CODEX_DEFAULT_MODEL_ID;
1135
+ const modelName = config.model ?? CODEX_DEFAULT_MODEL_NAME;
989
1136
  return toSafeJson({
990
- default: { codex: config.model },
1137
+ default: { codex: modelID },
991
1138
  providers: [
992
1139
  {
993
1140
  id: "codex",
994
1141
  name: "Codex",
995
1142
  models: [
996
1143
  {
997
- id: config.model,
998
- name: config.model,
1144
+ id: modelID,
1145
+ name: modelName,
999
1146
  attachment: false,
1000
1147
  reasoning: true,
1001
1148
  toolCall: true,
@@ -1245,7 +1392,7 @@ import {
1245
1392
  verify
1246
1393
  } from "crypto";
1247
1394
  import { homedir as homedir2 } from "os";
1248
- import { dirname, join as join2 } from "path";
1395
+ import { dirname as dirname2, join as join2 } from "path";
1249
1396
  import { mkdir, open, readFile as readFile2 } from "fs/promises";
1250
1397
  import { z as z4 } from "zod";
1251
1398
  var DEVICE_PROOF_PREFIX = "tumnel-device-proof-v1";
@@ -1305,7 +1452,7 @@ async function readIdentity(path) {
1305
1452
  return validateKeyPair(identitySchema.parse(JSON.parse(contents)));
1306
1453
  }
1307
1454
  async function persistNewIdentity(path, identity) {
1308
- await mkdir(dirname(path), { recursive: true, mode: 448 });
1455
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
1309
1456
  const handle = await open(path, "wx", 384);
1310
1457
  try {
1311
1458
  await handle.writeFile(`${JSON.stringify(identity, null, 2)}