@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,155 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
isImageFile,
|
|
8
|
+
resolveAttachmentDestPath,
|
|
9
|
+
importExternalFile,
|
|
10
|
+
rewritePromptWithImportedPaths,
|
|
11
|
+
} from "./attachments.js";
|
|
12
|
+
|
|
13
|
+
// ─── isImageFile ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test("isImageFile returns true for .png", () => {
|
|
16
|
+
assert.equal(isImageFile("screenshot.png"), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("isImageFile returns true for .jpg", () => {
|
|
20
|
+
assert.equal(isImageFile("photo.jpg"), true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("isImageFile returns true for .jpeg", () => {
|
|
24
|
+
assert.equal(isImageFile("image.jpeg"), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("isImageFile returns true for uppercase extension", () => {
|
|
28
|
+
assert.equal(isImageFile("PHOTO.JPG"), true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("isImageFile returns true for .webp", () => {
|
|
32
|
+
assert.equal(isImageFile("img.webp"), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("isImageFile returns false for .txt", () => {
|
|
36
|
+
assert.equal(isImageFile("document.txt"), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("isImageFile returns false for .ts", () => {
|
|
40
|
+
assert.equal(isImageFile("component.ts"), false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("isImageFile returns false for no extension", () => {
|
|
44
|
+
assert.equal(isImageFile("noext"), false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── resolveAttachmentDestPath ────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
test("resolveAttachmentDestPath returns base filename when no collision", async () => {
|
|
50
|
+
const dir = path.join(tmpdir(), `codexa-attach-test-${Date.now()}`);
|
|
51
|
+
await mkdir(dir, { recursive: true });
|
|
52
|
+
try {
|
|
53
|
+
const dest = await resolveAttachmentDestPath("/some/path/file.png", dir);
|
|
54
|
+
assert.equal(path.basename(dest), "file.png");
|
|
55
|
+
assert.equal(path.dirname(dest), dir);
|
|
56
|
+
} finally {
|
|
57
|
+
await rm(dir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("resolveAttachmentDestPath appends -1 on collision", async () => {
|
|
62
|
+
const dir = path.join(tmpdir(), `codexa-attach-test-${Date.now()}`);
|
|
63
|
+
await mkdir(dir, { recursive: true });
|
|
64
|
+
try {
|
|
65
|
+
// Create a file that would collide
|
|
66
|
+
await writeFile(path.join(dir, "file.png"), "existing");
|
|
67
|
+
const dest = await resolveAttachmentDestPath("/some/path/file.png", dir);
|
|
68
|
+
assert.equal(path.basename(dest), "file-1.png");
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("resolveAttachmentDestPath appends -2 on double collision", async () => {
|
|
75
|
+
const dir = path.join(tmpdir(), `codexa-attach-test-${Date.now()}`);
|
|
76
|
+
await mkdir(dir, { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
await writeFile(path.join(dir, "file.png"), "existing");
|
|
79
|
+
await writeFile(path.join(dir, "file-1.png"), "existing-1");
|
|
80
|
+
const dest = await resolveAttachmentDestPath("/some/path/file.png", dir);
|
|
81
|
+
assert.equal(path.basename(dest), "file-2.png");
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(dir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── importExternalFile ───────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
test("importExternalFile creates attachments dir if missing and copies file", async () => {
|
|
90
|
+
const baseDir = path.join(tmpdir(), `codexa-import-test-${Date.now()}`);
|
|
91
|
+
const srcDir = path.join(baseDir, "src");
|
|
92
|
+
const destDir = path.join(baseDir, "attachments");
|
|
93
|
+
await mkdir(srcDir, { recursive: true });
|
|
94
|
+
const srcPath = path.join(srcDir, "test.png");
|
|
95
|
+
await writeFile(srcPath, "image-content");
|
|
96
|
+
try {
|
|
97
|
+
const destPath = await importExternalFile(srcPath, destDir);
|
|
98
|
+
assert.equal(path.basename(destPath), "test.png");
|
|
99
|
+
// Verify the file was copied
|
|
100
|
+
const { readFile } = await import("node:fs/promises");
|
|
101
|
+
const content = await readFile(destPath, "utf8");
|
|
102
|
+
assert.equal(content, "image-content");
|
|
103
|
+
} finally {
|
|
104
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── rewritePromptWithImportedPaths ──────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
test("rewritePromptWithImportedPaths rewrites quoted path with spaces", () => {
|
|
111
|
+
const prompt = '"C:\\Users\\jorda\\OneDrive\\Screenshots\\Screenshot 2026.png" what is this?';
|
|
112
|
+
const result = rewritePromptWithImportedPaths(prompt, [{
|
|
113
|
+
rawPath: "C:\\Users\\jorda\\OneDrive\\Screenshots\\Screenshot 2026.png",
|
|
114
|
+
workspaceRelativePath: ".codexa/attachments/Screenshot 2026.png",
|
|
115
|
+
}]);
|
|
116
|
+
assert.equal(result, '".codexa/attachments/Screenshot 2026.png" what is this?');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("rewritePromptWithImportedPaths rewrites unquoted path without spaces", () => {
|
|
120
|
+
const prompt = "Look at C:\\Users\\jorda\\file.png please";
|
|
121
|
+
const result = rewritePromptWithImportedPaths(prompt, [{
|
|
122
|
+
rawPath: "C:\\Users\\jorda\\file.png",
|
|
123
|
+
workspaceRelativePath: ".codexa/attachments/file.png",
|
|
124
|
+
}]);
|
|
125
|
+
assert.equal(result, "Look at .codexa/attachments/file.png please");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("rewritePromptWithImportedPaths leaves non-matched text unchanged", () => {
|
|
129
|
+
const prompt = "hello world, no paths here";
|
|
130
|
+
const result = rewritePromptWithImportedPaths(prompt, [{
|
|
131
|
+
rawPath: "C:\\some\\other\\path.png",
|
|
132
|
+
workspaceRelativePath: ".codexa/attachments/path.png",
|
|
133
|
+
}]);
|
|
134
|
+
assert.equal(result, "hello world, no paths here");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("rewritePromptWithImportedPaths does not double-quote already-quoted replacement path with spaces", () => {
|
|
138
|
+
const prompt = '"C:\\Users\\file with spaces.png" describe';
|
|
139
|
+
const result = rewritePromptWithImportedPaths(prompt, [{
|
|
140
|
+
rawPath: "C:\\Users\\file with spaces.png",
|
|
141
|
+
workspaceRelativePath: ".codexa/attachments/file with spaces.png",
|
|
142
|
+
}]);
|
|
143
|
+
// workspaceRelativePath has spaces → re-quoted; original was quoted → double-quoted match replaced
|
|
144
|
+
assert.equal(result, '".codexa/attachments/file with spaces.png" describe');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("rewritePromptWithImportedPaths handles multiple replacements", () => {
|
|
148
|
+
// Paths without spaces get no quotes; the enclosing quotes from the original are consumed
|
|
149
|
+
const prompt = '"C:\\path\\a.png" and "C:\\path\\b.png" compare them';
|
|
150
|
+
const result = rewritePromptWithImportedPaths(prompt, [
|
|
151
|
+
{ rawPath: "C:\\path\\a.png", workspaceRelativePath: ".codexa/attachments/a.png" },
|
|
152
|
+
{ rawPath: "C:\\path\\b.png", workspaceRelativePath: ".codexa/attachments/b.png" },
|
|
153
|
+
]);
|
|
154
|
+
assert.equal(result, ".codexa/attachments/a.png and .codexa/attachments/b.png compare them");
|
|
155
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { access, copyFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const IMAGE_EXTENSIONS = new Set([
|
|
5
|
+
".png",
|
|
6
|
+
".jpg",
|
|
7
|
+
".jpeg",
|
|
8
|
+
".gif",
|
|
9
|
+
".webp",
|
|
10
|
+
".bmp",
|
|
11
|
+
".tiff",
|
|
12
|
+
".tif",
|
|
13
|
+
".svg",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function isImageFile(filePath: string): boolean {
|
|
17
|
+
return IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
await access(filePath);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function resolveAttachmentDestPath(
|
|
30
|
+
srcPath: string,
|
|
31
|
+
attachmentsDir: string,
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const ext = path.extname(srcPath);
|
|
34
|
+
const base = path.basename(srcPath, ext);
|
|
35
|
+
let dest = path.join(attachmentsDir, `${base}${ext}`);
|
|
36
|
+
let counter = 1;
|
|
37
|
+
while (await fileExists(dest)) {
|
|
38
|
+
dest = path.join(attachmentsDir, `${base}-${counter}${ext}`);
|
|
39
|
+
counter++;
|
|
40
|
+
}
|
|
41
|
+
return dest;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function importExternalFile(
|
|
45
|
+
srcPath: string,
|
|
46
|
+
attachmentsDir: string,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
49
|
+
const destPath = await resolveAttachmentDestPath(srcPath, attachmentsDir);
|
|
50
|
+
await copyFile(srcPath, destPath);
|
|
51
|
+
return destPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rewritePromptWithImportedPaths(
|
|
55
|
+
prompt: string,
|
|
56
|
+
replacements: Array<{ rawPath: string; workspaceRelativePath: string }>,
|
|
57
|
+
): string {
|
|
58
|
+
let result = prompt;
|
|
59
|
+
for (const { rawPath, workspaceRelativePath } of replacements) {
|
|
60
|
+
const needsQuotes = /\s/.test(workspaceRelativePath);
|
|
61
|
+
const quotedReplacement = needsQuotes
|
|
62
|
+
? `"${workspaceRelativePath}"`
|
|
63
|
+
: workspaceRelativePath;
|
|
64
|
+
// Replace quoted forms first so the unquoted pass doesn't double-replace
|
|
65
|
+
result = result.split(`"${rawPath}"`).join(quotedReplacement);
|
|
66
|
+
result = result.split(`'${rawPath}'`).join(quotedReplacement);
|
|
67
|
+
// Replace any remaining unquoted occurrences
|
|
68
|
+
result = result.split(rawPath).join(quotedReplacement);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
getRunGateDecision,
|
|
5
|
+
getAuthStateLabel,
|
|
6
|
+
inferAuthStateFromProbe,
|
|
7
|
+
isLikelyAuthFailure,
|
|
8
|
+
} from "./codexAuth.js";
|
|
9
|
+
|
|
10
|
+
test("infers authenticated from successful probe exit", () => {
|
|
11
|
+
const state = inferAuthStateFromProbe(0, "", "");
|
|
12
|
+
assert.equal(state, "authenticated");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("infers unauthenticated from signed-out probe output", () => {
|
|
16
|
+
const state = inferAuthStateFromProbe(
|
|
17
|
+
1,
|
|
18
|
+
"No active session. Please login.",
|
|
19
|
+
"",
|
|
20
|
+
);
|
|
21
|
+
assert.equal(state, "unauthenticated");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("infers unknown from ambiguous probe output", () => {
|
|
25
|
+
const state = inferAuthStateFromProbe(
|
|
26
|
+
2,
|
|
27
|
+
"Usage: codex login status [OPTIONS]",
|
|
28
|
+
"unexpected argument '--json'",
|
|
29
|
+
);
|
|
30
|
+
assert.equal(state, "unknown");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("blocks runs when unauthenticated", () => {
|
|
34
|
+
const decision = getRunGateDecision("unauthenticated");
|
|
35
|
+
assert.equal(decision.allowRun, false);
|
|
36
|
+
assert.match(decision.blockMessage ?? "", /codex login/i);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("warns but allows runs when auth state is unknown", () => {
|
|
40
|
+
const decision = getRunGateDecision("unknown");
|
|
41
|
+
assert.equal(decision.allowRun, true);
|
|
42
|
+
assert.match(decision.warningMessage ?? "", /unknown/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("allows unchecked initial auth state without a warning", () => {
|
|
46
|
+
const decision = getRunGateDecision("unknown", { warnOnUnknown: false });
|
|
47
|
+
assert.equal(decision.allowRun, true);
|
|
48
|
+
assert.equal(decision.warningMessage, undefined);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("allows runs when authenticated", () => {
|
|
52
|
+
const decision = getRunGateDecision("authenticated");
|
|
53
|
+
assert.equal(decision.allowRun, true);
|
|
54
|
+
assert.equal(decision.warningMessage, undefined);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("detects runtime auth failure messages", () => {
|
|
58
|
+
const detected = isLikelyAuthFailure("Unauthorized (401): token expired. Run codex login.");
|
|
59
|
+
assert.equal(detected, true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("getAuthStateLabel returns Checking for checking state", () => {
|
|
63
|
+
assert.equal(getAuthStateLabel("checking"), "Checking");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("getAuthStateLabel returns Unknown for unknown state", () => {
|
|
67
|
+
assert.equal(getAuthStateLabel("unknown"), "Unknown");
|
|
68
|
+
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { formatCodexLaunchError, resolveCodexExecutable, spawnCodexProcess } from "../executables/codexExecutable.js";
|
|
3
|
+
|
|
4
|
+
export type CodexAuthState = "checking" | "authenticated" | "unauthenticated" | "unknown";
|
|
5
|
+
|
|
6
|
+
export interface CodexAuthProbeResult {
|
|
7
|
+
state: CodexAuthState;
|
|
8
|
+
checkedAt: number;
|
|
9
|
+
rawSummary: string;
|
|
10
|
+
recommendedAction: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CommandResult {
|
|
14
|
+
exitCode: number | null;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
timedOut: boolean;
|
|
18
|
+
error?: NodeJS.ErrnoException;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const JSON_STATUS_FLAGS = ["authenticated", "is_authenticated", "logged_in", "signed_in"] as const;
|
|
22
|
+
|
|
23
|
+
const UNAUTHENTICATED_PATTERNS = [
|
|
24
|
+
"not logged in",
|
|
25
|
+
"not signed in",
|
|
26
|
+
"no active session",
|
|
27
|
+
"login required",
|
|
28
|
+
"sign in required",
|
|
29
|
+
"unauthenticated",
|
|
30
|
+
"authentication required",
|
|
31
|
+
"run `codex login`",
|
|
32
|
+
"run codex login",
|
|
33
|
+
"please login",
|
|
34
|
+
"please log in",
|
|
35
|
+
"session expired",
|
|
36
|
+
"token expired",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const AUTH_FAILURE_PATTERNS = [
|
|
40
|
+
...UNAUTHENTICATED_PATTERNS,
|
|
41
|
+
"unauthorized",
|
|
42
|
+
"forbidden",
|
|
43
|
+
"invalid token",
|
|
44
|
+
"invalid grant",
|
|
45
|
+
"access denied",
|
|
46
|
+
"401",
|
|
47
|
+
"403",
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export function getAuthStateLabel(state: CodexAuthState): string {
|
|
51
|
+
switch (state) {
|
|
52
|
+
case "authenticated":
|
|
53
|
+
return "Authenticated";
|
|
54
|
+
case "unauthenticated":
|
|
55
|
+
return "Signed out";
|
|
56
|
+
case "checking":
|
|
57
|
+
return "Checking";
|
|
58
|
+
default:
|
|
59
|
+
return "Unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getLoginGuidance(): string {
|
|
64
|
+
return [
|
|
65
|
+
"Sign in to the Codexa neural network to continue.",
|
|
66
|
+
"Run this in your terminal:",
|
|
67
|
+
" codex login",
|
|
68
|
+
"",
|
|
69
|
+
"If you were previously using API-key auth and want ChatGPT subscription auth:",
|
|
70
|
+
" codex logout",
|
|
71
|
+
" codex",
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getLogoutGuidance(): string {
|
|
76
|
+
return [
|
|
77
|
+
"You are managing your Codexa sign-out state.",
|
|
78
|
+
"Run this in your terminal:",
|
|
79
|
+
" codex logout",
|
|
80
|
+
"",
|
|
81
|
+
"After logging out, use /auth status in this UI to refresh state.",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getAuthStatusMessage(result: CodexAuthProbeResult): string {
|
|
86
|
+
if (result.state === "authenticated") {
|
|
87
|
+
return [
|
|
88
|
+
"Codexa authentication looks healthy.",
|
|
89
|
+
"State: Authenticated",
|
|
90
|
+
`Summary: ${result.rawSummary}`,
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.state === "unauthenticated") {
|
|
95
|
+
return [
|
|
96
|
+
"Codexa is currently signed out.",
|
|
97
|
+
"State: Signed out",
|
|
98
|
+
`Summary: ${result.rawSummary}`,
|
|
99
|
+
"Recovery:",
|
|
100
|
+
" codex login",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result.state === "checking") {
|
|
105
|
+
return "Authentication check is currently running.";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
"Codexa auth state is unknown. This can happen on unsupported neural versions.",
|
|
110
|
+
`Summary: ${result.rawSummary}`,
|
|
111
|
+
`Recommended action: ${result.recommendedAction}`,
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface RunGateDecision {
|
|
116
|
+
allowRun: boolean;
|
|
117
|
+
blockMessage?: string;
|
|
118
|
+
warningMessage?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface RunGateDecisionOptions {
|
|
122
|
+
warnOnUnknown?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getRunGateDecision(
|
|
126
|
+
authState: CodexAuthState,
|
|
127
|
+
options: RunGateDecisionOptions = {},
|
|
128
|
+
): RunGateDecision {
|
|
129
|
+
if (authState === "unauthenticated") {
|
|
130
|
+
return {
|
|
131
|
+
allowRun: false,
|
|
132
|
+
blockMessage: [
|
|
133
|
+
"Run blocked: Codexa is signed out.",
|
|
134
|
+
"Sign in first with your ChatGPT subscription:",
|
|
135
|
+
" codex login",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (authState === "unknown" || authState === "checking") {
|
|
141
|
+
if (options.warnOnUnknown === false) {
|
|
142
|
+
return { allowRun: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
allowRun: true,
|
|
147
|
+
warningMessage:
|
|
148
|
+
"Auth state is unknown. Run is allowed, but if this fails, use `codex login` and try again.",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { allowRun: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function inferAuthStateFromProbe(
|
|
156
|
+
exitCode: number | null,
|
|
157
|
+
stdout: string,
|
|
158
|
+
stderr: string,
|
|
159
|
+
): CodexAuthState {
|
|
160
|
+
if (exitCode === 0) return "authenticated";
|
|
161
|
+
|
|
162
|
+
const jsonState = inferAuthStateFromJson(stdout);
|
|
163
|
+
if (jsonState) return jsonState;
|
|
164
|
+
|
|
165
|
+
const output = `${stdout}\n${stderr}`.toLowerCase();
|
|
166
|
+
|
|
167
|
+
if (UNAUTHENTICATED_PATTERNS.some((pattern) => output.includes(pattern))) {
|
|
168
|
+
return "unauthenticated";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function isLikelyAuthFailure(message: string): boolean {
|
|
175
|
+
const lower = message.toLowerCase();
|
|
176
|
+
return AUTH_FAILURE_PATTERNS.some((pattern) => lower.includes(pattern));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function probeCodexAuthStatus(): Promise<CodexAuthProbeResult> {
|
|
180
|
+
const attempts: string[][] = [
|
|
181
|
+
["login", "status", "--json"],
|
|
182
|
+
["login", "status"],
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const summaries: string[] = [];
|
|
186
|
+
|
|
187
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
188
|
+
const args = attempts[index]!;
|
|
189
|
+
const result = await runCodexCommand(args);
|
|
190
|
+
const summary = summarizeAttempt(args, result);
|
|
191
|
+
summaries.push(summary);
|
|
192
|
+
|
|
193
|
+
if (result.error) {
|
|
194
|
+
if (isCodexUnavailableError(result.error)) {
|
|
195
|
+
return {
|
|
196
|
+
state: "unknown",
|
|
197
|
+
checkedAt: Date.now(),
|
|
198
|
+
rawSummary: summary,
|
|
199
|
+
recommendedAction:
|
|
200
|
+
"Set CODEX_EXECUTABLE to a working Codex command/path, restart Codexa, then run /auth status again.",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const inferred = inferAuthStateFromProbe(result.exitCode, result.stdout, result.stderr);
|
|
208
|
+
|
|
209
|
+
if (inferred === "authenticated" || inferred === "unauthenticated") {
|
|
210
|
+
return {
|
|
211
|
+
state: inferred,
|
|
212
|
+
checkedAt: Date.now(),
|
|
213
|
+
rawSummary: summary,
|
|
214
|
+
recommendedAction: inferred === "authenticated" ? "None" : "Run `codex login` and retry.",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
state: "unknown",
|
|
221
|
+
checkedAt: Date.now(),
|
|
222
|
+
rawSummary: summaries.join(" | "),
|
|
223
|
+
recommendedAction: "If runs fail, sign in with `codex login`, then retry `/auth status`.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function inferAuthStateFromJson(raw: string): CodexAuthState | null {
|
|
228
|
+
const trimmed = raw.trim();
|
|
229
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const payload = JSON.parse(trimmed) as Record<string, unknown>;
|
|
233
|
+
|
|
234
|
+
for (const key of JSON_STATUS_FLAGS) {
|
|
235
|
+
const value = payload[key];
|
|
236
|
+
if (typeof value === "boolean") {
|
|
237
|
+
return value ? "authenticated" : "unauthenticated";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const status = payload.status;
|
|
242
|
+
if (typeof status === "string") {
|
|
243
|
+
const lower = status.toLowerCase();
|
|
244
|
+
if (lower.includes("auth")) return "authenticated";
|
|
245
|
+
if (lower.includes("unauth")) return "unauthenticated";
|
|
246
|
+
if (lower.includes("signed_out")) return "unauthenticated";
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runCodexCommand(args: string[], timeoutMs = 6000): Promise<CommandResult> {
|
|
256
|
+
return new Promise<CommandResult>((resolve) => {
|
|
257
|
+
let done = false;
|
|
258
|
+
let proc: ReturnType<typeof spawn> | null = null;
|
|
259
|
+
|
|
260
|
+
let stdout = "";
|
|
261
|
+
let stderr = "";
|
|
262
|
+
|
|
263
|
+
const finish = (result: CommandResult) => {
|
|
264
|
+
if (done) return;
|
|
265
|
+
done = true;
|
|
266
|
+
resolve(result);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const startWithExecutable = async () => {
|
|
270
|
+
let executable: string;
|
|
271
|
+
try {
|
|
272
|
+
executable = await resolveCodexExecutable();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const errno = error as NodeJS.ErrnoException;
|
|
275
|
+
finish({
|
|
276
|
+
exitCode: null,
|
|
277
|
+
stdout,
|
|
278
|
+
stderr: formatCodexLaunchError(errno),
|
|
279
|
+
timedOut: false,
|
|
280
|
+
error: errno,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (done) return;
|
|
286
|
+
try {
|
|
287
|
+
proc = spawnCodexProcess(executable, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const errno = error as NodeJS.ErrnoException;
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
finish({
|
|
292
|
+
exitCode: null,
|
|
293
|
+
stdout,
|
|
294
|
+
stderr: formatCodexLaunchError(errno),
|
|
295
|
+
timedOut: false,
|
|
296
|
+
error: errno,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
302
|
+
stdout += chunk.toString();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
306
|
+
stderr += chunk.toString();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
proc.on("error", (error) => {
|
|
310
|
+
clearTimeout(timer);
|
|
311
|
+
const errno = error as NodeJS.ErrnoException;
|
|
312
|
+
finish({
|
|
313
|
+
exitCode: null,
|
|
314
|
+
stdout,
|
|
315
|
+
stderr: formatCodexLaunchError(errno),
|
|
316
|
+
timedOut: false,
|
|
317
|
+
error: errno,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
proc.on("close", (exitCode) => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
finish({
|
|
324
|
+
exitCode,
|
|
325
|
+
stdout,
|
|
326
|
+
stderr,
|
|
327
|
+
timedOut: false,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const timer = setTimeout(() => {
|
|
333
|
+
proc?.kill();
|
|
334
|
+
finish({
|
|
335
|
+
exitCode: null,
|
|
336
|
+
stdout,
|
|
337
|
+
stderr,
|
|
338
|
+
timedOut: true,
|
|
339
|
+
});
|
|
340
|
+
}, timeoutMs);
|
|
341
|
+
|
|
342
|
+
void startWithExecutable();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function summarizeAttempt(args: string[], result: CommandResult): string {
|
|
347
|
+
const status =
|
|
348
|
+
result.error?.code ??
|
|
349
|
+
(result.timedOut ? "timeout" : `exit:${result.exitCode ?? "null"}`);
|
|
350
|
+
|
|
351
|
+
const combined = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join(" | ");
|
|
352
|
+
const output = combined.length > 140 ? `${combined.slice(0, 137)}...` : combined || "no output";
|
|
353
|
+
|
|
354
|
+
return `[${args.join(" ")}] ${status} - ${output}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isCodexUnavailableError(error: NodeJS.ErrnoException): boolean {
|
|
358
|
+
return error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM";
|
|
359
|
+
}
|