@stigmer/cli 3.12.3 → 3.12.5

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 (82) hide show
  1. package/commands/draft.js +4 -2
  2. package/commands/draft.js.map +1 -1
  3. package/commands/list.js +7 -15
  4. package/commands/list.js.map +1 -1
  5. package/commands/run.js +4 -2
  6. package/commands/run.js.map +1 -1
  7. package/commands/seedpack.d.ts.map +1 -1
  8. package/commands/seedpack.js +27 -5
  9. package/commands/seedpack.js.map +1 -1
  10. package/local/seedpack/apply.d.ts +32 -3
  11. package/local/seedpack/apply.d.ts.map +1 -1
  12. package/local/seedpack/apply.js +59 -8
  13. package/local/seedpack/apply.js.map +1 -1
  14. package/package.json +5 -5
  15. package/registry/verb-support.d.ts.map +1 -1
  16. package/registry/verb-support.js +6 -3
  17. package/registry/verb-support.js.map +1 -1
  18. package/resources/apply/apply.d.ts.map +1 -1
  19. package/resources/apply/apply.js +10 -1
  20. package/resources/apply/apply.js.map +1 -1
  21. package/resources/connect/connect.d.ts +7 -0
  22. package/resources/connect/connect.d.ts.map +1 -1
  23. package/resources/connect/connect.js +47 -30
  24. package/resources/connect/connect.js.map +1 -1
  25. package/resources/connect/display.d.ts.map +1 -1
  26. package/resources/connect/display.js +3 -0
  27. package/resources/connect/display.js.map +1 -1
  28. package/resources/list.d.ts.map +1 -1
  29. package/resources/list.js +39 -9
  30. package/resources/list.js.map +1 -1
  31. package/resources/run/prepare.d.ts +12 -1
  32. package/resources/run/prepare.d.ts.map +1 -1
  33. package/resources/run/prepare.js +27 -2
  34. package/resources/run/prepare.js.map +1 -1
  35. package/resources/session.d.ts +0 -8
  36. package/resources/session.d.ts.map +1 -1
  37. package/resources/session.js +6 -33
  38. package/resources/session.js.map +1 -1
  39. package/resources/skill.d.ts +0 -10
  40. package/resources/skill.d.ts.map +1 -1
  41. package/resources/skill.js +8 -59
  42. package/resources/skill.js.map +1 -1
  43. package/resources/stream/events.d.ts +1 -1
  44. package/resources/stream/events.d.ts.map +1 -1
  45. package/resources/stream/events.js +8 -6
  46. package/resources/stream/events.js.map +1 -1
  47. package/resources/stream/render-plaintext.d.ts.map +1 -1
  48. package/resources/stream/render-plaintext.js +19 -5
  49. package/resources/stream/render-plaintext.js.map +1 -1
  50. package/resources/task-configs.d.ts +15 -0
  51. package/resources/task-configs.d.ts.map +1 -0
  52. package/resources/task-configs.js +135 -0
  53. package/resources/task-configs.js.map +1 -0
  54. package/resources/validate.d.ts.map +1 -1
  55. package/resources/validate.js +8 -1
  56. package/resources/validate.js.map +1 -1
  57. package/src/commands/draft.ts +4 -2
  58. package/src/commands/list.ts +7 -17
  59. package/src/commands/run.ts +4 -2
  60. package/src/commands/seedpack.ts +27 -5
  61. package/src/local/seedpack/apply.test.ts +127 -6
  62. package/src/local/seedpack/apply.ts +69 -9
  63. package/src/registry/registry.test.ts +50 -8
  64. package/src/registry/verb-support.ts +6 -3
  65. package/src/resources/apply/apply.ts +11 -1
  66. package/src/resources/connect/connect.ts +70 -44
  67. package/src/resources/connect/display.ts +3 -0
  68. package/src/resources/list.ts +40 -9
  69. package/src/resources/read-completion.integration.test.ts +14 -4
  70. package/src/resources/run/prepare.test.ts +80 -0
  71. package/src/resources/run/prepare.ts +41 -1
  72. package/src/resources/session.ts +6 -39
  73. package/src/resources/skill.test.ts +5 -77
  74. package/src/resources/skill.ts +8 -67
  75. package/src/resources/stream/events.ts +9 -7
  76. package/src/resources/stream/render-plaintext.test.ts +51 -0
  77. package/src/resources/stream/render-plaintext.ts +24 -6
  78. package/src/resources/task-configs.test.ts +98 -0
  79. package/src/resources/task-configs.ts +150 -0
  80. package/src/resources/validate.test.ts +26 -0
  81. package/src/resources/validate.ts +10 -2
  82. package/src/resources/session.test.ts +0 -48
