@stigmer/cli 3.4.0 → 3.5.0

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 (42) hide show
  1. package/commands/setup.d.ts.map +1 -1
  2. package/commands/setup.js +11 -16
  3. package/commands/setup.js.map +1 -1
  4. package/local/daemon/components.d.ts.map +1 -1
  5. package/local/daemon/components.js +3 -2
  6. package/local/daemon/components.js.map +1 -1
  7. package/local/daemon/env.d.ts +0 -3
  8. package/local/daemon/env.d.ts.map +1 -1
  9. package/local/daemon/env.js +4 -8
  10. package/local/daemon/env.js.map +1 -1
  11. package/local/daemon/launch.d.ts.map +1 -1
  12. package/local/daemon/launch.js +9 -17
  13. package/local/daemon/launch.js.map +1 -1
  14. package/local/llm-config.d.ts +7 -7
  15. package/local/llm-config.d.ts.map +1 -1
  16. package/local/llm-config.js +29 -42
  17. package/local/llm-config.js.map +1 -1
  18. package/local/setup/wizard.d.ts +16 -7
  19. package/local/setup/wizard.d.ts.map +1 -1
  20. package/local/setup/wizard.js +41 -44
  21. package/local/setup/wizard.js.map +1 -1
  22. package/local/status.d.ts.map +1 -1
  23. package/local/status.js +16 -15
  24. package/local/status.js.map +1 -1
  25. package/package.json +5 -5
  26. package/resources/list.d.ts.map +1 -1
  27. package/resources/list.js +70 -4
  28. package/resources/list.js.map +1 -1
  29. package/src/commands/setup.ts +13 -20
  30. package/src/local/daemon/components.test.ts +8 -2
  31. package/src/local/daemon/components.ts +3 -1
  32. package/src/local/daemon/env.test.ts +13 -6
  33. package/src/local/daemon/env.ts +10 -13
  34. package/src/local/daemon/launch.ts +9 -17
  35. package/src/local/llm-config.test.ts +16 -27
  36. package/src/local/llm-config.ts +27 -41
  37. package/src/local/setup/wizard.test.ts +19 -11
  38. package/src/local/setup/wizard.ts +42 -47
  39. package/src/local/status.test.ts +20 -0
  40. package/src/local/status.ts +17 -15
  41. package/src/resources/list.ts +76 -4
  42. package/src/resources/read-verbs.integration.test.ts +127 -3
@@ -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
  });
@@ -1,35 +1,47 @@
1
1
  // LLM provider selection for `stigmer setup`.
2
2
  //
3
3
  // The decision logic is a pure function (`buildLlmForChoice`) shared by the
4
- // non-interactive flag path and the interactive menu, so both produce identical
5
- // config and the rules are unit-testable in isolation. The interactive menu
6
- // (`runInteractiveWizard`) is a thin shell around it, mirroring the Go wizard's
7
- // provider list and API-key handling (env var first, then a hidden prompt).
4
+ // non-interactive flag path and the interactive prompt, so both produce
5
+ // identical config and the rules are unit-testable in isolation.
6
+ //
7
+ // Local agent execution is Anthropic-only (the native runner only constructs
8
+ // Anthropic clients and the registry's native entries are Anthropic models),
9
+ // so the interactive flow is a single API-key prompt rather than a provider
10
+ // menu. The model is deliberately NOT asked for: the platform model registry
11
+ // owns the execution default, and an explicit override is a power-user flag
12
+ // (`stigmer setup --model …`), not a setup question.
8
13
 
9
14
  import type { Config } from "../../config/config.js";
10
- import { type LlmSettings, setLlm, withProviderDefaults } from "../llm-config.js";
11
- import { promptLine, promptSecret } from "./prompt.js";
15
+ import { type LlmSettings, setLlm } from "../llm-config.js";
16
+ import { promptSecret } from "./prompt.js";
12
17
 
