vibe-coding-master 0.7.15 → 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 CHANGED
@@ -254,11 +254,17 @@ DevContainer, make the second endpoint reachable from the container. In the VCM
254
254
  The key is stored in global VCM state (`~/.vcm/settings.json`) with owner-only
255
255
  permissions and is never returned by the settings API. It is used for CCR
256
256
  checks, model discovery, and the GPT-only `apiKeyHelper`. GPT sessions receive a
257
- child-only `--settings` override; VCM removes inherited Anthropic token
258
- variables only from those child processes. VCM removes CCR-owned takeover
259
- entries from global Claude settings, so native Claude selections keep their
260
- normal launch and account authentication. If CCR is disabled, unreachable,
261
- rejects the key, or does not expose
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
262
268
  `Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or Restart and does
263
269
  not fall back to another model.
264
270
 
@@ -324,7 +330,9 @@ Conversation translation is controlled from the sidebar `Translation` section.
324
330
 
325
331
  VCM uses a task-scoped Translator role and Claude transcript JSONL files, not
326
332
  raw terminal text. Translation memory and completed file translations remain
327
- 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.
328
336
 
329
337
  Common controls:
330
338
 
@@ -428,9 +436,10 @@ Use it to:
428
436
  - merge task harness commits back to the connected repository branch when
429
437
  appropriate
430
438
 
431
- Harness Engineer is task-scoped and runs from the active task worktree. A new
432
- task receives its own Harness Engineer session. Durable memory is versioned with
433
- the project harness files.
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.
434
443
 
435
444
  ### Auto Memory
436
445
 
@@ -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) {
@@ -5,7 +5,6 @@ import fastifyStatic from "@fastify/static";
5
5
  import { createArtifactService } from "./services/artifact-service.js";
6
6
  import { createClaudeAdapter } from "./adapters/claude-adapter.js";
7
7
  import { createCcrGatewayAdapter } from "./adapters/ccr-gateway-adapter.js";
8
- import { createClaudeSettingsAdapter } from "./adapters/claude-settings-adapter.js";
9
8
  import { createCommandRunner } from "./adapters/command-runner.js";
10
9
  import { createCommandDispatcher } from "./services/command-dispatcher.js";
11
10
  import { createClaudeHookService } from "./services/claude-hook-service.js";
@@ -209,11 +208,9 @@ export function createDefaultServerDeps(options = {}) {
209
208
  const git = createGitAdapter(runner);
210
209
  const claude = createClaudeAdapter(runner);
211
210
  const appSettings = createAppSettingsService({ fs });
212
- const claudeSettings = createClaudeSettingsAdapter({ fs });
213
211
  const ccrIntegration = createCcrIntegrationService({
214
212
  settings: appSettings,
215
- gateway: createCcrGatewayAdapter(),
216
- restoreNativeClaudeSettings: () => claudeSettings.restoreNativeSettings()
213
+ gateway: createCcrGatewayAdapter()
217
214
  });
218
215
  const runtime = createNodePtyTerminalRuntime({ fs });
219
216
  const registry = createSessionRegistry();
@@ -1,11 +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";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { VcmError } from "../errors.js";
5
+ import { resolveVcmDataDir } from "../vcm-data-dir.js";
4
6
  const DEFAULT_CACHE_TTL_MS = 10_000;
5
7
  const DEFAULT_API_KEY_HELPER_PATH = fileURLToPath(new URL("../../../scripts/ccr-api-key-helper.mjs", import.meta.url));
6
8
  export function createCcrIntegrationService(deps) {
7
9
  const now = deps.now ?? (() => new Date());
8
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");
9
13
  let cachedProbe;
10
14
  let inFlight;
11
15
  async function probe(force = false) {
@@ -67,9 +71,6 @@ export function createCcrIntegrationService(deps) {
67
71
  return {
68
72
  async initialize() {
69
73
  const settings = await deps.settings.getCcrIntegrationSettings();
70
- if (settings.apiKey) {
71
- await deps.restoreNativeClaudeSettings?.();
72
- }
73
74
  if (settings.enabled) {
74
75
  await probe(true);
75
76
  }
@@ -102,9 +103,6 @@ export function createCcrIntegrationService(deps) {
102
103
  enabled: nextEnabled,
103
104
  apiKey: nextApiKey
104
105
  });
105
- if (nextApiKey) {
106
- await deps.restoreNativeClaudeSettings?.();
107
- }
108
106
  if (clearApiKey || !nextEnabled) {
109
107
  cachedProbe = undefined;
110
108
  return buildStatus();
@@ -130,7 +128,7 @@ export function createCcrIntegrationService(deps) {
130
128
  },
131
129
  async getLaunchEnvironment(model) {
132
130
  if (!isCcrSessionModel(model)) {
133
- return {};
131
+ return buildNativeLaunchEnvironment(baseEnv);
134
132
  }
135
133
  const settings = await deps.settings.getCcrIntegrationSettings();
136
134
  if (!settings.enabled) {
@@ -164,15 +162,12 @@ export function createCcrIntegrationService(deps) {
164
162
  CODEXL_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
165
163
  ANTHROPIC_SMALL_FAST_MODEL: CCR_GPT_MODEL_ID,
166
164
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
165
+ CLAUDE_CONFIG_DIR: configDir,
167
166
  NO_PROXY: noProxy,
168
167
  no_proxy: noProxy
169
168
  };
170
169
  },
171
170
  async getLaunchSettingsOverride(model) {
172
- const settings = await deps.settings.getCcrIntegrationSettings();
173
- if (settings.apiKey) {
174
- await deps.restoreNativeClaudeSettings?.();
175
- }
176
171
  if (!isCcrSessionModel(model)) {
177
172
  return undefined;
178
173
  }
@@ -182,6 +177,70 @@ export function createCcrIntegrationService(deps) {
182
177
  }
183
178
  };
184
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
+ }
185
244
  function createStatus(input) {
186
245
  const reason = input.error
187
246
  ?? (input.enabled ? "CCR GPT-5.6 Sol is unavailable." : "Enable CCR GPT models in Settings.");
@@ -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(repoRoot, input = {}) {
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 withRepoLock(project.repoRoot, async () => {
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 Boolean(session && (session.status === "running" || session.claudeSessionId));
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,6 +55,9 @@ 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
+ if (launchMode === "resume" && persisted) {
59
+ assertResumeProviderCompatible(persisted, model);
60
+ }
58
61
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
59
62
  getModelLaunchEnvironment(model),
60
63
  getModelLaunchSettingsOverride(model)
@@ -74,7 +77,7 @@ export function createSessionService(deps) {
74
77
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
75
78
  ? persisted.transcriptPath
76
79
  : resumeClaudeSessionId
77
- ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
80
+ ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
78
81
  : undefined;
79
82
  const startCommand = {
80
83
  ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
@@ -113,6 +116,7 @@ export function createSessionService(deps) {
113
116
  model,
114
117
  effort,
115
118
  cwd: startCommand.cwd,
119
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
116
120
  terminalBackend: "node-pty",
117
121
  pid: runtimeSession.pid,
118
122
  roleCommandPath: isDispatchableRole(role)
@@ -163,6 +167,9 @@ export function createSessionService(deps) {
163
167
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
164
168
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
165
169
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
170
+ if (launchMode === "resume" && persisted) {
171
+ assertResumeProviderCompatible(persisted, model);
172
+ }
166
173
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
167
174
  getModelLaunchEnvironment(model),
168
175
  getModelLaunchSettingsOverride(model)
@@ -194,7 +201,7 @@ export function createSessionService(deps) {
194
201
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
195
202
  const claudeSessionId = resumeClaudeSessionId ?? "";
196
203
  const transcriptPath = resumeClaudeSessionId
197
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
204
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
198
205
  : undefined;
199
206
  const startCommand = {
200
207
  ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
@@ -236,6 +243,7 @@ export function createSessionService(deps) {
236
243
  model,
237
244
  effort,
238
245
  cwd: sessionCwd,
246
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
239
247
  terminalBackend: "node-pty",
240
248
  pid: runtimeSession.pid,
241
249
  startedAt: runtimeSession.startedAt,
@@ -276,6 +284,9 @@ export function createSessionService(deps) {
276
284
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
277
285
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
278
286
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
287
+ if (launchMode === "resume" && persisted) {
288
+ assertResumeProviderCompatible(persisted, model);
289
+ }
279
290
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
280
291
  getModelLaunchEnvironment(model),
281
292
  getModelLaunchSettingsOverride(model)
@@ -303,7 +314,7 @@ export function createSessionService(deps) {
303
314
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
304
315
  const claudeSessionId = resumeClaudeSessionId ?? "";
305
316
  const transcriptPath = resumeClaudeSessionId
306
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
317
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
307
318
  : undefined;
308
319
  const startCommand = {
309
320
  ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
@@ -345,6 +356,7 @@ export function createSessionService(deps) {
345
356
  model,
346
357
  effort,
347
358
  cwd: sessionCwd,
359
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
348
360
  terminalBackend: "node-pty",
349
361
  pid: runtimeSession.pid,
350
362
  startedAt: runtimeSession.startedAt,
@@ -489,6 +501,7 @@ export function createSessionService(deps) {
489
501
  const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
490
502
  const model = normalizeClaudeModel(session.model);
491
503
  const effort = normalizeClaudeEffort(session.effort);
504
+ assertResumeProviderCompatible(session, model);
492
505
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
493
506
  getModelLaunchEnvironment(model),
494
507
  getModelLaunchSettingsOverride(model)
@@ -546,13 +559,14 @@ export function createSessionService(deps) {
546
559
  permissionMode,
547
560
  model,
548
561
  effort,
562
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
549
563
  pid: runtimeSession.pid,
550
564
  startedAt: runtimeSession.startedAt,
551
565
  updatedAt: timestamp,
552
566
  lastOutputAt: runtimeSession.lastOutputAt,
553
567
  exitCode: runtimeSession.exitCode,
554
568
  transcriptPath: session.claudeSessionId
555
- ? claudeTranscriptPath(repoRoot, session.claudeSessionId)
569
+ ? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
556
570
  : session.transcriptPath
557
571
  };
558
572
  deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
@@ -560,16 +574,16 @@ export function createSessionService(deps) {
560
574
  return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
561
575
  }
562
576
  async function getModelLaunchEnvironment(model) {
563
- if (!isCcrSessionModel(model)) {
564
- return {};
565
- }
566
577
  if (!deps.ccrIntegration) {
567
- throw new VcmError({
568
- code: "CCR_UNAVAILABLE",
569
- message: "CCR integration is not available in this VCM runtime.",
570
- statusCode: 409,
571
- hint: "Enable and configure CCR GPT models before starting this session."
572
- });
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 {};
573
587
  }
574
588
  return deps.ccrIntegration.getLaunchEnvironment(model);
575
589
  }
@@ -1490,6 +1504,28 @@ function normalizeClaudeEffort(value) {
1490
1504
  }
1491
1505
  return "default";
1492
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
+ }
1493
1529
  function formatClaudeCdCommand(targetCwd) {
1494
1530
  // Claude Code's `/cd` slash command takes the literal remainder of the line as
1495
1531
  // the path, so the target must NOT be wrapped in quotes (quotes are taken as part
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.7.15",
3
+ "version": "0.7.16",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,79 +0,0 @@
1
- import { homedir } from "node:os";
2
- import path from "node:path";
3
- const CCR_ENV_KEYS = [
4
- "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
5
- "ANTHROPIC_BASE_URL",
6
- "ANTHROPIC_API_BASE_URL",
7
- "CLAUDE_AGENT_API_BASE_URL",
8
- "ANTHROPIC_MODEL",
9
- "CCR_CLAUDE_CODE_MODEL",
10
- "CODEXL_CLAUDE_CODE_MODEL",
11
- "ANTHROPIC_SMALL_FAST_MODEL"
12
- ];
13
- export function createClaudeSettingsAdapter(deps) {
14
- const settingsPath = deps.settingsPath ?? resolveClaudeSettingsPath(deps.env ?? process.env);
15
- return {
16
- async restoreNativeSettings() {
17
- if (!(await deps.fs.pathExists(settingsPath))) {
18
- return false;
19
- }
20
- const current = await deps.fs.readJson(settingsPath);
21
- const restored = restoreNativeClaudeSettings(current);
22
- if (!restored.changed) {
23
- return false;
24
- }
25
- await deps.fs.writeJsonAtomic(settingsPath, restored.settings);
26
- return true;
27
- }
28
- };
29
- }
30
- export function restoreNativeClaudeSettings(value) {
31
- const settings = isObject(value) ? structuredClone(value) : {};
32
- const helperIsCcr = typeof settings.apiKeyHelper === "string"
33
- && settings.apiKeyHelper.includes(".claude-code-router");
34
- const env = isObject(settings.env) ? settings.env : undefined;
35
- const envUsesCcr = env
36
- ? ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_BASE_URL", "CLAUDE_AGENT_API_BASE_URL"]
37
- .some((key) => isCcrGatewayUrl(env[key]))
38
- : false;
39
- if (!helperIsCcr && !envUsesCcr) {
40
- return { changed: false, settings };
41
- }
42
- let changed = false;
43
- if (helperIsCcr) {
44
- delete settings.apiKeyHelper;
45
- changed = true;
46
- }
47
- if (env) {
48
- for (const key of CCR_ENV_KEYS) {
49
- if (key in env) {
50
- delete env[key];
51
- changed = true;
52
- }
53
- }
54
- if (Object.keys(env).length === 0) {
55
- delete settings.env;
56
- }
57
- }
58
- return { changed, settings };
59
- }
60
- function resolveClaudeSettingsPath(env) {
61
- const configDir = env.CLAUDE_CONFIG_DIR?.trim();
62
- return path.join(configDir ? path.resolve(configDir) : path.join(homedir(), ".claude"), "settings.json");
63
- }
64
- function isCcrGatewayUrl(value) {
65
- if (typeof value !== "string") {
66
- return false;
67
- }
68
- try {
69
- const url = new URL(value);
70
- return url.port === "3456"
71
- && ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
72
- }
73
- catch {
74
- return false;
75
- }
76
- }
77
- function isObject(value) {
78
- return typeof value === "object" && value !== null && !Array.isArray(value);
79
- }