@stigmer/cli 3.14.1 → 3.15.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.
@@ -0,0 +1,96 @@
1
+ // Unit tests for `stigmer auth whoami`'s result (commands/auth/whoami.ts;
2
+ // 20260911.11 A3): the command runs the SDK's ensureMyIdentityAccount — the
3
+ // same first-sign-in flow the console runs — and RENDERS what it learned.
4
+ // whoamiResult is the pure half: given the account and whether this call
5
+ // created it, the CommandResult a person reads. A first sign-in says so
6
+ // (visibility of system status); a missing organization is a hint, not a
7
+ // failure; an empty profile (the unconfigured laptop's operator) renders
8
+ // only the fields it has. Accounts are built with the generated schema
9
+ // (DD-007: the generated type is the contract; refinement 12, slice 4).
10
+
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
13
+ import { describe, expect, it } from "vitest";
14
+
15
+ import { whoamiResult } from "./whoami.js";
16
+
17
+ const ALICE = {
18
+ metadata: { id: "ida_wtr3jcf281yfk9xx61kj59fsme", name: "alice@example.com" },
19
+ spec: {
20
+ idpId: "auth0|alice",
21
+ email: "alice@example.com",
22
+ firstName: "Alice",
23
+ lastName: "Liddell",
24
+ isMachineAccount: false,
25
+ },
26
+ };
27
+ const ACCOUNT = create(IdentityAccountSchema, ALICE);
28
+
29
+ function fields(
30
+ result: ReturnType<typeof whoamiResult>,
31
+ ): Record<string, string> {
32
+ return Object.fromEntries(
33
+ result.sections.flatMap((s) => s.fields.map((f) => [f.key, f.value])),
34
+ );
35
+ }
36
+
37
+ describe("whoamiResult", () => {
38
+ it("renders an existing account as 'Authenticated' with its identity fields", () => {
39
+ const result = whoamiResult(ACCOUNT, { created: false, org: "acme" });
40
+ expect(result.status).toBe("success");
41
+ expect(result.message).toBe("Authenticated");
42
+ expect(fields(result)).toEqual({
43
+ "Account ID": "ida_wtr3jcf281yfk9xx61kj59fsme",
44
+ Name: "alice@example.com",
45
+ Email: "alice@example.com",
46
+ "Full Name": "Alice Liddell",
47
+ "Account Type": "User Account",
48
+ Organization: "acme",
49
+ });
50
+ expect(result.hints).toEqual([]);
51
+ });
52
+
53
+ it("says so when this call created the account — the first sign-in is visible, never silent", () => {
54
+ const result = whoamiResult(ACCOUNT, { created: true, org: "acme" });
55
+ expect(result.status).toBe("success");
56
+ expect(result.message).toBe(
57
+ "Authenticated — your account was created on this first sign-in",
58
+ );
59
+ expect(fields(result)["Account ID"]).toBe("ida_wtr3jcf281yfk9xx61kj59fsme");
60
+ });
61
+
62
+ it("a missing organization is a hint with the command that sets it", () => {
63
+ const result = whoamiResult(ACCOUNT, { created: false, org: "" });
64
+ expect(fields(result).Organization).toBeUndefined();
65
+ expect(result.hints).toEqual([
66
+ "No organization set. Use: stigmer config context set --org <slug>",
67
+ ]);
68
+ });
69
+
70
+ it("renders only the fields an empty profile has (the unconfigured laptop's operator)", () => {
71
+ const result = whoamiResult(
72
+ create(IdentityAccountSchema, {
73
+ metadata: { id: "ida_fn0zdvkkkhhrb4wry43zba8gnn", name: "system" },
74
+ spec: { idpId: "local|system" },
75
+ }),
76
+ { created: false, org: "local" },
77
+ );
78
+ expect(fields(result)).toEqual({
79
+ "Account ID": "ida_fn0zdvkkkhhrb4wry43zba8gnn",
80
+ Name: "system",
81
+ "Account Type": "User Account",
82
+ Organization: "local",
83
+ });
84
+ });
85
+
86
+ it("names a machine account", () => {
87
+ const result = whoamiResult(
88
+ create(IdentityAccountSchema, {
89
+ ...ALICE,
90
+ spec: { ...ALICE.spec, isMachineAccount: true },
91
+ }),
92
+ { created: false, org: "acme" },
93
+ );
94
+ expect(fields(result)["Account Type"]).toBe("Machine Account");
95
+ });
96
+ });
@@ -0,0 +1,87 @@
1
+ // `stigmer auth whoami` — who the connected server says you are.
2
+ //
3
+ // Runs the SDK's ensureMyIdentityAccount, the same first-sign-in flow the
4
+ // console's gate runs (20260911.11 A3): an authenticated caller with no
5
+ // account yet is provisioned here rather than told "not found", so a person
6
+ // who only ever uses the CLI is never left without an account. The result
7
+ // says when THIS call created it — a first sign-in is visible, never silent.
8
+ //
9
+ // whoamiResult is the pure half (given the account and what the call learned,
10
+ // the CommandResult a person reads); runWhoami is the I/O half.
11
+
12
+ import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
13
+ import { ensureMyIdentityAccount } from "@stigmer/sdk";
14
+ import {
15
+ ensureAuthenticated,
16
+ resolveContextOrganization,
17
+ } from "../../config/index.js";
18
+ import { CommandResult } from "../../output/index.js";
19
+
20
+ /** What the call learned beyond the account itself. */
21
+ export interface WhoamiContext {
22
+ /** `true` when this call created the account (a first sign-in). */
23
+ readonly created: boolean;
24
+ /** The organization the CLI context resolves to; "" when none is set. */
25
+ readonly org: string;
26
+ }
27
+
28
+ /**
29
+ * Render the caller's account. Every field is optional on the wire and an
30
+ * empty profile (the unconfigured laptop's operator) renders only what it
31
+ * has; a missing organization is a hint with the command that sets it, not a
32
+ * failure.
33
+ */
34
+ export function whoamiResult(
35
+ account: IdentityAccount,
36
+ context: WhoamiContext,
37
+ ): CommandResult {
38
+ const result = CommandResult.success(
39
+ context.created
40
+ ? "Authenticated — your account was created on this first sign-in"
41
+ : "Authenticated",
42
+ );
43
+ const section = result.addSection("");
44
+
45
+ if (account.metadata !== undefined) {
46
+ section.field("Account ID", account.metadata.id);
47
+ if (account.metadata.name !== "")
48
+ section.field("Name", account.metadata.name);
49
+ }
50
+ if (account.spec !== undefined) {
51
+ if (account.spec.email !== "") section.field("Email", account.spec.email);
52
+ if (account.spec.firstName !== "" || account.spec.lastName !== "") {
53
+ section.field(
54
+ "Full Name",
55
+ `${account.spec.firstName} ${account.spec.lastName}`.trim(),
56
+ );
57
+ }
58
+ section.field(
59
+ "Account Type",
60
+ account.spec.isMachineAccount ? "Machine Account" : "User Account",
61
+ );
62
+ }
63
+
64
+ if (context.org !== "") {
65
+ section.field("Organization", context.org);
66
+ } else {
67
+ result.hint(
68
+ "No organization set. Use: stigmer config context set --org <slug>",
69
+ );
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ export async function runWhoami(): Promise<CommandResult> {
76
+ const { connectBackend } = await import("../../backend.js");
77
+ const client = connectBackend();
78
+ ensureAuthenticated(client.config);
79
+
80
+ const { account, created } = await ensureMyIdentityAccount(client.stigmer, {
81
+ onProvisioning: () => process.stderr.write("Setting up your account...\n"),
82
+ });
83
+ return whoamiResult(account, {
84
+ created,
85
+ org: resolveContextOrganization(client.config),
86
+ });
87
+ }
@@ -8,9 +8,14 @@
8
8
  // unless detached or a local workspace is in play.
9
9
 
10
10
  import type { Command } from "commander";
11
- import { ensureAuthenticated, isCloudMode, resolveOrganization } from "../config/index.js";
11
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
12
+ import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
12
13
  import { UsageError } from "../errors/index.js";
13
- import { addAgentExecFlags, type AgentExecOptions, toAgentExecFlags } from "./agent-exec-flags.js";
14
+ import {
15
+ addAgentExecFlags,
16
+ type AgentExecOptions,
17
+ toAgentExecFlags,
18
+ } from "./agent-exec-flags.js";
14
19
  import { globalOrg } from "./shared.js";
15
20
 
16
21
  // The org that owns the system agent blueprints (installed by the seedpack),
@@ -31,9 +36,21 @@ interface DraftConfig {
31
36
  }
32
37
 
33
38
  const CONFIGS: readonly DraftConfig[] = [
34
- { name: "skill", resourceType: "Skill", summary: "create a skill with AI assistance" },
35
- { name: "agent", resourceType: "Agent", summary: "create an agent with AI assistance" },
36
- { name: "mcp-server", resourceType: "McpServer", summary: "create an MCP server config with AI assistance" },
39
+ {
40
+ name: "skill",
41
+ resourceType: "Skill",
42
+ summary: "create a skill with AI assistance",
43
+ },
44
+ {
45
+ name: "agent",
46
+ resourceType: "Agent",
47
+ summary: "create an agent with AI assistance",
48
+ },
49
+ {
50
+ name: "mcp-server",
51
+ resourceType: "McpServer",
52
+ summary: "create an MCP server config with AI assistance",
53
+ },
37
54
  ];
38
55
 
39
56
  // System agent slug per subcommand (Go's draftConfig.AgentName: "<type>-creator").
@@ -42,17 +59,28 @@ function agentSlug(name: string): string {
42
59
  }
43
60
 
44
61
  export function registerDraft(program: Command): void {
45
- const draft = program.command("draft").description("create resource configurations with AI assistance");
62
+ const draft = program
63
+ .command("draft")
64
+ .description("create resource configurations with AI assistance");
46
65
  for (const config of CONFIGS) {
47
66
  const sub = draft.command(config.name).description(config.summary);
48
67
  addAgentExecFlags(sub, true)
49
- .option("-o, --output <dir>", "directory to save generated artifacts (default: current directory)")
68
+ .option(
69
+ "-o, --output <dir>",
70
+ "directory to save generated artifacts (default: current directory)",
71
+ )
50
72
  .option("--json", "stream events as newline-delimited JSON")
51
- .action((options: DraftFlags, command: Command) => runDraft(config, options, command));
73
+ .action((options: DraftFlags, command: Command) =>
74
+ runDraft(config, options, command),
75
+ );
52
76
  }
53
77
  }
54
78
 
55
- async function runDraft(config: DraftConfig, options: DraftFlags, command: Command): Promise<void> {
79
+ async function runDraft(
80
+ config: DraftConfig,
81
+ options: DraftFlags,
82
+ command: Command,
83
+ ): Promise<void> {
56
84
  const { connectBackend } = await import("../backend.js");
57
85
  const client = connectBackend();
58
86
  ensureAuthenticated(client.config);
@@ -63,29 +91,52 @@ async function runDraft(config: DraftConfig, options: DraftFlags, command: Comma
63
91
  );
64
92
  }
65
93
 
66
- const [{ prepareAgentExec }, { executeResolvedAgent }, { resolveAgentRef }, { localWorkspaceRoots }] =
67
- await Promise.all([
68
- import("../resources/run/prepare.js"),
69
- import("../resources/run/agent-exec.js"),
70
- import("../resources/run/resolve.js"),
71
- import("../resources/run/workspace.js"),
72
- ]);
73
-
74
- const agent = await resolveDraftAgent(resolveAgentRef, client.stigmer, config);
75
-
76
- const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress(), {
77
- cloudBackend: isCloudMode(client.config),
78
- });
94
+ const [
95
+ { prepareAgentExec },
96
+ { executeResolvedAgent },
97
+ { resolveAgentRef },
98
+ { localWorkspaceRoots },
99
+ ] = await Promise.all([
100
+ import("../resources/run/prepare.js"),
101
+ import("../resources/run/agent-exec.js"),
102
+ import("../resources/run/resolve.js"),
103
+ import("../resources/run/workspace.js"),
104
+ ]);
105
+
106
+ const agent = await resolveDraftAgent(
107
+ resolveAgentRef,
108
+ client.stigmer,
109
+ config,
110
+ );
111
+
112
+ const prepared = await prepareAgentExec(
113
+ toAgentExecFlags(options),
114
+ client.stigmer,
115
+ org,
116
+ stderrProgress(),
117
+ {
118
+ // The kind's tier against the server's reported edition, as `run` asks it.
119
+ accountPreferencesAvailable: await client.isResourceAvailable(
120
+ ApiResourceKind.identity_account,
121
+ ),
122
+ },
123
+ );
79
124
 
80
125
  if (prepared.attachments.length + prepared.workspaceFileRefs.length > 0) {
81
- process.stderr.write(`Attached ${prepared.attachments.length + prepared.workspaceFileRefs.length} file(s) as context\n`);
126
+ process.stderr.write(
127
+ `Attached ${prepared.attachments.length + prepared.workspaceFileRefs.length} file(s) as context\n`,
128
+ );
82
129
  }
83
130
 
84
131
  await executeResolvedAgent({
85
132
  agent,
86
133
  prepared,
87
134
  org,
88
- downloadDir: resolveDownloadDir(options.output ?? "", prepared.detach, localWorkspaceRoots(prepared.workspaceEntries)),
135
+ downloadDir: resolveDownloadDir(
136
+ options.output ?? "",
137
+ prepared.detach,
138
+ localWorkspaceRoots(prepared.workspaceEntries),
139
+ ),
89
140
  outputMode: options.json === true ? "json" : "inline",
90
141
  client,
91
142
  });
@@ -100,7 +151,11 @@ async function resolveDraftAgent(
100
151
  ): Promise<import("@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb").Agent> {
101
152
  const slug = agentSlug(config.name);
102
153
  try {
103
- return await resolveAgentRef(client, `${SYSTEM_AGENT_ORG}/${slug}`, SYSTEM_AGENT_ORG);
154
+ return await resolveAgentRef(
155
+ client,
156
+ `${SYSTEM_AGENT_ORG}/${slug}`,
157
+ SYSTEM_AGENT_ORG,
158
+ );
104
159
  } catch {
105
160
  throw new UsageError(
106
161
  `${slug} agent not found in organization "${SYSTEM_AGENT_ORG}"\n\n` +
@@ -117,7 +172,11 @@ async function resolveDraftAgent(
117
172
  // explicit -o → that directory
118
173
  // local workspace → skip (the agent writes directly to disk)
119
174
  // otherwise → current directory
120
- function resolveDownloadDir(output: string, detach: boolean, localRoots: readonly string[]): string {
175
+ function resolveDownloadDir(
176
+ output: string,
177
+ detach: boolean,
178
+ localRoots: readonly string[],
179
+ ): string {
121
180
  if (detach) return "";
122
181
  if (output !== "") return output;
123
182
  if (localRoots.length > 0) return "";
@@ -5,10 +5,14 @@
5
5
  // the differ) load lazily inside the action so `--help` stays fast (DD-001).
6
6
 
7
7
  import type { Command } from "commander";
8
- import { ensureAuthenticated, isCloudMode, resolveOrganization } from "../config/index.js";
8
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
9
+ import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
9
10
  import { UsageError } from "../errors/index.js";
10
11
  import { interactiveBrowseEnabled } from "../resources/picker/tty.js";
11
- import { addAgentExecFlags, type AgentExecOptions } from "./agent-exec-flags.js";
12
+ import {
13
+ addAgentExecFlags,
14
+ type AgentExecOptions,
15
+ } from "./agent-exec-flags.js";
12
16
  import { globalOrg } from "./shared.js";
13
17
 
14
18
  interface RunFlags extends AgentExecOptions {
@@ -23,8 +27,13 @@ export function registerRun(program: Command): void {
23
27
  addAgentExecFlags(run)
24
28
  .option("--json", "stream events as newline-delimited JSON")
25
29
  .option("--download <dir>", "download artifacts to directory when complete")
26
- .action((type: string | undefined, reference: string | undefined, options: RunFlags, command: Command) =>
27
- runRun(type, reference, options, command),
30
+ .action(
31
+ (
32
+ type: string | undefined,
33
+ reference: string | undefined,
34
+ options: RunFlags,
35
+ command: Command,
36
+ ) => runRun(type, reference, options, command),
28
37
  );
29
38
  }
30
39
 
@@ -76,7 +85,8 @@ async function runSmart(
76
85
  outputMode: "inline" | "json",
77
86
  client: import("../client/index.js").BackendClient,
78
87
  ): Promise<void> {
79
- const { isSessionId, hasResourceIdPrefix, isAgentId, validateResourceId } = await import("../resources/reference.js");
88
+ const { isSessionId, hasResourceIdPrefix, isAgentId, validateResourceId } =
89
+ await import("../resources/reference.js");
80
90
 
81
91
  if (isSessionId(value)) {
82
92
  throw new UsageError(
@@ -126,13 +136,14 @@ async function runExplicit(
126
136
  client: import("../client/index.js").BackendClient,
127
137
  ): Promise<void> {
128
138
  const { defaultRegistry, Verb } = await import("../registry/index.js");
129
- const { ApiResourceKind } = await import(
130
- "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb"
131
- );
139
+ const { ApiResourceKind } =
140
+ await import("@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb");
132
141
 
133
142
  const info = defaultRegistry().getByAlias(type);
134
143
  if (info === undefined) {
135
- throw new UsageError(`unknown resource type: ${type}\n\nAvailable types: agent, workflow`);
144
+ throw new UsageError(
145
+ `unknown resource type: ${type}\n\nAvailable types: agent, workflow`,
146
+ );
136
147
  }
137
148
  if (!info.supportedVerbs.has(Verb.Run)) {
138
149
  throw new UsageError(`${info.displayName} does not support 'run'`);
@@ -168,15 +179,26 @@ async function runResolvedAgent(
168
179
  client: import("../client/index.js").BackendClient,
169
180
  ): Promise<void> {
170
181
  const { prepareAgentExec } = await import("../resources/run/prepare.js");
171
- const { executeResolvedAgent } = await import("../resources/run/agent-exec.js");
182
+ const { executeResolvedAgent } =
183
+ await import("../resources/run/agent-exec.js");
172
184
  const { toAgentExecFlags } = await import("./agent-exec-flags.js");
173
185
 
174
- const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress(), {
175
- cloudBackend: isCloudMode(client.config),
176
- // run opts into the account default_harness fill; draft deliberately
177
- // does not (see the option's doc comment for the D5 rationale).
178
- applyAccountHarnessDefault: true,
179
- });
186
+ const prepared = await prepareAgentExec(
187
+ toAgentExecFlags(options),
188
+ client.stigmer,
189
+ org,
190
+ stderrProgress(),
191
+ {
192
+ // The kind's tier against the server's reported edition — the same
193
+ // question the console asks — never the config's backend type.
194
+ accountPreferencesAvailable: await client.isResourceAvailable(
195
+ ApiResourceKind.identity_account,
196
+ ),
197
+ // run opts into the account default_harness fill; draft deliberately
198
+ // does not (see the option's doc comment for the D5 rationale).
199
+ applyAccountHarnessDefault: true,
200
+ },
201
+ );
180
202
  await executeResolvedAgent({
181
203
  agent,
182
204
  prepared,
@@ -199,15 +221,21 @@ async function runWorkflow(
199
221
  client: import("../client/index.js").BackendClient,
200
222
  ): Promise<void> {
201
223
  if (options.workspace.length > 0) {
202
- throw new UsageError("--workspace is not supported for workflows (workspace is an agent-level concept)");
224
+ throw new UsageError(
225
+ "--workspace is not supported for workflows (workspace is an agent-level concept)",
226
+ );
203
227
  }
204
- const [{ resolveWorkflowRef }, { createWorkflowExecution }, { loadAndMergeEnv }, { parseApprovalAction }] =
205
- await Promise.all([
206
- import("../resources/run/resolve.js"),
207
- import("../resources/run/create.js"),
208
- import("../resources/run/env.js"),
209
- import("../resources/run/prepare.js"),
210
- ]);
228
+ const [
229
+ { resolveWorkflowRef },
230
+ { createWorkflowExecution },
231
+ { loadAndMergeEnv },
232
+ { parseApprovalAction },
233
+ ] = await Promise.all([
234
+ import("../resources/run/resolve.js"),
235
+ import("../resources/run/create.js"),
236
+ import("../resources/run/env.js"),
237
+ import("../resources/run/prepare.js"),
238
+ ]);
211
239
 
212
240
  const workflow = await resolveWorkflowRef(client.stigmer, reference, org);
213
241
  const runtimeEnv = loadAndMergeEnv({
@@ -220,27 +248,37 @@ async function runWorkflow(
220
248
  runtimeEnv.STIGMER_ORG_ID = { value: org, isSecret: false };
221
249
  }
222
250
 
223
- const execution = await createWorkflowExecution(client.controller.bind(client), {
224
- workflowId: workflow.metadata?.id ?? "",
225
- orgId: org,
226
- message: options.message ?? "",
227
- runtimeEnv,
228
- });
251
+ const execution = await createWorkflowExecution(
252
+ client.controller.bind(client),
253
+ {
254
+ workflowId: workflow.metadata?.id ?? "",
255
+ orgId: org,
256
+ message: options.message ?? "",
257
+ runtimeEnv,
258
+ },
259
+ );
229
260
  const id = execution.metadata?.id ?? "";
230
261
 
231
262
  if (options.detach === true) {
232
263
  process.stdout.write(`Workflow execution created: ${id}\n`);
233
- process.stdout.write(`Track it with: stigmer execution logs ${id} --follow\n`);
264
+ process.stdout.write(
265
+ `Track it with: stigmer execution logs ${id} --follow\n`,
266
+ );
234
267
  return;
235
268
  }
236
269
 
237
- const { ApprovalAction } = await import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb");
238
- const { streamWorkflowExecution } = await import("../resources/run/workflow-stream.js");
270
+ const { ApprovalAction } =
271
+ await import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb");
272
+ const { streamWorkflowExecution } =
273
+ await import("../resources/run/workflow-stream.js");
239
274
  await streamWorkflowExecution({
240
275
  client: client.stigmer,
241
276
  executionId: id,
242
277
  outputMode,
243
- defaultAction: options.autoApprove === true ? ApprovalAction.APPROVE_ALL : parseApprovalAction(options.approveDefault ?? ""),
278
+ defaultAction:
279
+ options.autoApprove === true
280
+ ? ApprovalAction.APPROVE_ALL
281
+ : parseApprovalAction(options.approveDefault ?? ""),
244
282
  });
245
283
  }
246
284
 
@@ -260,7 +298,11 @@ async function browseAndRunAgent(
260
298
  client: import("../client/index.js").BackendClient,
261
299
  ): Promise<void> {
262
300
  const { pickAgent } = await import("../resources/picker/ink.js");
263
- const selected = await pickAgent({ client: client.stigmer, org, initialQuery });
301
+ const selected = await pickAgent({
302
+ client: client.stigmer,
303
+ org,
304
+ initialQuery,
305
+ });
264
306
  if (selected === undefined) return;
265
307
  await runAgent(selected.id, options, org, outputMode, client);
266
308
  }