@stigmer/sdk 3.12.2 → 3.12.4

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 (62) hide show
  1. package/__tests__/billing.test.js +36 -0
  2. package/__tests__/billing.test.js.map +1 -1
  3. package/__tests__/skill.test.d.ts +2 -0
  4. package/__tests__/skill.test.d.ts.map +1 -0
  5. package/__tests__/skill.test.js +117 -0
  6. package/__tests__/skill.test.js.map +1 -0
  7. package/billing.d.ts +27 -0
  8. package/billing.d.ts.map +1 -1
  9. package/billing.js +26 -1
  10. package/billing.js.map +1 -1
  11. package/errors.d.ts +1 -1
  12. package/errors.d.ts.map +1 -1
  13. package/errors.js +1 -1
  14. package/errors.js.map +1 -1
  15. package/execution/__tests__/tool-view.fixtures.test.js +23 -4
  16. package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
  17. package/execution/tool-view.d.ts +32 -0
  18. package/execution/tool-view.d.ts.map +1 -1
  19. package/execution/tool-view.js +45 -0
  20. package/execution/tool-view.js.map +1 -1
  21. package/gen/client.d.ts +1 -1
  22. package/gen/client.d.ts.map +1 -1
  23. package/gen/client.js +1 -1
  24. package/gen/client.js.map +1 -1
  25. package/gen/errors.d.ts +7 -0
  26. package/gen/errors.d.ts.map +1 -1
  27. package/gen/errors.js +9 -0
  28. package/gen/errors.js.map +1 -1
  29. package/gen/skill.d.ts +3 -1
  30. package/gen/skill.d.ts.map +1 -1
  31. package/gen/skill.js +16 -0
  32. package/gen/skill.js.map +1 -1
  33. package/index.d.ts +4 -2
  34. package/index.d.ts.map +1 -1
  35. package/index.js +5 -2
  36. package/index.js.map +1 -1
  37. package/package.json +2 -2
  38. package/provider-standing.d.ts +19 -0
  39. package/provider-standing.d.ts.map +1 -0
  40. package/provider-standing.js +31 -0
  41. package/provider-standing.js.map +1 -0
  42. package/skill.d.ts +51 -0
  43. package/skill.d.ts.map +1 -0
  44. package/skill.js +102 -0
  45. package/skill.js.map +1 -0
  46. package/src/__tests__/billing.test.ts +44 -0
  47. package/src/__tests__/skill.test.ts +150 -0
  48. package/src/billing.ts +44 -0
  49. package/src/errors.ts +1 -0
  50. package/src/execution/__tests__/tool-view.fixtures.test.ts +41 -4
  51. package/src/execution/tool-view.ts +52 -0
  52. package/src/gen/client.ts +1 -1
  53. package/src/gen/errors.ts +10 -0
  54. package/src/gen/skill.ts +13 -1
  55. package/src/index.ts +8 -0
  56. package/src/provider-standing.ts +37 -0
  57. package/src/skill.ts +125 -0
  58. package/src/stigmer.ts +12 -0
  59. package/stigmer.d.ts +10 -0
  60. package/stigmer.d.ts.map +1 -1
  61. package/stigmer.js +12 -0
  62. package/stigmer.js.map +1 -1
