@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/README.md
CHANGED
|
@@ -10,13 +10,13 @@ with the same experience as the OpenCode connector: sessions, streaming turns, t
|
|
|
10
10
|
The standard setup command prepares the host identity and starts the connector:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
npx @tumnel/codex@0.1.
|
|
13
|
+
npx @tumnel/codex@0.1.5 install
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
`connect` remains available as an explicit alias:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
npx @tumnel/codex@0.1.
|
|
19
|
+
npx @tumnel/codex@0.1.5 connect
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The connector starts the Codex agent bridge and keeps it connected to the Tumnel relay until
|
|
@@ -26,6 +26,9 @@ interrupted:
|
|
|
26
26
|
npx @tumnel/codex --model gpt-5-mini --sandbox workspace-write --approval-policy on-request
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
If `--model` (or `CODEX_MODEL`) is omitted, the web model picker still exposes a
|
|
30
|
+
`Codex default` option and the SDK resolves the model from the local Codex configuration.
|
|
31
|
+
|
|
29
32
|
It binds a pairing-only endpoint to `127.0.0.1:43817` (use `--port` to override). It accepts
|
|
30
33
|
approved OpenBridge origins, returns short-lived signed proofs, and never exposes Codex API keys
|
|
31
34
|
or model credentials over localhost.
|
|
@@ -58,8 +61,10 @@ Legacy Codex identities are migrated automatically when no shared or OpenCode id
|
|
|
58
61
|
- A turn (`session.prompt`) runs `thread.runStreamed(input, { signal })`; every thread event is
|
|
59
62
|
relayed as a `session.status` / `message.updated` / `message.part.updated` / `session.idle`
|
|
60
63
|
bridge event, and the observed messages are served back through `session.messages`.
|
|
61
|
-
-
|
|
62
|
-
|
|
64
|
+
- Projects and threads are discovered from the Codex desktop catalog (`~/.codex/.codex-global-state.json`
|
|
65
|
+
and `~/.codex/session_index.jsonl`) plus the persisted rollout files under
|
|
66
|
+
`~/.codex/sessions/**/rollout-*.jsonl`. This keeps project names/order and session titles aligned
|
|
67
|
+
with the local Codex app while still allowing older rollouts to be reconstructed and resumed.
|
|
63
68
|
|
|
64
69
|
## Known limitations
|
|
65
70
|
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { z as z2 } from "zod";
|
|
|
10
10
|
import { createHash, randomBytes } from "crypto";
|
|
11
11
|
import { createReadStream } from "fs";
|
|
12
12
|
import { homedir } from "os";
|
|
13
|
-
import { join } from "path";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
14
|
import { readFile, readdir, stat } from "fs/promises";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
var sessionMetaSchema = z.object({
|
|
@@ -24,8 +24,10 @@ var sessionMetaSchema = z.object({
|
|
|
24
24
|
}).passthrough()
|
|
25
25
|
});
|
|
26
26
|
function defaultSessionsDir(environment = process.env) {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
return join(defaultCodexHome(environment), "sessions");
|
|
28
|
+
}
|
|
29
|
+
function defaultCodexHome(environment = process.env) {
|
|
30
|
+
return environment.CODEX_HOME ?? join(homedir(), ".codex");
|
|
29
31
|
}
|
|
30
32
|
function projectId(directory) {
|
|
31
33
|
const hash = createHash("sha256").update(directory.toLowerCase()).digest("base64url").slice(0, 12);
|
|
@@ -80,6 +82,87 @@ async function readRolloutTitle(path, fallback) {
|
|
|
80
82
|
}
|
|
81
83
|
return title;
|
|
82
84
|
}
|
|
85
|
+
async function readSessionIndex(path) {
|
|
86
|
+
const result = /* @__PURE__ */ new Map();
|
|
87
|
+
let source;
|
|
88
|
+
try {
|
|
89
|
+
source = await readFile(path, "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
for (const line of source.split(/\r?\n/)) {
|
|
94
|
+
if (!line.trim()) continue;
|
|
95
|
+
try {
|
|
96
|
+
const value = JSON.parse(line);
|
|
97
|
+
const id = stringValue(value.id);
|
|
98
|
+
if (!id) continue;
|
|
99
|
+
result.set(id, {
|
|
100
|
+
thread_name: stringValue(value.thread_name) ?? void 0,
|
|
101
|
+
updated_at: stringValue(value.updated_at) ?? void 0
|
|
102
|
+
});
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
async function readGlobalProjects(path) {
|
|
109
|
+
let source;
|
|
110
|
+
try {
|
|
111
|
+
source = await readFile(path, "utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const root = asRecord(JSON.parse(source));
|
|
117
|
+
const projects = asRecord(root?.["local-projects"]);
|
|
118
|
+
const order = Array.isArray(root?.["project-order"]) ? root?.["project-order"].filter((value) => typeof value === "string") : [];
|
|
119
|
+
const orderById = new Map(order.map((id, index) => [id, index]));
|
|
120
|
+
const result = [];
|
|
121
|
+
for (const [id, value] of Object.entries(projects ?? {})) {
|
|
122
|
+
const project = asRecord(value);
|
|
123
|
+
const name = stringValue(project?.name) ?? void 0;
|
|
124
|
+
const roots = Array.isArray(project?.rootPaths) ? project.rootPaths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
|
|
125
|
+
if (!roots.length) continue;
|
|
126
|
+
const created = timestampValue(project?.createdAt, 0);
|
|
127
|
+
const updated = timestampValue(project?.updatedAt, created);
|
|
128
|
+
for (const directory of roots) {
|
|
129
|
+
result.push({
|
|
130
|
+
id,
|
|
131
|
+
name,
|
|
132
|
+
directory,
|
|
133
|
+
created,
|
|
134
|
+
updated,
|
|
135
|
+
order: orderById.get(id) ?? Number.MAX_SAFE_INTEGER
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result.sort((left, right) => left.order - right.order || left.created - right.created);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function readGlobalSessionAssignments(path) {
|
|
145
|
+
let source;
|
|
146
|
+
try {
|
|
147
|
+
source = await readFile(path, "utf8");
|
|
148
|
+
} catch {
|
|
149
|
+
return /* @__PURE__ */ new Map();
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const root = asRecord(JSON.parse(source));
|
|
153
|
+
const assignments = asRecord(root?.["thread-project-assignments"]);
|
|
154
|
+
const result = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const [threadId, value] of Object.entries(assignments ?? {})) {
|
|
156
|
+
const assignment = asRecord(value);
|
|
157
|
+
const projectId2 = stringValue(assignment?.projectId);
|
|
158
|
+
const projectKind = stringValue(assignment?.projectKind);
|
|
159
|
+
if (projectId2 && (!projectKind || projectKind === "local")) result.set(threadId, projectId2);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
} catch {
|
|
163
|
+
return /* @__PURE__ */ new Map();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
83
166
|
async function walkRollouts(dir) {
|
|
84
167
|
let entries;
|
|
85
168
|
try {
|
|
@@ -289,8 +372,9 @@ async function readRolloutHistory(path, sessionId) {
|
|
|
289
372
|
}
|
|
290
373
|
return { messages, title, updated };
|
|
291
374
|
}
|
|
292
|
-
async function discoverSessions(sessionsDir) {
|
|
375
|
+
async function discoverSessions(sessionsDir, sessionIndexPath = join(dirname(sessionsDir), "session_index.jsonl"), projectRootsById = /* @__PURE__ */ new Map(), projectAssignments = /* @__PURE__ */ new Map()) {
|
|
293
376
|
const rollouts = await walkRollouts(sessionsDir);
|
|
377
|
+
const index = await readSessionIndex(sessionIndexPath);
|
|
294
378
|
const sessions = [];
|
|
295
379
|
for (const path of rollouts) {
|
|
296
380
|
const line = await readFirstLine(path);
|
|
@@ -305,7 +389,11 @@ async function discoverSessions(sessionsDir) {
|
|
|
305
389
|
if (!result.success) continue;
|
|
306
390
|
const meta = result.data.payload;
|
|
307
391
|
const id = meta.session_id ?? meta.id;
|
|
308
|
-
if (!id
|
|
392
|
+
if (!id) continue;
|
|
393
|
+
const workingDirectory = meta.cwd;
|
|
394
|
+
const assignedProjectId = projectAssignments.get(id);
|
|
395
|
+
const directory = (assignedProjectId ? projectRootsById.get(assignedProjectId) : void 0) ?? workingDirectory;
|
|
396
|
+
if (!directory) continue;
|
|
309
397
|
const created = meta.timestamp ? Date.parse(meta.timestamp) : NaN;
|
|
310
398
|
let mtime = created;
|
|
311
399
|
try {
|
|
@@ -314,13 +402,16 @@ async function discoverSessions(sessionsDir) {
|
|
|
314
402
|
} catch {
|
|
315
403
|
mtime = Number.isFinite(created) ? created : 0;
|
|
316
404
|
}
|
|
405
|
+
const indexed = index.get(id);
|
|
406
|
+
const indexedUpdated = indexed?.updated_at ? Date.parse(indexed.updated_at) : NaN;
|
|
317
407
|
sessions.push({
|
|
318
408
|
id,
|
|
319
|
-
directory
|
|
409
|
+
directory,
|
|
410
|
+
...workingDirectory && workingDirectory !== directory ? { workingDirectory } : {},
|
|
320
411
|
version: meta.cli_version ?? "unknown",
|
|
321
412
|
created: Number.isFinite(created) ? created : mtime,
|
|
322
|
-
updated: mtime,
|
|
323
|
-
title: await readRolloutTitle(path, "(untitled session)"),
|
|
413
|
+
updated: Number.isFinite(indexedUpdated) ? Math.max(mtime, indexedUpdated) : mtime,
|
|
414
|
+
title: await readRolloutTitle(path, indexed?.thread_name ?? "(untitled session)"),
|
|
324
415
|
rolloutPath: path
|
|
325
416
|
});
|
|
326
417
|
}
|
|
@@ -336,12 +427,19 @@ var CodexSessionStore = class {
|
|
|
336
427
|
at: 0,
|
|
337
428
|
byDirectory: /* @__PURE__ */ new Map()
|
|
338
429
|
};
|
|
430
|
+
projectCache = {
|
|
431
|
+
at: 0,
|
|
432
|
+
projects: [],
|
|
433
|
+
rootsById: /* @__PURE__ */ new Map(),
|
|
434
|
+
assignments: /* @__PURE__ */ new Map()
|
|
435
|
+
};
|
|
339
436
|
create(input) {
|
|
340
437
|
const now = Date.now();
|
|
341
438
|
const record = {
|
|
342
439
|
id: input.id,
|
|
343
440
|
projectID: projectId(input.directory),
|
|
344
441
|
directory: input.directory,
|
|
442
|
+
workingDirectory: input.workingDirectory,
|
|
345
443
|
title: input.title ?? "(untitled session)",
|
|
346
444
|
version: input.version,
|
|
347
445
|
created: now,
|
|
@@ -377,7 +475,13 @@ var CodexSessionStore = class {
|
|
|
377
475
|
async refreshDiscovery() {
|
|
378
476
|
const now = Date.now();
|
|
379
477
|
if (this.discoveryCache.at !== 0 && now - this.discoveryCache.at <= 3e4) return;
|
|
380
|
-
|
|
478
|
+
await this.refreshProjectCatalog(now);
|
|
479
|
+
const discovered = await discoverSessions(
|
|
480
|
+
this.sessionsDir,
|
|
481
|
+
join(dirname(this.sessionsDir), "session_index.jsonl"),
|
|
482
|
+
this.projectCache.rootsById,
|
|
483
|
+
this.projectCache.assignments
|
|
484
|
+
);
|
|
381
485
|
const byDirectory = /* @__PURE__ */ new Map();
|
|
382
486
|
for (const session of discovered) {
|
|
383
487
|
const list = byDirectory.get(session.directory) ?? [];
|
|
@@ -391,6 +495,24 @@ var CodexSessionStore = class {
|
|
|
391
495
|
await this.refreshDiscovery();
|
|
392
496
|
return directory ? [...this.discoveryCache.byDirectory.entries()].filter(([dir]) => samePath(dir, directory)).flatMap(([, sessions]) => sessions) : [...this.discoveryCache.byDirectory.values()].flat();
|
|
393
497
|
}
|
|
498
|
+
async discoverProjects(directory) {
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
await this.refreshProjectCatalog(now);
|
|
501
|
+
return directory ? this.projectCache.projects.filter((project) => samePath(project.directory, directory)) : [...this.projectCache.projects];
|
|
502
|
+
}
|
|
503
|
+
async refreshProjectCatalog(now = Date.now()) {
|
|
504
|
+
if (this.projectCache.at !== 0 && now - this.projectCache.at <= 3e4) return;
|
|
505
|
+
const globalStatePath = join(dirname(this.sessionsDir), ".codex-global-state.json");
|
|
506
|
+
const projects = await readGlobalProjects(globalStatePath);
|
|
507
|
+
const rootsById = /* @__PURE__ */ new Map();
|
|
508
|
+
for (const project of projects) {
|
|
509
|
+
if (!rootsById.has(project.id)) rootsById.set(project.id, project.directory);
|
|
510
|
+
}
|
|
511
|
+
this.projectCache.projects = projects;
|
|
512
|
+
this.projectCache.rootsById = rootsById;
|
|
513
|
+
this.projectCache.assignments = await readGlobalSessionAssignments(globalStatePath);
|
|
514
|
+
this.projectCache.at = now;
|
|
515
|
+
}
|
|
394
516
|
async discover(directory) {
|
|
395
517
|
const matching = await this.discoverAll(directory);
|
|
396
518
|
const liveThreadIds = new Set(
|
|
@@ -538,6 +660,11 @@ var questionRejectSchema = z2.object({
|
|
|
538
660
|
requestId: z2.string().min(1).max(256),
|
|
539
661
|
directory: directorySchema
|
|
540
662
|
}).strict();
|
|
663
|
+
var CODEX_DEFAULT_MODEL_ID = "__codex_default__";
|
|
664
|
+
var CODEX_DEFAULT_MODEL_NAME = "Codex default";
|
|
665
|
+
function normalizeModelId(model) {
|
|
666
|
+
return model === CODEX_DEFAULT_MODEL_ID ? void 0 : model;
|
|
667
|
+
}
|
|
541
668
|
var EventBus = class {
|
|
542
669
|
queue = [];
|
|
543
670
|
wake = null;
|
|
@@ -760,13 +887,14 @@ function ensureThread(codex, record, config, model) {
|
|
|
760
887
|
const selectedModel = model ?? record.model ?? config.model;
|
|
761
888
|
if (record.thread && record.model === selectedModel) return record.thread;
|
|
762
889
|
record.model = selectedModel;
|
|
890
|
+
const workingDirectory = record.workingDirectory ?? record.directory;
|
|
763
891
|
if (record.codexThreadId) {
|
|
764
892
|
record.thread = codex.resumeThread(
|
|
765
893
|
record.codexThreadId,
|
|
766
|
-
threadOptions(config,
|
|
894
|
+
threadOptions(config, workingDirectory, selectedModel)
|
|
767
895
|
);
|
|
768
896
|
} else {
|
|
769
|
-
record.thread = codex.startThread(threadOptions(config,
|
|
897
|
+
record.thread = codex.startThread(threadOptions(config, workingDirectory, selectedModel));
|
|
770
898
|
}
|
|
771
899
|
return record.thread;
|
|
772
900
|
}
|
|
@@ -876,25 +1004,42 @@ function createCodexAdapter(options) {
|
|
|
876
1004
|
case "project.list": {
|
|
877
1005
|
const params = projectListSchema.parse(rawParams);
|
|
878
1006
|
const directory = params.directory || void 0;
|
|
1007
|
+
const catalogProjects = await sessions.discoverProjects(directory);
|
|
879
1008
|
const records = sessions.list(directory);
|
|
880
1009
|
const discovered = await sessions.discoverAll(directory);
|
|
881
1010
|
const directories = /* @__PURE__ */ new Map();
|
|
1011
|
+
for (const project of catalogProjects) {
|
|
1012
|
+
directories.set(project.directory, {
|
|
1013
|
+
created: project.created,
|
|
1014
|
+
name: project.name,
|
|
1015
|
+
order: project.order
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
882
1018
|
for (const record of records) {
|
|
883
1019
|
const existing = directories.get(record.directory);
|
|
884
|
-
if (existing === void 0 || record.created < existing) {
|
|
885
|
-
directories.set(record.directory,
|
|
1020
|
+
if (existing === void 0 || record.created < existing.created) {
|
|
1021
|
+
directories.set(record.directory, {
|
|
1022
|
+
created: record.created,
|
|
1023
|
+
name: existing?.name,
|
|
1024
|
+
order: existing?.order ?? Number.MAX_SAFE_INTEGER
|
|
1025
|
+
});
|
|
886
1026
|
}
|
|
887
1027
|
}
|
|
888
1028
|
for (const session of discovered) {
|
|
889
1029
|
const existing = directories.get(session.directory);
|
|
890
|
-
if (existing === void 0 || session.created < existing) {
|
|
891
|
-
directories.set(session.directory,
|
|
1030
|
+
if (existing === void 0 || session.created < existing.created) {
|
|
1031
|
+
directories.set(session.directory, {
|
|
1032
|
+
created: session.created,
|
|
1033
|
+
name: existing?.name,
|
|
1034
|
+
order: existing?.order ?? Number.MAX_SAFE_INTEGER
|
|
1035
|
+
});
|
|
892
1036
|
}
|
|
893
1037
|
}
|
|
894
|
-
return [...directories.entries()].map(([worktree,
|
|
1038
|
+
return [...directories.entries()].sort(([, left], [, right]) => left.order - right.order || left.created - right.created).map(([worktree, project]) => ({
|
|
895
1039
|
id: projectId(worktree),
|
|
1040
|
+
...project.name ? { name: project.name } : {},
|
|
896
1041
|
worktree,
|
|
897
|
-
time: { created }
|
|
1042
|
+
time: { created: project.created }
|
|
898
1043
|
}));
|
|
899
1044
|
}
|
|
900
1045
|
case "session.list": {
|
|
@@ -953,7 +1098,7 @@ function createCodexAdapter(options) {
|
|
|
953
1098
|
}
|
|
954
1099
|
case "session.prompt": {
|
|
955
1100
|
const params = sessionPromptSchema.parse(rawParams);
|
|
956
|
-
const selectedModel = params.model ? params.model.providerID === "codex" ? params.model.modelID : (() => {
|
|
1101
|
+
const selectedModel = params.model ? params.model.providerID === "codex" ? normalizeModelId(params.model.modelID) : (() => {
|
|
957
1102
|
throw new Error(`Codex does not support provider ${params.model?.providerID}`);
|
|
958
1103
|
})() : void 0;
|
|
959
1104
|
let record = sessions.get(params.sessionId);
|
|
@@ -965,6 +1110,7 @@ function createCodexAdapter(options) {
|
|
|
965
1110
|
record = sessions.create({
|
|
966
1111
|
id: params.sessionId,
|
|
967
1112
|
directory: session.directory,
|
|
1113
|
+
workingDirectory: session.workingDirectory,
|
|
968
1114
|
title: history?.title ?? session.title,
|
|
969
1115
|
version: session.version,
|
|
970
1116
|
codexThreadId: session.id,
|
|
@@ -987,17 +1133,18 @@ function createCodexAdapter(options) {
|
|
|
987
1133
|
}
|
|
988
1134
|
case "provider.list": {
|
|
989
1135
|
providerListSchema.parse(rawParams);
|
|
990
|
-
|
|
1136
|
+
const modelID = config.model ?? CODEX_DEFAULT_MODEL_ID;
|
|
1137
|
+
const modelName = config.model ?? CODEX_DEFAULT_MODEL_NAME;
|
|
991
1138
|
return toSafeJson({
|
|
992
|
-
default: { codex:
|
|
1139
|
+
default: { codex: modelID },
|
|
993
1140
|
providers: [
|
|
994
1141
|
{
|
|
995
1142
|
id: "codex",
|
|
996
1143
|
name: "Codex",
|
|
997
1144
|
models: [
|
|
998
1145
|
{
|
|
999
|
-
id:
|
|
1000
|
-
name:
|
|
1146
|
+
id: modelID,
|
|
1147
|
+
name: modelName,
|
|
1001
1148
|
attachment: false,
|
|
1002
1149
|
reasoning: true,
|
|
1003
1150
|
toolCall: true,
|
|
@@ -1247,7 +1394,7 @@ import {
|
|
|
1247
1394
|
verify
|
|
1248
1395
|
} from "crypto";
|
|
1249
1396
|
import { homedir as homedir2 } from "os";
|
|
1250
|
-
import { dirname, join as join2 } from "path";
|
|
1397
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1251
1398
|
import { mkdir, open, readFile as readFile2 } from "fs/promises";
|
|
1252
1399
|
import { z as z4 } from "zod";
|
|
1253
1400
|
var DEVICE_PROOF_PREFIX = "tumnel-device-proof-v1";
|
|
@@ -1307,7 +1454,7 @@ async function readIdentity(path) {
|
|
|
1307
1454
|
return validateKeyPair(identitySchema.parse(JSON.parse(contents)));
|
|
1308
1455
|
}
|
|
1309
1456
|
async function persistNewIdentity(path, identity) {
|
|
1310
|
-
await mkdir(
|
|
1457
|
+
await mkdir(dirname2(path), { recursive: true, mode: 448 });
|
|
1311
1458
|
const handle = await open(path, "wx", 384);
|
|
1312
1459
|
try {
|
|
1313
1460
|
await handle.writeFile(`${JSON.stringify(identity, null, 2)}
|