@juspay/neurolink 12.13.0 → 12.14.1

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.
@@ -88,5 +88,6 @@ export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
88
88
  export declare const proxyTelemetryCommand: CommandModule<object, ProxyTelemetryArgs>;
89
89
  export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
90
90
  export declare const proxySetupCommand: CommandModule;
91
+ export declare function buildProxyLaunchdPlist(port: number, host: string, envFile?: string, configFile?: string): string;
91
92
  export declare const proxyInstallCommand: CommandModule;
92
93
  export declare const proxyUninstallCommand: CommandModule;
@@ -1,23 +1,15 @@
1
- /**
2
- * Proxy CLI Commands for NeuroLink
3
- *
4
- * Implements commands for managing the Claude multi-account proxy:
5
- * - neurolink proxy start — Start the proxy server
6
- * - neurolink proxy status — Show proxy status (accounts, sessions, routing)
7
- *
8
- * The proxy creates a NeuroLink instance and builds a Hono app that registers
9
- * Claude-compatible proxy routes. All requests flow through ctx.neurolink
10
- * (generate/stream), with an optional ModelRouter for model remapping.
11
- */
1
+ import { initializeProxyOtelLogs, routeProxyConsoleToOtel, flushProxyOtelLogs, shutdownProxyOtelLogs, isProxyOtelOnly, withProxyOtelLogShutdown, } from "../../proxy/otelLogSink.js";
2
+ import { writeFileAtomic } from "../proxy-clients/snapshot.js";
12
3
  import { spawn } from "node:child_process";
13
4
  import { homedir } from "node:os";
14
- import { dirname, join, resolve } from "node:path";
5
+ import { dirname, join } from "node:path";
15
6
  import { stripVTControlCharacters } from "node:util";
16
7
  import chalk from "chalk";
17
8
  import ora from "ora";
18
9
  import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../proxy/proxyHealth.js";
19
10
  import { logger } from "../../utils/logger.js";
20
11
  import { applyAllClients, restoreAllClients, } from "../proxy-clients/registry.js";
12
+ import { resolveProxyConfigPath } from "../../proxy/proxyConfig.js";
21
13
  import { redactUrlsInText, sanitizeForLog, } from "../../utils/logSanitize.js";
22
14
  import { withTimeout } from "../../utils/async/withTimeout.js";
23
15
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
@@ -2397,6 +2389,8 @@ function registerProxyShutdownHandlers(params) {
2397
2389
  }
2398
2390
  try {
2399
2391
  const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../services/server/ai/observability/instrumentation.js");
2392
+ await flushProxyOtelLogs();
2393
+ await shutdownProxyOtelLogs();
2400
2394
  await flushOpenTelemetry();
2401
2395
  await shutdownOpenTelemetry();
2402
2396
  }
