codex-copilot-dx 0.6.12 → 0.6.14
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 +31 -41
- package/package.json +1 -1
- package/scripts/benchmark.mjs +74 -0
- package/src/anthropic.mjs +22 -2
- package/src/config.mjs +38 -3
- package/src/doctor.mjs +3 -4
- package/src/pm-studio-asar.mjs +136 -22
- package/src/pm-studio-setup.mjs +103 -301
- package/src/pm-studio-status.mjs +15 -22
- package/src/responses-bridge.mjs +44 -7
- package/src/responses-compaction.mjs +1 -50
- package/src/responses-proxy.mjs +116 -35
- package/src/responses-request.mjs +2 -3
- package/src/stream.mjs +27 -13
- package/src/pm-studio-provenance.mjs +0 -701
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/
|
|
|
7
7
|
A single in-process adapter (port `2026`) exposes both APIs over one Copilot
|
|
8
8
|
account by default, or over isolated Codex and Claude accounts when configured:
|
|
9
9
|
|
|
10
|
-
- **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, chat models convert
|
|
10
|
+
- **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, while chat models convert the text, image, and function-tool subset and explicitly reject incompatible top-level items, tools, and tool choices.
|
|
11
11
|
- **Claude Code** -> Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`), translated to/from Chat Completions.
|
|
12
12
|
- **Claude App** -> optional Claude Desktop App gateway profile using the same local Messages API plus local model discovery for the configured gateway key.
|
|
13
13
|
- **PM Studio** -> optional macOS app patch that keeps the active PM profile for GPT models while routing enabled Claude models through an isolated CCDX Claude account.
|
|
@@ -118,15 +118,15 @@ discovery tries the compatible local CCDX merge first, and only exact enabled
|
|
|
118
118
|
Anthropic model IDs use the isolated CCDX Claude profile. Neither credential is
|
|
119
119
|
copied into the other application.
|
|
120
120
|
|
|
121
|
-
CCDX
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
ambiguous module, changed request transport, unsupported
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
CCDX treats the local PM Studio App selected for setup as the source of truth.
|
|
122
|
+
Its version and build label backup and recovery records; neither they nor the
|
|
123
|
+
installation source or original signature are compatibility admission gates.
|
|
124
|
+
Setup derives a source-bound recipe whenever the local ASAR integrity metadata
|
|
125
|
+
and exactly one compatible Copilot configuration module can be verified. A
|
|
126
|
+
missing or ambiguous module, changed request transport, or unsupported
|
|
127
|
+
integrity policy is rejected without modifying the App. Authorize the isolated
|
|
128
|
+
Claude profile first, quit PM Studio and its updater, then run the one-time
|
|
129
|
+
setup command:
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
@@ -142,29 +142,20 @@ ccdx pms status
|
|
|
142
142
|
|
|
143
143
|
`ccdx pm-studio status` and `ccdx pm-studio setup` are equivalent long-form aliases.
|
|
144
144
|
|
|
145
|
-
`ccdx pms setup` performs preflight,
|
|
146
|
-
integrity, signing, and replacement work. It
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
(apart from strictly scoped locale pruning) before any backup or staging write.
|
|
154
|
-
Network, hash, tree, structure, partial-patch, running-process, space, and every
|
|
155
|
-
other pre-replacement failure leave the installed App unchanged. If final
|
|
145
|
+
`ccdx pms setup` performs local preflight, structural verification, backup,
|
|
146
|
+
staging, integrity, signing, and replacement work. It does not access a PM
|
|
147
|
+
Studio repository, release asset, PAT, or source-signing service. It never
|
|
148
|
+
starts Device Flow, the adapter, Codex, or PM Studio, and it does not modify PM
|
|
149
|
+
profiles, cached model lists, or saved PM tokens. Local fingerprints bind the
|
|
150
|
+
recipe, backup, staged App, and installed App without deciding whether the
|
|
151
|
+
original package is trusted. Structure, partial-patch, running-process, space,
|
|
152
|
+
and every other pre-replacement failure leave the installed App unchanged. If final
|
|
156
153
|
read-only verification after an atomic replacement detects exceptional
|
|
157
154
|
filesystem drift, setup does not attempt a second automatic write: it preserves
|
|
158
155
|
the verified backup, retains any diagnostic stage still present, and prints
|
|
159
156
|
recovery paths. Repeating setup reuses the version/source-bound verified backup
|
|
160
|
-
and is idempotent
|
|
161
|
-
|
|
162
|
-
Some official PM Studio artifacts can fail macOS's vendor-signature verifier
|
|
163
|
-
even though their signing metadata remains readable. CCDX does not generally
|
|
164
|
-
ignore that failure: exact built-in recipes require their frozen official
|
|
165
|
-
fingerprint, while compatible unknown versions require the verified official
|
|
166
|
-
GitHub asset and tree comparison described above. The staged and installed
|
|
167
|
-
CCDX patch must still pass strict ad-hoc signature verification and match the
|
|
157
|
+
and is idempotent. The staged and installed CCDX patch must still pass strict
|
|
158
|
+
ad-hoc signature verification, use the App's bundle identifier, and match the
|
|
168
159
|
complete patched-tree record in the backup manifest.
|
|
169
160
|
|
|
170
161
|
The patch uses split origins, not an App-wide proxy. GPT and other non-Claude
|
|
@@ -183,9 +174,9 @@ CCDX uses the PM bearer only to fetch or validate official model access; it is
|
|
|
183
174
|
never used as the credential for the isolated Claude upstream request. Keep
|
|
184
175
|
port 2026 local to the machine.
|
|
185
176
|
|
|
186
|
-
Patching
|
|
177
|
+
Patching applies an ad-hoc signature to the staged App. PM Studio's
|
|
187
178
|
updater is not disabled; an official update may overwrite the patch or may
|
|
188
|
-
require reinstalling
|
|
179
|
+
require reinstalling PM Studio. Setup prints the verified backup and
|
|
189
180
|
manifest paths plus the version-matched restore procedure. Never restore a
|
|
190
181
|
backup over a different PM Studio version or build.
|
|
191
182
|
|
|
@@ -202,7 +193,7 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
|
202
193
|
```
|
|
203
194
|
|
|
204
195
|
Only when all four values match each other and the version/build recorded in the
|
|
205
|
-
source-bound manifest, move the patched App aside, restore the complete
|
|
196
|
+
source-bound manifest, move the patched App aside, restore the complete local
|
|
206
197
|
bundle, and verify it before launch:
|
|
207
198
|
|
|
208
199
|
```bash
|
|
@@ -214,12 +205,11 @@ mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
|
214
205
|
ccdx pms status
|
|
215
206
|
```
|
|
216
207
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
or both verification paths fail,
|
|
208
|
+
The App-patch row from `ccdx pms status` must classify the restored bundle as
|
|
209
|
+
the recorded clean local content rather than integrity drift. If the version
|
|
210
|
+
check, copy, or either verification path fails,
|
|
221
211
|
do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
|
|
222
|
-
and reinstall
|
|
212
|
+
and reinstall PM Studio instead of applying a version-mismatched
|
|
223
213
|
backup. CCDX never runs these restore commands or `sudo` automatically.
|
|
224
214
|
|
|
225
215
|
To change the model used by Codex Auto-review, run:
|
|
@@ -311,7 +301,7 @@ To actively verify the protocol path through an already-running adapter, run:
|
|
|
311
301
|
ccdx doctor --compat
|
|
312
302
|
```
|
|
313
303
|
|
|
314
|
-
The compatibility doctor sends a few minimal Copilot requests to check Codex Auto-review, native Responses, streaming history, compaction,
|
|
304
|
+
The compatibility doctor sends a few minimal Copilot requests to check Codex Auto-review, native Responses, streaming history, compaction, and Anthropic streaming. It consumes a small amount of Copilot usage, never starts the adapter or device flow, and does not change client configuration. Combine it with `--online` when both the saved-token entitlement check and the adapter protocol checks are needed.
|
|
315
305
|
|
|
316
306
|
For a read-only summary of the running adapter, use:
|
|
317
307
|
|
|
@@ -448,9 +438,9 @@ Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchang
|
|
|
448
438
|
|
|
449
439
|
When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
|
|
450
440
|
|
|
451
|
-
For `/v1/responses/compact`, the adapter sends one terminal `compaction_trigger` in unary mode, validates that Copilot returned completed compaction state, and
|
|
441
|
+
For `/v1/responses/compact`, the adapter sends one terminal `compaction_trigger` in unary mode, validates that Copilot returned completed compaction state, and preserves the complete upstream compacted window as the canonical next context. Only a validated window becomes a new local history root. Later turns continue from it without re-expanding pre-compaction history; existing older response branches remain available until normal history eviction.
|
|
452
442
|
|
|
453
|
-
Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Inference responses forward safe quota, retry, model, trace, and upstream request-ID metadata; cookies, authorization, body-length, and encoding headers are not forwarded.
|
|
443
|
+
Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event; bytes after that terminal are not forwarded. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Inference responses forward safe quota, retry, model, trace, and upstream request-ID metadata; cookies, authorization, body-length, and encoding headers are not forwarded.
|
|
454
444
|
|
|
455
445
|
Newer ChatGPT/Codex clients can advertise an `image_gen` namespace that already exists upstream. The adapter removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image inputs and screenshot optimization remain enabled.
|
|
456
446
|
|
|
@@ -466,4 +456,4 @@ npm run verify
|
|
|
466
456
|
npm run bench:payload
|
|
467
457
|
```
|
|
468
458
|
|
|
469
|
-
`npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run bench:check` enforces relative token-counting performance and request-admission resource limits. `npm run pack:check` verifies the npm tarball contents without publishing. `npm run bench:payload` remains a report-only, isolated-process benchmark for 5-60 MiB image payloads and does not contact Copilot. The CI workflow runs the verification checks on supported Node.js release lines.
|
|
459
|
+
`npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run bench:check` enforces relative token-counting performance, linear SSE scanning, and request-admission resource limits. `npm run pack:check` verifies the npm tarball contents without publishing. `npm run bench:payload` remains a report-only, isolated-process benchmark for 5-60 MiB image payloads and does not contact Copilot. The CI workflow runs the verification checks on supported Node.js release lines.
|
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -31,6 +31,65 @@ function median(values) {
|
|
|
31
31
|
: ordered[middle];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function sseLinearityProbe() {
|
|
35
|
+
return runProbe(`
|
|
36
|
+
import { webStreamLines } from "./src/stream.mjs";
|
|
37
|
+
|
|
38
|
+
const chunkBytes = 16 * 1024;
|
|
39
|
+
const maxBufferBytes = 8 * 1024 * 1024;
|
|
40
|
+
const chunk = new TextEncoder().encode("x".repeat(chunkBytes));
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const inputMiB of [1, 2, 4, 8]) {
|
|
43
|
+
const inputBytes = inputMiB * 1024 * 1024;
|
|
44
|
+
let remaining = inputBytes;
|
|
45
|
+
const response = {
|
|
46
|
+
body: new ReadableStream({
|
|
47
|
+
pull(controller) {
|
|
48
|
+
if (remaining === 0) {
|
|
49
|
+
controller.close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const nextBytes = Math.min(chunk.byteLength, remaining);
|
|
53
|
+
controller.enqueue(nextBytes === chunk.byteLength ? chunk : chunk.subarray(0, nextBytes));
|
|
54
|
+
remaining -= nextBytes;
|
|
55
|
+
},
|
|
56
|
+
}, { highWaterMark: 0 }),
|
|
57
|
+
};
|
|
58
|
+
const originalByteLength = Buffer.byteLength;
|
|
59
|
+
let byteLengthCalls = 0;
|
|
60
|
+
let scannedBytes = 0;
|
|
61
|
+
Buffer.byteLength = function trackedByteLength(value, ...args) {
|
|
62
|
+
const bytes = originalByteLength(value, ...args);
|
|
63
|
+
byteLengthCalls += 1;
|
|
64
|
+
scannedBytes += bytes;
|
|
65
|
+
return bytes;
|
|
66
|
+
};
|
|
67
|
+
let lines = 0;
|
|
68
|
+
let outputBytes = 0;
|
|
69
|
+
const started = performance.now();
|
|
70
|
+
try {
|
|
71
|
+
for await (const line of webStreamLines(response, { maxBufferBytes })) {
|
|
72
|
+
lines += 1;
|
|
73
|
+
outputBytes += originalByteLength(line);
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
Buffer.byteLength = originalByteLength;
|
|
77
|
+
}
|
|
78
|
+
results.push({
|
|
79
|
+
input_mib: inputMiB,
|
|
80
|
+
chunks: Math.ceil(inputBytes / chunkBytes),
|
|
81
|
+
lines,
|
|
82
|
+
output_bytes: outputBytes,
|
|
83
|
+
byte_length_calls: byteLengthCalls,
|
|
84
|
+
scanned_bytes: scannedBytes,
|
|
85
|
+
scan_ratio: +(scannedBytes / inputBytes).toFixed(3),
|
|
86
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(JSON.stringify({ chunk_kib: chunkBytes / 1024, max_buffer_mib: 8, results }));
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
|
|
34
93
|
function deterministicPixels(byteLength, initialSeed) {
|
|
35
94
|
const pixels = Buffer.alloc(byteLength);
|
|
36
95
|
let seed = initialSeed;
|
|
@@ -422,6 +481,7 @@ const report = {
|
|
|
422
481
|
};
|
|
423
482
|
|
|
424
483
|
if (checkMode) {
|
|
484
|
+
report.sse_linearity = sseLinearityProbe();
|
|
425
485
|
report.large_token_count = {
|
|
426
486
|
proxy: largeTokenCountProbe("proxy"),
|
|
427
487
|
materialized_array: largeTokenCountProbe("array"),
|
|
@@ -466,6 +526,20 @@ if (checkMode) {
|
|
|
466
526
|
if (report.tool_output_parse_cache.parse_calls !== 1) {
|
|
467
527
|
failures.push(`stringified tool output parsed ${report.tool_output_parse_cache.parse_calls} times instead of once`);
|
|
468
528
|
}
|
|
529
|
+
const expectedSseSizes = [1, 2, 4, 8];
|
|
530
|
+
if (report.sse_linearity.results.length !== expectedSseSizes.length
|
|
531
|
+
|| report.sse_linearity.results.some((sample, index) => sample.input_mib !== expectedSseSizes[index])) {
|
|
532
|
+
failures.push("SSE linearity probe did not cover 1/2/4/8 MiB inputs");
|
|
533
|
+
}
|
|
534
|
+
for (const sample of report.sse_linearity.results) {
|
|
535
|
+
const inputBytes = sample.input_mib * 1024 * 1024;
|
|
536
|
+
if (sample.lines !== 1 || sample.output_bytes !== inputBytes) {
|
|
537
|
+
failures.push(`SSE parser changed the ${sample.input_mib}MiB fragmented line`);
|
|
538
|
+
}
|
|
539
|
+
if (sample.scanned_bytes > inputBytes * 1.01 || sample.byte_length_calls > sample.chunks + 1) {
|
|
540
|
+
failures.push(`SSE parser scan complexity is not linear at ${sample.input_mib}MiB`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
469
543
|
for (const payload of report.large_payload_peak) {
|
|
470
544
|
if (payload.admission_max_active_mib > payload.admission_budget_mib) {
|
|
471
545
|
failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
|
package/src/anthropic.mjs
CHANGED
|
@@ -130,6 +130,14 @@ export function anthropicToChat(body, options = {}) {
|
|
|
130
130
|
|
|
131
131
|
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
132
132
|
|
|
133
|
+
function invalidToolArguments(toolName) {
|
|
134
|
+
const name = String(toolName || "unknown").slice(0, 100);
|
|
135
|
+
return invalidUpstreamStream(
|
|
136
|
+
`Upstream Chat Completions tool "${name}" did not return valid JSON object arguments`,
|
|
137
|
+
"upstream_tool_arguments_json_invalid",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
export function chatToAnthropic(openaiResp, model, options = {}) {
|
|
134
142
|
const choice = openaiResp.choices?.[0];
|
|
135
143
|
const msg = choice?.message || {};
|
|
@@ -138,8 +146,20 @@ export function chatToAnthropic(openaiResp, model, options = {}) {
|
|
|
138
146
|
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
139
147
|
if (Array.isArray(msg.tool_calls)) {
|
|
140
148
|
for (const tc of msg.tool_calls) {
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
const rawArguments = tc.function?.arguments;
|
|
150
|
+
let input;
|
|
151
|
+
if (rawArguments === undefined || rawArguments === null || rawArguments === "") {
|
|
152
|
+
input = {};
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
input = JSON.parse(rawArguments);
|
|
156
|
+
} catch {
|
|
157
|
+
throw invalidToolArguments(tc.function?.name);
|
|
158
|
+
}
|
|
159
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
160
|
+
throw invalidToolArguments(tc.function?.name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
143
163
|
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
|
|
144
164
|
}
|
|
145
165
|
}
|
package/src/config.mjs
CHANGED
|
@@ -9,8 +9,39 @@ const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
|
9
9
|
const MODEL_CONTEXT_WINDOW = 1_000_000;
|
|
10
10
|
const MODEL_AUTO_COMPACT_TOKEN_LIMIT = 900_000;
|
|
11
11
|
|
|
12
|
+
function isTomlTableHeader(line) {
|
|
13
|
+
const source = line.trimStart();
|
|
14
|
+
const openingBrackets = source.startsWith("[[") ? 2 : source.startsWith("[") ? 1 : 0;
|
|
15
|
+
if (!openingBrackets) return false;
|
|
16
|
+
|
|
17
|
+
let quote = "";
|
|
18
|
+
let escaped = false;
|
|
19
|
+
for (let index = openingBrackets; index < source.length; index += 1) {
|
|
20
|
+
const char = source[index];
|
|
21
|
+
if (quote) {
|
|
22
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
23
|
+
escaped = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (char === quote && !escaped) quote = "";
|
|
27
|
+
escaped = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === '"' || char === "'") {
|
|
31
|
+
quote = char;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (char !== "]" || (openingBrackets === 2 && source[index + 1] !== "]")) continue;
|
|
35
|
+
|
|
36
|
+
if (!source.slice(openingBrackets, index).trim()) return false;
|
|
37
|
+
const tail = source.slice(index + openingBrackets).trim();
|
|
38
|
+
return tail === "" || tail.startsWith("#");
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
function setTopLevelTomlDefault(lines, key, value) {
|
|
13
|
-
let end = lines.findIndex(
|
|
44
|
+
let end = lines.findIndex(isTomlTableHeader);
|
|
14
45
|
if (end === -1) end = lines.length;
|
|
15
46
|
|
|
16
47
|
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
@@ -28,7 +59,7 @@ function setTomlKey(lines, sectionName, key, value) {
|
|
|
28
59
|
|
|
29
60
|
let end = lines.length;
|
|
30
61
|
for (let i = start + 1; i < lines.length; i++) {
|
|
31
|
-
if (
|
|
62
|
+
if (isTomlTableHeader(lines[i])) {
|
|
32
63
|
end = i;
|
|
33
64
|
break;
|
|
34
65
|
}
|
|
@@ -57,7 +88,11 @@ export function computeUpdatedCodexConfig(content, adapterPort = 2026, adapterHo
|
|
|
57
88
|
|
|
58
89
|
let changed = false;
|
|
59
90
|
const openaiLine = `openai_base_url = "${baseUrl}"`;
|
|
60
|
-
const
|
|
91
|
+
const firstSection = lines.findIndex(isTomlTableHeader);
|
|
92
|
+
const topLevelEnd = firstSection === -1 ? lines.length : firstSection;
|
|
93
|
+
const openaiIndex = lines.findIndex((line, index) => (
|
|
94
|
+
index < topLevelEnd && /^\s*openai_base_url\s*=/.test(line)
|
|
95
|
+
));
|
|
61
96
|
if (openaiIndex === -1) {
|
|
62
97
|
lines.unshift(openaiLine);
|
|
63
98
|
changed = true;
|
package/src/doctor.mjs
CHANGED
|
@@ -181,13 +181,12 @@ export async function inspectAdapterCompatibility({
|
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
if (firstResponse) {
|
|
184
|
-
await runCompatibilityCheck(checks, "Responses stream
|
|
184
|
+
await runCompatibilityCheck(checks, "Responses stream and history compatibility", async () => {
|
|
185
185
|
const text = await compatibilityRequest(fetchImpl, `${baseUrl}/v1/responses`, {
|
|
186
186
|
model: responsesModel,
|
|
187
187
|
stream: true,
|
|
188
188
|
previous_response_id: firstResponse.id,
|
|
189
189
|
input: "Reply with OK again.",
|
|
190
|
-
tools: [{ type: "image_generation" }],
|
|
191
190
|
}, timeoutMs);
|
|
192
191
|
if (!/^event:\s*response\.completed\s*$/m.test(text)) throw new Error("stream did not contain response.completed");
|
|
193
192
|
});
|
|
@@ -599,11 +598,11 @@ export async function collectDoctorChecks({
|
|
|
599
598
|
} else if (pm.app.state === "clean") {
|
|
600
599
|
checks.push({
|
|
601
600
|
kind: "warn",
|
|
602
|
-
message: `PM Studio ${version}
|
|
601
|
+
message: `PM Studio ${version} has a compatible local patch structure but is not patched`,
|
|
603
602
|
fix: "ccdx pms setup",
|
|
604
603
|
});
|
|
605
604
|
} else if (pm.app.state === "unsupported") {
|
|
606
|
-
checks.push({ kind: "warn", message: `PM Studio ${version}
|
|
605
|
+
checks.push({ kind: "warn", message: `PM Studio ${version} does not expose one uniquely compatible patch structure; no files will be changed` });
|
|
607
606
|
} else if (pm.app.state === "drift" || pm.app.state === "error") {
|
|
608
607
|
checks.push({ kind: "err", message: `PM Studio integrity check failed: ${pm.app.issues.join("; ")}` });
|
|
609
608
|
}
|
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -8,17 +8,42 @@ export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2
|
|
|
8
8
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
|
|
9
9
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
|
|
10
10
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_ANCHOR = "47024(I,e,t){";
|
|
11
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL = `API_ENDPOINT:"${PM_STUDIO_ORIGIN}"`;
|
|
11
12
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_PATH = "dist/main/main.js";
|
|
12
13
|
const PM_STUDIO_COMPATIBILITY_ID = "exact-copilot-config-module-v1";
|
|
13
14
|
const PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES = 128 * 1024 * 1024;
|
|
14
15
|
const PM_STUDIO_COMPATIBLE_BLOCK_MAX_COUNT = 16_384;
|
|
16
|
+
const PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES = 512;
|
|
17
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE = '47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"https://api.githubcopilot.com",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>`req_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}};class c{constructor(){this.config={...l}}static getInstance(){return c.instance||(c.instance=new c),c.instance}getConfig(){return{...this.config}}updateConfig(I){this.config={...this.config,...I}}resetConfig(){this.config={...l}}validateConfig(){return function(){const I=[];return l.CLIENT_ID||I.push("GitHub Copilot Client ID is required"),l.API_ENDPOINT||I.push("GitHub Copilot API endpoint is required"),{valid:0===I.length,errors:I}}()}getStandardHeaders(I){return function(I){const e={Accept:l.STANDARD_HEADERS.Accept,"Content-Type":l.STANDARD_HEADERS["Content-Type"],"User-Agent":l.STANDARD_HEADERS["User-Agent"],"Editor-Version":l.STANDARD_HEADERS["Editor-Version"],"Editor-Plugin-Version":l.STANDARD_HEADERS["Editor-Plugin-Version"],"Copilot-Integration-Id":l.STANDARD_HEADERS["Copilot-Integration-Id"],"X-Request-Id":"function"==typeof l.STANDARD_HEADERS["X-Request-Id"]?l.STANDARD_HEADERS["X-Request-Id"]():l.STANDARD_HEADERS["X-Request-Id"]};return I&&(e.Authorization=`Bearer ${I}`),e}(I)}getCopilotTokenHeaders(I){return function(I){return{Authorization:`Bearer ${I}`,Accept:"application/json","User-Agent":l.USER_AGENT,"Editor-Version":l.EDITOR_VERSION,"Editor-Plugin-Version":l.EDITOR_PLUGIN_VERSION}}(I)}getDeviceCodeHeaders(){return{Accept:"application/json","User-Agent":l.USER_AGENT}}}c.getInstance()}';
|
|
18
|
+
|
|
19
|
+
function copilotConfigAlphaTemplate() {
|
|
20
|
+
// This is a reviewed byte template, not a generic JavaScript normalizer:
|
|
21
|
+
// only the five minifier bindings vary; every other byte remains exact.
|
|
22
|
+
const parts = [];
|
|
23
|
+
const moduleIdEnd = PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.indexOf("(");
|
|
24
|
+
const pattern = /(?<![$\w])(?:I|e|t|l|c)(?![$\w])/g;
|
|
25
|
+
pattern.lastIndex = moduleIdEnd;
|
|
26
|
+
let offset = moduleIdEnd;
|
|
27
|
+
for (let match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE); match; match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE)) {
|
|
28
|
+
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset, match.index));
|
|
29
|
+
parts.push(Object.freeze({ identifier: match[0] }));
|
|
30
|
+
offset = match.index + match[0].length;
|
|
31
|
+
}
|
|
32
|
+
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset));
|
|
33
|
+
return Object.freeze(parts);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE = copilotConfigAlphaTemplate();
|
|
15
37
|
|
|
16
|
-
function splitOriginConfigModule(
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
function splitOriginConfigModule(
|
|
39
|
+
moduleId = "47024",
|
|
40
|
+
windowLength = PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
41
|
+
) {
|
|
42
|
+
const source = `${moduleId}(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,e);if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
|
|
43
|
+
if (source.length > windowLength) {
|
|
19
44
|
throw new Error("PM Studio split-origin module exceeds its exact source window");
|
|
20
45
|
}
|
|
21
|
-
return `${source.slice(0, -1)}${" ".repeat(
|
|
46
|
+
return `${source.slice(0, -1)}${" ".repeat(windowLength - source.length)}}`;
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
|
|
@@ -320,6 +345,93 @@ function occurrencePositions(haystack, needle) {
|
|
|
320
345
|
return positions;
|
|
321
346
|
}
|
|
322
347
|
|
|
348
|
+
function asciiDigit(byte) {
|
|
349
|
+
return byte >= 48 && byte <= 57;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function asciiIdentifierStart(byte) {
|
|
353
|
+
return byte === 36
|
|
354
|
+
|| byte === 95
|
|
355
|
+
|| (byte >= 65 && byte <= 90)
|
|
356
|
+
|| (byte >= 97 && byte <= 122);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function matchCopilotConfigAlphaTemplate(source, offset) {
|
|
360
|
+
let moduleIdEnd = offset;
|
|
361
|
+
while (moduleIdEnd < source.length && asciiDigit(source[moduleIdEnd])) moduleIdEnd += 1;
|
|
362
|
+
const moduleIdLength = moduleIdEnd - offset;
|
|
363
|
+
if (moduleIdLength === 0
|
|
364
|
+
|| moduleIdLength > 10
|
|
365
|
+
|| (moduleIdLength > 1 && source[offset] === 48)) return null;
|
|
366
|
+
const moduleId = source.subarray(offset, moduleIdEnd).toString("ascii");
|
|
367
|
+
if (!Number.isSafeInteger(Number(moduleId))) return null;
|
|
368
|
+
|
|
369
|
+
let cursor = moduleIdEnd;
|
|
370
|
+
const identifiers = new Map();
|
|
371
|
+
const reverseIdentifiers = new Map();
|
|
372
|
+
for (const part of PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE) {
|
|
373
|
+
if (typeof part === "string") {
|
|
374
|
+
const literal = Buffer.from(part);
|
|
375
|
+
if (cursor + literal.length > source.length
|
|
376
|
+
|| !source.subarray(cursor, cursor + literal.length).equals(literal)) return null;
|
|
377
|
+
cursor += literal.length;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!asciiIdentifierStart(source[cursor])
|
|
382
|
+
|| asciiIdentifierStart(source[cursor + 1])
|
|
383
|
+
|| asciiDigit(source[cursor + 1])) return null;
|
|
384
|
+
const candidate = String.fromCharCode(source[cursor]);
|
|
385
|
+
// The audited module uses single-byte minifier names. Keeping that bound
|
|
386
|
+
// rejects capture-prone "semantic" guesses while allowing permutations.
|
|
387
|
+
const mapped = identifiers.get(part.identifier);
|
|
388
|
+
const reverse = reverseIdentifiers.get(candidate);
|
|
389
|
+
if ((mapped && mapped !== candidate) || (reverse && reverse !== part.identifier)) return null;
|
|
390
|
+
identifiers.set(part.identifier, candidate);
|
|
391
|
+
reverseIdentifiers.set(candidate, part.identifier);
|
|
392
|
+
cursor += 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (cursor < source.length && source[cursor] !== 44 && source[cursor] !== 125) return null;
|
|
396
|
+
return {
|
|
397
|
+
offset,
|
|
398
|
+
end: cursor,
|
|
399
|
+
length: cursor - offset,
|
|
400
|
+
moduleId,
|
|
401
|
+
anchor: `${moduleId}(`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function locateCompatibleCopilotConfigModule(source) {
|
|
406
|
+
const sentinel = Buffer.from(PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL);
|
|
407
|
+
const matches = new Map();
|
|
408
|
+
let sentinelCount = 0;
|
|
409
|
+
let searchOffset = 0;
|
|
410
|
+
while (searchOffset <= source.length - sentinel.length) {
|
|
411
|
+
const sentinelOffset = source.indexOf(sentinel, searchOffset);
|
|
412
|
+
if (sentinelOffset < 0) break;
|
|
413
|
+
sentinelCount += 1;
|
|
414
|
+
searchOffset = sentinelOffset + sentinel.length;
|
|
415
|
+
|
|
416
|
+
const lowerBound = Math.max(0, sentinelOffset - PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES);
|
|
417
|
+
for (let offset = lowerBound; offset <= sentinelOffset; offset += 1) {
|
|
418
|
+
if (!asciiDigit(source[offset]) || (offset > 0 && asciiDigit(source[offset - 1]))) continue;
|
|
419
|
+
if (offset > 0 && source[offset - 1] !== 44 && source[offset - 1] !== 123) continue;
|
|
420
|
+
const match = matchCopilotConfigAlphaTemplate(source, offset);
|
|
421
|
+
if (match && sentinelOffset >= match.offset && sentinelOffset < match.end) {
|
|
422
|
+
matches.set(match.offset, match);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (sentinelCount === 0) {
|
|
427
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has no compatible Copilot configuration sentinel`);
|
|
428
|
+
}
|
|
429
|
+
if (matches.size !== 1) {
|
|
430
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has ${matches.size} alpha-compatible Copilot configuration modules, expected exactly one`);
|
|
431
|
+
}
|
|
432
|
+
return matches.values().next().value;
|
|
433
|
+
}
|
|
434
|
+
|
|
323
435
|
export function blockSha256(buffer, blockSize) {
|
|
324
436
|
const size = integer(blockSize, "block size");
|
|
325
437
|
if (size === 0) throw new Error("Invalid ASAR block size: 0");
|
|
@@ -385,22 +497,24 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
385
497
|
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} integrity metadata does not match its bytes`);
|
|
386
498
|
}
|
|
387
499
|
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
|
|
500
|
+
const compatibleModule = locateCompatibleCopilotConfigModule(sourceTarget);
|
|
501
|
+
const editOffset = compatibleModule.offset;
|
|
502
|
+
const editEnd = compatibleModule.end;
|
|
503
|
+
const sourceEdit = sourceTarget.subarray(editOffset, editEnd);
|
|
504
|
+
const anchorPositions = occurrencePositions(sourceTarget, Buffer.from(compatibleModule.anchor));
|
|
505
|
+
if (anchorPositions.length !== 1 || anchorPositions[0] !== editOffset) {
|
|
506
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} compatible module header is not unique`);
|
|
507
|
+
}
|
|
508
|
+
let replacementSource;
|
|
509
|
+
try {
|
|
510
|
+
replacementSource = splitOriginConfigModule(compatibleModule.moduleId, compatibleModule.length);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
throw incompatibleAsar(error.message);
|
|
394
513
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|| sha256Hex(sourceTarget.subarray(editOffset, editEnd))
|
|
399
|
-
!== PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256) {
|
|
400
|
-
throw incompatibleAsar("PM Studio Copilot configuration module is not the exact compatible source window");
|
|
514
|
+
const replacement = Buffer.from(replacementSource);
|
|
515
|
+
if (replacement.length !== compatibleModule.length) {
|
|
516
|
+
throw incompatibleAsar("PM Studio split-origin replacement does not preserve the compatible module size");
|
|
401
517
|
}
|
|
402
|
-
|
|
403
|
-
const replacement = Buffer.from(PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE);
|
|
404
518
|
const patchedTarget = Buffer.from(sourceTarget);
|
|
405
519
|
replacement.copy(patchedTarget, editOffset);
|
|
406
520
|
const patchedTargetSha256 = sha256Hex(patchedTarget);
|
|
@@ -436,12 +550,12 @@ export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
|
436
550
|
const edit = Object.freeze({
|
|
437
551
|
offset: editOffset,
|
|
438
552
|
absoluteOffset: absoluteOffset + editOffset,
|
|
439
|
-
length:
|
|
440
|
-
anchor:
|
|
553
|
+
length: compatibleModule.length,
|
|
554
|
+
anchor: compatibleModule.anchor,
|
|
441
555
|
anchorCount: 1,
|
|
442
|
-
sourceSha256:
|
|
556
|
+
sourceSha256: sha256Hex(sourceEdit),
|
|
443
557
|
patchedSha256: sha256Hex(replacement),
|
|
444
|
-
replacement:
|
|
558
|
+
replacement: replacementSource,
|
|
445
559
|
});
|
|
446
560
|
const target = Object.freeze({
|
|
447
561
|
path: PM_STUDIO_COPILOT_CONFIG_MODULE_PATH,
|