@velum-labs/routekit-tool-claude 0.16.8 → 0.17.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/README.md CHANGED
@@ -5,6 +5,7 @@ Product-neutral Claude Code launcher and canonical harness driver.
5
5
  ## Architecture
6
6
 
7
7
  This package owns the one Claude Code profile serializer, launcher, and driver.
8
+ The native Claude client owns its transcript store and session lifecycle.
8
9
 
9
10
  ## Usage
10
11
 
@@ -18,3 +19,9 @@ import { claudeTool } from "@velum-labs/routekit-tool-claude";
18
19
 
19
20
  - CLI reference: [../../docs/cli.md](../../docs/cli.md)
20
21
  - Maintainer reference: [../../docs/typescript-reference.md](../../docs/typescript-reference.md)
22
+
23
+ ## Native client ownership
24
+
25
+ RouteKit does not observe, store, resume, or delete Claude sessions. Use Claude's
26
+ native history and resume/delete commands directly; launcher arguments after `--`
27
+ are forwarded to Claude unchanged.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ToolIntegration } from "@velum-labs/routekit-tools";
2
2
  export declare const claudeTool: ToolIntegration;
3
- export { claudeDriverConfigSchema, createClaudeDriver } from "./driver.js";
4
3
  export type { ClaudeDriverConfig, ClaudeDriverOptions, ClaudeQueryFn } from "./driver.js";
5
- export { installClaudeIntegration, uninstallClaudeIntegration } from "./install.js";
4
+ export { claudeDriverConfigSchema, createClaudeDriver } from "./driver.js";
6
5
  export type { ClaudeInstallInput, ClaudeInstallOwner, ClaudeInstallResult } from "./install.js";
6
+ export { claudeIntegrationConfigPath, installClaudeIntegration, uninstallClaudeIntegration } from "./install.js";
7
7
  export { claudeAgentsJson, claudeEnv, claudeLaunchArgs, launchClaude } from "./launch.js";
package/dist/index.js CHANGED
@@ -30,5 +30,5 @@ export const claudeTool = {
30
30
  }
31
31
  };
32
32
  export { claudeDriverConfigSchema, createClaudeDriver } from "./driver.js";
33
- export { installClaudeIntegration, uninstallClaudeIntegration } from "./install.js";
33
+ export { claudeIntegrationConfigPath, installClaudeIntegration, uninstallClaudeIntegration } from "./install.js";
34
34
  export { claudeAgentsJson, claudeEnv, claudeLaunchArgs, launchClaude } from "./launch.js";
package/dist/install.d.ts CHANGED
@@ -7,7 +7,6 @@ export type ClaudeInstallOwner = {
7
7
  };
8
8
  export type ClaudeInstallInput = {
9
9
  gatewayUrl: string;
10
- authToken?: string;
11
10
  owner: ClaudeInstallOwner;
12
11
  claudeConfigDir?: string;
13
12
  };
@@ -16,6 +15,7 @@ export type ClaudeInstallResult = {
16
15
  action: "installed" | "updated";
17
16
  managedKeys: string[];
18
17
  };
18
+ export declare function claudeIntegrationConfigPath(claudeConfigDir?: string): string;
19
19
  export declare function installClaudeIntegration(input: ClaudeInstallInput): Promise<ClaudeInstallResult>;