@@ -2696,7 +2690,9 @@ async function startProxyRuntime(params) {
2696
2690
  logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`);
2697
2691
  }
2698
2692
  if (!isDev) {
2699
- for (const result of await applyAllClients(url)) {
2693
+ for (const result of await applyAllClients(url, {
2694
+ configPath: params.configPath,
2695
+ })) {
2700
2696
  if (result.error) {
2701
2697
  // Visible, not debug-level. A client whose config could not be written
2702
2698
  // will keep talking to its own upstream, which looks like the proxy
@@ -2755,6 +2751,9 @@ async function startProxyRuntime(params) {
2755
2751
  * serving-process exits.
2756
2752
  */
2757
2753
  async function runLaunchdProxySupervisor(argv, spinner) {
2754
+ await loadProxyStartEnv(argv, spinner);
2755
+ initializeProxyOtelLogs("supervisor");
2756
+ routeProxyConsoleToOtel();
2758
2757
  await ensureProxyStartAllowed(spinner);
2759
2758
  const entryScript = process.argv[1];
2760
2759
  if (!entryScript) {
@@ -2868,6 +2867,8 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2868
2867
  updaterSupervisor.stop();
2869
2868
  await rollingServer.close();
2870
2869
  await flushProxyLifecycleEvents().catch((error) => logger.warn(String(error)));
2870
+ await flushProxyOtelLogs().catch(() => undefined);
2871
+ await shutdownProxyOtelLogs().catch(() => undefined);
2871
2872
  const supervisorState = loadProxySupervisorState();
2872
2873
  if (supervisorState?.pid === process.pid) {
2873
2874
  clearProxySupervisorState();
@@ -2948,14 +2949,14 @@ async function startProxyCommandHandler(argv) {
2948
2949
  env: baseEnv,
2949
2950
  });
2950
2951
  const loadedEnvFile = await loadProxyStartEnv(argv, spinner);
2952
+ initializeProxyOtelLogs("worker");
2953
+ routeProxyConsoleToOtel();
2951
2954
  // Reuse upstream TCP connections (longer keep-alive + bounded pool) instead
2952
2955
  // of opening a new flow per request — cuts outbound flow churn through host
2953
2956
  // content-filters. Runs once, after env load so it can be tuned via env.
2954
2957
  configureProxyKeepAliveDispatcher();
2955
2958
  const { neurolink, logsDir } = await createProxyNeurolinkRuntime(devPaths?.logsDir);
2956
- const configPath = argv.config
2957
- ? resolve(argv.config)
2958
- : join(homedir(), ".neurolink", "proxy-config.yaml");
2959
+ const configPath = resolveProxyConfigPath(argv.config);
2959
2960
  const runtimeConfigStore = await ProxyRuntimeConfigStore.create({
2960
2961
  configPath,
2961
2962
  configRequired: Boolean(argv.config),
@@ -3012,6 +3013,7 @@ async function startProxyCommandHandler(argv) {
3012
3013
  passthrough,
3013
3014
  logsDir,
3014
3015
  runtimeConfigStore,
3016
+ configPath,
3015
3017
  });
3016
3018
  }
3017
3019
  catch (error) {
@@ -3590,7 +3592,9 @@ export const proxyGuardCommand = {
3590
3592
  default: true,
3591
3593
  });
3592
3594
  },
3593
- handler: async (argv) => {
3595
+ handler: withProxyOtelLogShutdown(async (argv) => {
3596
+ initializeProxyOtelLogs("updater");
3597
+ routeProxyConsoleToOtel();
3594
3598
  const host = argv.host ?? "127.0.0.1";
3595
3599
  const port = argv.port ?? 55669;
3596
3600
  const parentPid = Number(argv.parentPid);
@@ -3914,6 +3918,8 @@ export const proxyGuardCommand = {
3914
3918
  logger.always(`[updater] update successful: now running ${result.latestVersion}`);
3915
3919
  persistUpdaterState("record successful update", () => recordSuccessfulUpdate(result.latestVersion));
3916
3920
  // The replacement proxy starts a worker running the new version.
3921
+ await flushProxyOtelLogs().catch(() => undefined);
3922
+ await shutdownProxyOtelLogs().catch(() => undefined);
3917
3923
  process.exit(0);
3918
3924
  }
3919
3925
  else {
@@ -4081,7 +4087,7 @@ export const proxyGuardCommand = {
4081
4087
  if (cleared && !argv.quiet) {
4082
4088
  logger.always(`[proxy] fail-open guard removed stale ${expectedBaseUrl} from Claude settings`);
4083
4089
  }
4084
- },
4090
+ }),
4085
4091
  };
4086
4092
  // =============================================================================
4087
4093
  // PROXY SETUP COMMAND
@@ -4174,7 +4180,9 @@ export const proxySetupCommand = {
4174
4180
  const nextStep = stepNum + 1;
4175
4181
  console.info(chalk.blue(`\nStep ${nextStep}:`) + " Configuring Claude Code...");
4176
4182
  const url = `http://127.0.0.1:${port}`;
4177
- for (const result of await applyAllClients(url)) {
4183
+ for (const result of await applyAllClients(url, {
4184
+ configPath: resolveProxyConfigPath(argv.config),
4185
+ })) {
4178
4186
  if (result.error) {
4179
4187
  console.info(chalk.yellow(` ⚠ Could not auto-configure ${result.displayName}: ${result.error.message}`));
4180
4188
  // Claude Code is the one client whose manual fallback is a single
@@ -4253,7 +4261,7 @@ function buildLaunchdPath() {
4253
4261
  }
4254
4262
  return [...segments].join(":");
4255
4263
  }
4256
- function buildPlist(port, host, envFile, configFile) {
4264
+ export function buildProxyLaunchdPlist(port, host, envFile, configFile) {
4257
4265
  // The plist invokes the trampoline script (a tiny shell wrapper at
4258
4266
  // ~/.neurolink/bin/neurolink-proxy) which re-resolves the real
4259
4267
  // `neurolink` binary via PATH on every launch. This way, launchd
@@ -4269,6 +4277,20 @@ function buildPlist(port, host, envFile, configFile) {
4269
4277
  <string>--config</string>
4270
4278
  <string>${escapeXml(configFile)}</string>`
4271
4279
  : "";
4280
+ const otelEnvironment = isProxyOtelOnly()
4281
+ ? [
4282
+ "NEUROLINK_PROXY_LOG_SINK",
4283
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
4284
+ "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
4285
+ "OTEL_EXPORTER_OTLP_HEADERS",
4286
+ "OTEL_EXPORTER_OTLP_LOGS_HEADERS",
4287
+ "OTEL_SERVICE_NAME",
4288
+ "NEUROLINK_PROXY_SESSION_SECRET",
4289
+ ]
4290
+ .filter((name) => process.env[name] !== undefined)
4291
+ .map((name) => ` <key>${name}</key>\n <string>${escapeXml(process.env[name])}</string>`)
4292
+ .join("\n")
4293
+ : "";
4272
4294
  return `<?xml version="1.0" encoding="UTF-8"?>
4273
4295
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
4274
4296
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -4307,10 +4329,10 @@ ${configArgs}
4307
4329
  <integer>45</integer>
4308
4330
 
4309
4331
  <key>StandardOutPath</key>
4310
- <string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
4332
+ <string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
4311
4333
 
4312
4334
  <key>StandardErrorPath</key>
4313
- <string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
4335
+ <string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
4314
4336
 
4315
4337
  <key>EnvironmentVariables</key>
4316
4338
  <dict>
@@ -4318,6 +4340,7 @@ ${configArgs}
4318
4340
  <string>${buildLaunchdPath()}</string>
4319
4341
  <key>HOME</key>
4320
4342
  <string>${homedir()}</string>
4343
+ ${otelEnvironment}
4321
4344
  </dict>
4322
4345
  </dict>
4323
4346
  </plist>`;
@@ -4358,15 +4381,13 @@ export const proxyInstallCommand = {
4358
4381
  console.info(chalk.yellow("On Linux, use systemd. On Windows, use Task Scheduler."));
4359
4382
  process.exit(1);
4360
4383
  }
4361
- const { writeFileSync, mkdirSync, existsSync } = await import("fs");
4384
+ const { mkdirSync, existsSync, chmodSync } = await import("fs");
4362
4385
  const envResolution = resolveProxyEnvFile({
4363
4386
  explicitEnvFile: argv.envFile,
4364
4387
  });
4365
4388
  const envFile = envResolution.path;
4366
4389
  const explicitConfig = argv.config;
4367
- const configPath = explicitConfig
4368
- ? resolve(explicitConfig)
4369
- : join(homedir(), ".neurolink", "proxy-config.yaml");
4390
+ const configPath = resolveProxyConfigPath(explicitConfig);
4370
4391
  if (explicitConfig && !existsSync(configPath)) {
4371
4392
  console.info(chalk.red(`Proxy config file not found: ${configPath}`));
4372
4393
  process.exit(1);
@@ -4376,8 +4397,9 @@ export const proxyInstallCommand = {
4376
4397
  console.info(chalk.red(`Proxy env file not found: ${envFile}`));
4377
4398
  process.exit(1);
4378
4399
  }
4400
+ await loadProxyEnvFile({ explicitEnvFile: envFile });
4379
4401
  const logsDir = join(homedir(), ".neurolink", "logs");
4380
- if (!existsSync(logsDir)) {
4402
+ if (!isProxyOtelOnly() && !existsSync(logsDir)) {
4381
4403
  mkdirSync(logsDir, { recursive: true });
4382
4404
  }
4383
4405
  if (!existsSync(PLIST_DIR)) {
@@ -4401,8 +4423,9 @@ export const proxyInstallCommand = {
4401
4423
  process.exit(1);
4402
4424
  }
4403
4425
  console.info(chalk.green(`✓ Trampoline validated (resolves to neurolink v${trampolineVersion})`));
4404
- const plist = buildPlist(port, host, envFile, configFile);
4405
- writeFileSync(PLIST_PATH, plist, "utf-8");
4426
+ const plist = buildProxyLaunchdPlist(port, host, envFile, configFile);
4427
+ await writeFileAtomic(PLIST_PATH, plist, 0o600);
4428
+ chmodSync(PLIST_PATH, 0o600);
4406
4429
  console.info(chalk.green(`✓ Plist written to ${PLIST_PATH}`));
4407
4430
  if (envFile) {
4408
4431
  console.info(chalk.green(`✓ Proxy env file: ${envFile}`));
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Grok Build client configurator.
3
+ *
4
+ * Grok Build (`grok`, xAI's terminal agent) is a Codex-shaped TOML client
5
+ * that speaks three wire formats. Built-in `grok-4.6` / `grok-4.5` stay on
6
+ * xAI (`cli-chat-proxy.grok.com`, Responses API, 500k window). This writer
7
+ * does not remap those. It adds the proxy's advertised catalog as extra
8
+ * picker entries so Grok can send Claude traffic through `/v1/messages`
9
+ * (passthrough) and Gemini/OpenAI traffic through `/v1/chat/completions`
10
+ * (translation), each with a `context_window` that Grok's own compaction
11
+ * will honour — the proxy does not truncate.
12
+ *
13
+ * Snapshot lives in `~/.neurolink/`, following Codex, never inside
14
+ * `config.toml`. Grok's TOML parser is not a closed schema, but putting
15
+ * bookkeeping keys in the user's file is how OpenCode was bricked, and new
16
+ * writers do not rely on tolerance.
17
+ *
18
+ * Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
19
+ * becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
20
+ * with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
21
+ */
22
+ import type { CliGrokProxyModelSpec, CliProxyClientApplyOptions, CliProxyClientConfigurator, ModelMapping } from "../../types/index.js";
23
+ declare function getGrokConfigDir(): string;
24
+ declare function getGrokConfigPath(): string;
25
+ declare function getGrokSnapshotPath(): string;
26
+ declare function classifyGrokProxyModel(id: string, mapping?: ModelMapping): CliGrokProxyModelSpec;
27
+ declare function loadRoutedModelIds(configPath?: string): Promise<string[]>;
28
+ declare function catalogModelIds(configPath?: string): Promise<string[]>;
29
+ declare function buildGrokManagedBlock(baseUrl: string, configPath?: string): Promise<string>;
30
+ export declare function setGrokProxySettings(baseUrl: string, options?: CliProxyClientApplyOptions): Promise<boolean>;
31
+ export declare function clearGrokProxySettings(expectedBaseUrl?: string): Promise<boolean>;
32
+ export declare const grokConfigurator: CliProxyClientConfigurator;
33
+ /** Test-only export (CLAUDE.md rule 15 determinism exception). See openCode.ts. */
34
+ export declare const __grokTestHooks: {
35
+ getGrokConfigDir: typeof getGrokConfigDir;
36
+ getGrokConfigPath: typeof getGrokConfigPath;
37
+ getGrokSnapshotPath: typeof getGrokSnapshotPath;
38
+ setGrokProxySettings: typeof setGrokProxySettings;
39
+ clearGrokProxySettings: typeof clearGrokProxySettings;
40
+ classifyGrokProxyModel: typeof classifyGrokProxyModel;
41
+ loadRoutedModelIds: typeof loadRoutedModelIds;
42
+ catalogModelIds: typeof catalogModelIds;
43
+ buildGrokManagedBlock: typeof buildGrokManagedBlock;
44
+ };
45
+ export {};
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Grok Build client configurator.
3
+ *
4
+ * Grok Build (`grok`, xAI's terminal agent) is a Codex-shaped TOML client
5
+ * that speaks three wire formats. Built-in `grok-4.6` / `grok-4.5` stay on
6
+ * xAI (`cli-chat-proxy.grok.com`, Responses API, 500k window). This writer
7
+ * does not remap those. It adds the proxy's advertised catalog as extra
8
+ * picker entries so Grok can send Claude traffic through `/v1/messages`
9
+ * (passthrough) and Gemini/OpenAI traffic through `/v1/chat/completions`
10
+ * (translation), each with a `context_window` that Grok's own compaction
11
+ * will honour — the proxy does not truncate.
12
+ *
13
+ * Snapshot lives in `~/.neurolink/`, following Codex, never inside
14
+ * `config.toml`. Grok's TOML parser is not a closed schema, but putting
15
+ * bookkeeping keys in the user's file is how OpenCode was bricked, and new
16
+ * writers do not rely on tolerance.
17
+ *
18
+ * Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
19
+ * becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
20
+ * with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
21
+ */
22
+ import { readFileSync } from "fs";
23
+ import { homedir } from "os";
24
+ import { join } from "path";
25
+ import { logger } from "../../utils/logger.js";
26
+ import { getContextWindowSize } from "../../constants/contextWindows.js";
27
+ import { DEFAULT_PROXY_MODEL_IDS } from "../../constants/proxyModels.js";
28
+ import { defaultProxyConfigPath, parseProxyConfigString, } from "../../proxy/proxyConfig.js";
29
+ import { isUsableSnapshot, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
30
+ const GROK_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
31
+ const GROK_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
32
+ const PLACEHOLDER_KEY = "neurolink-proxy";
33
+ const ANTHROPIC_VERSION = "2023-06-01";
34
+ function getGrokConfigDir() {
35
+ const env = process.env.GROK_HOME;
36
+ return env !== undefined && env.trim().length > 0
37
+ ? env.trim()
38
+ : join(homedir(), ".grok");
39
+ }
40
+ function getGrokConfigPath() {
41
+ return join(getGrokConfigDir(), "config.toml");
42
+ }
43
+ function getGrokSnapshotPath() {
44
+ return join(homedir(), ".neurolink", "grok-proxy-snapshot.json");
45
+ }
46
+ function escapeRegExp(value) {
47
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48
+ }
49
+ function tomlKey(id) {
50
+ return /^[A-Za-z0-9_-]+$/.test(id) ? id : JSON.stringify(id);
51
+ }
52
+ function displayNameFor(id) {
53
+ const base = id.replace(/-\d{8}$/, "");
54
+ const pretty = base
55
+ .split("-")
56
+ .map((part) => /^\d/.test(part) ? part : part.charAt(0).toUpperCase() + part.slice(1))
57
+ .join(" ")
58
+ .replace(/(\d) (\d)/g, "$1.$2");
59
+ return `${pretty} (NeuroLink)`;
60
+ }
61
+ function providerForModelId(id) {
62
+ if (id.startsWith("claude-")) {
63
+ return "anthropic";
64
+ }
65
+ if (id.startsWith("gemini-")) {
66
+ return "vertex";
67
+ }
68
+ return "openai";
69
+ }
70
+ function providerFromMapping(provider, fallbackId) {
71
+ const normalized = provider.trim().toLowerCase();
72
+ if (normalized === "anthropic" || normalized === "claude") {
73
+ return "anthropic";
74
+ }
75
+ if (normalized === "openai") {
76
+ return "openai";
77
+ }
78
+ if (normalized === "vertex" ||
79
+ normalized === "google" ||
80
+ normalized === "google-ai" ||
81
+ normalized === "gemini") {
82
+ return "vertex";
83
+ }
84
+ return providerForModelId(fallbackId);
85
+ }
86
+ function isMissingFileError(error) {
87
+ if (!error || typeof error !== "object" || !("code" in error)) {
88
+ return false;
89
+ }
90
+ return error.code === "ENOENT";
91
+ }
92
+ /**
93
+ * Anthropic `thinking.type = "adaptive"` is what Grok emits for `xhigh`.
94
+ * Measured: Haiku 4.5 returns 400 "adaptive thinking is not supported".
95
+ * Opus/Sonnet 4.6 and the 5-series accept it.
96
+ */
97
+ function supportsAdaptiveThinking(id) {
98
+ return (/^claude-(opus|sonnet)-4-6$/.test(id) || /^claude-(opus|sonnet)-5/.test(id));
99
+ }
100
+ function classifyGrokProxyModel(id, mapping) {
101
+ const classifyId = mapping && mapping.to.trim().length > 0 ? mapping.to.trim() : id;
102
+ const provider = mapping
103
+ ? providerFromMapping(mapping.provider, classifyId)
104
+ : providerForModelId(id);
105
+ const windowProvider = mapping && mapping.provider.trim().length > 0
106
+ ? mapping.provider.trim()
107
+ : provider;
108
+ const apiBackend = provider === "anthropic" ? "messages" : "chat_completions";
109
+ const contextWindow = getContextWindowSize(windowProvider, classifyId);
110
+ return {
111
+ id,
112
+ name: displayNameFor(id),
113
+ apiBackend,
114
+ contextWindow,
115
+ maxCompletionTokens: contextWindow >= 1_000_000 ? 16_384 : 8_192,
116
+ supportsReasoningEffort: supportsAdaptiveThinking(classifyId),
117
+ };
118
+ }
119
+ async function loadRoutedMappings(configPath) {
120
+ const resolvedPath = configPath ?? defaultProxyConfigPath();
121
+ try {
122
+ const config = await parseProxyConfigString(readFileSync(resolvedPath, "utf8"));
123
+ return (config.routing?.modelMappings ?? []).filter((mapping) => mapping.from.trim().length > 0);
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ }
129
+ async function loadRoutedModelIds(configPath) {
130
+ return (await loadRoutedMappings(configPath)).map((mapping) => mapping.from.trim());
131
+ }
132
+ async function catalogGrokSpecs(configPath) {
133
+ const seen = new Set();
134
+ const specs = [];
135
+ const routed = await loadRoutedMappings(configPath);
136
+ const routedByFrom = new Map(routed.map((mapping) => [mapping.from.trim(), mapping]));
137
+ for (const id of [
138
+ ...DEFAULT_PROXY_MODEL_IDS,
139
+ ...routed.map((mapping) => mapping.from.trim()),
140
+ ]) {
141
+ if (id.startsWith("grok-") || seen.has(id)) {
142
+ continue;
143
+ }
144
+ seen.add(id);
145
+ specs.push(classifyGrokProxyModel(id, routedByFrom.get(id)));
146
+ }
147
+ return specs;
148
+ }
149
+ async function catalogModelIds(configPath) {
150
+ return (await catalogGrokSpecs(configPath)).map((spec) => spec.id);
151
+ }
152
+ function buildGrokModelBlock(spec, baseUrl) {
153
+ const lines = [
154
+ `[model.${tomlKey(spec.id)}]`,
155
+ `model = ${JSON.stringify(spec.id)}`,
156
+ `name = ${JSON.stringify(spec.name)}`,
157
+ `base_url = ${JSON.stringify(baseUrl)}`,
158
+ `api_backend = ${JSON.stringify(spec.apiBackend)}`,
159
+ `context_window = ${spec.contextWindow}`,
160
+ `auto_compact_threshold_percent = 80`,
161
+ `max_completion_tokens = ${spec.maxCompletionTokens}`,
162
+ `supports_backend_search = false`,
163
+ ];
164
+ if (!spec.supportsReasoningEffort) {
165
+ lines.push("supports_reasoning_effort = false");
166
+ }
167
+ if (spec.apiBackend === "messages") {
168
+ lines.push(`extra_headers = { "x-api-key" = ${JSON.stringify(PLACEHOLDER_KEY)}, "anthropic-version" = ${JSON.stringify(ANTHROPIC_VERSION)} }`);
169
+ }
170
+ else {
171
+ lines.push(`api_key = ${JSON.stringify(PLACEHOLDER_KEY)}`);
172
+ }
173
+ return lines.join("\n");
174
+ }
175
+ async function buildGrokManagedBlock(baseUrl, configPath) {
176
+ const specs = await catalogGrokSpecs(configPath);
177
+ const body = specs
178
+ .map((spec) => buildGrokModelBlock(spec, baseUrl))
179
+ .join("\n\n");
180
+ return [
181
+ GROK_BLOCK_BEGIN,
182
+ "# Proxy catalog. Built-in grok-4.6 / grok-4.5 stay on xAI.",
183
+ "# context_window is Grok's compaction limit and must be <= upstream.",
184
+ "",
185
+ body,
186
+ GROK_BLOCK_END,
187
+ "",
188
+ ].join("\n");
189
+ }
190
+ function stripGrokManagedBlock(text) {
191
+ const blockRe = new RegExp(`\\n?${escapeRegExp(GROK_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(GROK_BLOCK_END)}\\n?`, "g");
192
+ return text.replace(blockRe, "\n");
193
+ }
194
+ function extractManagedBaseUrl(text) {
195
+ const match = text.match(new RegExp(`${escapeRegExp(GROK_BLOCK_BEGIN)}[\\s\\S]*?base_url\\s*=\\s*"([^"]*)"`));
196
+ return match?.[1];
197
+ }
198
+ async function readGrokSnapshot() {
199
+ const fs = await import("fs");
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(fs.readFileSync(getGrokSnapshotPath(), "utf8"));
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ if (!isUsableSnapshot(parsed, "originalExisted")) {
208
+ logger.debug("[proxy] Grok: ignoring a malformed snapshot rather than treating it as empty");
209
+ return null;
210
+ }
211
+ const record = parsed;
212
+ const originalExisted = record.originalExisted;
213
+ const writtenBaseUrl = record.writtenBaseUrl;
214
+ if (typeof originalExisted !== "boolean") {
215
+ return null;
216
+ }
217
+ if (typeof writtenBaseUrl !== "string") {
218
+ return null;
219
+ }
220
+ return { originalExisted, writtenBaseUrl };
221
+ }
222
+ export async function setGrokProxySettings(baseUrl, options) {
223
+ const fs = await import("fs");
224
+ try {
225
+ fs.accessSync(getGrokConfigDir());
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ let original;
231
+ try {
232
+ original = fs.readFileSync(getGrokConfigPath(), "utf8");
233
+ }
234
+ catch (error) {
235
+ if (!isMissingFileError(error)) {
236
+ logger.warn("[proxy] Grok: unable to read config.toml; leaving it untouched");
237
+ return false;
238
+ }
239
+ original = null;
240
+ }
241
+ const existingSnapshot = await readGrokSnapshot();
242
+ if (existingSnapshot === null && fs.existsSync(getGrokSnapshotPath())) {
243
+ logger.warn("[proxy] Grok: snapshot file is unreadable; leaving config.toml untouched rather than overwriting with no way back");
244
+ return false;
245
+ }
246
+ const currentBlock = original
247
+ ? original.includes(GROK_BLOCK_BEGIN)
248
+ ? original.slice(original.indexOf(GROK_BLOCK_BEGIN), original.indexOf(GROK_BLOCK_END) === -1
249
+ ? original.length
250
+ : original.indexOf(GROK_BLOCK_END) + GROK_BLOCK_END.length)
251
+ : undefined
252
+ : undefined;
253
+ if (existingSnapshot === null ||
254
+ shouldCaptureSnapshot({
255
+ hasSnapshot: existingSnapshot !== null,
256
+ written: existingSnapshot?.writtenBaseUrl,
257
+ current: extractManagedBaseUrl(original ?? "") ?? currentBlock,
258
+ })) {
259
+ fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
260
+ await writeFileAtomic(getGrokSnapshotPath(), JSON.stringify({
261
+ originalExisted: existingSnapshot?.originalExisted ?? original !== null,
262
+ writtenBaseUrl: baseUrl,
263
+ }, null, 2), 0o600);
264
+ }
265
+ const withoutBlock = original ? stripGrokManagedBlock(original) : "";
266
+ const trimmed = withoutBlock.replace(/\s*$/, "\n");
267
+ const next = `${trimmed}\n${await buildGrokManagedBlock(baseUrl, options?.configPath)}`;
268
+ await writeFileAtomic(getGrokConfigPath(), next.startsWith("\n") && original === null
269
+ ? next.replace(/^\n+/, "")
270
+ : next, original === null ? 0o600 : undefined);
271
+ return true;
272
+ }
273
+ export async function clearGrokProxySettings(expectedBaseUrl) {
274
+ const fs = await import("fs");
275
+ let current;
276
+ try {
277
+ current = fs.readFileSync(getGrokConfigPath(), "utf8");
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ if (!current.includes(GROK_BLOCK_BEGIN)) {
283
+ return false;
284
+ }
285
+ const configuredUrl = extractManagedBaseUrl(current);
286
+ if (expectedBaseUrl &&
287
+ configuredUrl !== undefined &&
288
+ configuredUrl !== expectedBaseUrl) {
289
+ logger.debug("[proxy] Grok clear: base URL is not the one we wrote, leaving it intact");
290
+ return false;
291
+ }
292
+ const snapshot = await readGrokSnapshot();
293
+ if (snapshot === null) {
294
+ logger.warn("[proxy] Grok clear: no usable snapshot, leaving config.toml untouched rather than stripping a block we cannot prove we own");
295
+ return false;
296
+ }
297
+ const remainder = stripGrokManagedBlock(current).replace(/\s*$/, "\n");
298
+ if (!snapshot.originalExisted && remainder.trim().length === 0) {
299
+ fs.rmSync(getGrokConfigPath(), { force: true });
300
+ }
301
+ else {
302
+ await writeFileAtomic(getGrokConfigPath(), remainder);
303
+ }
304
+ try {
305
+ fs.rmSync(getGrokSnapshotPath(), { force: true });
306
+ }
307
+ catch {
308
+ // next apply overwrites
309
+ }
310
+ return true;
311
+ }
312
+ export const grokConfigurator = {
313
+ id: "grok",
314
+ displayName: "Grok Build",
315
+ detect: async () => {
316
+ const fs = await import("fs");
317
+ try {
318
+ fs.accessSync(getGrokConfigDir());
319
+ return true;
320
+ }
321
+ catch {
322
+ return false;
323
+ }
324
+ },
325
+ // Grok appends `/messages` or `/chat/completions` to `base_url`, so it
326
+ // takes the `/v1` door rather than the proxy root.
327
+ apply: (proxyBaseUrl, options) => setGrokProxySettings(`${proxyBaseUrl}/v1`, options),
328
+ restore: (proxyBaseUrl) => clearGrokProxySettings(`${proxyBaseUrl}/v1`),
329
+ };
330
+ /** Test-only export (CLAUDE.md rule 15 determinism exception). See openCode.ts. */
331
+ export const __grokTestHooks = {
332
+ getGrokConfigDir,
333
+ getGrokConfigPath,
334
+ getGrokSnapshotPath,
335
+ setGrokProxySettings,
336
+ clearGrokProxySettings,
337
+ classifyGrokProxyModel,
338
+ loadRoutedModelIds,
339
+ catalogModelIds,
340
+ buildGrokManagedBlock,
341
+ };
342
+ //# sourceMappingURL=grok.js.map
@@ -1,4 +1,4 @@
1
- import type { CliProxyClientApplyResult, CliProxyClientConfigurator, CliProxyClientRestoreResult } from "../../types/index.js";
1
+ import type { CliProxyClientApplyOptions, CliProxyClientApplyResult, CliProxyClientConfigurator, CliProxyClientRestoreResult } from "../../types/index.js";
2
2
  /**
3
3
  * Every CLI the proxy auto-configures, in apply order.
4
4
  *
@@ -14,6 +14,6 @@ export declare const PROXY_CLIENT_CONFIGURATORS: readonly CliProxyClientConfigur
14
14
  * how loudly to report — the daemon-start path logs failures at debug level
15
15
  * while the setup wizard prints a visible warning.
16
16
  */
17
- export declare function applyAllClients(proxyBaseUrl: string): Promise<CliProxyClientApplyResult[]>;
17
+ export declare function applyAllClients(proxyBaseUrl: string, options?: CliProxyClientApplyOptions): Promise<CliProxyClientApplyResult[]>;
18
18
  /** Restore every client's previous configuration. See applyAllClients. */
19
19
  export declare function restoreAllClients(proxyBaseUrl: string): Promise<CliProxyClientRestoreResult[]>;
@@ -5,6 +5,7 @@ import { codexConfigurator } from "./codex.js";
5
5
  import { qwenCodeConfigurator } from "./qwenCode.js";
6
6
  import { copilotConfigurator } from "./copilot.js";
7
7
  import { geminiConfigurator } from "./gemini.js";
8
+ import { grokConfigurator } from "./grok.js";
8
9
  /**
9
10
  * Every CLI the proxy auto-configures, in apply order.
10
11
  *
@@ -18,6 +19,7 @@ export const PROXY_CLIENT_CONFIGURATORS = [
18
19
  qwenCodeConfigurator,
19
20
  copilotConfigurator,
20
21
  geminiConfigurator,
22
+ grokConfigurator,
21
23
  ];
22
24
  /**
23
25
  * Point every detected client at the proxy.
@@ -27,12 +29,12 @@ export const PROXY_CLIENT_CONFIGURATORS = [
27
29
  * how loudly to report — the daemon-start path logs failures at debug level
28
30
  * while the setup wizard prints a visible warning.
29
31
  */
30
- export async function applyAllClients(proxyBaseUrl) {
32
+ export async function applyAllClients(proxyBaseUrl, options) {
31
33
  const results = [];
32
34
  for (const client of PROXY_CLIENT_CONFIGURATORS) {
33
35
  try {
34
36
  const applied = (await client.detect())
35
- ? await client.apply(proxyBaseUrl)
37
+ ? await client.apply(proxyBaseUrl, options)
36
38
  : false;
37
39
  // Only ask for a note when something was actually written: a note on a
38
40
  // client that was skipped would read as an instruction to act on a