@velum-labs/routekit-tool-claude 0.18.6 → 1.0.0
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/dist/driver.d.ts +1 -1
- package/dist/driver.js +111 -108
- package/dist/index.js +2 -2
- package/dist/install-codec.d.ts +58 -0
- package/dist/install-codec.js +165 -0
- package/dist/install.js +18 -287
- package/dist/launch.js +7 -14
- package/dist/test/driver.test.js +29 -0
- package/dist/test/install.test.js +0 -189
- package/dist/test/launch.test.js +3 -0
- package/package.json +19 -7
package/dist/driver.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import type { Options, Query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
2
|
import type { HarnessDriver } from "@velum-labs/routekit-harness-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
export declare const claudeDriverConfigSchema: z.ZodObject<{
|
|
5
5
|
command: z.ZodDefault<z.ZodString>;
|
|
6
6
|
model: z.ZodOptional<z.ZodString>;
|
package/dist/driver.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
-
import {
|
|
2
|
+
import { AsyncChannel, asHarnessError, buildChildEnv, createCachedHarnessDriver, DEFAULT_AUTOMATION_APPROVAL_POLICY, decideApproval, HarnessError, nowIso, PendingRequests, probeCliVersion, resolveDriverEnv, resumeStringField, SessionResourceRegistry, SingleFlightTurnController } from "@velum-labs/routekit-harness-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
const RESUME_CURSOR_VERSION = 1;
|
|
5
5
|
const DEFAULT_COMMAND = "claude";
|
|
6
6
|
const AUTH_ENV_NAMES = [
|
|
@@ -18,9 +18,6 @@ export const claudeDriverConfigSchema = z.object({
|
|
|
18
18
|
/** Extra credential env var names forwarded into the claude child. */
|
|
19
19
|
credentialEnvNames: z.array(z.string()).default([])
|
|
20
20
|
});
|
|
21
|
-
function nowIso() {
|
|
22
|
-
return new Date().toISOString();
|
|
23
|
-
}
|
|
24
21
|
/**
|
|
25
22
|
* The canonical approval request type for a claude tool name. Note `Task`
|
|
26
23
|
* (Claude's sub-agent tool) deliberately lands in the generic `tool_approval`
|
|
@@ -50,6 +47,7 @@ class ClaudeSession {
|
|
|
50
47
|
#sessionId;
|
|
51
48
|
#activeQuery;
|
|
52
49
|
#stopped = false;
|
|
50
|
+
#turns = new SingleFlightTurnController();
|
|
53
51
|
#queryFn;
|
|
54
52
|
constructor(input) {
|
|
55
53
|
this.#config = input.config;
|
|
@@ -57,8 +55,7 @@ class ClaudeSession {
|
|
|
57
55
|
this.#cwd = input.options.cwd;
|
|
58
56
|
this.#model = input.options.model ?? input.config.model;
|
|
59
57
|
this.#reasoning = input.options.reasoning;
|
|
60
|
-
this.#approvalPolicy =
|
|
61
|
-
input.options.approvalPolicy ?? DEFAULT_AUTOMATION_APPROVAL_POLICY;
|
|
58
|
+
this.#approvalPolicy = input.options.approvalPolicy ?? DEFAULT_AUTOMATION_APPROVAL_POLICY;
|
|
62
59
|
this.#sessionId = resumeSessionId(input.options.resume) ?? "claude:pending";
|
|
63
60
|
this.#queryFn = input.queryFn;
|
|
64
61
|
}
|
|
@@ -116,109 +113,104 @@ class ClaudeSession {
|
|
|
116
113
|
return decision;
|
|
117
114
|
}
|
|
118
115
|
#emit(event) {
|
|
119
|
-
this.#channel?.(event);
|
|
116
|
+
this.#channel?.push(event);
|
|
120
117
|
}
|
|
121
118
|
async *sendTurn(input) {
|
|
122
119
|
if (this.#stopped)
|
|
123
120
|
throw new HarnessError("session_closed", "claude session is stopped");
|
|
121
|
+
const turn = this.#turns.start(input.signal);
|
|
124
122
|
const controller = new AbortController();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
pathToClaudeCodeExecutable: this.#config.command,
|
|
136
|
-
// The gateway carries this cursor across front-door turns. Make the
|
|
137
|
-
// persistence requirement explicit: SDK defaults have changed across
|
|
138
|
-
// releases and an ephemeral session id cannot be resumed by the next
|
|
139
|
-
// candidate process.
|
|
140
|
-
persistSession: true,
|
|
141
|
-
permissionMode: "default",
|
|
142
|
-
canUseTool: this.#canUseTool(),
|
|
143
|
-
abortController: controller,
|
|
144
|
-
env: this.#childEnv(),
|
|
145
|
-
...(this.#model !== undefined ? { model: this.#model } : {}),
|
|
146
|
-
...(reasoning?.mode === "effort"
|
|
147
|
-
? {
|
|
148
|
-
thinking: { type: "adaptive" },
|
|
149
|
-
effort: reasoning.effort
|
|
150
|
-
}
|
|
151
|
-
: reasoning?.mode === "budget"
|
|
152
|
-
? {
|
|
153
|
-
thinking: {
|
|
154
|
-
type: "enabled",
|
|
155
|
-
budgetTokens: reasoning.budgetTokens
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
: reasoning?.mode === "adaptive"
|
|
159
|
-
? { thinking: { type: "adaptive" } }
|
|
160
|
-
: reasoning?.mode === "disabled"
|
|
161
|
-
? { thinking: { type: "disabled" } }
|
|
162
|
-
: {}),
|
|
163
|
-
...(resume !== undefined ? { resume } : {})
|
|
164
|
-
};
|
|
165
|
-
const queued = [];
|
|
166
|
-
let notify;
|
|
167
|
-
this.#channel = (event) => {
|
|
168
|
-
queued.push(event);
|
|
169
|
-
notify?.();
|
|
170
|
-
};
|
|
123
|
+
const onAbort = () => controller.abort(turn.signal.reason);
|
|
124
|
+
if (turn.signal.aborted)
|
|
125
|
+
onAbort();
|
|
126
|
+
else
|
|
127
|
+
turn.signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
const channel = new AsyncChannel({
|
|
129
|
+
capacity: 256,
|
|
130
|
+
onConsumerReturn: () => turn.dispose()
|
|
131
|
+
});
|
|
132
|
+
this.#channel = channel;
|
|
171
133
|
const turnId = `${this.#sessionId}:turn:${Date.now()}`;
|
|
172
134
|
// turn.started is emitted from mapMessage once system/init fixes the real
|
|
173
135
|
// session id, so it (and every later event) carries the final id.
|
|
174
|
-
|
|
175
|
-
|
|
136
|
+
let q;
|
|
137
|
+
let pump;
|
|
176
138
|
let done = false;
|
|
177
|
-
let failure;
|
|
178
|
-
const pump = (async () => {
|
|
179
|
-
try {
|
|
180
|
-
for await (const message of q) {
|
|
181
|
-
for (const event of this.#mapMessage(message, turnId))
|
|
182
|
-
queued.push(event);
|
|
183
|
-
notify?.();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
failure = error;
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
done = true;
|
|
191
|
-
notify?.();
|
|
192
|
-
}
|
|
193
|
-
})();
|
|
194
139
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
140
|
+
const resume = this.#sessionId !== "claude:pending" ? this.#sessionId : undefined;
|
|
141
|
+
const reasoning = input.reasoning ?? this.#reasoning;
|
|
142
|
+
const options = {
|
|
143
|
+
cwd: this.#cwd,
|
|
144
|
+
pathToClaudeCodeExecutable: this.#config.command,
|
|
145
|
+
// The gateway carries this cursor across front-door turns. Make the
|
|
146
|
+
// persistence requirement explicit: SDK defaults have changed across
|
|
147
|
+
// releases and an ephemeral session id cannot be resumed by the next
|
|
148
|
+
// candidate process.
|
|
149
|
+
persistSession: true,
|
|
150
|
+
permissionMode: "default",
|
|
151
|
+
canUseTool: this.#canUseTool(),
|
|
152
|
+
abortController: controller,
|
|
153
|
+
env: this.#childEnv(),
|
|
154
|
+
...(this.#model !== undefined ? { model: this.#model } : {}),
|
|
155
|
+
...(reasoning?.mode === "effort"
|
|
156
|
+
? {
|
|
157
|
+
thinking: { type: "adaptive" },
|
|
158
|
+
effort: reasoning.effort
|
|
159
|
+
}
|
|
160
|
+
: reasoning?.mode === "budget"
|
|
161
|
+
? {
|
|
162
|
+
thinking: {
|
|
163
|
+
type: "enabled",
|
|
164
|
+
budgetTokens: reasoning.budgetTokens
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
: reasoning?.mode === "adaptive"
|
|
168
|
+
? { thinking: { type: "adaptive" } }
|
|
169
|
+
: reasoning?.mode === "disabled"
|
|
170
|
+
? { thinking: { type: "disabled" } }
|
|
171
|
+
: {}),
|
|
172
|
+
...(resume !== undefined ? { resume } : {})
|
|
173
|
+
};
|
|
174
|
+
q = this.#queryFn({ prompt: input.prompt, options });
|
|
175
|
+
this.#activeQuery = q;
|
|
176
|
+
pump = (async () => {
|
|
177
|
+
try {
|
|
178
|
+
for await (const message of q) {
|
|
179
|
+
for (const event of this.#mapMessage(message, turnId))
|
|
180
|
+
channel.push(event);
|
|
181
|
+
}
|
|
199
182
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
183
|
+
catch (error) {
|
|
184
|
+
const base = { kind: this.#kind, sessionId: this.#sessionId, at: nowIso(), turnId };
|
|
185
|
+
if (controller.signal.aborted) {
|
|
186
|
+
channel.push({ ...base, type: "turn.completed", endReason: "aborted" });
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const harnessError = asHarnessError(error);
|
|
190
|
+
channel.push({
|
|
191
|
+
...base,
|
|
192
|
+
type: "turn.failed",
|
|
193
|
+
errorCode: harnessError.code,
|
|
194
|
+
message: harnessError.message
|
|
195
|
+
});
|
|
196
|
+
}
|
|
214
197
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
198
|
+
finally {
|
|
199
|
+
done = true;
|
|
200
|
+
channel.close();
|
|
218
201
|
}
|
|
219
|
-
}
|
|
202
|
+
})();
|
|
203
|
+
yield* channel;
|
|
204
|
+
await pump;
|
|
220
205
|
}
|
|
221
206
|
finally {
|
|
207
|
+
if (done)
|
|
208
|
+
turn.complete();
|
|
209
|
+
turn.dispose();
|
|
210
|
+
turn.signal.removeEventListener("abort", onAbort);
|
|
211
|
+
if (!done)
|
|
212
|
+
await q?.interrupt().catch(() => undefined);
|
|
213
|
+
channel.close();
|
|
222
214
|
this.#channel = undefined;
|
|
223
215
|
this.#activeQuery = undefined;
|
|
224
216
|
}
|
|
@@ -255,7 +247,13 @@ class ClaudeSession {
|
|
|
255
247
|
yield { ...base, type: "content.delta", stream: "assistant_text", text: block.text, raw };
|
|
256
248
|
}
|
|
257
249
|
else if (block.type === "thinking" && block.thinking !== undefined) {
|
|
258
|
-
yield {
|
|
250
|
+
yield {
|
|
251
|
+
...base,
|
|
252
|
+
type: "content.delta",
|
|
253
|
+
stream: "reasoning_text",
|
|
254
|
+
text: block.thinking,
|
|
255
|
+
raw
|
|
256
|
+
};
|
|
259
257
|
}
|
|
260
258
|
else if (block.type === "tool_use") {
|
|
261
259
|
yield {
|
|
@@ -288,7 +286,9 @@ class ClaudeSession {
|
|
|
288
286
|
return buildChildEnv({
|
|
289
287
|
base: resolveDriverEnv(this.#context),
|
|
290
288
|
allow: [...AUTH_ENV_NAMES, ...this.#config.credentialEnvNames, /^CLAUDE_/],
|
|
291
|
-
...(this.#config.baseUrl !== undefined
|
|
289
|
+
...(this.#config.baseUrl !== undefined
|
|
290
|
+
? { extra: { ANTHROPIC_BASE_URL: this.#config.baseUrl } }
|
|
291
|
+
: {})
|
|
292
292
|
});
|
|
293
293
|
}
|
|
294
294
|
async respondToRequest(requestId, decision) {
|
|
@@ -297,25 +297,30 @@ class ClaudeSession {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
async interrupt() {
|
|
300
|
+
this.#turns.interrupt();
|
|
300
301
|
this.#pending.settleAll("cancel");
|
|
301
302
|
await this.#activeQuery?.interrupt().catch(() => undefined);
|
|
302
303
|
}
|
|
303
304
|
resumeCursor() {
|
|
304
305
|
if (this.#sessionId === "claude:pending")
|
|
305
306
|
return undefined;
|
|
306
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
version: RESUME_CURSOR_VERSION,
|
|
309
|
+
kind: this.#kind,
|
|
310
|
+
data: { sessionId: this.#sessionId }
|
|
311
|
+
};
|
|
307
312
|
}
|
|
308
313
|
async stop() {
|
|
314
|
+
if (this.#stopped)
|
|
315
|
+
return;
|
|
309
316
|
this.#stopped = true;
|
|
317
|
+
this.#turns.interrupt(new Error("claude session stopped"));
|
|
310
318
|
this.#pending.settleAll("cancel");
|
|
311
319
|
await this.#activeQuery?.interrupt().catch(() => undefined);
|
|
312
320
|
}
|
|
313
321
|
}
|
|
314
322
|
function resumeSessionId(resume) {
|
|
315
|
-
|
|
316
|
-
return undefined;
|
|
317
|
-
const data = resume.data;
|
|
318
|
-
return typeof data.sessionId === "string" ? data.sessionId : undefined;
|
|
323
|
+
return resumeStringField(resume, "claude_code", "sessionId");
|
|
319
324
|
}
|
|
320
325
|
class ClaudeInstance {
|
|
321
326
|
kind = "claude_code";
|
|
@@ -323,7 +328,7 @@ class ClaudeInstance {
|
|
|
323
328
|
#context;
|
|
324
329
|
#status;
|
|
325
330
|
#queryFn;
|
|
326
|
-
#sessions = new
|
|
331
|
+
#sessions = new SessionResourceRegistry();
|
|
327
332
|
constructor(input) {
|
|
328
333
|
this.#config = input.config;
|
|
329
334
|
this.#context = input.context;
|
|
@@ -334,19 +339,17 @@ class ClaudeInstance {
|
|
|
334
339
|
return this.#status;
|
|
335
340
|
}
|
|
336
341
|
async startSession(options) {
|
|
342
|
+
this.#sessions.assertOpen();
|
|
337
343
|
const session = new ClaudeSession({
|
|
338
344
|
config: this.#config,
|
|
339
345
|
context: this.#context,
|
|
340
346
|
options,
|
|
341
347
|
queryFn: this.#queryFn
|
|
342
348
|
});
|
|
343
|
-
this.#sessions.
|
|
344
|
-
return session;
|
|
349
|
+
return this.#sessions.manage(session);
|
|
345
350
|
}
|
|
346
351
|
async dispose() {
|
|
347
|
-
|
|
348
|
-
await session.stop();
|
|
349
|
-
this.#sessions.clear();
|
|
352
|
+
await this.#sessions.dispose();
|
|
350
353
|
}
|
|
351
354
|
}
|
|
352
355
|
/** Probe the claude CLI: version via `claude --version`, auth from credential env. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { gatewayOrigin } from "@velum-labs/routekit-runtime/network";
|
|
2
2
|
import { claudeDriverConfigSchema, createClaudeDriver } from "./driver.js";
|
|
3
3
|
import { launchClaude } from "./launch.js";
|
|
4
4
|
const driver = createClaudeDriver();
|
|
@@ -12,7 +12,7 @@ export const claudeTool = {
|
|
|
12
12
|
installHint: "install Claude Code: https://docs.anthropic.com/en/docs/claude-code/overview",
|
|
13
13
|
authSummary: "Claude Code uses the gateway's Anthropic-compatible surface.",
|
|
14
14
|
setupSnippet: ({ gatewayUrl, model = "gateway-model" }) => [
|
|
15
|
-
`ANTHROPIC_BASE_URL=${
|
|
15
|
+
`ANTHROPIC_BASE_URL=${gatewayOrigin(gatewayUrl)}`,
|
|
16
16
|
"ANTHROPIC_AUTH_TOKEN=routekit",
|
|
17
17
|
`ANTHROPIC_MODEL=${model}`
|
|
18
18
|
].join("\n"),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type ClaudeSettings = Record<string, unknown> & {
|
|
2
|
+
env?: Record<string, unknown>;
|
|
3
|
+
};
|
|
4
|
+
export type FileSnapshot = {
|
|
5
|
+
content: string | null;
|
|
6
|
+
mode: number | null;
|
|
7
|
+
hash: string | null;
|
|
8
|
+
};
|
|
9
|
+
export type InstalledManifest = {
|
|
10
|
+
version: 2;
|
|
11
|
+
state: "installed";
|
|
12
|
+
ownerId: string;
|
|
13
|
+
original: FileSnapshot;
|
|
14
|
+
exactRestoreEligible: boolean;
|
|
15
|
+
installedContentHash: string;
|
|
16
|
+
managedEnvValues: Record<string, string>;
|
|
17
|
+
/** `availableModels` entries contributed by RouteKit, never user entries. */
|
|
18
|
+
managedPickerModels?: string[];
|
|
19
|
+
/** True only when RouteKit created the `availableModels` array itself. */
|
|
20
|
+
managedAvailableModels?: true;
|
|
21
|
+
/** True only when RouteKit created this top-level policy setting. */
|
|
22
|
+
managedEnforceAvailableModels?: true;
|
|
23
|
+
/** Exact top-level apiKeyHelper string contributed by RouteKit. */
|
|
24
|
+
managedApiKeyHelper?: string;
|
|
25
|
+
};
|
|
26
|
+
export type InstallPendingManifest = {
|
|
27
|
+
version: 2;
|
|
28
|
+
state: "install-pending";
|
|
29
|
+
ownerId: string;
|
|
30
|
+
beforeSettings: FileSnapshot;
|
|
31
|
+
beforeManifest: InstalledManifest | null;
|
|
32
|
+
targetSettings: FileSnapshot;
|
|
33
|
+
targetManifest: InstalledManifest;
|
|
34
|
+
};
|
|
35
|
+
export type UninstallPendingManifest = {
|
|
36
|
+
version: 2;
|
|
37
|
+
state: "uninstall-pending";
|
|
38
|
+
ownerId: string;
|
|
39
|
+
beforeSettings: FileSnapshot;
|
|
40
|
+
targetSettings: FileSnapshot;
|
|
41
|
+
};
|
|
42
|
+
export type ClaudeInstallManifest = InstalledManifest | InstallPendingManifest | UninstallPendingManifest;
|
|
43
|
+
export declare const MANAGED_ENV_KEYS: readonly ["ANTHROPIC_BASE_URL", "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT"];
|
|
44
|
+
export declare function hash(content: string): string;
|
|
45
|
+
export declare function serialize(value: unknown): string;
|
|
46
|
+
export declare function snapshot(content: string | null, mode: number | null): FileSnapshot;
|
|
47
|
+
export declare function parseSettings(content: string, configPath: string): ClaudeSettings;
|
|
48
|
+
export declare function unsupportedManifest(manifestPath: string): Error;
|
|
49
|
+
export declare function parseManifest(content: string, manifestPath: string): ClaudeInstallManifest;
|
|
50
|
+
export declare function managedEnv(input: {
|
|
51
|
+
gatewayUrl: string;
|
|
52
|
+
}): Record<string, string>;
|
|
53
|
+
export declare function pickerModels(input: {
|
|
54
|
+
models: readonly string[];
|
|
55
|
+
}): string[];
|
|
56
|
+
export declare function availableModels(settings: ClaudeSettings, configPath: string): string[] | undefined;
|
|
57
|
+
export declare function enforceAvailableModels(settings: ClaudeSettings, configPath: string): boolean | undefined;
|
|
58
|
+
export declare function removableManagedKeys(env: Record<string, unknown>, managedEnvValues: Record<string, string | string[]>, configPath: string): string[];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gatewayOrigin } from "@velum-labs/routekit-runtime/network";
|
|
3
|
+
export const MANAGED_ENV_KEYS = ["ANTHROPIC_BASE_URL", "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT"];
|
|
4
|
+
const RETIRED_MANAGED_ENV_KEYS = [
|
|
5
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
6
|
+
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
|
|
7
|
+
];
|
|
8
|
+
const CLAUDE_PICKER_PREFIX = "anthropic.routekit.";
|
|
9
|
+
export function hash(content) {
|
|
10
|
+
return createHash("sha256").update(content).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
export function serialize(value) {
|
|
13
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
14
|
+
}
|
|
15
|
+
export function snapshot(content, mode) {
|
|
16
|
+
return {
|
|
17
|
+
content,
|
|
18
|
+
mode,
|
|
19
|
+
hash: content === null ? null : hash(content)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function parseSettings(content, configPath) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const detail = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
29
|
+
throw new Error(`your Claude settings (${configPath}) are not valid JSON (${detail})`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
32
|
+
throw new Error(`your Claude settings (${configPath}) must contain a JSON object`);
|
|
33
|
+
}
|
|
34
|
+
const settings = parsed;
|
|
35
|
+
if (settings.env !== undefined &&
|
|
36
|
+
(typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env))) {
|
|
37
|
+
throw new Error(`the "env" field in your Claude settings (${configPath}) must be an object`);
|
|
38
|
+
}
|
|
39
|
+
return settings;
|
|
40
|
+
}
|
|
41
|
+
export function unsupportedManifest(manifestPath) {
|
|
42
|
+
return new Error(`RouteKit's Claude ownership metadata (${manifestPath}) has an unsupported format`);
|
|
43
|
+
}
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function isSnapshot(value) {
|
|
48
|
+
if (!isRecord(value))
|
|
49
|
+
return false;
|
|
50
|
+
const validMode = value.mode === null ||
|
|
51
|
+
(typeof value.mode === "number" &&
|
|
52
|
+
Number.isInteger(value.mode) &&
|
|
53
|
+
value.mode >= 0 &&
|
|
54
|
+
value.mode <= 0o777);
|
|
55
|
+
return ((typeof value.content === "string" || value.content === null) &&
|
|
56
|
+
validMode &&
|
|
57
|
+
(value.content !== null || value.mode === null) &&
|
|
58
|
+
(typeof value.hash === "string" || value.hash === null) &&
|
|
59
|
+
value.hash === (value.content === null ? null : hash(value.content)));
|
|
60
|
+
}
|
|
61
|
+
function isStringRecord(value) {
|
|
62
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
63
|
+
}
|
|
64
|
+
function isStringArray(value) {
|
|
65
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
66
|
+
}
|
|
67
|
+
function isInstalledManifest(value) {
|
|
68
|
+
return (isRecord(value) &&
|
|
69
|
+
value.version === 2 &&
|
|
70
|
+
value.state === "installed" &&
|
|
71
|
+
typeof value.ownerId === "string" &&
|
|
72
|
+
isSnapshot(value.original) &&
|
|
73
|
+
typeof value.exactRestoreEligible === "boolean" &&
|
|
74
|
+
typeof value.installedContentHash === "string" &&
|
|
75
|
+
isStringRecord(value.managedEnvValues) &&
|
|
76
|
+
(value.managedPickerModels === undefined || isStringArray(value.managedPickerModels)) &&
|
|
77
|
+
(value.managedAvailableModels === undefined || value.managedAvailableModels === true) &&
|
|
78
|
+
(value.managedEnforceAvailableModels === undefined ||
|
|
79
|
+
value.managedEnforceAvailableModels === true) &&
|
|
80
|
+
(value.managedApiKeyHelper === undefined || typeof value.managedApiKeyHelper === "string"));
|
|
81
|
+
}
|
|
82
|
+
export function parseManifest(content, manifestPath) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(content);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`RouteKit's Claude ownership metadata (${manifestPath}) is invalid; ` +
|
|
89
|
+
"move it aside and restore settings.json before retrying");
|
|
90
|
+
}
|
|
91
|
+
if (isInstalledManifest(parsed))
|
|
92
|
+
return parsed;
|
|
93
|
+
if (isRecord(parsed) &&
|
|
94
|
+
parsed.version === 2 &&
|
|
95
|
+
parsed.state === "install-pending" &&
|
|
96
|
+
typeof parsed.ownerId === "string" &&
|
|
97
|
+
isSnapshot(parsed.beforeSettings) &&
|
|
98
|
+
(parsed.beforeManifest === null || isInstalledManifest(parsed.beforeManifest)) &&
|
|
99
|
+
isSnapshot(parsed.targetSettings) &&
|
|
100
|
+
isInstalledManifest(parsed.targetManifest)) {
|
|
101
|
+
const pending = parsed;
|
|
102
|
+
if (pending.targetManifest.ownerId !== pending.ownerId ||
|
|
103
|
+
pending.targetManifest.installedContentHash !== pending.targetSettings.hash ||
|
|
104
|
+
(pending.beforeManifest !== null && pending.beforeManifest.ownerId !== pending.ownerId)) {
|
|
105
|
+
throw unsupportedManifest(manifestPath);
|
|
106
|
+
}
|
|
107
|
+
return pending;
|
|
108
|
+
}
|
|
109
|
+
if (isRecord(parsed) &&
|
|
110
|
+
parsed.version === 2 &&
|
|
111
|
+
parsed.state === "uninstall-pending" &&
|
|
112
|
+
typeof parsed.ownerId === "string" &&
|
|
113
|
+
isSnapshot(parsed.beforeSettings) &&
|
|
114
|
+
isSnapshot(parsed.targetSettings)) {
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
throw unsupportedManifest(manifestPath);
|
|
118
|
+
}
|
|
119
|
+
export function managedEnv(input) {
|
|
120
|
+
return {
|
|
121
|
+
ANTHROPIC_BASE_URL: gatewayOrigin(input.gatewayUrl),
|
|
122
|
+
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function pickerModels(input) {
|
|
126
|
+
const models = new Set();
|
|
127
|
+
for (const model of input.models) {
|
|
128
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
129
|
+
throw new Error("RouteKit's Claude picker catalog contains an invalid model id");
|
|
130
|
+
}
|
|
131
|
+
models.add(`${CLAUDE_PICKER_PREFIX}${model}`);
|
|
132
|
+
}
|
|
133
|
+
return [...models];
|
|
134
|
+
}
|
|
135
|
+
export function availableModels(settings, configPath) {
|
|
136
|
+
if (settings.availableModels === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (!isStringArray(settings.availableModels)) {
|
|
139
|
+
throw new Error(`the "availableModels" field in your Claude settings (${configPath}) must be an array of strings`);
|
|
140
|
+
}
|
|
141
|
+
return [...settings.availableModels];
|
|
142
|
+
}
|
|
143
|
+
export function enforceAvailableModels(settings, configPath) {
|
|
144
|
+
if (settings.enforceAvailableModels === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (typeof settings.enforceAvailableModels !== "boolean") {
|
|
147
|
+
throw new Error(`the "enforceAvailableModels" field in your Claude settings (${configPath}) must be a boolean`);
|
|
148
|
+
}
|
|
149
|
+
return settings.enforceAvailableModels;
|
|
150
|
+
}
|
|
151
|
+
export function removableManagedKeys(env, managedEnvValues, configPath) {
|
|
152
|
+
const removable = [];
|
|
153
|
+
for (const [key, expected] of Object.entries(managedEnvValues)) {
|
|
154
|
+
const accepted = Array.isArray(expected) ? expected : [expected];
|
|
155
|
+
if (accepted.includes(String(env[key]))) {
|
|
156
|
+
removable.push(key);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (RETIRED_MANAGED_ENV_KEYS.includes(key))
|
|
160
|
+
continue;
|
|
161
|
+
throw new Error(`your Claude settings changed RouteKit-managed env.${key} in ${configPath}; ` +
|
|
162
|
+
`remove or restore that value before rerunning the install command`);
|
|
163
|
+
}
|
|
164
|
+
return removable;
|
|
165
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { chmodSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { SUBSCRIPTIONS } from "@velum-labs/routekit-registry";
|
|
6
|
-
import { acquireLifecycleLock
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"CLAUDE_CODE_ALWAYS_ENABLE_EFFORT"
|
|
10
|
-
];
|
|
11
|
-
const RETIRED_MANAGED_ENV_KEYS = [
|
|
12
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
13
|
-
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
|
|
14
|
-
];
|
|
5
|
+
import { acquireLifecycleLock } from "@velum-labs/routekit-runtime/service";
|
|
6
|
+
import { writeFileAtomic } from "@velum-labs/routekit-runtime/filesystem";
|
|
7
|
+
import { availableModels, enforceAvailableModels, MANAGED_ENV_KEYS, managedEnv, parseManifest, parseSettings, pickerModels, removableManagedKeys, serialize, snapshot, unsupportedManifest } from "./install-codec.js";
|
|
15
8
|
function reached(boundary) {
|
|
16
9
|
// Deliberately not part of the package API: tests install a same-process
|
|
17
10
|
// throw hook to model termination immediately after an atomic boundary.
|
|
@@ -48,158 +41,6 @@ function paths(input) {
|
|
|
48
41
|
lockPath: join(configDirectory, ".routekit-claude-integration.lock")
|
|
49
42
|
};
|
|
50
43
|
}
|
|
51
|
-
function hash(content) {
|
|
52
|
-
return createHash("sha256").update(content).digest("hex");
|
|
53
|
-
}
|
|
54
|
-
function parseSettings(content, configPath) {
|
|
55
|
-
let parsed;
|
|
56
|
-
try {
|
|
57
|
-
parsed = JSON.parse(content);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
const detail = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
61
|
-
throw new Error(`your Claude settings (${configPath}) are not valid JSON (${detail})`);
|
|
62
|
-
}
|
|
63
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
64
|
-
throw new Error(`your Claude settings (${configPath}) must contain a JSON object`);
|
|
65
|
-
}
|
|
66
|
-
const settings = parsed;
|
|
67
|
-
if (settings.env !== undefined &&
|
|
68
|
-
(typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env))) {
|
|
69
|
-
throw new Error(`the "env" field in your Claude settings (${configPath}) must be an object`);
|
|
70
|
-
}
|
|
71
|
-
return settings;
|
|
72
|
-
}
|
|
73
|
-
function unsupportedManifest(manifestPath) {
|
|
74
|
-
return new Error(`RouteKit's Claude ownership metadata (${manifestPath}) has an unsupported format`);
|
|
75
|
-
}
|
|
76
|
-
function isRecord(value) {
|
|
77
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
-
}
|
|
79
|
-
function isSnapshot(value) {
|
|
80
|
-
if (!isRecord(value))
|
|
81
|
-
return false;
|
|
82
|
-
const validMode = value.mode === null ||
|
|
83
|
-
(typeof value.mode === "number" &&
|
|
84
|
-
Number.isInteger(value.mode) &&
|
|
85
|
-
value.mode >= 0 &&
|
|
86
|
-
value.mode <= 0o777);
|
|
87
|
-
return ((typeof value.content === "string" || value.content === null) &&
|
|
88
|
-
validMode &&
|
|
89
|
-
(value.content !== null || value.mode === null) &&
|
|
90
|
-
(typeof value.hash === "string" || value.hash === null) &&
|
|
91
|
-
value.hash === (value.content === null ? null : hash(value.content)));
|
|
92
|
-
}
|
|
93
|
-
function isStringRecord(value) {
|
|
94
|
-
return (isRecord(value) &&
|
|
95
|
-
Object.values(value).every((entry) => typeof entry === "string"));
|
|
96
|
-
}
|
|
97
|
-
function isStringArray(value) {
|
|
98
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
99
|
-
}
|
|
100
|
-
function isInstalledManifest(value) {
|
|
101
|
-
return (isRecord(value) &&
|
|
102
|
-
value.version === 2 &&
|
|
103
|
-
value.state === "installed" &&
|
|
104
|
-
typeof value.ownerId === "string" &&
|
|
105
|
-
isSnapshot(value.original) &&
|
|
106
|
-
typeof value.exactRestoreEligible === "boolean" &&
|
|
107
|
-
typeof value.installedContentHash === "string" &&
|
|
108
|
-
isStringRecord(value.managedEnvValues) &&
|
|
109
|
-
(value.managedPickerModels === undefined || isStringArray(value.managedPickerModels)) &&
|
|
110
|
-
(value.managedAvailableModels === undefined || value.managedAvailableModels === true) &&
|
|
111
|
-
(value.managedEnforceAvailableModels === undefined ||
|
|
112
|
-
value.managedEnforceAvailableModels === true) &&
|
|
113
|
-
(value.managedApiKeyHelper === undefined || typeof value.managedApiKeyHelper === "string"));
|
|
114
|
-
}
|
|
115
|
-
function parseManifest(content, manifestPath) {
|
|
116
|
-
let parsed;
|
|
117
|
-
try {
|
|
118
|
-
parsed = JSON.parse(content);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
throw new Error(`RouteKit's Claude ownership metadata (${manifestPath}) is invalid; ` +
|
|
122
|
-
"move it aside and restore settings.json before retrying");
|
|
123
|
-
}
|
|
124
|
-
if (isRecord(parsed) &&
|
|
125
|
-
parsed.version === 1 &&
|
|
126
|
-
typeof parsed.ownerId === "string" &&
|
|
127
|
-
(typeof parsed.originalContent === "string" || parsed.originalContent === null) &&
|
|
128
|
-
typeof parsed.exactRestoreEligible === "boolean" &&
|
|
129
|
-
Array.isArray(parsed.installedContentHashes) &&
|
|
130
|
-
parsed.installedContentHashes.every((entry) => typeof entry === "string") &&
|
|
131
|
-
isRecord(parsed.managedEnvValues) &&
|
|
132
|
-
Object.values(parsed.managedEnvValues).every((entry) => Array.isArray(entry) &&
|
|
133
|
-
entry.every((candidate) => typeof candidate === "string"))) {
|
|
134
|
-
return parsed;
|
|
135
|
-
}
|
|
136
|
-
if (isInstalledManifest(parsed))
|
|
137
|
-
return parsed;
|
|
138
|
-
if (isRecord(parsed) &&
|
|
139
|
-
parsed.version === 2 &&
|
|
140
|
-
parsed.state === "install-pending" &&
|
|
141
|
-
typeof parsed.ownerId === "string" &&
|
|
142
|
-
isSnapshot(parsed.beforeSettings) &&
|
|
143
|
-
(parsed.beforeManifest === null ||
|
|
144
|
-
isInstalledManifest(parsed.beforeManifest)) &&
|
|
145
|
-
isSnapshot(parsed.targetSettings) &&
|
|
146
|
-
isInstalledManifest(parsed.targetManifest)) {
|
|
147
|
-
const pending = parsed;
|
|
148
|
-
if (pending.targetManifest.ownerId !== pending.ownerId ||
|
|
149
|
-
pending.targetManifest.installedContentHash !==
|
|
150
|
-
pending.targetSettings.hash ||
|
|
151
|
-
(pending.beforeManifest !== null &&
|
|
152
|
-
pending.beforeManifest.ownerId !== pending.ownerId)) {
|
|
153
|
-
throw unsupportedManifest(manifestPath);
|
|
154
|
-
}
|
|
155
|
-
return pending;
|
|
156
|
-
}
|
|
157
|
-
if (isRecord(parsed) &&
|
|
158
|
-
parsed.version === 2 &&
|
|
159
|
-
parsed.state === "uninstall-pending" &&
|
|
160
|
-
typeof parsed.ownerId === "string" &&
|
|
161
|
-
isSnapshot(parsed.beforeSettings) &&
|
|
162
|
-
isSnapshot(parsed.targetSettings)) {
|
|
163
|
-
return parsed;
|
|
164
|
-
}
|
|
165
|
-
throw unsupportedManifest(manifestPath);
|
|
166
|
-
}
|
|
167
|
-
function serialize(value) {
|
|
168
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
169
|
-
}
|
|
170
|
-
function managedEnv(input) {
|
|
171
|
-
return {
|
|
172
|
-
ANTHROPIC_BASE_URL: trimTrailingSlashes(input.gatewayUrl),
|
|
173
|
-
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1"
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
const CLAUDE_PICKER_PREFIX = "anthropic.routekit.";
|
|
177
|
-
function pickerModels(input) {
|
|
178
|
-
const models = new Set();
|
|
179
|
-
for (const model of input.models) {
|
|
180
|
-
if (typeof model !== "string" || model.length === 0) {
|
|
181
|
-
throw new Error("RouteKit's Claude picker catalog contains an invalid model id");
|
|
182
|
-
}
|
|
183
|
-
models.add(`${CLAUDE_PICKER_PREFIX}${model}`);
|
|
184
|
-
}
|
|
185
|
-
return [...models];
|
|
186
|
-
}
|
|
187
|
-
function availableModels(settings, configPath) {
|
|
188
|
-
if (settings.availableModels === undefined)
|
|
189
|
-
return undefined;
|
|
190
|
-
if (!isStringArray(settings.availableModels)) {
|
|
191
|
-
throw new Error(`the "availableModels" field in your Claude settings (${configPath}) must be an array of strings`);
|
|
192
|
-
}
|
|
193
|
-
return [...settings.availableModels];
|
|
194
|
-
}
|
|
195
|
-
function enforceAvailableModels(settings, configPath) {
|
|
196
|
-
if (settings.enforceAvailableModels === undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
if (typeof settings.enforceAvailableModels !== "boolean") {
|
|
199
|
-
throw new Error(`the "enforceAvailableModels" field in your Claude settings (${configPath}) must be a boolean`);
|
|
200
|
-
}
|
|
201
|
-
return settings.enforceAvailableModels;
|
|
202
|
-
}
|
|
203
44
|
function entryIfExists(path) {
|
|
204
45
|
try {
|
|
205
46
|
return lstatSync(path);
|
|
@@ -244,20 +85,8 @@ function readSnapshot(path, label) {
|
|
|
244
85
|
assertRegularFileIfExists(path, label);
|
|
245
86
|
const entry = entryIfExists(path);
|
|
246
87
|
if (entry === undefined)
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
content,
|
|
251
|
-
mode: entry.mode & 0o777,
|
|
252
|
-
hash: hash(content)
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
function snapshot(content, mode) {
|
|
256
|
-
return {
|
|
257
|
-
content,
|
|
258
|
-
mode,
|
|
259
|
-
hash: content === null ? null : hash(content)
|
|
260
|
-
};
|
|
88
|
+
return snapshot(null, null);
|
|
89
|
+
return snapshot(readFileSync(path, "utf8"), entry.mode & 0o777);
|
|
261
90
|
}
|
|
262
91
|
function writePrivateFile(path, content, label) {
|
|
263
92
|
assertRegularFileIfExists(path, label);
|
|
@@ -322,21 +151,6 @@ function recoverPending(manifest, configPath, manifestPath) {
|
|
|
322
151
|
assertRegularFileIfExists(manifestPath, "Claude ownership metadata");
|
|
323
152
|
rmSync(manifestPath, { force: true });
|
|
324
153
|
}
|
|
325
|
-
function removableManagedKeys(env, managedEnvValues, configPath) {
|
|
326
|
-
const removable = [];
|
|
327
|
-
for (const [key, expected] of Object.entries(managedEnvValues)) {
|
|
328
|
-
const accepted = Array.isArray(expected) ? expected : [expected];
|
|
329
|
-
if (accepted.includes(String(env[key]))) {
|
|
330
|
-
removable.push(key);
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (RETIRED_MANAGED_ENV_KEYS.includes(key))
|
|
334
|
-
continue;
|
|
335
|
-
throw new Error(`your Claude settings changed RouteKit-managed env.${key} in ${configPath}; ` +
|
|
336
|
-
`remove or restore that value before rerunning the install command`);
|
|
337
|
-
}
|
|
338
|
-
return removable;
|
|
339
|
-
}
|
|
340
154
|
const CLAUDE_LOCK_TIMEOUT_MS = 5_000;
|
|
341
155
|
async function withConfigLock(resolved, operation) {
|
|
342
156
|
ensureConfigDirectory(resolved.configDirectory);
|
|
@@ -355,93 +169,24 @@ async function withConfigLock(resolved, operation) {
|
|
|
355
169
|
lock.release();
|
|
356
170
|
}
|
|
357
171
|
}
|
|
358
|
-
function migrateLegacyManifest(manifest, current, configPath, manifestPath) {
|
|
359
|
-
const settings = parseSettings(current.content ?? "{}\n", configPath);
|
|
360
|
-
const env = { ...(settings.env ?? {}) };
|
|
361
|
-
const hasNoManagedValues = Object.keys(manifest.managedEnvValues).every((key) => env[key] === undefined);
|
|
362
|
-
if (current.content === manifest.originalContent &&
|
|
363
|
-
hasNoManagedValues) {
|
|
364
|
-
assertRegularFileIfExists(manifestPath, "Claude ownership metadata");
|
|
365
|
-
rmSync(manifestPath, { force: true });
|
|
366
|
-
return undefined;
|
|
367
|
-
}
|
|
368
|
-
const currentHash = current.hash;
|
|
369
|
-
if (currentHash === null) {
|
|
370
|
-
throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) cannot ` +
|
|
371
|
-
"match missing settings; refusing to overwrite them");
|
|
372
|
-
}
|
|
373
|
-
let recognizedContent = manifest.installedContentHashes.includes(currentHash);
|
|
374
|
-
if (!recognizedContent) {
|
|
375
|
-
for (const key of RETIRED_MANAGED_ENV_KEYS) {
|
|
376
|
-
const accepted = manifest.managedEnvValues[key];
|
|
377
|
-
const value = env[key];
|
|
378
|
-
if (accepted === undefined || accepted.includes(String(value)))
|
|
379
|
-
continue;
|
|
380
|
-
for (const candidate of accepted) {
|
|
381
|
-
const candidateSettings = {
|
|
382
|
-
...settings,
|
|
383
|
-
env: { ...env, [key]: candidate }
|
|
384
|
-
};
|
|
385
|
-
if (manifest.installedContentHashes.includes(hash(serialize(candidateSettings)))) {
|
|
386
|
-
recognizedContent = true;
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (!recognizedContent) {
|
|
393
|
-
throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) does not ` +
|
|
394
|
-
"match the current settings; refusing to overwrite them");
|
|
395
|
-
}
|
|
396
|
-
const selectedValues = {};
|
|
397
|
-
for (const [key, accepted] of Object.entries(manifest.managedEnvValues)) {
|
|
398
|
-
const value = env[key];
|
|
399
|
-
if (value === undefined || !accepted.includes(String(value))) {
|
|
400
|
-
if (RETIRED_MANAGED_ENV_KEYS.includes(key))
|
|
401
|
-
continue;
|
|
402
|
-
throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) cannot ` +
|
|
403
|
-
`safely identify the current env.${key} value; refusing to overwrite settings`);
|
|
404
|
-
}
|
|
405
|
-
selectedValues[key] = String(value);
|
|
406
|
-
}
|
|
407
|
-
const migrated = {
|
|
408
|
-
version: 2,
|
|
409
|
-
state: "installed",
|
|
410
|
-
ownerId: manifest.ownerId,
|
|
411
|
-
original: snapshot(manifest.originalContent, null),
|
|
412
|
-
exactRestoreEligible: manifest.exactRestoreEligible &&
|
|
413
|
-
manifest.installedContentHashes.includes(currentHash),
|
|
414
|
-
installedContentHash: currentHash,
|
|
415
|
-
managedEnvValues: selectedValues
|
|
416
|
-
};
|
|
417
|
-
writeManifest(manifestPath, migrated);
|
|
418
|
-
return migrated;
|
|
419
|
-
}
|
|
420
172
|
export async function installClaudeIntegration(input) {
|
|
421
173
|
const resolved = paths({
|
|
422
174
|
ownerId: input.owner.id,
|
|
423
|
-
...(input.claudeConfigDir !== undefined
|
|
424
|
-
? { claudeConfigDir: input.claudeConfigDir }
|
|
425
|
-
: {})
|
|
175
|
+
...(input.claudeConfigDir !== undefined ? { claudeConfigDir: input.claudeConfigDir } : {})
|
|
426
176
|
});
|
|
427
177
|
return await withConfigLock(resolved, () => {
|
|
428
178
|
const { configPath, manifestPath } = resolved;
|
|
429
179
|
let manifest = currentManifest(manifestPath);
|
|
430
|
-
if (manifest !== undefined &&
|
|
431
|
-
manifest.ownerId !== input.owner.id) {
|
|
180
|
+
if (manifest !== undefined && manifest.ownerId !== input.owner.id) {
|
|
432
181
|
throw new Error(`Claude ownership metadata in ${manifestPath} belongs to another integration`);
|
|
433
182
|
}
|
|
434
|
-
if (manifest?.version === 2 &&
|
|
435
|
-
manifest.state !== "installed") {
|
|
183
|
+
if (manifest?.version === 2 && manifest.state !== "installed") {
|
|
436
184
|
recoverPending(manifest, configPath, manifestPath);
|
|
437
185
|
manifest = currentManifest(manifestPath);
|
|
438
186
|
}
|
|
439
187
|
const beforeSettings = readSnapshot(configPath, "Claude settings");
|
|
440
188
|
let previousManifest;
|
|
441
|
-
if (manifest?.
|
|
442
|
-
previousManifest = migrateLegacyManifest(manifest, beforeSettings, configPath, manifestPath);
|
|
443
|
-
}
|
|
444
|
-
else if (manifest?.state === "installed") {
|
|
189
|
+
if (manifest?.state === "installed") {
|
|
445
190
|
previousManifest = manifest;
|
|
446
191
|
}
|
|
447
192
|
else if (manifest !== undefined) {
|
|
@@ -450,14 +195,12 @@ export async function installClaudeIntegration(input) {
|
|
|
450
195
|
const settings = parseSettings(beforeSettings.content ?? "{}\n", configPath);
|
|
451
196
|
const env = { ...(settings.env ?? {}) };
|
|
452
197
|
const nextManaged = managedEnv(input);
|
|
453
|
-
if (settings.apiKeyHelper !== undefined &&
|
|
454
|
-
typeof settings.apiKeyHelper !== "string") {
|
|
198
|
+
if (settings.apiKeyHelper !== undefined && typeof settings.apiKeyHelper !== "string") {
|
|
455
199
|
throw new Error(`the "apiKeyHelper" field in your Claude settings (${configPath}) must be a string`);
|
|
456
200
|
}
|
|
457
201
|
const currentApiKeyHelper = settings.apiKeyHelper;
|
|
458
202
|
const previousApiKeyHelper = previousManifest?.managedApiKeyHelper;
|
|
459
|
-
if (previousApiKeyHelper !== undefined &&
|
|
460
|
-
currentApiKeyHelper !== previousApiKeyHelper) {
|
|
203
|
+
if (previousApiKeyHelper !== undefined && currentApiKeyHelper !== previousApiKeyHelper) {
|
|
461
204
|
throw new Error(`your Claude settings changed RouteKit-managed apiKeyHelper in ${configPath}; ` +
|
|
462
205
|
"restore that value before rerunning the install command");
|
|
463
206
|
}
|
|
@@ -508,12 +251,8 @@ export async function installClaudeIntegration(input) {
|
|
|
508
251
|
...settings,
|
|
509
252
|
env,
|
|
510
253
|
availableModels: nextPickerModels,
|
|
511
|
-
...(input.apiKeyHelper !== undefined
|
|
512
|
-
|
|
513
|
-
: {}),
|
|
514
|
-
...(nextManagedEnforceAvailableModels === true
|
|
515
|
-
? { enforceAvailableModels: true }
|
|
516
|
-
: {})
|
|
254
|
+
...(input.apiKeyHelper !== undefined ? { apiKeyHelper: input.apiKeyHelper } : {}),
|
|
255
|
+
...(nextManagedEnforceAvailableModels === true ? { enforceAvailableModels: true } : {})
|
|
517
256
|
};
|
|
518
257
|
if (input.apiKeyHelper === undefined && previousApiKeyHelper !== undefined) {
|
|
519
258
|
delete nextSettings.apiKeyHelper;
|
|
@@ -539,9 +278,7 @@ export async function installClaudeIntegration(input) {
|
|
|
539
278
|
...(nextManagedEnforceAvailableModels === true
|
|
540
279
|
? { managedEnforceAvailableModels: true }
|
|
541
280
|
: {}),
|
|
542
|
-
...(input.apiKeyHelper !== undefined
|
|
543
|
-
? { managedApiKeyHelper: input.apiKeyHelper }
|
|
544
|
-
: {})
|
|
281
|
+
...(input.apiKeyHelper !== undefined ? { managedApiKeyHelper: input.apiKeyHelper } : {})
|
|
545
282
|
};
|
|
546
283
|
const pendingManifest = {
|
|
547
284
|
version: 2,
|
|
@@ -559,8 +296,7 @@ export async function installClaudeIntegration(input) {
|
|
|
559
296
|
applySnapshot(configPath, targetSettings, "Claude settings");
|
|
560
297
|
reached("install-settings");
|
|
561
298
|
const pending = currentManifest(manifestPath);
|
|
562
|
-
if (pending?.version !== 2 ||
|
|
563
|
-
pending.state !== "install-pending") {
|
|
299
|
+
if (pending?.version !== 2 || pending.state !== "install-pending") {
|
|
564
300
|
throw new Error(`Claude ownership metadata changed unexpectedly during install (${manifestPath})`);
|
|
565
301
|
}
|
|
566
302
|
writeManifest(manifestPath, targetManifest);
|
|
@@ -599,10 +335,7 @@ export async function uninstallClaudeIntegration(input) {
|
|
|
599
335
|
}
|
|
600
336
|
const beforeSettings = readSnapshot(configPath, "Claude settings");
|
|
601
337
|
let installed;
|
|
602
|
-
if (manifest.
|
|
603
|
-
installed = migrateLegacyManifest(manifest, beforeSettings, configPath, manifestPath);
|
|
604
|
-
}
|
|
605
|
-
else if (manifest.state === "installed") {
|
|
338
|
+
if (manifest.state === "installed") {
|
|
606
339
|
installed = manifest;
|
|
607
340
|
}
|
|
608
341
|
else {
|
|
@@ -611,8 +344,7 @@ export async function uninstallClaudeIntegration(input) {
|
|
|
611
344
|
if (installed === undefined)
|
|
612
345
|
return { configPath, removed: false };
|
|
613
346
|
let targetSettings;
|
|
614
|
-
if (installed.exactRestoreEligible &&
|
|
615
|
-
beforeSettings.hash === installed.installedContentHash) {
|
|
347
|
+
if (installed.exactRestoreEligible && beforeSettings.hash === installed.installedContentHash) {
|
|
616
348
|
targetSettings = installed.original;
|
|
617
349
|
}
|
|
618
350
|
else if (beforeSettings.content !== null) {
|
|
@@ -665,8 +397,7 @@ export async function uninstallClaudeIntegration(input) {
|
|
|
665
397
|
applySnapshot(configPath, targetSettings, "Claude settings");
|
|
666
398
|
reached("uninstall-settings");
|
|
667
399
|
const pending = currentManifest(manifestPath);
|
|
668
|
-
if (pending?.version !== 2 ||
|
|
669
|
-
pending.state !== "uninstall-pending") {
|
|
400
|
+
if (pending?.version !== 2 || pending.state !== "uninstall-pending") {
|
|
670
401
|
throw new Error(`Claude ownership metadata changed unexpectedly during uninstall (${manifestPath})`);
|
|
671
402
|
}
|
|
672
403
|
rmSync(manifestPath, { force: true });
|
package/dist/launch.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasFlag } from "@velum-labs/routekit-runtime/args";
|
|
2
|
+
import { gatewayOrigin } from "@velum-labs/routekit-runtime/network";
|
|
3
|
+
import { spawnTool } from "@velum-labs/routekit-runtime/process";
|
|
2
4
|
export function claudeEnv(gatewayUrl, authToken) {
|
|
3
5
|
return {
|
|
4
|
-
ANTHROPIC_BASE_URL: gatewayUrl,
|
|
6
|
+
ANTHROPIC_BASE_URL: gatewayOrigin(gatewayUrl),
|
|
5
7
|
ANTHROPIC_AUTH_TOKEN: authToken ?? "routekit",
|
|
6
8
|
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1",
|
|
7
9
|
...(process.env.CLAUDE_CONFIG_DIR !== undefined
|
|
@@ -23,25 +25,16 @@ export function claudeAgentsJson(profiles) {
|
|
|
23
25
|
}
|
|
24
26
|
])));
|
|
25
27
|
}
|
|
26
|
-
function hasAgentsArg(args) {
|
|
27
|
-
return args.some((arg) => arg === "--agents" || arg.startsWith("--agents="));
|
|
28
|
-
}
|
|
29
|
-
function hasModelArg(args) {
|
|
30
|
-
return args.some((arg) => arg === "--model" || arg.startsWith("--model="));
|
|
31
|
-
}
|
|
32
|
-
function hasEffortArg(args) {
|
|
33
|
-
return args.some((arg) => arg === "--effort" || arg.startsWith("--effort="));
|
|
34
|
-
}
|
|
35
28
|
export function claudeLaunchArgs(ctx) {
|
|
36
29
|
const args = [...ctx.spec.args];
|
|
37
|
-
if (!
|
|
30
|
+
if (!hasFlag(args, "--model")) {
|
|
38
31
|
args.unshift("--model", claudeModelId(ctx.spec.defaultModel));
|
|
39
|
-
if (ctx.spec.reasoning?.mode === "effort" && !
|
|
32
|
+
if (ctx.spec.reasoning?.mode === "effort" && !hasFlag(args, "--effort")) {
|
|
40
33
|
args.unshift("--effort", ctx.spec.reasoning.effort);
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
const profiles = ctx.spec.agentProfiles ?? [];
|
|
44
|
-
if (profiles.length > 0 && !
|
|
37
|
+
if (profiles.length > 0 && !hasFlag(args, "--agents")) {
|
|
45
38
|
args.push("--agents", claudeAgentsJson(profiles));
|
|
46
39
|
}
|
|
47
40
|
return args;
|
package/dist/test/driver.test.js
CHANGED
|
@@ -115,6 +115,35 @@ test("claude driver forwards opaque effort through SDK options", async () => {
|
|
|
115
115
|
await instance.dispose();
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
|
+
test("claude driver releases the turn when query setup throws", async () => {
|
|
119
|
+
const delegate = scriptedQuery("claude-setup-retry");
|
|
120
|
+
let attempts = 0;
|
|
121
|
+
const setupDriver = createClaudeDriver({
|
|
122
|
+
queryFn: (input) => {
|
|
123
|
+
attempts += 1;
|
|
124
|
+
if (attempts === 1)
|
|
125
|
+
throw new Error("query setup failed");
|
|
126
|
+
return delegate(input);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const instance = await setupDriver.createInstance(setupDriver.configSchema.parse({ command: "claude" }));
|
|
130
|
+
try {
|
|
131
|
+
const session = await instance.startSession({ cwd: process.cwd() });
|
|
132
|
+
await assert.rejects(async () => {
|
|
133
|
+
for await (const _event of session.sendTurn({ prompt: "first" })) {
|
|
134
|
+
// Drain.
|
|
135
|
+
}
|
|
136
|
+
}, /query setup failed/);
|
|
137
|
+
const events = [];
|
|
138
|
+
for await (const event of session.sendTurn({ prompt: "second" })) {
|
|
139
|
+
events.push(event);
|
|
140
|
+
}
|
|
141
|
+
assert.equal(events.find((event) => event.type === "turn.completed")?.endReason, "completed");
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
await instance.dispose();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
118
147
|
test("claude driver auto-approves tools under the automation policy", async () => {
|
|
119
148
|
const instance = await driver.createInstance(driver.configSchema.parse({ command: "claude" }));
|
|
120
149
|
try {
|
|
@@ -489,195 +489,6 @@ test("updating a v2 install preserves a user-edited token", async () => {
|
|
|
489
489
|
rmSync(configDirectory, { recursive: true, force: true });
|
|
490
490
|
}
|
|
491
491
|
});
|
|
492
|
-
test("updating a legacy install removes its formerly managed ANTHROPIC_MODEL", async () => {
|
|
493
|
-
const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-legacy-model-"));
|
|
494
|
-
const configPath = join(configDirectory, "settings.json");
|
|
495
|
-
const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
|
|
496
|
-
const original = '{"theme":"light"}\n';
|
|
497
|
-
const installed = `${JSON.stringify({
|
|
498
|
-
theme: "light",
|
|
499
|
-
env: {
|
|
500
|
-
ANTHROPIC_BASE_URL: "http://127.0.0.1:7000",
|
|
501
|
-
ANTHROPIC_AUTH_TOKEN: "routekit",
|
|
502
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
503
|
-
ANTHROPIC_MODEL: "claude-code/legacy"
|
|
504
|
-
}
|
|
505
|
-
}, null, 2)}\n`;
|
|
506
|
-
writeFileSync(configPath, installed);
|
|
507
|
-
writeFileSync(manifestPath, `${JSON.stringify({
|
|
508
|
-
version: 1,
|
|
509
|
-
ownerId: OWNER.id,
|
|
510
|
-
originalContent: original,
|
|
511
|
-
exactRestoreEligible: true,
|
|
512
|
-
installedContentHashes: [
|
|
513
|
-
createHash("sha256").update(installed).digest("hex")
|
|
514
|
-
],
|
|
515
|
-
managedEnvValues: {
|
|
516
|
-
ANTHROPIC_BASE_URL: ["http://127.0.0.1:7000"],
|
|
517
|
-
ANTHROPIC_AUTH_TOKEN: ["routekit"],
|
|
518
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: ["1"],
|
|
519
|
-
ANTHROPIC_MODEL: ["claude-code/legacy"]
|
|
520
|
-
}
|
|
521
|
-
}, null, 2)}\n`);
|
|
522
|
-
try {
|
|
523
|
-
await install(configDirectory, "http://127.0.0.1:7001");
|
|
524
|
-
const settings = JSON.parse(readFileSync(configPath, "utf8"));
|
|
525
|
-
assert.equal("ANTHROPIC_MODEL" in settings.env, false);
|
|
526
|
-
await uninstallClaudeIntegration({
|
|
527
|
-
ownerId: OWNER.id,
|
|
528
|
-
claudeConfigDir: configDirectory
|
|
529
|
-
});
|
|
530
|
-
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
531
|
-
}
|
|
532
|
-
finally {
|
|
533
|
-
rmSync(configDirectory, { recursive: true, force: true });
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
function writeInterruptedLegacyUpdate(configDirectory, phase) {
|
|
537
|
-
const configPath = join(configDirectory, "settings.json");
|
|
538
|
-
const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
|
|
539
|
-
const original = '{"theme":"light"}\n';
|
|
540
|
-
const content = (baseUrl, authToken, model) => `${JSON.stringify({
|
|
541
|
-
theme: "light",
|
|
542
|
-
env: {
|
|
543
|
-
ANTHROPIC_BASE_URL: baseUrl,
|
|
544
|
-
ANTHROPIC_AUTH_TOKEN: authToken,
|
|
545
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
546
|
-
ANTHROPIC_MODEL: model
|
|
547
|
-
}
|
|
548
|
-
}, null, 2)}\n`;
|
|
549
|
-
const before = content("http://127.0.0.1:7000", "before-token", "claude-code/before");
|
|
550
|
-
const after = content("http://127.0.0.1:7001", "after-token", "claude-code/after");
|
|
551
|
-
writeFileSync(configPath, phase === "before-settings" ? before : after);
|
|
552
|
-
writeFileSync(manifestPath, `${JSON.stringify({
|
|
553
|
-
version: 1,
|
|
554
|
-
ownerId: OWNER.id,
|
|
555
|
-
originalContent: original,
|
|
556
|
-
exactRestoreEligible: true,
|
|
557
|
-
installedContentHashes: [before, after].map((value) => createHash("sha256").update(value).digest("hex")),
|
|
558
|
-
managedEnvValues: {
|
|
559
|
-
ANTHROPIC_BASE_URL: [
|
|
560
|
-
"http://127.0.0.1:7000",
|
|
561
|
-
"http://127.0.0.1:7001"
|
|
562
|
-
],
|
|
563
|
-
ANTHROPIC_AUTH_TOKEN: ["before-token", "after-token"],
|
|
564
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: ["1"],
|
|
565
|
-
ANTHROPIC_MODEL: [
|
|
566
|
-
"claude-code/before",
|
|
567
|
-
"claude-code/after"
|
|
568
|
-
]
|
|
569
|
-
}
|
|
570
|
-
}, null, 2)}\n`);
|
|
571
|
-
return { configPath, manifestPath, original };
|
|
572
|
-
}
|
|
573
|
-
test("install migrates interrupted v1 updates before and after settings", async () => {
|
|
574
|
-
for (const phase of ["before-settings", "after-settings"]) {
|
|
575
|
-
const configDirectory = mkdtempSync(join(tmpdir(), `routekit-claude-v1-install-${phase}-`));
|
|
576
|
-
const { configPath, manifestPath, original } = writeInterruptedLegacyUpdate(configDirectory, phase);
|
|
577
|
-
try {
|
|
578
|
-
const result = await install(configDirectory, "http://127.0.0.1:7002", "final-token");
|
|
579
|
-
assert.equal(result.action, "updated");
|
|
580
|
-
const settings = JSON.parse(readFileSync(configPath, "utf8"));
|
|
581
|
-
assert.equal(settings.env.ANTHROPIC_BASE_URL, "http://127.0.0.1:7002");
|
|
582
|
-
assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
|
|
583
|
-
assert.equal("ANTHROPIC_MODEL" in settings.env, false);
|
|
584
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
585
|
-
assert.deepEqual({ version: manifest.version, state: manifest.state }, { version: 2, state: "installed" });
|
|
586
|
-
assert.equal("ANTHROPIC_AUTH_TOKEN" in manifest.managedEnvValues, false);
|
|
587
|
-
assert.equal(readFileSync(manifestPath, "utf8").includes("final-token"), false);
|
|
588
|
-
assert.equal(readFileSync(manifestPath, "utf8").includes("before-token"), false);
|
|
589
|
-
assert.equal(readFileSync(manifestPath, "utf8").includes("after-token"), false);
|
|
590
|
-
await uninstallClaudeIntegration({
|
|
591
|
-
ownerId: OWNER.id,
|
|
592
|
-
claudeConfigDir: configDirectory
|
|
593
|
-
});
|
|
594
|
-
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
595
|
-
}
|
|
596
|
-
finally {
|
|
597
|
-
rmSync(configDirectory, { recursive: true, force: true });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
test("uninstall migrates interrupted v1 updates before and after settings", async () => {
|
|
602
|
-
for (const phase of ["before-settings", "after-settings"]) {
|
|
603
|
-
const configDirectory = mkdtempSync(join(tmpdir(), `routekit-claude-v1-uninstall-${phase}-`));
|
|
604
|
-
const { configPath, manifestPath, original } = writeInterruptedLegacyUpdate(configDirectory, phase);
|
|
605
|
-
try {
|
|
606
|
-
assert.equal((await uninstallClaudeIntegration({
|
|
607
|
-
ownerId: OWNER.id,
|
|
608
|
-
claudeConfigDir: configDirectory
|
|
609
|
-
})).removed, true);
|
|
610
|
-
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
611
|
-
assert.equal(existsSync(manifestPath), false);
|
|
612
|
-
}
|
|
613
|
-
finally {
|
|
614
|
-
rmSync(configDirectory, { recursive: true, force: true });
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
test("legacy migration refuses unrecognized settings without overwriting", async () => {
|
|
619
|
-
const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-v1-external-"));
|
|
620
|
-
const { configPath, manifestPath } = writeInterruptedLegacyUpdate(configDirectory, "after-settings");
|
|
621
|
-
const external = '{"external":true}\n';
|
|
622
|
-
writeFileSync(configPath, external);
|
|
623
|
-
const manifest = readFileSync(manifestPath, "utf8");
|
|
624
|
-
try {
|
|
625
|
-
await assert.rejects(install(configDirectory), /does not match the current settings/);
|
|
626
|
-
assert.equal(readFileSync(configPath, "utf8"), external);
|
|
627
|
-
assert.equal(readFileSync(manifestPath, "utf8"), manifest);
|
|
628
|
-
}
|
|
629
|
-
finally {
|
|
630
|
-
rmSync(configDirectory, { recursive: true, force: true });
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
test("untouched original settings discard a stale pre-install v1 manifest", async () => {
|
|
634
|
-
const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-v1-original-"));
|
|
635
|
-
const configPath = join(configDirectory, "settings.json");
|
|
636
|
-
const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
|
|
637
|
-
const original = '{"theme":"light"}\n';
|
|
638
|
-
const target = `${JSON.stringify({
|
|
639
|
-
theme: "light",
|
|
640
|
-
env: {
|
|
641
|
-
ANTHROPIC_BASE_URL: "http://127.0.0.1:7000",
|
|
642
|
-
ANTHROPIC_AUTH_TOKEN: "routekit",
|
|
643
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
|
|
644
|
-
}
|
|
645
|
-
}, null, 2)}\n`;
|
|
646
|
-
const writeStaleManifest = () => {
|
|
647
|
-
writeFileSync(manifestPath, `${JSON.stringify({
|
|
648
|
-
version: 1,
|
|
649
|
-
ownerId: OWNER.id,
|
|
650
|
-
originalContent: original,
|
|
651
|
-
exactRestoreEligible: true,
|
|
652
|
-
installedContentHashes: [original, target].map((value) => createHash("sha256").update(value).digest("hex")),
|
|
653
|
-
managedEnvValues: {
|
|
654
|
-
ANTHROPIC_BASE_URL: ["http://127.0.0.1:7000"],
|
|
655
|
-
ANTHROPIC_AUTH_TOKEN: ["routekit"],
|
|
656
|
-
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: ["1"]
|
|
657
|
-
}
|
|
658
|
-
}, null, 2)}\n`);
|
|
659
|
-
};
|
|
660
|
-
writeFileSync(configPath, original);
|
|
661
|
-
try {
|
|
662
|
-
writeStaleManifest();
|
|
663
|
-
assert.equal((await uninstallClaudeIntegration({
|
|
664
|
-
ownerId: OWNER.id,
|
|
665
|
-
claudeConfigDir: configDirectory
|
|
666
|
-
})).removed, false);
|
|
667
|
-
assert.equal(existsSync(manifestPath), false);
|
|
668
|
-
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
669
|
-
writeStaleManifest();
|
|
670
|
-
assert.equal((await install(configDirectory)).action, "installed");
|
|
671
|
-
await uninstallClaudeIntegration({
|
|
672
|
-
ownerId: OWNER.id,
|
|
673
|
-
claudeConfigDir: configDirectory
|
|
674
|
-
});
|
|
675
|
-
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
676
|
-
}
|
|
677
|
-
finally {
|
|
678
|
-
rmSync(configDirectory, { recursive: true, force: true });
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
492
|
test("new config directories are 0700 and exact restore preserves settings mode", async () => {
|
|
682
493
|
const root = mkdtempSync(join(tmpdir(), "routekit-claude-mode-"));
|
|
683
494
|
const createdDirectory = join(root, "nested", "claude");
|
package/dist/test/launch.test.js
CHANGED
|
@@ -78,6 +78,9 @@ test("Claude launcher forwards an explicit isolated config directory", () => {
|
|
|
78
78
|
process.env.CLAUDE_CONFIG_DIR = previous;
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
|
+
test("Claude launcher strips an OpenAI API suffix from the gateway origin", () => {
|
|
82
|
+
assert.equal(claudeEnv("http://127.0.0.1:8080/v1/").ANTHROPIC_BASE_URL, "http://127.0.0.1:8080");
|
|
83
|
+
});
|
|
81
84
|
test("claudeLaunchArgs adds profiles unless the user supplied agents", () => {
|
|
82
85
|
const args = claudeLaunchArgs(context(["--verbose"]));
|
|
83
86
|
assert.deepEqual(args.slice(0, 4), [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-tool-claude",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -14,6 +14,18 @@
|
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./driver": {
|
|
19
|
+
"types": "./dist/driver.d.ts",
|
|
20
|
+
"default": "./dist/driver.js"
|
|
21
|
+
},
|
|
22
|
+
"./install": {
|
|
23
|
+
"types": "./dist/install.d.ts",
|
|
24
|
+
"default": "./dist/install.js"
|
|
25
|
+
},
|
|
26
|
+
"./launch": {
|
|
27
|
+
"types": "./dist/launch.d.ts",
|
|
28
|
+
"default": "./dist/launch.js"
|
|
17
29
|
}
|
|
18
30
|
},
|
|
19
31
|
"files": [
|
|
@@ -30,11 +42,11 @@
|
|
|
30
42
|
"@anthropic-ai/sdk": "0.115.0",
|
|
31
43
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
32
44
|
"zod": "4.4.3",
|
|
33
|
-
"@velum-labs/routekit-contracts": "0.
|
|
34
|
-
"@velum-labs/routekit-harness-core": "0.
|
|
35
|
-
"@velum-labs/routekit-
|
|
36
|
-
"@velum-labs/routekit-
|
|
37
|
-
"@velum-labs/routekit-
|
|
45
|
+
"@velum-labs/routekit-contracts": "1.0.0",
|
|
46
|
+
"@velum-labs/routekit-harness-core": "1.0.0",
|
|
47
|
+
"@velum-labs/routekit-runtime": "1.0.0",
|
|
48
|
+
"@velum-labs/routekit-tools": "1.0.0",
|
|
49
|
+
"@velum-labs/routekit-registry": "1.0.0"
|
|
38
50
|
},
|
|
39
51
|
"keywords": [
|
|
40
52
|
"llm",
|
|
@@ -44,7 +56,7 @@
|
|
|
44
56
|
"adapter"
|
|
45
57
|
],
|
|
46
58
|
"scripts": {
|
|
47
|
-
"build": "tsc -
|
|
59
|
+
"build": "tsc -p tsconfig.json",
|
|
48
60
|
"clean": "tsc -b --clean",
|
|
49
61
|
"test": "node --test \"dist/test/*.test.js\""
|
|
50
62
|
}
|