package/skill.js ADDED
@@ -0,0 +1,102 @@
1
+ import { create } from "@bufbuild/protobuf";
2
+ import { Code } from "@connectrpc/connect";
3
+ import { CreateSkillArtifactUploadUrlRequestSchema, PushSkillRequestSchema, } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
4
+ import { isUnimplemented, StigmerError } from "./gen/errors.js";
5
+ import { SkillClient } from "./gen/skill.js";
6
+ /**
7
+ * Largest artifact pushed inline in the gRPC request (#675). The server's
8
+ * transport cap is 10MB for the WHOLE message, so the artifact leaves 64KB
9
+ * of headroom for the request envelope (org, tag, provenance, framing).
10
+ * Mirrors the Go SDK's maxInlineArtifactBytes.
11
+ */
12
+ export const MAX_INLINE_ARTIFACT_BYTES = 10 * 1024 * 1024 - 64 * 1024;
13
+ /**
14
+ * Skill client with transport-aware push routing (stigmer#675 / #701).
15
+ *
16
+ * The gRPC transport caps messages at 10MB while skills may be up to 100MB,
17
+ * so `push` routes by size: small artifacts travel inline in the request
18
+ * (one round trip, unchanged behavior), larger ones are staged over HTTP
19
+ * via `createArtifactUploadUrl` — a capability URL, so no auth header —
20
+ * and pushed by reference. Callers never see the mechanics: `push(req)`
21
+ * simply works for any valid skill size.
22
+ *
23
+ * Every other method is the generated client's, inherited unchanged.
24
+ */
25
+ export class RoutedSkillClient extends SkillClient {
26
+ fetchImpl;
27
+ /**
28
+ * @param fetchImpl - Custom `fetch` for the staging PUT. Must be the same
29
+ * implementation the transport uses where the global one is restricted
30
+ * (the Tauri CSP/CORS case the `Stigmer.fetch` property documents).
31
+ */
32
+ constructor(transport, fetchImpl) {
33
+ super(transport);
34
+ this.fetchImpl = fetchImpl;
35
+ }
36
+ /**
37
+ * Push a skill, routing the artifact by size (see the class comment).
38
+ *
39
+ * A request that already carries an `artifactUploadRef` is passed through
40
+ * untouched — the caller has done its own staging.
41
+ */
42
+ async push(input) {
43
+ if (input.artifactUploadRef !== "" || input.artifact.length <= MAX_INLINE_ARTIFACT_BYTES) {
44
+ return super.push(input);
45
+ }
46
+ return this.pushViaUploadUrl(input);
47
+ }
48
+ /**
49
+ * Stage the artifact over HTTP and push by reference:
50
+ * createArtifactUploadUrl → PUT bytes → push(artifactUploadRef).
51
+ */
52
+ async pushViaUploadUrl(input) {
53
+ let minted;
54
+ try {
55
+ minted = await super.createArtifactUploadUrl(create(CreateSkillArtifactUploadUrlRequestSchema, {
56
+ org: input.org,
57
+ sizeBytes: BigInt(input.artifact.length),
58
+ }));
59
+ }
60
+ catch (err) {
61
+ if (isUnimplemented(err)) {
62
+ // Pre-transfer-lane server: without staging, an artifact this size
63
+ // physically cannot travel. Say so instead of surfacing the raw
64
+ // transport error (the failure mode #675 reported).
65
+ throw new StigmerError("unknown", `skill artifact is ${input.artifact.length} bytes, above the ~10MB gRPC message cap, ` +
66
+ "and this server does not support the HTTP artifact transfer lane — " +
67
+ "upgrade stigmer-server to push skills of this size", Code.Unimplemented, { cause: err });
68
+ }
69
+ throw err;
70
+ }
71
+ await this.putArtifact(minted.url, input.artifact);
72
+ // Same request, artifact traveling by reference instead of by value.
73
+ const byRef = create(PushSkillRequestSchema, {
74
+ ...input,
75
+ artifact: new Uint8Array(0),
76
+ artifactUploadRef: minted.artifactUploadRef,
77
+ });
78
+ return super.push(byRef);
79
+ }
80
+ /**
81
+ * PUT the artifact ZIP to the staging URL. The URL is the credential
82
+ * (capability semantics — a pre-signed R2 URL on cloud, the server's own
83
+ * transfer lane on OSS), so no auth header is attached.
84
+ */
85
+ async putArtifact(url, artifact) {
86
+ const doFetch = this.fetchImpl ?? globalThis.fetch;
87
+ const resp = await doFetch(url, {
88
+ method: "PUT",
89
+ // Both DOM and undici accept an ArrayBufferView body at runtime; the
90
+ // cast bridges TS 5.7's ArrayBufferLike generic, which BodyInit's
91
+ // typing predates. A Blob/Buffer wrapper would copy up to 100MB for
92
+ // nothing, and Buffer is Node-only while this client is isomorphic.
93
+ body: artifact,
94
+ headers: { "content-type": "application/zip" },
95
+ });
96
+ if (!resp.ok) {
97
+ const detail = (await resp.text().catch(() => "")).slice(0, 512).trim();
98
+ throw new StigmerError("unknown", `skill artifact upload rejected with HTTP ${resp.status}${detail === "" ? "" : `: ${detail}`}`, Code.Unknown);
99
+ }
100
+ }
101
+ }
102
+ //# sourceMappingURL=skill.js.map
package/skill.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ import { BillingQueryController } from "@stigmer/protos/ai/stigmer/billing/v1/qu
12
12
  import {
13
13
  CreditLedgerResponseSchema,
14
14
  type AdjustCreditsInput,
15
+ type GrantCreditsInput,
15
16
  type GetCreditLedgerInput,
16
17
  } from "@stigmer/protos/ai/stigmer/billing/v1/io_pb";
17
18
  import { CreditLedgerEntrySchema } from "@stigmer/protos/ai/stigmer/billing/v1/credit_pb";
