rebellm-bridge 0.1.0 → 0.1.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/README.md CHANGED
@@ -55,7 +55,7 @@ SDKs: OpenAI with `base_url="http://127.0.0.1:7343/v1"`, Anthropic with
55
55
  As Claude Code's model (its own config directory, your claude.ai login untouched):
56
56
 
57
57
  ```
58
- rebellm-claude # interactive
58
+ npx rebellm-bridge claude # nothing to install; rebellm-claude after npm install -g
59
59
  rebellm-claude -p "What is 2 + 3?"
60
60
  ```
61
61
 
@@ -65,6 +65,14 @@ and a long request timeout, and passes every other argument to `claude`. Options
65
65
  Claude Code's first request is ~11k tokens of system prompt and tools, so it needs a tab
66
66
  with a 32k context and patience on a slow GPU.
67
67
 
68
+ On a work machine Claude Code may be pinned to the company's endpoint or cloud provider.
69
+ The launcher blanks `ANTHROPIC_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS` and `CLAUDE_CODE_USE_*`
70
+ from your shell and passes its own variables with `--settings`, which ranks above the
71
+ project's `.claude/settings.json` and your user settings. Managed settings (MDM,
72
+ `managed-settings.json`, the claude.ai admin console) rank above that and only your
73
+ organisation can change them: the launcher names the file when it finds one, and `/status`
74
+ inside `claude` lists the setting sources in force.
75
+
68
76
  As a tool inside your normal Claude Code (Claude keeps its own model, can ask yours):
69
77
 
70
78
  ```
package/dist/cli.d.ts CHANGED
@@ -5,7 +5,7 @@ import { type BridgeServer } from './server.js';
5
5
  import type { Health } from './tab.js';
6
6
  export declare const VERSION: string;
7
7
  export declare const TOKEN_ENV = "REBELLM_BRIDGE_TOKEN";
8
- export declare const USAGE = "Usage: rebellm-bridge [options]\n\nLets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.\n\n --port <n> port for the tab and the HTTP API (default 7343)\n --host <addr> address to listen on (default 127.0.0.1; anything else exposes the model)\n --token <t> token the tab must present (default: REBELLM_BRIDGE_TOKEN, else ~/.rebellm-bridge/token)\n --wait <s> seconds a request waits for the tab and its model (default 120)\n --mcp serve MCP over stdio as well (Claude Code starts the bridge this way)\n --version print the version\n --help print this help";
8
+ export declare const USAGE = "Usage: rebellm-bridge [options]\n rebellm-bridge claude [launcher options] [claude arguments]\n\nLets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.\n\n claude ... run Claude Code on the tab's model, the same as rebellm-claude\n --port <n> port for the tab and the HTTP API (default 7343)\n --host <addr> address to listen on (default 127.0.0.1; anything else exposes the model)\n --token <t> token the tab must present (default: REBELLM_BRIDGE_TOKEN, else ~/.rebellm-bridge/token)\n --wait <s> seconds a request waits for the tab and its model (default 120)\n --mcp serve MCP over stdio as well (Claude Code starts the bridge this way)\n --version print the version\n --help print this help";
9
9
  export interface CliOptions {
10
10
  port: number;
11
11
  host: string;
package/dist/cli.js CHANGED
@@ -11,9 +11,11 @@ import { DEFAULT_HOST, DEFAULT_PORT, isLoopback, startServer } from './server.js
11
11
  export const VERSION = createRequire(import.meta.url)('../package.json').version;
12
12
  export const TOKEN_ENV = 'REBELLM_BRIDGE_TOKEN';
13
13
  export const USAGE = `Usage: rebellm-bridge [options]
14
+ rebellm-bridge claude [launcher options] [claude arguments]
14
15
 
15
16
  Lets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.
16
17
 
18
+ claude ... run Claude Code on the tab's model, the same as rebellm-claude
17
19
  --port <n> port for the tab and the HTTP API (default ${DEFAULT_PORT})
18
20
  --host <addr> address to listen on (default ${DEFAULT_HOST}; anything else exposes the model)
19
21
  --token <t> token the tab must present (default: ${TOKEN_ENV}, else ~/.rebellm-bridge/token)
@@ -161,6 +163,11 @@ export async function run(o, io) {
161
163
  }
162
164
  /** The command line; resolves with the exit code once the bridge has stopped. */
