@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/README.md +9 -4
- package/dist/cli.js +171 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +171 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
892
|
+
threadOptions(config, workingDirectory, selectedModel)
|
|
765
893
|
);
|
|
766
894
|
} else {
|
|
767
|
-
record.thread = codex.startThread(threadOptions(config,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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:
|
|
1137
|
+
default: { codex: modelID },
|
|
991
1138
|
providers: [
|
|
992
1139
|
{
|
|
993
1140
|
id: "codex",
|
|
994
1141
|
name: "Codex",
|
|
995
1142
|
models: [
|
|
996
1143
|
{
|
|
997
|
-
id:
|
|
998
|
-
name:
|
|
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(
|
|
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)}
|