@callstack/apex 0.4.1 → 0.4.3

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.
Files changed (3) hide show
  1. package/README.md +67 -42
  2. package/dist/cli.js +362 -150
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,10 +1,25 @@
1
1
  # Apex CLI
2
2
 
3
3
  Configures the AI coding assistants on your machine to use
4
- [Callstack Apex](https://apex.callstack.com/) (`callstack/Apex`).
4
+ [Callstack Apex](https://apex.callstack.com/) through Vercel AI Gateway (`callstack/apex`) or the
5
+ Callstack portal (`callstack/Apex`).
5
6
 
6
- `apex init` detects the assistants on your machine, lets you pick which ones to configure,
7
- shows every change it wants to make, and only then writes. Nothing changes until you say so.
7
+ `apex init` asks which gateway serves Apex, detects the assistants on your machine, lets you pick
8
+ which ones to configure, shows every change it wants to make, and only then writes. Nothing
9
+ changes until you say so.
10
+
11
+ ## Gateways
12
+
13
+ | Gateway | `--provider` | Model | Key |
14
+ | --- | --- | --- | --- |
15
+ | Vercel AI Gateway | `vercel` | `callstack/apex` | `AI_GATEWAY_API_KEY` |
16
+ | Callstack portal | `callstack` | `callstack/Apex` | `CALLSTACK_AUTH_TOKEN` |
17
+
18
+ `apex init` asks which one to use. Without `--provider`, the default is the gateway whose key is
19
+ already set in your shell, and Vercel AI Gateway when neither or both are. `apex detect` and
20
+ `apex run` pick the gateway the same way, so `apex run codex` launches whatever you set up last as
21
+ long as only that key is exported. Each gateway gets its own provider entry in every config, so
22
+ switching gateways adds a provider next to the old one instead of rewriting it.
8
23
 
9
24
  ## Install
10
25
 
@@ -30,25 +45,26 @@ undo your configs, so run `apex undo` first if you want them back.
30
45
  | `apex init --apply` | Writes the planned changes without asking | Yes |
31
46
  | `apex undo` | Reverts the most recent Apex CLI setup, with the same preview and confirmation | Only after you confirm |
32
47
  | `apex undo --list` | Shows recorded setups and which are already undone (`--json` prints the journal) | No |
33
- | `apex run <assistant> [-- <args>]` | Launches the assistant with the gateway, model and credentials in the child environment | No |
48
+ | `apex run [--provider <id>] <assistant> [-- <args>]` | Launches the assistant with the gateway, model and credentials in the child environment | No |
34
49
  | `apex completion <zsh\|bash\|fish>` | Prints a shell completion script | No |
35
50
 
36
51
  Each command accepts only its own flags, and `--help` and the shell completions list exactly those:
37
52
 
38
53
  | Command | Flags |
39
54
  | --- | --- |
40
- | `init` | `--assistants <ids>`, `--no-interactive`, `--apply`, `--no-diff`, `--json`, `--help` |
41
- | `detect` | `--json`, `--help` |
55
+ | `init` | `--assistants <ids>`, `--provider <id>`, `--no-interactive`, `--apply`, `--no-diff`, `--json`, `--help` |
56
+ | `detect` | `--provider <id>`, `--json`, `--help` |
57
+ | `run` | `--provider <id>`, right before or after the assistant |
42
58
  | `undo` | `--no-interactive`, `--apply`, `--no-diff`, `--json`, `--list`, `--help` |
43
59
 
44
60
  `apex init --wat` fails fast, and so does a flag in the wrong command
45
61
  (`apex detect --no-diff`). Only `--apply` writes, so there is no "preview *and* write" to argue
46
62
  about. `--assistants <ids>` is a comma list of `opencode`, `codex`,
47
- `claude`, `pi`, `cursor`, `copilot`, `ai-sdk`.
63
+ `claude`, `pi`, `cursor`, `copilot`, `ai-sdk`. `--provider <id>` is `vercel` or `callstack`.
48
64
 
49
65
  Non-interactive callers (`--no-interactive`, `--json`, pipes, CI) never get a prompt and never
50
66
  have files changed unless they also pass `--apply`. `--json` prints a machine-readable plan with
51
- `mode`, `applied`, `appliedPaths`, per-file `changes` and `nextSteps` instead of the human UI. If a
67
+ `mode`, `provider`, `model`, `envVar`, `applied`, `appliedPaths`, per-file `changes` and `nextSteps` instead of the human UI. If a
52
68
  write fails halfway through a batch, the payload still reports what already landed plus the `error`,
53
69
  and the exit code is 1.
54
70
 
@@ -64,15 +80,15 @@ apex completion fish > ~/.config/fish/completions/apex.fish
64
80
 
65
81
  ```
66
82
  ┌────────────────────────────────────────────────────────────────────────────┐
67
- │ Apex CLI · Configure callstack/Apex for your favorite harness │
83
+ │ Apex CLI · Configure Apex for your favorite harness │
68
84
  │ │
69
85
  │ Nothing is written until you confirm. │
70
86
  └────────────────────────────────────────────────────────────────────────────┘
71
87
  │
72
- ◆ Set up callstack/Apex for which assistants? (space toggles, enter confirms)
73
- │ OpenCode, Codex
88
+ ◇ Which gateway should your assistants use for Apex?
89
+ │ Vercel AI Gateway
74
90
  │
75
- ◇ Set up callstack/Apex for which assistants? (space toggles, enter confirms)
91
+ ◇ Set up callstack/apex for which assistants? (space toggles, enter confirms)
76
92
  │ OpenCode, Codex
77
93
 
78
94
 
@@ -87,15 +103,15 @@ apex completion fish > ~/.config/fish/completions/apex.fish
87
103
  - "theme": "dark"
88
104
  + "theme": "dark",
89
105
  + "provider": {
90
- + "callstack.ai": { … }
106
+ + "apex-vercel": { … }
91
107
  + }
92
108
  }
93
109
 
94
- Create ~/.codex/callstack_ai.config.toml
110
+ Create ~/.codex/apex_vercel.config.toml
95
111
  --- /dev/null
96
- +++ ~/.codex/callstack_ai.config.toml
112
+ +++ ~/.codex/apex_vercel.config.toml
97
113
  @@ -0,0 +1,10 @@
98
- +model_provider = "callstack_ai"
114
+ +model_provider = "apex_vercel"
99
115
 
100
116
  Existing files get a .apex-backup-<id> copy first. (dimmed)
101
117
  │
@@ -105,24 +121,25 @@ apex completion fish > ~/.config/fish/completions/apex.fish
105
121
 
106
122
  Environment
107
123
 
108
- ✓ CALLSTACK_AUTH_TOKEN is set in this shell
124
+ ✓ AI_GATEWAY_API_KEY is set in this shell
109
125
  Apex CLI never reads, stores or logs the key itself.
110
126
 
111
127
 
112
- Use these commands to run callstack/Apex with your selected harnesses:
128
+ Use these commands to run callstack/apex with your selected harnesses:
113
129
 
114
- apex run opencode opencode --model callstack.ai/callstack/Apex (dimmed)
115
- apex run codex codex --profile callstack_ai
130
+ apex run opencode opencode --model apex-vercel/callstack/apex (dimmed)
131
+ apex run codex codex --profile apex_vercel
116
132
 
117
- ...or pick "callstack/Apex" from the UI when setting up manually.
118
- For more instructions, visit:
119
- https://app.notion.com/p/callstack/Apex-how-to-use-it-36d5d027c0f880e99d03d1c37a77382f
133
+ ...or pick "callstack/apex" from the UI when setting up manually.
134
+ For more instructions, visit: https://vercel.com/docs/ai-gateway/coding-agents
120
135
 
121
136
  If you want to undo the changes, run apex undo
122
137
  ```
123
- `Environment` only shows the `export CALLSTACK_AUTH_TOKEN=…` line while the variable is missing; once
124
- it is set you get a green `✓ CALLSTACK_AUTH_TOKEN is set in this shell` instead. The closing block shows
125
- what each `apex run` expands to, links the guide (the URL is never folded, so it stays clickable), and
138
+ `Environment` only shows the `export AI_GATEWAY_API_KEY=…` line (and, for Vercel AI Gateway, where to
139
+ create a key) while the variable is missing; once it is set you get a green
140
+ `✓ AI_GATEWAY_API_KEY is set in this shell` instead. The Callstack portal shows `CALLSTACK_AUTH_TOKEN`. The closing block shows
141
+ what each `apex run` expands to, links the gateway's guide (Vercel's coding-agents docs or the Callstack portal guide; the URL is never
142
+ folded, so it stays clickable), and
126
143
  mentions `apex undo` only when something was actually written.
127
144
 
128
145
  The diff is the change list: it only shows the lines that actually move, paths are shortened to
@@ -143,15 +160,17 @@ their own backup first, so an undo is itself reversible. Apex CLI records each b
143
160
 
144
161
  ## Integrations
145
162
 
163
+ Names below are Vercel AI Gateway / Callstack portal.
164
+
146
165
  | Assistant | Configuration | Behavior |
147
166
  | --- | --- | --- |
148
- | OpenCode | `~/.config/opencode/opencode.json` or `.jsonc` | Adds `provider.callstack.ai`, OpenAI-compatible transport, model and environment-key reference; preserves the default model. A config already in OpenCode 2's `providers` format gets the v2 entry instead, and you add the key with `/connect`. |
149
- | Codex 0.134.0+ | `~/.codex/callstack_ai.config.toml` | Adds a self-contained Responses API provider/profile; leaves base config and default model untouched. |
150
- | Claude Code | `~/.claude/settings.json` | Merges attribution flag only; use `apex run claude` for gateway credentials and model selection. |
151
- | Pi | `~/.pi/agent/models.json` and `settings.json` | Adds `providers.callstack` with Chat Completions, Apex and `$CALLSTACK_AUTH_TOKEN`; saves `medium` as Apex's per-model thinking default for direct launches. Preserves other models and their defaults. Requires a Pi version supporting `$VAR` key interpolation and `modelThinkingLevels`. |
152
- | Cursor | Guided setup | Prints endpoint, API-key and custom-model steps; does not modify private editor storage. |
167
+ | OpenCode | `~/.config/opencode/opencode.json` or `.jsonc` | Adds `provider.apex-vercel` / `provider.callstack.ai`, OpenAI-compatible transport, model and environment-key reference; preserves the default model. A config already in OpenCode 2's `providers` format gets the v2 entry instead, and you add the key with `/connect`. |
168
+ | Codex 0.134.0+ | `~/.codex/apex_vercel.config.toml` / `callstack_ai.config.toml` | Adds a self-contained Responses API provider/profile (Vercel's Codex endpoint `/codex/v1`); leaves base config and default model untouched. |
169
+ | Claude Code | `~/.claude/settings.json` | Merges attribution flag only; use `apex run claude` for gateway credentials and model selection (Vercel's Claude Code endpoint `/claude-code`). |
170
+ | Pi | `~/.pi/agent/models.json` and `settings.json` | Adds `providers.apex-vercel` / `providers.callstack` with Chat Completions, Apex and `$AI_GATEWAY_API_KEY` / `$CALLSTACK_AUTH_TOKEN`; saves `medium` as Apex's per-model thinking default for direct launches. Preserves other models and their defaults. Requires a Pi version supporting `$VAR` key interpolation and `modelThinkingLevels`. |
171
+ | Cursor | Guided setup | Prints endpoint (Vercel's Cursor endpoint `/cursor/v1`), API-key and custom-model steps; does not modify private editor storage. |
153
172
  | VS Code / Copilot | Guided setup | Detects VS Code, not whether Copilot is installed; prints custom-endpoint steps and model JSON, retaining the editor-generated secret reference. |
154
- | Vercel AI SDK / Eve | Guided setup | Detected from the `package.json` in the directory you run Apex CLI from (`ai`, `@ai-sdk/openai` or `eve`); prints the connector snippet, which reads the key from `CALLSTACK_AUTH_TOKEN`. Nothing is written into the project. |
173
+ | Vercel AI SDK / Eve | Guided setup | Detected from the `package.json` in the directory you run Apex CLI from (`ai`, `@ai-sdk/openai` or `eve`); prints the connector snippet, which reads the key from `AI_GATEWAY_API_KEY` or `CALLSTACK_AUTH_TOKEN` (Vercel AI Gateway uses its general `/v1` endpoint here, since an app is not a coding agent). Nothing is written into the project. |
155
174
 
156
175
  Apex configs declare tool calling, image input, and the supported
157
176
  `none`/`low`/`medium`/`xhigh` reasoning efforts (default `medium`) where supported.
@@ -163,8 +182,8 @@ reserve.
163
182
  Pi's per-model default applies to fresh direct launches and model selection; `apex run pi` also
164
183
  selects `medium` explicitly. Resumed Pi sessions retain their saved thinking level.
165
184
 
166
- The gateway model ID, authentication, `medium` default, and `off` → `none` translation remain
167
- Callstack-specific.
185
+ The model ID and key depend on the gateway you pick; the `medium` default and `off` → `none`
186
+ translation are Apex-specific.
168
187
 
169
188
  | Harness | Context field | Output field | Official reference |
170
189
  | --- | --- | --- | --- |
@@ -197,16 +216,20 @@ avoid shell interpolation; launch the configured assistant directly or use WSL.
197
216
 
198
217
  ## Authentication
199
218
 
200
- Provide `CALLSTACK_AUTH_TOKEN` through your shell or a secret manager. Apex CLI never prompts for,
201
- logs, stores or validates the key, and never writes it to a config file; configs only reference it.
202
- A hidden prompt keeps it out of your shell history:
219
+ Provide `AI_GATEWAY_API_KEY` (Vercel AI Gateway) or `CALLSTACK_AUTH_TOKEN` (Callstack portal)
220
+ through your shell or a secret manager. Create a Vercel key on the AI Gateway page of your Vercel
221
+ dashboard. Apex CLI never prompts for, logs, stores or validates the key, and never writes it to a
222
+ config file; configs only reference it. A hidden prompt keeps it out of your shell history:
203
223
 
204
224
  ```bash
205
- read -r -s -p 'Callstack API key: ' CALLSTACK_AUTH_TOKEN; echo
206
- export CALLSTACK_AUTH_TOKEN
225
+ read -r -s -p 'AI Gateway API key: ' AI_GATEWAY_API_KEY; echo
226
+ export AI_GATEWAY_API_KEY
207
227
  apex run codex
208
228
  ```
209
229
 
230
+ With both keys exported, `apex run` uses Vercel AI Gateway; pass `--provider callstack` for the
231
+ portal: `apex run --provider callstack codex`.
232
+
210
233
  Other launchers: `apex run opencode`, `apex run claude`, `apex run pi`, `apex run codex -- --help`.
211
234
  Launchers pass extra arguments through unchanged (without a shell), can be overridden by explicit
212
235
  user arguments, and never run `init`. `apex run claude` sets the gateway URL, token, attribution
@@ -221,7 +244,7 @@ Apex CLI will not break what you already have.
221
244
  - It copies a file next to itself before it edits that file, so you can always put the old one back.
222
245
  - It keeps your comments, your other settings, and your default model.
223
246
  - It never asks for your API key, writes it down, or sends it anywhere. You keep it in
224
- `CALLSTACK_AUTH_TOKEN`, and the CLI prints `<redacted>` wherever a secret would show.
247
+ `AI_GATEWAY_API_KEY` or `CALLSTACK_AUTH_TOKEN`, and the CLI prints `<redacted>` wherever a secret would show.
225
248
  - If a config file is broken, set up another way, or a link to somewhere else, Apex CLI stops and
226
249
  tells you. It does not overwrite it.
227
250
  - `apex undo` gives your old setup back. It only touches files that still match what Apex CLI wrote.
@@ -235,10 +258,10 @@ How that works underneath:
235
258
  then land in your dotfiles repo, so add `*.apex-backup-*` to its `.gitignore`.
236
259
  - The dedicated Codex Apex profile's owned keys are upgraded rather than rejected as conflicts;
237
260
  the base `config.toml` and other profiles remain untouched.
238
- - JSON/JSONC edits preserve comments and unrelated keys. The Callstack endpoint is set to the
261
+ - JSON/JSONC edits preserve comments and unrelated keys. The gateway endpoint is set to the
239
262
  documented one. A key the assistant already has (in OpenCode's own key store, or in Pi's
240
263
  `models.json`, where Apex CLI 0.2 put them) is kept, so upgrading never leaves an assistant
241
- without a key; otherwise the config references `CALLSTACK_AUTH_TOKEN`.
264
+ without a key; otherwise the config references `AI_GATEWAY_API_KEY` or `CALLSTACK_AUTH_TOKEN`.
242
265
  - Writes use same-directory temporary files plus rename with mode `0600` (new directories `0700`),
243
266
  and abort if the file changed on disk between planning and saving. This is not a lock or a
244
267
  multi-file transaction: a later I/O failure can leave earlier writes applied. `apex undo`
@@ -255,6 +278,8 @@ How that works underneath:
255
278
  (separate profile files for 0.134.0+; provider is placed in that profile).
256
279
  - OpenCode v1 configuration: `https://dev.opencode.ai/docs/config/`
257
280
  (JSONC and `{env:VARIABLE}` support).
281
+ - Vercel AI Gateway for coding agents: `https://vercel.com/docs/ai-gateway/coding-agents`
282
+ (per-agent endpoints; Apex is listed as `callstack/apex`).
258
283
  - Pi custom models: `https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/models.md`
259
284
  (`$VARIABLE` key interpolation; bare uppercase names are literals in current docs).
260
285
 
package/dist/cli.js CHANGED
@@ -2451,9 +2451,50 @@ async function writeChange(change) {
2451
2451
 
2452
2452
  //#endregion
2453
2453
  //#region src/assistants.js
2454
- const MODEL = "callstack/Apex";
2455
- const BASE_URL = "https://api.callstack.ai/v1";
2456
- const GUIDE_URL = "https://app.notion.com/p/callstack/Apex-how-to-use-it-36d5d027c0f880e99d03d1c37a77382f";
2454
+ const VERCEL = "https://ai-gateway.vercel.sh";
2455
+ const PROVIDERS = {
2456
+ vercel: {
2457
+ id: "vercel",
2458
+ name: "Vercel AI Gateway",
2459
+ label: "Vercel AI Gateway",
2460
+ model: "callstack/apex",
2461
+ envVar: "AI_GATEWAY_API_KEY",
2462
+ keyName: "your Vercel AI Gateway API key",
2463
+ keysUrl: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway&title=AI+Gateway",
2464
+ guideUrl: "https://vercel.com/docs/ai-gateway/coding-agents",
2465
+ baseUrl: `${VERCEL}/coding-agent/v1`,
2466
+ codexUrl: `${VERCEL}/codex/v1`,
2467
+ cursorUrl: `${VERCEL}/cursor/v1`,
2468
+ anthropicUrl: `${VERCEL}/claude-code`,
2469
+ appUrl: `${VERCEL}/v1`,
2470
+ opencode: "apex-vercel",
2471
+ pi: "apex-vercel",
2472
+ codex: "apex_vercel"
2473
+ },
2474
+ callstack: {
2475
+ id: "callstack",
2476
+ name: "Callstack portal",
2477
+ label: "callstack.ai",
2478
+ model: "callstack/Apex",
2479
+ envVar: "CALLSTACK_AUTH_TOKEN",
2480
+ keyName: "your callstack.ai key",
2481
+ keysUrl: null,
2482
+ guideUrl: "https://app.notion.com/p/callstack/Apex-how-to-use-it-36d5d027c0f880e99d03d1c37a77382f",
2483
+ baseUrl: "https://api.callstack.ai/v1",
2484
+ codexUrl: "https://api.callstack.ai/v1",
2485
+ cursorUrl: "https://api.callstack.ai/v1",
2486
+ anthropicUrl: "https://api.callstack.ai",
2487
+ appUrl: "https://api.callstack.ai/v1",
2488
+ opencode: "callstack.ai",
2489
+ pi: "callstack",
2490
+ codex: "callstack_ai"
2491
+ }
2492
+ };
2493
+ const PROVIDER_IDS = Object.keys(PROVIDERS);
2494
+ function defaultProvider(env = process.env) {
2495
+ const keyed = PROVIDER_IDS.filter((id) => env[PROVIDERS[id].envVar]?.trim());
2496
+ return PROVIDERS[keyed.length === 1 ? keyed[0] : "vercel"];
2497
+ }
2457
2498
  const IDS = [
2458
2499
  "opencode",
2459
2500
  "codex",
@@ -2502,30 +2543,25 @@ const OPENCODE_MODEL = {
2502
2543
  options: { reasoningEffort: "medium" },
2503
2544
  variants: Object.fromEntries(EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
2504
2545
  };
2505
- const OPENCODE_V2_PROVIDER = {
2506
- name: "callstack.ai",
2507
- package: "@opencode/ai/providers/openai-compatible",
2508
- settings: { baseURL: BASE_URL },
2509
- models: { [MODEL]: {
2510
- name: "Apex",
2511
- capabilities: {
2512
- tools: true,
2513
- input: ["text", "image"],
2514
- output: ["text"]
2515
- },
2516
- limit: {
2517
- context: CONTEXT_WINDOW,
2518
- output: MAX_OUTPUT
2519
- },
2520
- settings: { reasoningEffort: "medium" },
2521
- variants: EFFORTS.map((effort) => ({
2522
- id: effort,
2523
- settings: { reasoningEffort: effort }
2524
- }))
2525
- } }
2546
+ const OPENCODE_V2_PACKAGE = "@opencode/ai/providers/openai-compatible";
2547
+ const OPENCODE_V2_MODEL = {
2548
+ name: "Apex",
2549
+ capabilities: {
2550
+ tools: true,
2551
+ input: ["text", "image"],
2552
+ output: ["text"]
2553
+ },
2554
+ limit: {
2555
+ context: CONTEXT_WINDOW,
2556
+ output: MAX_OUTPUT
2557
+ },
2558
+ settings: { reasoningEffort: "medium" },
2559
+ variants: EFFORTS.map((effort) => ({
2560
+ id: effort,
2561
+ settings: { reasoningEffort: effort }
2562
+ }))
2526
2563
  };
2527
2564
  const PI_MODEL = {
2528
- id: MODEL,
2529
2565
  reasoning: true,
2530
2566
  input: ["text", "image"],
2531
2567
  thinkingLevelMap: {
@@ -2632,16 +2668,17 @@ async function detect(options = {}) {
2632
2668
  };
2633
2669
  }));
2634
2670
  }
2635
- async function openCodeHasKey(authFile) {
2671
+ async function openCodeHasKey(authFile, providerId) {
2636
2672
  if (!authFile) return false;
2637
2673
  try {
2638
- return Object.hasOwn(JSON.parse(await readConfig(authFile) ?? "{}"), "callstack.ai");
2674
+ return Object.hasOwn(JSON.parse(await readConfig(authFile) ?? "{}"), providerId);
2639
2675
  } catch {
2640
2676
  return false;
2641
2677
  }
2642
2678
  }
2643
2679
  const PI_PLACEHOLDER = "XXX";
2644
- async function planAssistant(assistant) {
2680
+ async function planAssistant(assistant, provider = PROVIDERS.vercel) {
2681
+ const { model, envVar } = provider;
2645
2682
  const changes = [];
2646
2683
  const json = async (filename, updates) => {
2647
2684
  const path = join(assistant.directory, filename);
@@ -2660,124 +2697,131 @@ async function planAssistant(assistant) {
2660
2697
  const hasJsonc = await exists(join(assistant.directory, "opencode.jsonc"));
2661
2698
  if (hasJson && hasJsonc) throw new Error("Both opencode.json and opencode.jsonc exist. Consolidate them before init.");
2662
2699
  let v2 = false;
2663
- const storedKey = await openCodeHasKey(assistant.authFile);
2700
+ const id = provider.opencode;
2701
+ const storedKey = await openCodeHasKey(assistant.authFile, id);
2664
2702
  await json(hasJsonc ? "opencode.jsonc" : "opencode.json", (existing) => {
2665
2703
  v2 = Object.hasOwn(existing, "providers");
2666
2704
  if (v2) return [
2667
2705
  [[
2668
2706
  "providers",
2669
- "callstack.ai",
2707
+ id,
2670
2708
  "name"
2671
- ], OPENCODE_V2_PROVIDER.name],
2709
+ ], provider.label],
2672
2710
  [[
2673
2711
  "providers",
2674
- "callstack.ai",
2712
+ id,
2675
2713
  "package"
2676
- ], OPENCODE_V2_PROVIDER.package],
2714
+ ], OPENCODE_V2_PACKAGE],
2677
2715
  [[
2678
2716
  "providers",
2679
- "callstack.ai",
2717
+ id,
2680
2718
  "settings",
2681
2719
  "baseURL"
2682
- ], BASE_URL],
2683
- ...Object.entries(OPENCODE_V2_PROVIDER.models[MODEL]).map(([key, value]) => [[
2720
+ ], provider.baseUrl],
2721
+ ...Object.entries(OPENCODE_V2_MODEL).map(([key, value]) => [[
2684
2722
  "providers",
2685
- "callstack.ai",
2723
+ id,
2686
2724
  "models",
2687
- MODEL,
2725
+ model,
2688
2726
  key
2689
2727
  ], key === "settings" ? {
2690
- ...existing.providers?.["callstack.ai"]?.models?.[MODEL]?.settings,
2728
+ ...existing.providers?.[id]?.models?.[model]?.settings,
2691
2729
  ...value
2692
2730
  } : value])
2693
2731
  ];
2694
2732
  return [
2695
2733
  [[
2696
2734
  "provider",
2697
- "callstack.ai",
2735
+ id,
2698
2736
  "npm"
2699
2737
  ], "@ai-sdk/openai-compatible"],
2700
2738
  [[
2701
2739
  "provider",
2702
- "callstack.ai",
2740
+ id,
2703
2741
  "name"
2704
- ], "callstack.ai"],
2742
+ ], provider.label],
2705
2743
  [[
2706
2744
  "provider",
2707
- "callstack.ai",
2745
+ id,
2708
2746
  "options",
2709
2747
  "baseURL"
2710
- ], BASE_URL],
2711
- ...storedKey || existing.provider?.["callstack.ai"]?.options?.apiKey ? [] : [[[
2748
+ ], provider.baseUrl],
2749
+ ...storedKey || existing.provider?.[id]?.options?.apiKey ? [] : [[[
2712
2750
  "provider",
2713
- "callstack.ai",
2751
+ id,
2714
2752
  "options",
2715
2753
  "apiKey"
2716
- ], "{env:CALLSTACK_AUTH_TOKEN}"]],
2754
+ ], `{env:${envVar}}`]],
2717
2755
  ...Object.entries(OPENCODE_MODEL).map(([key, value]) => [[
2718
2756
  "provider",
2719
- "callstack.ai",
2757
+ id,
2720
2758
  "models",
2721
- MODEL,
2759
+ model,
2722
2760
  key
2723
2761
  ], key === "options" ? {
2724
- ...existing.provider?.["callstack.ai"]?.models?.[MODEL]?.options,
2762
+ ...existing.provider?.[id]?.models?.[model]?.options,
2725
2763
  ...value
2726
2764
  } : value])
2727
2765
  ];
2728
2766
  });
2729
- if (v2) changes[0].steps = ["OpenCode 2 stores provider keys itself: run /connect in OpenCode, pick callstack.ai → \"Manually enter API Key\", and paste your Callstack key."];
2767
+ if (v2) changes[0].steps = [`OpenCode 2 stores provider keys itself: run /connect in OpenCode, pick ${provider.label} → "Manually enter API Key", and paste ${provider.keyName}.`];
2730
2768
  break;
2731
2769
  }
2732
- case "pi":
2770
+ case "pi": {
2771
+ const id = provider.pi;
2772
+ const entry = {
2773
+ id: model,
2774
+ ...PI_MODEL
2775
+ };
2733
2776
  await json("models.json", (existing) => {
2734
- const models = existing.providers?.callstack?.models ?? [];
2735
- const key = existing.providers?.callstack?.apiKey;
2736
- if (!Array.isArray(models) || models.some((model) => !model || typeof model.id !== "string")) throw new Error("Expected Pi models to be an array of model objects.");
2777
+ const models = existing.providers?.[id]?.models ?? [];
2778
+ const key = existing.providers?.[id]?.apiKey;
2779
+ if (!Array.isArray(models) || models.some((item) => !item || typeof item.id !== "string")) throw new Error("Expected Pi models to be an array of model objects.");
2737
2780
  return [
2738
2781
  [[
2739
2782
  "providers",
2740
- "callstack",
2783
+ id,
2741
2784
  "baseUrl"
2742
- ], BASE_URL],
2785
+ ], provider.baseUrl],
2743
2786
  [[
2744
2787
  "providers",
2745
- "callstack",
2788
+ id,
2746
2789
  "api"
2747
2790
  ], "openai-completions"],
2748
2791
  ...key && key !== PI_PLACEHOLDER ? [] : [[[
2749
2792
  "providers",
2750
- "callstack",
2793
+ id,
2751
2794
  "apiKey"
2752
- ], "$CALLSTACK_AUTH_TOKEN"]],
2795
+ ], `$${envVar}`]],
2753
2796
  [[
2754
2797
  "providers",
2755
- "callstack",
2798
+ id,
2756
2799
  "models"
2757
- ], models.some((model) => model.id === "callstack/Apex") ? models.map((model) => model.id === "callstack/Apex" ? {
2758
- ...model,
2759
- ...PI_MODEL,
2760
- ...model.compat ? { compat: Object.fromEntries(Object.entries(model.compat).filter(([key]) => key !== "reasoningEffortMap")) } : {}
2761
- } : model) : [...models, PI_MODEL]]
2800
+ ], models.some((item) => item.id === model) ? models.map((item) => item.id === model ? {
2801
+ ...item,
2802
+ ...entry,
2803
+ ...item.compat ? { compat: Object.fromEntries(Object.entries(item.compat).filter(([key]) => key !== "reasoningEffortMap")) } : {}
2804
+ } : item) : [...models, entry]]
2762
2805
  ];
