@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.
- package/commands/draft.js +4 -2
- package/commands/draft.js.map +1 -1
- package/commands/list.js +7 -15
- package/commands/list.js.map +1 -1
- package/commands/run.js +4 -2
- package/commands/run.js.map +1 -1
- package/commands/seedpack.d.ts.map +1 -1
- package/commands/seedpack.js +27 -5
- package/commands/seedpack.js.map +1 -1
- package/local/seedpack/apply.d.ts +32 -3
- package/local/seedpack/apply.d.ts.map +1 -1
- package/local/seedpack/apply.js +59 -8
- package/local/seedpack/apply.js.map +1 -1
- package/package.json +5 -5
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +6 -3
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +10 -1
- package/resources/apply/apply.js.map +1 -1
- package/resources/connect/connect.d.ts +7 -0
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +47 -30
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/display.d.ts.map +1 -1
- package/resources/connect/display.js +3 -0
- package/resources/connect/display.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +39 -9
- package/resources/list.js.map +1 -1
- package/resources/run/prepare.d.ts +12 -1
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +27 -2
- package/resources/run/prepare.js.map +1 -1
- package/resources/session.d.ts +0 -8
- package/resources/session.d.ts.map +1 -1
- package/resources/session.js +6 -33
- package/resources/session.js.map +1 -1
- package/resources/skill.d.ts +0 -10
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +8 -59
- package/resources/skill.js.map +1 -1
- package/resources/stream/events.d.ts +1 -1
- package/resources/stream/events.d.ts.map +1 -1
- package/resources/stream/events.js +8 -6
- package/resources/stream/events.js.map +1 -1
- package/resources/stream/render-plaintext.d.ts.map +1 -1
- package/resources/stream/render-plaintext.js +19 -5
- package/resources/stream/render-plaintext.js.map +1 -1
- package/resources/task-configs.d.ts +15 -0
- package/resources/task-configs.d.ts.map +1 -0
- package/resources/task-configs.js +135 -0
- package/resources/task-configs.js.map +1 -0
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +8 -1
- package/resources/validate.js.map +1 -1
- package/src/commands/draft.ts +4 -2
- package/src/commands/list.ts +7 -17
- package/src/commands/run.ts +4 -2
- package/src/commands/seedpack.ts +27 -5
- package/src/local/seedpack/apply.test.ts +127 -6
- package/src/local/seedpack/apply.ts +69 -9
- package/src/registry/registry.test.ts +50 -8
- package/src/registry/verb-support.ts +6 -3
- package/src/resources/apply/apply.ts +11 -1
- package/src/resources/connect/connect.ts +70 -44
- package/src/resources/connect/display.ts +3 -0
- package/src/resources/list.ts +40 -9
- package/src/resources/read-completion.integration.test.ts +14 -4
- package/src/resources/run/prepare.test.ts +80 -0
- package/src/resources/run/prepare.ts +41 -1
- package/src/resources/session.ts +6 -39
- package/src/resources/skill.test.ts +5 -77
- package/src/resources/skill.ts +8 -67
- package/src/resources/stream/events.ts +9 -7
- package/src/resources/stream/render-plaintext.test.ts +51 -0
- package/src/resources/stream/render-plaintext.ts +24 -6
- package/src/resources/task-configs.test.ts +98 -0
- package/src/resources/task-configs.ts +150 -0
- package/src/resources/validate.test.ts +26 -0
- package/src/resources/validate.ts +10 -2
- package/src/resources/session.test.ts +0 -48
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// `connect mcp-server` orchestration.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
package/src/resources/list.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// `list` dispatch: render a collection of resources for a kind.
|
|
2
2
|
//
|
|
3
|
-
// Most registry kinds
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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 —
|
|
41
|
-
//
|
|
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
|
|
147
|
-
expect(
|
|
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
|
|
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
|
package/src/resources/session.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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.
|