codex-copilot-dx 0.6.0 → 0.6.2
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 +100 -8
- package/bin/cli.mjs +23 -4
- package/package.json +1 -1
- package/scripts/benchmark.mjs +34 -0
- package/src/adapter.mjs +16 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +143 -47
- package/src/cli-status.mjs +35 -2
- package/src/doctor.mjs +56 -3
- package/src/observability.mjs +10 -4
- package/src/pm-studio-asar.mjs +392 -0
- package/src/pm-studio-relay.mjs +523 -0
- package/src/pm-studio-setup.mjs +734 -0
- package/src/pm-studio-status.mjs +130 -0
- package/src/profile-model-runtime.mjs +3 -2
- package/src/profile-routing.mjs +74 -0
- package/src/profile-runtime.mjs +13 -0
- package/src/running-adapter.mjs +16 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
AUTH_PROFILE_CLAUDE,
|
|
6
|
+
readAuthProfileCredentials,
|
|
7
|
+
} from "./auth-profile.mjs";
|
|
8
|
+
import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createPmStudioSetupOperations,
|
|
11
|
+
DEFAULT_PM_STUDIO_APP_PATH,
|
|
12
|
+
inspectPmStudioApp,
|
|
13
|
+
PM_STUDIO_CLAUDE_AUTH_COMMAND,
|
|
14
|
+
} from "./pm-studio-setup.mjs";
|
|
15
|
+
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
16
|
+
import { status } from "./status.mjs";
|
|
17
|
+
|
|
18
|
+
const PM_RELAY_HOST = "127.0.0.1";
|
|
19
|
+
const PM_RELAY_PORT = 2026;
|
|
20
|
+
|
|
21
|
+
function selectRecipe(metadata, recipes) {
|
|
22
|
+
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
23
|
+
&& recipe.build === String(metadata.build)
|
|
24
|
+
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeMessage(error) {
|
|
28
|
+
return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inspectClaudeProfile(home, readCredentials) {
|
|
32
|
+
try {
|
|
33
|
+
const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
34
|
+
return {
|
|
35
|
+
configured: credentials.configured === true,
|
|
36
|
+
valid: credentials.valid === true,
|
|
37
|
+
reason: credentials.reason || "",
|
|
38
|
+
login: credentials.identity?.login || "",
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { configured: true, valid: false, reason: safeMessage(error), login: "" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function inspectPmStudioStatus({
|
|
46
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
47
|
+
home = os.homedir(),
|
|
48
|
+
recipes = PM_STUDIO_RECIPES,
|
|
49
|
+
operationOverrides = {},
|
|
50
|
+
existsSync = fs.existsSync,
|
|
51
|
+
inspectApp = inspectPmStudioApp,
|
|
52
|
+
readClaudeCredentials = readAuthProfileCredentials,
|
|
53
|
+
checkRunningAdapterFn = checkRunningAdapter,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
56
|
+
let app;
|
|
57
|
+
if (!existsSync(appPath)) {
|
|
58
|
+
app = { state: "not_installed", metadata: null, issues: [] };
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
const metadata = operations.readBundleMetadata({
|
|
62
|
+
appPath,
|
|
63
|
+
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
64
|
+
processRunner: operations.processRunner,
|
|
65
|
+
});
|
|
66
|
+
const normalized = {
|
|
67
|
+
version: String(metadata.version),
|
|
68
|
+
build: String(metadata.build),
|
|
69
|
+
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
70
|
+
};
|
|
71
|
+
const recipe = selectRecipe(normalized, recipes);
|
|
72
|
+
app = recipe
|
|
73
|
+
? { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id }
|
|
74
|
+
: { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const claude = inspectClaudeProfile(home, readClaudeCredentials);
|
|
81
|
+
let adapter;
|
|
82
|
+
try {
|
|
83
|
+
adapter = await checkRunningAdapterFn({ host: PM_RELAY_HOST, port: PM_RELAY_PORT });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
adapter = { ok: false, error };
|
|
86
|
+
}
|
|
87
|
+
const operational = app.state === "patched" && claude.valid && adapter?.ok === true;
|
|
88
|
+
return { ok: operational, appPath, app, claude, adapter };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function appStatusLine(result, commandName) {
|
|
92
|
+
const { app } = result;
|
|
93
|
+
const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
|
|
94
|
+
if (app.state === "patched") return status("ok", `PM Studio ${version} is patched and verified`);
|
|
95
|
+
if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
|
|
96
|
+
if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
|
|
97
|
+
if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
|
|
98
|
+
if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
|
|
99
|
+
return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
|
|
103
|
+
const lines = [
|
|
104
|
+
`${commandName} pms status`,
|
|
105
|
+
appStatusLine(result, commandName),
|
|
106
|
+
];
|
|
107
|
+
if (result.claude.valid) {
|
|
108
|
+
const account = result.claude.login ? ` for ${result.claude.login}` : "";
|
|
109
|
+
lines.push(status("ok", `Isolated Claude profile is valid${account}`));
|
|
110
|
+
} else {
|
|
111
|
+
const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
|
|
112
|
+
lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
|
|
113
|
+
}
|
|
114
|
+
if (result.adapter?.ok) {
|
|
115
|
+
lines.push(status("ok", `PM relay is available at ${result.adapter.baseUrl}`));
|
|
116
|
+
} else if (result.adapter?.incompatible) {
|
|
117
|
+
lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
|
|
118
|
+
} else {
|
|
119
|
+
lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
|
|
120
|
+
}
|
|
121
|
+
lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runPmStudioStatus(options = {}) {
|
|
126
|
+
const result = await inspectPmStudioStatus(options);
|
|
127
|
+
const output = formatPmStudioStatus(result, options);
|
|
128
|
+
(options.log || console.log)(output);
|
|
129
|
+
return { ...result, output };
|
|
130
|
+
}
|
|
@@ -33,6 +33,7 @@ export function createProfileModelRuntime({
|
|
|
33
33
|
codexClient,
|
|
34
34
|
claudeClient = codexClient,
|
|
35
35
|
claudeMode = "inherited",
|
|
36
|
+
codexCredentialFingerprint = "",
|
|
36
37
|
claudeCredentialFingerprint = "",
|
|
37
38
|
home,
|
|
38
39
|
env = process.env,
|
|
@@ -93,7 +94,7 @@ export function createProfileModelRuntime({
|
|
|
93
94
|
function loadCached({ client, profile, registry, ownsClaudeModels }) {
|
|
94
95
|
const credentialFingerprint = profile === "claude"
|
|
95
96
|
? String(claudeCredentialFingerprint || "").trim()
|
|
96
|
-
: "";
|
|
97
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
97
98
|
if (profile === "claude" && !credentialFingerprint) return false;
|
|
98
99
|
const cached = loadModelCache({ home, profile, credentialFingerprint });
|
|
99
100
|
if (!cached) return false;
|
|
@@ -133,7 +134,7 @@ export function createProfileModelRuntime({
|
|
|
133
134
|
function persist(models, profile) {
|
|
134
135
|
const credentialFingerprint = profile === "claude"
|
|
135
136
|
? String(claudeCredentialFingerprint || "").trim()
|
|
136
|
-
: "";
|
|
137
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
137
138
|
if (profile === "claude" && !credentialFingerprint) return;
|
|
138
139
|
try {
|
|
139
140
|
saveModelCache(models, { home, profile, credentialFingerprint });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function catalogData(catalog) {
|
|
2
|
+
if (Array.isArray(catalog)) return catalog;
|
|
3
|
+
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isClaudeCatalogEntry(model) {
|
|
7
|
+
const id = String(model?.id || "").trim();
|
|
8
|
+
const vendor = String(model?.vendor || "").trim().toLowerCase();
|
|
9
|
+
return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isAllowedClaudeModel(model) {
|
|
13
|
+
return isClaudeCatalogEntry(model)
|
|
14
|
+
&& String(model?.vendor || "").trim().toLowerCase() === "anthropic"
|
|
15
|
+
&& model?.model_picker_enabled !== false
|
|
16
|
+
&& Array.isArray(model?.supported_endpoints)
|
|
17
|
+
&& model.supported_endpoints.includes("/chat/completions");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
21
|
+
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
22
|
+
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
|
|
26
|
+
if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
|
|
27
|
+
if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
|
|
28
|
+
|
|
29
|
+
const unset = Symbol("unset");
|
|
30
|
+
let cachedCatalog = unset;
|
|
31
|
+
let cachedEnabled = false;
|
|
32
|
+
let cachedAllowed = new Map();
|
|
33
|
+
let cachedKnown = new Set();
|
|
34
|
+
let rebuilds = 0;
|
|
35
|
+
|
|
36
|
+
function availability() {
|
|
37
|
+
const catalog = getCatalog();
|
|
38
|
+
const enabled = isClaudeEnabled() === true;
|
|
39
|
+
if (catalog === cachedCatalog && enabled === cachedEnabled) {
|
|
40
|
+
return { allowed: cachedAllowed, known: cachedKnown };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allowed = new Map();
|
|
44
|
+
const known = new Set();
|
|
45
|
+
for (const model of catalogData(catalog)) {
|
|
46
|
+
const id = String(model?.id || "").trim();
|
|
47
|
+
if (!id || !isClaudeCatalogEntry(model)) continue;
|
|
48
|
+
known.add(id);
|
|
49
|
+
if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
50
|
+
}
|
|
51
|
+
cachedCatalog = catalog;
|
|
52
|
+
cachedEnabled = enabled;
|
|
53
|
+
cachedAllowed = allowed;
|
|
54
|
+
cachedKnown = known;
|
|
55
|
+
rebuilds += 1;
|
|
56
|
+
return { allowed, known };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
classify(modelId) {
|
|
61
|
+
const id = String(modelId || "").trim();
|
|
62
|
+
const { allowed, known } = availability();
|
|
63
|
+
if (allowed.has(id)) return "claude";
|
|
64
|
+
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
65
|
+
return "enterprise";
|
|
66
|
+
},
|
|
67
|
+
allowedModels() {
|
|
68
|
+
return [...availability().allowed.values()];
|
|
69
|
+
},
|
|
70
|
+
diagnostics() {
|
|
71
|
+
return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AUTH_PROFILE_CLAUDE,
|
|
3
|
+
AUTH_PROFILE_CODEX,
|
|
3
4
|
authProfilePaths,
|
|
4
5
|
profileReauthMessage,
|
|
5
6
|
readAuthProfileCredentials,
|
|
@@ -45,6 +46,16 @@ export function createProfileRuntime({
|
|
|
45
46
|
};
|
|
46
47
|
const credentials = readCredentials();
|
|
47
48
|
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
+
let codexCredentialFingerprint = "";
|
|
50
|
+
try {
|
|
51
|
+
const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
52
|
+
if (codexCredentials.valid) {
|
|
53
|
+
codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// The Codex client remains authoritative if its optional metadata cannot
|
|
57
|
+
// be inspected; only credential-bound cache reuse is disabled.
|
|
58
|
+
}
|
|
48
59
|
|
|
49
60
|
if (!credentials.configured) {
|
|
50
61
|
return Object.freeze({
|
|
@@ -52,6 +63,7 @@ export function createProfileRuntime({
|
|
|
52
63
|
claudeClient: codexClient,
|
|
53
64
|
claudeMode: "inherited",
|
|
54
65
|
claudeProfile,
|
|
66
|
+
codexCredentialFingerprint,
|
|
55
67
|
claudeCredentialFingerprint: "",
|
|
56
68
|
});
|
|
57
69
|
}
|
|
@@ -73,6 +85,7 @@ export function createProfileRuntime({
|
|
|
73
85
|
claudeClient,
|
|
74
86
|
claudeMode: "isolated",
|
|
75
87
|
claudeProfile,
|
|
88
|
+
codexCredentialFingerprint,
|
|
76
89
|
claudeCredentialFingerprint: credentials.valid
|
|
77
90
|
? githubTokenFingerprint(credentials.token)
|
|
78
91
|
: "",
|
package/src/running-adapter.mjs
CHANGED
|
@@ -5,6 +5,7 @@ export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
|
5
5
|
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
6
6
|
export const ADAPTER_PROTOCOL_VERSION = 2;
|
|
7
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
8
|
+
export const ADAPTER_CAPABILITIES = Object.freeze(["pm_studio_relay_v1"]);
|
|
8
9
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
9
10
|
|
|
10
11
|
export function adapterHealthPayload() {
|
|
@@ -14,6 +15,7 @@ export function adapterHealthPayload() {
|
|
|
14
15
|
pid: process.pid,
|
|
15
16
|
version: ADAPTER_VERSION,
|
|
16
17
|
protocol_version: ADAPTER_PROTOCOL_VERSION,
|
|
18
|
+
capabilities: [...ADAPTER_CAPABILITIES],
|
|
17
19
|
instance_id: ADAPTER_INSTANCE_ID,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -37,6 +39,7 @@ export async function checkRunningAdapter({
|
|
|
37
39
|
timeoutMs = 500,
|
|
38
40
|
expectedVersion = ADAPTER_VERSION,
|
|
39
41
|
expectedProtocolVersion = ADAPTER_PROTOCOL_VERSION,
|
|
42
|
+
requiredCapabilities = ADAPTER_CAPABILITIES,
|
|
40
43
|
} = {}) {
|
|
41
44
|
const baseUrl = adapterBaseUrl(host, port);
|
|
42
45
|
const ctrl = new AbortController();
|
|
@@ -62,6 +65,19 @@ export async function checkRunningAdapter({
|
|
|
62
65
|
expectedProtocolVersion,
|
|
63
66
|
};
|
|
64
67
|
}
|
|
68
|
+
const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
|
|
69
|
+
if (requiredCapabilities.some((capability) => !capabilities.has(capability))) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
incompatible: true,
|
|
73
|
+
baseUrl,
|
|
74
|
+
status: resp.status,
|
|
75
|
+
data,
|
|
76
|
+
expectedVersion,
|
|
77
|
+
expectedProtocolVersion,
|
|
78
|
+
requiredCapabilities: [...requiredCapabilities],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
65
81
|
return { ok: true, baseUrl, data };
|
|
66
82
|
} catch (e) {
|
|
67
83
|
return { ok: false, baseUrl, error: e };
|