@velum-labs/routekit-tool-claude 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/install.d.ts CHANGED
@@ -9,6 +9,11 @@ export type ClaudeInstallInput = {
9
9
  gatewayUrl: string;
10
10
  /** Canonical RouteKit catalog ids permitted by the current model policy. */
11
11
  models: readonly string[];
12
+ /**
13
+ * Command Claude Code invokes when it needs the gateway bearer token. The
14
+ * helper output is never persisted in settings.json.
15
+ */
16
+ apiKeyHelper?: string;
12
17
  owner: ClaudeInstallOwner;
13
18
  claudeConfigDir?: string;
14
19
  };
package/dist/install.js CHANGED
@@ -109,7 +109,8 @@ function isInstalledManifest(value) {
109
109
  (value.managedPickerModels === undefined || isStringArray(value.managedPickerModels)) &&
110
110
  (value.managedAvailableModels === undefined || value.managedAvailableModels === true) &&
111
111
  (value.managedEnforceAvailableModels === undefined ||
112
- value.managedEnforceAvailableModels === true));
112
+ value.managedEnforceAvailableModels === true) &&
113
+ (value.managedApiKeyHelper === undefined || typeof value.managedApiKeyHelper === "string"));
113
114
  }
114
115
  function parseManifest(content, manifestPath) {
115
116
  let parsed;
@@ -449,6 +450,23 @@ export async function installClaudeIntegration(input) {
449
450
  const settings = parseSettings(beforeSettings.content ?? "{}\n", configPath);
450
451
  const env = { ...(settings.env ?? {}) };
451
452
  const nextManaged = managedEnv(input);
453
+ if (settings.apiKeyHelper !== undefined &&
454
+ typeof settings.apiKeyHelper !== "string") {
455
+ throw new Error(`the "apiKeyHelper" field in your Claude settings (${configPath}) must be a string`);
456
+ }
457
+ const currentApiKeyHelper = settings.apiKeyHelper;
458
+ const previousApiKeyHelper = previousManifest?.managedApiKeyHelper;
459
+ if (previousApiKeyHelper !== undefined &&
460
+ currentApiKeyHelper !== previousApiKeyHelper) {
461
+ throw new Error(`your Claude settings changed RouteKit-managed apiKeyHelper in ${configPath}; ` +
462
+ "restore that value before rerunning the install command");
463
+ }
464
+ if (previousManifest === undefined &&
465
+ input.apiKeyHelper !== undefined &&
466
+ currentApiKeyHelper !== undefined) {
467
+ throw new Error(`your Claude settings already define apiKeyHelper in ${configPath}; ` +
468
+ `remove it before rerunning \`${input.owner.installCommand}\``);
469
+ }
452
470
  const desiredPickerModels = pickerModels(input);
453
471
  if (desiredPickerModels.length === 0) {
454
472
  throw new Error("RouteKit's Claude picker catalog cannot be empty");
@@ -490,10 +508,16 @@ export async function installClaudeIntegration(input) {
490
508
  ...settings,
491
509
  env,
492
510
  availableModels: nextPickerModels,
511
+ ...(input.apiKeyHelper !== undefined
512
+ ? { apiKeyHelper: input.apiKeyHelper }
513
+ : {}),
493
514
  ...(nextManagedEnforceAvailableModels === true
494
515
  ? { enforceAvailableModels: true }
495
516
  : {})
496
517
  };
518
+ if (input.apiKeyHelper === undefined && previousApiKeyHelper !== undefined) {
519
+ delete nextSettings.apiKeyHelper;
520
+ }
497
521
  const nextContent = serialize(nextSettings);
498
522
  const targetSettings = snapshot(nextContent, 0o600);
499
523
  const exactRestoreEligible = previousManifest === undefined
@@ -514,6 +538,9 @@ export async function installClaudeIntegration(input) {
514
538
  : {}),
515
539
  ...(nextManagedEnforceAvailableModels === true
516
540
  ? { managedEnforceAvailableModels: true }
541
+ : {}),
542
+ ...(input.apiKeyHelper !== undefined
543
+ ? { managedApiKeyHelper: input.apiKeyHelper }
517
544
  : {})
518
545
  };
519
546
  const pendingManifest = {
@@ -543,6 +570,7 @@ export async function installClaudeIntegration(input) {
543
570
  action: previousManifest === undefined ? "installed" : "updated",
544
571
  managedKeys: [
545
572
  ...Object.keys(nextManaged),
573
+ ...(input.apiKeyHelper !== undefined ? ["apiKeyHelper"] : []),
546
574
  "availableModels",
547
575
  ...(nextManagedEnforceAvailableModels === true ? ["enforceAvailableModels"] : [])
548
576
  ]
@@ -595,6 +623,10 @@ export async function uninstallClaudeIntegration(input) {
595
623
  delete env[key];
596
624
  }
597
625
  const next = { ...settings };
626
+ if (installed.managedApiKeyHelper !== undefined &&
627
+ settings.apiKeyHelper === installed.managedApiKeyHelper) {
628
+ delete next.apiKeyHelper;
629
+ }
598
630
  if (Object.keys(env).length === 0)
599
631
  delete next.env;
600
632
  else
@@ -67,6 +67,52 @@ test("Claude managed install updates and restores the exact original settings",
67
67
  rmSync(configDirectory, { recursive: true, force: true });
68
68
  }
69
69
  });
70
+ test("Claude install owns a pull-based apiKeyHelper without storing its output", async () => {
71
+ const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-helper-"));
72
+ const configPath = join(configDirectory, "settings.json");
73
+ const original = '{"theme":"dark"}\n';
74
+ writeFileSync(configPath, original);
75
+ try {
76
+ const helper = "'/opt/routekit/node' '/opt/routekit/index.js' credential get";
77
+ const installed = await installClaudeIntegration({
78
+ gatewayUrl: "http://127.0.0.1:9999",
79
+ models: ["openai/mock-model"],
80
+ apiKeyHelper: helper,
81
+ owner: OWNER,
82
+ claudeConfigDir: configDirectory
83
+ });
84
+ assert.ok(installed.managedKeys.includes("apiKeyHelper"));
85
+ assert.equal(JSON.parse(readFileSync(configPath, "utf8")).apiKeyHelper, helper);
86
+ const settings = JSON.parse(readFileSync(configPath, "utf8"));
87
+ settings.theme = "light";
88
+ writeFileSync(configPath, `${JSON.stringify(settings, null, 2)}\n`);
89
+ await uninstallClaudeIntegration({
90
+ ownerId: OWNER.id,
91
+ claudeConfigDir: configDirectory
92
+ });
93
+ assert.equal(JSON.parse(readFileSync(configPath, "utf8")).apiKeyHelper, undefined);
94
+ assert.equal(JSON.parse(readFileSync(configPath, "utf8")).theme, "light");
95
+ }
96
+ finally {
97
+ rmSync(configDirectory, { recursive: true, force: true });
98
+ }
99
+ });
100
+ test("Claude install refuses to overwrite a user apiKeyHelper", async () => {
101
+ const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-helper-conflict-"));
102
+ writeFileSync(join(configDirectory, "settings.json"), '{"apiKeyHelper":"/usr/local/bin/user-helper"}\n');
103
+ try {
104
+ await assert.rejects(installClaudeIntegration({
105
+ gatewayUrl: "http://127.0.0.1:9999",
106
+ models: ["openai/mock-model"],
107
+ apiKeyHelper: "/usr/local/bin/routekit-helper",
108
+ owner: OWNER,
109
+ claudeConfigDir: configDirectory
110
+ }), /already define apiKeyHelper/);
111
+ }
112
+ finally {
113
+ rmSync(configDirectory, { recursive: true, force: true });
114
+ }
115
+ });
70
116
  test("Claude uninstall preserves user edits made after install", async () => {
71
117
  const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-edits-"));
72
118
  const configPath = join(configDirectory, "settings.json");
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.18.0",
4
+ "version": "0.18.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -30,11 +30,11 @@
30
30
  "@anthropic-ai/sdk": "0.115.0",
31
31
  "@modelcontextprotocol/sdk": "1.29.0",
32
32
  "zod": "4.4.3",
33
- "@velum-labs/routekit-contracts": "0.18.0",
34
- "@velum-labs/routekit-harness-core": "0.18.0",
35
- "@velum-labs/routekit-registry": "0.18.0",
36
- "@velum-labs/routekit-runtime": "0.18.0",
37
- "@velum-labs/routekit-tools": "0.18.0"
33
+ "@velum-labs/routekit-contracts": "0.18.2",
34
+ "@velum-labs/routekit-harness-core": "0.18.2",
35
+ "@velum-labs/routekit-registry": "0.18.2",
36
+ "@velum-labs/routekit-runtime": "0.18.2",
37
+ "@velum-labs/routekit-tools": "0.18.2"
38
38
  },
39
39
  "keywords": [
40
40
  "llm",