@bitmagic/cli 0.1.68-dev.3 → 0.1.68-dev.5

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * How each agent is pointed at the event proxy.
3
+ *
4
+ * Nothing here writes a config file. The participant's agent is spawned with the environment
5
+ * already set, which is the only approach that works for both agents and leaves nothing behind
6
+ * when the proxy is torn down and the keys stop working:
7
+ *
8
+ * - The CLI has never written to ~/.claude/ or ~/.codex/, and editing a participant's global
9
+ * Codex config is not something a game SDK should do.
10
+ * - A `.claude/settings.json` env block only takes effect once the directory is trusted, which
11
+ * a freshly scaffolded project is not (cli/lab/drivers/claude-code.ts documents the same
12
+ * trap and sidesteps it the same way).
13
+ * - Codex config is TOML, and this repo has no TOML writer, dependency or merge precedent.
14
+ *
15
+ * The shape of "spawn an agent with a modified environment" is taken from cli/lab/drivers/ —
16
+ * `baseEnv()` in types.ts and the `-c key=value` overrides in codex.ts. That code is the lab
17
+ * harness and is never built into dist/, so this is a copied pattern rather than an import.
18
+ */
19
+ /** Model aliases the event proxy serves. Anything else is a 400 — it knows only its own names. */
20
+ export declare const OPUS_ALIAS = "claude-5.5-opus";
21
+ /** Codex's model: GPT-6 Sol on Bedrock, the same sponsor account that serves Opus. */
22
+ export declare const CODEX_MODEL = "bedrock-gpt-6-sol";
23
+ /** The env var Codex is told to read the key from, via `env_key`. */
24
+ export declare const CODEX_KEY_ENV = "BITMAGIC_EVENT_KEY";
25
+ export type EventAgent = 'claude' | 'codex';
26
+ export interface LaunchSpec {
27
+ bin: string;
28
+ argv: string[];
29
+ env: Record<string, string | undefined>;
30
+ }
31
+ /** The shells `bitmagic event env` can print for. */
32
+ export type EnvShell = 'posix' | 'powershell' | 'cmd';
33
+ /**
34
+ * The two clients want DIFFERENT forms of the same URL, and swapping them produces a 404 that
35
+ * reads like the proxy is down:
36
+ *
37
+ * Claude Code the bare host — it appends /v1/messages itself
38
+ * Codex host + /v1 — an OpenAI-compatible base, no trailing slash
39
+ *
40
+ * docs/ci-ai-credentials.md warns about exactly this (and about reusing getLiteLLMProxyUrl(),
41
+ * which already ends in /v1). Both forms are derived here so no caller has to remember which.
42
+ */
43
+ export declare function anthropicBaseUrl(baseUrl: string): string;
44
+ export declare function openAiBaseUrl(baseUrl: string): string;
45
+ /**
46
+ * Claude Code's environment: the block docs/ci-ai-credentials.md records and every Claude Code
47
+ * workflow proves daily.
48
+ *
49
+ * All three model pins are required, not just Opus. Claude Code makes background calls of its
50
+ * own with Anthropic-native model ids, and the proxy serves only its own aliases — so naming
51
+ * only the Opus default leaves those background calls 400ing. There is no Sonnet on the proxy,
52
+ * which is why Sonnet is pinned to Opus rather than to a Sonnet alias.
53
+ */
54
+ export declare function claudeEnv(key: string, baseUrl: string, parentEnv?: Record<string, string | undefined>, platform?: string): Record<string, string | undefined>;
55
+ /**
56
+ * Codex is configured through an isolated CODEX_HOME rather than `-c` overrides — see
57
+ * event/codex-home.ts for why overrides cannot work against a real installation.
58
+ */
59
+ export declare function codexEnv(key: string, codexHome: string, parentEnv?: Record<string, string | undefined>): Record<string, string | undefined>;
60
+ /**
61
+ * Everything needed to spawn one agent. Pure — the caller does the spawning, and for Codex the
62
+ * caller has already materialised `codexHome` (ensureCodexHome).
63
+ */
64
+ export declare function launchSpec(agent: EventAgent, key: string, baseUrl: string, codexHome: string, passthrough?: string[], parentEnv?: Record<string, string | undefined>, platform?: string): LaunchSpec;
65
+ /** Which shell `bitmagic event env` should print for when nobody said. */
66
+ export declare function defaultEnvShell(platform?: string): EnvShell;
67
+ /**
68
+ * The environment `bitmagic event env` prints, for anyone wiring it up by hand.
69
+ *
70
+ * Per-shell because this is the documented fallback for whoever the launcher failed, and a Windows
71
+ * participant is the likeliest person to reach it. POSIX `export` lines are a syntax error in both
72
+ * `cmd.exe` and PowerShell, so printing only those would send exactly the person who needs help
73
+ * somewhere that cannot work.
74
+ */
75
+ export declare function exportLines(key: string, baseUrl: string, shell?: EnvShell): string[];
@@ -0,0 +1,188 @@
1
+ /**
2
+ * How each agent is pointed at the event proxy.
3
+ *
4
+ * Nothing here writes a config file. The participant's agent is spawned with the environment
5
+ * already set, which is the only approach that works for both agents and leaves nothing behind
6
+ * when the proxy is torn down and the keys stop working:
7
+ *
8
+ * - The CLI has never written to ~/.claude/ or ~/.codex/, and editing a participant's global
9
+ * Codex config is not something a game SDK should do.
10
+ * - A `.claude/settings.json` env block only takes effect once the directory is trusted, which
11
+ * a freshly scaffolded project is not (cli/lab/drivers/claude-code.ts documents the same
12
+ * trap and sidesteps it the same way).
13
+ * - Codex config is TOML, and this repo has no TOML writer, dependency or merge precedent.
14
+ *
15
+ * The shape of "spawn an agent with a modified environment" is taken from cli/lab/drivers/ —
16
+ * `baseEnv()` in types.ts and the `-c key=value` overrides in codex.ts. That code is the lab
17
+ * harness and is never built into dist/, so this is a copied pattern rather than an import.
18
+ */
19
+ /** Model aliases the event proxy serves. Anything else is a 400 — it knows only its own names. */
20
+ export const OPUS_ALIAS = 'claude-5.5-opus';
21
+ const HAIKU_ALIAS = 'claude-4.5-haiku';
22
+ /** Codex's model: GPT-6 Sol on Bedrock, the same sponsor account that serves Opus. */
23
+ export const CODEX_MODEL = 'bedrock-gpt-6-sol';
24
+ /** The env var Codex is told to read the key from, via `env_key`. */
25
+ export const CODEX_KEY_ENV = 'BITMAGIC_EVENT_KEY';
26
+ /**
27
+ * Variables that select a model provider, and so OUTRANK the event key.
28
+ *
29
+ * Claude Code's documented credential precedence puts cloud-provider selection at rank 1 and
30
+ * `ANTHROPIC_AUTH_TOKEN` — what this launcher sets — at rank 2. A participant who already uses
31
+ * Claude Code against their employer's Bedrock has `CLAUDE_CODE_USE_BEDROCK=1` exported, and it
32
+ * would win: their session would run on their own AWS account, the event key would spend nothing,
33
+ * and no error would appear anywhere. The sponsor's credits go unused and the participant's
34
+ * employer gets the bill.
35
+ *
36
+ * At an AWS-sponsored event the people carrying this variable are not a rare case — they are the
37
+ * likeliest attendees.
38
+ *
39
+ * The lower-ranked credentials need no handling: `ANTHROPIC_API_KEY` (3), `apiKeyHelper` (4),
40
+ * `CLAUDE_CODE_OAUTH_TOKEN` (5), profiles (6) and a `/login` subscription (7) all lose to the
41
+ * bearer token on their own. ANTHROPIC_API_KEY is removed anyway because setting it alongside
42
+ * ANTHROPIC_AUTH_TOKEN is explicitly forbidden.
43
+ */
44
+ const CLAUDE_PROVIDER_SELECTORS = ['CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'];
45
+ /** Session-scoped variables a spawning Claude Code exports, which must not reach a fresh agent. */
46
+ const CLAUDE_SESSION_MARKERS = [
47
+ 'CLAUDECODE',
48
+ 'CLAUDE_CODE_ENTRYPOINT',
49
+ 'CLAUDE_CODE_SESSION_ID',
50
+ 'CLAUDE_CODE_CHILD_SESSION',
51
+ 'CLAUDE_CODE_SESSION_ATTENDED',
52
+ 'CLAUDE_CODE_MESSAGING_SOCKET',
53
+ 'CLAUDE_CODE_MESSAGING_TOKEN',
54
+ 'CLAUDE_CODE_EXECPATH',
55
+ 'CLAUDE_PID',
56
+ ];
57
+ /**
58
+ * Removes a variable from the child environment.
59
+ *
60
+ * Windows environment names are CASE-INSENSITIVE: `Anthropic_Api_Key` set through the System
61
+ * Properties dialog is the same variable Claude Code reads as `ANTHROPIC_API_KEY`, but
62
+ * `delete env.ANTHROPIC_API_KEY` does not remove it — so the one condition this code exists to
63
+ * prevent survives the launch. On POSIX the opposite is true and a case-insensitive sweep would
64
+ * delete unrelated variables, so the platform decides.
65
+ */
66
+ function deleteEnvVar(env, name, platform) {
67
+ if (platform !== 'win32') {
68
+ delete env[name];
69
+ return;
70
+ }
71
+ const lower = name.toLowerCase();
72
+ for (const key of Object.keys(env)) {
73
+ if (key.toLowerCase() === lower)
74
+ delete env[key];
75
+ }
76
+ }
77
+ /**
78
+ * The two clients want DIFFERENT forms of the same URL, and swapping them produces a 404 that
79
+ * reads like the proxy is down:
80
+ *
81
+ * Claude Code the bare host — it appends /v1/messages itself
82
+ * Codex host + /v1 — an OpenAI-compatible base, no trailing slash
83
+ *
84
+ * docs/ci-ai-credentials.md warns about exactly this (and about reusing getLiteLLMProxyUrl(),
85
+ * which already ends in /v1). Both forms are derived here so no caller has to remember which.
86
+ */
87
+ export function anthropicBaseUrl(baseUrl) {
88
+ return baseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
89
+ }
90
+ export function openAiBaseUrl(baseUrl) {
91
+ return `${anthropicBaseUrl(baseUrl)}/v1`;
92
+ }
93
+ /**
94
+ * Claude Code's environment: the block docs/ci-ai-credentials.md records and every Claude Code
95
+ * workflow proves daily.
96
+ *
97
+ * All three model pins are required, not just Opus. Claude Code makes background calls of its
98
+ * own with Anthropic-native model ids, and the proxy serves only its own aliases — so naming
99
+ * only the Opus default leaves those background calls 400ing. There is no Sonnet on the proxy,
100
+ * which is why Sonnet is pinned to Opus rather than to a Sonnet alias.
101
+ */
102
+ export function claudeEnv(key, baseUrl, parentEnv = process.env, platform = process.platform) {
103
+ const env = { ...parentEnv };
104
+ // Setting ANTHROPIC_API_KEY alongside ANTHROPIC_AUTH_TOKEN is explicitly forbidden, and a
105
+ // participant who already uses Claude Code on their own key will have one exported. Removing
106
+ // it is the main thing this launcher can do that a copied export block cannot.
107
+ deleteEnvVar(env, 'ANTHROPIC_API_KEY', platform);
108
+ // ANTHROPIC_MODEL, if the participant has pinned one for their own use, wins over the
109
+ // DEFAULT_* vars below and names a model the proxy does not serve — a 400 on every call.
110
+ deleteEnvVar(env, 'ANTHROPIC_MODEL', platform);
111
+ // Session markers from a Claude Code that spawned us — participants really do ask their agent
112
+ // to run the setup for them. cli/lab/drivers/types.ts drops the first two so a nested agent
113
+ // does not think it is inside another session; the rest are handles (sockets, tokens, pids)
114
+ // that name a process this one is not. CLAUDE_CONFIG_DIR is deliberately left alone: that one
115
+ // is a standing preference rather than session state.
116
+ //
117
+ // Note this is NOT what causes Claude Code's `unrecognized_model: claude-5.5-opus[1m]` line —
118
+ // that appears with a clean environment and no launcher at all, because Claude Code's own
119
+ // model registry does not know the proxy's aliases. It is cosmetic; the call succeeds.
120
+ // A provider selector outranks the event key outright — see CLAUDE_PROVIDER_SELECTORS. Removing
121
+ // these is the difference between the sponsor paying for the session and the participant's own
122
+ // employer paying for it without either of them being told.
123
+ for (const selector of CLAUDE_PROVIDER_SELECTORS)
124
+ deleteEnvVar(env, selector, platform);
125
+ for (const marker of CLAUDE_SESSION_MARKERS)
126
+ deleteEnvVar(env, marker, platform);
127
+ env.ANTHROPIC_BASE_URL = anthropicBaseUrl(baseUrl);
128
+ env.ANTHROPIC_AUTH_TOKEN = key;
129
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = OPUS_ALIAS;
130
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = OPUS_ALIAS;
131
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = HAIKU_ALIAS;
132
+ env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '1';
133
+ return env;
134
+ }
135
+ /**
136
+ * Codex is configured through an isolated CODEX_HOME rather than `-c` overrides — see
137
+ * event/codex-home.ts for why overrides cannot work against a real installation.
138
+ */
139
+ export function codexEnv(key, codexHome, parentEnv = process.env) {
140
+ const env = { ...parentEnv };
141
+ env[CODEX_KEY_ENV] = key;
142
+ env.CODEX_HOME = codexHome;
143
+ return env;
144
+ }
145
+ /**
146
+ * Everything needed to spawn one agent. Pure — the caller does the spawning, and for Codex the
147
+ * caller has already materialised `codexHome` (ensureCodexHome).
148
+ */
149
+ export function launchSpec(agent, key, baseUrl, codexHome, passthrough = [], parentEnv = process.env, platform = process.platform) {
150
+ if (agent === 'claude') {
151
+ return { bin: 'claude', argv: [...passthrough], env: claudeEnv(key, baseUrl, parentEnv, platform) };
152
+ }
153
+ // No argv configuration at all: the generated config.toml in CODEX_HOME carries the provider.
154
+ return { bin: 'codex', argv: [...passthrough], env: codexEnv(key, codexHome, parentEnv) };
155
+ }
156
+ /** Which shell `bitmagic event env` should print for when nobody said. */
157
+ export function defaultEnvShell(platform = process.platform) {
158
+ return platform === 'win32' ? 'powershell' : 'posix';
159
+ }
160
+ /**
161
+ * The environment `bitmagic event env` prints, for anyone wiring it up by hand.
162
+ *
163
+ * Per-shell because this is the documented fallback for whoever the launcher failed, and a Windows
164
+ * participant is the likeliest person to reach it. POSIX `export` lines are a syntax error in both
165
+ * `cmd.exe` and PowerShell, so printing only those would send exactly the person who needs help
166
+ * somewhere that cannot work.
167
+ */
168
+ export function exportLines(key, baseUrl, shell = defaultEnvShell()) {
169
+ const vars = [
170
+ ['ANTHROPIC_BASE_URL', anthropicBaseUrl(baseUrl)],
171
+ ['ANTHROPIC_AUTH_TOKEN', key],
172
+ ['ANTHROPIC_DEFAULT_OPUS_MODEL', OPUS_ALIAS],
173
+ ['ANTHROPIC_DEFAULT_SONNET_MODEL', OPUS_ALIAS],
174
+ ['ANTHROPIC_DEFAULT_HAIKU_MODEL', HAIKU_ALIAS],
175
+ ['CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS', '1'],
176
+ [CODEX_KEY_ENV, key],
177
+ ];
178
+ switch (shell) {
179
+ case 'powershell':
180
+ return vars.map(([name, value]) => `$env:${name} = "${value}"`);
181
+ case 'cmd':
182
+ // No quotes: cmd.exe would make them part of the value.
183
+ return vars.map(([name, value]) => `set ${name}=${value}`);
184
+ default:
185
+ return vars.map(([name, value]) => `export ${name}=${value}`);
186
+ }
187
+ }
188
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../../src/event/launch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { type EventAgent } from './launch.js';
2
+ export interface PreflightResult {
3
+ /** What the proxy actually served, from `x-litellm-model-name`. */
4
+ servedModel: string;
5
+ /** Cost of this call in USD, from `x-litellm-response-cost`. Present once pricing resolves. */
6
+ cost: number | undefined;
7
+ /** Cumulative spend on this key in USD, from `x-litellm-key-spend`. */
8
+ keySpend: number | undefined;
9
+ }
10
+ export declare function preflight(key: string, baseUrl: string, agent?: EventAgent): Promise<PreflightResult>;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * One tiny call against the event proxy, before the participant starts working.
3
+ *
4
+ * This is the same assertion every Claude Code workflow makes before it runs
5
+ * (.github/workflows/code-simplifier.yml, the "Verify Bedrock gateway" step): a 200 is not
6
+ * enough, because the proxy can serve a different model behind a perfectly healthy response.
7
+ * The only tell is the `x-litellm-model-name` header.
8
+ *
9
+ * The point is timing. Every failure here — dead proxy, mistyped key, exhausted budget, silent
10
+ * downgrade — otherwise shows up ten minutes into a session as something that reads like the
11
+ * agent being broken. Catching it at `setup` and at each launch costs one token and turns it
12
+ * into a sentence the participant can act on.
13
+ */
14
+ import { CliError } from '../errors.js';
15
+ import { CODEX_MODEL, OPUS_ALIAS, anthropicBaseUrl, openAiBaseUrl } from './launch.js';
16
+ const PREFLIGHT_TIMEOUT_MS = 15_000;
17
+ /**
18
+ * Each agent is probed on the model and wire API it will actually use, so a proxy that serves
19
+ * Opus but not GPT (or the reverse) is caught before that agent starts, not after.
20
+ *
21
+ * Codex goes through the Responses API because that is what `wire_api = "responses"` makes it
22
+ * call; 16 is the smallest `max_output_tokens` the Responses API accepts.
23
+ */
24
+ const PROBES = {
25
+ claude: {
26
+ label: 'Opus 5.5',
27
+ url: (baseUrl) => `${anthropicBaseUrl(baseUrl)}/v1/messages`,
28
+ headers: { 'anthropic-version': '2023-06-01' },
29
+ // One token: enough to prove the whole path works, cheap enough to run on every launch.
30
+ body: { model: OPUS_ALIAS, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] },
31
+ served: ['opus-5-5', 'opus-5.5'],
32
+ },
33
+ codex: {
34
+ label: 'GPT-6 Sol',
35
+ url: (baseUrl) => `${openAiBaseUrl(baseUrl)}/responses`,
36
+ headers: {},
37
+ body: { model: CODEX_MODEL, max_output_tokens: 16, input: 'hi' },
38
+ served: ['gpt-6-sol'],
39
+ },
40
+ };
41
+ function numberHeader(headers, name) {
42
+ const raw = headers.get(name);
43
+ if (raw === null)
44
+ return undefined;
45
+ const value = Number(raw);
46
+ return Number.isFinite(value) ? value : undefined;
47
+ }
48
+ /**
49
+ * Maps the proxy's refusals onto advice. Anything unmapped keeps its status and body so a new
50
+ * failure mode is never silently rephrased as one of these.
51
+ */
52
+ function refusal(status, body) {
53
+ if (status === 401 || status === 403) {
54
+ return new CliError('The event proxy rejected this key.\n' +
55
+ 'Check it matches the card exactly — it starts with "sk-" and has no spaces.\n' +
56
+ 'Re-run: bitmagic event setup --key sk-…', 1, 'event_key_rejected');
57
+ }
58
+ if (status === 429 && body.includes('budget')) {
59
+ return new CliError('This key has reached its spending limit.\n' +
60
+ 'Your work is safe. Find one of the organisers — raising it takes a minute.', 1, 'event_budget_exceeded');
61
+ }
62
+ if (status === 429) {
63
+ return new CliError('The event proxy is rate-limiting this key. Wait a moment and try again.', 1, 'event_rate_limited');
64
+ }
65
+ return new CliError(`The event proxy returned HTTP ${status}.\n${body.slice(0, 300)}`, 1, 'event_proxy_error');
66
+ }
67
+ export async function preflight(key, baseUrl, agent = 'claude') {
68
+ const probe = PROBES[agent];
69
+ let response;
70
+ try {
71
+ response = await fetch(probe.url(baseUrl), {
72
+ method: 'POST',
73
+ headers: {
74
+ Authorization: `Bearer ${key}`,
75
+ 'content-type': 'application/json',
76
+ ...probe.headers,
77
+ },
78
+ body: JSON.stringify(probe.body),
79
+ signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS),
80
+ });
81
+ }
82
+ catch (error) {
83
+ throw new CliError(`Cannot reach the event proxy at ${anthropicBaseUrl(baseUrl)}.\n` +
84
+ 'Check your network connection. If you are on event wifi, ask an organiser.\n' +
85
+ `(${error instanceof Error ? error.message : String(error)})`, 1, 'event_proxy_unreachable');
86
+ }
87
+ if (!response.ok) {
88
+ throw refusal(response.status, await response.text().catch(() => ''));
89
+ }
90
+ const servedModel = response.headers.get('x-litellm-model-name') ?? '';
91
+ if (!probe.served.some((name) => servedModel.includes(name))) {
92
+ // A 200 that did not come from the intended model means a fallback served it. Refusing here rather than
93
+ // warning is deliberate: the participant would otherwise spend their budget on a weaker
94
+ // model without ever being told, and the spend accounting would be wrong too.
95
+ throw new CliError(`The event proxy answered with "${servedModel || 'an unknown model'}" instead of ${probe.label}.\n` +
96
+ 'That usually means the proxy is degraded. Tell an organiser before continuing.', 1, 'event_model_downgrade');
97
+ }
98
+ return {
99
+ servedModel,
100
+ cost: numberHeader(response.headers, 'x-litellm-response-cost'),
101
+ keySpend: numberHeader(response.headers, 'x-litellm-key-spend'),
102
+ };
103
+ }
104
+ //# sourceMappingURL=preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../src/event/preflight.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type Exists = (candidate: string) => boolean;
2
+ /**
3
+ * The absolute path of `name` on PATH, or `null` when it is not there.
4
+ *
5
+ * On anything but Windows the name is returned unchanged: `spawn` does the PATH search itself, and
6
+ * resolving it here would only add a way to disagree with the thing that actually launches it.
7
+ */
8
+ export declare function resolveAgentBinary(name: string, env?: Record<string, string | undefined>, platform?: string, exists?: Exists): string | null;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Finding the participant's `claude` / `codex` executable before spawning it.
3
+ *
4
+ * On POSIX this is not a problem worth solving: the name goes to `spawn`, which searches PATH and
5
+ * executes whatever it finds. On Windows it is the whole problem. `CreateProcess` — which is where
6
+ * a non-shell spawn ends up — does not consult PATHEXT and cannot execute a batch file at all, so
7
+ * `spawn('claude')` fails for every installation that is not literally a file named `claude`:
8
+ *
9
+ * - `npm install -g @anthropic-ai/claude-code` writes `claude.cmd`. That is a batch shim, and
10
+ * since the CVE-2024-27980 fix (Node 18.20.2 / 20.12.2) spawning one without `shell: true`
11
+ * throws EINVAL rather than running it.
12
+ * - The agents' own native installers write `claude.exe`, which runs fine — but only once
13
+ * something has resolved the name to the file, extension included.
14
+ *
15
+ * So this module answers "which file", and `project/spawn-bin.ts` answers "how to run it". Keeping
16
+ * them apart is what lets a real `.exe` spawn directly, with no `cmd.exe` wrapped around an
17
+ * interactive terminal UI, while a `.cmd` shim still gets the shell it cannot run without.
18
+ *
19
+ * Pure, with PATH, PATHEXT and the existence probe injected, so the Windows behaviour is a test
20
+ * table rather than a machine we do not have — the same approach `binInvocation` and `openerFor`
21
+ * already take.
22
+ */
23
+ import * as fs from 'fs';
24
+ import * as path from 'path';
25
+ /** What Windows uses when PATHEXT is unset. Only the executable kinds an agent could ship as. */
26
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
27
+ const realExists = (candidate) => {
28
+ try {
29
+ return fs.statSync(candidate).isFile();
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ };
35
+ /**
36
+ * The absolute path of `name` on PATH, or `null` when it is not there.
37
+ *
38
+ * On anything but Windows the name is returned unchanged: `spawn` does the PATH search itself, and
39
+ * resolving it here would only add a way to disagree with the thing that actually launches it.
40
+ */
41
+ export function resolveAgentBinary(name, env = process.env, platform = process.platform, exists = realExists) {
42
+ if (platform !== 'win32')
43
+ return name;
44
+ const pathValue = env.PATH ?? env.Path ?? '';
45
+ if (pathValue === '')
46
+ return null;
47
+ // An empty PATHEXT entry stands for the name exactly as given, which is how a extensionless
48
+ // executable (or one the participant typed with its extension) is still found.
49
+ const extensions = ['', ...(env.PATHEXT ?? DEFAULT_PATHEXT).split(';').filter((ext) => ext !== '')];
50
+ for (const dir of pathValue.split(';')) {
51
+ if (dir === '')
52
+ continue;
53
+ for (const ext of extensions) {
54
+ // `path.win32`, not `path.join`: the latter uses the separator of the machine running the
55
+ // code, so building a Windows path on a developer's Mac would produce `C:\dir/claude.exe`.
56
+ // The tests are the only place that happens, which is exactly where it would go unnoticed.
57
+ //
58
+ // Windows paths are case-insensitive, so the casing PATHEXT uses (`.EXE`) does not have to
59
+ // match the file's (`claude.exe`) — but the extension must be ON the candidate, because
60
+ // `statSync('…/claude')` does not find `claude.exe`.
61
+ const candidate = path.win32.join(dir, `${name}${ext}`);
62
+ if (exists(candidate))
63
+ return candidate;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ //# sourceMappingURL=resolve-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-agent.js","sourceRoot":"","sources":["../../src/event/resolve-agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,iGAAiG;AACjG,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAI9C,MAAM,UAAU,GAAW,CAAC,SAAS,EAAE,EAAE;IACvC,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,MAA0C,OAAO,CAAC,GAAG,EACrD,WAAmB,OAAO,CAAC,QAAQ,EACnC,SAAiB,UAAU;IAE3B,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAEtC,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAC7C,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAElC,4FAA4F;IAC5F,+EAA+E;IAC/E,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,eAAe,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;IAEpG,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,IAAI,GAAG,KAAK,EAAE;YAAE,SAAS;QACzB,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,0FAA0F;YAC1F,2FAA2F;YAC3F,2FAA2F;YAC3F,EAAE;YACF,2FAA2F;YAC3F,wFAAwF;YACxF,qDAAqD;YACrD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC;YACxD,IAAI,MAAM,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface EventKey {
2
+ /** Host root of the event proxy, no trailing slash and no `/v1`. See launch.ts. */
3
+ baseUrl: string;
4
+ key: string;
5
+ /** Epoch ms, for `bitmagic event status` to report when the key was stored. */
6
+ savedAt: number;
7
+ }
8
+ export declare function defaultBaseDir(): string;
9
+ export declare function eventKeyPath(baseDir?: string): string;
10
+ export declare function readEventKey(baseDir?: string): EventKey | null;
11
+ export declare function writeEventKey(entry: EventKey, baseDir?: string): void;
12
+ /** Returns false when there was nothing to forget, so `clear` can say so rather than imply it acted. */
13
+ export declare function clearEventKey(baseDir?: string): boolean;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Where the sponsored-event API key lives on a participant's machine.
3
+ *
4
+ * Deliberately a SEPARATE file from credentials.json rather than another field in it. This is
5
+ * disposable state that stops working when the event proxy is torn down, and a participant
6
+ * deleting it afterwards — or us telling them to — must carry no risk to their login.
7
+ *
8
+ * File discipline is copied from config/credentials.ts: 0600 inside a 0700 directory, written
9
+ * through a temp file and renamed, chmod'd before and after because writeFileSync's `mode` only
10
+ * applies when it creates the file. A corrupt file is treated as absent, not as an error: the fix
11
+ * is always `bitmagic event setup` again, and a parse failure must not wedge the command that
12
+ * would repair it.
13
+ *
14
+ * Those modes are POSIX-only. Windows `chmod` toggles nothing but the read-only bit, so the file
15
+ * there is protected by the user profile's ACL and not by anything this module does — which is why
16
+ * `bitmagic event setup` does not claim otherwise on that platform.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { CliError } from '../errors.js';
22
+ const DIR_MODE = 0o700;
23
+ const FILE_MODE = 0o600;
24
+ export function defaultBaseDir() {
25
+ return path.join(os.homedir(), '.bitmagic');
26
+ }
27
+ export function eventKeyPath(baseDir = defaultBaseDir()) {
28
+ return path.join(baseDir, 'event.json');
29
+ }
30
+ function isEventKey(value) {
31
+ if (typeof value !== 'object' || value === null)
32
+ return false;
33
+ const v = value;
34
+ return (typeof v.baseUrl === 'string' &&
35
+ v.baseUrl.length > 0 &&
36
+ typeof v.key === 'string' &&
37
+ v.key.length > 0 &&
38
+ typeof v.savedAt === 'number' &&
39
+ Number.isFinite(v.savedAt));
40
+ }
41
+ export function readEventKey(baseDir = defaultBaseDir()) {
42
+ const filePath = eventKeyPath(baseDir);
43
+ let raw;
44
+ try {
45
+ raw = fs.readFileSync(filePath, 'utf-8');
46
+ }
47
+ catch (err) {
48
+ const code = err.code;
49
+ if (code === 'ENOENT' || code === 'ENOTDIR')
50
+ return null;
51
+ // A real filesystem problem must not be reported as "no key" — that sends the participant
52
+ // through setup again, which cannot fix a permissions error.
53
+ throw new CliError(`Cannot read ${filePath}: ${code ?? String(err)}`);
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ return isEventKey(parsed) ? parsed : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export function writeEventKey(entry, baseDir = defaultBaseDir()) {
64
+ fs.mkdirSync(baseDir, { recursive: true, mode: DIR_MODE });
65
+ const filePath = eventKeyPath(baseDir);
66
+ const tmpPath = path.join(baseDir, `.event.json.${process.pid}.${Date.now()}.tmp`);
67
+ fs.writeFileSync(tmpPath, `${JSON.stringify(entry, null, 2)}\n`, { mode: FILE_MODE });
68
+ fs.chmodSync(tmpPath, FILE_MODE);
69
+ fs.renameSync(tmpPath, filePath);
70
+ fs.chmodSync(filePath, FILE_MODE);
71
+ fs.chmodSync(baseDir, DIR_MODE);
72
+ }
73
+ /** Returns false when there was nothing to forget, so `clear` can say so rather than imply it acted. */
74
+ export function clearEventKey(baseDir = defaultBaseDir()) {
75
+ try {
76
+ fs.unlinkSync(eventKeyPath(baseDir));
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ const code = err.code;
81
+ if (code === 'ENOENT' || code === 'ENOTDIR')
82
+ return false;
83
+ throw new CliError(`Cannot remove ${eventKeyPath(baseDir)}: ${code ?? String(err)}`);
84
+ }
85
+ }
86
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/event/store.ts"],"names":[],"mappings":"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"}
@@ -1,34 +1,15 @@
1
+ import { isSafeCandidateId, newCandidateId, type AcceptedReference as SharedAcceptedReference, type CandidateRecord } from '@bitmagic/asset-core/reference-record';
1
2
  export declare const REFERENCE_DIR: string;
2
3
  export declare function referenceDirPath(root: string): string;
3
4
  export declare function acceptedReferencePath(root: string): string;
4
5
  export declare function candidateImagePath(root: string, id: string): string;
5
- /** One generated candidate — a picture the creator may accept, iterate from, or ignore. */
6
- export interface CandidateRecord {
7
- id: string;
8
- /** The Asset Forger URL the generation returned. Short-lived; the accept re-hosts it. */
9
- url: string;
10
- /** The prompt actually sent, truncated — enough to recognise a candidate in a list. */
11
- prompt: string;
12
- /** The candidate this one was iterated from (`--from`), if any. */
13
- from?: string;
14
- /** An explicit `--reference-image-url` this candidate was steered by, if any. */
15
- referenceImageUrl?: string;
16
- createdAt: string;
17
- }
18
- /** The accepted reference — what every generator and forge inherits. */
19
- export interface AcceptedReference {
20
- /** The URL generators are given: the re-hosted public URL, or the source URL when re-hosting failed. */
21
- url: string;
22
- /** False when re-hosting failed and `url` is the (expiring) source URL — worth re-accepting. */
23
- hosted: boolean;
24
- /** What was accepted, before any re-hosting. */
25
- sourceUrl: string;
26
- /** Absent when a bare URL was accepted rather than a generated candidate. */
27
- candidateId?: string;
6
+ /** The CLI keeps a local copy of the accepted bytes; the shared record has no such field. */
7
+ export type AcceptedReference = SharedAcceptedReference & {
28
8
  /** The local copy, when bytes were available to keep. */
29
9
  imagePath?: string;
30
- acceptedAt: string;
31
- }
10
+ };
11
+ export type { CandidateRecord };
12
+ export { isSafeCandidateId, newCandidateId };
32
13
  export declare function readAcceptedReference(root: string): AcceptedReference | null;
33
14
  export declare function writeAcceptedReference(root: string, accepted: AcceptedReference): void;
34
15
  /** Remove the accepted reference. Candidates stay — clearing a decision is not deleting the work. */
@@ -37,10 +18,6 @@ export declare function readCandidate(root: string, id: string): CandidateRecord
37
18
  export declare function writeCandidate(root: string, record: CandidateRecord): void;
38
19
  /** Every readable candidate, newest first (ids sort chronologically by construction). */
39
20
  export declare function listCandidates(root: string): CandidateRecord[];
40
- /** `ref-<yyyymmdd-hhmmss>-<4 rand>` — short enough to type, sortable by creation time. */
41
- export declare function newCandidateId(now?: Date): string;
42
- /** Candidate ids become file names, so anything path-shaped is refused, never sanitised. */
43
- export declare function isSafeCandidateId(id: string): boolean;
44
21
  /** The two flags every reference-aware command declares (see REFERENCE_IMAGE_FLAG / NO_REFERENCE_FLAG). */
45
22
  export interface ReferenceFlagArgs {
46
23
  'reference-image-url'?: string;