parallel-codex-tui 0.1.0 → 0.1.3
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 +39 -1
- package/dist/bootstrap.js +17 -5
- package/dist/cli-args.js +65 -14
- package/dist/cli-workspace.js +104 -0
- package/dist/cli.js +82 -24
- package/dist/core/app-root.js +8 -0
- package/dist/core/config.js +38 -0
- package/dist/core/file-store.js +11 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/router.js +13 -9
- package/dist/core/session-index.js +53 -9
- package/dist/core/session-manager.js +101 -9
- package/dist/core/workspace.js +118 -0
- package/dist/doctor.js +58 -24
- package/dist/orchestrator/orchestrator.js +37 -19
- package/dist/tui/App.js +10 -8
- package/dist/tui/task-memory.js +3 -0
- package/dist/version.js +1 -1
- package/dist/workers/native-attach.js +69 -5
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +11 -5
- package/package.json +14 -2
|
@@ -4,13 +4,20 @@ import { readdir } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { spawn } from "node-pty";
|
|
7
|
-
import { pathExists, readJson, readTextIfExists, writeJson } from "../core/file-store.js";
|
|
7
|
+
import { pathExists, pathIsDirectory, readJson, readTextIfExists, removeIfExists, writeJson } from "../core/file-store.js";
|
|
8
8
|
import { NativeSessionSchema, TaskMetaSchema } from "../domain/schemas.js";
|
|
9
|
+
import { detectResumeSessionId } from "./native-session-detection.js";
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
export async function buildNativeAttachLaunch(input) {
|
|
11
12
|
const nativeSession = await readWorkerNativeSession(input.worker);
|
|
12
13
|
const workerConfig = input.config.workers[input.worker.engine];
|
|
13
14
|
const modelConfig = workerConfig.model;
|
|
15
|
+
if (!(await pathExists(nativeSession.cwd))) {
|
|
16
|
+
throw new Error(`Native session workspace not found for ${input.worker.label}: ${nativeSession.cwd}`);
|
|
17
|
+
}
|
|
18
|
+
if (!(await pathIsDirectory(nativeSession.cwd))) {
|
|
19
|
+
throw new Error(`Native session workspace is not a directory for ${input.worker.label}: ${nativeSession.cwd}`);
|
|
20
|
+
}
|
|
14
21
|
return {
|
|
15
22
|
command: workerConfig.interactive.command,
|
|
16
23
|
args: workerConfig.interactive.args.map((arg) => renderTemplate(arg, nativeSession.session_id, modelConfig)),
|
|
@@ -65,7 +72,13 @@ function ensureNodePtySpawnHelperExecutable() {
|
|
|
65
72
|
async function readWorkerNativeSession(worker) {
|
|
66
73
|
const workerDir = dirname(worker.statusPath);
|
|
67
74
|
const nativePath = join(workerDir, "native-session.json");
|
|
68
|
-
|
|
75
|
+
const record = await readAttachNativeSessionIfValid(nativePath);
|
|
76
|
+
if (!record) {
|
|
77
|
+
const recoveredCodex = await recoverCodexNativeSession(worker, workerDir);
|
|
78
|
+
if (recoveredCodex) {
|
|
79
|
+
await writeJson(nativePath, NativeSessionSchema.parse(recoveredCodex));
|
|
80
|
+
return recoveredCodex;
|
|
81
|
+
}
|
|
69
82
|
const recovered = await recoverClaudeNativeSession(worker, workerDir);
|
|
70
83
|
if (recovered) {
|
|
71
84
|
await writeJson(nativePath, NativeSessionSchema.parse(recovered));
|
|
@@ -73,12 +86,53 @@ async function readWorkerNativeSession(worker) {
|
|
|
73
86
|
}
|
|
74
87
|
throw new Error(`No native session recorded for ${worker.label}. Run the worker once before attaching, or make sure ${worker.engine} persisted a resumable session.`);
|
|
75
88
|
}
|
|
76
|
-
const record = await readJson(nativePath, NativeSessionSchema);
|
|
77
89
|
if (record.engine !== worker.engine) {
|
|
78
90
|
throw new Error(`Native session engine mismatch for ${worker.label}: expected ${worker.engine}, got ${record.engine}`);
|
|
79
91
|
}
|
|
92
|
+
if (record.worker_id !== worker.id || record.role !== worker.role) {
|
|
93
|
+
throw new Error(`Native session worker mismatch for ${worker.label}: expected ${worker.role}/${worker.id}, got ${record.role}/${record.worker_id}`);
|
|
94
|
+
}
|
|
80
95
|
return record;
|
|
81
96
|
}
|
|
97
|
+
async function readAttachNativeSessionIfValid(nativePath) {
|
|
98
|
+
if (!(await pathExists(nativePath))) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return await readJson(nativePath, NativeSessionSchema);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
await removeIfExists(nativePath);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function recoverCodexNativeSession(worker, workerDir) {
|
|
110
|
+
if (worker.engine !== "codex") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const taskDir = dirname(workerDir);
|
|
114
|
+
const cwd = await readTaskCwdIfValid(join(taskDir, "meta.json"));
|
|
115
|
+
if (!cwd) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const output = await readTextIfExists(join(workerDir, "output.log"));
|
|
119
|
+
const sessionId = detectResumeSessionId(output);
|
|
120
|
+
if (!sessionId) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
return {
|
|
125
|
+
engine: "codex",
|
|
126
|
+
role: worker.role,
|
|
127
|
+
worker_id: worker.id,
|
|
128
|
+
session_id: sessionId,
|
|
129
|
+
scope: "task",
|
|
130
|
+
cwd,
|
|
131
|
+
created_at: now,
|
|
132
|
+
last_used_at: now,
|
|
133
|
+
source: "output-detected"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
82
136
|
async function recoverClaudeNativeSession(worker, workerDir) {
|
|
83
137
|
if (worker.engine !== "claude") {
|
|
84
138
|
return null;
|
|
@@ -88,8 +142,7 @@ async function recoverClaudeNativeSession(worker, workerDir) {
|
|
|
88
142
|
return null;
|
|
89
143
|
}
|
|
90
144
|
const taskDir = dirname(workerDir);
|
|
91
|
-
const
|
|
92
|
-
const cwd = (await pathExists(metaPath)) ? (await readJson(metaPath, TaskMetaSchema)).cwd : null;
|
|
145
|
+
const cwd = await readTaskCwdIfValid(join(taskDir, "meta.json"));
|
|
93
146
|
if (!cwd) {
|
|
94
147
|
return null;
|
|
95
148
|
}
|
|
@@ -112,6 +165,17 @@ async function recoverClaudeNativeSession(worker, workerDir) {
|
|
|
112
165
|
source: "claude-project-log"
|
|
113
166
|
};
|
|
114
167
|
}
|
|
168
|
+
async function readTaskCwdIfValid(metaPath) {
|
|
169
|
+
if (!(await pathExists(metaPath))) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return (await readJson(metaPath, TaskMetaSchema)).cwd;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
115
179
|
async function findClaudeProjectSession(input) {
|
|
116
180
|
const projectDir = join(claudeProjectsDir(), claudeProjectSlug(input.cwd));
|
|
117
181
|
if (!(await pathExists(projectDir))) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function detectNativeSessionId(text) {
|
|
2
|
+
const labeled = text.match(/\b(?:session id|session_id|session)\s*[:=]\s*([A-Za-z0-9._:@-]{4,})/i);
|
|
3
|
+
if (labeled?.[1] && isLikelyNativeSessionId(labeled[1])) {
|
|
4
|
+
return labeled[1];
|
|
5
|
+
}
|
|
6
|
+
return detectResumeSessionId(text);
|
|
7
|
+
}
|
|
8
|
+
export function detectResumeSessionId(text) {
|
|
9
|
+
const resume = text.match(/\b(?:codex|claude)\s+resume\s+([A-Za-z0-9._:@-]{4,})\b/i);
|
|
10
|
+
return resume?.[1] && isLikelyNativeSessionId(resume[1]) ? resume[1] : null;
|
|
11
|
+
}
|
|
12
|
+
export function isLikelyNativeSessionId(value) {
|
|
13
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return value.length >= 8 && /[0-9]/.test(value) && /[._:@-]/.test(value);
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendText, writeJson } from "../core/file-store.js";
|
|
3
|
+
import { detectNativeSessionId } from "./native-session-detection.js";
|
|
3
4
|
export class ProcessWorkerAdapter {
|
|
4
5
|
name;
|
|
5
6
|
command;
|
|
@@ -43,7 +44,7 @@ export class ProcessWorkerAdapter {
|
|
|
43
44
|
}
|
|
44
45
|
async runAttempt(runSpec, launch, options = {}) {
|
|
45
46
|
await setStatus(runSpec, "starting", options.startPhase ?? "process-starting", options.startSummary ?? `Starting ${this.command}`);
|
|
46
|
-
await appendText(runSpec.outputLogPath, `$ ${this.command
|
|
47
|
+
await appendText(runSpec.outputLogPath, `$ ${formatShellCommand(this.command, launch.args)}\n`);
|
|
47
48
|
return new Promise((resolve, reject) => {
|
|
48
49
|
const child = spawn(this.command, launch.args, {
|
|
49
50
|
cwd: runSpec.cwd,
|
|
@@ -239,6 +240,15 @@ function renderTemplate(value, sessionId, modelConfig) {
|
|
|
239
240
|
.replaceAll("{provider}", modelConfig?.provider ?? "")
|
|
240
241
|
.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => process.env[name] ?? "");
|
|
241
242
|
}
|
|
243
|
+
function formatShellCommand(command, args) {
|
|
244
|
+
return [command, ...args].map(shellQuote).join(" ");
|
|
245
|
+
}
|
|
246
|
+
function shellQuote(value) {
|
|
247
|
+
if (value.length > 0 && /^[A-Za-z0-9_./:=@%+,-]+$/.test(value)) {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
251
|
+
}
|
|
242
252
|
function summarizeOutput(text) {
|
|
243
253
|
const lines = text
|
|
244
254
|
.split(/\r?\n/)
|
|
@@ -259,7 +269,3 @@ async function setStatus(spec, state, phase, summary, nativeSessionId) {
|
|
|
259
269
|
...(nativeSessionId ? { native_session_id: nativeSessionId } : {})
|
|
260
270
|
});
|
|
261
271
|
}
|
|
262
|
-
function detectNativeSessionId(text) {
|
|
263
|
-
const match = text.match(/\b(?:session id|session_id|session)\s*[:=]\s*([A-Za-z0-9._:@-]{4,})/i);
|
|
264
|
-
return match?.[1] ?? null;
|
|
265
|
-
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallel-codex-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/allendred/parallel-codex-tui.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/allendred/parallel-codex-tui/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/allendred/parallel-codex-tui#readme",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
7
19
|
"keywords": [
|
|
8
20
|
"codex",
|
|
9
21
|
"claude",
|
|
@@ -12,7 +24,7 @@
|
|
|
12
24
|
"agent-orchestration"
|
|
13
25
|
],
|
|
14
26
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
27
|
+
"node": ">=26.0.0"
|
|
16
28
|
},
|
|
17
29
|
"bin": {
|
|
18
30
|
"parallel-codex-tui": "dist/cli.js"
|