@stigmer/cli 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/commands/delete.js +5 -2
  2. package/commands/delete.js.map +1 -1
  3. package/commands/setup.d.ts.map +1 -1
  4. package/commands/setup.js +11 -16
  5. package/commands/setup.js.map +1 -1
  6. package/local/daemon/components.d.ts.map +1 -1
  7. package/local/daemon/components.js +3 -2
  8. package/local/daemon/components.js.map +1 -1
  9. package/local/daemon/env.d.ts +0 -3
  10. package/local/daemon/env.d.ts.map +1 -1
  11. package/local/daemon/env.js +4 -8
  12. package/local/daemon/env.js.map +1 -1
  13. package/local/daemon/launch.d.ts.map +1 -1
  14. package/local/daemon/launch.js +9 -17
  15. package/local/daemon/launch.js.map +1 -1
  16. package/local/llm-config.d.ts +7 -7
  17. package/local/llm-config.d.ts.map +1 -1
  18. package/local/llm-config.js +29 -42
  19. package/local/llm-config.js.map +1 -1
  20. package/local/setup/wizard.d.ts +16 -7
  21. package/local/setup/wizard.d.ts.map +1 -1
  22. package/local/setup/wizard.js +41 -44
  23. package/local/setup/wizard.js.map +1 -1
  24. package/local/status.d.ts.map +1 -1
  25. package/local/status.js +16 -15
  26. package/local/status.js.map +1 -1
  27. package/package.json +5 -5
  28. package/resources/delete.d.ts +5 -1
  29. package/resources/delete.d.ts.map +1 -1
  30. package/resources/delete.js +40 -8
  31. package/resources/delete.js.map +1 -1
  32. package/resources/get-bindings.d.ts.map +1 -1
  33. package/resources/get-bindings.js +8 -0
  34. package/resources/get-bindings.js.map +1 -1
  35. package/resources/list.d.ts.map +1 -1
  36. package/resources/list.js +70 -4
  37. package/resources/list.js.map +1 -1
  38. package/src/commands/delete.ts +5 -2
  39. package/src/commands/setup.ts +13 -20
  40. package/src/local/daemon/components.test.ts +8 -2
  41. package/src/local/daemon/components.ts +3 -1
  42. package/src/local/daemon/env.test.ts +13 -6
  43. package/src/local/daemon/env.ts +10 -13
  44. package/src/local/daemon/launch.ts +9 -17
  45. package/src/local/llm-config.test.ts +16 -27
  46. package/src/local/llm-config.ts +27 -41
  47. package/src/local/setup/wizard.test.ts +19 -11
  48. package/src/local/setup/wizard.ts +42 -47
  49. package/src/local/status.test.ts +20 -0
  50. package/src/local/status.ts +17 -15
  51. package/src/resources/delete.ts +48 -9
  52. package/src/resources/get-bindings.ts +8 -0
  53. package/src/resources/list.ts +76 -4
  54. package/src/resources/mutate-verbs.integration.test.ts +178 -1
  55. package/src/resources/read-verbs.integration.test.ts +236 -2
@@ -1,9 +1,11 @@
1
1
  // `stigmer setup` — configure the LLM provider used for agent execution.
2
2
  //
3
3
  // Two paths share one decision core (local/setup/wizard.ts): a non-interactive
4
- // flag path (`--provider`, `--api-key`, …) for scripts and CI, and an
5
- // interactive menu when no provider flag is given. Both write to
6
- // backend.local.llm, preserving every other config field.
4
+ // flag path (`--provider`, `--api-key`, `--model`) for scripts and CI, and an
5
+ // interactive API-key prompt when no provider flag is given. Both write to
6
+ // backend.local.llm, preserving every other config field. The valid provider
7
+ // set is owned by wizard.ts's PROVIDER_CHOICES (Anthropic-only + skip — the
8
+ // only story the native runner serves).
7
9
 
8
10
  import type { Command } from "commander";
9
11
  import { homedir } from "node:os";
@@ -14,51 +16,42 @@ interface SetupFlags extends OutputFlags {
14
16
  provider?: string;
15
17
  model?: string;
16
18
  apiKey?: string;
17
- baseUrl?: string;
18
19
  }