@@ -20,6 +21,7 @@ import { BillingClient } from "../billing.js";
20
21
 
21
22
  interface Captured {
22
23
  adjustCredits?: AdjustCreditsInput;
24
+ grantCredits?: GrantCreditsInput;
23
25
  getCreditLedger?: GetCreditLedgerInput;
24
26
  }
25
27
 
@@ -30,6 +32,10 @@ function fakeTransport(captured: Captured): Transport {
30
32
  captured.adjustCredits = req;
31
33
  return create(CreditLedgerEntrySchema, { amountMicros: req.amountMicros });
32
34
  },
35
+ grantCredits: (req) => {
36
+ captured.grantCredits = req;
37
+ return create(CreditLedgerEntrySchema, { amountMicros: req.amountMicros });
38
+ },
33
39
  });
34
40
  service(BillingQueryController, {
35
41
  getCreditLedger: (req) => {
@@ -60,6 +66,44 @@ describe("BillingClient.adjustCredits", () => {
60
66
  });
61
67
  });
62
68
 
69
+ describe("BillingClient.grantCredits", () => {
70
+ it("maps params including the expiry onto GrantCreditsInput", async () => {
71
+ const captured: Captured = {};
72
+ const client = new BillingClient(fakeTransport(captured));
73
+
74
+ const expiresAt = new Date("2026-08-31T23:59:59Z");
75
+ const entry = await client.grantCredits({
76
+ orgId: "acme",
77
+ amountMicros: 5_000_000n,
78
+ expiresAt,
79
+ reason: "monthly free allowance 2026-08",
80
+ idempotencyKey: "allowance-acme-2026-08",
81
+ });
82
+
83
+ expect(entry.amountMicros).toBe(5_000_000n);
84
+ const req = captured.grantCredits;
85
+ expect(req?.orgId).toBe("acme");
86
+ expect(req?.amountMicros).toBe(5_000_000n);
87
+ expect(req?.expiresAt && timestampDate(req.expiresAt)).toEqual(expiresAt);
88
+ expect(req?.reason).toBe("monthly free allowance 2026-08");
89
+ expect(req?.idempotencyKey).toBe("allowance-acme-2026-08");
90
+ });
91
+
92
+ it("omits the expiry for a never-expiring grant", async () => {
93
+ const captured: Captured = {};
94
+ const client = new BillingClient(fakeTransport(captured));
95
+
96
+ await client.grantCredits({
97
+ orgId: "acme",
98
+ amountMicros: 1_000_000n,
99
+ reason: "welcome credit",
100
+ idempotencyKey: "welcome-acme",
101
+ });
102
+
103
+ expect(captured.grantCredits?.expiresAt).toBeUndefined();
104
+ });
105
+ });
106
+
63
107
  describe("BillingClient.getCreditLedger", () => {
64
108
  it("maps all filters including the time range", async () => {
65
109
  const captured: Captured = {};
@@ -0,0 +1,150 @@
1
+ // Wire-shape tests for RoutedSkillClient's size-routed push (stigmer#701).
2
+ // A router transport fakes the skill service in-process, so the routing is
3
+ // exercised through the REAL generated client and its error wrapping — the
4
+ // CLI's earlier copy of this logic keyed its fallback on `instanceof
5
+ // ConnectError`, which the SDK's wrapError made unreachable; these pins run
6
+ // the wrapped path. Mirrors sdk/go/skill_test.go's five routing pins.
7
+ import { describe, expect, it, vi } from "vitest";
8
+ import { Code, ConnectError, createRouterTransport, type Transport } from "@connectrpc/connect";
9
+ import { create } from "@bufbuild/protobuf";
10
+ import { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/command_pb";
11
+ import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
12
+ import {
13
+ PushSkillRequestSchema,
14
+ SkillArtifactUploadUrlSchema,
15
+ type PushSkillRequest,
16
+ } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
17
+ import { StigmerError } from "../gen/errors.js";
18
+ import { MAX_INLINE_ARTIFACT_BYTES, RoutedSkillClient } from "../skill.js";
19
+
20
+ const STAGING_URL = "http://stage.example/v1/skill-artifacts/uploads/sau_t";
21
+
22
+ interface Captured {
23
+ pushes: PushSkillRequest[];
24
+ mints: number;
25
+ }
26
+
27
+ /**
28
+ * In-process skill service. `withMint: false` leaves createArtifactUploadUrl
29
+ * unregistered, so the router answers Unimplemented — the exact wire shape
30
+ * of a pre-transfer-lane server.
31
+ */
32
+ function fakeTransport(captured: Captured, opts: { withMint: boolean } = { withMint: true }): Transport {
33
+ return createRouterTransport(({ service }) => {
34
+ service(SkillCommandController, {
35
+ push: (req) => {
36
+ captured.pushes.push(req);
37
+ return create(SkillSchema, {});
38
+ },
39
+ ...(opts.withMint
40
+ ? {
41
+ createArtifactUploadUrl: (req) => {
42
+ captured.mints++;
43
+ expect(req.sizeBytes).toBeGreaterThan(0n);
44
+ return create(SkillArtifactUploadUrlSchema, {
45
+ url: STAGING_URL,
46
+ artifactUploadRef: "sau_t",
47
+ ttlSeconds: 900,
48
+ });
49
+ },
50
+ }
51
+ : {}),
52
+ });
53
+ });
54
+ }
55
+
56
+ function okFetch(record: { putUrl?: string; putBytes?: number; contentType?: string }) {
57
+ return vi.fn(async (url: any, init: any) => {
58
+ record.putUrl = String(url);
59
+ record.putBytes = init.body instanceof Uint8Array ? init.body.byteLength : -1;
60
+ record.contentType = init.headers["content-type"];
61
+ return new Response(null, { status: 200 });
62
+ }) as unknown as typeof globalThis.fetch;
63
+ }
64
+
65
+ describe("RoutedSkillClient.push size routing", () => {
66
+ it("keeps small artifacts inline — no mint, bytes in the request", async () => {
67
+ const captured: Captured = { pushes: [], mints: 0 };
68
+ const client = new RoutedSkillClient(fakeTransport(captured));
69
+
70
+ await client.push(create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(1024) }));
71
+
72
+ expect(captured.mints).toBe(0);
73
+ expect(captured.pushes).toHaveLength(1);
74
+ expect(captured.pushes[0].artifact.length).toBe(1024);
75
+ });
76
+
77
+ it("stages large artifacts over HTTP and pushes by reference, preserving the envelope", async () => {
78
+ const captured: Captured = { pushes: [], mints: 0 };
79
+ const record: { putUrl?: string; putBytes?: number; contentType?: string } = {};
80
+ const client = new RoutedSkillClient(fakeTransport(captured), okFetch(record));
81
+ const artifact = new Uint8Array(MAX_INLINE_ARTIFACT_BYTES + 1);
82
+
83
+ await client.push(create(PushSkillRequestSchema, { org: "acme", artifact, tag: "stable", message: "big" }));
84
+
85
+ expect(captured.mints).toBe(1);
86
+ expect(record.putUrl).toBe(STAGING_URL);
87
+ expect(record.putBytes).toBe(artifact.length);
88
+ expect(record.contentType).toBe("application/zip");
89
+ expect(captured.pushes).toHaveLength(1);
90
+ expect(captured.pushes[0].artifact.length).toBe(0);
91
+ expect(captured.pushes[0].artifactUploadRef).toBe("sau_t");
92
+ // The by-ref rewrite must not lose the rest of the request.
93
+ expect(captured.pushes[0].tag).toBe("stable");
94
+ expect(captured.pushes[0].message).toBe("big");
95
+ });
96
+
97
+ it("passes an explicit upload ref through untouched — the caller staged it", async () => {
98
+ const captured: Captured = { pushes: [], mints: 0 };
99
+ const client = new RoutedSkillClient(fakeTransport(captured));
100
+
101
+ await client.push(create(PushSkillRequestSchema, { org: "acme", artifactUploadRef: "sau_mine" }));
102
+
103
+ expect(captured.mints).toBe(0);
104
+ expect(captured.pushes).toHaveLength(1);
105
+ expect(captured.pushes[0].artifactUploadRef).toBe("sau_mine");
106
+ });
107
+
108
+ it("fails loud against servers that predate the transfer lane (wrapped Unimplemented)", async () => {
109
+ const captured: Captured = { pushes: [], mints: 0 };
110
+ const client = new RoutedSkillClient(fakeTransport(captured, { withMint: false }));
111
+
112
+ await expect(
113
+ client.push(create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(MAX_INLINE_ARTIFACT_BYTES + 1) })),
114
+ ).rejects.toThrow(/upgrade stigmer-server/);
115
+ expect(captured.pushes).toHaveLength(0);
116
+ });
117
+
118
+ it("surfaces the staging rejection body and never proceeds to push", async () => {
119
+ const captured: Captured = { pushes: [], mints: 0 };
120
+ const rejectingFetch = (async () =>
121
+ new Response("staging slot expired", { status: 404 })) as unknown as typeof globalThis.fetch;
122
+ const client = new RoutedSkillClient(fakeTransport(captured), rejectingFetch);
123
+
124
+ await expect(
125
+ client.push(create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(MAX_INLINE_ARTIFACT_BYTES + 1) })),
126
+ ).rejects.toThrow(/HTTP 404: staging slot expired/);
127
+ expect(captured.pushes).toHaveLength(0);
128
+ });
129
+
130
+ it("propagates non-Unimplemented mint errors unmasked", async () => {
131
+ const captured: Captured = { pushes: [], mints: 0 };
132
+ const transport = createRouterTransport(({ service }) => {
133
+ service(SkillCommandController, {
134
+ push: (req) => {
135
+ captured.pushes.push(req);
136
+ return create(SkillSchema, {});
137
+ },
138
+ createArtifactUploadUrl: () => {
139
+ throw new ConnectError("artifact too large", Code.InvalidArgument);
140
+ },
141
+ });
142
+ });
143
+ const client = new RoutedSkillClient(transport);
144
+
145
+ await expect(
146
+ client.push(create(PushSkillRequestSchema, { org: "acme", artifact: new Uint8Array(MAX_INLINE_ARTIFACT_BYTES + 1) })),
147
+ ).rejects.toSatisfy((err: unknown) => err instanceof StigmerError && err.code === "invalid-argument");
148
+ expect(captured.pushes).toHaveLength(0);
149
+ });
150
+ });
package/src/billing.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  GetBillingAccountInputSchema,
8
8
  GetCreditBalanceInputSchema,