13
- export type ProviderChoice = "anthropic" | "openai" | "ollama" | "skip";
18
+ /**
19
+ * The providers `stigmer setup` accepts. This is the single source of truth
20
+ * for the CLI's local-provider surface — `setup --provider` validates against
21
+ * it and a regression test pins it, because every entry here promises an
22
+ * execution path the native runner must actually serve.
23
+ */
24
+ export const PROVIDER_CHOICES = ["anthropic", "skip"] as const;
25
+
26
+ export type ProviderChoice = (typeof PROVIDER_CHOICES)[number];
14
27
 
15
28
  export interface SelectionInputs {
16
29
  apiKey?: string;
17
30
  model?: string;
18
- baseUrl?: string;
19
31
  }
20
32
 
21
33
  /**
22
34
  * Resolve a provider choice (plus optional overrides) to the LLM settings to
23
- * persist. "skip" yields `undefined`, which clears the section.
35
+ * persist. "skip" yields `undefined`, which clears the section. An omitted
36
+ * model is stored as absent — meaning "platform default", resolved from the
37
+ * model registry at execution time.
24
38
  */
25
39
  export function buildLlmForChoice(choice: ProviderChoice, inputs: SelectionInputs = {}): LlmSettings | undefined {
26
40
  if (choice === "skip") return undefined;
27
- return withProviderDefaults({
28
- provider: choice,
29
- model: inputs.model,
30
- api_key: inputs.apiKey,
31
- base_url: inputs.baseUrl,
32
- });
41
+ const settings: LlmSettings = { provider: choice };
42
+ if (inputs.model) settings.model = inputs.model;
43
+ if (inputs.apiKey) settings.api_key = inputs.apiKey;
44
+ return settings;
33
45
  }
34
46
 
35
47
  /** Apply a provider choice to a config, returning the updated copy. */
@@ -38,41 +50,24 @@ export function applyChoice(config: Config, choice: ProviderChoice, inputs: Sele
38
50
  }
39
51
 
40
52
  /**
41
- * Run the interactive provider menu against the current terminal. Returns the
42
- * updated config, or `null` if the user aborts. For cloud providers, an API key
43
- * already in the environment is used without prompting (matching the Go wizard).
53
+ * Run the interactive setup against the current terminal: one Anthropic
54
+ * API-key prompt. A key already in the environment is used without prompting
55
+ * (and never persisted — env precedence makes a copy redundant); an empty
56
+ * entry is an explicit skip.
44
57
  */
