@stigmer/cli 3.4.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.
- package/commands/setup.d.ts.map +1 -1
- package/commands/setup.js +11 -16
- package/commands/setup.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +3 -2
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/env.d.ts +0 -3
- package/local/daemon/env.d.ts.map +1 -1
- package/local/daemon/env.js +4 -8
- package/local/daemon/env.js.map +1 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +9 -17
- package/local/daemon/launch.js.map +1 -1
- package/local/llm-config.d.ts +7 -7
- package/local/llm-config.d.ts.map +1 -1
- package/local/llm-config.js +29 -42
- package/local/llm-config.js.map +1 -1
- package/local/setup/wizard.d.ts +16 -7
- package/local/setup/wizard.d.ts.map +1 -1
- package/local/setup/wizard.js +41 -44
- package/local/setup/wizard.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +16 -15
- package/local/status.js.map +1 -1
- package/package.json +5 -5
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +70 -4
- package/resources/list.js.map +1 -1
- package/src/commands/setup.ts +13 -20
- package/src/local/daemon/components.test.ts +8 -2
- package/src/local/daemon/components.ts +3 -1
- package/src/local/daemon/env.test.ts +13 -6
- package/src/local/daemon/env.ts +10 -13
- package/src/local/daemon/launch.ts +9 -17
- package/src/local/llm-config.test.ts +16 -27
- package/src/local/llm-config.ts +27 -41
- package/src/local/setup/wizard.test.ts +19 -11
- package/src/local/setup/wizard.ts +42 -47
- package/src/local/status.test.ts +20 -0
- package/src/local/status.ts +17 -15
- package/src/resources/list.ts +76 -4
- 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
|
|
103
|
-
*
|
|
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.
|
|
232
|
-
//
|
|
233
|
-
function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicApiKey"
|
|
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
|
-
|
|
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
|
|
247
|
-
//
|
|
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("
|
|
19
|
-
expect(detectProviderFromEnv({ ANTHROPIC_API_KEY: "a"
|
|
20
|
-
|
|
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: "
|
|
28
|
-
expect(resolveProvider(cfg, { STIGMER_LLM_PROVIDER: "
|
|
29
|
-
expect(resolveProvider(cfg, {})).toBe("
|
|
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("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
expect(resolveModel(config({ llm: { provider: "
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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: "
|
|
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 });
|
package/src/local/llm-config.ts
CHANGED
|
@@ -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
|
|
6
|
-
// lens* for the one sub-tree
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
|
51
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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:
|
|
73
|
+
/** Effective API key: ANTHROPIC_API_KEY env var > config. */
|
|
76
74
|
export function resolveApiKey(config: Config, env: NodeJS.ProcessEnv = process.env): string {
|
|
77
|
-
|
|
78
|
-
|
|
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("
|
|
12
|
-
expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic"
|
|
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: "
|
|
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",
|
|
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: "
|
|
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
|
|
5
|
-
// config and the rules are unit-testable in isolation.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
11
|
-
import {
|
|
15
|
+
import { type LlmSettings, setLlm } from "../llm-config.js";
|
|
16
|
+
import { promptSecret } from "./prompt.js";
|
|
12
17
|
|
|
13
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
46
|
-
process.stderr.write("\
|
|
47
|
-
process.stderr.write("
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
return key === "" ? undefined : key;
|
|
72
|
+
return applyChoice(config, "anthropic", { apiKey: key });
|
|
78
73
|
}
|
package/src/local/status.test.ts
CHANGED
|
@@ -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,
|
package/src/local/status.ts
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 {
|
package/src/resources/list.ts
CHANGED
|
@@ -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
|
|
5
|
-
// API keys,
|
|
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
|
|
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"],
|