tlc-claude-code 2.4.2 → 2.4.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/.claude/commands/tlc/build.md +68 -0
- package/.claude/commands/tlc/discuss.md +174 -123
- package/.claude/commands/tlc/e2e-verify.md +1 -1
- package/.claude/commands/tlc/plan.md +77 -2
- package/.claude/commands/tlc/tlc.md +204 -473
- package/CLAUDE.md +6 -5
- package/package.json +4 -1
- package/scripts/dev-link.sh +29 -0
- package/scripts/test-package.sh +54 -0
- package/scripts/version-sync.js +42 -0
- package/scripts/version-sync.test.js +100 -0
- package/server/lib/model-router.js +11 -2
- package/server/lib/model-router.test.js +27 -1
- package/server/lib/orchestration/codex-orchestrator.js +185 -0
- package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
- package/server/lib/orchestration/dep-linker.js +61 -0
- package/server/lib/orchestration/dep-linker.test.js +174 -0
- package/server/lib/router-config.js +18 -3
- package/server/lib/router-config.test.js +57 -1
- package/server/lib/routing/index.js +34 -0
- package/server/lib/routing/index.test.js +33 -0
- package/server/lib/routing-command.js +11 -2
- package/server/lib/routing-command.test.js +39 -1
- package/server/lib/routing-preamble.integration.test.js +319 -0
- package/server/lib/routing-preamble.js +34 -11
- package/server/lib/routing-preamble.test.js +11 -0
- package/server/lib/task-router-config.js +35 -14
- package/server/lib/task-router-config.test.js +77 -13
package/CLAUDE.md
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## Rules (Enforced by hooks — violations are blocked)
|
|
6
6
|
|
|
7
|
-
1. **
|
|
8
|
-
2. **
|
|
9
|
-
3. **
|
|
10
|
-
4. **No
|
|
11
|
-
5. **
|
|
7
|
+
1. **Never commit to main.** Always create `phase/{N}` branch. PR back to main when done.
|
|
8
|
+
2. **Tests before code.** Always. Red → Green → Refactor. Use `/tlc:build`.
|
|
9
|
+
3. **Plans go in files.** Use `/tlc:plan` → writes to `.planning/phases/`. Never plan in chat.
|
|
10
|
+
4. **No direct implementation.** User says "build X" → run `/tlc:progress` then `/tlc:build`.
|
|
11
|
+
5. **No Co-Authored-By in commits.** The user is the author. Claude is a tool.
|
|
12
|
+
6. **Ask before `git push`.** Never push without explicit approval.
|
|
12
13
|
|
|
13
14
|
## Command Dispatch
|
|
14
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tlc-claude-code",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "TLC - Test Led Coding for Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tlc-claude-code": "./bin/install.js",
|
|
@@ -34,11 +34,14 @@
|
|
|
34
34
|
"postinstall": "node bin/postinstall.js",
|
|
35
35
|
"build": "cd dashboard && npm run build",
|
|
36
36
|
"build:web": "cd dashboard-web && npm install && npm run build",
|
|
37
|
+
"preversion": "node scripts/version-sync.js && git diff --quiet || (echo 'Working tree not clean' && exit 1)",
|
|
38
|
+
"version": "node scripts/version-sync.js && git add .tlc.json",
|
|
37
39
|
"prepublishOnly": "npm run build && npm run build:web",
|
|
38
40
|
"docs": "node scripts/docs-update.js",
|
|
39
41
|
"docs:check": "node scripts/docs-update.js --check",
|
|
40
42
|
"docs:screenshots": "node scripts/generate-screenshots.js",
|
|
41
43
|
"docs:capture": "node scripts/capture-screenshots.js",
|
|
44
|
+
"test:version-sync": "cd server && npx vitest run --dir .. scripts/version-sync.test.js",
|
|
42
45
|
"test:e2e": "npx playwright test",
|
|
43
46
|
"test:e2e:ui": "npx playwright test --ui"
|
|
44
47
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -u
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
7
|
+
|
|
8
|
+
cd "${REPO_ROOT}" || {
|
|
9
|
+
echo "Error: failed to change to repo root: ${REPO_ROOT}" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if ! npm link; then
|
|
14
|
+
echo "Error: npm link failed" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
LINK_PATH="$(command -v tlc-claude-code || true)"
|
|
19
|
+
|
|
20
|
+
if [ -z "${LINK_PATH}" ]; then
|
|
21
|
+
echo "Error: tlc-claude-code was not found on PATH after npm link" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "Local link created successfully."
|
|
26
|
+
echo "Use it from another project with:"
|
|
27
|
+
echo " npm link tlc-claude-code"
|
|
28
|
+
echo "Resolved binary:"
|
|
29
|
+
echo " ${LINK_PATH}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -u
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
7
|
+
TMP_DIR=""
|
|
8
|
+
TARBALL_PATH=""
|
|
9
|
+
NPM_CACHE_DIR=""
|
|
10
|
+
|
|
11
|
+
cleanup() {
|
|
12
|
+
if [ -n "${TMP_DIR}" ] && [ -d "${TMP_DIR}" ]; then
|
|
13
|
+
rm -rf "${TMP_DIR}"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ -n "${TARBALL_PATH}" ] && [ -f "${TARBALL_PATH}" ]; then
|
|
17
|
+
rm -f "${TARBALL_PATH}"
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fail() {
|
|
22
|
+
echo "Error: $1" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
trap cleanup EXIT
|
|
27
|
+
|
|
28
|
+
TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t tlc-test-package)" || fail "failed to create temporary directory"
|
|
29
|
+
NPM_CACHE_DIR="${TMP_DIR}/npm-cache"
|
|
30
|
+
mkdir -p "${NPM_CACHE_DIR}" || fail "failed to create npm cache directory"
|
|
31
|
+
|
|
32
|
+
cd "${REPO_ROOT}" || fail "failed to change to repo root: ${REPO_ROOT}"
|
|
33
|
+
|
|
34
|
+
PACK_OUTPUT="$(npm_config_cache="${NPM_CACHE_DIR}" npm pack --json 2>&1)" || fail "npm pack failed: ${PACK_OUTPUT}"
|
|
35
|
+
TARBALL_NAME="$(printf '%s\n' "${PACK_OUTPUT}" | node -e "let input=''; process.stdin.on('data', (chunk) => input += chunk); process.stdin.on('end', () => { const start = input.indexOf('['); if (start === -1) { process.exit(1); } const data = JSON.parse(input.slice(start)); if (!Array.isArray(data) || !data[0] || !data[0].filename) { process.exit(1); } process.stdout.write(data[0].filename); });")" || fail "failed to parse npm pack output"
|
|
36
|
+
|
|
37
|
+
[ -n "${TARBALL_NAME}" ] || fail "npm pack did not return a tarball name"
|
|
38
|
+
|
|
39
|
+
TARBALL_PATH="${REPO_ROOT}/${TARBALL_NAME}"
|
|
40
|
+
[ -f "${TARBALL_PATH}" ] || fail "tarball was not created: ${TARBALL_PATH}"
|
|
41
|
+
|
|
42
|
+
cd "${TMP_DIR}" || fail "failed to change to temp directory: ${TMP_DIR}"
|
|
43
|
+
|
|
44
|
+
npm_config_cache="${NPM_CACHE_DIR}" npm install "${TARBALL_PATH}" >/dev/null 2>&1 || fail "npm install failed for ${TARBALL_NAME}"
|
|
45
|
+
|
|
46
|
+
PACKAGE_FILE="${TMP_DIR}/node_modules/tlc-claude-code/server/lib/routing-preamble.js"
|
|
47
|
+
[ -f "${PACKAGE_FILE}" ] || fail "installed package is missing server/lib/routing-preamble.js"
|
|
48
|
+
|
|
49
|
+
OUTPUT="$(node -e "const {generatePreamble} = require('tlc-claude-code/server/lib/routing-preamble'); const p = generatePreamble('build'); console.log(p.substring(0,20))" 2>&1)" || fail "routing preamble validation failed: ${OUTPUT}"
|
|
50
|
+
|
|
51
|
+
[ -n "${OUTPUT}" ] || fail "routing preamble validation produced no output"
|
|
52
|
+
|
|
53
|
+
echo "Package test passed."
|
|
54
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function syncVersion(rootDir = path.resolve(__dirname, '..')) {
|
|
5
|
+
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
6
|
+
const tlcConfigPath = path.join(rootDir, '.tlc.json');
|
|
7
|
+
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
9
|
+
|
|
10
|
+
if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
|
|
11
|
+
throw new Error('package.json is missing a valid version field');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let tlcConfig = {};
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(tlcConfigPath)) {
|
|
17
|
+
tlcConfig = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
tlcConfig.version = packageJson.version;
|
|
21
|
+
tlcConfig.tlcVersion = packageJson.version;
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(tlcConfigPath, `${JSON.stringify(tlcConfig, null, 2)}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function main() {
|
|
27
|
+
try {
|
|
28
|
+
syncVersion();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (require.main === module) {
|
|
37
|
+
main();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
syncVersion,
|
|
42
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import versionSyncModule from './version-sync.js';
|
|
8
|
+
|
|
9
|
+
const { syncVersion } = versionSyncModule;
|
|
10
|
+
|
|
11
|
+
const tempDirs = [];
|
|
12
|
+
|
|
13
|
+
function createTempProject(packageVersion, tlcConfig) {
|
|
14
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-sync-'));
|
|
15
|
+
tempDirs.push(tempDir);
|
|
16
|
+
|
|
17
|
+
fs.writeFileSync(
|
|
18
|
+
path.join(tempDir, 'package.json'),
|
|
19
|
+
`${JSON.stringify({ version: packageVersion }, null, 2)}\n`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (tlcConfig !== undefined) {
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(tempDir, '.tlc.json'),
|
|
25
|
+
`${JSON.stringify(tlcConfig, null, 2)}\n`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return tempDir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(filePath) {
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
while (tempDirs.length > 0) {
|
|
38
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('syncVersion', () => {
|
|
43
|
+
it('reads version from package.json correctly', () => {
|
|
44
|
+
const tempDir = createTempProject('1.2.3', { existing: true });
|
|
45
|
+
|
|
46
|
+
syncVersion(tempDir);
|
|
47
|
+
|
|
48
|
+
const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
|
|
49
|
+
expect(tlcConfig.version).toBe('1.2.3');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('writes to .tlc.json version field', () => {
|
|
53
|
+
const tempDir = createTempProject('2.3.4', {});
|
|
54
|
+
|
|
55
|
+
syncVersion(tempDir);
|
|
56
|
+
|
|
57
|
+
const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
|
|
58
|
+
expect(tlcConfig.version).toBe('2.3.4');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('writes to .tlc.json tlcVersion field', () => {
|
|
62
|
+
const tempDir = createTempProject('3.4.5', {});
|
|
63
|
+
|
|
64
|
+
syncVersion(tempDir);
|
|
65
|
+
|
|
66
|
+
const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
|
|
67
|
+
expect(tlcConfig.tlcVersion).toBe('3.4.5');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles missing .tlc.json gracefully', () => {
|
|
71
|
+
const tempDir = createTempProject('4.5.6');
|
|
72
|
+
|
|
73
|
+
expect(() => syncVersion(tempDir)).not.toThrow();
|
|
74
|
+
|
|
75
|
+
const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
|
|
76
|
+
expect(tlcConfig).toEqual({
|
|
77
|
+
version: '4.5.6',
|
|
78
|
+
tlcVersion: '4.5.6',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not overwrite other .tlc.json fields', () => {
|
|
83
|
+
const tempDir = createTempProject('5.6.7', {
|
|
84
|
+
project: 'TLC',
|
|
85
|
+
nested: { keep: true },
|
|
86
|
+
version: 'old',
|
|
87
|
+
tlcVersion: 'old',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
syncVersion(tempDir);
|
|
91
|
+
|
|
92
|
+
const tlcConfig = readJson(path.join(tempDir, '.tlc.json'));
|
|
93
|
+
expect(tlcConfig).toEqual({
|
|
94
|
+
project: 'TLC',
|
|
95
|
+
nested: { keep: true },
|
|
96
|
+
version: '5.6.7',
|
|
97
|
+
tlcVersion: '5.6.7',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -13,6 +13,10 @@ const DEFAULT_CONFIG = {
|
|
|
13
13
|
devserver: { url: null },
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
function isMissingConfigError(error) {
|
|
17
|
+
return error?.code === 'ENOENT' || error?.message?.includes('ENOENT');
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export class ModelRouter {
|
|
17
21
|
constructor(config = {}) {
|
|
18
22
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -88,8 +92,13 @@ export class ModelRouter {
|
|
|
88
92
|
this.config = { ...DEFAULT_CONFIG, ...configData.router };
|
|
89
93
|
this.devserverUrl = configData.router.devserver?.url || null;
|
|
90
94
|
}
|
|
91
|
-
} catch {
|
|
92
|
-
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!isMissingConfigError(error)) {
|
|
97
|
+
const message = error instanceof SyntaxError
|
|
98
|
+
? 'Failed to parse .tlc.json router config; using defaults'
|
|
99
|
+
: `Failed to load .tlc.json router config; using defaults (${error?.message || 'unknown error'})`;
|
|
100
|
+
console.warn(`[TLC WARNING] ${message}`);
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
ModelRouter,
|
|
4
4
|
resolveProvider,
|
|
@@ -22,6 +22,10 @@ describe('Model Router', () => {
|
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
25
29
|
describe('resolveProvider', () => {
|
|
26
30
|
it('returns local when CLI detected', async () => {
|
|
27
31
|
router._detectCLI = vi.fn().mockResolvedValue({ found: true, path: '/usr/bin/claude' });
|
|
@@ -105,6 +109,28 @@ describe('Model Router', () => {
|
|
|
105
109
|
|
|
106
110
|
expect(router.config).toBeDefined();
|
|
107
111
|
});
|
|
112
|
+
|
|
113
|
+
it('warns when config JSON is invalid', async () => {
|
|
114
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
115
|
+
router._readConfig = vi.fn().mockRejectedValue(new SyntaxError('Unexpected token }'));
|
|
116
|
+
|
|
117
|
+
await router.loadConfig();
|
|
118
|
+
|
|
119
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
120
|
+
'[TLC WARNING] Failed to parse .tlc.json router config; using defaults'
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('stays silent when config file is missing', async () => {
|
|
125
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
126
|
+
const error = new Error('ENOENT');
|
|
127
|
+
error.code = 'ENOENT';
|
|
128
|
+
router._readConfig = vi.fn().mockRejectedValue(error);
|
|
129
|
+
|
|
130
|
+
await router.loadConfig();
|
|
131
|
+
|
|
132
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
108
134
|
});
|
|
109
135
|
|
|
110
136
|
describe('handleUnavailable', () => {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Dispatches tasks to Codex CLI and captures thread IDs for resume flows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function createCliNotFoundError() {
|
|
8
|
+
return {
|
|
9
|
+
code: 'CODEX_CLI_NOT_FOUND',
|
|
10
|
+
message: 'Codex CLI is not installed or not available on PATH',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createTimeoutError(timeout) {
|
|
15
|
+
return {
|
|
16
|
+
code: 'PROCESS_TIMEOUT',
|
|
17
|
+
message: `Process timed out after ${timeout}ms`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runCodex({
|
|
22
|
+
args,
|
|
23
|
+
prompt = '',
|
|
24
|
+
timeout = 120000,
|
|
25
|
+
cwd,
|
|
26
|
+
spawn = require('child_process').spawn,
|
|
27
|
+
onStdoutLine,
|
|
28
|
+
}) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const spawnOpts = {};
|
|
32
|
+
if (cwd) spawnOpts.cwd = cwd;
|
|
33
|
+
|
|
34
|
+
let proc;
|
|
35
|
+
try {
|
|
36
|
+
proc = spawn('codex', args, spawnOpts);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const error = err && err.code === 'ENOENT'
|
|
39
|
+
? createCliNotFoundError()
|
|
40
|
+
: {
|
|
41
|
+
code: 'SPAWN_FAILED',
|
|
42
|
+
message: err && err.message ? err.message : 'Failed to spawn Codex CLI',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
resolve({
|
|
46
|
+
stdout: '',
|
|
47
|
+
stderr: err && err.message ? err.message : error.message,
|
|
48
|
+
exitCode: -1,
|
|
49
|
+
duration: Date.now() - start,
|
|
50
|
+
error,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
let settled = false;
|
|
58
|
+
let stdoutBuffer = '';
|
|
59
|
+
|
|
60
|
+
const finish = (exitCode, error = null, stderrOverride) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve({
|
|
65
|
+
stdout,
|
|
66
|
+
stderr: stderrOverride !== undefined ? stderrOverride : stderr,
|
|
67
|
+
exitCode,
|
|
68
|
+
duration: Date.now() - start,
|
|
69
|
+
error,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
proc.kill();
|
|
75
|
+
const error = createTimeoutError(timeout);
|
|
76
|
+
const nextStderr = stderr ? `${stderr}\n${error.message}` : error.message;
|
|
77
|
+
finish(-1, error, nextStderr);
|
|
78
|
+
}, timeout);
|
|
79
|
+
|
|
80
|
+
proc.stdout.on('data', (data) => {
|
|
81
|
+
const chunk = data.toString();
|
|
82
|
+
stdout += chunk;
|
|
83
|
+
|
|
84
|
+
if (!onStdoutLine) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stdoutBuffer += chunk;
|
|
89
|
+
|
|
90
|
+
while (stdoutBuffer.includes('\n')) {
|
|
91
|
+
const newlineIndex = stdoutBuffer.indexOf('\n');
|
|
92
|
+
const line = stdoutBuffer.slice(0, newlineIndex);
|
|
93
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
94
|
+
onStdoutLine(line);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
proc.stderr.on('data', (data) => {
|
|
99
|
+
stderr += data.toString();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
proc.on('close', (code) => {
|
|
103
|
+
if (onStdoutLine && stdoutBuffer) {
|
|
104
|
+
onStdoutLine(stdoutBuffer);
|
|
105
|
+
stdoutBuffer = '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
finish(code, null);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
proc.on('error', (err) => {
|
|
112
|
+
const error = err && err.code === 'ENOENT'
|
|
113
|
+
? createCliNotFoundError()
|
|
114
|
+
: {
|
|
115
|
+
code: 'SPAWN_FAILED',
|
|
116
|
+
message: err && err.message ? err.message : 'Failed to spawn Codex CLI',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const nextStderr = stderr || (err && err.message) || error.message;
|
|
120
|
+
finish(-1, error, nextStderr);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
proc.stdin.on('error', () => {});
|
|
124
|
+
|
|
125
|
+
if (prompt) {
|
|
126
|
+
proc.stdin.write(prompt);
|
|
127
|
+
}
|
|
128
|
+
proc.stdin.end();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function dispatchToCodex({
|
|
133
|
+
worktreePath,
|
|
134
|
+
prompt,
|
|
135
|
+
timeout = 120000,
|
|
136
|
+
spawn = require('child_process').spawn,
|
|
137
|
+
}) {
|
|
138
|
+
let threadId = null;
|
|
139
|
+
|
|
140
|
+
const result = await runCodex({
|
|
141
|
+
args: ['exec', '--json', '--full-auto', '-C', worktreePath],
|
|
142
|
+
prompt,
|
|
143
|
+
timeout,
|
|
144
|
+
spawn,
|
|
145
|
+
onStdoutLine(line) {
|
|
146
|
+
if (!line) return;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const event = JSON.parse(line);
|
|
150
|
+
if (event.type === 'thread.started' && typeof event.thread_id === 'string') {
|
|
151
|
+
threadId = event.thread_id;
|
|
152
|
+
}
|
|
153
|
+
} catch (_) {
|
|
154
|
+
// Ignore non-JSON lines in mixed stdout streams.
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
threadId,
|
|
161
|
+
stdout: result.stdout,
|
|
162
|
+
stderr: result.stderr,
|
|
163
|
+
exitCode: result.exitCode,
|
|
164
|
+
duration: result.duration,
|
|
165
|
+
error: result.error,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resumeSession({
|
|
170
|
+
threadId,
|
|
171
|
+
prompt,
|
|
172
|
+
timeout = 120000,
|
|
173
|
+
spawn = require('child_process').spawn,
|
|
174
|
+
}) {
|
|
175
|
+
return runCodex({
|
|
176
|
+
args: ['exec', 'resume', threadId, prompt],
|
|
177
|
+
timeout,
|
|
178
|
+
spawn,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
dispatchToCodex,
|
|
184
|
+
resumeSession,
|
|
185
|
+
};
|