19
20
 
20
- const PROVIDERS = ["anthropic", "openai", "ollama", "skip"] as const;
21
-
22
21
  export function registerSetup(program: Command): void {
23
22
  const setup = program
24
23
  .command("setup")
25
24
  .description("configure the LLM provider for agent execution")
26
- .option("--provider <name>", "provider: anthropic, openai, ollama, or skip (non-interactive)")
27
- .option("--model <name>", "model name (defaults per provider)")
28
- .option("--api-key <key>", "API key for a cloud provider")
29
- .option("--base-url <url>", "custom base URL (e.g. an Ollama endpoint)")
25
+ .option("--provider <name>", "provider: anthropic, or skip to clear (non-interactive)")
26
+ .option("--model <name>", "pin a specific model (defaults to the platform-selected model)")
27
+ .option("--api-key <key>", "Anthropic API key")
30
28
  .action((options: SetupFlags) => runSetup(options));
31
29
  addResultFlags(setup);
32
30
  }
33
31
 
34
32
  async function runSetup(options: SetupFlags): Promise<void> {
35
33
  const { load, save } = await import("../config/config.js");
36
- const { applyChoice, runInteractiveWizard } = await import("../local/setup/wizard.js");
34
+ const { PROVIDER_CHOICES, applyChoice, runInteractiveWizard } = await import("../local/setup/wizard.js");
37
35
  const { isRunning } = await import("../local/daemon/launch.js");
38
36
  const { CliExitError } = await import("../errors/cli-exit-error.js");
39
37
  const { ExitCode } = await import("../errors/exit-codes.js");
40
38
 
41
39
  const config = load();
42
40
 
43
- let updated: typeof config | null;
41
+ let updated: typeof config;
44
42
  if (options.provider !== undefined) {
45
43
  const provider = options.provider.toLowerCase();
46
- if (!PROVIDERS.includes(provider as (typeof PROVIDERS)[number])) {
44
+ if (!(PROVIDER_CHOICES as readonly string[]).includes(provider)) {
47
45
  throw new CliExitError(`unknown provider: ${options.provider}`, ExitCode.Usage, [
48
- `Valid providers: ${PROVIDERS.join(", ")}`,
46
+ `Valid providers: ${PROVIDER_CHOICES.join(", ")}`,
49
47
  ]);
50
48
  }
51
- updated = applyChoice(config, provider as (typeof PROVIDERS)[number], {
49
+ updated = applyChoice(config, provider as (typeof PROVIDER_CHOICES)[number], {
52
50
  model: options.model,
53
51
  apiKey: options.apiKey,
54
- baseUrl: options.baseUrl,
55
52
  });
56
53
  } else {
57
54
  updated = await runInteractiveWizard(config);
58
- if (updated === null) {
59
- renderResult(CommandResult.warning("Setup cancelled"), resultFormat(options));
60
- return;
61
- }
62
55
  }
63
56
 
64
57
  save(updated);
@@ -47,10 +47,9 @@ describe("buildRunnerEnv", () => {
47
47
  expect(withKey.STIGMER_ACTIVITY_ROUTING).toBe("session");
48
48
  });
49
49
 
50
- it("forwards LLM provider keys only when set, resolved value winning over the base env", () => {
50
+ it("forwards the Anthropic key only when set, resolved value winning over the base env", () => {
51
51
  const bare = buildRunnerEnv(config, {});
52
52
  expect(bare.ANTHROPIC_API_KEY).toBeUndefined();
53
- expect(bare.OPENAI_API_KEY).toBeUndefined();
54
53
 
55
54
  // The contract value is authoritative: the launcher already applied the
56
55
  // env > config precedence, so whatever sits in the child's base env must
@@ -62,6 +61,13 @@ describe("buildRunnerEnv", () => {
62
61
  expect(env.ANTHROPIC_API_KEY).toBe("sk-ant-resolved");
63
62
  });
64
63
 
64
+ // Anthropic is the only key with a contract slot; other provider keys (for
65
+ // advanced per-agent overrides) flow through shell-env inheritance untouched.
66
+ it("passes other shell-exported provider keys through via the base env spread", () => {
67
+ const env = buildRunnerEnv(config, { OPENAI_API_KEY: "sk-oai-shell" });
68
+ expect(env.OPENAI_API_KEY).toBe("sk-oai-shell");
69
+ });
70
+
65
71
  // The regression guard for the queue bug: the server dispatches to, and the
66
72
  // runner polls, the exact same queue.
67
73
  it("pins the runner's poll queue to the server's dispatch queue", () => {
@@ -53,8 +53,10 @@ export function buildRunnerEnv(config: DaemonConfig, base: NodeJS.ProcessEnv = p
53
53
  if (config.cursorApiKey !== undefined) env.CURSOR_API_KEY = config.cursorApiKey;
54
54
  // Explicit set (after the base spread) so the launcher-resolved key wins over any
55
55
  // stale inherited value — the delivery path for a `stigmer setup`-persisted key.
56
+ // Anthropic only: it is the sole provider local execution supports. Other keys
57
+ // (e.g. a shell-exported OPENAI_API_KEY for advanced per-agent gpt-* overrides)
58
+ // still reach the runner through the base env spread above.
56
59
  if (config.anthropicApiKey !== undefined) env.ANTHROPIC_API_KEY = config.anthropicApiKey;
57
- if (config.openaiApiKey !== undefined) env.OPENAI_API_KEY = config.openaiApiKey;
58
60
  if (config.activityRouting !== undefined) env.STIGMER_ACTIVITY_ROUTING = config.activityRouting;
59
61
  return env;
60
62
  }
@@ -27,7 +27,6 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
27
27
  runner: { nodeBin: "/usr/bin/node", entryPath: "/repo/runner/dist/main.js", appDir: "/repo/runner" },
28
28
  cursorApiKey: undefined,
29
29
  anthropicApiKey: undefined,
30
- openaiApiKey: undefined,
31
30
  activityRouting: undefined,
32
31
  });
33
32
  });
@@ -48,18 +47,26 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
48
47
 
49
48
  // The config-file-only delivery path: a key from `stigmer setup` is not in the
50
49
  // shell env, so the launcher must write it into the contract explicitly.
51
- it("delivers a launcher-resolved LLM key with no help from the base env", () => {
50
+ // Anthropic is the only provider with this path — the local stack executes
51
+ // on Anthropic only.
52
+ it("delivers a launcher-resolved Anthropic key with no help from the base env", () => {
52
53
  const env = buildDaemonEnv({ ...baseInputs, anthropicApiKey: "sk-ant-cfg" }, {});
53
54
  const config = readDaemonConfig(env);
54
55
  expect(config.anthropicApiKey).toBe("sk-ant-cfg");
55
- expect(config.openaiApiKey).toBeUndefined();
56
56
  });
57
57
 
58
- it("passes shell-exported LLM keys through from the base env", () => {
59
- const env = buildDaemonEnv(baseInputs, { ANTHROPIC_API_KEY: "sk-ant-env", OPENAI_API_KEY: "sk-oai-env" });
58
+ it("passes a shell-exported Anthropic key through from the base env", () => {
59
+ const env = buildDaemonEnv(baseInputs, { ANTHROPIC_API_KEY: "sk-ant-env" });
60
60
  const config = readDaemonConfig(env);
61
61
  expect(config.anthropicApiKey).toBe("sk-ant-env");
62
- expect(config.openaiApiKey).toBe("sk-oai-env");
62
+ });
63
+
64
+ // Other provider keys have no contract slot; they reach the runner solely via
65
+ // shell-env inheritance (the child env spreads the base env).
66
+ it("leaves shell-exported non-Anthropic keys in the env without parsing them", () => {
67
+ const env = buildDaemonEnv(baseInputs, { OPENAI_API_KEY: "sk-oai-env" });
68
+ expect(env.OPENAI_API_KEY).toBe("sk-oai-env");
69
+ expect(readDaemonConfig(env)).not.toHaveProperty("openaiApiKey");
63
70
  });
64
71
 
65
72
  it("requires the data dir and server binary", () => {
@@ -1,8 +1,8 @@
1
1
  // The foreground launcher hands the supervised daemon everything it needs to
2
- // run through the environment — the same mechanism the Go CLI uses. Centralizing
3
- // the variable names and the (de)serialization here keeps that contract in one
4
- // place: the launcher writes it with buildDaemonEnv, the daemon reads it with
5
- // readDaemonConfig, and nothing in between guesses at string keys.
2
+ // run through the environment. Centralizing the variable names and the
3
+ // (de)serialization here keeps that contract in one place: the launcher writes
4
+ // it with buildDaemonEnv, the daemon reads it with readDaemonConfig, and
5
+ // nothing in between guesses at string keys.
6
6
 
7
7
  /** Environment variable names that make up the launcher -> daemon contract. */
8
8
  export const DaemonEnvVar = {
@@ -18,7 +18,6 @@ export const DaemonEnvVar = {
18
18
  RunnerAppDir: "STIGMER_RUNNER_APP_DIR",
19
19
  CursorApiKey: "CURSOR_API_KEY",
20
20
  AnthropicApiKey: "ANTHROPIC_API_KEY",
21
- OpenAiApiKey: "OPENAI_API_KEY",
22
21
  ActivityRouting: "STIGMER_ACTIVITY_ROUTING",
23
22
  } as const;
24
23
 
@@ -41,7 +40,6 @@ export interface DaemonConfig {
41
40
  runner?: RunnerLaunch;
42
41
  cursorApiKey?: string;
43
42
  anthropicApiKey?: string;
44
- openaiApiKey?: string;
45
43
  activityRouting?: string;
46
44
  }
47
45
 
@@ -55,12 +53,13 @@ export interface DaemonEnvInputs {
55
53
  noWeb: boolean;
56
54
  serverBin: string;
57
55
  runner?: RunnerLaunch;
58
- // LLM provider key resolved by the launcher (env > config file). Must be written
59
- // into the daemon env explicitly: unlike a shell-exported key, a key persisted by
60
- // `stigmer setup` exists only in the config file and would otherwise never reach
61
- // the runner. Only the effective provider's key is set.
56
+ // Anthropic API key resolved by the launcher (env > config file). Must be
57
+ // written into the daemon env explicitly: unlike a shell-exported key, a key
58
+ // persisted by `stigmer setup` exists only in the config file and would
59
+ // otherwise never reach the runner. Anthropic is the only provider with a
60
+ // persisted delivery path — other keys (OPENAI_API_KEY, CURSOR_API_KEY)
61
+ // reach the runner solely via shell-env inheritance.
62
62
  anthropicApiKey?: string;
63
- openaiApiKey?: string;
64
63
  }
65
64
 
66
65
  /**
@@ -83,7 +82,6 @@ export function buildDaemonEnv(inputs: DaemonEnvInputs, base: NodeJS.ProcessEnv
83
82
  env[DaemonEnvVar.RunnerAppDir] = inputs.runner.appDir;
84
83
  }
85
84
  if (inputs.anthropicApiKey !== undefined) env[DaemonEnvVar.AnthropicApiKey] = inputs.anthropicApiKey;
86
- if (inputs.openaiApiKey !== undefined) env[DaemonEnvVar.OpenAiApiKey] = inputs.openaiApiKey;
87
85
  return env;
88
86
  }
89
87
 
@@ -106,7 +104,6 @@ export function readDaemonConfig(env: NodeJS.ProcessEnv = process.env): DaemonCo
106
104
  runner: serverOnly ? undefined : runner,
107
105
  cursorApiKey: nonEmpty(env[DaemonEnvVar.CursorApiKey]),
108
106
  anthropicApiKey: nonEmpty(env[DaemonEnvVar.AnthropicApiKey]),
109
- openaiApiKey: nonEmpty(env[DaemonEnvVar.OpenAiApiKey]),
110
107
  activityRouting: nonEmpty(env[DaemonEnvVar.ActivityRouting]),
111
108
  };
112
109
  }
@@ -99,9 +99,8 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
99
99
  }
100
100
 
101
101
  /**
102
- * Bootstrap the system seedpack into the freshly-started local backend (the TS
103
- * equivalent of the Go daemon's EnsureSeedpackBootstrapped). Idempotent via a
104
- * content-hash marker, so repeated `up`s are cheap. Best-effort: the stack is
102
+ * Bootstrap the system seedpack into the freshly-started local backend.
103
+ * Idempotent via a content-hash marker, so repeated `up`s are cheap. Best-effort: the stack is
105
104
  * already serving by this point, so a seedpack failure is surfaced as a warning
106
105
  * with a retry hint rather than tearing the stack down.
107
106
  */
@@ -228,23 +227,16 @@ async function stopManagedTemporal(home: string): Promise<void> {
228
227
  // the config file, so the launcher must write it into the daemon contract explicitly
229
228
  // — unlike a shell-exported key, it cannot flow by env inheritance. resolveApiKey's
230
229
  // precedence (env var > config file) makes both cases one code path; when the env
231
- // var is set this re-writes the same value. Only the effective provider's key is
232
- // passed: the config file stores a single key, keyed to the chosen provider.
233
- function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicApiKey" | "openaiApiKey"> {
230
+ // var is set this re-writes the same value. Anthropic is the only provider the
231
+ // local stack executes on, so it is the only key with a persisted delivery path.
232
+ function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicApiKey"> {
233
+ if (resolveProvider(config) !== "anthropic") return {};
234
234
  const key = resolveApiKey(config);
235
- if (key === "") return {};
236
- switch (resolveProvider(config)) {
237
- case "anthropic":
238
- return { anthropicApiKey: key };
239
- case "openai":
240
- return { openaiApiKey: key };
241
- default:
242
- return {};
243
- }
235
+ return key === "" ? {} : { anthropicApiKey: key };
244
236
  }
245
237
 
246
- // Default to managed Temporal unless the opaque local config explicitly opts out
247
- // (Go parity: managed is the default). Wave 4's `setup` models this section.
238
+ // Default to managed Temporal unless the opaque local config explicitly opts
239
+ // out — zero-config `stigmer up` must work on a fresh machine.
248
240
  function isTemporalManaged(config: Config): boolean {
249
241
  const local = config.backend.local as { temporal?: { managed?: boolean } } | undefined;
250
242
  return local?.temporal?.managed !== false;
@@ -7,7 +7,6 @@ import {
7
7
  resolveModel,
8
8
  resolveProvider,
9
9
  setLlm,
10
- withProviderDefaults,
11
10
  } from "./llm-config.js";
12
11
 
13
12
  function config(local?: unknown): Config {
@@ -15,60 +14,50 @@ function config(local?: unknown): Config {
15
14
  }
16
15
 
17
16
  describe("detectProviderFromEnv", () => {
18
- it("prefers Anthropic, then OpenAI, else empty", () => {
19
- expect(detectProviderFromEnv({ ANTHROPIC_API_KEY: "a", OPENAI_API_KEY: "b" })).toBe("anthropic");
20
- expect(detectProviderFromEnv({ OPENAI_API_KEY: "b" })).toBe("openai");
17
+ it("detects Anthropic only — the sole provider local execution supports", () => {
18
+ expect(detectProviderFromEnv({ ANTHROPIC_API_KEY: "a" })).toBe("anthropic");
19
+ // Other provider keys in the shell must not flip the local story.
20
+ expect(detectProviderFromEnv({ OPENAI_API_KEY: "b" })).toBe("");
21
21
  expect(detectProviderFromEnv({})).toBe("");
22
22
  });
23
23
  });
24
24
 
25
25
  describe("resolveProvider", () => {
26
26
  it("honors env override > config > autodetect", () => {
27
- const cfg = config({ llm: { provider: "ollama" } });
28
- expect(resolveProvider(cfg, { STIGMER_LLM_PROVIDER: "openai" })).toBe("openai");
29
- expect(resolveProvider(cfg, {})).toBe("ollama");
27
+ const cfg = config({ llm: { provider: "anthropic" } });
28
+ expect(resolveProvider(cfg, { STIGMER_LLM_PROVIDER: "other" })).toBe("other");
29
+ expect(resolveProvider(cfg, {})).toBe("anthropic");
30
30
  expect(resolveProvider(config(), { ANTHROPIC_API_KEY: "x" })).toBe("anthropic");
31
31
  expect(resolveProvider(config(), {})).toBe("");
32
32
  });
33
33
  });
34
34
 
35
35
  describe("resolveModel", () => {
36
- it("falls back to the provider default", () => {
37
- expect(resolveModel(config({ llm: { provider: "anthropic" } }), {})).toBe("claude-sonnet-4.5");
38
- expect(resolveModel(config({ llm: { provider: "openai" } }), {})).toBe("gpt-4");
39
- expect(resolveModel(config({ llm: { provider: "ollama" } }), {})).toBe("qwen2.5-coder:7b");
36
+ it("returns the explicit override, else empty — the registry owns the default", () => {
37
+ // No hardcoded model version anywhere in the CLI: unset means "platform
38
+ // default", resolved by the runner from the model registry.
39
+ expect(resolveModel(config({ llm: { provider: "anthropic" } }), {})).toBe("");
40
+ expect(resolveModel(config(), {})).toBe("");
40
41
  expect(resolveModel(config({ llm: { provider: "anthropic", model: "claude-x" } }), {})).toBe("claude-x");
41
42
  expect(resolveModel(config({ llm: { provider: "anthropic" } }), { STIGMER_LLM_MODEL: "envm" })).toBe("envm");
42
43
  });
43
44
  });
44
45
 
45
46
  describe("resolveApiKey", () => {
46
- it("uses the provider-specific env var, then config", () => {
47
+ it("uses ANTHROPIC_API_KEY, then config", () => {
47
48
  const cfg = config({ llm: { provider: "anthropic", api_key: "cfgkey" } });
48
49
  expect(resolveApiKey(cfg, { ANTHROPIC_API_KEY: "envkey" })).toBe("envkey");
49
50
  expect(resolveApiKey(cfg, {})).toBe("cfgkey");
50
51
  });
51
- });
52
52
 
53
- describe("withProviderDefaults", () => {
54
- it("fills model and base URL per provider, keeping api_key only when set", () => {
55
- expect(withProviderDefaults({ provider: "anthropic" })).toEqual({ provider: "anthropic", model: "claude-sonnet-4.5" });
56
- expect(withProviderDefaults({ provider: "ollama" })).toEqual({
57
- provider: "ollama",
58
- model: "qwen2.5-coder:7b",
59
- base_url: "http://localhost:11434",
60
- });
61
- expect(withProviderDefaults({ provider: "openai", api_key: "k" })).toEqual({
62
- provider: "openai",
63
- model: "gpt-4",
64
- api_key: "k",
65
- });
53
+ it("returns empty for non-anthropic providers (e.g. hand-edited config)", () => {
54
+ expect(resolveApiKey(config({ llm: { provider: "other", api_key: "k" } }), {})).toBe("");
66
55
  });
67
56
  });
68
57
 
69
58
  describe("setLlm", () => {
70
59
  it("preserves sibling local keys and can clear the section", () => {
71
- const cfg = config({ temporal: { managed: true }, llm: { provider: "openai" } });
60
+ const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic" } });
72
61
  const updated = setLlm(cfg, { provider: "anthropic", model: "m" });
73
62
  expect(readLlm(updated)).toEqual({ provider: "anthropic", model: "m" });
74
63
  expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
@@ -2,12 +2,23 @@
2
2
  // the LLM settings `setup` writes and `status` reads.
3
3
  //
4
4
  // The config module (config/config.ts) carries `backend.local` through verbatim
5
- // so the TS CLI never drops fields it does not model. This module adds a *typed
6
- // lens* for the one sub-tree T05 owns — `local.llm` — and ports the Go CLI's
7
- // `ResolveLLM*` precedence (env override > config file > provider default) so the
8
- // two CLIs agree on which provider/model is effectively in force. Writes merge
9
- // into a shallow copy of `local`, preserving every sibling key (temporal,
10
- // execution, …).
5
+ // so the CLI never drops fields it does not model. This module adds a *typed
6
+ // lens* for the one sub-tree setup owns — `local.llm` — and resolves the
7
+ // effective provider/model with env-override precedence (env > config file >
8
+ // API-key autodetect). Writes merge into a shallow copy of `local`, preserving
9
+ // every sibling key (temporal, execution, …).
10
+ //
11
+ // Local agent execution is Anthropic-only: the native runner constructs
12
+ // Anthropic clients, and the platform model registry's native-harness entries
13
+ // are all Anthropic models. Two invariants follow:
14
+ //
15
+ // 1. The model registry — not this config — owns the default execution model.
16
+ // The runner resolves it from the registry at execution time, so this
17
+ // module never hardcodes a model version (hardcoded versions here drifted
18
+ // from the registry repeatedly). An unset model means "platform default".
19
+ // 2. `provider` exists to route the right API key to the runner, not to
20
+ // select an execution backend; "anthropic" is the only provider the local
21
+ // stack can serve.
11
22
 
12
23
  import type { Config } from "../config/config.js";
13
24
 
@@ -16,7 +27,6 @@ export interface LlmSettings {
16
27
  provider?: string;
17
28
  model?: string;
18
29
  api_key?: string;
19
- base_url?: string;
20
30
  }
21
31
 
22
32
  interface LocalSection {
@@ -24,18 +34,6 @@ interface LocalSection {
24
34
  [key: string]: unknown;
25
35
  }
26
36
 
27
- /** Default model per provider, matching the Go setup wizard's choices. */
28
- const DEFAULT_MODEL: Record<string, string> = {
29
- ollama: "qwen2.5-coder:7b",
30
- anthropic: "claude-sonnet-4.5",
31
- openai: "gpt-4",
32
- };
33
-
34
- /** Default base URL per provider (only Ollama needs one). */
35
- const DEFAULT_BASE_URL: Record<string, string> = {
36
- ollama: "http://localhost:11434",
37
- };
38
-
39
37
  function localSection(config: Config): LocalSection {
40
38
  const local = config.backend.local;
41
39
  return local !== null && typeof local === "object" ? (local as LocalSection) : {};
@@ -47,12 +45,11 @@ export function readLlm(config: Config): LlmSettings | undefined {
47
45
  }
48
46
 
49
47
  /**
50
- * Detect a provider from API keys present in the environment (Anthropic first,
51
- * then OpenAI), matching the Go CLI's `DetectProviderFromAPIKeys`.
48
+ * Detect a provider from API keys present in the environment. Anthropic is the
49
+ * only provider local execution supports, so only ANTHROPIC_API_KEY counts.
52
50
  */
53
51
  export function detectProviderFromEnv(env: NodeJS.ProcessEnv = process.env): string {
54
52
  if (env.ANTHROPIC_API_KEY) return "anthropic";
55
- if (env.OPENAI_API_KEY) return "openai";
56
53
  return "";
57
54
  }
58
55
 
@@ -64,33 +61,22 @@ export function resolveProvider(config: Config, env: NodeJS.ProcessEnv = process
64
61
  return detectProviderFromEnv(env);
65
62
  }
66
63
 
67
- /** Effective model: env override > config > provider default. */
64
+ /**
65
+ * Effective model override: env override > config. Empty string means "no
66
+ * override" — the runner picks the default from the platform model registry.
67
+ */
68
68
  export function resolveModel(config: Config, env: NodeJS.ProcessEnv = process.env): string {
69
69
  if (env.STIGMER_LLM_MODEL) return env.STIGMER_LLM_MODEL;
70
- const llm = readLlm(config);
71
- if (llm?.model) return llm.model;
72
- return DEFAULT_MODEL[resolveProvider(config, env)] ?? "";
70
+ return readLlm(config)?.model ?? "";
73
71
  }
74
72
 
75
- /** Effective API key: provider-specific env var > config. */
73
+ /** Effective API key: ANTHROPIC_API_KEY env var > config. */
76
74
  export function resolveApiKey(config: Config, env: NodeJS.ProcessEnv = process.env): string {
77
- const provider = resolveProvider(config, env);
78
- const envKey = provider === "anthropic" ? env.ANTHROPIC_API_KEY : provider === "openai" ? env.OPENAI_API_KEY : "";
79
- if (envKey) return envKey;
75
+ if (resolveProvider(config, env) !== "anthropic") return "";
76
+ if (env.ANTHROPIC_API_KEY) return env.ANTHROPIC_API_KEY;
80
77
  return readLlm(config)?.api_key ?? "";
81
78
  }
82
79
 
83
- /** Fill in provider-appropriate defaults for a partial selection. */
84
- export function withProviderDefaults(settings: LlmSettings): LlmSettings {
85
- const provider = settings.provider ?? "";
86
- const out: LlmSettings = { provider };
87
- out.model = settings.model ?? DEFAULT_MODEL[provider];
88
- if (settings.api_key) out.api_key = settings.api_key;
89
- const baseUrl = settings.base_url ?? DEFAULT_BASE_URL[provider];
90
- if (baseUrl) out.base_url = baseUrl;
91
- return out;
92
- }
93
-
94
80
  /**
95
81
  * Return a copy of `config` with the LLM section replaced, preserving every
96
82
  * other key under `backend.local`. Passing `undefined` clears the LLM section
@@ -1,25 +1,33 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { Config } from "../../config/config.js";
3
3
  import { readLlm } from "../llm-config.js";
4
- import { applyChoice, buildLlmForChoice } from "./wizard.js";
4
+ import { PROVIDER_CHOICES, applyChoice, buildLlmForChoice } from "./wizard.js";
5
5
 
6
6
  function config(local?: unknown): Config {
7
7
  return { backend: { type: "local", local } };
8
8
  }
9
9
 
10
+ // Regression pin: every provider offered by `stigmer setup` promises an
11
+ // execution path the native runner must actually serve. The runner constructs
12
+ // Anthropic clients only and the registry's native-harness entries are all
13
+ // Anthropic — so this list is anthropic + skip, full stop. If this test fails
14
+ // because a provider was added, the native runner and model registry must gain
15
+ // real support for it first (see sdk/react DISABLED_PROVIDERS for the same
16
+ // decision on the UI side).
17
+ describe("PROVIDER_CHOICES", () => {
18
+ it("offers exactly anthropic and skip", () => {
19
+ expect([...PROVIDER_CHOICES]).toEqual(["anthropic", "skip"]);
20
+ });
21
+ });
22
+
10
23
  describe("buildLlmForChoice", () => {
11
- it("applies provider defaults and overrides", () => {
12
- expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic", model: "claude-sonnet-4.5" });
24
+ it("persists no model unless explicitly overridden — the registry owns the default", () => {
25
+ expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic" });
13
26
  expect(buildLlmForChoice("anthropic", { apiKey: "k", model: "m" })).toEqual({
14
27
  provider: "anthropic",
15
28
  model: "m",
16
29
  api_key: "k",
17
30
  });
18
- expect(buildLlmForChoice("ollama", { baseUrl: "http://host:1" })).toEqual({
19
- provider: "ollama",
20
- model: "qwen2.5-coder:7b",
21
- base_url: "http://host:1",
22
- });
23
31
  });
24
32
 
25
33
  it("returns undefined for skip (clears config)", () => {
@@ -29,14 +37,14 @@ describe("buildLlmForChoice", () => {
29
37
 
30
38
  describe("applyChoice", () => {
31
39
  it("writes the selection while preserving siblings", () => {
32
- const cfg = config({ temporal: { managed: true }, llm: { provider: "openai" } });
40
+ const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic", model: "stale" } });
33
41
  const updated = applyChoice(cfg, "anthropic", { apiKey: "k" });
34
- expect(readLlm(updated)).toEqual({ provider: "anthropic", model: "claude-sonnet-4.5", api_key: "k" });
42
+ expect(readLlm(updated)).toEqual({ provider: "anthropic", api_key: "k" });
35
43
  expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
36
44
  });
37
45
 
38
46
  it("skip clears the llm section", () => {
39
- const updated = applyChoice(config({ llm: { provider: "openai" } }), "skip");
47
+ const updated = applyChoice(config({ llm: { provider: "anthropic" } }), "skip");
40
48
  expect(readLlm(updated)).toBeUndefined();
41
49
  });
42
50
  });