45
- export async function runInteractiveWizard(config: Config, env: NodeJS.ProcessEnv = process.env): Promise<Config | null> {
46
- process.stderr.write("\nChoose your LLM provider (required for agent execution):\n\n");
47
- process.stderr.write(" [1] Anthropic — Cloud API, best quality (requires API key)\n");
48
- process.stderr.write(" [2] OpenAI — Cloud API (requires API key)\n");
49
- process.stderr.write(" [3] Ollama — Free, local, offline (lower quality output)\n");
50
- process.stderr.write(" [4] Skip — Configure later (agents won't execute)\n\n");
58
+ export async function runInteractiveWizard(config: Config, env: NodeJS.ProcessEnv = process.env): Promise<Config> {
59
+ process.stderr.write("\nStigmer runs local agents on Anthropic Claude models.\n");
60
+ process.stderr.write("The model is chosen automatically from the platform model registry.\n\n");
51
61
 
52
- for (;;) {
53
- const choice = (await promptLine("Select [1-4]: ")).trim();
54
- switch (choice) {
55
- case "1":
56
- return applyChoice(config, "anthropic", { apiKey: await resolveCloudKey("anthropic", env) });
57
- case "2":
58
- return applyChoice(config, "openai", { apiKey: await resolveCloudKey("openai", env) });
59
- case "3":
60
- return applyChoice(config, "ollama");
61
- case "4":
62
- return applyChoice(config, "skip");
63
- default:
64
- process.stderr.write("Invalid choice. Please enter 1, 2, 3, or 4.\n");
65
- }
62
+ if (env.ANTHROPIC_API_KEY) {
63
+ process.stderr.write("Using ANTHROPIC_API_KEY from environment\n");
64
+ return applyChoice(config, "anthropic");
66
65
  }
67
- }
68
66
 
69
- async function resolveCloudKey(provider: "anthropic" | "openai", env: NodeJS.ProcessEnv): Promise<string | undefined> {
70
- const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
71
- const existing = env[envVar];
72
- if (existing) {
73
- process.stderr.write(`Using ${envVar} from environment\n`);
74
- return undefined; // env var takes precedence; do not persist a copy
67
+ const key = (await promptSecret("Enter your Anthropic API key (press Enter to skip)")).trim();
68
+ if (key === "") {
69
+ process.stderr.write("Skipped. Agents won't execute until a provider is configured (stigmer setup).\n");
70
+ return applyChoice(config, "skip");
75
71
  }
76
- const key = (await promptSecret(`Enter your ${provider === "anthropic" ? "Anthropic" : "OpenAI"} API key`)).trim();
77
- return key === "" ? undefined : key;
72
+ return applyChoice(config, "anthropic", { apiKey: key });
78
73
  }
@@ -55,10 +55,30 @@ describe("buildStatusResult", () => {
55
55
  expect(field(result, "Runner", "Status")).toBe("Running ✓ (polling)");
56
56
  expect(field(result, "Runner", "Restarts")).toBe("1");
57
57
  expect(field(result, "LLM Configuration", "Provider")).toBe("Anthropic (Cloud)");
58
+ // No model configured: the platform model registry owns the execution
59
+ // default, and status says so instead of asserting a version.
60
+ expect(field(result, "LLM Configuration", "Model")).toBe("Auto (platform default)");
58
61
  expect(field(result, "LLM Configuration", "API Key")).toBe("Configured ✓");
59
62
  expect(field(result, "Web UI", "Temporal")).toBe("http://localhost:8233");
60
63
  });
61
64
 
65
+ it("shows an explicit model override verbatim", async () => {
66
+ writeHealth({ daemon_pid: process.pid, started_at: new Date().toISOString(), components: {} });
67
+ writeFileSync(configPath(home), "backend:\n type: local\n local:\n llm:\n provider: anthropic\n model: claude-x\n api_key: sk-x\n");
68
+
69
+ const result = await buildStatusResult(home, open);
70
+ expect(field(result, "LLM Configuration", "Model")).toBe("claude-x");
71
+ });
72
+
73
+ it("reports a non-anthropic provider as unknown — local execution is Anthropic-only", async () => {
74
+ writeHealth({ daemon_pid: process.pid, started_at: new Date().toISOString(), components: {} });
75
+ writeFileSync(configPath(home), "backend:\n type: local\n local:\n llm:\n provider: ollama\n");
76
+
77
+ const result = await buildStatusResult(home, open);
78
+ expect(field(result, "LLM Configuration", "Provider")).toBe("Unknown (ollama)");
79
+ expect(field(result, "LLM Configuration", "Status")).toBe("Agents will not execute");
80
+ });
81
+
62
82
  it("shows runner as starting before the readiness marker", async () => {
63
83
  writeHealth({
64
84
  daemon_pid: process.pid,
@@ -89,7 +89,7 @@ function addComponentSection(result: CommandResult, name: string, label: string,
89
89
 
90
90
  let status = `${displayState(state)} ${symbolFor(state)}`;
91
91
  // The runner reports observed readiness (it polls its Temporal queue) on top
92
- // of mere liveness — the truthful-visibility improvement over the Go CLI.
92
+ // of mere liveness — a live process that hasn't connected yet is "starting".
93
93
  if (name === "runner" && state === "running") {
94
94
  status += component.ready === true ? " (polling)" : " (starting)";
95
95
  }
@@ -120,21 +120,23 @@ function addLlmSection(result: CommandResult, home: string): void {
120
120
  return;
121
121
  }
122
122
 
123
- const model = resolveModel(config);
124
- switch (provider) {
125
- case "ollama":
126
- section.field("Provider", "Local (Ollama)");
127
- section.field("Model", model);
128
- break;
129
- case "anthropic":
130
- case "openai":
131
- section.field("Provider", provider === "anthropic" ? "Anthropic (Cloud)" : "OpenAI (Cloud)");
132
- section.field("Model", model);
133
- section.field("API Key", resolveApiKey(config) !== "" ? "Configured ✓" : "Not configured ✗");
134
- break;
135
- default:
136
- section.field("Provider", `Unknown (${provider})`);
123
+ // Anthropic is the only provider local execution supports. Anything else in
124
+ // the config (hand-edited, or left over from an older CLI) is reported as
125
+ // unknown rather than pretended to work.
126
+ if (provider !== "anthropic") {
127
+ section.field("Provider", `Unknown (${provider})`);
128
+ section.field("Status", "Agents will not execute");
129
+ result.hint("Reconfigure the provider: stigmer setup");
130
+ return;
137
131
  }
132
+
133
+ // An unset model is not a gap: the platform model registry picks the
134
+ // execution default, so the CLI reports that truthfully instead of asserting
135
+ // a version it does not control.
136
+ const model = resolveModel(config);
137
+ section.field("Provider", "Anthropic (Cloud)");
138
+ section.field("Model", model !== "" ? model : "Auto (platform default)");
139
+ section.field("API Key", resolveApiKey(config) !== "" ? "Configured ✓" : "Not configured ✗");
138
140
  }
139
141
 
140
142
  function addWebUiSection(result: CommandResult, health: HealthState): void {
@@ -1,13 +1,17 @@
1
1
  // `list` dispatch: render a collection of resources for a kind.
2
2
  //
3
3
  // Most registry kinds are search-indexed and list through the unified
4
- // SearchService (matching the Go CLI's search-backed list). Organizations,
5
- // API keys, and agent instances are not search-indexed and use their
6
- // dedicated find/list RPCs.
4
+ // SearchService. Kinds that are deliberately not search-indexed
5
+ // (organizations, API keys, agent instances, agent channels, channel apps)
6
+ // use their dedicated find/list RPCs with bespoke table shapes.
7
7
 
8
8
  import { create } from "@bufbuild/protobuf";
9
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
10
+ import { ListAgentChannelsRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/io_pb";
9
11
  import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
10
12
  import { ListAgentInstancesRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/io_pb";
13
+ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
14
+ import { ListChannelAppsByOrgInputSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/io_pb";
11
15
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
12
16
  import { PageInfoSchema } from "@stigmer/protos/ai/stigmer/commons/rpc/pagination_pb";
13
17
  import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
@@ -18,7 +22,12 @@ import { UsageError } from "../errors/index.js";
18
22
  import type { OutputFormat } from "../output/index.js";
19
23
  import { bool, type JsonObject, obj, renderCollection, str, type TableShape } from "./render.js";
20
24
 
21
- // Kinds the SearchService indexes; mirrors the Go CLI's search-backed list set.
25
+ // Kinds that list through the SearchService (list mode: empty query, org
26
+ // scope). Must stay in step with the server's SearchableKinds allowlist
27
+ // (backend/services/stigmer-server/pkg/query/search/valueobject/
28
+ // search_criteria.go): a kind present here but absent there silently lists
29
+ // as empty. agent_channel and channel_app are deliberately NOT here — they
30
+ // are not_search_indexed by design and take dedicated-RPC branches below.
22
31
  const SEARCH_KINDS = new Set<ApiResourceKind>([
23
32
  ApiResourceKind.agent,
24
33
  ApiResourceKind.workflow,
@@ -26,6 +35,7 @@ const SEARCH_KINDS = new Set<ApiResourceKind>([
26
35
  ApiResourceKind.project,
27
36
  ApiResourceKind.skill,
28
37
  ApiResourceKind.datastore,
38
+ ApiResourceKind.environment,
29
39
  ]);
30
40
 
31
41
  export async function listResources(
@@ -49,6 +59,19 @@ export async function listResources(
49
59
  );
50
60
  return renderCollection(AgentInstanceSchema, result.items, format, INSTANCE_TABLE);
51
61
  }
62
+ if (kind === ApiResourceKind.agent_channel) {
63
+ const result = await client.agentChannel.list(
64
+ create(ListAgentChannelsRequestSchema, { org, pageInfo: create(PageInfoSchema, { num: 1, size: limit }) }),
65
+ );
66
+ return renderCollection(AgentChannelSchema, result.items, format, AGENT_CHANNEL_TABLE);
67
+ }
68
+ if (kind === ApiResourceKind.channel_app) {
69
+ // listByOrg has no pagination on its contract (the per-org set is small
70
+ // by design); the limit is applied client-side so --limit stays honest.
71
+ // `channelapp` (not `channelApp`) is a recorded SDK codegen naming quirk.
72
+ const result = await client.channelapp.listByOrg(create(ListChannelAppsByOrgInputSchema, { org }));
73
+ return renderCollection(ChannelAppSchema, result.entries.slice(0, limit), format, CHANNEL_APP_TABLE);
74
+ }
52
75
  if (!SEARCH_KINDS.has(kind)) {
53
76
  throw new UsageError("list is not implemented for this resource type");
54
77
  }
@@ -83,6 +106,55 @@ const INSTANCE_TABLE: TableShape = {
83
106
  },
84
107
  };
85
108
 
109
+ // A channel serves traffic only when installed AND enabled (the install
110
+ // lifecycle and the owner's serving switch are deliberately distinct), so
111
+ // the table surfaces both signals side by side.
112
+ const AGENT_CHANNEL_TABLE: TableShape = {
113
+ resourceName: "agent channels",
114
+ headers: ["ID", "SLUG", "AGENT", "PROVIDER", "STATE", "ENABLED"],
115
+ row: (json) => {
116
+ const metadata = obj(json, "metadata");
117
+ const spec = obj(json, "spec");
118
+ const agentRef = obj(spec, "agent_ref");
119
+ return [
120
+ str(metadata, "id"),
121
+ str(metadata, "slug"),
122
+ `${str(agentRef, "org")}/${str(agentRef, "slug")}`,
123
+ providerOf(spec),
124
+ // Zero-valued enums are omitted from protojson; a channel is
125
+ // initialized to pending_install on create, so "-" is the rare
126
+ // unspecified case, matching the date() empty convention.
127
+ str(obj(json, "status"), "install_state") || "-",
128
+ bool(spec, "enabled") ? "true" : "false",
129
+ ];
130
+ },
131
+ };
132
+
133
+ // Secret fields never reach this table: list responses are redacted
134
+ // server-side in both editions (the RedactChannelApp pipeline).
135
+ const CHANNEL_APP_TABLE: TableShape = {
136
+ resourceName: "channel apps",
137
+ headers: ["ID", "SLUG", "PROVIDER", "CREATED"],
138
+ row: (json) => {
139
+ const metadata = obj(json, "metadata");
140
+ return [
141
+ str(metadata, "id"),
142
+ str(metadata, "slug"),
143
+ providerOf(obj(json, "spec")),
144
+ date(str(obj(obj(obj(json, "status"), "audit"), "spec_audit"), "created_at")),
145
+ ];
146
+ },
147
+ };
148
+
149
+ // The provider_config oneof serializes as exactly one provider-named key in
150
+ // protojson (AgentChannelSpec and ChannelAppSpec share the same oneof
151
+ // shape). Extend this list when a new provider arm lands in the protos.
152
+ const PROVIDER_KEYS = ["slack", "whatsapp"] as const;
153
+
154
+ function providerOf(spec: JsonObject): string {
155
+ return PROVIDER_KEYS.find((key) => key in spec) ?? "-";
156
+ }
157
+
86
158
  const ORG_TABLE: TableShape = {
87
159
  resourceName: "organizations",
88
160
  headers: ["NAME", "SLUG", "ID"],