codex-copilot-dx 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -2
- package/bin/cli.mjs +25 -1
- package/package.json +1 -1
- package/src/adapter.mjs +2 -0
- package/src/auto-review-model.mjs +141 -0
- package/src/cli-options.mjs +21 -0
- package/src/models.mjs +15 -4
- package/src/package-update.mjs +100 -0
- package/src/responses-handler.mjs +11 -2
- package/src/user-settings.mjs +67 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A single in-process adapter (port `2026`) exposes both APIs over your Copilot su
|
|
|
12
12
|
|
|
13
13
|
Supports both HTTP SSE streaming and non-streaming.
|
|
14
14
|
|
|
15
|
-
Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adapter maps it to Copilot's `gpt-5.5` Responses model by default and logs both model IDs when the mapping is used.
|
|
15
|
+
Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adapter maps it to Copilot's `gpt-5.5` Responses model by default and logs both model IDs when the mapping is used. The target can be changed interactively without reinstalling the package.
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
@@ -33,6 +33,42 @@ global install, use `npx codex-copilot-dx@latest`.
|
|
|
33
33
|
Both installed commands expose the same options and subcommands; help, version,
|
|
34
34
|
doctor, status, and argument-error output use the command name that was invoked.
|
|
35
35
|
|
|
36
|
+
To change the model used by Codex Auto-review, run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ccdx auto-review-model
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The selector reads the current adapter model list, falling back to the fresh local
|
|
43
|
+
model cache, and offers only enabled models that advertise a Responses endpoint.
|
|
44
|
+
It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
45
|
+
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
46
|
+
the package default. A running 0.5.1+
|
|
47
|
+
adapter reads the setting on the next Auto-review request, so it does not need to
|
|
48
|
+
be restarted. `codex-copilot-dx auto-review-model` is fully equivalent.
|
|
49
|
+
|
|
50
|
+
To update the globally installed package, choose a source interactively:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ccdx update
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For scripts or a direct choice, pass the source explicitly:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ccdx update npm
|
|
60
|
+
ccdx update github
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The npm source installs `codex-copilot-dx@latest` through the registry already
|
|
64
|
+
configured for npm, including a company mirror. The GitHub source installs the
|
|
65
|
+
latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching
|
|
66
|
+
for that command, as required by npm 12. It does not change npm's persistent
|
|
67
|
+
`allow-git` setting. Both paths use npm's global installer without shell
|
|
68
|
+
interpolation. A currently running adapter keeps its loaded version until it is
|
|
69
|
+
stopped and started again. The
|
|
70
|
+
`codex-copilot-dx update` compatibility command behaves identically.
|
|
71
|
+
|
|
36
72
|
On first run, it will:
|
|
37
73
|
1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
|
|
38
74
|
2. Print the local package version and check for a newer npm release in the background
|
|
@@ -145,7 +181,7 @@ Environment variables:
|
|
|
145
181
|
| `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
|
|
146
182
|
| `CCDX_UPSTREAM_RETRIES` | `2` | Retries for safe requests and clearly pre-connect POST failures; capped at `5` |
|
|
147
183
|
| `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
|
|
148
|
-
| `CCDX_AUTO_REVIEW_MODEL` | `gpt-5.5` | Copilot Responses model used for Codex Auto-review requests |
|
|
184
|
+
| `CCDX_AUTO_REVIEW_MODEL` | saved selection or `gpt-5.5` | Copilot Responses model used for Codex Auto-review requests; overrides the interactive selection |
|
|
149
185
|
| `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
|
|
150
186
|
| `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
|
|
151
187
|
| `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
|
package/bin/cli.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
|
|
|
19
19
|
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
20
20
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
21
21
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
22
|
+
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
23
|
+
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
22
24
|
|
|
23
25
|
const LOCAL_VERSION = localPackageVersion();
|
|
24
26
|
const CLI_NAME = cliCommandName();
|
|
@@ -66,6 +68,25 @@ if (CLI.command === "status") {
|
|
|
66
68
|
process.exit(1);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
if (CLI.command === "auto-review-model") {
|
|
72
|
+
try {
|
|
73
|
+
await runAutoReviewModelCommand({ commandName: CLI_NAME });
|
|
74
|
+
process.exit(0);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error(status("err", e.message));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (CLI.command === "update") {
|
|
81
|
+
try {
|
|
82
|
+
const { runPackageUpdateCommand } = await import("../src/package-update.mjs");
|
|
83
|
+
await runPackageUpdateCommand({ commandName: CLI_NAME, source: CLI.updateSource });
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(status("err", e.message));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
69
90
|
|
|
70
91
|
let RUNTIME;
|
|
71
92
|
try {
|
|
@@ -128,7 +149,9 @@ async function refreshClaudeDesktopModelDefs() {
|
|
|
128
149
|
throw new Error(`Copilot models returned HTTP ${httpStatus}`);
|
|
129
150
|
}
|
|
130
151
|
const models = JSON.parse(body);
|
|
131
|
-
const autoReview = codexAutoReviewModelStatus(models
|
|
152
|
+
const autoReview = codexAutoReviewModelStatus(models, process.env, {
|
|
153
|
+
autoReviewModel: autoReviewModelPreference().model,
|
|
154
|
+
});
|
|
132
155
|
if (!autoReview.available) {
|
|
133
156
|
console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ${CLI_NAME} doctor --compat to verify the live path.`));
|
|
134
157
|
}
|
|
@@ -292,6 +315,7 @@ try {
|
|
|
292
315
|
activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
|
|
293
316
|
claudeDesktopApiKey,
|
|
294
317
|
claudeDesktopModelDefs,
|
|
318
|
+
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
295
319
|
modelRegistry: MODEL_REGISTRY,
|
|
296
320
|
showRequestId: CLI.showRequestId,
|
|
297
321
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -106,6 +106,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
106
106
|
};
|
|
107
107
|
const responsesHandler = createResponsesHandler({
|
|
108
108
|
acquireRequest,
|
|
109
|
+
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
109
110
|
chatCompletionsFn,
|
|
110
111
|
openAIModelEnv,
|
|
111
112
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
@@ -116,6 +117,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
116
117
|
});
|
|
117
118
|
const responsesCompactHandler = createResponsesCompactHandler({
|
|
118
119
|
acquireRequest,
|
|
120
|
+
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
119
121
|
openAIModelEnv,
|
|
120
122
|
responsesCompactFn,
|
|
121
123
|
streamHandshakeTimeoutMs,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { loadModelCache } from "./model-cache.mjs";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_CODEX_AUTO_REVIEW_MODEL,
|
|
6
|
+
responsesModelIdsFromCopilotModels,
|
|
7
|
+
} from "./models.mjs";
|
|
8
|
+
import { parseAdapterProbeOptions } from "./cli-options.mjs";
|
|
9
|
+
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
10
|
+
import {
|
|
11
|
+
autoReviewModelPreference,
|
|
12
|
+
readUserSettings,
|
|
13
|
+
writeAutoReviewModel,
|
|
14
|
+
} from "./user-settings.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 5000;
|
|
17
|
+
|
|
18
|
+
function modelFetchTimeout(env) {
|
|
19
|
+
const value = Number(env.CCDX_MODEL_REFRESH_TIMEOUT_MS);
|
|
20
|
+
return Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_MODEL_FETCH_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sortedModelIds(modelIds, currentModel) {
|
|
24
|
+
return [...modelIds].sort((left, right) => {
|
|
25
|
+
const leftRank = left === currentModel ? 0 : left === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? 1 : 2;
|
|
26
|
+
const rightRank = right === currentModel ? 0 : right === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? 1 : 2;
|
|
27
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadAutoReviewModelCatalog({
|
|
32
|
+
env = process.env,
|
|
33
|
+
fetchImpl = fetch,
|
|
34
|
+
home = os.homedir(),
|
|
35
|
+
loadModelCacheFn = loadModelCache,
|
|
36
|
+
} = {}) {
|
|
37
|
+
const { adapterHost, adapterPort } = parseAdapterProbeOptions(env);
|
|
38
|
+
const url = `${adapterBaseUrl(adapterHost, adapterPort)}/v1/models`;
|
|
39
|
+
const ctrl = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => ctrl.abort(), modelFetchTimeout(env));
|
|
41
|
+
let liveError = "adapter is not running";
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl(url, {
|
|
44
|
+
headers: { Accept: "application/json" },
|
|
45
|
+
redirect: "error",
|
|
46
|
+
signal: ctrl.signal,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
liveError = `adapter returned HTTP ${response.status}`;
|
|
50
|
+
} else {
|
|
51
|
+
const models = await response.json();
|
|
52
|
+
const modelIds = responsesModelIdsFromCopilotModels(models);
|
|
53
|
+
if (modelIds.length) {
|
|
54
|
+
const fallback = response.headers.get("X-CCDX-Model-Source") === "last-known-good";
|
|
55
|
+
return { modelIds, source: fallback ? "running adapter cache" : "running adapter" };
|
|
56
|
+
}
|
|
57
|
+
liveError = "adapter returned no selectable Responses models";
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
liveError = ctrl.signal.aborted
|
|
61
|
+
? `adapter model lookup timed out after ${modelFetchTimeout(env)}ms`
|
|
62
|
+
: error?.message || String(error);
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cached = loadModelCacheFn({ home });
|
|
68
|
+
const modelIds = responsesModelIdsFromCopilotModels(cached);
|
|
69
|
+
if (modelIds.length) return { modelIds, source: "local model cache" };
|
|
70
|
+
throw new Error(`No selectable Responses models are available (${liveError}). Start ccdx once to refresh the model list.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runAutoReviewModelCommand({
|
|
74
|
+
commandName = "ccdx",
|
|
75
|
+
env = process.env,
|
|
76
|
+
fetchImpl = fetch,
|
|
77
|
+
home = os.homedir(),
|
|
78
|
+
input = process.stdin,
|
|
79
|
+
loadCatalog = loadAutoReviewModelCatalog,
|
|
80
|
+
output = process.stdout,
|
|
81
|
+
prompt,
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
84
|
+
throw new Error(`${commandName} auto-review-model requires an interactive terminal`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
readUserSettings({ env, home, strict: true });
|
|
88
|
+
const current = autoReviewModelPreference({ env, home });
|
|
89
|
+
const catalog = await loadCatalog({ env, fetchImpl, home });
|
|
90
|
+
const modelIds = sortedModelIds(catalog.modelIds, current.model);
|
|
91
|
+
if (!modelIds.length) throw new Error("No selectable Responses models are available");
|
|
92
|
+
|
|
93
|
+
output.write(`${commandName} auto-review-model\n`);
|
|
94
|
+
output.write(`Current: ${current.model} (${current.source})\n`);
|
|
95
|
+
output.write(`Responses models from ${catalog.source}:\n`);
|
|
96
|
+
modelIds.forEach((model, index) => {
|
|
97
|
+
const markers = [];
|
|
98
|
+
if (model === current.model) markers.push("current");
|
|
99
|
+
if (model === DEFAULT_CODEX_AUTO_REVIEW_MODEL) markers.push("default");
|
|
100
|
+
output.write(` ${index + 1}. ${model}${markers.length ? ` [${markers.join(", ")}]` : ""}\n`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let defaultIndex = modelIds.indexOf(current.model);
|
|
104
|
+
if (defaultIndex < 0) defaultIndex = modelIds.indexOf(DEFAULT_CODEX_AUTO_REVIEW_MODEL);
|
|
105
|
+
if (defaultIndex < 0) defaultIndex = 0;
|
|
106
|
+
|
|
107
|
+
let readline;
|
|
108
|
+
const ask = prompt || (async (question) => {
|
|
109
|
+
readline ||= createInterface({ input, output });
|
|
110
|
+
return readline.question(question);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let selectedModel;
|
|
114
|
+
try {
|
|
115
|
+
while (!selectedModel) {
|
|
116
|
+
const answer = String(await ask(`Select a model [${defaultIndex + 1}], or q to cancel: `) || "").trim();
|
|
117
|
+
if (answer.toLowerCase() === "q" || answer.toLowerCase() === "quit") {
|
|
118
|
+
output.write("No changes made.\n");
|
|
119
|
+
return { changed: false, cancelled: true, model: current.model };
|
|
120
|
+
}
|
|
121
|
+
const selectedIndex = answer === "" ? defaultIndex : Number(answer) - 1;
|
|
122
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 0 && selectedIndex < modelIds.length) {
|
|
123
|
+
selectedModel = modelIds[selectedIndex];
|
|
124
|
+
} else {
|
|
125
|
+
output.write(`Enter a number from 1 to ${modelIds.length}, or q to cancel.\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
readline?.close();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const savedModel = selectedModel === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? "" : selectedModel;
|
|
133
|
+
const result = writeAutoReviewModel(savedModel, { env, home });
|
|
134
|
+
output.write(`${result.changed ? "Saved" : "Kept"} Auto Review model: ${selectedModel}\n`);
|
|
135
|
+
if (String(env.CCDX_AUTO_REVIEW_MODEL || "").trim()) {
|
|
136
|
+
output.write(`CCDX_AUTO_REVIEW_MODEL=${current.model} remains the effective override until it is unset.\n`);
|
|
137
|
+
} else {
|
|
138
|
+
output.write("The running 0.5.1+ adapter will use this model on the next Auto Review request.\n");
|
|
139
|
+
}
|
|
140
|
+
return { ...result, cancelled: false, model: selectedModel };
|
|
141
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -26,6 +26,25 @@ export function parseCliArgs(args = []) {
|
|
|
26
26
|
if (rest.length) unexpectedArgs(rest);
|
|
27
27
|
return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
28
28
|
}
|
|
29
|
+
if (command === "auto-review-model") {
|
|
30
|
+
if (rest.length) unexpectedArgs(rest);
|
|
31
|
+
return { command: "auto-review-model", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
32
|
+
}
|
|
33
|
+
if (command === "update") {
|
|
34
|
+
if (rest.length > 1) unexpectedArgs(rest.slice(1));
|
|
35
|
+
const source = rest[0];
|
|
36
|
+
if (source && !new Set(["npm", "github", "gh"]).has(source)) {
|
|
37
|
+
throw new Error(`Update source must be npm or github: ${source}`);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: "update",
|
|
41
|
+
configureClaudeDesktop: false,
|
|
42
|
+
showRequestId: false,
|
|
43
|
+
online: false,
|
|
44
|
+
compat: false,
|
|
45
|
+
updateSource: source === "gh" ? "github" : source,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
29
48
|
if (command === "doctor" || command === "--doctor") {
|
|
30
49
|
const supported = new Set(["--online", "--compat"]);
|
|
31
50
|
const invalid = rest.filter((arg) => !supported.has(arg));
|
|
@@ -107,6 +126,8 @@ export function cliHelp(commandName = "ccdx") {
|
|
|
107
126
|
${name} doctor [--online] [--compat]
|
|
108
127
|
${name} status
|
|
109
128
|
${name} usage
|
|
129
|
+
${name} auto-review-model
|
|
130
|
+
${name} update [npm|github]
|
|
110
131
|
${name} --version
|
|
111
132
|
${name} --help
|
|
112
133
|
|
package/src/models.mjs
CHANGED
|
@@ -158,6 +158,17 @@ export function gptModelIdsFromCopilotModels(models) {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
export function responsesModelIdsFromCopilotModels(models) {
|
|
162
|
+
return uniqueIds(copilotModelData(models), (model) => {
|
|
163
|
+
const id = String(model?.id || "").trim();
|
|
164
|
+
const endpoints = modelEndpoints(model);
|
|
165
|
+
return id !== CODEX_AUTO_REVIEW_MODEL
|
|
166
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/.test(id)
|
|
167
|
+
&& model?.model_picker_enabled !== false
|
|
168
|
+
&& (endpoints.includes("/responses") || endpoints.includes("/v1/responses"));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
export function claudeDesktopModelDefsFromCopilotModels(models) {
|
|
162
173
|
const data = copilotModelData(models);
|
|
163
174
|
|
|
@@ -212,21 +223,21 @@ export function resolveAnthropicModel(model, env = process.env, options = {}) {
|
|
|
212
223
|
};
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
export function resolveOpenAIModel(model, env = process.env) {
|
|
226
|
+
export function resolveOpenAIModel(model, env = process.env, options = {}) {
|
|
216
227
|
const requestedModel = String(model || "");
|
|
217
228
|
if (requestedModel !== CODEX_AUTO_REVIEW_MODEL) {
|
|
218
229
|
return { requestedModel, upstreamModel: requestedModel };
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
const configuredModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
|
|
232
|
+
const configuredModel = String(env.CCDX_AUTO_REVIEW_MODEL || options.autoReviewModel || "").trim();
|
|
222
233
|
return {
|
|
223
234
|
requestedModel,
|
|
224
235
|
upstreamModel: configuredModel || DEFAULT_CODEX_AUTO_REVIEW_MODEL,
|
|
225
236
|
};
|
|
226
237
|
}
|
|
227
238
|
|
|
228
|
-
export function codexAutoReviewModelStatus(models, env = process.env) {
|
|
229
|
-
const { upstreamModel } = resolveOpenAIModel(CODEX_AUTO_REVIEW_MODEL, env);
|
|
239
|
+
export function codexAutoReviewModelStatus(models, env = process.env, options = {}) {
|
|
240
|
+
const { upstreamModel } = resolveOpenAIModel(CODEX_AUTO_REVIEW_MODEL, env, options);
|
|
230
241
|
const model = copilotModelData(models).find((entry) => String(entry?.id || "").trim() === upstreamModel);
|
|
231
242
|
if (!model) return { available: false, upstreamModel, reason: "model is not advertised" };
|
|
232
243
|
const endpoints = modelEndpoints(model);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = "codex-copilot-dx";
|
|
5
|
+
const UPDATE_SPECS = {
|
|
6
|
+
npm: `${PACKAGE_NAME}@latest`,
|
|
7
|
+
github: "github:DaleXiao/codex-copilot-dx#main",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function normalizeUpdateSource(value) {
|
|
11
|
+
const source = String(value || "").trim().toLowerCase();
|
|
12
|
+
if (source === "gh") return "github";
|
|
13
|
+
return Object.hasOwn(UPDATE_SPECS, source) ? source : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function globalUpdateCommand(source, { platform = process.platform } = {}) {
|
|
17
|
+
const normalized = normalizeUpdateSource(source);
|
|
18
|
+
if (!normalized) throw new Error(`Update source must be npm or github: ${source}`);
|
|
19
|
+
const args = ["install", "--global"];
|
|
20
|
+
if (normalized === "github") args.push("--allow-git=all");
|
|
21
|
+
args.push(UPDATE_SPECS[normalized]);
|
|
22
|
+
return {
|
|
23
|
+
command: platform === "win32" ? "npm.cmd" : "npm",
|
|
24
|
+
args,
|
|
25
|
+
source: normalized,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function waitForExit(child) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
let settled = false;
|
|
32
|
+
const finish = (handler, value) => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
handler(value);
|
|
36
|
+
};
|
|
37
|
+
child.once("error", (error) => finish(reject, error));
|
|
38
|
+
child.once("exit", (code, signal) => {
|
|
39
|
+
if (signal) finish(reject, new Error(`npm update was interrupted by ${signal}`));
|
|
40
|
+
else if (code !== 0) finish(reject, new Error(`npm update exited with status ${code}`));
|
|
41
|
+
else finish(resolve);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runPackageUpdateCommand({
|
|
47
|
+
commandName = "ccdx",
|
|
48
|
+
env = process.env,
|
|
49
|
+
input = process.stdin,
|
|
50
|
+
output = process.stdout,
|
|
51
|
+
platform = process.platform,
|
|
52
|
+
prompt,
|
|
53
|
+
source,
|
|
54
|
+
spawnImpl = spawn,
|
|
55
|
+
} = {}) {
|
|
56
|
+
let selectedSource = normalizeUpdateSource(source);
|
|
57
|
+
if (source && !selectedSource) throw new Error(`Update source must be npm or github: ${source}`);
|
|
58
|
+
|
|
59
|
+
if (!selectedSource) {
|
|
60
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
61
|
+
throw new Error(`Choose a source with ${commandName} update npm or ${commandName} update github`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
output.write(`${commandName} update\n`);
|
|
65
|
+
output.write(" 1. npm registry [default]\n");
|
|
66
|
+
output.write(" 2. GitHub main\n");
|
|
67
|
+
|
|
68
|
+
let readline;
|
|
69
|
+
const ask = prompt || (async (question) => {
|
|
70
|
+
readline ||= createInterface({ input, output });
|
|
71
|
+
return readline.question(question);
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
while (!selectedSource) {
|
|
75
|
+
const answer = String(await ask("Select update source [1], or q to cancel: ") || "").trim().toLowerCase();
|
|
76
|
+
if (answer === "q" || answer === "quit") {
|
|
77
|
+
output.write("No changes made.\n");
|
|
78
|
+
return { cancelled: true, source: "" };
|
|
79
|
+
}
|
|
80
|
+
if (answer === "" || answer === "1" || answer === "npm") selectedSource = "npm";
|
|
81
|
+
else if (answer === "2" || answer === "github" || answer === "gh") selectedSource = "github";
|
|
82
|
+
else output.write("Enter 1 for npm, 2 for GitHub, or q to cancel.\n");
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
readline?.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const update = globalUpdateCommand(selectedSource, { platform });
|
|
90
|
+
const sourceLabel = selectedSource === "npm" ? "npm registry" : "GitHub main";
|
|
91
|
+
output.write(`Updating ${PACKAGE_NAME} from ${sourceLabel}...\n`);
|
|
92
|
+
const child = spawnImpl(update.command, update.args, {
|
|
93
|
+
env,
|
|
94
|
+
shell: false,
|
|
95
|
+
stdio: "inherit",
|
|
96
|
+
});
|
|
97
|
+
await waitForExit(child);
|
|
98
|
+
output.write(`Updated ${PACKAGE_NAME} from ${sourceLabel}. Restart the running adapter to use the new version.\n`);
|
|
99
|
+
return { cancelled: false, source: selectedSource };
|
|
100
|
+
}
|
|
@@ -66,9 +66,17 @@ function unsupportedCustomToolsError(model) {
|
|
|
66
66
|
return error;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver) {
|
|
70
|
+
const options = model === CODEX_AUTO_REVIEW_MODEL && typeof autoReviewModelResolver === "function"
|
|
71
|
+
? { autoReviewModel: autoReviewModelResolver() }
|
|
72
|
+
: {};
|
|
73
|
+
return resolveOpenAIModel(model, openAIModelEnv, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export function createResponsesHandler(options) {
|
|
70
77
|
const {
|
|
71
78
|
acquireRequest,
|
|
79
|
+
autoReviewModelResolver,
|
|
72
80
|
chatCompletionsFn,
|
|
73
81
|
openAIModelEnv,
|
|
74
82
|
responsesPayloadOptions,
|
|
@@ -91,7 +99,7 @@ export function createResponsesHandler(options) {
|
|
|
91
99
|
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
92
100
|
prepared.surface = "responses";
|
|
93
101
|
const model = parsed.model || "unknown";
|
|
94
|
-
const { requestedModel, upstreamModel } =
|
|
102
|
+
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
95
103
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
96
104
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
97
105
|
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
@@ -186,6 +194,7 @@ export function createResponsesHandler(options) {
|
|
|
186
194
|
export function createResponsesCompactHandler(options) {
|
|
187
195
|
const {
|
|
188
196
|
acquireRequest,
|
|
197
|
+
autoReviewModelResolver,
|
|
189
198
|
openAIModelEnv,
|
|
190
199
|
responsesCompactFn,
|
|
191
200
|
upstreamTimeoutMs,
|
|
@@ -203,7 +212,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
203
212
|
);
|
|
204
213
|
prepared.surface = "responses_compact";
|
|
205
214
|
const model = parsed.model || "unknown";
|
|
206
|
-
const { requestedModel, upstreamModel } =
|
|
215
|
+
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
207
216
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
208
217
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
209
218
|
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
|
+
import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
|
+
|
|
7
|
+
const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
|
|
8
|
+
|
|
9
|
+
export function userSettingsPath({ env = process.env, home = os.homedir() } = {}) {
|
|
10
|
+
const xdgConfigHome = String(env.XDG_CONFIG_HOME || "").trim();
|
|
11
|
+
const configRoot = xdgConfigHome || path.join(home, ".config");
|
|
12
|
+
return path.join(configRoot, "codex-copilot-dx", "config.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function invalidSettings(filePath, reason) {
|
|
16
|
+
return new Error(`Invalid ccdx settings at ${filePath}: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readUserSettings({ env = process.env, home = os.homedir(), strict = false } = {}) {
|
|
20
|
+
const filePath = userSettingsPath({ env, home });
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
24
|
+
throw invalidSettings(filePath, "expected a JSON object");
|
|
25
|
+
}
|
|
26
|
+
if (Object.hasOwn(parsed, AUTO_REVIEW_MODEL_KEY)) {
|
|
27
|
+
const value = parsed[AUTO_REVIEW_MODEL_KEY];
|
|
28
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
29
|
+
throw invalidSettings(filePath, `${AUTO_REVIEW_MODEL_KEY} must be a non-empty string`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error?.code === "ENOENT") return {};
|
|
35
|
+
if (strict) {
|
|
36
|
+
if (String(error?.message || "").startsWith("Invalid ccdx settings")) throw error;
|
|
37
|
+
throw invalidSettings(filePath, error?.message || String(error));
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function savedAutoReviewModel(options = {}) {
|
|
44
|
+
const settings = readUserSettings(options);
|
|
45
|
+
return String(settings[AUTO_REVIEW_MODEL_KEY] || "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function autoReviewModelPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
49
|
+
const environmentModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
|
|
50
|
+
if (environmentModel) return { model: environmentModel, source: "environment" };
|
|
51
|
+
|
|
52
|
+
const savedModel = savedAutoReviewModel({ env, home });
|
|
53
|
+
if (savedModel) return { model: savedModel, source: "settings" };
|
|
54
|
+
return { model: DEFAULT_CODEX_AUTO_REVIEW_MODEL, source: "default" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir() } = {}) {
|
|
58
|
+
const filePath = userSettingsPath({ env, home });
|
|
59
|
+
const settings = readUserSettings({ env, home, strict: true });
|
|
60
|
+
const value = String(model || "").trim();
|
|
61
|
+
const next = { ...settings };
|
|
62
|
+
if (value) next[AUTO_REVIEW_MODEL_KEY] = value;
|
|
63
|
+
else delete next[AUTO_REVIEW_MODEL_KEY];
|
|
64
|
+
|
|
65
|
+
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
66
|
+
return { changed, filePath, model: value || DEFAULT_CODEX_AUTO_REVIEW_MODEL };
|
|
67
|
+
}
|