vibe-coding-master 0.7.14 → 0.7.16
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 +27 -14
- package/dist/backend/adapters/ccr-gateway-adapter.js +73 -50
- package/dist/backend/adapters/claude-adapter.js +6 -2
- package/dist/backend/runtime/session-registry.js +7 -0
- package/dist/backend/services/ccr-integration-service.js +95 -5
- package/dist/backend/services/claude-transcript-service.js +10 -10
- package/dist/backend/services/runtime-coordinator-service.js +37 -42
- package/dist/backend/services/session-service.js +72 -21
- package/dist/shared/types/session.js +7 -1
- package/package.json +1 -1
- package/scripts/ccr-api-key-helper.mjs +21 -0
package/README.md
CHANGED
|
@@ -235,14 +235,16 @@ a host-running [Claude Code Router](https://github.com/musistudio/claude-code-ro
|
|
|
235
235
|
CCR must already be installed, authenticated, configured, and running on the
|
|
236
236
|
host. VCM does not manage the CCR process.
|
|
237
237
|
|
|
238
|
-
VCM
|
|
238
|
+
VCM automatically checks the local-host and DevContainer endpoints:
|
|
239
239
|
|
|
240
240
|
```text
|
|
241
|
+
http://127.0.0.1:3456
|
|
241
242
|
http://host.docker.internal:3456
|
|
242
243
|
```
|
|
243
244
|
|
|
244
|
-
Configure CCR to listen on port `3456` with an API key
|
|
245
|
-
from the container. In the VCM
|
|
245
|
+
Configure CCR to listen on port `3456` with an API key. When VCM runs in a
|
|
246
|
+
DevContainer, make the second endpoint reachable from the container. In the VCM
|
|
247
|
+
`Settings` section:
|
|
246
248
|
|
|
247
249
|
1. enter and save the CCR API key;
|
|
248
250
|
2. enable `CCR GPT models`;
|
|
@@ -250,13 +252,21 @@ from the container. In the VCM `Settings` section:
|
|
|
250
252
|
4. select `GPT-5.6 Sol (CCR)` in any Session model control.
|
|
251
253
|
|
|
252
254
|
The key is stored in global VCM state (`~/.vcm/settings.json`) with owner-only
|
|
253
|
-
permissions
|
|
254
|
-
|
|
255
|
-
`
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
255
|
+
permissions and is never returned by the settings API. It is used for CCR
|
|
256
|
+
checks, model discovery, and the GPT-only `apiKeyHelper`. GPT sessions receive a
|
|
257
|
+
child-only `--settings` override and use the isolated Claude configuration root
|
|
258
|
+
`~/.vcm/claude/ccr`. VCM never edits `~/.claude/settings.json`. Native Claude
|
|
259
|
+
sessions keep their normal configuration and account authentication; VCM only
|
|
260
|
+
removes inherited environment variables that clearly point at the local CCR
|
|
261
|
+
gateway from the native child process. Configure CCR without enabling its
|
|
262
|
+
global Claude Code or Claude App takeover if those clients should remain on
|
|
263
|
+
Anthropic.
|
|
264
|
+
|
|
265
|
+
Resume keeps the provider recorded by the existing Session. Use Restart when
|
|
266
|
+
switching between a native Claude model and `GPT-5.6 Sol (CCR)`. If CCR is
|
|
267
|
+
disabled, unreachable, rejects the key, or does not expose
|
|
268
|
+
`Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or Restart and does
|
|
269
|
+
not fall back to another model.
|
|
260
270
|
|
|
261
271
|
## Launch Template
|
|
262
272
|
|
|
@@ -320,7 +330,9 @@ Conversation translation is controlled from the sidebar `Translation` section.
|
|
|
320
330
|
|
|
321
331
|
VCM uses a task-scoped Translator role and Claude transcript JSONL files, not
|
|
322
332
|
raw terminal text. Translation memory and completed file translations remain
|
|
323
|
-
project-level durable data.
|
|
333
|
+
project-level durable data. When translation is enabled and the active task's
|
|
334
|
+
Harness is initialized, the backend automatically starts a fresh Translator for
|
|
335
|
+
the task or resumes its saved Session.
|
|
324
336
|
|
|
325
337
|
Common controls:
|
|
326
338
|
|
|
@@ -424,9 +436,10 @@ Use it to:
|
|
|
424
436
|
- merge task harness commits back to the connected repository branch when
|
|
425
437
|
appropriate
|
|
426
438
|
|
|
427
|
-
Harness Engineer is task-scoped and runs from the active task worktree.
|
|
428
|
-
|
|
429
|
-
the project harness
|
|
439
|
+
Harness Engineer is task-scoped and runs from the active task worktree. The
|
|
440
|
+
backend automatically starts a fresh Harness Engineer for each active task or
|
|
441
|
+
resumes its saved Session. Durable memory is versioned with the project harness
|
|
442
|
+
files.
|
|
430
443
|
|
|
431
444
|
### Auto Memory
|
|
432
445
|
|
|
@@ -1,62 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CCR_GATEWAY_BASE_URLS, CCR_GPT_MODEL_ID } from "../../shared/types/session.js";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
3
3
|
const CCR_ENCODED_MODEL_PREFIX = "anthropic/claude-ccr-h";
|
|
4
4
|
export function createCcrGatewayAdapter(deps = {}) {
|
|
5
|
-
const
|
|
5
|
+
const baseUrls = (deps.baseUrl ? [deps.baseUrl] : CCR_GATEWAY_BASE_URLS)
|
|
6
|
+
.map((baseUrl) => baseUrl.replace(/\/+$/, ""));
|
|
6
7
|
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
7
8
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
8
9
|
return {
|
|
9
10
|
async probe(apiKey) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const failures = [];
|
|
12
|
+
for (const baseUrl of baseUrls) {
|
|
13
|
+
const result = await probeEndpoint(fetchImpl, baseUrl, apiKey, timeoutMs);
|
|
14
|
+
if (result.baseUrl) {
|
|
15
|
+
return result;
|
|
14
16
|
}
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
connectionState: "not-ccr",
|
|
18
|
-
modelAvailable: false,
|
|
19
|
-
error: `The fixed CCR endpoint ${baseUrl} did not identify a Claude Code Router gateway.`
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
const models = await requestJson(fetchImpl, `${baseUrl}/v1/models`, timeoutMs, {
|
|
23
|
-
authorization: `Bearer ${apiKey}`,
|
|
24
|
-
"user-agent": "Claude Code"
|
|
25
|
-
});
|
|
26
|
-
if (models.response.status === 401 || models.response.status === 403) {
|
|
27
|
-
return {
|
|
28
|
-
connectionState: "unauthorized",
|
|
29
|
-
modelAvailable: false,
|
|
30
|
-
error: "CCR rejected the configured API key."
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
if (!models.response.ok) {
|
|
34
|
-
return invalidResponse(`CCR model discovery returned HTTP ${models.response.status}.`);
|
|
35
|
-
}
|
|
36
|
-
const modelDescriptors = readModelDescriptors(models.payload);
|
|
37
|
-
if (!modelDescriptors) {
|
|
38
|
-
return invalidResponse("CCR returned an invalid /v1/models response.");
|
|
39
|
-
}
|
|
40
|
-
const modelAvailable = modelDescriptors.some(isRequiredModel);
|
|
41
|
-
return {
|
|
42
|
-
connectionState: "available",
|
|
43
|
-
modelAvailable,
|
|
44
|
-
...(modelAvailable
|
|
45
|
-
? {}
|
|
46
|
-
: { error: `CCR does not expose the required model ${CCR_GPT_MODEL_ID}.` })
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
const timedOut = isAbortError(error);
|
|
51
|
-
return {
|
|
52
|
-
connectionState: "unreachable",
|
|
53
|
-
modelAvailable: false,
|
|
54
|
-
error: timedOut
|
|
55
|
-
? `CCR connection timed out after ${timeoutMs} ms at ${baseUrl}.`
|
|
56
|
-
: `CCR could not be reached from the VCM container backend at ${baseUrl}. Reason: ${errorMessage(error)}`
|
|
57
|
-
};
|
|
17
|
+
failures.push(result);
|
|
58
18
|
}
|
|
19
|
+
return combineProbeFailures(baseUrls, failures);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function probeEndpoint(fetchImpl, baseUrl, apiKey, timeoutMs) {
|
|
24
|
+
try {
|
|
25
|
+
const root = await requestJson(fetchImpl, `${baseUrl}/`, timeoutMs);
|
|
26
|
+
if (!root.response.ok) {
|
|
27
|
+
return invalidResponse(`Gateway root at ${baseUrl} returned HTTP ${root.response.status}.`);
|
|
28
|
+
}
|
|
29
|
+
if (!isCcrGateway(root.payload)) {
|
|
30
|
+
return {
|
|
31
|
+
connectionState: "not-ccr",
|
|
32
|
+
modelAvailable: false,
|
|
33
|
+
error: `${baseUrl} did not identify a Claude Code Router gateway.`
|
|
34
|
+
};
|
|
59
35
|
}
|
|
36
|
+
const models = await requestJson(fetchImpl, `${baseUrl}/v1/models`, timeoutMs, {
|
|
37
|
+
authorization: `Bearer ${apiKey}`,
|
|
38
|
+
"user-agent": "Claude Code"
|
|
39
|
+
});
|
|
40
|
+
if (models.response.status === 401 || models.response.status === 403) {
|
|
41
|
+
return {
|
|
42
|
+
connectionState: "unauthorized",
|
|
43
|
+
modelAvailable: false,
|
|
44
|
+
baseUrl,
|
|
45
|
+
error: "CCR rejected the configured API key."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!models.response.ok) {
|
|
49
|
+
return invalidResponse(`CCR model discovery at ${baseUrl} returned HTTP ${models.response.status}.`, baseUrl);
|
|
50
|
+
}
|
|
51
|
+
const modelDescriptors = readModelDescriptors(models.payload);
|
|
52
|
+
if (!modelDescriptors) {
|
|
53
|
+
return invalidResponse("CCR returned an invalid /v1/models response.", baseUrl);
|
|
54
|
+
}
|
|
55
|
+
const modelAvailable = modelDescriptors.some(isRequiredModel);
|
|
56
|
+
return {
|
|
57
|
+
connectionState: "available",
|
|
58
|
+
modelAvailable,
|
|
59
|
+
baseUrl,
|
|
60
|
+
...(modelAvailable
|
|
61
|
+
? {}
|
|
62
|
+
: { error: `CCR does not expose the required model ${CCR_GPT_MODEL_ID}.` })
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const timedOut = isAbortError(error);
|
|
67
|
+
return {
|
|
68
|
+
connectionState: "unreachable",
|
|
69
|
+
modelAvailable: false,
|
|
70
|
+
error: timedOut
|
|
71
|
+
? `CCR connection timed out after ${timeoutMs} ms at ${baseUrl}.`
|
|
72
|
+
: `CCR could not be reached from the VCM backend at ${baseUrl}. Reason: ${errorMessage(error)}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function combineProbeFailures(baseUrls, failures) {
|
|
77
|
+
const lastFailure = failures.at(-1);
|
|
78
|
+
return {
|
|
79
|
+
connectionState: lastFailure?.connectionState ?? "unreachable",
|
|
80
|
+
modelAvailable: false,
|
|
81
|
+
error: `CCR was not found at ${baseUrls.join(" or ")}. ${failures.map((failure) => failure.error).filter(Boolean).join(" ")}`.trim()
|
|
60
82
|
};
|
|
61
83
|
}
|
|
62
84
|
async function requestJson(fetchImpl, url, timeoutMs, headers = {}) {
|
|
@@ -131,10 +153,11 @@ function decodeCcrModelId(modelId) {
|
|
|
131
153
|
function normalizeModelName(value) {
|
|
132
154
|
return (value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
133
155
|
}
|
|
134
|
-
function invalidResponse(error) {
|
|
156
|
+
function invalidResponse(error, baseUrl) {
|
|
135
157
|
return {
|
|
136
158
|
connectionState: "invalid-response",
|
|
137
159
|
modelAvailable: false,
|
|
160
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
138
161
|
error
|
|
139
162
|
};
|
|
140
163
|
}
|
|
@@ -18,8 +18,9 @@ export function createClaudeAdapter(runner) {
|
|
|
18
18
|
}
|
|
19
19
|
return result.stdout.trim();
|
|
20
20
|
},
|
|
21
|
-
buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default") {
|
|
21
|
+
buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride) {
|
|
22
22
|
const args = ["--agent", role];
|
|
23
|
+
const sessionSettings = { ...settingsOverride };
|
|
23
24
|
if (claudeSessionId) {
|
|
24
25
|
args.push(resume ? "--resume" : "--session-id", claudeSessionId);
|
|
25
26
|
}
|
|
@@ -27,11 +28,14 @@ export function createClaudeAdapter(runner) {
|
|
|
27
28
|
args.push("--model", model);
|
|
28
29
|
}
|
|
29
30
|
if (effort === "ultracode") {
|
|
30
|
-
|
|
31
|
+
sessionSettings.ultracode = true;
|
|
31
32
|
}
|
|
32
33
|
else if (effort !== "default") {
|
|
33
34
|
args.push("--effort", effort);
|
|
34
35
|
}
|
|
36
|
+
if (Object.keys(sessionSettings).length > 0) {
|
|
37
|
+
args.push("--settings", JSON.stringify(sessionSettings));
|
|
38
|
+
}
|
|
35
39
|
if (permissionMode !== "default") {
|
|
36
40
|
args.push("--permission-mode", permissionMode);
|
|
37
41
|
}
|
|
@@ -2,6 +2,13 @@ export function createSessionRegistry() {
|
|
|
2
2
|
const sessions = new Map();
|
|
3
3
|
return {
|
|
4
4
|
upsert(session) {
|
|
5
|
+
for (const [sessionId, candidate] of sessions) {
|
|
6
|
+
if (sessionId !== session.id
|
|
7
|
+
&& candidate.taskSlug === session.taskSlug
|
|
8
|
+
&& candidate.role === session.role) {
|
|
9
|
+
sessions.delete(sessionId);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
5
12
|
sessions.set(session.id, session);
|
|
6
13
|
},
|
|
7
14
|
get(sessionId) {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID, createSessionModelOptions, isCcrSessionModel } from "../../shared/types/session.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { VcmError } from "../errors.js";
|
|
5
|
+
import { resolveVcmDataDir } from "../vcm-data-dir.js";
|
|
3
6
|
const DEFAULT_CACHE_TTL_MS = 10_000;
|
|
7
|
+
const DEFAULT_API_KEY_HELPER_PATH = fileURLToPath(new URL("../../../scripts/ccr-api-key-helper.mjs", import.meta.url));
|
|
4
8
|
export function createCcrIntegrationService(deps) {
|
|
5
9
|
const now = deps.now ?? (() => new Date());
|
|
6
10
|
const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
11
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
12
|
+
const configDir = deps.configDir ?? path.join(resolveVcmDataDir(baseEnv), "claude", "ccr");
|
|
7
13
|
let cachedProbe;
|
|
8
14
|
let inFlight;
|
|
9
15
|
async function probe(force = false) {
|
|
@@ -122,7 +128,7 @@ export function createCcrIntegrationService(deps) {
|
|
|
122
128
|
},
|
|
123
129
|
async getLaunchEnvironment(model) {
|
|
124
130
|
if (!isCcrSessionModel(model)) {
|
|
125
|
-
return
|
|
131
|
+
return buildNativeLaunchEnvironment(baseEnv);
|
|
126
132
|
}
|
|
127
133
|
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
128
134
|
if (!settings.enabled) {
|
|
@@ -142,11 +148,13 @@ export function createCcrIntegrationService(deps) {
|
|
|
142
148
|
hint: "Check that host CCR is running on port 3456 and exposes GPT-5.6 Sol."
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
|
-
const
|
|
151
|
+
const gatewayBaseUrl = checked.result.baseUrl ?? CCR_GATEWAY_BASE_URL;
|
|
152
|
+
const gatewayHost = new URL(gatewayBaseUrl).hostname;
|
|
153
|
+
const noProxy = mergeNoProxy(deps.baseEnv?.NO_PROXY ?? deps.baseEnv?.no_proxy ?? process.env.NO_PROXY ?? process.env.no_proxy, gatewayHost);
|
|
146
154
|
return {
|
|
147
|
-
ANTHROPIC_BASE_URL:
|
|
148
|
-
ANTHROPIC_API_BASE_URL:
|
|
149
|
-
CLAUDE_AGENT_API_BASE_URL:
|
|
155
|
+
ANTHROPIC_BASE_URL: gatewayBaseUrl,
|
|
156
|
+
ANTHROPIC_API_BASE_URL: gatewayBaseUrl,
|
|
157
|
+
CLAUDE_AGENT_API_BASE_URL: gatewayBaseUrl,
|
|
150
158
|
ANTHROPIC_AUTH_TOKEN: undefined,
|
|
151
159
|
ANTHROPIC_API_KEY: undefined,
|
|
152
160
|
ANTHROPIC_MODEL: CCR_GPT_MODEL_ID,
|
|
@@ -154,12 +162,85 @@ export function createCcrIntegrationService(deps) {
|
|
|
154
162
|
CODEXL_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
|
|
155
163
|
ANTHROPIC_SMALL_FAST_MODEL: CCR_GPT_MODEL_ID,
|
|
156
164
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
165
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
157
166
|
NO_PROXY: noProxy,
|
|
158
167
|
no_proxy: noProxy
|
|
159
168
|
};
|
|
169
|
+
},
|
|
170
|
+
async getLaunchSettingsOverride(model) {
|
|
171
|
+
if (!isCcrSessionModel(model)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
apiKeyHelper: buildApiKeyHelperCommand(deps.apiKeyHelperPath ?? DEFAULT_API_KEY_HELPER_PATH)
|
|
176
|
+
};
|
|
160
177
|
}
|
|
161
178
|
};
|
|
162
179
|
}
|
|
180
|
+
const CCR_BASE_URL_KEYS = [
|
|
181
|
+
"ANTHROPIC_BASE_URL",
|
|
182
|
+
"ANTHROPIC_API_BASE_URL",
|
|
183
|
+
"CLAUDE_AGENT_API_BASE_URL"
|
|
184
|
+
];
|
|
185
|
+
const CCR_MODEL_KEYS = [
|
|
186
|
+
"ANTHROPIC_MODEL",
|
|
187
|
+
"ANTHROPIC_SMALL_FAST_MODEL"
|
|
188
|
+
];
|
|
189
|
+
const CCR_ONLY_ENV_KEYS = [
|
|
190
|
+
"CCR_CLAUDE_CODE_MODEL",
|
|
191
|
+
"CODEXL_CLAUDE_CODE_MODEL",
|
|
192
|
+
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
|
|
193
|
+
];
|
|
194
|
+
export function buildNativeLaunchEnvironment(baseEnv) {
|
|
195
|
+
const environment = {};
|
|
196
|
+
let inheritedCcrTakeover = false;
|
|
197
|
+
if (baseEnv.CLAUDE_CONFIG_DIR?.trim()) {
|
|
198
|
+
environment.CLAUDE_CONFIG_DIR = baseEnv.CLAUDE_CONFIG_DIR.trim();
|
|
199
|
+
}
|
|
200
|
+
for (const key of CCR_BASE_URL_KEYS) {
|
|
201
|
+
if (isCcrGatewayUrl(baseEnv[key])) {
|
|
202
|
+
environment[key] = undefined;
|
|
203
|
+
inheritedCcrTakeover = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const key of CCR_MODEL_KEYS) {
|
|
207
|
+
if (isCcrModelName(baseEnv[key])) {
|
|
208
|
+
environment[key] = undefined;
|
|
209
|
+
inheritedCcrTakeover = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const key of CCR_ONLY_ENV_KEYS) {
|
|
213
|
+
if (baseEnv[key] !== undefined) {
|
|
214
|
+
environment[key] = undefined;
|
|
215
|
+
inheritedCcrTakeover = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (inheritedCcrTakeover) {
|
|
219
|
+
environment.ANTHROPIC_AUTH_TOKEN = undefined;
|
|
220
|
+
environment.ANTHROPIC_API_KEY = undefined;
|
|
221
|
+
}
|
|
222
|
+
return environment;
|
|
223
|
+
}
|
|
224
|
+
function isCcrGatewayUrl(value) {
|
|
225
|
+
if (!value) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const url = new URL(value);
|
|
230
|
+
return url.port === "3456"
|
|
231
|
+
&& ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function isCcrModelName(value) {
|
|
238
|
+
if (!value) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return value === CCR_GPT_MODEL_ID
|
|
242
|
+
|| value.startsWith("anthropic/claude-ccr-h");
|
|
243
|
+
}
|
|
163
244
|
function createStatus(input) {
|
|
164
245
|
const reason = input.error
|
|
165
246
|
?? (input.enabled ? "CCR GPT-5.6 Sol is unavailable." : "Enable CCR GPT models in Settings.");
|
|
@@ -193,3 +274,12 @@ export function mergeNoProxy(current, host) {
|
|
|
193
274
|
}
|
|
194
275
|
return values.join(",");
|
|
195
276
|
}
|
|
277
|
+
export function buildApiKeyHelperCommand(helperPath) {
|
|
278
|
+
if (process.platform === "win32") {
|
|
279
|
+
return `${JSON.stringify(process.execPath)} ${JSON.stringify(helperPath)}`;
|
|
280
|
+
}
|
|
281
|
+
return `${quotePosixShell(process.execPath)} ${quotePosixShell(helperPath)}`;
|
|
282
|
+
}
|
|
283
|
+
function quotePosixShell(value) {
|
|
284
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
285
|
+
}
|
|
@@ -208,14 +208,14 @@ export function resolveExistingClaudeTranscriptPath(session) {
|
|
|
208
208
|
if (sessionPath) {
|
|
209
209
|
return sessionPath;
|
|
210
210
|
}
|
|
211
|
-
const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId));
|
|
211
|
+
const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId, session.claudeConfigDir));
|
|
212
212
|
if (cwdPath) {
|
|
213
213
|
return cwdPath;
|
|
214
214
|
}
|
|
215
|
-
return findClaudeTranscriptPathBySessionId(session.claudeSessionId);
|
|
215
|
+
return findClaudeTranscriptPathBySessionId(session.claudeSessionId, session.claudeConfigDir);
|
|
216
216
|
}
|
|
217
|
-
export function findClaudeTranscriptPathBySessionId(claudeSessionId) {
|
|
218
|
-
const root = claudeProjectsRoot();
|
|
217
|
+
export function findClaudeTranscriptPathBySessionId(claudeSessionId, configDir) {
|
|
218
|
+
const root = claudeProjectsRoot(configDir);
|
|
219
219
|
let projectDirs;
|
|
220
220
|
try {
|
|
221
221
|
projectDirs = readdirSync(root, { withFileTypes: true });
|
|
@@ -253,17 +253,17 @@ function existingFile(candidate) {
|
|
|
253
253
|
return undefined;
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
export function claudeProjectsRoot() {
|
|
257
|
-
return join(homedir(), ".claude", "projects");
|
|
256
|
+
export function claudeProjectsRoot(configDir) {
|
|
257
|
+
return join(configDir ?? join(homedir(), ".claude"), "projects");
|
|
258
258
|
}
|
|
259
259
|
export function projectHash(projectDir) {
|
|
260
260
|
return projectDir.replace(/[\/\s]+/g, "-");
|
|
261
261
|
}
|
|
262
|
-
export function projectsTranscriptDir(projectDir) {
|
|
263
|
-
return join(claudeProjectsRoot(), projectHash(projectDir));
|
|
262
|
+
export function projectsTranscriptDir(projectDir, configDir) {
|
|
263
|
+
return join(claudeProjectsRoot(configDir), projectHash(projectDir));
|
|
264
264
|
}
|
|
265
|
-
export function claudeTranscriptPath(projectDir, claudeSessionId) {
|
|
266
|
-
return join(projectsTranscriptDir(projectDir), `${claudeSessionId}.jsonl`);
|
|
265
|
+
export function claudeTranscriptPath(projectDir, claudeSessionId, configDir) {
|
|
266
|
+
return join(projectsTranscriptDir(projectDir, configDir), `${claudeSessionId}.jsonl`);
|
|
267
267
|
}
|
|
268
268
|
export function parseAssistantContent(line) {
|
|
269
269
|
let obj;
|
|
@@ -34,6 +34,40 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function reconcileProject(repoRoot, input = {}) {
|
|
38
|
+
return withRepoLock(repoRoot, async () => {
|
|
39
|
+
const [activeTask, gatewayStatus] = await Promise.all([
|
|
40
|
+
resolveActiveTask(repoRoot, input.taskSlug),
|
|
41
|
+
deps.gatewayService.getStatus().catch(() => null)
|
|
42
|
+
]);
|
|
43
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
44
|
+
if (!activeTask) {
|
|
45
|
+
return { activeTask: null, gatewayStatus };
|
|
46
|
+
}
|
|
47
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
48
|
+
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
49
|
+
await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
|
|
50
|
+
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
51
|
+
.then((status) => status.initialized)
|
|
52
|
+
.catch(() => false);
|
|
53
|
+
await Promise.all([
|
|
54
|
+
reconcileHarnessEngineer(repoRoot, activeTask),
|
|
55
|
+
reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
|
|
56
|
+
]);
|
|
57
|
+
if (preferences.translationEnabled && harnessInitialized) {
|
|
58
|
+
await startConversationTranslationListeners(repoRoot, activeTask);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
64
|
+
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
65
|
+
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
66
|
+
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
67
|
+
}
|
|
68
|
+
return { activeTask, gatewayStatus };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
37
71
|
return {
|
|
38
72
|
start() {
|
|
39
73
|
if (reconcileTimer !== undefined) {
|
|
@@ -51,53 +85,14 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
51
85
|
clearTimer(reconcileTimer);
|
|
52
86
|
reconcileTimer = undefined;
|
|
53
87
|
},
|
|
54
|
-
reconcileProject
|
|
55
|
-
return withRepoLock(repoRoot, async () => {
|
|
56
|
-
const [activeTask, gatewayStatus] = await Promise.all([
|
|
57
|
-
resolveActiveTask(repoRoot, input.taskSlug),
|
|
58
|
-
deps.gatewayService.getStatus().catch(() => null)
|
|
59
|
-
]);
|
|
60
|
-
const preferences = await deps.appSettings.getPreferences();
|
|
61
|
-
if (!activeTask) {
|
|
62
|
-
return { activeTask: null, gatewayStatus };
|
|
63
|
-
}
|
|
64
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
65
|
-
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
66
|
-
await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
|
|
67
|
-
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
68
|
-
.then((status) => status.initialized)
|
|
69
|
-
.catch(() => false);
|
|
70
|
-
await Promise.all([
|
|
71
|
-
reconcileHarnessEngineer(repoRoot, activeTask),
|
|
72
|
-
reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
|
|
73
|
-
]);
|
|
74
|
-
if (preferences.translationEnabled && harnessInitialized) {
|
|
75
|
-
await startConversationTranslationListeners(repoRoot, activeTask);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
79
|
-
}
|
|
80
|
-
await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
81
|
-
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
82
|
-
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
83
|
-
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
84
|
-
}
|
|
85
|
-
return { activeTask, gatewayStatus };
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
+
reconcileProject
|
|
88
89
|
};
|
|
89
90
|
async function reconcileCurrentProject() {
|
|
90
91
|
const project = await deps.projectService.getCurrentProject();
|
|
91
92
|
if (!project) {
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
|
-
await
|
|
95
|
-
const activeTask = await resolveActiveTask(project.repoRoot);
|
|
96
|
-
if (activeTask) {
|
|
97
|
-
await deps.turnReconciler.reconcileTask(project.repoRoot, activeTask, await deps.getStateRoot(project.repoRoot));
|
|
98
|
-
}
|
|
99
|
-
return { activeTask, gatewayStatus: null };
|
|
100
|
-
});
|
|
95
|
+
await reconcileProject(project.repoRoot);
|
|
101
96
|
}
|
|
102
97
|
async function resolveActiveTask(repoRoot, requestedTaskSlug) {
|
|
103
98
|
const tasks = await deps.taskService.listTasks(repoRoot);
|
|
@@ -136,7 +131,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
136
131
|
});
|
|
137
132
|
}
|
|
138
133
|
function shouldAutoEnsureTaskToolSession(session) {
|
|
139
|
-
return
|
|
134
|
+
return !session || session.status === "running" || Boolean(session.claudeSessionId);
|
|
140
135
|
}
|
|
141
136
|
async function ensureTaskToolRoleSession(repoRoot, taskSlug, role, input) {
|
|
142
137
|
const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
@@ -55,7 +55,13 @@ export function createSessionService(deps) {
|
|
|
55
55
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
56
56
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
57
57
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
58
|
-
|
|
58
|
+
if (launchMode === "resume" && persisted) {
|
|
59
|
+
assertResumeProviderCompatible(persisted, model);
|
|
60
|
+
}
|
|
61
|
+
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
62
|
+
getModelLaunchEnvironment(model),
|
|
63
|
+
getModelLaunchSettingsOverride(model)
|
|
64
|
+
]);
|
|
59
65
|
const resumeClaudeSessionId = launchMode === "resume"
|
|
60
66
|
? persisted?.claudeSessionId
|
|
61
67
|
: undefined;
|
|
@@ -71,10 +77,10 @@ export function createSessionService(deps) {
|
|
|
71
77
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
72
78
|
? persisted.transcriptPath
|
|
73
79
|
: resumeClaudeSessionId
|
|
74
|
-
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
|
|
80
|
+
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
75
81
|
: undefined;
|
|
76
82
|
const startCommand = {
|
|
77
|
-
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
|
|
83
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
78
84
|
cwd: taskRepoRoot
|
|
79
85
|
};
|
|
80
86
|
const runtimeSession = await deps.runtime.createSession({
|
|
@@ -110,6 +116,7 @@ export function createSessionService(deps) {
|
|
|
110
116
|
model,
|
|
111
117
|
effort,
|
|
112
118
|
cwd: startCommand.cwd,
|
|
119
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
113
120
|
terminalBackend: "node-pty",
|
|
114
121
|
pid: runtimeSession.pid,
|
|
115
122
|
roleCommandPath: isDispatchableRole(role)
|
|
@@ -160,7 +167,13 @@ export function createSessionService(deps) {
|
|
|
160
167
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
161
168
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
162
169
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
|
|
163
|
-
|
|
170
|
+
if (launchMode === "resume" && persisted) {
|
|
171
|
+
assertResumeProviderCompatible(persisted, model);
|
|
172
|
+
}
|
|
173
|
+
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
174
|
+
getModelLaunchEnvironment(model),
|
|
175
|
+
getModelLaunchSettingsOverride(model)
|
|
176
|
+
]);
|
|
164
177
|
const resumeClaudeSessionId = launchMode === "resume"
|
|
165
178
|
? persisted?.claudeSessionId
|
|
166
179
|
: undefined;
|
|
@@ -188,10 +201,10 @@ export function createSessionService(deps) {
|
|
|
188
201
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
189
202
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
190
203
|
const transcriptPath = resumeClaudeSessionId
|
|
191
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
|
|
204
|
+
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
192
205
|
: undefined;
|
|
193
206
|
const startCommand = {
|
|
194
|
-
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
|
|
207
|
+
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
195
208
|
cwd: launchCwd
|
|
196
209
|
};
|
|
197
210
|
const runtimeSession = await deps.runtime.createSession({
|
|
@@ -230,6 +243,7 @@ export function createSessionService(deps) {
|
|
|
230
243
|
model,
|
|
231
244
|
effort,
|
|
232
245
|
cwd: sessionCwd,
|
|
246
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
233
247
|
terminalBackend: "node-pty",
|
|
234
248
|
pid: runtimeSession.pid,
|
|
235
249
|
startedAt: runtimeSession.startedAt,
|
|
@@ -270,7 +284,13 @@ export function createSessionService(deps) {
|
|
|
270
284
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
271
285
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
272
286
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
|
|
273
|
-
|
|
287
|
+
if (launchMode === "resume" && persisted) {
|
|
288
|
+
assertResumeProviderCompatible(persisted, model);
|
|
289
|
+
}
|
|
290
|
+
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
291
|
+
getModelLaunchEnvironment(model),
|
|
292
|
+
getModelLaunchSettingsOverride(model)
|
|
293
|
+
]);
|
|
274
294
|
const resumeClaudeSessionId = launchMode === "resume"
|
|
275
295
|
? persisted?.claudeSessionId
|
|
276
296
|
: undefined;
|
|
@@ -294,10 +314,10 @@ export function createSessionService(deps) {
|
|
|
294
314
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
295
315
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
296
316
|
const transcriptPath = resumeClaudeSessionId
|
|
297
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
|
|
317
|
+
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
298
318
|
: undefined;
|
|
299
319
|
const startCommand = {
|
|
300
|
-
...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
|
|
320
|
+
...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
301
321
|
cwd: launchCwd
|
|
302
322
|
};
|
|
303
323
|
const runtimeSession = await deps.runtime.createSession({
|
|
@@ -336,6 +356,7 @@ export function createSessionService(deps) {
|
|
|
336
356
|
model,
|
|
337
357
|
effort,
|
|
338
358
|
cwd: sessionCwd,
|
|
359
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
339
360
|
terminalBackend: "node-pty",
|
|
340
361
|
pid: runtimeSession.pid,
|
|
341
362
|
startedAt: runtimeSession.startedAt,
|
|
@@ -480,14 +501,18 @@ export function createSessionService(deps) {
|
|
|
480
501
|
const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
|
|
481
502
|
const model = normalizeClaudeModel(session.model);
|
|
482
503
|
const effort = normalizeClaudeEffort(session.effort);
|
|
483
|
-
|
|
504
|
+
assertResumeProviderCompatible(session, model);
|
|
505
|
+
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
506
|
+
getModelLaunchEnvironment(model),
|
|
507
|
+
getModelLaunchSettingsOverride(model)
|
|
508
|
+
]);
|
|
484
509
|
// Spawn (`claude --resume`) always anchors at the base repoRoot so resume works
|
|
485
510
|
// even if the persisted task cwd was deleted. `--resume` then restores the
|
|
486
511
|
// session's own last cwd (tracked on `session.cwd`), so the `/cd` migrate below
|
|
487
512
|
// fires only when that restored cwd differs from the target worktree.
|
|
488
513
|
const launchCwd = repoRoot;
|
|
489
514
|
const startCommand = {
|
|
490
|
-
...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort),
|
|
515
|
+
...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort, modelSettingsOverride),
|
|
491
516
|
cwd: launchCwd
|
|
492
517
|
};
|
|
493
518
|
const runtimeSession = await deps.runtime.createSession({
|
|
@@ -534,13 +559,14 @@ export function createSessionService(deps) {
|
|
|
534
559
|
permissionMode,
|
|
535
560
|
model,
|
|
536
561
|
effort,
|
|
562
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
537
563
|
pid: runtimeSession.pid,
|
|
538
564
|
startedAt: runtimeSession.startedAt,
|
|
539
565
|
updatedAt: timestamp,
|
|
540
566
|
lastOutputAt: runtimeSession.lastOutputAt,
|
|
541
567
|
exitCode: runtimeSession.exitCode,
|
|
542
568
|
transcriptPath: session.claudeSessionId
|
|
543
|
-
? claudeTranscriptPath(repoRoot, session.claudeSessionId)
|
|
569
|
+
? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
|
|
544
570
|
: session.transcriptPath
|
|
545
571
|
};
|
|
546
572
|
deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
|
|
@@ -548,19 +574,22 @@ export function createSessionService(deps) {
|
|
|
548
574
|
return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
|
|
549
575
|
}
|
|
550
576
|
async function getModelLaunchEnvironment(model) {
|
|
551
|
-
if (!isCcrSessionModel(model)) {
|
|
552
|
-
return {};
|
|
553
|
-
}
|
|
554
577
|
if (!deps.ccrIntegration) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
578
|
+
if (isCcrSessionModel(model)) {
|
|
579
|
+
throw new VcmError({
|
|
580
|
+
code: "CCR_UNAVAILABLE",
|
|
581
|
+
message: "CCR integration is not available in this VCM runtime.",
|
|
582
|
+
statusCode: 409,
|
|
583
|
+
hint: "Enable and configure CCR GPT models before starting this session."
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return {};
|
|
561
587
|
}
|
|
562
588
|
return deps.ccrIntegration.getLaunchEnvironment(model);
|
|
563
589
|
}
|
|
590
|
+
async function getModelLaunchSettingsOverride(model) {
|
|
591
|
+
return deps.ccrIntegration?.getLaunchSettingsOverride(model);
|
|
592
|
+
}
|
|
564
593
|
function isRuntimeSessionAlive(session) {
|
|
565
594
|
if (!session || isExitedStatus(session.status) || session.pid === undefined) {
|
|
566
595
|
return false;
|
|
@@ -1475,6 +1504,28 @@ function normalizeClaudeEffort(value) {
|
|
|
1475
1504
|
}
|
|
1476
1505
|
return "default";
|
|
1477
1506
|
}
|
|
1507
|
+
function assertResumeProviderCompatible(session, requestedModel) {
|
|
1508
|
+
const persistedModel = normalizeClaudeModel(session.model);
|
|
1509
|
+
if (isCcrSessionModel(persistedModel) !== isCcrSessionModel(requestedModel)) {
|
|
1510
|
+
throw new VcmError({
|
|
1511
|
+
code: "SESSION_PROVIDER_SWITCH_REQUIRES_RESTART",
|
|
1512
|
+
message: `Cannot resume ${session.role} with a different model provider.`,
|
|
1513
|
+
statusCode: 409,
|
|
1514
|
+
hint: "Use Restart to switch between native Claude and CCR models."
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
if (isCcrSessionModel(requestedModel) && !session.claudeConfigDir) {
|
|
1518
|
+
throw new VcmError({
|
|
1519
|
+
code: "CCR_SESSION_CONFIG_MISSING",
|
|
1520
|
+
message: `${session.role} was created before isolated CCR session storage was enabled.`,
|
|
1521
|
+
statusCode: 409,
|
|
1522
|
+
hint: "Restart this role once to create an isolated CCR session."
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function readClaudeConfigDir(environment) {
|
|
1527
|
+
return environment.CLAUDE_CONFIG_DIR?.trim() || undefined;
|
|
1528
|
+
}
|
|
1478
1529
|
function formatClaudeCdCommand(targetCwd) {
|
|
1479
1530
|
// Claude Code's `/cd` slash command takes the literal remainder of the line as
|
|
1480
1531
|
// the path, so the target must NOT be wrapped in quotes (quotes are taken as part
|
|
@@ -45,7 +45,13 @@ export const CLAUDE_MODEL_OPTIONS = [
|
|
|
45
45
|
available: true
|
|
46
46
|
}
|
|
47
47
|
];
|
|
48
|
-
export const
|
|
48
|
+
export const CCR_LOCAL_GATEWAY_BASE_URL = "http://127.0.0.1:3456";
|
|
49
|
+
export const CCR_CONTAINER_GATEWAY_BASE_URL = "http://host.docker.internal:3456";
|
|
50
|
+
export const CCR_GATEWAY_BASE_URL = CCR_CONTAINER_GATEWAY_BASE_URL;
|
|
51
|
+
export const CCR_GATEWAY_BASE_URLS = [
|
|
52
|
+
CCR_LOCAL_GATEWAY_BASE_URL,
|
|
53
|
+
CCR_CONTAINER_GATEWAY_BASE_URL
|
|
54
|
+
];
|
|
49
55
|
export const CCR_GPT_MODEL_ID = "Codex API/gpt-5.6-sol";
|
|
50
56
|
export const CCR_GPT_SESSION_MODEL = `ccr:${CCR_GPT_MODEL_ID}`;
|
|
51
57
|
export function isCcrSessionModel(model) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const dataDir = process.env.VCM_DATA_DIR?.trim()
|
|
8
|
+
? path.resolve(process.env.VCM_DATA_DIR)
|
|
9
|
+
: path.join(homedir(), ".vcm");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const settings = JSON.parse(await readFile(path.join(dataDir, "settings.json"), "utf8"));
|
|
13
|
+
const apiKey = settings?.ccr?.apiKey;
|
|
14
|
+
if (typeof apiKey !== "string" || !apiKey.trim()) {
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else {
|
|
17
|
+
process.stdout.write(`${apiKey.trim()}\n`);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|