@@ -1,7 +1,9 @@
1
- // `connect mcp-server` orchestration. Mirrors Go's mcpserver.Connect (connect.go):
2
- // resolve the server, then either push to the backend (the Connect RPC runs
3
- // discovery server-side and persists capabilities + tool-approval policies) or,
4
- // for --dry-run, discover locally and return without persisting.
1
+ // `connect mcp-server` orchestration. Resolve the server, then either run the
2
+ // server-side connect (discovery + tool-approval classification, persisted on
3
+ // the resource) or, for --dry-run, discover locally and return without
4
+ // persisting. The server-side path uses the async lane — startConnect + poll
5
+ // (stigmer/stigmer#425) — with a blocking-RPC fallback for backends that
6
+ // predate it.
5
7
  //
6
8
  // OAuth: when a server requires OAuth, has no existing grant, and no --env was
7
9
  // supplied, the interactive browser flow (oauth.ts) shepherds the user through
@@ -11,10 +13,12 @@
11
13
 
12
14
  import { create } from "@bufbuild/protobuf";
13
15
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
16
+ import type { ConnectInput } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
14
17
  import { ConnectInputSchema, GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
15
18
  import type { DiscoveredCapabilities } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
16
19
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
17
20
  import type { Stigmer } from "@stigmer/sdk";
21
+ import { connectAndWait, ConnectStillRunningError, CONNECT_SETTLE_BOUND_MS } from "@stigmer/sdk";
18
22
  import type { BackendType } from "../../config/config.js";
19
23
  import { CliExitError, ExitCode, UsageError } from "../../errors/index.js";
20
24
  import { defaultRegistry } from "../../registry/index.js";
@@ -49,6 +53,13 @@ export interface ConnectResult {
49
53
  readonly capabilities: DiscoveredCapabilities | undefined;
50
54
  /** Set when capabilities were persisted (non-dry-run); undefined for dry-run. */
51
55
  readonly updated: McpServer | undefined;
56
+ /**
57
+ * Start-time advisory from the backend's connect pre-flight (e.g. "no
58
+ * runner appears to be polling the task queue"). Only set on the async
59
+ * lane, and only when the operation ultimately settled anyway — surfaced
60
+ * so the user learns their runner came up late.
61
+ */
62
+ readonly warning?: string;
52
63
  }
53
64
 
54
65
  /** Connect to an MCP server and discover its capabilities (push or dry-run). */
@@ -75,50 +86,65 @@ export async function connectMcpServer(client: Stigmer, opts: ConnectOptions): P
75
86
 
76
87
  await ensureOAuthSatisfied(client, server, opts);
77
88
 
78
- const push = client.mcpServer.connect(
79
- create(ConnectInputSchema, {
80
- mcpServerId: server.metadata?.id ?? "",
81
- org: opts.org,
82
- runtimeEnv: buildRuntimeEnv(server, opts.envOverrides),
83
- }),
84
- );
85
- const updated = opts.pushTimeoutMs === undefined
86
- ? await push
87
- : await boundedPush(push, opts.pushTimeoutMs, server);
88
- return { server, capabilities: updated.status?.discoveredCapabilities, updated };
89
+ const input = create(ConnectInputSchema, {
90
+ mcpServerId: server.metadata?.id ?? "",
91
+ org: opts.org,
92
+ runtimeEnv: buildRuntimeEnv(server, opts.envOverrides),
93
+ });
94
+ return serverSideConnect(client, server, input, opts);
89
95
  }
90
96
 