9
9
  AdjustCreditsInputSchema,
10
+ GrantCreditsInputSchema,
10
11
  GetCreditLedgerInputSchema,
11
12
  CreateCreditCheckoutSessionInputSchema,
12
13
  CreateBillingPortalSessionInputSchema,
@@ -69,6 +70,22 @@ export interface AdjustCreditsParams {
69
70
  readonly idempotencyKey: string;
70
71
  }
71
72
 
73
+ /** Parameters for a promotional credit grant. */
74
+ export interface GrantCreditsParams {
75
+ readonly orgId: string;
76
+ /** Amount to grant. Must be positive; grants never remove credits. */
77
+ readonly amountMicros: bigint;
78
+ /**
79
+ * When the grant expires (use-it-or-lose-it). Omit for a grant that never
80
+ * expires. Precision is whole seconds; sub-second precision is discarded.
81
+ */
82
+ readonly expiresAt?: Date;
83
+ /** Human-readable reason recorded on the ledger entry (audit trail). */
84
+ readonly reason: string;
85
+ /** Client-supplied deduplication key to prevent double-processing. */
86
+ readonly idempotencyKey: string;
87
+ }
88
+
72
89
  /** Parameters for querying the credit ledger. */
73
90
  export interface GetCreditLedgerParams {
74
91
  readonly orgId: string;
@@ -222,6 +239,33 @@ export class BillingClient {
222
239
  }
223
240
  }
