@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,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 {
@@ -45,33 +45,60 @@ interface HasMetadata {
45
45
  readonly metadata?: { readonly id?: string; readonly name?: string; readonly slug?: string; readonly org?: string };
46
46
  }
47
47
 
48
- type DeleteFn = (client: Stigmer, id: string) => Promise<HasMetadata>;
48
+ // `force` is the RPC-level acknowledgment of destructive side effects (e.g. a
49
+ // datastore's held records) — distinct from, but carried by, the CLI's
50
+ // `-f/--force` flag. Kinds whose delete RPC takes a plain typed ID have no
51
+ // force field and simply ignore the argument.
52
+ type DeleteFn = (client: Stigmer, id: string, force: boolean) => Promise<HasMetadata>;
49
53
 
50
54
  // Kinds the unified delete handles directly, each bound to its SDK delete call.
51
55
  // Kinds that carry a Delete verb but no entry here (identity_provider,
52
- // oauth_app, environment, workflow_instance, session) fall through to a
56
+ // oauth_app, agent_share, workflow_instance, session) fall through to a
53
57
  // "not implemented" usage error, matching Go's default branch.
54
- // McpServer is the outlier whose delete takes a DeleteResourceInput, not an ID.
58
+ // Kinds whose delete takes a DeleteResourceInput (not an ID) thread the force
59
+ // acknowledgment through; today only the datastore server honors it (the
60
+ // non-empty guard), but force is the contract's generic ack carrier.
55
61
  const DELETE_HANDLERS: ReadonlyMap<ApiResourceKind, DeleteFn> = new Map<ApiResourceKind, DeleteFn>([
56
62
  [ApiResourceKind.agent, (c, id) => c.agent.delete(id)],
57
63
  [ApiResourceKind.agent_instance, (c, id) => c.agentInstance.delete(id)],
58
64
  [ApiResourceKind.workflow, (c, id) => c.workflow.delete(id)],
59
- [ApiResourceKind.mcp_server, (c, id) => c.mcpServer.delete({ resourceId: id })],
65
+ [ApiResourceKind.mcp_server, (c, id, force) => c.mcpServer.delete({ resourceId: id, force })],
60
66
  [ApiResourceKind.project, (c, id) => c.project.delete(id)],
67
+ [ApiResourceKind.datastore, (c, id, force) => c.datastore.delete({ resourceId: id, force })],
68
+ [ApiResourceKind.environment, (c, id, force) => c.environment.delete({ resourceId: id, force })],
69
+ [ApiResourceKind.agent_channel, (c, id) => c.agentChannel.delete(id)],
70
+ [ApiResourceKind.channel_app, (c, id, force) => c.channelapp.delete({ resourceId: id, force })],
61
71
  [ApiResourceKind.skill, (c, id) => c.skill.delete(id)],
62
72
  [ApiResourceKind.api_key, (c, id) => c.apiKey.delete(id)],
63
73
  ]);
64
74
 
75
+ // The deletable-types list for the unknown-type error, derived from the
76
+ // handler map (plus the two special-cased routes) so it can never drift from
77
+ // what the dispatch below actually supports. Deliberately NOT derived from the
78
+ // registry's verb matrix — that declares Delete for kinds this dispatch does
79
+ // not wire, and listing those would over-promise.
80
+ function availableDeleteTypes(): string {
81
+ const wired = [...DELETE_HANDLERS.keys()]
82
+ .map((kind) => defaultRegistry().getByKind(kind)?.singular)
83
+ .filter((singular): singular is string => singular !== undefined);
84
+ return [...wired, "execution", "organization"].join(", ");
85
+ }
86
+
65
87
  /**
66
88
  * Build the delete plan for a `<type> <reference>` pair. Routes the three
67
89
  * special cases first, then the registry-driven standard path. Throws on
68
90
  * unknown/unsupported types before any network call.
91
+ *
92
+ * `force` is the caller's acknowledgment of destructive side effects; it rides
93
+ * the delete RPC for kinds whose contract carries a force field (see
94
+ * DELETE_HANDLERS). The command layer maps `-f/--force` onto it.
69
95
  */
70
96
  export async function planDelete(
71
97
  client: Stigmer,
72
98
  typeArg: string,
73
99
  reference: string,
74
100
  org: string,
101
+ force = false,
75
102
  ): Promise<DeletePlan> {
76
103
  if (isExecutionAlias(typeArg)) {
77
104
  return planExecutionCancel(client, reference);
@@ -79,9 +106,7 @@ export async function planDelete(
79
106
 
80
107
  const info = defaultRegistry().getByAlias(typeArg);
81
108
  if (info === undefined) {
82
- throw new UsageError(
83
- `unknown resource type: ${typeArg}\n\nAvailable types: agent, agent-instance, workflow, mcpserver, project, skill, execution, organization`,
84
- );
109
+ throw new UsageError(`unknown resource type: ${typeArg}\n\nAvailable types: ${availableDeleteTypes()}`);
85
110
  }
86
111
 
87
112
  if (info.kind === ApiResourceKind.organization) {
@@ -97,7 +122,7 @@ export async function planDelete(
97
122
  throw new UsageError(`delete not implemented for ${info.displayName}`);
98
123
  }
99
124
 
100
- return planStandardDelete(client, info, reference, org, deleteFn);
125
+ return planStandardDelete(client, info, reference, org, deleteFn, force);
101
126
  }
102
127
 
103
128
  async function planStandardDelete(
@@ -106,6 +131,7 @@ async function planStandardDelete(
106
131
  reference: string,
107
132
  org: string,
108
133
  deleteFn: DeleteFn,
134
+ force: boolean,
109
135
  ): Promise<DeletePlan> {
110
136
  // Safe by construction: every DELETE_HANDLERS key also has a get binding.
111
137
  const getter = getterFor(info.kind);
@@ -122,7 +148,7 @@ async function planStandardDelete(
122
148
  return {
123
149
  warning: buildDeleteWarning(info, resource),
124
150
  confirmPrompt: "Proceed with deletion? [y/N]",
125
- perform: async () => buildDeleteSuccess(info, await deleteFn(client, id)),
151
+ perform: async () => buildDeleteSuccess(info, await deleteFn(client, id, force)),
126
152
  };
127
153
  }
128
154
 
@@ -200,6 +226,19 @@ function buildDeleteWarning(info: TypeInfo, message: HasMetadata): CommandResult
200
226
  warning.hint("This will delete the skill and all its versions.");
201
227
  }
202
228
 
229
+ if (info.kind === ApiResourceKind.datastore) {
230
+ // Mirrors the server's non-empty guard (GuardNonEmptyStep): the delete of
231
+ // a datastore holding records is refused unless force acknowledges it.
232
+ warning.hint("All records held by this datastore will be destroyed.");
233
+ warning.hint("A datastore holding records is refused unless --force acknowledges destroying them.");
234
+ }
235
+
236
+ if (info.kind === ApiResourceKind.agent_channel) {
237
+ // The server's documented distinction: delete is the connection's full
238
+ // teardown; disabling is the config-preserving pause.
239
+ warning.hint("Delete is the connection's full teardown; to pause instead, apply with spec.enabled: false.");
240
+ }
241
+
203
242
  warning.hint("This action cannot be undone.");
204
243
  return warning;
205
244
  }
@@ -5,7 +5,11 @@
5
5
 
6
6
  import type { DescMessage, Message } from "@bufbuild/protobuf";
7
7
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
8
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
8
9
  import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
10
+ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
11
+ import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
12
+ import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
9
13
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
10
14
  import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
11
15
  import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
@@ -29,6 +33,10 @@ const GET_BINDINGS: ReadonlyMap<ApiResourceKind, Getter> = new Map([
29
33
  [ApiResourceKind.workflow, refGetter(ApiResourceKind.workflow, WorkflowSchema, (c) => c.workflow)],
30
34
  [ApiResourceKind.mcp_server, refGetter(ApiResourceKind.mcp_server, McpServerSchema, (c) => c.mcpServer)],
31
35
  [ApiResourceKind.project, refGetter(ApiResourceKind.project, ProjectSchema, (c) => c.project)],
36
+ [ApiResourceKind.datastore, refGetter(ApiResourceKind.datastore, DatastoreSchema, (c) => c.datastore)],
37
+ [ApiResourceKind.environment, refGetter(ApiResourceKind.environment, EnvironmentSchema, (c) => c.environment)],
38
+ [ApiResourceKind.agent_channel, refGetter(ApiResourceKind.agent_channel, AgentChannelSchema, (c) => c.agentChannel)],
39
+ [ApiResourceKind.channel_app, refGetter(ApiResourceKind.channel_app, ChannelAppSchema, (c) => c.channelapp)],
32
40
  [ApiResourceKind.skill, refGetter(ApiResourceKind.skill, SkillSchema, (c) => c.skill)],
33
41
  [ApiResourceKind.api_key, idOnlyGetter(ApiKeySchema, (c) => c.apiKey, "API keys")],
34
42
  ]);
@@ -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"],