20
20
  export declare function uninstallClaudeIntegration(input: {
21
21
  ownerId: string;
package/dist/install.js CHANGED
@@ -6,9 +6,9 @@ import { SUBSCRIPTIONS } from "@velum-labs/routekit-registry";
6
6
  import { acquireLifecycleLock, trimTrailingSlashes, writeFileAtomic } from "@velum-labs/routekit-runtime";
7
7
  const MANAGED_ENV_KEYS = [
8
8
  "ANTHROPIC_BASE_URL",
9
- "ANTHROPIC_AUTH_TOKEN",
10
9
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
11
10
  ];
11
+ const RETIRED_MANAGED_ENV_KEYS = ["ANTHROPIC_AUTH_TOKEN"];
12
12
  function reached(boundary) {
13
13
  // Deliberately not part of the package API: tests install a same-process
14
14
  // throw hook to model termination immediately after an atomic boundary.
@@ -31,14 +31,16 @@ function defaultClaudeConfigDir() {
31
31
  : registryPath;
32
32
  return dirname(configPath);
33
33
  }
34
+ export function claudeIntegrationConfigPath(claudeConfigDir) {
35
+ return join(claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? defaultClaudeConfigDir(), "settings.json");
36
+ }
34
37
  function paths(input) {
35
38
  assertSafeOwnerId(input.ownerId);
36
- const configDirectory = input.claudeConfigDir ??
37
- process.env.CLAUDE_CONFIG_DIR ??
38
- defaultClaudeConfigDir();
39
+ const configPath = claudeIntegrationConfigPath(input.claudeConfigDir);
40
+ const configDirectory = dirname(configPath);
39
41
  return {
40
42
  configDirectory,
41
- configPath: join(configDirectory, "settings.json"),
43
+ configPath,
42
44
  manifestPath: join(configDirectory, `.${input.ownerId}-integration.json`),
43
45
  lockPath: join(configDirectory, ".routekit-claude-integration.lock")
44
46
  };
@@ -157,7 +159,6 @@ function serialize(value) {
157
159
  function managedEnv(input) {
158
160
  return {
159
161
  ANTHROPIC_BASE_URL: trimTrailingSlashes(input.gatewayUrl),
160
- ANTHROPIC_AUTH_TOKEN: input.authToken ?? "routekit",
161
162
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
162
163
  };
163
164
  }
@@ -283,14 +284,20 @@ function recoverPending(manifest, configPath, manifestPath) {
283
284
  assertRegularFileIfExists(manifestPath, "Claude ownership metadata");
284
285
  rmSync(manifestPath, { force: true });
285
286
  }
286
- function assertManagedValuesUnchanged(env, managedEnvValues, configPath) {
287
+ function removableManagedKeys(env, managedEnvValues, configPath) {
288
+ const removable = [];
287
289
  for (const [key, expected] of Object.entries(managedEnvValues)) {
288
290
  const accepted = Array.isArray(expected) ? expected : [expected];
289
- if (!accepted.includes(String(env[key]))) {
290
- throw new Error(`your Claude settings changed RouteKit-managed env.${key} in ${configPath}; ` +
291
- `remove or restore that value before rerunning the install command`);
291
+ if (accepted.includes(String(env[key]))) {
292
+ removable.push(key);
293
+ continue;
292
294
  }
295
+ if (RETIRED_MANAGED_ENV_KEYS.includes(key))
296
+ continue;
297
+ throw new Error(`your Claude settings changed RouteKit-managed env.${key} in ${configPath}; ` +
298
+ `remove or restore that value before rerunning the install command`);
293
299
  }
300
+ return removable;
294
301
  }
295
302
  const CLAUDE_LOCK_TIMEOUT_MS = 5_000;
296
303
  async function withConfigLock(resolved, operation) {
@@ -320,8 +327,31 @@ function migrateLegacyManifest(manifest, current, configPath, manifestPath) {
320
327
  rmSync(manifestPath, { force: true });
321
328
  return undefined;
322
329
  }
323
- if (current.hash === null ||
324
- !manifest.installedContentHashes.includes(current.hash)) {
330
+ const currentHash = current.hash;
331
+ if (currentHash === null) {
332
+ throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) cannot ` +
333
+ "match missing settings; refusing to overwrite them");
334
+ }
335
+ let recognizedContent = manifest.installedContentHashes.includes(currentHash);
336
+ if (!recognizedContent) {
337
+ for (const key of RETIRED_MANAGED_ENV_KEYS) {
338
+ const accepted = manifest.managedEnvValues[key];
339
+ const value = env[key];
340
+ if (accepted === undefined || accepted.includes(String(value)))
341
+ continue;
342
+ for (const candidate of accepted) {
343
+ const candidateSettings = {
344
+ ...settings,
345
+ env: { ...env, [key]: candidate }
346
+ };
347
+ if (manifest.installedContentHashes.includes(hash(serialize(candidateSettings)))) {
348
+ recognizedContent = true;
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ if (!recognizedContent) {
325
355
  throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) does not ` +
326
356
  "match the current settings; refusing to overwrite them");
327
357
  }
@@ -329,6 +359,8 @@ function migrateLegacyManifest(manifest, current, configPath, manifestPath) {
329
359
  for (const [key, accepted] of Object.entries(manifest.managedEnvValues)) {
330
360
  const value = env[key];
331
361
  if (value === undefined || !accepted.includes(String(value))) {
362
+ if (RETIRED_MANAGED_ENV_KEYS.includes(key))
363
+ continue;
332
364
  throw new Error(`RouteKit's legacy Claude ownership metadata (${manifestPath}) cannot ` +
333
365
  `safely identify the current env.${key} value; refusing to overwrite settings`);
334
366
  }
@@ -339,8 +371,9 @@ function migrateLegacyManifest(manifest, current, configPath, manifestPath) {
339
371
  state: "installed",
340
372
  ownerId: manifest.ownerId,
341
373
  original: snapshot(manifest.originalContent, null),
342
- exactRestoreEligible: manifest.exactRestoreEligible,
343
- installedContentHash: current.hash,
374
+ exactRestoreEligible: manifest.exactRestoreEligible &&
375
+ manifest.installedContentHashes.includes(currentHash),
376
+ installedContentHash: currentHash,
344
377
  managedEnvValues: selectedValues
345
378
  };
346
379
  writeManifest(manifestPath, migrated);
@@ -387,12 +420,11 @@ export async function installClaudeIntegration(input) {
387
420
  }
388
421
  }
389
422
  }
390
- else {
391
- assertManagedValuesUnchanged(env, previousManifest.managedEnvValues, configPath);
392
- }
393
- for (const key of Object.keys(previousManifest?.managedEnvValues ?? {})) {
423
+ const removableKeys = previousManifest === undefined
424
+ ? []
425
+ : removableManagedKeys(env, previousManifest.managedEnvValues, configPath);
426
+ for (const key of removableKeys)
394
427
  delete env[key];
395
- }
396
428
  Object.assign(env, nextManaged);
397
429
  const nextSettings = { ...settings, env };
398
430
  const nextContent = serialize(nextSettings);
@@ -15,10 +15,9 @@ const OWNER = {
15
15
  uninstallCommand: "example uninstall claude",
16
16
  startCommand: "example serve"
17
17
  };
18
- function install(configDirectory, gatewayUrl = "http://127.0.0.1:9999/", authToken) {
18
+ function install(configDirectory, gatewayUrl = "http://127.0.0.1:9999/", _authToken) {
19
19
  return installClaudeIntegration({
20
20
  gatewayUrl,
21
- ...(authToken !== undefined ? { authToken } : {}),
22
21
  owner: OWNER,
23
22
  claudeConfigDir: configDirectory
24
23
  });
@@ -32,21 +31,23 @@ test("Claude managed install updates and restores the exact original settings",
32
31
  const installed = await install(configDirectory, "http://127.0.0.1:9999/", "gateway-secret");
33
32
  assert.equal(installed.action, "installed");
34
33
  assert.deepEqual(installed.managedKeys.sort(), [
35
- "ANTHROPIC_AUTH_TOKEN",
36
34
  "ANTHROPIC_BASE_URL",
37
35
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
38
36
  ]);
39
37
  const settings = JSON.parse(readFileSync(configPath, "utf8"));
40
38
  assert.deepEqual(settings.permissions, { allow: ["Bash(git status)"] });
41
39
  assert.equal(settings.env.ANTHROPIC_BASE_URL, "http://127.0.0.1:9999");
42
- assert.equal(settings.env.ANTHROPIC_AUTH_TOKEN, "gateway-secret");
40
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
43
41
  assert.equal(statSync(configPath).mode & 0o777, 0o600);
44
42
  const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
43
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
45
44
  assert.deepEqual({
46
- version: JSON.parse(readFileSync(manifestPath, "utf8")).version,
47
- state: JSON.parse(readFileSync(manifestPath, "utf8")).state,
45
+ version: manifest.version,
46
+ state: manifest.state,
48
47
  mode: statSync(manifestPath).mode & 0o777
49
48
  }, { version: 2, state: "installed", mode: 0o600 });
49
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in manifest.managedEnvValues, false);
50
+ assert.equal(readFileSync(manifestPath, "utf8").includes("gateway-secret"), false);
50
51
  assert.equal((await install(configDirectory, "http://127.0.0.1:8888", "updated-secret")).action, "updated");
51
52
  assert.equal(JSON.parse(readFileSync(configPath, "utf8")).env.ANTHROPIC_BASE_URL, "http://127.0.0.1:8888");
52
53
  assert.equal((await uninstallClaudeIntegration({
@@ -324,6 +325,71 @@ test("install never persists or takes ownership of ANTHROPIC_MODEL", async () =>
324
325
  rmSync(configDirectory, { recursive: true, force: true });
325
326
  }
326
327
  });
328
+ test("updating a v2 install removes an owned token without persisting its replacement", async () => {
329
+ const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-v2-token-"));
330
+ const configPath = join(configDirectory, "settings.json");
331
+ const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
332
+ const original = '{"theme":"light"}\n';
333
+ const installed = `${JSON.stringify({
334
+ theme: "light",
335
+ env: {
336
+ ANTHROPIC_BASE_URL: "http://127.0.0.1:7000",
337
+ ANTHROPIC_AUTH_TOKEN: "old-secret",
338
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
339
+ }
340
+ }, null, 2)}\n`;
341
+ writeFileSync(configPath, installed);
342
+ writeFileSync(manifestPath, `${JSON.stringify({
343
+ version: 2,
344
+ state: "installed",
345
+ ownerId: OWNER.id,
346
+ original: {
347
+ content: original,
348
+ mode: 0o644,
349
+ hash: createHash("sha256").update(original).digest("hex")
350
+ },
351
+ exactRestoreEligible: true,
352
+ installedContentHash: createHash("sha256").update(installed).digest("hex"),
353
+ managedEnvValues: {
354
+ ANTHROPIC_BASE_URL: "http://127.0.0.1:7000",
355
+ ANTHROPIC_AUTH_TOKEN: "old-secret",
356
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
357
+ }
358
+ }, null, 2)}\n`);
359
+ try {
360
+ await install(configDirectory, "http://127.0.0.1:7001", "new-secret");
361
+ const settings = JSON.parse(readFileSync(configPath, "utf8"));
362
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
363
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
364
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in manifest.managedEnvValues, false);
365
+ assert.equal(readFileSync(manifestPath, "utf8").includes("old-secret"), false);
366
+ assert.equal(readFileSync(manifestPath, "utf8").includes("new-secret"), false);
367
+ }
368
+ finally {
369
+ rmSync(configDirectory, { recursive: true, force: true });
370
+ }
371
+ });
372
+ test("updating a v2 install preserves a user-edited token", async () => {
373
+ const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-v2-user-token-"));
374
+ const configPath = join(configDirectory, "settings.json");
375
+ try {
376
+ await install(configDirectory, "http://127.0.0.1:7000");
377
+ const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
378
+ const settings = JSON.parse(readFileSync(configPath, "utf8"));
379
+ settings.env.ANTHROPIC_AUTH_TOKEN = "user-secret";
380
+ writeFileSync(configPath, `${JSON.stringify(settings, null, 2)}\n`);
381
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
382
+ manifest.managedEnvValues.ANTHROPIC_AUTH_TOKEN = "old-secret";
383
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
384
+ await install(configDirectory, "http://127.0.0.1:7001", "new-secret");
385
+ assert.equal(JSON.parse(readFileSync(configPath, "utf8")).env.ANTHROPIC_AUTH_TOKEN, "user-secret");
386
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in
387
+ JSON.parse(readFileSync(manifestPath, "utf8")).managedEnvValues, false);
388
+ }
389
+ finally {
390
+ rmSync(configDirectory, { recursive: true, force: true });
391
+ }
392
+ });
327
393
  test("updating a legacy install removes its formerly managed ANTHROPIC_MODEL", async () => {
328
394
  const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-legacy-model-"));
329
395
  const configPath = join(configDirectory, "settings.json");
@@ -414,12 +480,14 @@ test("install migrates interrupted v1 updates before and after settings", async
414
480
  assert.equal(result.action, "updated");
415
481
  const settings = JSON.parse(readFileSync(configPath, "utf8"));
416
482
  assert.equal(settings.env.ANTHROPIC_BASE_URL, "http://127.0.0.1:7002");
417
- assert.equal(settings.env.ANTHROPIC_AUTH_TOKEN, "final-token");
483
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
418
484
  assert.equal("ANTHROPIC_MODEL" in settings.env, false);
419
- assert.deepEqual({
420
- version: JSON.parse(readFileSync(manifestPath, "utf8")).version,
421
- state: JSON.parse(readFileSync(manifestPath, "utf8")).state
422
- }, { version: 2, state: "installed" });
485
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
486
+ assert.deepEqual({ version: manifest.version, state: manifest.state }, { version: 2, state: "installed" });
487
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in manifest.managedEnvValues, false);
488
+ assert.equal(readFileSync(manifestPath, "utf8").includes("final-token"), false);
489
+ assert.equal(readFileSync(manifestPath, "utf8").includes("before-token"), false);
490
+ assert.equal(readFileSync(manifestPath, "utf8").includes("after-token"), false);
423
491
  await uninstallClaudeIntegration({
424
492
  ownerId: OWNER.id,
425
493
  claudeConfigDir: configDirectory
@@ -35,7 +35,10 @@ test("claudeAgentsJson serializes generic profiles", () => {
35
35
  });
36
36
  });
37
37
  test("Claude launcher projects claude-code models to native picker ids", () => {
38
- assert.deepEqual(claudeLaunchArgs(context([], [], "claude-code/claude-sonnet-4-6")), ["--model", "claude-sonnet-4-6"]);
38
+ assert.deepEqual(claudeLaunchArgs(context([], [], "claude-code/claude-sonnet-4-6")), [
39
+ "--model",
40
+ "claude-sonnet-4-6"
41
+ ]);
39
42
  assert.deepEqual(JSON.parse(claudeAgentsJson([
40
43
  {
41
44
  id: "native",
@@ -77,22 +80,17 @@ test("Claude launcher forwards an explicit isolated config directory", () => {
77
80
  });
78
81
  test("claudeLaunchArgs adds profiles unless the user supplied agents", () => {
79
82
  const args = claudeLaunchArgs(context(["--verbose"]));
80
- assert.deepEqual(args.slice(0, 4), [
81
- "--model",
82
- "claude-opaque-model",
83
- "--verbose",
84
- "--agents"
85
- ]);
83
+ assert.deepEqual(args.slice(0, 4), ["--model", "claude-opaque-model", "--verbose", "--agents"]);
86
84
  assert.deepEqual(claudeLaunchArgs(context(["--agents={}"])), [
87
85
  "--model",
88
86
  "claude-opaque-model",
89
87
  "--agents={}"
90
88
  ]);
91
- assert.deepEqual(claudeLaunchArgs(context([], [])), [
89
+ assert.deepEqual(claudeLaunchArgs(context([], [])), ["--model", "claude-opaque-model"]);
90
+ assert.deepEqual(claudeLaunchArgs(context(["--model", "claude-user-selected"], [])), [
92
91
  "--model",
93
- "claude-opaque-model"
92
+ "claude-user-selected"
94
93
  ]);
95
- assert.deepEqual(claudeLaunchArgs(context(["--model", "claude-user-selected"], [])), ["--model", "claude-user-selected"]);
96
94
  });
97
95
  test("Claude launcher projects validated effort onto the picker model id", () => {
98
96
  const withEffort = {
@@ -102,10 +100,7 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
102
100
  reasoning: { mode: "effort", effort: "high" }
103
101
  }
104
102
  };
105
- assert.deepEqual(claudeLaunchArgs(withEffort), [
106
- "--model",
107
- "claude-sonnet-4-6:high"
108
- ]);
103
+ assert.deepEqual(claudeLaunchArgs(withEffort), ["--model", "claude-sonnet-4-6:high"]);
109
104
  const crossProvider = {
110
105
  ...context([], [], "codex/gpt-5.5"),
111
106
  spec: {
@@ -113,10 +108,7 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
113
108
  reasoning: { mode: "effort", effort: "deep" }
114
109
  }
115
110
  };
116
- assert.deepEqual(claudeLaunchArgs(crossProvider), [
117
- "--model",
118
- "claude-codex/gpt-5.5:deep"
119
- ]);
111
+ assert.deepEqual(claudeLaunchArgs(crossProvider), ["--model", "claude-codex/gpt-5.5:deep"]);
120
112
  assert.deepEqual(claudeLaunchArgs({
121
113
  ...withEffort,
122
114
  spec: {
@@ -125,3 +117,12 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
125
117
  }
126
118
  }), ["--model", "claude-user-selected"]);
127
119
  });
120
+ test("Claude launches keep native session flags under native-client control", () => {
121
+ assert.deepEqual(claudeLaunchArgs(context(["--continue", "--resume", "native-id"], [])), [
122
+ "--model",
123
+ "claude-opaque-model",
124
+ "--continue",
125
+ "--resume",
126
+ "native-id"
127
+ ]);
128
+ });
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.16.8",
4
+ "version": "0.17.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -28,11 +28,11 @@
28
28
  "dependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "0.3.198",
30
30
  "zod": "4.4.3",
31
- "@velum-labs/routekit-contracts": "0.16.8",
32
- "@velum-labs/routekit-harness-core": "0.16.8",
33
- "@velum-labs/routekit-registry": "0.16.8",
34
- "@velum-labs/routekit-runtime": "0.16.8",
35
- "@velum-labs/routekit-tools": "0.16.8"
31
+ "@velum-labs/routekit-contracts": "0.17.0",
32
+ "@velum-labs/routekit-harness-core": "0.17.0",
33
+ "@velum-labs/routekit-registry": "0.17.0",
34
+ "@velum-labs/routekit-runtime": "0.17.0",
35
+ "@velum-labs/routekit-tools": "0.17.0"
36
36
  },
37
37
  "keywords": [
38
38
  "llm",