@stigmer/sdk 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/__tests__/errors.test.js +44 -1
  2. package/__tests__/errors.test.js.map +1 -1
  3. package/__tests__/manifest.test.d.ts +2 -0
  4. package/__tests__/manifest.test.d.ts.map +1 -0
  5. package/__tests__/manifest.test.js +229 -0
  6. package/__tests__/manifest.test.js.map +1 -0
  7. package/billing.d.ts +83 -1
  8. package/billing.d.ts.map +1 -1
  9. package/billing.js +91 -1
  10. package/billing.js.map +1 -1
  11. package/cursor-accounts.d.ts +94 -0
  12. package/cursor-accounts.d.ts.map +1 -0
  13. package/cursor-accounts.js +119 -0
  14. package/cursor-accounts.js.map +1 -0
  15. package/errors.d.ts +14 -0
  16. package/errors.d.ts.map +1 -1
  17. package/errors.js +26 -0
  18. package/errors.js.map +1 -1
  19. package/execution/approval-provenance.d.ts.map +1 -1
  20. package/execution/approval-provenance.js +7 -0
  21. package/execution/approval-provenance.js.map +1 -1
  22. package/gen/agent.d.ts +5 -0
  23. package/gen/agent.d.ts.map +1 -1
  24. package/gen/agent.js +9 -1
  25. package/gen/agent.js.map +1 -1
  26. package/gen/agentexecution.d.ts +3 -1
  27. package/gen/agentexecution.d.ts.map +1 -1
  28. package/gen/agentexecution.js +4 -0
  29. package/gen/agentexecution.js.map +1 -1
  30. package/gen/agentinstance.d.ts +1 -0
  31. package/gen/agentinstance.d.ts.map +1 -1
  32. package/gen/agentinstance.js +1 -0
  33. package/gen/agentinstance.js.map +1 -1
  34. package/gen/authorization-config.d.ts.map +1 -1
  35. package/gen/authorization-config.js +1 -0
  36. package/gen/authorization-config.js.map +1 -1
  37. package/gen/client.d.ts +6 -2
  38. package/gen/client.d.ts.map +1 -1
  39. package/gen/client.js +4 -0
  40. package/gen/client.js.map +1 -1
  41. package/gen/datastore.d.ts +128 -0
  42. package/gen/datastore.d.ts.map +1 -0
  43. package/gen/datastore.js +302 -0
  44. package/gen/datastore.js.map +1 -0
  45. package/gen/session.d.ts +1 -0
  46. package/gen/session.d.ts.map +1 -1
  47. package/gen/session.js +1 -0
  48. package/gen/session.js.map +1 -1
  49. package/index.d.ts +5 -3
  50. package/index.d.ts.map +1 -1
  51. package/index.js +7 -3
  52. package/index.js.map +1 -1
  53. package/manifest/client.d.ts +64 -0
  54. package/manifest/client.d.ts.map +1 -0
  55. package/manifest/client.js +109 -0
  56. package/manifest/client.js.map +1 -0
  57. package/manifest/index.d.ts +9 -0
  58. package/manifest/index.d.ts.map +1 -0
  59. package/manifest/index.js +11 -0
  60. package/manifest/index.js.map +1 -0
  61. package/manifest/parse.d.ts +57 -0
  62. package/manifest/parse.d.ts.map +1 -0
  63. package/manifest/parse.js +134 -0
  64. package/manifest/parse.js.map +1 -0
  65. package/manifest/redaction.d.ts +16 -0
  66. package/manifest/redaction.d.ts.map +1 -0
  67. package/manifest/redaction.js +19 -0
  68. package/manifest/redaction.js.map +1 -0
  69. package/manifest/registry.d.ts +44 -0
  70. package/manifest/registry.d.ts.map +1 -0
  71. package/manifest/registry.js +178 -0
  72. package/manifest/registry.js.map +1 -0
  73. package/manifest/serialize.d.ts +24 -0
  74. package/manifest/serialize.d.ts.map +1 -0
  75. package/manifest/serialize.js +78 -0
  76. package/manifest/serialize.js.map +1 -0
  77. package/package.json +4 -3
  78. package/src/__tests__/errors.test.ts +58 -0
  79. package/src/__tests__/manifest.test.ts +271 -0
  80. package/src/billing.ts +160 -0
  81. package/src/cursor-accounts.ts +207 -0
  82. package/src/errors.ts +27 -0
  83. package/src/execution/approval-provenance.ts +7 -0
  84. package/src/gen/agent.ts +15 -1
  85. package/src/gen/agentexecution.ts +5 -1
  86. package/src/gen/agentinstance.ts +2 -0
  87. package/src/gen/authorization-config.ts +1 -0
  88. package/src/gen/client.ts +7 -2
  89. package/src/gen/datastore.ts +379 -0
  90. package/src/gen/session.ts +2 -0
  91. package/src/index.ts +30 -0
  92. package/src/manifest/client.ts +145 -0
  93. package/src/manifest/index.ts +25 -0
  94. package/src/manifest/parse.ts +204 -0
  95. package/src/manifest/redaction.ts +20 -0
  96. package/src/manifest/registry.ts +232 -0
  97. package/src/manifest/serialize.ts +92 -0
  98. package/src/stigmer.ts +8 -0
  99. package/stigmer.d.ts +6 -0
  100. package/stigmer.d.ts.map +1 -1
  101. package/stigmer.js +8 -0
  102. package/stigmer.js.map +1 -1