91
- // Soft timeout on the server-side connect (the client/client.ts idiom): races
92
- // the RPC against a timer without cancelling it — the backend's connect
93
- // workflow keeps running and persists its result on its own, so the message
94
- // says exactly that instead of implying the connect failed.
95
- async function boundedPush<T>(push: Promise<T>, timeoutMs: number, server: McpServer): Promise<T> {
97
+ // Run the server-side connect through the SDK's shared async-lane protocol
98
+ // (connectAndWait, stigmer/stigmer#425): startConnect + poll, with the
99
+ // blocking-RPC fallback for backends that predate the lane. The CLI's job is
100
+ // only to translate its --timeout semantics and turn a still-running stop
101
+ // into its own actionable guidance.
102
+ async function serverSideConnect(
103
+ client: Stigmer,
104
+ server: McpServer,
105
+ input: ConnectInput,
106
+ opts: ConnectOptions,
107
+ ): Promise<ConnectResult> {
96
108
  const slug = server.metadata?.slug ?? server.metadata?.id ?? "the server";
97
- let timer: NodeJS.Timeout | undefined;
98
- const timeout = new Promise<never>((_, reject) => {
99
- timer = setTimeout(
100
- () =>
101
- reject(
102
- new CliExitError(
103
- `Stopped waiting for the connect of MCP server '${slug}' after ${timeoutMs / 1000}s (--timeout)`,
104
- ExitCode.Connection,
105
- [
106
- "The server-side connect is still running and will persist its result if it succeeds.",
107
- `Check the outcome with: stigmer get mcp-server ${slug}`,
108
- "Re-run without --timeout to wait for completion.",
109
- ],
110
- ),
111
- ),
112
- timeoutMs,
113
- );
114
- });
115
- // The losing push may settle after the CLI has started exiting; swallow its
116
- // late rejection so it cannot surface as an unhandled-rejection crash.
117
- void push.catch(() => {});
109
+ let warning = "";
118
110
  try {
119
- return await Promise.race([push, timeout]);
120
- } finally {
121
- if (timer !== undefined) clearTimeout(timer);
111
+ const updated = await connectAndWait(client.mcpServer, input, {
112
+ deadlineMs: opts.pushTimeoutMs,
113
+ onStarted: (started) => {
114
+ warning = started.status?.connectStatus?.warning ?? "";
115
+ },
116
+ });
117
+ return {
118
+ server,
119
+ capabilities: updated.status?.discoveredCapabilities,
120
+ updated,
121
+ warning: warning !== "" ? warning : undefined,
122
+ };
123
+ } catch (err) {
124
+ if (!(err instanceof ConnectStillRunningError)) throw err;
125
+ if (opts.pushTimeoutMs !== undefined) {
126
+ // The user's explicit --timeout fired: historical soft-bound semantics.
127
+ throw new CliExitError(
128
+ `Stopped waiting for the connect of MCP server '${slug}' after ${opts.pushTimeoutMs / 1000}s (--timeout)`,
129
+ ExitCode.Connection,
130
+ [
131
+ "The server-side connect is still running and will persist its result if it succeeds.",
132
+ `Check the outcome with: stigmer get mcp-server ${slug}`,
133
+ "Re-run without --timeout to wait for completion.",
134
+ ],
135
+ );
136
+ }
137
+ // The SDK's settle bound fired: past the backend's own ceiling, only a
138
+ // connect_status orphaned by a backend restart can still read CONNECTING.
139
+ throw new CliExitError(
140
+ `The connect of MCP server '${slug}' did not settle within ${CONNECT_SETTLE_BOUND_MS / 60_000} minutes`,
141
+ ExitCode.Connection,
142
+ [
143
+ "This usually means the backend restarted mid-operation.",
144
+ `Check the current state with: stigmer get mcp-server ${slug}`,
145
+ "Re-run the connect to start a fresh operation.",
146
+ ],
147
+ );
122
148
  }
123
149
  }
124
150
 
@@ -26,6 +26,9 @@ export function renderConnectResult(result: ConnectResult, sink: ConnectSink, co
26
26
  sink(result.updated !== undefined
27
27
  ? style.green("✓ Connected — capabilities and tool approvals saved")
28
28
  : style.yellow("⚠ Dry run — results not saved to backend"));
29
+ if (result.warning !== undefined) {
30
+ sink(style.yellow(`⚠ ${result.warning}`));
31
+ }
29
32
  sink("");
30
33
  }
31
34
 
@@ -1,12 +1,14 @@
1
1
  // `list` dispatch: render a collection of resources for a kind.
2
2
  //
3
- // Most registry kinds are search-indexed and list through the unified
4
- // SearchService. Kinds that are deliberately not search-indexed
5
- // (organizations, API keys, agent instances, agent channels, channel apps,
6
- // schedules) use their dedicated find/list RPCs with bespoke table shapes,
7
- // registered in LIST_HANDLERS — the same map-dispatch shape as the get,
8
- // delete, and apply registries, so the conformance test in
9
- // registry/registry.test.ts can hold all four to the verb matrix.
3
+ // Most registry kinds list through the unified SearchService. The rest take
4
+ // dedicated find/list RPCs with bespoke table shapes, registered in
5
+ // LIST_HANDLERS — either because the kind is not search-indexed (API keys,
6
+ // agent channels, channel apps, schedules) or because the dedicated RPC is
7
+ // the better list surface than the kind's search index (organizations are
8
+ // caller-scoped; agent instances and sessions carry columns search results
9
+ // don't have). Same map-dispatch shape as the get, delete, and apply
10
+ // registries, so the conformance test in registry/registry.test.ts can hold
11
+ // all four to the verb matrix.
10
12
  //
11
13
  // Handlers FETCH, the dispatcher RENDERS: every handler returns entries plus
12
14
  // its schema/table pair, and listResources alone applies --limit and renders.
@@ -23,6 +25,8 @@ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/
23
25
  import { ListChannelAppsByOrgInputSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/io_pb";
24
26
  import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
25
27
  import { ListSchedulesRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
28
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
29
+ import { ListSessionsRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/io_pb";
26
30
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
31
  import { PageInfoSchema } from "@stigmer/protos/ai/stigmer/commons/rpc/pagination_pb";
28
32
  import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
@@ -37,8 +41,8 @@ import { bool, type JsonObject, obj, renderCollection, str, type TableShape } fr
37
41
  // scope). Must stay in step with the server's SearchableKinds allowlist
38
42
  // (backend/services/stigmer-server/pkg/query/search/valueobject/
39
43
  // search_criteria.go): a kind present here but absent there silently lists
40
- // as empty. The LIST_HANDLERS kinds are deliberately NOT here — they are
41
- // not_search_indexed by design and take dedicated-RPC handlers below.
44
+ // as empty. The LIST_HANDLERS kinds are deliberately NOT here — see the
45
+ // header: not search-indexed, or better served by their dedicated RPC.
42
46
  export const SEARCH_KINDS: ReadonlySet<ApiResourceKind> = new Set<ApiResourceKind>([
43
47
  ApiResourceKind.agent,
44
48
  ApiResourceKind.workflow,
@@ -129,6 +133,19 @@ export const LIST_HANDLERS: ReadonlyMap<ApiResourceKind, ListFn> = new Map<ApiRe
129
133
  return { schema: ScheduleSchema, entries: result.items, table: SCHEDULE_TABLE };
130
134
  },
131
135
  ],
136
+ [
137
+ ApiResourceKind.session,
138
+ async (client, _org, limit) => {
139
+ // Sessions ARE search-indexed, but list deliberately uses the dedicated
140
+ // RPC: the table's columns (agent instance, subject) don't exist on
141
+ // SearchResult rows, and the RPC is caller-scoped like organization and
142
+ // api_key above — the agent_instance precedent, not an oversight.
143
+ // Promoted from a bespoke pre-gate route in commands/list.ts by
144
+ // stigmer/stigmer#469 (it had shipped working-but-unadvertised).
145
+ const result = await client.session.list(create(ListSessionsRequestSchema, { pageSize: limit }));
146
+ return { schema: SessionSchema, entries: result.entries, table: SESSION_TABLE };
147
+ },
148
+ ],
132
149
  ]);
133
150
 
134
151
  export async function listResources(
@@ -177,6 +194,20 @@ export const SEARCH_TABLE: TableShape = {
177
194
  ],
178
195
  };
179
196
 
197
+ // A session's identity for follow-up commands is its ID (`stigmer session
198
+ // resume <id>`), so the ID leads; agent instance and subject are how a human
199
+ // recognizes which conversation it was.
200
+ const SESSION_TABLE: TableShape = {
201
+ resourceName: "sessions",
202
+ headers: ["SESSION ID", "AGENT", "SUBJECT", "CREATED"],
203
+ row: (json) => [
204
+ str(obj(json, "metadata"), "id"),
205
+ str(obj(json, "spec"), "agent_instance_id") || "-",
206
+ truncate(str(obj(json, "spec"), "subject") || "-", 50),
207
+ date(str(obj(json, "metadata"), "created_at")),
208
+ ],
209
+ };
210
+
180
211
  const INSTANCE_TABLE: TableShape = {
181
212
  resourceName: "agent instances",
182
213
  headers: ["ID", "SLUG", "AGENT", "DESCRIPTION"],
@@ -25,9 +25,9 @@ import type { AddressInfo } from "node:net";
25
25
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
26
26
  import { classify, ExitCode } from "../errors/index.js";
27
27
  import { getExecution, listAgentExecutions, renderExecutionList } from "./execution.js";
28
+ import { listResources } from "./list.js";
28
29
  import { renderResource } from "./render.js";
29
30
  import { searchResources } from "./search.js";
30
- import { listSessions, renderSessionList } from "./session.js";
31
31
  import { getSessionUsageReport, renderSessionUsage } from "./usage.js";
32
32
 
33
33
  const knownExec = create(AgentExecutionSchema, {
@@ -141,10 +141,20 @@ describe("execution integration", () => {
141
141
  });
142
142
  });
143
143
 
144
+ // Session list runs through the same registry dispatch as every other kind
145
+ // (LIST_HANDLERS; promoted from a bespoke route by stigmer/stigmer#469).
144
146
  describe("session integration", () => {
145
- it("lists sessions as a human table", async () => {
146
- const result = await listSessions(client, 50);
147
- expect(renderSessionList(result, "table")).toContain("ses_1");
147
+ it("lists sessions through the registry dispatch as a human table", async () => {
148
+ const rendered = await listResources(client, ApiResourceKind.session, "", 50, "table");
149
+ expect(rendered).toContain("SESSION ID");
150
+ expect(rendered).toContain("ses_1");
151
+ expect(rendered).toContain("Fix the build");
152
+ });
153
+
154
+ it("renders session list json as the entries array every list kind shares", async () => {
155
+ const rendered = await listResources(client, ApiResourceKind.session, "", 50, "json");
156
+ const json = JSON.parse(rendered);
157
+ expect(json).toEqual([toJson(SessionSchema, knownSession, { useProtoFieldName: true })]);
148
158
  });
149
159
  });
150
160
 
@@ -70,6 +70,27 @@ const BASE_FLAGS: AgentExecFlags = {
70
70
  // with no attachments a bare stub suffices.
71
71
  const STUB_CLIENT = { agentExecution: {} } as unknown as Stigmer;
72
72
 
73
+ /** Stub whose whoAmI resolves an account carrying the given preference. */
74
+ function clientWithPreference(defaultNativeModel: string): Stigmer {
75
+ return {
76
+ agentExecution: {},
77
+ identityAccount: {
78
+ whoAmI: () =>
79
+ Promise.resolve({ spec: { preferences: { defaultNativeModel } } }),
80
+ },
81
+ } as unknown as Stigmer;
82
+ }
83
+
84
+ /** Stub whose whoAmI rejects (auth failure, backend without the RPC, ...). */
85
+ function clientWithFailingWhoAmI(): Stigmer {
86
+ return {
87
+ agentExecution: {},
88
+ identityAccount: {
89
+ whoAmI: () => Promise.reject(new Error("unimplemented")),
90
+ },
91
+ } as unknown as Stigmer;
92
+ }
93
+
73
94
  describe("prepareAgentExec env injection", () => {
74
95
  it("injects STIGMER_ORG_ID when absent", async () => {
75
96
  const prepared = await prepareAgentExec(BASE_FLAGS, STUB_CLIENT, "acme");
@@ -103,3 +124,62 @@ describe("prepareAgentExec env injection", () => {
103
124
  expect(prepared.mode).toBe("plan");
104
125
  });
105
126
  });
127
+
128
+ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", () => {
129
+ it("fills an omitted --model from the account preference on cloud", async () => {
130
+ const prepared = await prepareAgentExec(
131
+ BASE_FLAGS,
132
+ clientWithPreference("claude-sonnet-4.6"),
133
+ "acme",
134
+ undefined,
135
+ { cloudBackend: true },
136
+ );
137
+ expect(prepared.model).toBe("claude-sonnet-4.6");
138
+ });
139
+
140
+ it("an explicit --model always outranks the preference", async () => {
141
+ const prepared = await prepareAgentExec(
142
+ { ...BASE_FLAGS, model: "gpt-5.3" },
143
+ clientWithPreference("claude-sonnet-4.6"),
144
+ "acme",
145
+ undefined,
146
+ { cloudBackend: true },
147
+ );
148
+ expect(prepared.model).toBe("gpt-5.3");
149
+ });
150
+
151
+ it("never consults identity on a local backend", async () => {
152
+ // The failing stub doubles as a call detector: local mode must not even
153
+ // attempt whoAmI, so a rejecting client cannot affect the result.
154
+ const prepared = await prepareAgentExec(
155
+ BASE_FLAGS,
156
+ clientWithFailingWhoAmI(),
157
+ "acme",
158
+ undefined,
159
+ { cloudBackend: false },
160
+ );
161
+ expect(prepared.model).toBe("");
162
+ });
163
+
164
+ it("falls through silently when whoAmI fails — a missing preference never fails a run", async () => {
165
+ const prepared = await prepareAgentExec(
166
+ BASE_FLAGS,
167
+ clientWithFailingWhoAmI(),
168
+ "acme",
169
+ undefined,
170
+ { cloudBackend: true },
171
+ );
172
+ expect(prepared.model).toBe("");
173
+ });
174
+
175
+ it("stays empty when the account declares no native default", async () => {
176
+ const prepared = await prepareAgentExec(
177
+ BASE_FLAGS,
178
+ clientWithPreference(""),
179
+ "acme",
180
+ undefined,
181
+ { cloudBackend: true },
182
+ );
183
+ expect(prepared.model).toBe("");
184
+ });
185
+ });
@@ -65,6 +65,18 @@ export interface PreparedRun {
65
65
  readonly serviceTier: ServiceTierFlag;
66
66
  }
67
67
 
68
+ /** Optional behavior switches for {@link prepareAgentExec}. */
69
+ export interface PrepareAgentExecOptions {
70
+ /**
71
+ * When `true` (the caller's backend is Stigmer Cloud) and `--model` is
72
+ * omitted, the model is filled from the caller's account preference
73
+ * (`IdentityAccountPreferences.default_native_model`) via `whoAmI()`.
74
+ * Local mode has no IdentityAccount, so callers pass `false` there and
75
+ * the omitted model keeps resolving to the platform default.
76
+ */
77
+ readonly cloudBackend?: boolean;
78
+ }
79
+
68
80
  /**
69
81
  * Validate flags and resolve all execution inputs. The `org` is used to inject
70
82
  * STIGMER_ORG_ID and the `client` to upload non-workspace attachments.
@@ -74,11 +86,22 @@ export async function prepareAgentExec(
74
86
  client: Stigmer,
75
87
  org: string,
76
88
  progress?: ProgressSink,
89
+ options?: PrepareAgentExecOptions,
77
90
  ): Promise<PreparedRun> {
78
91
  const defaultAction = parseApprovalAction(flags.approveDefault);
79
92
  validateMode(flags.mode);
80
93
  validateServiceTier(flags.serviceTier);
81
94
 
95
+ // Layered model seed (oss#293 Phase 1.5): an explicit --model always wins;
96
+ // an omitted one fills from the account preference on cloud. `run` and
97
+ // `draft` always create a NEW session (threading lives in `resume`, which
98
+ // does not pass through here), so the fill never injects a native model
99
+ // into an existing cursor-harness session.
100
+ const model =
101
+ flags.model === "" && options?.cloudBackend === true
102
+ ? await resolveModelFromAccountPreference(client)
103
+ : flags.model;
104
+
82
105
  const workspaceEntries = parseWorkspaceEntries(flags.workspace, flags.branch, flags.commit);
83
106
 
84
107
  const runtimeEnv = loadAndMergeEnv({
@@ -107,13 +130,30 @@ export async function prepareAgentExec(
107
130
  message: flags.message,
108
131
  detach: flags.detach,
109
132
  verbose: flags.verbose,
110
- model: flags.model,
133
+ model,
111
134
  autoApproveAll: flags.autoApprove,
112
135
  mode: flags.mode,
113
136
  serviceTier: flags.serviceTier,
114
137
  };
115
138
  }
116
139
 
140
+ /**
141
+ * Best-effort read of the caller's default model for native-harness runs
142
+ * (CLI sessions are native — there is no --harness flag yet, so the cursor
143
+ * default is deliberately not consulted). Any failure — network, auth, a
144
+ * backend without IdentityAccount — resolves to "" and the run proceeds
145
+ * exactly as an unfilled --model does today: a missing preference must
146
+ * never fail a run.
147
+ */
148
+ async function resolveModelFromAccountPreference(client: Stigmer): Promise<string> {
149
+ try {
150
+ const account = await client.identityAccount.whoAmI();
151
+ return account.spec?.preferences?.defaultNativeModel ?? "";
152
+ } catch {
153
+ return "";
154
+ }
155
+ }
156
+
117
157
  /**
118
158
  * Parse the `--approve-default` flag to an ApprovalAction. Empty means "no
119
159
  * default" (UNSPECIFIED). Mirrors Go's approval.ParseAction, including the
@@ -1,17 +1,15 @@
1
- // `list sessions` support. Sessions, like executions, are runtime resources that
2
- // bypass the resource registry: there is no `get`/`create`/`delete session` in
3
- // the unified verbs, only a list. This module owns the alias predicate and the
4
- // list fetch so the command layer stays declarative.
1
+ // Read helpers for the dedicated `stigmer session` command surface (resume
2
+ // and friends). Session LIST is a first-class registry verb served by
3
+ // LIST_HANDLERS in resources/list.ts — stigmer/stigmer#469 promoted it there
4
+ // from a bespoke pre-gate route in commands/list.ts. get/delete stay
5
+ // deliberately unpromised in the verb matrix (stigmer/stigmer#354);
6
+ // resume-style flows read sessions through these helpers instead.
5
7
 
6
8
  import { create } from "@bufbuild/protobuf";
7
9
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
8
10
  import { ListAgentExecutionsBySessionRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
9
11
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
10
- import { ListSessionsRequestSchema, SessionListSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/io_pb";
11
12
  import type { Stigmer } from "@stigmer/sdk";
12
- import type { OutputFormat } from "../output/index.js";
13
- import type { ResourceResult } from "./get-bindings.js";
14
- import { obj, renderListMessage, str, type TableShape } from "./render.js";
15
13
 
16
14
  // Mirrors Go's execution.MaxPageSize — fetch the whole session history in one
17
15
  // call (sessions don't accumulate enough executions to need pagination here).
@@ -33,34 +31,3 @@ export async function listExecutionsBySession(client: Stigmer, sessionId: string
33
31
  return list.entries;
34
32
  }
35
33
 
36
- /** True for the `session`/`sessions` type-alias family. */
37
- export function isSessionAlias(type: string): boolean {
38
- const normalized = type.trim().toLowerCase();
39
- return normalized === "session" || normalized === "sessions";
40
- }
41
-
42
- /** List sessions for the current context, paginated by `limit`. */
43
- export async function listSessions(client: Stigmer, limit: number): Promise<ResourceResult> {
44
- const message = await client.session.list(create(ListSessionsRequestSchema, { pageSize: limit }));
45
- return { schema: SessionListSchema, message };
46
- }
47
-
48
- const SESSION_TABLE: TableShape = {
49
- resourceName: "sessions",
50
- headers: ["SESSION ID", "AGENT", "SUBJECT", "CREATED"],
51
- row: (json) => [
52
- str(obj(json, "metadata"), "id"),
53
- dash(str(obj(json, "spec"), "agent_instance_id")),
54
- dash(str(obj(json, "spec"), "subject")),
55
- dash(str(obj(json, "metadata"), "created_at")),
56
- ],
57
- };
58
-
59
- /** Render a session list (json/yaml = full envelope; table = grid). */
60
- export function renderSessionList(result: ResourceResult, format: OutputFormat): string {
61
- return renderListMessage(result.schema, result.message, format, SESSION_TABLE);
62
- }
63
-
64
- function dash(value: string): string {
65
- return value === "" ? "-" : value;
66
- }
@@ -4,9 +4,6 @@
4
4
  import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
- import { create } from "@bufbuild/protobuf";
8
- import { Code, ConnectError } from "@connectrpc/connect";
9
- import { PushSkillRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
10
7
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
11
8
  import type { Stigmer } from "@stigmer/sdk";
12
9
  import { unzipSync, zipSync } from "fflate";
@@ -19,7 +16,6 @@ import {
19
16
  hasSkillFile,
20
17
  parseSkillMetadata,
21
18
  parseVisibility,
22
- pushRouted,
23
19
  pushSkill,
24
20
  pushSkillFromArchive,
25
21
  readSkillArchive,
@@ -354,76 +350,8 @@ describe("formatBytes / shortHash", () => {
354
350
  });
355
351
  });
356
352
 
357
- // ─── pushRouted — transfer-lane size routing (#675) ──────────────────────
358
-
359
- describe("pushRouted size routing", () => {
360
- const INLINE_MAX = 10 * 1024 * 1024 - 64 * 1024;
361
-
362
- function routingFakeClient(overrides: Record<string, any> = {}) {
363
- const calls = { push: [] as any[], mint: [] as any[] };
364
- const client = {
365
- skill: {
366
- async push(req: any) {
367
- calls.push.push(req);
368
- return { metadata: { id: "skill_x" } };
369
- },
370
- async createArtifactUploadUrl(req: any) {
371
- calls.mint.push(req);
372
- return { url: "http://localhost:7234/v1/skill-artifacts/uploads/sau_t", artifactUploadRef: "sau_t", ttlSeconds: 900 };
373
- },
374
- ...overrides,
375
- },
376
- } as any;
377
- return { client, calls };
378
- }
379
-
380
- afterEach(() => {
381
- vi.unstubAllGlobals();
382
- });
383
-
384
- it("keeps small artifacts inline (no mint, bytes in the request)", async () => {
385
- const { client, calls } = routingFakeClient();
386
- const req = create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(1024) });
387
-
388
- await pushRouted(client, req);
389
-
390
- expect(calls.mint).toHaveLength(0);
391
- expect(calls.push).toHaveLength(1);
392
- expect(calls.push[0].artifact.length).toBe(1024);
393
- });
394
-
395
- it("stages large artifacts over HTTP and pushes by reference", async () => {
396
- const { client, calls } = routingFakeClient();
397
- const artifact = new Uint8Array(INLINE_MAX + 1);
398
- const req = create(PushSkillRequestSchema, { org: "acme", artifact, tag: "stable", message: "big" });
399
-
400
- let putBytes = 0;
401
- vi.stubGlobal("fetch", vi.fn(async (_url: string, init: any) => {
402
- putBytes = init.body.length;
403
- return { ok: true } as any;
404
- }));
405
-
406
- await pushRouted(client, req);
407
-
408
- expect(calls.mint).toHaveLength(1);
409
- expect(calls.mint[0].sizeBytes).toBe(BigInt(artifact.length));
410
- expect(putBytes).toBe(artifact.length);
411
- expect(calls.push).toHaveLength(1);
412
- expect(calls.push[0].artifact.length).toBe(0);
413
- expect(calls.push[0].artifactUploadRef).toBe("sau_t");
414
- // The rewrite must not lose the rest of the request.
415
- expect(calls.push[0].tag).toBe("stable");
416
- expect(calls.push[0].message).toBe("big");
417
- });
418
-
419
- it("fails loud against servers that predate the transfer lane", async () => {
420
- const { client } = routingFakeClient({
421
- async createArtifactUploadUrl() {
422
- throw new ConnectError("unknown method", Code.Unimplemented);
423
- },
424
- });
425
- const req = create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(INLINE_MAX + 1) });
426
-
427
- await expect(pushRouted(client, req)).rejects.toThrow(/Upgrade stigmer-server/);
428
- });
429
- });
353
+ // The transfer-lane size routing moved down into the SDK (stigmer#701):
354
+ // its pins live in sdk/typescript/src/__tests__/skill.test.ts, exercised
355
+ // through the REAL generated client and error wrapping — the fake client
356
+ // this file previously used threw raw ConnectError, which the SDK's
357
+ // wrapError makes unreachable on the real path.