2763
2806
  });
2764
- await json("settings.json", [[["modelThinkingLevels", `callstack/${MODEL}`], "medium"]]);
2807
+ await json("settings.json", [[["modelThinkingLevels", `${id}/${model}`], "medium"]]);
2765
2808
  break;
2809
+ }
2766
2810
  case "claude":
2767
2811
  await json("settings.json", [[["env", "CLAUDE_CODE_ATTRIBUTION_HEADER"], "0"]]);
2768
2812
  break;
2769
2813
  case "codex": {
2770
- const path = join(assistant.directory, "callstack_ai.config.toml");
2814
+ const path = join(assistant.directory, `${provider.codex}.config.toml`);
2771
2815
  const before = await readConfig(path);
2772
2816
  const after = updateToml(before, {
2773
- model_provider: "callstack_ai",
2774
- model: MODEL,
2817
+ model_provider: provider.codex,
2818
+ model,
2775
2819
  model_context_window: CONTEXT_WINDOW,
2776
2820
  model_reasoning_effort: "medium",
2777
- model_providers: { callstack_ai: {
2778
- name: "callstack.ai",
2779
- base_url: BASE_URL,
2780
- env_key: "CALLSTACK_AUTH_TOKEN",
2821
+ model_providers: { [provider.codex]: {
2822
+ name: provider.label,
2823
+ base_url: provider.codexUrl,
2824
+ env_key: envVar,
2781
2825
  wire_api: "responses",
2782
2826
  requires_openai_auth: false
2783
2827
  } }
@@ -2793,25 +2837,26 @@ async function planAssistant(assistant) {
2793
2837
  }
2794
2838
  return changes;
2795
2839
  }
2796
- function launchOptions(id, env = process.env) {
2797
- const token = env.CALLSTACK_AUTH_TOKEN;
2798
- if (!token?.trim()) throw new Error("Set CALLSTACK_AUTH_TOKEN in your environment before launching.");
2840
+ function launchOptions(id, env = process.env, provider = defaultProvider(env)) {
2841
+ const { model, envVar } = provider;
2842
+ const token = env[envVar];
2843
+ if (!token?.trim()) throw new Error(`Set ${envVar} in your environment before launching with ${provider.name}.`);
2799
2844
  const childEnv = { ...env };
2800
2845
  switch (id) {
2801
2846
  case "opencode": return {
2802
- args: ["--model", `callstack.ai/${MODEL}`],
2847
+ args: ["--model", `${provider.opencode}/${model}`],
2803
2848
  env: childEnv
2804
2849
  };
2805
2850
  case "codex": return {
2806
- args: ["--profile", "callstack_ai"],
2851
+ args: ["--profile", provider.codex],
2807
2852
  env: childEnv
2808
2853
  };
2809
2854
  case "pi": return {
2810
2855
  args: [
2811
2856
  "--provider",
2812
- "callstack",
2857
+ provider.pi,
2813
2858
  "--model",
2814
- MODEL,
2859
+ model,
2815
2860
  "--thinking",
2816
2861
  "medium"
2817
2862
  ],
@@ -2826,33 +2871,33 @@ function launchOptions(id, env = process.env) {
2826
2871
  "CLAUDE_CODE_USE_FOUNDRY"
2827
2872
  ]) delete childEnv[key];
2828
2873
  Object.assign(childEnv, {
2829
- ANTHROPIC_BASE_URL: "https://api.callstack.ai",
2874
+ ANTHROPIC_BASE_URL: provider.anthropicUrl,
2830
2875
  ANTHROPIC_AUTH_TOKEN: token,
2831
- ANTHROPIC_MODEL: MODEL,
2876
+ ANTHROPIC_MODEL: model,
2832
2877
  CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
2833
- ANTHROPIC_DEFAULT_OPUS_MODEL: MODEL,
2834
- ANTHROPIC_DEFAULT_SONNET_MODEL: MODEL,
2835
- ANTHROPIC_DEFAULT_HAIKU_MODEL: MODEL,
2836
- CLAUDE_CODE_SUBAGENT_MODEL: MODEL,
2878
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
2879
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
2880
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
2881
+ CLAUDE_CODE_SUBAGENT_MODEL: model,
2837
2882
  CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(CONTEXT_WINDOW),
2838
2883
  CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(MAX_OUTPUT)
2839
2884
  });
2840
2885
  return {
2841
- args: ["--model", MODEL],
2886
+ args: ["--model", model],
2842
2887
  env: childEnv
2843
2888
  };
2844
2889
  default: throw new Error(`Cannot launch ${id}. Use the editor's model selector.`);
2845
2890
  }
2846
2891
  }
2847
2892
  const RUN_NOTE = { claude: "(ANTHROPIC_* gateway environment set)" };
2848
- function runExpansion(id) {
2849
- const { args } = launchOptions(id, { CALLSTACK_AUTH_TOKEN: "placeholder" });
2893
+ function runExpansion(id, provider) {
2894
+ const { args } = launchOptions(id, { [provider.envVar]: "placeholder" }, provider);
2850
2895
  return [`${id} ${args.join(" ")}`, RUN_NOTE[id]].filter(Boolean).join(" ");
2851
2896
  }
2852
- const COPILOT_MODEL = JSON.stringify({
2853
- id: MODEL,
2897
+ const copilotModel = (provider) => JSON.stringify({
2898
+ id: provider.model,
2854
2899
  name: "Apex",
2855
- url: BASE_URL,
2900
+ url: provider.baseUrl,
2856
2901
  toolCalling: true,
2857
2902
  vision: true,
2858
2903
  thinking: true,
@@ -2861,32 +2906,39 @@ const COPILOT_MODEL = JSON.stringify({
2861
2906
  supportsReasoningEffort: EFFORTS,
2862
2907
  reasoningEffortFormat: "chat-completions"
2863
2908
  }, null, 2);
2864
- const AI_SDK_SNIPPET = [
2909
+ const aiSdkSnippet = (provider) => [
2865
2910
  "import { createOpenAI } from '@ai-sdk/openai';",
2866
2911
  "",
2867
2912
  "const apex = createOpenAI({",
2868
2913
  " name: 'callstack',",
2869
- ` baseURL: '${BASE_URL}',`,
2870
- " apiKey: process.env.CALLSTACK_AUTH_TOKEN,",
2914
+ ` baseURL: '${provider.appUrl}',`,
2915
+ ` apiKey: process.env.${provider.envVar},`,
2871
2916
  "});",
2872
2917
  "const requestOptions = {",
2873
- ` model: apex('${MODEL}'),`,
2918
+ ` model: apex('${provider.model}'),`,
2874
2919
  ` maxOutputTokens: ${MAX_OUTPUT},`,
2875
2920
  "};",
2876
2921
  "// Pass requestOptions with your prompt to generateText / streamText.",
2877
2922
  `// Reasoning effort (${EFFORTS.join(", ")}):`,
2878
2923
  "// providerOptions: { callstack: { reasoningEffort: 'medium' } }"
2879
2924
  ];
2880
- const MANUAL = {
2881
- cursor: [`Cursor: Settings → Models → API Keys → OpenAI API Key. Enter your Callstack key, override the base URL with ${BASE_URL}, add and enable ${MODEL}, then select it in Agent.`, "Cursor has no way to declare what a custom model can do (tool calling, vision, reasoning); it infers that itself."],
2882
- copilot: [
2883
- "Installation of Copilot itself is not verified: Copilot → model selector → Manage Models → Add Models → Custom Endpoint → name callstack.ai → enter your key → Chat Completions.",
2884
- "Keep the generated apiKey secret reference; add this object to its models array:",
2885
- ...COPILOT_MODEL.split("\n")
2886
- ],
2887
- "ai-sdk": ["Apex CLI never writes your key into a project. Keep it in CALLSTACK_AUTH_TOKEN (for example in a .env that git ignores) and create the provider with:", ...AI_SDK_SNIPPET]
2888
- };
2889
- const MANUAL_IDS = Object.keys(MANUAL);
2925
+ const MANUAL_IDS = [
2926
+ "cursor",
2927
+ "copilot",
2928
+ "ai-sdk"
2929
+ ];
2930
+ function manualSteps(id, provider) {
2931
+ switch (id) {
2932
+ case "cursor": return [`Cursor: Settings → Models → API Keys → OpenAI API Key. Enter ${provider.keyName}, override the base URL with ${provider.cursorUrl}, add and enable ${provider.model}, then select it in Agent.`, "Cursor has no way to declare what a custom model can do (tool calling, vision, reasoning); it infers that itself."];
2933
+ case "copilot": return [
2934
+ `Installation of Copilot itself is not verified: Copilot → model selector → Manage Models → Add Models → Custom Endpoint → name ${provider.label} → enter ${provider.keyName} → Chat Completions.`,
2935
+ "Keep the generated apiKey secret reference; add this object to its models array:",
2936
+ ...copilotModel(provider).split("\n")
2937
+ ];
2938
+ case "ai-sdk": return [`Apex CLI never writes your key into a project. Keep it in ${provider.envVar} (for example in a .env that git ignores) and create the provider with:`, ...aiSdkSnippet(provider)];
2939
+ default: throw new Error(`${id} is set up automatically.`);
2940
+ }
2941
+ }
2890
2942
 
2891
2943
  //#endregion
2892
2944
  //#region node_modules/fast-string-truncated-width/dist/utils.js
@@ -3585,6 +3637,32 @@ var a = class extends y {
3585
3637
  });
3586
3638
  }
3587
3639
  };
3640
+ let n$1 = class n extends y {
3641
+ options;
3642
+ cursor = 0;
3643
+ get _selectedValue() {
3644
+ return this.options[this.cursor];
3645
+ }
3646
+ changeValue() {
3647
+ const e = this._selectedValue;
3648
+ this.value = e === void 0 ? void 0 : e.value;
3649
+ }
3650
+ constructor(e) {
3651
+ super(e, false), this.options = e.options;
3652
+ const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
3653
+ this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
3654
+ switch (s) {
3655
+ case "left":
3656
+ case "up":
3657
+ this.cursor = findCursor(this.cursor, -1, this.options);
3658
+ break;
3659
+ case "down":
3660
+ case "right": this.cursor = findCursor(this.cursor, 1, this.options);
3661
+ }
3662
+ this.changeValue();
3663
+ });
3664
+ }
3665
+ };
3588
3666
 
3589
3667
  //#endregion
3590
3668
  //#region node_modules/@clack/prompts/dist/index.mjs
@@ -3816,6 +3894,63 @@ const u = {
3816
3894
  block: unicodeOr("█", "#")
3817
3895
  };
3818
3896
  const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
3897
+ const c = (t, o) => t.includes(`
3898
+ `) ? t.split(`
3899
+ `).map((d) => o(d)).join(`
3900
+ `) : o(t);
3901
+ const select = (t) => {
3902
+ const o = (n, m) => {
3903
+ if (n === void 0) return "";
3904
+ const s = n.label ?? String(n.value);
3905
+ switch (m) {
3906
+ case "disabled": return `${styleText("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("gray", i))}${n.hint ? ` ${styleText("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
3907
+ case "selected": return `${c(s, (i) => styleText("dim", i))}`;
3908
+ case "active": return `${styleText("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`;
3909
+ case "cancelled": return `${c(s, (i) => styleText(["strikethrough", "dim"], i))}`;
3910
+ default: return `${styleText("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("dim", i))}`;
3911
+ }
3912
+ }, d = t.showInstructions ?? true;
3913
+ return new n$1({
3914
+ options: t.options,
3915
+ signal: t.signal,
3916
+ input: t.input,
3917
+ output: t.output,
3918
+ initialValue: t.initialValue,
3919
+ render() {
3920
+ const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText("gray", S_BAR)}
3921
+ ` : ""}${i}
3922
+ `;
3923
+ switch (this.state) {
3924
+ case "submit": {
3925
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
3926
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r)}`;
3927
+ }
3928
+ case "cancel": {
3929
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
3930
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r)}${n ? `
3931
+ ${styleText("gray", S_BAR)}` : ""}`;
3932
+ }
3933
+ default: {
3934
+ const r = n ? `${styleText("cyan", S_BAR)} ` : "", a = u.split(`
3935
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], f = p.join(`
3936
+ `), b = p.length + 1;
3937
+ return `${u}${r}${limitOptions({
3938
+ output: t.output,
3939
+ cursor: this.cursor,
3940
+ options: this.options,
3941
+ maxItems: t.maxItems,
3942
+ columnPadding: r.length,
3943
+ rowPadding: a + b,
3944
+ style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
3945
+ }).join(`
3946
+ ${r}`)}
3947
+ ${f}
3948
+ `;
3949
+ }
3950
+ }
3951
+ }
3952
+ }).prompt();
3953
+ };
3819
3954
  const i = `${styleText("gray", S_BAR)} `;
3820
3955
 
3821
3956
  //#endregion
@@ -4072,7 +4207,8 @@ const pad = (text, column) => {
4072
4207
  const value = String(text);
4073
4208
  return value + " ".repeat(Math.max(1, column - visible(value).length));
4074
4209
  };
4075
- const MODEL_PATTERN = new RegExp(`(?<![\\w/.-])${MODEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "g");
4210
+ const MODELS = [...new Set(Object.values(PROVIDERS).map((provider) => provider.model))].map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
4211
+ const MODEL_PATTERN = new RegExp(`(?<![\\w/.-])(?:${MODELS.join("|")})(?![\\w-])`, "g");
4076
4212
  const model = (text) => String(text).replace(MODEL_PATTERN, (match) => green(match));
4077
4213
  const STATUS_COLUMN = columnWidth(Object.values(STATUS).map((status) => status.word));
4078
4214
  function clip(text, limit) {
@@ -4176,9 +4312,21 @@ function bullet(marker, message) {
4176
4312
  const success = (message) => bullet(green("✔"), message);
4177
4313
  const warn = (message) => bullet(yellow("!"), message);
4178
4314
  const error = (message) => bullet(red("✖"), message);
4179
- async function selectAssistants(options, initial) {
4315
+ async function selectProvider(options, initial) {
4316
+ const chosen = await select({
4317
+ message: "Which gateway should your assistants use for Apex?",
4318
+ options,
4319
+ initialValue: initial
4320
+ });
4321
+ if (isCancel(chosen)) {
4322
+ cancel("Cancelled. Nothing was written.");
4323
+ return false;
4324
+ }
4325
+ return chosen;
4326
+ }
4327
+ async function selectAssistants(options, initial, modelId) {
4180
4328
  const chosen = await multiselect({
4181
- message: model(`Set up ${MODEL} for which assistants? ${dim("(space toggles, enter confirms)")}`),
4329
+ message: model(`Set up ${modelId} for which assistants? ${dim("(space toggles, enter confirms)")}`),
4182
4330
  options,
4183
4331
  initialValues: initial,
4184
4332
  required: false,
@@ -4387,6 +4535,12 @@ const FLAGS = {
4387
4535
  hint: "assistant ids",
4388
4536
  text: `comma list of: ${IDS.join(", ")} (skips the picker)`
4389
4537
  },
4538
+ provider: {
4539
+ type: "string",
4540
+ usage: "--provider <id>",
4541
+ hint: "gateway serving Apex",
4542
+ text: `${PROVIDER_IDS.join(" or ")}; defaults to the one whose key is set, else vercel (skips the question)`
4543
+ },
4390
4544
  "no-interactive": {
4391
4545
  type: "boolean",
4392
4546
  usage: "--no-interactive",
@@ -4431,6 +4585,7 @@ const COMMANDS = {
4431
4585
  summary: "configure assistants",
4432
4586
  flags: [
4433
4587
  "assistants",
4588
+ "provider",
4434
4589
  "no-interactive",
4435
4590
  "apply",
4436
4591
  "no-diff",
@@ -4439,9 +4594,13 @@ const COMMANDS = {
4439
4594
  ]
4440
4595
  },
4441
4596
  detect: {
4442
- args: "[--json]",
4597
+ args: "[--provider <id>] [--json]",
4443
4598
  summary: "show what Apex CLI can see on this machine",
4444
- flags: ["json", "help"]
4599
+ flags: [
4600
+ "provider",
4601
+ "json",
4602
+ "help"
4603
+ ]
4445
4604
  },
4446
4605
  undo: {
4447
4606
  args: "[--list] [options]",
@@ -4456,7 +4615,7 @@ const COMMANDS = {
4456
4615
  ]
4457
4616
  },
4458
4617
  run: {
4459
- args: `<${RUNNABLE.join("|")}> [-- <args>]`,
4618
+ args: `[--provider <id>] <${RUNNABLE.join("|")}> [-- <args>]`,
4460
4619
  summary: "launch an assistant with the gateway set",
4461
4620
  flags: []
4462
4621
  },
@@ -4520,12 +4679,13 @@ const exampleRows = [
4520
4679
  ["apex init", "pick assistants, review, confirm"],
4521
4680
  ["apex init --assistants codex,pi", "preview two tools (agent and CI friendly)"],
4522
4681
  ["apex init --assistants codex,pi --apply", "write those two without asking"],
4682
+ ["apex init --provider callstack", "use the Callstack portal instead of Vercel AI Gateway"],
4523
4683
  ["apex undo", "preview the reversal"],
4524
4684
  ["apex undo --apply", "reverse the last setup without asking"],
4525
4685
  ["apex detect --json", "machine-readable inventory"]
4526
4686
  ];
4527
4687
  const environmentRows = [
4528
- ["CALLSTACK_AUTH_TOKEN", "your callstack.ai key, needed by apex run and by every assistant you configure"],
4688
+ ...Object.values(PROVIDERS).map((provider) => [provider.envVar, `${provider.keyName}, needed by apex run and by every assistant you configure for ${provider.name}`]),
4529
4689
  ["Config directories", "XDG_CONFIG_HOME, CODEX_HOME, CLAUDE_CONFIG_DIR, PI_CODING_AGENT_DIR"],
4530
4690
  ["Undo journal", "APEX_STATE_DIR, XDG_STATE_HOME (default ~/.local/state/apex)"]
4531
4691
  ];
@@ -4574,7 +4734,7 @@ function helpText() {
4574
4734
  ""
4575
4735
  ];
4576
4736
  return [
4577
- ...wrap(`Apex CLI: point your coding assistants at ${MODEL}`, width$1).map((line) => bold(model(line))),
4737
+ ...wrap(`Apex CLI: point your coding assistants at Apex via ${Object.values(PROVIDERS).map((provider) => provider.name).join(" or the ")}`, width$1).map((line) => bold(model(line))),
4578
4738
  "",
4579
4739
  ...section("Usage", usageRows.map(([lead, text]) => [
4580
4740
  lead,
@@ -4614,9 +4774,11 @@ function readFlags(rest, command) {
4614
4774
  const selected = values.assistants === void 0 ? null : [...new Set(values.assistants.split(",").map((id) => id.trim()))];
4615
4775
  if (selected?.includes("")) throw new Error("--assistants needs at least one id.");
4616
4776
  if (selected?.some((id) => !IDS.includes(id))) throw new Error(`Unknown assistant in --assistants. Choose from: ${IDS.join(", ")}`);
4777
+ if (values.provider !== void 0 && !PROVIDER_IDS.includes(values.provider)) throw new Error(`Unknown --provider. Choose from: ${PROVIDER_IDS.join(", ")}`);
4617
4778
  const json = Boolean(values.json);
4618
4779
  return {
4619
4780
  selected,
4781
+ provider: values.provider === void 0 ? null : PROVIDERS[values.provider],
4620
4782
  showDiff: !values["no-diff"],
4621
4783
  json,
4622
4784
  help: Boolean(values.help),
@@ -4627,7 +4789,7 @@ function readFlags(rest, command) {
4627
4789
  })
4628
4790
  };
4629
4791
  }
4630
- async function planAll(assistants, options) {
4792
+ async function planAll(assistants, options, provider) {
4631
4793
  return Promise.all(assistants.map(async (assistant) => {
4632
4794
  const base = {
4633
4795
  id: assistant.id,
@@ -4640,7 +4802,7 @@ async function planAll(assistants, options) {
4640
4802
  steps: []
4641
4803
  };
4642
4804
  try {
4643
- base.files = (await planAssistant(assistant)).map((change) => describeFile(change, options));
4805
+ base.files = (await planAssistant(assistant, provider)).map((change) => describeFile(change, options));
4644
4806
  base.steps = base.files.flatMap((file) => file.change.steps ?? []);
4645
4807
  } catch (error) {
4646
4808
  base.error = error.message;
@@ -4648,7 +4810,7 @@ async function planAll(assistants, options) {
4648
4810
  return base;
4649
4811
  }));
4650
4812
  }
4651
- const token = () => process.env.CALLSTACK_AUTH_TOKEN?.trim() || void 0;
4813
+ const token = (provider) => process.env[provider.envVar]?.trim() || void 0;
4652
4814
  const stamp = (iso) => iso.replace("T", " ").replace(/\.\d{3}Z$/, " UTC");
4653
4815
  const pendingOf = (entry) => entry.files.filter((file) => file.status !== "unchanged");
4654
4816
  async function applyAll(items, apply) {
@@ -4681,6 +4843,11 @@ const jsonAssistant = (entry, secret) => ({
4681
4843
  ...entry.steps.length ? { steps: entry.steps } : {},
4682
4844
  files: entry.files.map((file) => jsonFile(file, secret))
4683
4845
  });
4846
+ const providerOptions = () => Object.values(PROVIDERS).map((provider) => ({
4847
+ value: provider.id,
4848
+ label: provider.name,
4849
+ hint: `${provider.model}, key in ${provider.envVar}${token(provider) ? " (set)" : ""}`
4850
+ }));
4684
4851
  function pickerOptions(entries) {
4685
4852
  return entries.map((entry) => {
4686
4853
  const pending = pendingOf(entry).length;
@@ -4697,24 +4864,29 @@ async function commandInit(rest) {
4697
4864
  printHelp();
4698
4865
  return;
4699
4866
  }
4700
- const secret = token();
4867
+ const journalFile = journalPath();
4868
+ if (flags.mode !== "preview") await readJournal(journalFile);
4869
+ const batch = randomUUID();
4870
+ const asking = !flags.json && flags.mode === "prompt";
4871
+ let provider = flags.provider ?? defaultProvider();
4872
+ if (!flags.json) intro("Apex CLI · Configure Apex for your favorite harness", MODE_NOTE[flags.mode]);
4873
+ if (asking && !flags.provider) {
4874
+ const picked = await selectProvider(providerOptions(), provider.id);
4875
+ if (picked === false) return;
4876
+ provider = PROVIDERS[picked];
4877
+ }
4878
+ const secret = token(provider);
4701
4879
  const options = {
4702
4880
  secret,
4703
4881
  showDiff: flags.showDiff && !flags.json
4704
4882
  };
4705
- const journalFile = journalPath();
4706
- if (flags.mode !== "preview") await readJournal(journalFile);
4707
- const batch = randomUUID();
4708
- const entries = await planAll(await detect(), options);
4883
+ const entries = await planAll(await detect(), options, provider);
4709
4884
  let ids = flags.selected ?? entries.filter((entry) => entry.detected).map((entry) => entry.id);
4710
- if (!flags.json) {
4711
- intro("Apex CLI · Configure callstack/Apex for your favorite harness", MODE_NOTE[flags.mode]);
4712
- if (flags.mode === "prompt" && !flags.selected) {
4713
- const defaults = entries.filter((entry) => entry.detected && !entry.manual && !entry.error && pendingOf(entry).length).map((entry) => entry.id);
4714
- const picked = await selectAssistants(pickerOptions(entries), defaults);
4715
- if (picked === false) return;
4716
- ids = picked;
4717
- }
4885
+ if (asking && !flags.selected) {
4886
+ const defaults = entries.filter((entry) => entry.detected && !entry.manual && !entry.error && pendingOf(entry).length).map((entry) => entry.id);
4887
+ const picked = await selectAssistants(pickerOptions(entries), defaults, provider.model);
4888
+ if (picked === false) return;
4889
+ ids = picked;
4718
4890
  }
4719
4891
  const chosen = ids.map((id) => entries.find((entry) => entry.id === id));
4720
4892
  const blocked = chosen.filter((entry) => entry.error);
@@ -4735,8 +4907,9 @@ async function commandInit(rest) {
4735
4907
  console.log(JSON.stringify({
4736
4908
  command: "init",
4737
4909
  mode: flags.mode,
4738
- model: MODEL,
4739
- endpoint: BASE_URL,
4910
+ provider: provider.id,
4911
+ model: provider.model,
4912
+ endpoint: provider.baseUrl,
4740
4913
  applied: applied.length,
4741
4914
  appliedPaths: applied,
4742
4915
  ...failure ? { error: failure.message } : {},
@@ -4748,7 +4921,7 @@ async function commandInit(rest) {
4748
4921
  assistants: chosen.map((entry) => jsonAssistant(entry, secret)),
4749
4922
  journal: journalFile,
4750
4923
  undo: `${APEX} undo`,
4751
- envVar: "CALLSTACK_AUTH_TOKEN",
4924
+ envVar: provider.envVar,
4752
4925
  nextSteps: active.filter((entry) => RUNNABLE.includes(entry.id)).map((entry) => `${APEX} run ${entry.id}`)
4753
4926
  }, null, 2));
4754
4927
  if (blocked.length || failure) process.exitCode = 1;
@@ -4758,7 +4931,10 @@ async function commandInit(rest) {
4758
4931
  outro(`No assistant selected, so nothing was written. Pick one with --assistants or run ${APEX} init interactively.`);
4759
4932
  return;
4760
4933
  }
4761
- if (flags.mode !== "prompt") prose(`${gray("setup for:")} ${chosen.map((entry) => entry.name + (entry.detected ? "" : " (not detected)")).join(", ")}`);
4934
+ if (flags.mode !== "prompt") {
4935
+ prose(`${gray("gateway:")} ${provider.name}`);
4936
+ prose(`${gray("setup for:")} ${chosen.map((entry) => entry.name + (entry.detected ? "" : " (not detected)")).join(", ")}`);
4937
+ }
4762
4938
  if (blocked.length) {
4763
4939
  plain("");
4764
4940
  for (const entry of blocked) error(`${entry.name}: ${shortPath(entry.error)}`);
@@ -4786,23 +4962,24 @@ async function commandInit(rest) {
4786
4962
  }
4787
4963
  const apex = APEX !== "apex" && flags.mode === "prompt" && await installApex() ? "apex" : APEX;
4788
4964
  for (const entry of active.filter((entry) => entry.steps.length)) section(`Finish setting up ${entry.name}`, entry.steps);
4789
- for (const entry of manual) section(`Manual setup for ${entry.name}: Apex CLI writes nothing`, MANUAL[entry.id]);
4965
+ for (const entry of manual) section(`Manual setup for ${entry.name}: Apex CLI writes nothing`, manualSteps(entry.id, provider));
4790
4966
  section("Environment", [
4791
- ...secret ? [] : ["export CALLSTACK_AUTH_TOKEN=<your callstack.ai key>"],
4792
- secret ? `${green("✓")} CALLSTACK_AUTH_TOKEN is set in this shell` : `${yellow("!")} CALLSTACK_AUTH_TOKEN is not set here yet`,
4967
+ ...secret ? [] : [`export ${provider.envVar}=<${provider.keyName.replace(/^your /, "")}>`],
4968
+ secret ? `${green("✓")} ${provider.envVar} is set in this shell` : `${yellow("!")} ${provider.envVar} is not set here yet`,
4969
+ ...!secret && provider.keysUrl ? [`Create a key at ${link(provider.keysUrl)}`] : [],
4793
4970
  dim("Apex CLI never reads, stores or logs the key itself.")
4794
4971
  ]);
4795
4972
  const runnable = active.map((entry) => entry.id).filter((id) => RUNNABLE.includes(id));
4796
4973
  const rows = runnable.map((id) => ({
4797
4974
  command: `${apex} run ${id}`,
4798
- expansion: runExpansion(id)
4975
+ expansion: runExpansion(id, provider)
4799
4976
  }));
4800
4977
  const commandWidth = columnWidth(rows.map((row) => row.command));
4801
- section(runnable.length ? `Use these commands to run ${MODEL} with your selected harnesses:` : `Use ${MODEL} with your assistant:`, [
4978
+ section(runnable.length ? `Use these commands to run ${provider.model} with your selected harnesses:` : `Use ${provider.model} with your assistant:`, [
4802
4979
  ...rows.flatMap((row) => wrap(row.expansion, width() - 3 - commandWidth).map((part, index) => `${pad(index ? "" : row.command, commandWidth)}${dim(part)}`)),
4803
4980
  "",
4804
- `...or pick "${MODEL}" from the UI when setting up manually.`,
4805
- `For more instructions, visit: ${link(GUIDE_URL)}`,
4981
+ `...or pick "${provider.model}" from the UI when setting up manually.`,
4982
+ `For more instructions, visit: ${link(provider.guideUrl)}`,
4806
4983
  ...applied ? ["", `If you want to undo the changes, run ${bold(`${apex} undo`)}`] : [],
4807
4984
  ...apex === "apex" ? [] : ["", dim("For the short apex command: npm install -g @callstack/apex")]
4808
4985
  ]);
@@ -4813,16 +4990,17 @@ async function commandDetect(rest) {
4813
4990
  printHelp();
4814
4991
  return;
4815
4992
  }
4816
- const secret = token();
4993
+ const provider = flags.provider ?? defaultProvider();
4994
+ const secret = token(provider);
4817
4995
  const entries = await planAll(await detect(), {
4818
4996
  secret,
4819
4997
  showDiff: false
4820
- });
4998
+ }, provider);
4821
4999
  if (flags.json) {
4822
5000
  console.log(JSON.stringify(entries.map((entry) => jsonAssistant(entry, secret)), null, 2));
4823
5001
  return;
4824
5002
  }
4825
- intro("Apex CLI · What this machine has", "Read only: nothing is created, edited or removed.");
5003
+ intro(`Apex CLI · What this machine has for ${provider.name}`, "Read only: nothing is created, edited or removed.");
4826
5004
  const rows = entries.map((entry) => {
4827
5005
  const pending = pendingOf(entry).length;
4828
5006
  return {
@@ -4848,7 +5026,7 @@ async function commandUndo(rest) {
4848
5026
  printHelp();
4849
5027
  return;
4850
5028
  }
4851
- const secret = token();
5029
+ const secret = token(defaultProvider());
4852
5030
  const options = {
4853
5031
  secret,
4854
5032
  showDiff: flags.showDiff && !flags.json
@@ -4946,10 +5124,33 @@ async function commandUndo(rest) {
4946
5124
  }
4947
5125
  })).applied) outro("Undone. Configs are back to how they were. Restart any open assistant to reload them.");
4948
5126
  }
5127
+ function takeProvider(args) {
5128
+ const [first, second] = args;
5129
+ if (first === "--provider") return {
5130
+ value: second ?? "",
5131
+ rest: args.slice(2)
5132
+ };
5133
+ if (first?.startsWith("--provider=")) return {
5134
+ value: first.slice(11),
5135
+ rest: args.slice(1)
5136
+ };
5137
+ return {
5138
+ value: null,
5139
+ rest: args
5140
+ };
5141
+ }
4949
5142
  async function commandRun(rest) {
4950
- const [id, ...passed] = rest;
5143
+ const before = takeProvider(rest);
5144
+ const [id, ...after] = before.rest;
4951
5145
  if (!RUNNABLE.includes(id)) throw new Error(`apex run needs one of: ${RUNNABLE.join(", ")}. See ${APEX} --help.`);
4952
- const options = launchOptions(id);
5146
+ const behind = before.value === null ? takeProvider(after) : {
5147
+ value: null,
5148
+ rest: after
5149
+ };
5150
+ const chosen = before.value ?? behind.value;
5151
+ if (chosen !== null && !PROVIDER_IDS.includes(chosen)) throw new Error(`Unknown --provider. Choose from: ${PROVIDER_IDS.join(", ")}`);
5152
+ const passed = behind.rest;
5153
+ const options = launchOptions(id, process.env, chosen === null ? defaultProvider() : PROVIDERS[chosen]);
4953
5154
  const binary = await executable(id);
4954
5155
  if (!binary) throw new Error(`${id} was not found on PATH. Install it first, then run ${APEX} init.`);
4955
5156
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(binary)) throw new Error("Windows .cmd/.bat launchers are not executed through a shell. Use WSL or launch the configured assistant directly.");
@@ -4972,7 +5173,8 @@ const completionsFor = (command) => COMMANDS[command].flags.map((flag) => ({
4972
5173
  long: `--${flag}`,
4973
5174
  short: FLAGS[flag].short ? `-${FLAGS[flag].short}` : null,
4974
5175
  hint: FLAGS[flag].hint,
4975
- ...command === "init" && flag === "assistants" ? { values: IDS } : {}
5176
+ ...command === "init" && flag === "assistants" ? { values: IDS } : {},
5177
+ ...flag === "provider" ? { values: PROVIDER_IDS } : {}
4976
5178
  }));
4977
5179
  const spellings = (entry) => [entry.long, entry.short].filter(Boolean);
4978
5180
  function completionScript(shell) {
@@ -4980,15 +5182,18 @@ function completionScript(shell) {
4980
5182
  const words = (list) => list.join(" ");
4981
5183
  if (shell === "zsh") return `#compdef apex
4982
5184
  _apex() {
4983
- local -a commands assistants runnable${taking.length ? " " : ""}${words(taking.map((name) => `${name}_flags`))}
5185
+ local -a commands assistants providers runnable${taking.length ? " " : ""}${words(taking.map((name) => `${name}_flags`))}
4984
5186
  commands=(${words([...Object.entries(COMMANDS).map(([name, command]) => `'${name}:${command.summary}'`), ...GLOBALS.map(([flag, text]) => `'${flag}:${text}'`)])})
4985
5187
  assistants=(${words(IDS)})
5188
+ providers=(${words(PROVIDER_IDS)})
4986
5189
  runnable=(${words(RUNNABLE)})
4987
5190
  ${taking.map((name) => ` ${name}_flags=(${words(completionsFor(name).flatMap((entry) => spellings(entry).map((word) => `'${word}:${entry.hint}'`)))})`).join("\n")}
4988
5191
  if (( CURRENT == 2 )); then
4989
5192
  _describe -t commands 'apex command' commands
4990
5193
  elif [[ "\${words[CURRENT - 1]}" == --assistants ]]; then
4991
5194
  _describe -t assistants 'assistant' assistants
5195
+ elif [[ "\${words[CURRENT - 1]}" == --provider ]]; then
5196
+ _describe -t providers 'gateway' providers
4992
5197
  else
4993
5198
  case "\${words[2]}" in
4994
5199
  ${taking.map((name) => ` ${name}) _describe -t flags 'option' ${name}_flags ;;`).join("\n")}
@@ -5003,6 +5208,7 @@ if [[ "\${funcstack[1]}" == _apex ]]; then _apex "$@"; else compdef _apex apex;
5003
5208
  if (shell === "bash") return `_apex_completions() {
5004
5209
  local sub="\${COMP_WORDS[1]}" prev="\${COMP_WORDS[COMP_CWORD - 1]}" current="\${COMP_WORDS[COMP_CWORD]}"
5005
5210
  if [[ "$prev" == --assistants ]]; then COMPREPLY=( $(compgen -W "${words(IDS)}" -- "$current") ); return; fi
5211
+ if [[ "$prev" == --provider ]]; then COMPREPLY=( $(compgen -W "${words(PROVIDER_IDS)}" -- "$current") ); return; fi
5006
5212
  if (( COMP_CWORD == 1 )); then
5007
5213
  COMPREPLY=( $(compgen -W "${words([...Object.keys(COMMANDS), ...GLOBALS.map(([flag]) => flag)])}" -- "$current") )
5008
5214
  return
@@ -5021,6 +5227,12 @@ complete -F _apex_completions apex
5021
5227
  ...GLOBALS.map(([flag, hint]) => `complete -c apex -f -n "__fish_use_subcommand" -l ${flag.slice(2)} -d '${hint}'`),
5022
5228
  ...taking.flatMap((name) => completionsFor(name).map((entry) => fish(`__fish_seen_subcommand_from ${name}`, entry))),
5023
5229
  ...RUNNABLE.map((id) => `complete -c apex -f -n "__fish_seen_subcommand_from run" -a ${id} -d 'assistant'`),
5230
+ fish("__fish_seen_subcommand_from run", {
5231
+ long: "--provider",
5232
+ short: null,
5233
+ hint: FLAGS.provider.hint,
5234
+ values: PROVIDER_IDS
5235
+ }),
5024
5236
  ...SHELLS.map((name) => `complete -c apex -f -n "__fish_seen_subcommand_from completion" -a ${name} -d 'shell'`)
5025
5237
  ].join("\n") + "\n";
5026
5238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/apex",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Detects the AI coding assistants installed on your machine and configures them to use Callstack Apex.",
5
5
  "license": "MIT",
6
6
  "keywords": [