@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/README.md +6 -4
- package/dist/cli.js +181 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +161 -20
- 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(
|
|
@@ -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,
|
|
887
|
+
threadOptions(config, workingDirectory, selectedModel)
|
|
765
888
|
);
|
|
766
889
|
} else {
|
|
767
|
-
record.thread = codex.startThread(threadOptions(config,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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.
|
|
1646
|
+
agentVersion: "0.1.3",
|
|
1506
1647
|
connectorId: "codex"
|
|
1507
1648
|
};
|
|
1508
1649
|
socket.send(encodeBridgeMessage(hello));
|