@@ -0,0 +1,271 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create, equals } from "@bufbuild/protobuf";
3
+ import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
4
+ import { EnvironmentSchema, type Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
5
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
6
+ import { parse as parseYaml } from "yaml";
7
+ import { parseManifest } from "../manifest/parse";
8
+ import { serializeManifest } from "../manifest/serialize";
9
+ import { manifestKinds, manifestHandlerForYamlKind } from "../manifest/registry";
10
+
11
+ /**
12
+ * Manifest engine contract tests.
13
+ *
14
+ * The engine's core promise is *round-trip fidelity against the generated
15
+ * proto schemas*: any resource the server returns serializes to YAML that
16
+ * parses back to the same proto (minus system-managed state), and any
17
+ * repo-authored manifest (snake_case fields, `apiVersion` spelling) parses
18
+ * to a proto the `apply` RPC accepts. Fixtures mirror the real manifests
19
+ * in the whatsapp-doctor-assistant dogfooding repo.
20
+ */
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Fixtures — shaped like the whatsapp-doctor-assistant repo manifests
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const AGENT_YAML = `
27
+ apiVersion: agentic.stigmer.ai/v1
28
+ kind: Agent
29
+ metadata:
30
+ name: clinic-patient-assistant
31
+ org: rakeshreddi098
32
+ spec:
33
+ description: Patient-facing WhatsApp assistant for the clinic.
34
+ instructions: |
35
+ You are the appointment assistant for the clinic.
36
+ Short messages. One question at a time.
37
+ `;
38
+
39
+ const ENVIRONMENT_YAML = `
40
+ apiVersion: agentic.stigmer.ai/v1
41
+ kind: Environment
42
+ metadata:
43
+ name: clinic-patient-db
44
+ org: rakeshreddi098
45
+ spec:
46
+ description: "Clinic records access for the patient assistant"
47
+ data:
48
+ POSTGRES_CONNECTION_URL:
49
+ value: "postgresql://patient_role:secret@host:5432/postgres"
50
+ is_secret: true
51
+ description: "Supabase Postgres connection URL for patient_role"
52
+ `;
53
+
54
+ const AGENT_CHANNEL_YAML = `
55
+ apiVersion: agentic.stigmer.ai/v1
56
+ kind: AgentChannel
57
+ metadata:
58
+ name: clinic-patient-whatsapp
59
+ org: rakeshreddi098
60
+ spec:
61
+ agent_ref:
62
+ kind: agent
63
+ org: rakeshreddi098
64
+ slug: clinic-patient-assistant
65
+ enabled: true
66
+ whatsapp:
67
+ phone_number_id: "1234567890"
68
+ app_ref:
69
+ kind: channel_app
70
+ org: rakeshreddi098
71
+ slug: hosipital
72
+ environment_refs:
73
+ - kind: environment
74
+ org: rakeshreddi098
75
+ slug: clinic-patient-db
76
+ `;
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // parseManifest
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe("parseManifest", () => {
83
+ it("parses a snake_case Agent manifest into a full Agent proto", () => {
84
+ const docs = parseManifest(AGENT_YAML);
85
+ expect(docs).toHaveLength(1);
86
+
87
+ const doc = docs[0];
88
+ expect(doc.handler.yamlKind).toBe("Agent");
89
+ expect(doc.name).toBe("clinic-patient-assistant");
90
+ expect(doc.org).toBe("rakeshreddi098");
91
+
92
+ const agent = doc.message as Agent;
93
+ expect(agent.spec?.description).toBe(
94
+ "Patient-facing WhatsApp assistant for the clinic.",
95
+ );
96
+ expect(agent.spec?.instructions).toContain("appointment assistant");
97
+ });
98
+
99
+ it("parses an AgentChannel manifest with enum-valued resource refs", () => {
100
+ const docs = parseManifest(AGENT_CHANNEL_YAML);
101
+ expect(docs).toHaveLength(1);
102
+ expect(docs[0].handler.yamlKind).toBe("AgentChannel");
103
+ expect(docs[0].slug).toBe("clinic-patient-whatsapp");
104
+ });
105
+
106
+ it("sorts multi-document manifests into dependency apply order", () => {
107
+ // Authored channel-first; the Environment must still apply first.
108
+ const docs = parseManifest(`${AGENT_CHANNEL_YAML}\n---\n${ENVIRONMENT_YAML}`);
109
+ expect(docs.map((d) => d.handler.yamlKind)).toEqual([
110
+ "Environment",
111
+ "AgentChannel",
112
+ ]);
113
+ });
114
+
115
+ it("injects the target org when the document omits it", () => {
116
+ const yaml = AGENT_YAML.replace(" org: rakeshreddi098\n", "");
117
+ const docs = parseManifest(yaml, { org: "acme" });
118
+ expect(docs[0].org).toBe("acme");
119
+ expect(docs[0].warning).toBeUndefined();
120
+ });
121
+
122
+ it("honors the document org and warns when it differs from the target", () => {
123
+ const docs = parseManifest(AGENT_YAML, { org: "acme" });
124
+ expect(docs[0].org).toBe("rakeshreddi098");
125
+ expect(docs[0].warning).toContain('"rakeshreddi098"');
126
+ expect(docs[0].warning).toContain('"acme"');
127
+ });
128
+
129
+ it("rejects unknown fields loudly (strict schema contract)", () => {
130
+ const yaml = AGENT_YAML.replace("spec:", "spec:\n instrctions: typo");
131
+ expect(() => parseManifest(yaml)).toThrow(/Invalid Agent/);
132
+ });
133
+
134
+ it("rejects a document without a kind", () => {
135
+ expect(() => parseManifest("metadata:\n name: x\n")).toThrow(/'kind'/);
136
+ });
137
+
138
+ it("rejects unsupported kinds with the supported list", () => {
139
+ expect(() => parseManifest("kind: Skill\nmetadata:\n name: x\n")).toThrow(
140
+ /Unsupported resource kind "Skill".*Agent/s,
141
+ );
142
+ });
143
+
144
+ it("rejects empty input with actionable guidance", () => {
145
+ expect(() => parseManifest(" \n")).toThrow(/empty/i);
146
+ });
147
+
148
+ it("rejects malformed YAML", () => {
149
+ expect(() => parseManifest("kind: Agent\n bad indent: [")).toThrow(
150
+ /Invalid YAML/,
151
+ );
152
+ });
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // serializeManifest
157
+ // ---------------------------------------------------------------------------
158
+
159
+ describe("serializeManifest", () => {
160
+ const agent = create(AgentSchema, {
161
+ apiVersion: "agentic.stigmer.ai/v1",
162
+ kind: "Agent",
163
+ metadata: {
164
+ id: "agt_01example",
165
+ name: "clinic-patient-assistant",
166
+ slug: "clinic-patient-assistant",
167
+ org: "rakeshreddi098",
168
+ version: { id: "ver_01", message: "server-managed" },
169
+ },
170
+ spec: {
171
+ description: "Patient-facing WhatsApp assistant.",
172
+ instructions: "Short messages.\nOne question at a time.\n",
173
+ },
174
+ status: { defaultInstanceId: "agi_01example" },
175
+ });
176
+
177
+ it("emits the canonical envelope: apiVersion spelling, no status", () => {
178
+ const yaml = serializeManifest(agent);
179
+ const doc = parseYaml(yaml) as Record<string, unknown>;
180
+
181
+ expect(Object.keys(doc)[0]).toBe("apiVersion");
182
+ expect(doc.apiVersion).toBe("agentic.stigmer.ai/v1");
183
+ expect(doc.kind).toBe("Agent");
184
+ expect(doc.status).toBeUndefined();
185
+ expect(yaml).not.toContain("api_version:");
186
+ });
187
+
188
+ it("keeps metadata.id but strips server-managed metadata.version", () => {
189
+ const doc = parseYaml(serializeManifest(agent)) as {
190
+ metadata: Record<string, unknown>;
191
+ };
192
+ expect(doc.metadata.id).toBe("agt_01example");
193
+ expect(doc.metadata.version).toBeUndefined();
194
+ });
195
+
196
+ it("fills apiVersion and kind from the registry when the proto omits them", () => {
197
+ const bare = create(AgentSchema, {
198
+ metadata: { name: "x", org: "acme" },
199
+ });
200
+ const doc = parseYaml(serializeManifest(bare)) as Record<string, unknown>;
201
+ expect(doc.apiVersion).toBe("agentic.stigmer.ai/v1");
202
+ expect(doc.kind).toBe("Agent");
203
+ });
204
+
205
+ it("round-trips through parseManifest to an equal proto", () => {
206
+ const docs = parseManifest(serializeManifest(agent));
207
+ expect(docs).toHaveLength(1);
208
+
209
+ // The round-tripped proto matches the original minus system-managed
210
+ // state (status, metadata.version) — exactly what apply consumes.
211
+ const expected = create(AgentSchema, {
212
+ ...agent,
213
+ status: undefined,
214
+ metadata: { ...agent.metadata!, version: undefined },
215
+ });
216
+ expect(equals(AgentSchema, docs[0].message as Agent, expected)).toBe(true);
217
+ });
218
+
219
+ it("round-trips Environment secret values byte-identically", () => {
220
+ const env = create(EnvironmentSchema, {
221
+ metadata: { name: "clinic-patient-db", org: "rakeshreddi098" },
222
+ spec: {
223
+ data: {
224
+ POSTGRES_CONNECTION_URL: {
225
+ value: "***REDACTED***",
226
+ isSecret: true,
227
+ description: "Supabase Postgres connection URL",
228
+ },
229
+ },
230
+ },
231
+ });
232
+
233
+ const docs = parseManifest(serializeManifest(env));
234
+ const roundTripped = docs[0].message as Environment;
235
+ expect(roundTripped.spec?.data.POSTGRES_CONNECTION_URL?.value).toBe(
236
+ "***REDACTED***",
237
+ );
238
+ expect(roundTripped.spec?.data.POSTGRES_CONNECTION_URL?.isSecret).toBe(true);
239
+ });
240
+
241
+ it("rejects messages of kinds outside the registry", () => {
242
+ // ApiResourceMetadata is a real proto message but not a manifest kind.
243
+ const notAResource = create(ApiResourceMetadataSchema, { name: "x" });
244
+ expect(() => serializeManifest(notAResource)).toThrow(/not.*registry/i);
245
+ });
246
+ });
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Registry invariants
250
+ // ---------------------------------------------------------------------------
251
+
252
+ describe("manifest registry", () => {
253
+ it("every handler resolves by its own YAML kind", () => {
254
+ for (const handler of manifestKinds()) {
255
+ expect(manifestHandlerForYamlKind(handler.yamlKind)).toBe(handler);
256
+ }
257
+ });
258
+
259
+ it("apply order is strictly increasing (no accidental ties)", () => {
260
+ const orders = manifestKinds().map((h) => h.applyOrder);
261
+ for (let i = 1; i < orders.length; i++) {
262
+ expect(orders[i]).toBeGreaterThan(orders[i - 1]);
263
+ }
264
+ });
265
+
266
+ it("covers the kinds the console flows depend on", () => {
267
+ for (const kind of ["Agent", "McpServer", "Workflow", "Environment", "AgentChannel", "ChannelApp"]) {
268
+ expect(manifestHandlerForYamlKind(kind), `missing handler for ${kind}`).toBeDefined();
269
+ }
270
+ });
271
+ });
package/src/billing.ts CHANGED
@@ -12,12 +12,21 @@ import {
12
12
  SetAutoRechargeConfigInputSchema,
13
13
  GetBillingUsageReportInputSchema,
14
14
  GetCustomerModelPricingInputSchema,
15
+ GetModelPricingGovernanceInputSchema,
16
+ DecideModelPricingOverrideInputSchema,
17
+ UpsertModelPricingBaselineInputSchema,
18
+ RetireModelPricingBaselineInputSchema,
19
+ ListModelPricingBaselinesInputSchema,
15
20
  type CreateCreditCheckoutSessionResponse,
16
21
  type CreateBillingPortalSessionResponse,
17
22
  type CreditLedgerResponse,
18
23
  type BillingUsageReportResponse,
19
24
  type CustomerModelPricingResponse,
25
+ type ModelPricingGovernanceResponse,
26
+ type ModelPricingBaselinesResponse,
20
27
  } from "@stigmer/protos/ai/stigmer/billing/v1/io_pb";
28
+ import type { ModelPricingOverride } from "@stigmer/protos/ai/stigmer/billing/v1/pricing_override_pb";
29
+ import type { ModelPricingBaseline } from "@stigmer/protos/ai/stigmer/billing/v1/model_pricing_baseline_pb";
21
30
  import type { BillingAccount, CreditBalance } from "@stigmer/protos/ai/stigmer/billing/v1/billing_account_pb";
22
31
  import type { LedgerEntryType, LedgerView } from "@stigmer/protos/ai/stigmer/billing/v1/enum_pb";
23
32
  import { PageInfoSchema } from "@stigmer/protos/ai/stigmer/commons/rpc/pagination_pb";
@@ -74,6 +83,49 @@ export interface GetCustomerModelPricingParams {
74
83
  readonly orgId?: string;
75
84
  }
76
85
 
86
+ /** Parameters for creating or revising a model registry baseline entry. */
87
+ export interface UpsertModelPricingBaselineParams {
88
+ /**
89
+ * The baseline entry to create or revise, keyed by
90
+ * (modelId, provider, harness). Lifecycle fields (baselineId, status,
91
+ * decision stamps, pricing effectiveAt) are server-owned and ignored.
92
+ */
93
+ readonly baseline: ModelPricingBaseline;
94
+ /** Optional operator note recorded on the revision for the audit trail. */
95
+ readonly revisionNote?: string;
96
+ }
97
+
98
+ /** Parameters for retiring a model from the registry catalog. */
99
+ export interface RetireModelPricingBaselineParams {
100
+ readonly modelId: string;
101
+ readonly provider: string;
102
+ readonly harness: string;
103
+ /** Optional operator note recorded on the retirement. */
104
+ readonly revisionNote?: string;
105
+ }
106
+
107
+ /** Parameters for listing the model registry baseline catalog. */
108
+ export interface ListModelPricingBaselinesParams {
109
+ /**
110
+ * When `true`, includes SUPERSEDED and RETIRED revisions (the full
111
+ * audit history). Default: ACTIVE documents only.
112
+ */
113
+ readonly includeHistory?: boolean;
114
+ }
115
+
116
+ /** Parameters for deciding a pending pricing override. */
117
+ export interface DecideModelPricingOverrideParams {
118
+ /** The PENDING_SIGNOFF override to decide. */
119
+ readonly overrideId: string;
120
+ /**
121
+ * `true` approves (the override becomes ACTIVE and supersedes any
122
+ * current ACTIVE override on the same pricing key); `false` rejects.
123
+ */
124
+ readonly approve: boolean;
125
+ /** Optional note recorded on the decision for the audit trail. */
126
+ readonly decisionNote?: string;
127
+ }
128
+
77
129
  /**
78
130
  * Client for the billing bounded context.
79
131
  *
@@ -270,4 +322,112 @@ export class BillingClient {
270
322
  throw wrapError(e);
271
323
  }
272
324
  }
325
+
326
+ /**
327
+ * Retrieve the platform pricing governance view: baseline vs effective
328
+ * rates per model, ACTIVE override provenance, and pending sign-off
329
+ * proposals from the pricing feedback loop.
330
+ *
331
+ * Platform-operator surface (`can_manage_model_pricing` on
332
+ * `platform:stigmer`): rates are raw provider prices, pre-markup.
333
+ */
334
+ async getModelPricingGovernance(): Promise<ModelPricingGovernanceResponse> {
335
+ try {
336
+ return await this.query.getModelPricingGovernance(
337
+ create(GetModelPricingGovernanceInputSchema, {}),
338
+ );
339
+ } catch (e) {
340
+ throw wrapError(e);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Record a human decision on a PENDING_SIGNOFF pricing override.
346
+ *
347
+ * Approving makes the override ACTIVE (superseding any current ACTIVE
348
+ * override on the same pricing key) and recomposes the effective
349
+ * registry; rejecting archives it for audit. Returns the decided
350
+ * override with the decision stamped.
351
+ */
352
+ async decideModelPricingOverride(
353
+ params: DecideModelPricingOverrideParams,
354
+ ): Promise<ModelPricingOverride> {
355
+ try {
356
+ return await this.command.decideModelPricingOverride(
357
+ create(DecideModelPricingOverrideInputSchema, {
358
+ overrideId: params.overrideId,
359
+ approve: params.approve,
360
+ decisionNote: params.decisionNote ?? "",
361
+ }),
362
+ );
363
+ } catch (e) {
364
+ throw wrapError(e);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Retrieve the model registry baseline catalog: ACTIVE entries by
370
+ * default, or the full append-only revision history with
371
+ * `includeHistory`.
372
+ *
373
+ * Platform-operator surface (`can_manage_model_pricing` on
374
+ * `platform:stigmer`): rates are raw provider prices, pre-markup.
375
+ */
376
+ async listModelPricingBaselines(
377
+ params?: ListModelPricingBaselinesParams,
378
+ ): Promise<ModelPricingBaselinesResponse> {
379
+ try {
380
+ return await this.query.listModelPricingBaselines(
381
+ create(ListModelPricingBaselinesInputSchema, {
382
+ includeHistory: params?.includeHistory ?? false,
383
+ }),
384
+ );
385
+ } catch (e) {
386
+ throw wrapError(e);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Create or revise one model registry baseline entry (catalog + list
392
+ * prices). Append-only: an existing ACTIVE entry for the same
393
+ * (modelId, provider, harness) key is superseded, never mutated, and
394
+ * the effective registry recomposes immediately. Returns the new
395
+ * revision with server-stamped lifecycle fields.
396
+ */
397
+ async upsertModelPricingBaseline(
398
+ params: UpsertModelPricingBaselineParams,
399
+ ): Promise<ModelPricingBaseline> {
400
+ try {
401
+ return await this.command.upsertModelPricingBaseline(
402
+ create(UpsertModelPricingBaselineInputSchema, {
403
+ baseline: params.baseline,
404
+ revisionNote: params.revisionNote ?? "",
405
+ }),
406
+ );
407
+ } catch (e) {
408
+ throw wrapError(e);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Retire one model from the registry catalog. The model disappears
414
+ * from every price surface on the next composition pass; the document
415
+ * is kept for audit and the key can be revived by a subsequent upsert.
416
+ */
417
+ async retireModelPricingBaseline(
418
+ params: RetireModelPricingBaselineParams,
419
+ ): Promise<ModelPricingBaseline> {
420
+ try {
421
+ return await this.command.retireModelPricingBaseline(
422
+ create(RetireModelPricingBaselineInputSchema, {
423
+ modelId: params.modelId,
424
+ provider: params.provider,
425
+ harness: params.harness,
426
+ revisionNote: params.revisionNote ?? "",
427
+ }),
428
+ );
429
+ } catch (e) {
430
+ throw wrapError(e);
431
+ }
432
+ }
273
433
  }
@@ -0,0 +1,207 @@
1
+ import { createClient, type Client, type Transport } from "@connectrpc/connect";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { CursorAccountCommandController } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/command_pb";
4
+ import { CursorAccountQueryController } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/query_pb";
5
+ import {
6
+ UpsertCursorAccountInputSchema,
7
+ DeleteCursorAccountInputSchema,
8
+ AddCursorMemberKeyInputSchema,
9
+ RemoveCursorMemberKeyInputSchema,
10
+ SetCursorMemberKeyEnabledInputSchema,
11
+ SyncCursorAccountInputSchema,
12
+ ListCursorAccountsInputSchema,
13
+ GetCursorAccountViewInputSchema,
14
+ type CursorAccountsResponse,
15
+ type CursorAccountView,
16
+ } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/io_pb";
17
+ import type { CursorAccount } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
18
+ import { wrapError } from "./gen/errors.js";
19
+
20
+ /** Parameters for creating or updating a Cursor account. */
21
+ export interface UpsertCursorAccountParams {
22
+ /**
23
+ * The account to create (empty `accountId`) or update. The admin key is
24
+ * plaintext to set/rotate or `"***REDACTED***"` to keep the stored
25
+ * value; member keys on this message are ignored — use
26
+ * {@link CursorAccountsClient.addMemberKey} and friends.
27
+ */
28
+ readonly account: CursorAccount;
29
+ }
30
+
31
+ /** Parameters for deleting a Cursor account. */
32
+ export interface DeleteCursorAccountParams {
33
+ readonly accountId: string;
34
+ /**
35
+ * Deletion is refused while live sessions are pinned to the account's
36
+ * keys. `force: true` overrides — their Cursor agent handles will be
37
+ * orphaned at disposal.
38
+ */
39
+ readonly force?: boolean;
40
+ }
41
+
42
+ /** Parameters for adding one member key. */
43
+ export interface AddCursorMemberKeyParams {
44
+ readonly accountId: string;
45
+ /**
46
+ * Plaintext user-scoped Cursor API key. Validated live against Cursor's
47
+ * `/v1/me` and bound to its owning team member server-side; admin and
48
+ * service-account keys are rejected with Cursor's own explanation.
49
+ */
50
+ readonly apiKey: string;
51
+ /** Optional operator label (e.g. `"zane — stigmer-prod"`). */
52
+ readonly label?: string;
53
+ }
54
+
55
+ /** Parameters for removing one member key. */
56
+ export interface RemoveCursorMemberKeyParams {
57
+ readonly accountId: string;
58
+ readonly keyId: string;
59
+ /**
60
+ * Removal is refused while live sessions are pinned to this key.
61
+ * `force: true` overrides — prefer disabling instead (always safe).
62
+ */
63
+ readonly force?: boolean;
64
+ }
65
+
66
+ /** Parameters for enabling/disabling one member key. */
67
+ export interface SetCursorMemberKeyEnabledParams {
68
+ readonly accountId: string;
69
+ readonly keyId: string;
70
+ /**
71
+ * `false` blocks NEW sessions immediately; sessions already pinned to
72
+ * the key keep working (conversation continuity + disposal need it).
73
+ */
74
+ readonly enabled: boolean;
75
+ }
76
+
77
+ /**
78
+ * Client for managed Cursor accounts (platform operators only).
79
+ *
80
+ * A Cursor account is one managed Cursor team: its Admin API key (roster
81
+ * and spend, never executions), its member execution keys, and its org
82
+ * assignments. Every method requires `can_manage_cursor_accounts` on
83
+ * `platform:stigmer`; key material is always redacted in responses.
84
+ */
85
+ export class CursorAccountsClient {
86
+ private readonly command: Client<typeof CursorAccountCommandController>;
87
+ private readonly query: Client<typeof CursorAccountQueryController>;
88
+
89
+ constructor(transport: Transport) {
90
+ this.command = createClient(CursorAccountCommandController, transport);
91
+ this.query = createClient(CursorAccountQueryController, transport);
92
+ }
93
+
94
+ /** List all Cursor accounts with routing/sync summaries. */
95
+ async listAccounts(): Promise<CursorAccountsResponse> {
96
+ try {
97
+ return await this.query.listCursorAccounts(
98
+ create(ListCursorAccountsInputSchema, {}),
99
+ );
100
+ } catch (e) {
101
+ throw wrapError(e);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Retrieve one account's detail view: the redacted account, the latest
107
+ * roster/spend snapshot, and the computed key-coverage join.
108
+ */
109
+ async getAccountView(accountId: string): Promise<CursorAccountView> {
110
+ try {
111
+ return await this.query.getCursorAccountView(
112
+ create(GetCursorAccountViewInputSchema, { accountId }),
113
+ );
114
+ } catch (e) {
115
+ throw wrapError(e);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Create or update a Cursor account. The admin key is validated live
121
+ * against Cursor's `/teams/members` before persistence.
122
+ */
123
+ async upsertAccount(params: UpsertCursorAccountParams): Promise<CursorAccount> {
124
+ try {
125
+ return await this.command.upsertCursorAccount(
126
+ create(UpsertCursorAccountInputSchema, { account: params.account }),
127
+ );
128
+ } catch (e) {
129
+ throw wrapError(e);
130
+ }
131
+ }
132
+
133
+ /** Delete a Cursor account (guarded against live session pins). */
134
+ async deleteAccount(params: DeleteCursorAccountParams): Promise<CursorAccount> {
135
+ try {
136
+ return await this.command.deleteCursorAccount(
137
+ create(DeleteCursorAccountInputSchema, {
138
+ accountId: params.accountId,
139
+ force: params.force ?? false,
140
+ }),
141
+ );
142
+ } catch (e) {
143
+ throw wrapError(e);
144
+ }
145
+ }
146
+
147
+ /** Add one execution-capable member key (identity bound via `/v1/me`). */
148
+ async addMemberKey(params: AddCursorMemberKeyParams): Promise<CursorAccount> {
149
+ try {
150
+ return await this.command.addCursorMemberKey(
151
+ create(AddCursorMemberKeyInputSchema, {
152
+ accountId: params.accountId,
153
+ apiKey: params.apiKey,
154
+ label: params.label ?? "",
155
+ }),
156
+ );
157
+ } catch (e) {
158
+ throw wrapError(e);
159
+ }
160
+ }
161
+
162
+ /** Remove one member key (guarded against live session pins). */
163
+ async removeMemberKey(params: RemoveCursorMemberKeyParams): Promise<CursorAccount> {
164
+ try {
165
+ return await this.command.removeCursorMemberKey(
166
+ create(RemoveCursorMemberKeyInputSchema, {
167
+ accountId: params.accountId,
168
+ keyId: params.keyId,
169
+ force: params.force ?? false,
170
+ }),
171
+ );
172
+ } catch (e) {
173
+ throw wrapError(e);
174
+ }
175
+ }
176
+
177
+ /** Enable or disable one member key for new-session selection. */
178
+ async setMemberKeyEnabled(
179
+ params: SetCursorMemberKeyEnabledParams,
180
+ ): Promise<CursorAccount> {
181
+ try {
182
+ return await this.command.setCursorMemberKeyEnabled(
183
+ create(SetCursorMemberKeyEnabledInputSchema, {
184
+ accountId: params.accountId,
185
+ keyId: params.keyId,
186
+ enabled: params.enabled,
187
+ }),
188
+ );
189
+ } catch (e) {
190
+ throw wrapError(e);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Run an on-demand roster + spend sync and return the refreshed view.
196
+ * Rate-limited upstream (Cursor Admin API) — debounce in UIs.
197
+ */
198
+ async syncAccount(accountId: string): Promise<CursorAccountView> {
199
+ try {
200
+ return await this.command.syncCursorAccount(
201
+ create(SyncCursorAccountInputSchema, { accountId }),
202
+ );
203
+ } catch (e) {
204
+ throw wrapError(e);
205
+ }
206
+ }
207
+ }
package/src/errors.ts CHANGED
@@ -303,6 +303,33 @@ export function getErrorReason(error: unknown): ErrorReason | null {
303
303
  return null;
304
304
  }
305
305
 
306
+ /**
307
+ * The `google.rpc.ErrorInfo` domain attached by datastore record-RPC
308
+ * errors in both editions (a cross-edition contract; see the OSS
309
+ * `dserrors` package and its Java mirror).
310
+ */
311
+ const RECORDS_ERROR_DOMAIN = "datastore.stigmer.ai";
312
+
313
+ /**
314
+ * Extract the violated constraint's declared name from a datastore
315
+ * record-RPC error, or `null` when the error carries none.
316
+ *
317
+ * Record writes rejected by a declared constraint (`ALREADY_EXISTS` for
318
+ * uniques, `FAILED_PRECONDITION` for check/exists/not_exists) attach a
319
+ * `google.rpc.ErrorInfo` with reason `CONSTRAINT_VIOLATION` and the
320
+ * constraint's name in `metadata["constraint"]` — the same companion
321
+ * the MCP bridge's record tools surface to agents. Consumers map the
322
+ * name through the datastore spec to place the (verbatim) message next
323
+ * to the fields the constraint covers; `null` falls back to a
324
+ * form-level rendering of {@link getUserMessage}.
325
+ */
326
+ export function getRecordConstraint(error: unknown): string | null {
327
+ const reason = getErrorReason(error);
328
+ if (!reason || reason.domain !== RECORDS_ERROR_DOMAIN) return null;
329
+ const constraint = reason.metadata["constraint"];
330
+ return constraint !== undefined && constraint !== "" ? constraint : null;
331
+ }
332
+
306
333
  /**
307
334
  * Metadata about the RPC call that produced an error. Attached by the
308
335
  * SDK's metadata interceptor and readable via {@link getRpcMetadata}.