@golba98/codexa 1.0.1
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 +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { RunToolActivity } from "../session/types.js";
|
|
4
|
+
import { getBlockedCleanupFailure } from "./cleanupFastFail.js";
|
|
5
|
+
|
|
6
|
+
function activity(overrides: Partial<RunToolActivity>): RunToolActivity {
|
|
7
|
+
return {
|
|
8
|
+
id: "tool-1",
|
|
9
|
+
command: "Remove-Item -Recurse __pycache__",
|
|
10
|
+
status: "failed",
|
|
11
|
+
startedAt: 1,
|
|
12
|
+
completedAt: 2,
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("reports access-denied Remove-Item cleanup failures", () => {
|
|
18
|
+
const result = getBlockedCleanupFailure(activity({
|
|
19
|
+
summary: "Access to the path 'C:\\repo\\__pycache__\\mod.pyc' is denied.",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
assert.match(result ?? "", /access denied/i);
|
|
23
|
+
assert.match(result ?? "", /C:\\repo\\__pycache__\\mod\.pyc/);
|
|
24
|
+
assert.match(result ?? "", /stopped/i);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("reports generic POSIX and node locked delete failures", () => {
|
|
28
|
+
for (const summary of [
|
|
29
|
+
"rm: cannot remove 'tests/__pycache__/case.pyc': Permission denied",
|
|
30
|
+
"EPERM: operation not permitted, unlink 'tests/__pycache__/case.pyc'",
|
|
31
|
+
"EACCES: permission denied, rmdir 'tests/__pycache__'",
|
|
32
|
+
"EBUSY: resource busy or locked, unlink 'tests/__pycache__/case.pyc'",
|
|
33
|
+
"The process cannot access the file because it is being used by another process: 'tests/__pycache__/case.pyc'",
|
|
34
|
+
]) {
|
|
35
|
+
const result = getBlockedCleanupFailure(activity({
|
|
36
|
+
command: "rm -rf tests/__pycache__",
|
|
37
|
+
summary,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
assert.ok(result, `Expected blocked cleanup failure for: ${summary}`);
|
|
41
|
+
assert.match(result, /blocked|denied|locked|busy/i);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("reports git and generic lock artifacts during delete failures", () => {
|
|
46
|
+
const gitLock = getBlockedCleanupFailure(activity({
|
|
47
|
+
command: "Remove-Item -LiteralPath .git\\config.lock",
|
|
48
|
+
summary: "Access to the path '.git\\config.lock' is denied.",
|
|
49
|
+
}));
|
|
50
|
+
const genericLock = getBlockedCleanupFailure(activity({
|
|
51
|
+
command: "del cache.lock",
|
|
52
|
+
summary: "EPERM: operation not permitted, unlink 'cache.lock'",
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
assert.match(gitLock ?? "", /lock artifact/i);
|
|
56
|
+
assert.match(gitLock ?? "", /\.git\\config\.lock/i);
|
|
57
|
+
assert.match(genericLock ?? "", /lock artifact/i);
|
|
58
|
+
assert.match(genericLock ?? "", /cache\.lock/i);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("ignores non-delete failures and non-failed activities", () => {
|
|
62
|
+
assert.equal(getBlockedCleanupFailure(activity({
|
|
63
|
+
command: "git status",
|
|
64
|
+
summary: "fatal: Unable to create '.git/config.lock': File exists.",
|
|
65
|
+
})), null);
|
|
66
|
+
|
|
67
|
+
assert.equal(getBlockedCleanupFailure(activity({
|
|
68
|
+
status: "completed",
|
|
69
|
+
summary: "Access to the path 'x.pyc' is denied.",
|
|
70
|
+
})), null);
|
|
71
|
+
|
|
72
|
+
assert.equal(getBlockedCleanupFailure(activity({
|
|
73
|
+
command: "rm -rf __pycache__",
|
|
74
|
+
summary: "No such file or directory",
|
|
75
|
+
})), null);
|
|
76
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RunToolActivity } from "../session/types.js";
|
|
2
|
+
|
|
3
|
+
const DELETE_COMMAND_PATTERN =
|
|
4
|
+
/(?:^|[\s;&|])(?:remove-item|rm|rmdir|del|erase|unlink)\b/i;
|
|
5
|
+
|
|
6
|
+
const BLOCKED_DELETE_CAUSE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
7
|
+
{ pattern: /(?:^|[\\/])\.git[\\/][^\s"'`]*\.lock\b|(?:^|[\\/])?config\.lock\b|\.lock\b/i, label: "lock artifact" },
|
|
8
|
+
{ pattern: /\bEACCES\b/i, label: "access denied" },
|
|
9
|
+
{ pattern: /\bEPERM\b/i, label: "permission denied" },
|
|
10
|
+
{ pattern: /\bEBUSY\b/i, label: "file is busy or locked" },
|
|
11
|
+
{ pattern: /access(?:\s+to\s+the\s+path)?\s+.*?\s+denied|access is denied/i, label: "access denied" },
|
|
12
|
+
{ pattern: /permission denied|operation not permitted/i, label: "permission denied" },
|
|
13
|
+
{ pattern: /being used by another process|file is in use|resource busy|text file busy|device or resource busy/i, label: "file is locked or in use" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const PATH_PATTERNS = [
|
|
17
|
+
/Access to the path ['"]([^'"]+)['"] is denied/i,
|
|
18
|
+
/(?:EPERM|EACCES|EBUSY)[^,\n\r]*,\s*(?:unlink|rmdir|rm|open|scandir)\s+['"]?([^'"\n\r]+)['"]?/i,
|
|
19
|
+
/(?:cannot|can't|failed to|unable to)\s+(?:remove|delete|unlink|rmdir)[^'"\n\r]*['"]([^'"]+)['"]/i,
|
|
20
|
+
/(?:being used by another process|file is in use|permission denied|access is denied)[^'"\n\r]*['"]([^'"]+)['"]/i,
|
|
21
|
+
/((?:\.git[\\/])?[^\s"'`]+\.lock)\b/i,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function normalizeText(value: string | null | undefined): string {
|
|
25
|
+
return (value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isDeleteCommand(command: string): boolean {
|
|
29
|
+
return DELETE_COMMAND_PATTERN.test(command);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findCause(text: string): string | null {
|
|
33
|
+
for (const { pattern, label } of BLOCKED_DELETE_CAUSE_PATTERNS) {
|
|
34
|
+
if (pattern.test(text)) return label;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findBlockedPath(text: string): string | null {
|
|
40
|
+
for (const pattern of PATH_PATTERNS) {
|
|
41
|
+
const match = pattern.exec(text);
|
|
42
|
+
const path = match?.[1]?.trim();
|
|
43
|
+
if (path) return path.replace(/[.,;:]+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getBlockedCleanupFailure(activity: RunToolActivity): string | null {
|
|
49
|
+
if (activity.status !== "failed") return null;
|
|
50
|
+
|
|
51
|
+
const command = normalizeText(activity.command);
|
|
52
|
+
const summary = normalizeText(activity.summary);
|
|
53
|
+
const combined = [command, summary].filter(Boolean).join("\n");
|
|
54
|
+
if (!command || !isDeleteCommand(command)) return null;
|
|
55
|
+
|
|
56
|
+
const cause = findCause(combined);
|
|
57
|
+
if (!cause) return null;
|
|
58
|
+
|
|
59
|
+
const blockedPath = findBlockedPath(combined);
|
|
60
|
+
const target = blockedPath ? `\nBlocked item: ${blockedPath}` : "";
|
|
61
|
+
return [
|
|
62
|
+
"Cleanup stopped because a safe generated artifact could not be deleted.",
|
|
63
|
+
`Cause: ${cause}.`,
|
|
64
|
+
target,
|
|
65
|
+
"Codexa stopped after the first clear blocked-delete signal to avoid retrying a doomed cleanup.",
|
|
66
|
+
].filter(Boolean).join("\n");
|
|
67
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
|
|
4
|
+
function trySpawn(cmd: string, args: string[], text: string): Promise<boolean> {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
7
|
+
proc.on("error", () => resolve(false));
|
|
8
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
9
|
+
proc.stdin?.write(text);
|
|
10
|
+
proc.stdin?.end();
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
15
|
+
const os = platform();
|
|
16
|
+
|
|
17
|
+
if (os === "win32") return trySpawn("clip", [], text);
|
|
18
|
+
if (os === "darwin") return trySpawn("pbcopy", [], text);
|
|
19
|
+
|
|
20
|
+
// Linux: try xclip, fall back to xsel
|
|
21
|
+
const ok = await trySpawn("xclip", ["-selection", "clipboard"], text);
|
|
22
|
+
if (ok) return true;
|
|
23
|
+
return trySpawn("xsel", ["--clipboard", "--input"], text);
|
|
24
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
|
|
3
|
+
import { formatCodexLaunchError, spawnCodexProcess } from "./executables/codexExecutable.js";
|
|
4
|
+
import { prepareCodexExecLaunch } from "./codexLaunch.js";
|
|
5
|
+
import { createTerminalTitleSequenceStripper } from "./terminal/terminalTitle.js";
|
|
6
|
+
|
|
7
|
+
export interface CodexHandlers {
|
|
8
|
+
onLine: (line: string) => void;
|
|
9
|
+
onDone: () => void;
|
|
10
|
+
onError: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function streamCodex(
|
|
14
|
+
prompt: string,
|
|
15
|
+
runtime: ResolvedRuntimeConfig,
|
|
16
|
+
workspaceRoot: string,
|
|
17
|
+
handlers: CodexHandlers,
|
|
18
|
+
): () => void {
|
|
19
|
+
let done = false;
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
let proc: ReturnType<typeof spawn> | null = null;
|
|
22
|
+
|
|
23
|
+
const finishError = (message: string) => {
|
|
24
|
+
if (done) return;
|
|
25
|
+
done = true;
|
|
26
|
+
handlers.onError(message);
|
|
27
|
+
};
|
|
28
|
+
const finishSuccess = () => {
|
|
29
|
+
if (done) return;
|
|
30
|
+
done = true;
|
|
31
|
+
handlers.onDone();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
void prepareCodexExecLaunch(
|
|
35
|
+
{
|
|
36
|
+
runtime,
|
|
37
|
+
cwd: workspaceRoot,
|
|
38
|
+
structuredOutput: false,
|
|
39
|
+
},
|
|
40
|
+
import.meta.url,
|
|
41
|
+
)
|
|
42
|
+
.then((launchPlan) => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
if (!launchPlan.ok) {
|
|
45
|
+
finishError(launchPlan.error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!launchPlan.executable) {
|
|
49
|
+
finishError("Codex launch preparation did not return an executable.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
proc = spawnCodexProcess(
|
|
54
|
+
launchPlan.executable,
|
|
55
|
+
launchPlan.args,
|
|
56
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
proc.stdin?.write(prompt);
|
|
60
|
+
proc.stdin?.end();
|
|
61
|
+
|
|
62
|
+
let buffer = "";
|
|
63
|
+
|
|
64
|
+
// Strip title escape sequences from child output — prevents the subprocess from overwriting the terminal title.
|
|
65
|
+
const stdoutTitleStripper = createTerminalTitleSequenceStripper({
|
|
66
|
+
source: "src/core/codex.ts:codex.stdout",
|
|
67
|
+
stream: "stdout",
|
|
68
|
+
origin: "codex-cli",
|
|
69
|
+
});
|
|
70
|
+
const stderrTitleStripper = createTerminalTitleSequenceStripper({
|
|
71
|
+
source: "src/core/codex.ts:codex.stderr",
|
|
72
|
+
stream: "stderr",
|
|
73
|
+
origin: "codex-cli",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const flushLine = (line: string) => {
|
|
77
|
+
if (cancelled || done) return;
|
|
78
|
+
if (line.trim()) handlers.onLine(line);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleText = (text: string) => {
|
|
82
|
+
if (!text) return;
|
|
83
|
+
if (cancelled || done) return;
|
|
84
|
+
buffer += text;
|
|
85
|
+
const lines = buffer.split("\n");
|
|
86
|
+
buffer = lines.pop() ?? "";
|
|
87
|
+
lines.forEach(flushLine);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
proc.stdout?.on("data", (chunk: Buffer) => handleText(stdoutTitleStripper.process(chunk)));
|
|
91
|
+
proc.stderr?.on("data", (chunk: Buffer) => handleText(stderrTitleStripper.process(chunk)));
|
|
92
|
+
|
|
93
|
+
proc.on("close", (code) => {
|
|
94
|
+
if (cancelled || done) return;
|
|
95
|
+
handleText(stdoutTitleStripper.flush());
|
|
96
|
+
handleText(stderrTitleStripper.flush());
|
|
97
|
+
if (buffer.trim()) flushLine(buffer);
|
|
98
|
+
buffer = "";
|
|
99
|
+
if (done) return;
|
|
100
|
+
if (code === 0) {
|
|
101
|
+
finishSuccess();
|
|
102
|
+
} else {
|
|
103
|
+
finishError(`Process exited with code ${code}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
proc.on("error", (err) => {
|
|
108
|
+
if (cancelled || done) return;
|
|
109
|
+
const errno = err as NodeJS.ErrnoException;
|
|
110
|
+
const msg = formatCodexLaunchError(errno);
|
|
111
|
+
finishError(msg);
|
|
112
|
+
});
|
|
113
|
+
})
|
|
114
|
+
.catch((error) => {
|
|
115
|
+
const errno = error as NodeJS.ErrnoException;
|
|
116
|
+
finishError(formatCodexLaunchError(errno));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
done = true;
|
|
122
|
+
proc?.kill();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { resolveRuntimeConfig, normalizeRuntimeConfig } from "../config/runtimeConfig.js";
|
|
4
|
+
import { buildCodexExecArgs } from "./codexExecArgs.js";
|
|
5
|
+
import type { CodexCliCapabilities } from "./models/codexCapabilities.js";
|
|
6
|
+
|
|
7
|
+
const fullCapabilities: CodexCliCapabilities = {
|
|
8
|
+
askForApproval: true,
|
|
9
|
+
sandbox: true,
|
|
10
|
+
config: true,
|
|
11
|
+
fullAuto: true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test("uses dedicated runtime policy flags when supported", () => {
|
|
15
|
+
const result = buildCodexExecArgs({
|
|
16
|
+
runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
|
|
17
|
+
model: "gpt-5.4",
|
|
18
|
+
policy: {
|
|
19
|
+
approvalPolicy: "untrusted",
|
|
20
|
+
sandboxMode: "read-only",
|
|
21
|
+
},
|
|
22
|
+
})),
|
|
23
|
+
cwd: "C:/repo",
|
|
24
|
+
}, fullCapabilities);
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(result, {
|
|
27
|
+
ok: true,
|
|
28
|
+
strategy: "direct-flags",
|
|
29
|
+
args: [
|
|
30
|
+
"exec",
|
|
31
|
+
"--experimental-json",
|
|
32
|
+
"--skip-git-repo-check",
|
|
33
|
+
"--cd",
|
|
34
|
+
"C:/repo",
|
|
35
|
+
"--model",
|
|
36
|
+
"gpt-5.4",
|
|
37
|
+
"--config",
|
|
38
|
+
"model_reasoning_effort=high",
|
|
39
|
+
"--ask-for-approval",
|
|
40
|
+
"untrusted",
|
|
41
|
+
"--sandbox",
|
|
42
|
+
"read-only",
|
|
43
|
+
"-",
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("falls back to config overrides when dedicated policy flags are unavailable", () => {
|
|
49
|
+
const result = buildCodexExecArgs({
|
|
50
|
+
runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
|
|
51
|
+
model: "gpt-5.4-mini",
|
|
52
|
+
reasoningLevel: "medium",
|
|
53
|
+
policy: {
|
|
54
|
+
approvalPolicy: "on-request",
|
|
55
|
+
sandboxMode: "workspace-write",
|
|
56
|
+
},
|
|
57
|
+
})),
|
|
58
|
+
cwd: "C:/repo",
|
|
59
|
+
}, {
|
|
60
|
+
askForApproval: false,
|
|
61
|
+
sandbox: false,
|
|
62
|
+
config: true,
|
|
63
|
+
fullAuto: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(result, {
|
|
67
|
+
ok: true,
|
|
68
|
+
strategy: "config-overrides",
|
|
69
|
+
args: [
|
|
70
|
+
"exec",
|
|
71
|
+
"--experimental-json",
|
|
72
|
+
"--skip-git-repo-check",
|
|
73
|
+
"--cd",
|
|
74
|
+
"C:/repo",
|
|
75
|
+
"--model",
|
|
76
|
+
"gpt-5.4-mini",
|
|
77
|
+
"--config",
|
|
78
|
+
"model_reasoning_effort=medium",
|
|
79
|
+
"-c",
|
|
80
|
+
"approval_policy=on-request",
|
|
81
|
+
"-c",
|
|
82
|
+
"sandbox_mode=workspace-write",
|
|
83
|
+
"-",
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("fails clearly when the requested policy cannot be represented safely", () => {
|
|
89
|
+
const result = buildCodexExecArgs({
|
|
90
|
+
runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
|
|
91
|
+
model: "gpt-5.4",
|
|
92
|
+
policy: {
|
|
93
|
+
approvalPolicy: "on-request",
|
|
94
|
+
sandboxMode: "workspace-write",
|
|
95
|
+
},
|
|
96
|
+
})),
|
|
97
|
+
cwd: "C:/repo",
|
|
98
|
+
}, {
|
|
99
|
+
askForApproval: false,
|
|
100
|
+
sandbox: false,
|
|
101
|
+
config: false,
|
|
102
|
+
fullAuto: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.equal(result.ok, false);
|
|
106
|
+
assert.equal(result.strategy, "fail");
|
|
107
|
+
assert.match(result.ok ? "" : result.error, /cannot safely apply the selected reasoning level/i);
|
|
108
|
+
assert.match(result.ok ? "" : result.error, /does not support --config/i);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("uses mixed config and direct flags when only approval direct flag is unavailable", () => {
|
|
112
|
+
const result = buildCodexExecArgs({
|
|
113
|
+
runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
|
|
114
|
+
model: "gpt-5.4",
|
|
115
|
+
reasoningLevel: "medium",
|
|
116
|
+
policy: {
|
|
117
|
+
approvalPolicy: "on-request",
|
|
118
|
+
sandboxMode: "workspace-write",
|
|
119
|
+
},
|
|
120
|
+
})),
|
|
121
|
+
cwd: "C:/repo",
|
|
122
|
+
}, {
|
|
123
|
+
askForApproval: false,
|
|
124
|
+
sandbox: true,
|
|
125
|
+
config: true,
|
|
126
|
+
fullAuto: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(result, {
|
|
130
|
+
ok: true,
|
|
131
|
+
strategy: "config-overrides",
|
|
132
|
+
args: [
|
|
133
|
+
"exec",
|
|
134
|
+
"--experimental-json",
|
|
135
|
+
"--skip-git-repo-check",
|
|
136
|
+
"--cd",
|
|
137
|
+
"C:/repo",
|
|
138
|
+
"--model",
|
|
139
|
+
"gpt-5.4",
|
|
140
|
+
"--config",
|
|
141
|
+
"model_reasoning_effort=medium",
|
|
142
|
+
"-c",
|
|
143
|
+
"approval_policy=on-request",
|
|
144
|
+
"--sandbox",
|
|
145
|
+
"workspace-write",
|
|
146
|
+
"-",
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("forwards network, writable roots, service tier, and personality as config overrides", () => {
|
|
152
|
+
const result = buildCodexExecArgs({
|
|
153
|
+
runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
|
|
154
|
+
model: "gpt-5.4",
|
|
155
|
+
policy: {
|
|
156
|
+
approvalPolicy: "never",
|
|
157
|
+
sandboxMode: "workspace-write",
|
|
158
|
+
networkAccess: "enabled",
|
|
159
|
+
writableRoots: ["C:/repo/tmp", "C:/repo/cache"],
|
|
160
|
+
serviceTier: "fast",
|
|
161
|
+
personality: "pragmatic",
|
|
162
|
+
},
|
|
163
|
+
})),
|
|
164
|
+
cwd: "C:/repo",
|
|
165
|
+
}, fullCapabilities);
|
|
166
|
+
|
|
167
|
+
assert.deepEqual(result, {
|
|
168
|
+
ok: true,
|
|
169
|
+
strategy: "direct-flags",
|
|
170
|
+
args: [
|
|
171
|
+
"exec",
|
|
172
|
+
"--experimental-json",
|
|
173
|
+
"--skip-git-repo-check",
|
|
174
|
+
"--cd",
|
|
175
|
+
"C:/repo",
|
|
176
|
+
"--model",
|
|
177
|
+
"gpt-5.4",
|
|
178
|
+
"--config",
|
|
179
|
+
"model_reasoning_effort=high",
|
|
180
|
+
"--ask-for-approval",
|
|
181
|
+
"never",
|
|
182
|
+
"--sandbox",
|
|
183
|
+
"workspace-write",
|
|
184
|
+
"--config",
|
|
185
|
+
"sandbox_workspace_write.network_access=true",
|
|
186
|
+
"--config",
|
|
187
|
+
"sandbox_workspace_write.writable_roots=[\"C:\\\\repo\\\\tmp\",\"C:\\\\repo\\\\cache\"]",
|
|
188
|
+
"--config",
|
|
189
|
+
"service_tier=fast",
|
|
190
|
+
"--config",
|
|
191
|
+
"personality=pragmatic",
|
|
192
|
+
"-",
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
|
|
2
|
+
import type { CodexCliCapabilities } from "./models/codexCapabilities.js";
|
|
3
|
+
|
|
4
|
+
export interface BuildCodexExecArgsOptions {
|
|
5
|
+
runtime: ResolvedRuntimeConfig;
|
|
6
|
+
cwd: string;
|
|
7
|
+
structuredOutput?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CodexLaunchStrategy =
|
|
11
|
+
| "direct-flags"
|
|
12
|
+
| "config-overrides"
|
|
13
|
+
| "full-auto"
|
|
14
|
+
| "fail";
|
|
15
|
+
|
|
16
|
+
export type BuildCodexExecArgsResult =
|
|
17
|
+
| { ok: true; args: string[]; strategy: Exclude<CodexLaunchStrategy, "fail"> }
|
|
18
|
+
| { ok: false; error: string; strategy: "fail" };
|
|
19
|
+
|
|
20
|
+
// Prevents newline/null injection into CLI args — rejects the path if it contains unsafe characters.
|
|
21
|
+
function sanitizeWorkingDirectory(cwd: string): string {
|
|
22
|
+
if (cwd.includes("\n") || cwd.includes("\r") || cwd.includes("\0")) {
|
|
23
|
+
return process.cwd();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return cwd;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isFullAutoRuntime(runtime: ResolvedRuntimeConfig): boolean {
|
|
30
|
+
return runtime.policy.approvalPolicy === "never" && runtime.policy.sandboxMode === "danger-full-access";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildCapabilitySummary(capabilities: CodexCliCapabilities): string {
|
|
34
|
+
const supported: string[] = [];
|
|
35
|
+
|
|
36
|
+
if (capabilities.askForApproval) supported.push("--ask-for-approval");
|
|
37
|
+
if (capabilities.sandbox) supported.push("--sandbox");
|
|
38
|
+
if (capabilities.config) supported.push("--config/-c");
|
|
39
|
+
if (capabilities.fullAuto) supported.push("--full-auto");
|
|
40
|
+
|
|
41
|
+
return supported.length > 0 ? supported.join(", ") : "none";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildRuntimeFailureMessage(runtime: ResolvedRuntimeConfig, capabilities: CodexCliCapabilities): string {
|
|
45
|
+
return [
|
|
46
|
+
"Installed Codex CLI cannot safely apply the requested runtime configuration.",
|
|
47
|
+
`Requested approval policy: ${runtime.policy.approvalPolicy}.`,
|
|
48
|
+
`Requested sandbox mode: ${runtime.policy.sandboxMode}.`,
|
|
49
|
+
`Detected launch controls: ${buildCapabilitySummary(capabilities)}.`,
|
|
50
|
+
"Update Codex or choose a runtime configuration that your installed CLI can represent.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildRuntimePolicyArgs(runtime: ResolvedRuntimeConfig, capabilities: CodexCliCapabilities): BuildCodexExecArgsResult {
|
|
55
|
+
const args: string[] = [];
|
|
56
|
+
const missingDirectApproval = !capabilities.askForApproval;
|
|
57
|
+
const missingDirectSandbox = !capabilities.sandbox;
|
|
58
|
+
const runtimePolicy = runtime.policy;
|
|
59
|
+
|
|
60
|
+
if (!missingDirectApproval && !missingDirectSandbox) {
|
|
61
|
+
args.push("--ask-for-approval", runtimePolicy.approvalPolicy);
|
|
62
|
+
args.push("--sandbox", runtimePolicy.sandboxMode);
|
|
63
|
+
return { ok: true, args, strategy: "direct-flags" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isFullAutoRuntime(runtime) && capabilities.fullAuto) {
|
|
67
|
+
args.push("--full-auto");
|
|
68
|
+
return { ok: true, args, strategy: "full-auto" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (capabilities.config) {
|
|
72
|
+
if (capabilities.askForApproval) {
|
|
73
|
+
args.push("--ask-for-approval", runtimePolicy.approvalPolicy);
|
|
74
|
+
} else {
|
|
75
|
+
args.push("-c", `approval_policy=${runtimePolicy.approvalPolicy}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (capabilities.sandbox) {
|
|
79
|
+
args.push("--sandbox", runtimePolicy.sandboxMode);
|
|
80
|
+
} else {
|
|
81
|
+
args.push("-c", `sandbox_mode=${runtimePolicy.sandboxMode}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { ok: true, args, strategy: "config-overrides" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
strategy: "fail",
|
|
90
|
+
error: buildRuntimeFailureMessage(runtime, capabilities),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildCodexExecArgs(
|
|
95
|
+
options: BuildCodexExecArgsOptions,
|
|
96
|
+
capabilities: CodexCliCapabilities,
|
|
97
|
+
): BuildCodexExecArgsResult {
|
|
98
|
+
const { runtime } = options;
|
|
99
|
+
const args: string[] = ["exec"];
|
|
100
|
+
|
|
101
|
+
if (options.structuredOutput ?? true) {
|
|
102
|
+
args.push("--experimental-json");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
args.push(
|
|
106
|
+
"--skip-git-repo-check",
|
|
107
|
+
"--cd",
|
|
108
|
+
sanitizeWorkingDirectory(options.cwd),
|
|
109
|
+
"--model",
|
|
110
|
+
runtime.model,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!capabilities.config) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
strategy: "fail",
|
|
117
|
+
error: [
|
|
118
|
+
`Installed Codex CLI cannot safely apply the selected reasoning level "${runtime.reasoningLevel}".`,
|
|
119
|
+
`Detected launch controls: ${buildCapabilitySummary(capabilities)}.`,
|
|
120
|
+
"This Codex version does not support --config / -c overrides.",
|
|
121
|
+
].join("\n"),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
args.push("--config", `model_reasoning_effort=${runtime.reasoningLevel}`);
|
|
126
|
+
|
|
127
|
+
const policyArgs = buildRuntimePolicyArgs(runtime, capabilities);
|
|
128
|
+
if (!policyArgs.ok) {
|
|
129
|
+
return policyArgs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
args.push(...policyArgs.args);
|
|
133
|
+
|
|
134
|
+
if (runtime.policy.networkAccess) {
|
|
135
|
+
args.push("--config", `sandbox_workspace_write.network_access=${JSON.stringify(runtime.policy.networkAccess)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (runtime.policy.writableRoots.length > 0) {
|
|
139
|
+
args.push("--config", `sandbox_workspace_write.writable_roots=${JSON.stringify(runtime.policy.writableRoots)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (runtime.policy.serviceTier !== "flex") {
|
|
143
|
+
args.push("--config", `service_tier=${runtime.policy.serviceTier}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (runtime.policy.personality !== "none") {
|
|
147
|
+
args.push("--config", `personality=${runtime.policy.personality}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
args.push("-");
|
|
151
|
+
return { ok: true, args, strategy: policyArgs.strategy };
|
|
152
|
+
}
|