163
165
  export async function main(argv, io) {
166
+ if (argv[0] === 'claude') {
167
+ // Loaded here so the plain bridge never touches the launcher, and the two files can import each other.
168
+ const { launch } = await import('./launcher.js');
169
+ return launch(argv.slice(1), { stderr: io.stderr, env: io.env, ...(io.home ? { home: io.home } : {}) });
170
+ }
164
171
  const o = parseCli(argv);
165
172
  if ('error' in o) {
166
173
  io.stderr.write(`rebellm-bridge: ${o.error}\n\n${USAGE}\n`);
@@ -19,16 +19,23 @@ export declare function parseLauncher(argv: string[]): LauncherOptions | {
19
19
  export declare function findCommand(name: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | null;
20
20
  /** What sends every model request of claude to the bridge. */
21
21
  export declare function bridgeEnv(base: string, contextTokens?: number): Record<string, string>;
22
- /** The parent's environment with the bridge's on top; a real API key is removed, never sent to the bridge. */
22
+ /** The parent's environment with the bridge's on top; a real key or provider never reaches claude. */
23
23
  export declare function childEnv(parent: NodeJS.ProcessEnv, ours: Record<string, string>, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
24
24
  export declare const configDir: (home: string) => string;
25
25
  export declare const logFile: (home: string) => string;
26
- /** Puts `env` into the config dir's `settings.json`, keeping the rest; a warning when it cannot. */
27
- export declare function writeSettings(dir: string, env: Record<string, string>): string | null;
26
+ /** Given to claude as `--settings`, which ranks above project, local and user settings. */
27
+ export declare const launchSettingsFile: (home: string) => string;
28
+ export declare function writeLaunchSettings(file: string, env: Record<string, string>): void;
29
+ /** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
30
+ export declare function managedSettingsFile(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string;
31
+ /** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
32
+ export declare function managedOverrides(file: string): string[];
28
33
  export interface LaunchIo {
29
34
  stderr: Writable;
30
35
  env: NodeJS.ProcessEnv;
31
36
  home?: string;
37
+ /** The managed settings file to look at, for tests. */
38
+ managedFile?: string;
32
39
  /** How often to ask the bridge whether the tab is there. */
33
40
  pollMs?: number;
34
41
  platform?: NodeJS.Platform;
package/dist/launcher.js CHANGED
@@ -87,9 +87,19 @@ export function findCommand(name, env, platform = process.platform) {
87
87
  }
88
88
  return null;
89
89
  }
90
+ /** What Claude Code ranks above `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL`, or would send along. */
91
+ const OUTRANKING = [
92
+ 'ANTHROPIC_API_KEY',
93
+ 'ANTHROPIC_CUSTOM_HEADERS',
94
+ 'CLAUDE_CODE_USE_BEDROCK',
95
+ 'CLAUDE_CODE_USE_VERTEX',
96
+ 'CLAUDE_CODE_USE_FOUNDRY',
97
+ ];
90
98
  /** What sends every model request of claude to the bridge. */
91
99
  export function bridgeEnv(base, contextTokens) {
92
100
  return {
101
+ // Empty counts as unset; a value from a shell profile or a settings file would outrank the bridge.
102
+ ...Object.fromEntries(OUTRANKING.map((k) => [k, ''])),
93
103
  ANTHROPIC_BASE_URL: base,
94
104
  // The bridge checks no key; a Bearer token needs no approval prompt in Claude Code.
95
105
  ANTHROPIC_AUTH_TOKEN: 'rebellm-bridge-needs-no-key',
@@ -103,10 +113,10 @@ export function bridgeEnv(base, contextTokens) {
103
113
  ...(contextTokens ? { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextTokens) } : {}),
104
114
  };
105
115
  }
106
- /** The parent's environment with the bridge's on top; a real API key is removed, never sent to the bridge. */
116
+ /** The parent's environment with the bridge's on top; a real key or provider never reaches claude. */
107
117
  export function childEnv(parent, ours, platform = process.platform) {
108
118
  const win = platform === 'win32';
109
- const drop = new Set(['ANTHROPIC_API_KEY', ...Object.keys(ours)]);
119
+ const drop = new Set(Object.keys(ours));
110
120
  const env = {};
111
121
  for (const [k, v] of Object.entries(parent))
112
122
  if (!drop.has(win ? k.toUpperCase() : k))
@@ -115,25 +125,38 @@ export function childEnv(parent, ours, platform = process.platform) {
115
125
  }
116
126
  export const configDir = (home) => join(home, '.rebellm-bridge', 'claude');
117
127
  export const logFile = (home) => join(home, '.rebellm-bridge', 'bridge.log');
128
+ /** Given to claude as `--settings`, which ranks above project, local and user settings. */
129
+ export const launchSettingsFile = (home) => join(home, '.rebellm-bridge', 'claude-settings.json');
118
130
  const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
119
- /** Puts `env` into the config dir's `settings.json`, keeping the rest; a warning when it cannot. */
120
- export function writeSettings(dir, env) {
121
- const file = join(dir, 'settings.json');
122
- mkdirSync(dir, { recursive: true, mode: 0o700 });
123
- let settings = {};
131
+ export function writeLaunchSettings(file, env) {
132
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
133
+ writeFileSync(file, `${JSON.stringify({ env }, null, 2)}\n`, { mode: 0o600 });
134
+ }
135
+ /** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
136
+ export function managedSettingsFile(platform, env) {
137
+ if (platform === 'darwin')
138
+ return '/Library/Application Support/ClaudeCode/managed-settings.json';
139
+ if (platform === 'win32')
140
+ return join(env[envKey(env, 'PROGRAMFILES', true)] ?? 'C:\\Program Files', 'ClaudeCode', 'managed-settings.json');
141
+ return '/etc/claude-code/managed-settings.json';
142
+ }
143
+ const MANAGED_KEYS = ['apiKeyHelper', 'forceLoginMethod', 'forceLoginGatewayUrl'];
144
+ /** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
145
+ export function managedOverrides(file) {
146
+ let settings;
124
147
  try {
125
- const parsed = JSON.parse(readFileSync(file, 'utf8'));
126
- if (!isObj(parsed))
127
- return `${file} is not a JSON object; left it as it is`;
128
- settings = parsed;
148
+ settings = JSON.parse(readFileSync(file, 'utf8'));
129
149
  }
130
- catch (e) {
131
- if (e.code !== 'ENOENT')
132
- return `could not read ${file}; left it as it is`;
150
+ catch {
151
+ return [];
133
152
  }
134
- settings.env = { ...(isObj(settings.env) ? settings.env : {}), ...env };
135
- writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
136
- return null;
153
+ if (!isObj(settings))
154
+ return [];
155
+ const env = isObj(settings.env) ? Object.keys(settings.env) : [];
156
+ return [
157
+ ...env.filter((k) => /^(ANTHROPIC_|CLAUDE_CODE_USE_)/.test(k)),
158
+ ...MANAGED_KEYS.filter((k) => settings[k] !== undefined),
159
+ ];
137
160
  }
138
161
  const sleep = (ms, signal) => new Promise((resolve) => {
139
162
  const t = setTimeout(resolve, ms);
@@ -212,14 +235,17 @@ export async function launch(argv, io) {
212
235
  say(`the RebeLLM tab is connected${model}`);
213
236
  const dir = o.sharedConfig ? null : configDir(home);
214
237
  const ours = bridgeEnv(base, health.contextTokens);
215
- if (dir) {
216
- const warning = writeSettings(dir, ours);
217
- if (warning)
218
- say(warning);
219
- }
238
+ const settings = launchSettingsFile(home);
239
+ writeLaunchSettings(settings, ours);
240
+ const managed = io.managedFile ?? managedSettingsFile(platform, io.env);
241
+ const overrides = managedOverrides(managed);
242
+ if (overrides.length)
243
+ say(`your organisation's Claude Code settings in ${managed} set ${overrides.join(', ')}; they rank above ` +
244
+ `rebellm-claude, so claude may still go to your organisation's endpoint (/status in claude lists the sources)`);
220
245
  say(`starting ${claude} with ${dir ? `the config in ${dir}` : 'your own Claude Code config'}`);
221
246
  const env = childEnv(io.env, { ...ours, ...(dir ? { CLAUDE_CONFIG_DIR: dir } : {}) }, platform);
222
- const child = spawn(claude, o.args, { stdio: 'inherit', env });
247
+ // Ours first: a --settings the user passes comes later and wins, as asked.
248
+ const child = spawn(claude, ['--settings', settings, ...o.args], { stdio: 'inherit', env });
223
249
  return await new Promise((resolve) => {
224
250
  child.on('error', (e) => {
225
251
  say(`could not start ${claude}: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rebellm-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Use the model in your RebeLLM browser tab from the command line, OpenAI/Anthropic clients and Claude Code",
5
5
  "keywords": [
6
6
  "rebellm",