224
241
 
242
+ /**
243
+ * Grant promotional credits to an organization, optionally expiring
244
+ * (use-it-or-lose-it).
245
+ *
246
+ * The grant burns before adjustment and purchased credits. When
247
+ * `expiresAt` is set, any remainder unconsumed at that time is removed
248
+ * from the balance by the platform's grant-expiry sweep. Idempotent:
249
+ * replaying an applied idempotency key returns the original ledger entry,
250
+ * even after the expiry has passed. Requires `can_manage_billing` on
251
+ * the org.
252
+ */
253
+ async grantCredits(params: GrantCreditsParams): Promise<CreditLedgerEntry> {
254
+ try {
255
+ return await this.command.grantCredits(
256
+ create(GrantCreditsInputSchema, {
257
+ orgId: params.orgId,
258
+ amountMicros: params.amountMicros,
259
+ reason: params.reason,
260
+ idempotencyKey: params.idempotencyKey,
261
+ ...(params.expiresAt && { expiresAt: timestampFromDate(params.expiresAt) }),
262
+ }),
263
+ );
264
+ } catch (e) {
265
+ throw wrapError(e);
266
+ }
267
+ }
268
+
225
269
  /** Retrieve paginated credit ledger entries with optional filters. */
226
270
  async getCreditLedger(params: GetCreditLedgerParams): Promise<CreditLedgerResponse> {
227
271
  try {
package/src/errors.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  isUnauthenticated,
12
12
  isPermissionDenied,
13
13
  isRetryable,
14
+ isUnimplemented,
14
15
  } from "./gen/errors.js";
15
16
 
16
17
  /**
@@ -1,6 +1,7 @@
1
- // Validates the tool-view layer against the shared cross-language contract in
2
- // test/fixtures/tool-view/. The Go CLI runs the same fixtures, so the two
3
- // surfaces cannot drift in classification or result interpretation.
1
+ // Validates the tool-view layer against the shared cross-surface contract in
2
+ // test/fixtures/tool-view/. The runner's middleware tests assert the same
3
+ // fixtures from the writer side, so producers and this reader cannot drift in
4
+ // classification, result interpretation, or intent extraction.
4
5
 
5
6
  import { readFileSync } from "node:fs";
6
7
  import { fileURLToPath } from "node:url";
@@ -9,7 +10,13 @@ import { describe, it, expect } from "vitest";
9
10
  import { create, type JsonObject } from "@bufbuild/protobuf";
10
11
  import { ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
11
12
  import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
12
- import { ToolKind, resolveToolKindByName, normalizeToolResult } from "../tool-view";
13
+ import {
14
+ ToolKind,
15
+ resolveToolKindByName,
16
+ normalizeToolResult,
17
+ extractShellIntent,
18
+ SHELL_INTENT_ARG_FIELD,
19
+ } from "../tool-view";
13
20
 
14
21
  const here = dirname(fileURLToPath(import.meta.url));
15
22
  // sdk/typescript/src/execution/__tests__ -> repo root is five levels up.
@@ -115,6 +122,36 @@ describe("result-view fixtures", () => {
115
122
  }
116
123
  });
117
124
 
125
+ describe("intent-title fixtures", () => {
126
+ interface IntentCase {
127
+ name: string;
128
+ mcpServerSlug: string;
129
+ args: Record<string, unknown>;
130
+ intent: string | null;
131
+ }
132
+
133
+ const fixture = loadFixture<{ argField: string; cases: IntentCase[] }>(
134
+ "intent-title.json",
135
+ );
136
+
137
+ it("loads the fixture and agrees on the wire key", () => {
138
+ expect(fixture.cases.length).toBeGreaterThan(0);
139
+ expect(SHELL_INTENT_ARG_FIELD).toBe(fixture.argField);
140
+ });
141
+
142
+ for (const c of fixture.cases) {
143
+ it(`extracts ${c.name}${c.mcpServerSlug ? ` @${c.mcpServerSlug}` : ""} -> ${JSON.stringify(c.intent)}`, () => {
144
+ const toolCall = create(ToolCallSchema, {
145
+ id: c.name,
146
+ name: c.name,
147
+ mcpServerSlug: c.mcpServerSlug,
148
+ args: c.args as JsonObject,
149
+ });
150
+ expect(extractShellIntent(toolCall)).toBe(c.intent);
151
+ });
152
+ }
153
+ });
154
+
118
155
  describe("resolveToolKind wire field precedence", () => {
119
156
  it("prefers the wire tool_kind over the name fallback", () => {
120
157
  const toolCall = create(ToolCallSchema, {
@@ -228,6 +228,58 @@ export function resolveToolKindByName(name: string, mcpServerSlug?: string): Too
228
228
  return ToolKind.UNSPECIFIED;
229
229
  }
230
230
 
231
+ // ---------------------------------------------------------------------------
232
+ // Intent: extractShellIntent
233
+ // ---------------------------------------------------------------------------
234
+
235
+ /**
236
+ * The tool-call argument that carries a model-authored intent phrase for
237
+ * SHELL tools (stigmer#276) — a short human description of what the command
238
+ * does and why, rendered as the row title with the command as secondary text.
239
+ *
240
+ * Two writers populate it, converged on one wire key by design: the native
241
+ * harness's tool-intent middleware (`backend/services/runner/src/middleware/
242
+ * tool-intent.ts`, `INTENT_ARG`) extends the shell tool's bind-time schema
243
+ * with it, and the Cursor harness's built-in Shell tool carries it natively.
244
+ * The shared, machine-checked contract is
245
+ * `test/fixtures/tool-view/intent-title.json` — keep the writers and this
246
+ * reader in lockstep.
247
+ */
248
+ export const SHELL_INTENT_ARG_FIELD = "description";
249
+
250
+ /**
251
+ * Args-level intent extraction: SHELL-kind-scoped on purpose, because
252
+ * `description` means other things on other tools (a task tool's description
253
+ * is the sub-agent subject, never a row title). Blank and non-string values
254
+ * degrade to null — callers fall back to their category label.
255
+ *
256
+ * The seam for surfaces that hold a pre-resolved kind and raw args instead of
257
+ * a full ToolCall: the CLI's headless snapshot projection and @stigmer/react's
258
+ * approval argsPreview path. Everything else should prefer
259
+ * {@link extractShellIntent}.
260
+ */
261
+ export function shellIntentFromArgs(
262
+ kind: ToolKind,
263
+ args: Record<string, unknown> | undefined,
264
+ ): string | null {
265
+ if (kind !== ToolKind.SHELL || !args) return null;
266
+ const value = args[SHELL_INTENT_ARG_FIELD];
267
+ if (typeof value !== "string") return null;
268
+ const trimmed = value.trim();
269
+ return trimmed.length > 0 ? trimmed : null;
270
+ }
271
+
272
+ /**
273
+ * Extracts the model-authored intent phrase from a SHELL tool call, or null
274
+ * when absent (legacy executions, models that skipped the optional arg, and
275
+ * every non-shell kind). See {@link SHELL_INTENT_ARG_FIELD}.
276
+ */
277
+ export function extractShellIntent(
278
+ toolCall: Pick<ToolCall, "name" | "mcpServerSlug" | "toolKind" | "args">,
279
+ ): string | null {
280
+ return shellIntentFromArgs(resolveToolKind(toolCall), toolCall.args as Args);
281
+ }
282
+
231
283
  // ---------------------------------------------------------------------------
232
284
  // Result: normalizeToolResult
233
285
  // ---------------------------------------------------------------------------
package/src/gen/client.ts CHANGED
@@ -136,4 +136,4 @@ export { type WorkflowExecutionInput } from "./workflowexecution.js";
136
136
  export { WorkflowInstanceClient } from "./workflowinstance.js";
137
137
  export { type WorkflowInstanceInput } from "./workflowinstance.js";
138
138
  export { type ListParams, type ListResult, type DeleteResourceInput, type ResourceRef, type EnvSpecInput, type EnvVarInput, type Page } from "./types.js";
139
- export { StigmerError, type ErrorCode, isNotFound, isUnauthenticated, isPermissionDenied, isRetryable } from "./errors.js";
139
+ export { StigmerError, type ErrorCode, isNotFound, isUnauthenticated, isPermissionDenied, isRetryable, isUnimplemented } from "./errors.js";
package/src/gen/errors.ts CHANGED
@@ -74,3 +74,13 @@ export function isPermissionDenied(err: unknown): boolean {
74
74
  export function isRetryable(err: unknown): boolean {
75
75
  return err instanceof StigmerError && (err.code === "internal" || err.code === "unavailable");
76
76
  }
77
+
78
+ /**
79
+ * The server does not implement the called RPC — the code clients key
80
+ * capability fallbacks on (e.g. the skill artifact transfer lane's unary
81
+ * fallback, stigmer#675/#701). Checks the raw connect code: Unimplemented
82
+ * deliberately has no ErrorCode mapping, so it surfaces as "unknown".
83
+ */
84
+ export function isUnimplemented(err: unknown): boolean {
85
+ return err instanceof StigmerError && err.connectCode === Code.Unimplemented;
86
+ }
package/src/gen/skill.ts CHANGED
@@ -7,7 +7,7 @@ import { create } from "@bufbuild/protobuf";
7
7
  import { createClient, type Client, type Transport } from "@connectrpc/connect";
8
8
  import { SkillSchema, type Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
9
9
  import { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/command_pb";
10
- import { SkillIdSchema, PushSkillRequestSchema, PushSkillFromExecutionArtifactRequestSchema, GetArtifactRequestSchema, GetArtifactResponseSchema, ListSkillVersionsInputSchema, ListSkillVersionsResponseSchema, type PushSkillRequest, type PushSkillFromExecutionArtifactRequest, type GetArtifactRequest, type GetArtifactResponse, type ListSkillVersionsInput, type ListSkillVersionsResponse } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
10
+ import { SkillIdSchema, PushSkillRequestSchema, CreateSkillArtifactUploadUrlRequestSchema, SkillArtifactUploadUrlSchema, PushSkillFromExecutionArtifactRequestSchema, GetArtifactRequestSchema, GetArtifactResponseSchema, SkillArtifactDownloadUrlSchema, ListSkillVersionsInputSchema, ListSkillVersionsResponseSchema, type PushSkillRequest, type CreateSkillArtifactUploadUrlRequest, type SkillArtifactUploadUrl, type PushSkillFromExecutionArtifactRequest, type GetArtifactRequest, type GetArtifactResponse, type SkillArtifactDownloadUrl, type ListSkillVersionsInput, type ListSkillVersionsResponse } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
11
11
  import { SkillQueryController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/query_pb";
12
12
  import { SkillSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/spec_pb";
13
13
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -36,6 +36,12 @@ export class SkillClient {
36
36
  } catch (e) { throw wrapError(e); }
37
37
  }
38
38
 
39
+ async createArtifactUploadUrl(input: CreateSkillArtifactUploadUrlRequest): Promise<SkillArtifactUploadUrl> {
40
+ try {
41
+ return await this.command.createArtifactUploadUrl(input);
42
+ } catch (e) { throw wrapError(e); }
43
+ }
44
+
39
45
  async pushFromExecutionArtifact(input: PushSkillFromExecutionArtifactRequest): Promise<Skill> {
40
46
  try {
41
47
  return await this.command.pushFromExecutionArtifact(input);
@@ -72,6 +78,12 @@ export class SkillClient {
72
78
  } catch (e) { throw wrapError(e); }
73
79
  }
74
80
 
81
+ async getArtifactDownloadUrl(input: GetArtifactRequest): Promise<SkillArtifactDownloadUrl> {
82
+ try {
83
+ return await this.query.getArtifactDownloadUrl(input);
84
+ } catch (e) { throw wrapError(e); }
85
+ }
86
+
75
87
  async listVersions(input: ListSkillVersionsInput): Promise<ListSkillVersionsResponse> {
76
88
  try {
77
89
  return await this.query.listVersions(input);
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ export {
41
41
  isUnauthenticated,
42
42
  isPermissionDenied,
43
43
  isRetryable,
44
+ isUnimplemented,
44
45
  type ErrorCategory,
45
46
  isConnectError,
46
47
  classifyError,
@@ -104,6 +105,9 @@ export {
104
105
  type SetCursorMemberKeyEnabledParams,
105
106
  } from "./cursor-accounts.js";
106
107
 
108
+ // Provider standing client (platform operators only, read-only)
109
+ export { ProviderStandingClient } from "./provider-standing.js";
110
+
107
111
  // Search client
108
112
  export {
109
113
  SearchClient,
@@ -271,6 +275,9 @@ export {
271
275
  resolveToolKind,
272
276
  resolveToolKindByName,
273
277
  normalizeToolResult,
278
+ SHELL_INTENT_ARG_FIELD,
279
+ extractShellIntent,
280
+ shellIntentFromArgs,
274
281
  type ToolResultView,
275
282
  type ToolSearchMatch,
276
283
  type ToolContentBlock,
@@ -287,6 +294,7 @@ export {
287
294
  } from "./execution/file-review-fold.js";
288
295
  export { toDisplayFileChange } from "./execution/to-display-file-change.js";
289
296
  export { SkillClient, type SkillInput } from "./gen/skill.js";
297
+ export { RoutedSkillClient, MAX_INLINE_ARTIFACT_BYTES } from "./skill.js";
290
298
  export {
291
299
  WorkflowClient,
292
300
  type WorkflowInput,
@@ -0,0 +1,37 @@
1
+ import { createClient, type Client, type Transport } from "@connectrpc/connect";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { ProviderStandingQueryController } from "@stigmer/protos/ai/stigmer/platform/providerstanding/v1/query_pb";
4
+ import {
5
+ GetProviderStandingViewInputSchema,
6
+ type ProviderStandingView,
7
+ } from "@stigmer/protos/ai/stigmer/platform/providerstanding/v1/io_pb";
8
+ import { wrapError } from "./gen/errors.js";
9
+
10
+ /**
11
+ * Client for platform provider standing (platform operators only).
12
+ *
13
+ * Serves the read-only operator view of the platform's LLM provider
14
+ * account health: the latest canary-probe verdict per provider (status,
15
+ * HTTP status, latency, bounded error summary, probe time) recorded by
16
+ * the hourly standing probe. Requires `can_view_provider_standing` on
17
+ * `platform:stigmer`. Cloud-only — the OSS Go server does not implement
18
+ * this controller.
19
+ */
20
+ export class ProviderStandingClient {
21
+ private readonly query: Client<typeof ProviderStandingQueryController>;
22
+
23
+ constructor(transport: Transport) {
24
+ this.query = createClient(ProviderStandingQueryController, transport);
25
+ }
26
+
27
+ /** Retrieve the latest probe verdict for every platform provider. */
28
+ async getStandingView(): Promise<ProviderStandingView> {
29
+ try {
30
+ return await this.query.getProviderStandingView(
31
+ create(GetProviderStandingViewInputSchema, {}),
32
+ );
33
+ } catch (e) {
34
+ throw wrapError(e);
35
+ }
36
+ }
37
+ }