@stackbone/cli 0.4.0 → 0.4.1

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/main.js CHANGED
@@ -1427,7 +1427,9 @@ var init_errors$1 = __esmMin((() => {
1427
1427
  "connector_installation_required",
1428
1428
  "principal_required",
1429
1429
  "access_denied",
1430
- "invalid_grant"
1430
+ "invalid_grant",
1431
+ "precondition_failed",
1432
+ "cardinality_mismatch"
1431
1433
  ];
1432
1434
  BROKER_ERROR_CODES = z.enum([...CONNECTOR_EXECUTION_ERROR_CODES, ...BROKER_SPECIFIC_ERROR_CODES]);
1433
1435
  z.object({
@@ -1460,6 +1462,19 @@ var init_errors$1 = __esmMin((() => {
1460
1462
  connector_installation_required: false,
1461
1463
  /** The refresh token is dead. Needs a fresh authorization, not another try. */
1462
1464
  invalid_grant: false,
1465
+ /**
1466
+ * A write carrying a precondition matched no rows: somebody moved the row
1467
+ * between the read and the write, so the write was NOT applied (PLA-1403).
1468
+ *
1469
+ * Terminal, and this one is the whole point rather than a classification
1470
+ * detail. The condition is a fact about the provider's current state, so the
1471
+ * identical call refuses identically; and a retry is precisely the wrong move,
1472
+ * because the value the caller is guarding on is the stale one. What has to
1473
+ * happen next is a fresh read, which only the caller can do.
1474
+ */
1475
+ precondition_failed: false,
1476
+ /** A narrower target is needed; repeating the same write exceeds the same limit. */
1477
+ cardinality_mismatch: false,
1463
1478
  /** Transport, or the provider answering 5xx. */
1464
1479
  execute_failed: true,
1465
1480
  /** The deadline passed. The next attempt may fit inside it. */
@@ -1479,7 +1494,6 @@ var init_errors$1 = __esmMin((() => {
1479
1494
  no_token: true,
1480
1495
  user_authorization_required: true
1481
1496
  };
1482
- Object.entries(BROKER_ERROR_RETRYABILITY).filter(([, retryable]) => !retryable).map(([code]) => code);
1483
1497
  REASON_ALIASES = { app_not_installed: "connector_installation_required" };
1484
1498
  }));
1485
1499
  //#endregion
@@ -1969,7 +1983,19 @@ var init_connect_surface = __esmMin((() => {
1969
1983
  error: z.object({
1970
1984
  /** The broker taxonomy code, e.g. `not_found`, `no_token`, `execute_failed`. */
1971
1985
  code: z.string(),
1972
- message: z.string()
1986
+ message: z.string(),
1987
+ /**
1988
+ * The broker's own retry classification, decided one code at a time and
1989
+ * overridable per site. It has to cross the wire (PLA-1349): the workflow
1990
+ * engine re-runs a failed step unless the error is fatal, and the SDK cannot
1991
+ * mark it fatal from a code alone without keeping a second copy of the
1992
+ * taxonomy that would drift from the broker's.
1993
+ *
1994
+ * Optional because an older box may not send it, and an absent value means
1995
+ * "not classified" rather than "retryable" — the SDK leaves such a failure
1996
+ * retryable, which is the safe direction for a transient outage.
1997
+ */
1998
+ retryable: z.boolean().optional()
1973
1999
  })
1974
2000
  });
1975
2001
  connectEvictResponseSchema = z.object({
@@ -9694,14 +9720,17 @@ var init_connector_invoker = __esmMin((() => {
9694
9720
  * @param connector The connect connector id registered in Studio.
9695
9721
  * @param operation The operation / tool id to run on that connector.
9696
9722
  * @param args The operation arguments (path/query/body params). Defaults `{}`.
9697
- * @param opts Optional `{ principal }` — defaults to `{ type: 'app' }`.
9723
+ * @param opts Optional `{ principal, idempotent }`. `principal` defaults to
9724
+ * `{ type: 'app' }`; `idempotent` is UNSET by default and only
9725
+ * an explicit `false` changes how a failure is retried.
9698
9726
  */
9699
9727
  async function callConnector(connector, operation, args, opts) {
9700
9728
  return (await getConnectorInvoker().invoke({
9701
9729
  connector,
9702
9730
  operation,
9703
9731
  ...args !== void 0 ? { args } : {},
9704
- ...opts?.principal !== void 0 ? { principal: opts.principal } : {}
9732
+ ...opts?.principal !== void 0 ? { principal: opts.principal } : {},
9733
+ ...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {}
9705
9734
  })).output;
9706
9735
  }
9707
9736
  var connectorHandle, connection;
@@ -15221,7 +15250,8 @@ var createTriggerSubscriptionsStore = (opts) => {
15221
15250
  assertOpen();
15222
15251
  await pool.query(`UPDATE stackbone_platform.trigger_deliveries
15223
15252
  SET status = 'rejected', issues = $3::jsonb, updated_at = now()
15224
- WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
15253
+ WHERE subscription_id = $1::uuid AND provider_item_id = $2
15254
+ AND status IN ('claimed', 'starting')`, [
15225
15255
  subscriptionId,
15226
15256
  providerItemId,
15227
15257
  JSON.stringify(issues)
@@ -15669,7 +15699,7 @@ var emulatorCors = (allowlist) => cors({
15669
15699
  var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
15670
15700
  var init_contract = __esmMin((() => {
15671
15701
  init_src$19();
15672
- STACKBONE_CLI_BUILD_VERSION = "0.4.0";
15702
+ STACKBONE_CLI_BUILD_VERSION = "0.4.1";
15673
15703
  EMULATOR_CAPABILITY_ROUTES = {
15674
15704
  "database.postgres_direct": "/api/db",
15675
15705
  "rag.basic": "/api/rag",
@@ -30692,6 +30722,8 @@ var init_types$1 = __esmMin((() => {
30692
30722
  description: z.string().optional(),
30693
30723
  parameters: z.array(operationParameterSchema).optional(),
30694
30724
  requestBodySchema: z.unknown().optional(),
30725
+ /** Original Swagger body parameter name; allows body values alongside query filters. */
30726
+ bodyArgName: z.string().optional(),
30695
30727
  responseSchema: z.unknown().optional(),
30696
30728
  /**
30697
30729
  * This operation's body is BYTES, not JSON. The executor then resolves the
@@ -31615,7 +31647,9 @@ var init_types = __esmMin((() => {
31615
31647
  /** A policy denial on a VALID key: no credential eviction, no retry loop. */
31616
31648
  accessDenied: z.array(z.string()).default([]),
31617
31649
  /** A data conflict on a write (unique / FK violation): terminal, handleable. */
31618
- conflict: z.array(z.string()).default([])
31650
+ conflict: z.array(z.string()).default([]),
31651
+ /** The provider rejected a write that exceeded its requested row limit. */
31652
+ cardinalityMismatch: z.array(z.string()).optional()
31619
31653
  }).optional(),
31620
31654
  /**
31621
31655
  * The query parameter that carries "which columns to return" on this
@@ -33944,7 +33978,8 @@ var init_supabase_connector = __esmMin((() => {
33944
33978
  "42"
33945
33979
  ],
33946
33980
  accessDenied: ["42501"],
33947
- conflict: ["23"]
33981
+ conflict: ["23"],
33982
+ cardinalityMismatch: ["PGRST124"]
33948
33983
  },
33949
33984
  scopes: [],
33950
33985
  triggers: [{
@@ -34465,6 +34500,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
34465
34500
  const parameters = [];
34466
34501
  const seen = /* @__PURE__ */ new Set();
34467
34502
  let bodyParamSchema;
34503
+ let bodyArgName;
34468
34504
  for (let i = rawParameters.length - 1; i >= 0; i--) {
34469
34505
  const raw = derefParameter(rawParameters[i], root);
34470
34506
  if (raw === void 0) {
@@ -34474,7 +34510,10 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
34474
34510
  if (isRecord$2(raw) && source === "swagger") {
34475
34511
  const location = asString$1(raw["in"]);
34476
34512
  if (location === "body") {
34477
- if (bodyParamSchema === void 0 && raw["schema"] !== void 0) bodyParamSchema = raw["schema"];
34513
+ if (bodyParamSchema === void 0 && raw["schema"] !== void 0) {
34514
+ bodyParamSchema = raw["schema"];
34515
+ bodyArgName = asString$1(raw["name"]);
34516
+ }
34478
34517
  continue;
34479
34518
  }
34480
34519
  if (location === "formData") {
@@ -34507,6 +34546,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
34507
34546
  ...description !== void 0 ? { description } : {},
34508
34547
  ...parameters.length > 0 ? { parameters } : {},
34509
34548
  ...requestBodySchema !== void 0 ? { requestBodySchema } : {},
34549
+ ...bodyArgName !== void 0 ? { bodyArgName } : {},
34510
34550
  ...responseSchema !== void 0 ? { responseSchema } : {},
34511
34551
  ...rawBody ? { rawBody } : {},
34512
34552
  ...rawResponse !== void 0 ? { rawResponse } : {},
@@ -36148,17 +36188,18 @@ function narrows(value, narrowingOperators) {
36148
36188
  return !(operator === "is" && NEGATED_IS.has(rest.toLowerCase()));
36149
36189
  }
36150
36190
  /**
36151
- * Should this call be refused as an unfiltered delete?
36191
+ * Should this call be refused as an unfiltered DELETE or PATCH?
36152
36192
  *
36153
36193
  * Returns the refusal — `invalid_args`, not retryable, naming the columns that
36154
- * would narrow it — or `null` when the call may proceed. Applies to DELETE
36155
- * only: a read with no filter is a legitimate "select everything", and an
36156
- * unfiltered PATCH, while sweeping, writes the caller's own values rather than
36157
- * destroying data it never saw.
36194
+ * would narrow it — or `null` when the call may proceed. DELETE and PATCH
36195
+ * both affect existing rows. Reads and inserts keep their existing behavior.
36196
+ * Only declared query arguments count; values inside the named body do not.
36158
36197
  */
36159
36198
  function unfilteredDeleteError(def, operation, args) {
36160
36199
  const guard = def.unfilteredDeleteGuard;
36161
- if (guard === void 0 || operation.method.toUpperCase() !== "DELETE") return null;
36200
+ const method = operation.method.toUpperCase();
36201
+ if (guard === void 0 || method !== "DELETE" && method !== "PATCH") return null;
36202
+ const effect = method === "PATCH" ? "update" : "remove";
36162
36203
  const nonFilter = new Set(guard.nonFilterParams);
36163
36204
  const filterParams = (operation.parameters ?? []).filter((param) => param.in === "query" && !nonFilter.has(param.name)).map((param) => param.name);
36164
36205
  const present = filterParams.filter((name) => {
@@ -36169,9 +36210,9 @@ function unfilteredDeleteError(def, operation, args) {
36169
36210
  if (narrowingOperators === void 0) {
36170
36211
  if (present.length > 0) return null;
36171
36212
  } else if (present.some((name) => narrows(args[name], narrowingOperators))) return null;
36172
- if (narrowingOperators !== void 0 && present.length > 0) return refusal(operation, `no filter it carries narrows which rows go, so it would remove every row in the table.`, `Filters read: ${present.join(", ")}. Narrow it with one of: ${narrowingOperators.join(", ")}.`);
36173
- if (filterParams.length === 0) return refusal(operation, "a delete with no row filter would remove every row in the table.", "The operation declares no filterable parameter, so nothing could narrow it.");
36174
- return refusal(operation, "a delete with no row filter would remove every row in the table.", `Add at least one column filter: ${filterParams.join(", ")}.`);
36213
+ if (narrowingOperators !== void 0 && present.length > 0) return refusal(operation, `no filter it carries narrows which rows go, so it would ${effect} every row in the table.`, `Filters read: ${present.join(", ")}. Narrow it with one of: ${narrowingOperators.join(", ")}.`);
36214
+ if (filterParams.length === 0) return refusal(operation, `a ${method.toLowerCase()} with no row filter would ${effect} every row in the table.`, "The operation declares no filterable parameter, so nothing could narrow it.");
36215
+ return refusal(operation, `a ${method.toLowerCase()} with no row filter would ${effect} every row in the table.`, `Add at least one column filter: ${filterParams.join(", ")}.`);
36175
36216
  }
36176
36217
  /** The one refusal shape, so every path names the operation the same way. */
36177
36218
  function refusal(operation, reason, remedy) {
@@ -36261,6 +36302,7 @@ function classifyProviderError(def, status, body, operationId) {
36261
36302
  if (matched !== null && provider !== null) {
36262
36303
  const said = providerSaid(provider);
36263
36304
  switch (matched) {
36305
+ case "cardinalityMismatch": return makeBrokerError("cardinality_mismatch", `operation "${operationId}" exceeded the provider's requested row limit: ${said}`);
36264
36306
  case "invalidArgs": return makeBrokerError("invalid_args", `operation "${operationId}" was refused by the provider as a bad request: ${said}`);
36265
36307
  case "accessDenied": return makeBrokerError("access_denied", `operation "${operationId}" was denied by the provider's own policy — the credential is valid, the policy said no: ${said}`, false);
36266
36308
  case "conflict": return makeBrokerError("execute_failed", `operation "${operationId}" hit a constraint conflict: ${said}`, false);
@@ -36295,7 +36337,8 @@ function longestMatch(classes, code) {
36295
36337
  const lists = [
36296
36338
  ["invalidArgs", classes.invalidArgs],
36297
36339
  ["accessDenied", classes.accessDenied],
36298
- ["conflict", classes.conflict]
36340
+ ["conflict", classes.conflict],
36341
+ ["cardinalityMismatch", classes.cardinalityMismatch ?? []]
36299
36342
  ];
36300
36343
  for (const [kind, prefixes] of lists) for (const prefix of prefixes) if (code.startsWith(prefix) && (best === null || prefix.length > best.prefix.length)) best = {
36301
36344
  prefix,
@@ -36578,14 +36621,66 @@ async function runOperation(def, operation, args, token, http, timeoutMs, opts =
36578
36621
  headerValue: responseHeader(response, continuation.urlHeader)
36579
36622
  }) : void 0;
36580
36623
  const output = response.ok && wantsRawResponse(operation, args) ? await storeRawResponse(operation, args, response, opts) : await parseResponse(response);
36624
+ const matchedRows = matchedRowTotal(response);
36581
36625
  return {
36582
36626
  status: response.status,
36583
36627
  ok: response.ok,
36584
36628
  output,
36629
+ ...matchedRows !== void 0 ? { matchedRows } : {},
36585
36630
  ...continuationUrl !== void 0 ? { continuationUrl } : {}
36586
36631
  };
36587
36632
  }
36588
36633
  /**
36634
+ * The TOTAL out of a `Content-Range` response header — how many rows the call
36635
+ * matched — or `undefined` when the provider did not count.
36636
+ *
36637
+ * `Content-Range` is `<first>-<last>/<total>`, and either half may be `*`. The
36638
+ * range says which rows came back in THIS response and the total says how many
36639
+ * exist; only the total answers "did my filter hit anything", because a write
36640
+ * that asks for no representation returns no rows either way. Measured against
36641
+ * PostgREST v12.2.3: a matched update answers `0-0/1`, one that matched nothing
36642
+ * answers a star range over a zero total, and one that asked for no count
36643
+ * answers a star over a star (`docs/investigations/pla-1345/README-live.md`).
36644
+ *
36645
+ * A `*` total is dropped rather than read as zero. That distinction is the guard
36646
+ * on the whole feature: see `matchedRows`.
36647
+ */
36648
+ function matchedRowTotal(response) {
36649
+ const total = responseHeader(response, "Content-Range")?.split("/")[1];
36650
+ if (total === void 0 || total === "*") return void 0;
36651
+ const parsed = Number(total);
36652
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
36653
+ }
36654
+ /**
36655
+ * Did a write carrying a precondition match nothing (PLA-1403)?
36656
+ *
36657
+ * Returns the terminal refusal, or `null` when the call may be reported as the
36658
+ * success it looks like. A conditional write sends the value it read as part of
36659
+ * the row filter, so a row somebody else moved matches nothing and is left
36660
+ * alone — the protection working. Without this the caller cannot tell, because
36661
+ * `execute` returns `{ ok: true, output }` and the count never leaves the
36662
+ * broker.
36663
+ *
36664
+ * TWO conditions, and both are load-bearing:
36665
+ *
36666
+ * * The method changes EXISTING rows: `PATCH` and `DELETE`, the same pair
36667
+ * `unfilteredDeleteError` guards. A `GET` that matches nothing is a correct
36668
+ * answer to a question and must stay a success — the one case this must
36669
+ * never break. `POST` is out because an insert matches nothing by
36670
+ * definition, and `PUT` is out because PostgREST upserts on it: a PUT that
36671
+ * matches nothing CREATES the row, so zero matched is not a refusal there.
36672
+ * Unmeasured on our side, which is the second reason to leave it out.
36673
+ * * The provider reported a total of exactly zero, which it only does when the
36674
+ * caller asked for the count. Asking IS the declaration, so a write that did
36675
+ * not ask keeps the behaviour it has always had. An empty body is NOT the
36676
+ * trigger.
36677
+ */
36678
+ function unmatchedWriteError(operation, matchedRows, operationId) {
36679
+ const method = operation.method.toUpperCase();
36680
+ if (matchedRows !== 0 || method !== "PATCH" && method !== "DELETE") return null;
36681
+ return makeBrokerError("precondition_failed", `operation "${operationId}" matched no rows, so it changed nothing — the row moved since it was read. Re-read it and decide.`);
36682
+ }
36683
+ /**
36589
36684
  * Render a parsed operation-call body to a short single-line string for an error
36590
36685
  * message. Shared by the action executor (here) and the inbound-trigger poll
36591
36686
  * context, so the two never drift. `empty` is returned for null/undefined (the
@@ -36599,13 +36694,36 @@ function formatOperationBody(output, opts = {}) {
36599
36694
  return max > 0 && text.length > max ? `${text.slice(0, max)}…` : text;
36600
36695
  }
36601
36696
  /**
36697
+ * Fallback classification after provider-specific body codes (PLA-1321).
36698
+ * Known input, access and resource refusals are terminal. Unclassified statuses
36699
+ * retain the existing unknown-outcome policy, including 409: conflict vocabulary
36700
+ * is outside this slice. The caller's idempotency option still governs that path.
36701
+ */
36702
+ function brokerCodeForProviderStatus(status) {
36703
+ switch (status) {
36704
+ case 400:
36705
+ case 405:
36706
+ case 422: return "invalid_args";
36707
+ case 403: return "access_denied";
36708
+ case 404:
36709
+ case 410: return "not_found";
36710
+ default: return "execute_failed";
36711
+ }
36712
+ }
36713
+ /**
36602
36714
  * Build the JSON request body for an OpenAPI operation. When the operation
36603
- * declares a request body, every payload key that is NOT a declared path/query/
36715
+ * names a Swagger body argument containing an object, send that object directly.
36716
+ * Otherwise, every payload key that is NOT a declared path/query/
36604
36717
  * header param becomes the body. Sets `content-type` when a body is produced.
36605
36718
  * Returns `undefined` for body-less operations (e.g. GET).
36606
36719
  */
36607
36720
  function buildRequestBody(operation, args, headers) {
36608
36721
  if (operation.requestBodySchema === void 0) return void 0;
36722
+ const namedBody = operation.bodyArgName === void 0 ? void 0 : args[operation.bodyArgName];
36723
+ if (namedBody !== null && typeof namedBody === "object") {
36724
+ headers["content-type"] = "application/json";
36725
+ return JSON.stringify(namedBody);
36726
+ }
36609
36727
  const consumed = /* @__PURE__ */ new Set();
36610
36728
  for (const param of operation.parameters ?? []) if (param.in === "path" || param.in === "query" || param.in === "header") consumed.add(param.name);
36611
36729
  for (const arg of brokerConsumedArgs(operation)) consumed.add(arg.name);
@@ -36786,6 +36904,41 @@ function fail(error) {
36786
36904
  error
36787
36905
  };
36788
36906
  }
36907
+ /**
36908
+ * A PROVIDER ROUND-TRIP that failed with an outcome nobody here can determine,
36909
+ * classified against the caller's own idempotency assertion (PLA-1349).
36910
+ *
36911
+ * `execute_failed` and `timeout` are the two retryable codes in
36912
+ * `BROKER_ERROR_RETRYABILITY`, and that default is right: a transport failure or
36913
+ * a gateway 5xx usually did not land. But they are also the only two where it
36914
+ * MIGHT have — the provider committed and the response was lost, or the deadline
36915
+ * fired after the write was accepted and the abort only closed the socket. A
36916
+ * durable step is retried whole, so retrying one of these re-sends the write and
36917
+ * one run creates two objects.
36918
+ *
36919
+ * Which of the two it is cannot be read off the code, the status or the connector
36920
+ * definition: `commentCreate` and `listComments` fail identically, and an
36921
+ * operation the provider deduplicates only deduplicates when the CALLER minted
36922
+ * the key. So the caller states it and this decides accordingly:
36923
+ *
36924
+ * - `false` → `retryable: false`. The SDK turns that into `fatal: true`, the
36925
+ * engine stops the step on its first failure, and a human reconciles. Same
36926
+ * trade the World already made for a dispatch past `httpTimeoutMs`.
36927
+ * - `true`, or NOT STATED → the code's own default, untouched. Passing
36928
+ * `undefined` as the override is byte-identical to omitting the argument, which
36929
+ * is what keeps an unstated option a true no-op.
36930
+ *
36931
+ * ONLY these two codes go through here. A code that was already terminal stays
36932
+ * terminal with no second decision, and a retryable code whose outcome is KNOWN —
36933
+ * a 401 the provider refused before doing anything, a stale-spec refusal, a 429
36934
+ * a rate limiter answered in front of the operation, a 408 for a request the
36935
+ * provider never received whole — keeps its retry, because there is nothing to
36936
+ * reconcile. Callers pass `undefined` for those rather than the assertion; see
36937
+ * the provider non-2xx exit.
36938
+ */
36939
+ function unknownOutcome(code, message, idempotent) {
36940
+ return makeBrokerError(code, message, idempotent === false ? false : void 0);
36941
+ }
36789
36942
  function messageOf$6(err) {
36790
36943
  return err instanceof Error ? err.message : String(err);
36791
36944
  }
@@ -36840,7 +36993,7 @@ var init_connect_action_executor = __esmMin((() => {
36840
36993
  this.resumableStallTimeoutMs = deps.resumableStallTimeoutMs ?? 45e3;
36841
36994
  }
36842
36995
  async execute(input) {
36843
- const { orgId, runId, connectConnectorId, connectOperationId, payload } = input;
36996
+ const { orgId, runId, connectConnectorId, connectOperationId, payload, idempotent } = input;
36844
36997
  const runDef = runId !== void 0 ? await this.deps.runConnectors?.resolve(runId, orgId, connectConnectorId) ?? null : null;
36845
36998
  const record = await this.deps.connectorRows?.resolve(orgId, connectConnectorId) ?? null;
36846
36999
  let def;
@@ -36903,13 +37056,13 @@ var init_connect_action_executor = __esmMin((() => {
36903
37056
  if (followUp === void 0) return this.missingFromSpec(specEntry, cached, `operation "${connectOperationId}" continues as "${declared.operationId}", which is not on connector "${connectConnectorId}"`);
36904
37057
  if (declared.operationId === operation.operationId) return fail(makeBrokerError("invalid_args", `operation "${connectOperationId}" declares itself as its own continuation; a two-phase call needs a second, different operation`));
36905
37058
  }
36906
- return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp);
37059
+ return this.runOpenApi(def, operation, payload, tokenResponse, caller, specEntry, cached, followUp, idempotent);
36907
37060
  }
36908
37061
  const tool = findTool(introspection.tools, connectOperationId);
36909
37062
  if (tool === void 0) return this.missingFromSpec(specEntry, cached, `tool "${connectOperationId}" not found on connector "${connectConnectorId}"`);
36910
- return this.runMcp(def, tool, payload, tokenResponse, caller);
37063
+ return this.runMcp(def, tool, payload, tokenResponse, caller, idempotent);
36911
37064
  }
36912
- async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp) {
37065
+ async runOpenApi(def, operation, payload, token, caller, specEntry, cached, followUp, idempotent) {
36913
37066
  const args = isRecord$1(payload) ? payload : {};
36914
37067
  for (const phase of followUp === void 0 ? [operation] : [operation, followUp]) {
36915
37068
  const refused = unfilteredDeleteError(def, phase, args);
@@ -36933,11 +37086,12 @@ var init_connect_action_executor = __esmMin((() => {
36933
37086
  });
36934
37087
  }
36935
37088
  } catch (err) {
36936
- if (err instanceof ActionTimeoutError) return fail(makeBrokerError("timeout", `operation "${reported.operationId}" timed out`));
36937
- if (isTransferStalled(err)) return fail(makeBrokerError("timeout", `operation "${reported.operationId}" stalled mid-transfer: ${messageOf$6(err)}`));
36938
- if (err instanceof ContinuationError) return fail(makeBrokerError(err.code, err.message, err.retryable));
36939
- if (err instanceof RawBodyError || err instanceof RawResponseError) return fail(makeBrokerError(err.code, err.message));
36940
- return fail(makeBrokerError("execute_failed", messageOf$6(err)));
37089
+ if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" timed out`, idempotent));
37090
+ if (isTransferStalled(err)) return fail(unknownOutcome("timeout", `operation "${reported.operationId}" stalled mid-transfer: ${messageOf$6(err)}`, idempotent));
37091
+ if (err instanceof ContinuationError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : err.retryable));
37092
+ if (err instanceof RawResponseError) return fail(makeBrokerError(err.code, err.message, idempotent === false ? false : void 0));
37093
+ if (err instanceof RawBodyError) return fail(makeBrokerError(err.code, err.message));
37094
+ return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
36941
37095
  }
36942
37096
  const classified = classifyProviderError(def, result.status, result.output, reported.operationId);
36943
37097
  if (classified?.code === "access_denied") return fail(classified);
@@ -36948,8 +37102,13 @@ var init_connect_action_executor = __esmMin((() => {
36948
37102
  const body = formatOperationBody(result.output);
36949
37103
  if (specEntry !== null && cached?.servedFromCache() === true && isStaleSpecError(def, result.output)) return this.onStaleSpec(specEntry, `operation "${reported.operationId}" failed with status ${result.status} against a stale spec${body ? `: ${body}` : ""}`);
36950
37104
  if (classified !== null) return fail(classified);
36951
- return fail(makeBrokerError("execute_failed", `operation "${reported.operationId}" failed with status ${result.status}${body ? `: ${body}` : ""}`));
37105
+ const code = brokerCodeForProviderStatus(result.status);
37106
+ const message = `operation "${reported.operationId}" failed with status ${result.status}${body ? `: ${body}` : ""}`;
37107
+ if (code !== "execute_failed") return fail(makeBrokerError(code, message));
37108
+ return fail(unknownOutcome("execute_failed", message, result.status === 429 || result.status === 408 ? void 0 : idempotent));
36952
37109
  }
37110
+ const unmatched = unmatchedWriteError(operation, result.matchedRows, reported.operationId);
37111
+ if (unmatched !== null) return fail(unmatched);
36953
37112
  return {
36954
37113
  ok: true,
36955
37114
  output: result.output
@@ -36974,7 +37133,7 @@ var init_connect_action_executor = __esmMin((() => {
36974
37133
  ...this.deps.maxRawResponseBytes !== void 0 ? { maxRawResponseBytes: this.deps.maxRawResponseBytes } : {}
36975
37134
  };
36976
37135
  }
36977
- async runMcp(def, tool, payload, token, caller) {
37136
+ async runMcp(def, tool, payload, token, caller, idempotent) {
36978
37137
  if (this.deps.mcp === void 0) return fail(makeBrokerError("execute_failed", `connector "${def.id}" is an MCP connector but no MCP caller was wired`));
36979
37138
  let output;
36980
37139
  try {
@@ -36985,9 +37144,9 @@ var init_connect_action_executor = __esmMin((() => {
36985
37144
  token: token.token
36986
37145
  }), this.timeoutMs);
36987
37146
  } catch (err) {
36988
- if (err instanceof ActionTimeoutError) return fail(makeBrokerError("timeout", `tool "${tool.name}" timed out`));
37147
+ if (err instanceof ActionTimeoutError) return fail(unknownOutcome("timeout", `tool "${tool.name}" timed out`, idempotent));
36989
37148
  if (statusOf(err) === 401) return this.on401(def, caller, `tool "${tool.name}" returned 401`);
36990
- return fail(makeBrokerError("execute_failed", messageOf$6(err)));
37149
+ return fail(unknownOutcome("execute_failed", messageOf$6(err), idempotent));
36991
37150
  }
36992
37151
  return {
36993
37152
  ok: true,
@@ -51785,11 +51944,12 @@ var init_broker_refusal = __esmMin((() => {
51785
51944
  case "access_denied": return 401;
51786
51945
  case "invalid_args": return 400;
51787
51946
  case "invalid_grant": return 400;
51947
+ case "precondition_failed":
51948
+ case "cardinality_mismatch": return 409;
51788
51949
  case "credential_error":
51789
51950
  case "execute_failed":
51790
51951
  case "timeout":
51791
51952
  case "invalid_output": return 502;
51792
- default: return 502;
51793
51953
  }
51794
51954
  };
51795
51955
  brokerRefusal = (err) => {
@@ -53105,7 +53265,8 @@ var init_support = __esmMin((() => {
53105
53265
  args: z.record(z.string(), z.unknown()).default({}),
53106
53266
  principal: principalSchema.default({ type: "app" }),
53107
53267
  installationId: z.string().min(1).optional(),
53108
- runId: z.string().min(1).optional()
53268
+ runId: z.string().min(1).optional(),
53269
+ idempotent: z.boolean().optional()
53109
53270
  });
53110
53271
  requireBrokerSignature = (secret) => async (c, next) => {
53111
53272
  if (!verifyTokenRequestSignature(c, secret)) return c.json({
@@ -53123,13 +53284,15 @@ var init_support = __esmMin((() => {
53123
53284
  ok: false,
53124
53285
  error: {
53125
53286
  code: error.code,
53126
- message: error.message
53287
+ message: error.message,
53288
+ ...error.retryable !== void 0 ? { retryable: error.retryable } : {}
53127
53289
  }
53128
53290
  };
53129
53291
  switch (statusForBrokerCode(error.code)) {
53130
53292
  case 400: return c.json(body, 400);
53131
53293
  case 401: return c.json(body, 401);
53132
53294
  case 404: return c.json(body, 404);
53295
+ case 409: return c.json(body, 409);
53133
53296
  default: return c.json(body, 502);
53134
53297
  }
53135
53298
  };
@@ -53269,13 +53432,14 @@ var createConnectTokenRouter = (opts, sharedContext) => {
53269
53432
  security: HMAC_ROUTE_SECURITY,
53270
53433
  middleware: [requireBrokerSignature(opts.hmacSecret)],
53271
53434
  summary: "Run one connector operation",
53272
- description: "Mints the credential, applies it in the connector’s declared placement, calls the operation and hands back ONLY its output. Nothing about the credential — not the token, not its placement — crosses this boundary, which is the whole reason a workflow step calls the box instead of the provider.\n\n`operation` names an operation id (OpenAPI) or a tool (MCP) as `GET /api/connect/connectors/{connectorId}/operations` reports it. `runId` is the one field that changes WHICH connector answers: a run wired to a stand-in reaches the stand-in and never leaves the box, which is how an eval exercises a send without sending anything.\n\nA refusal comes back in TWO shapes on the same statuses: `{ ok: false, error }` when the executor completed and the provider or the broker said no, and the box’s `{ code, message }` envelope when the request never got that far.",
53435
+ description: "Mints the credential, applies it in the connector’s declared placement, calls the operation and hands back ONLY its output. Nothing about the credential — not the token, not its placement — crosses this boundary, which is the whole reason a workflow step calls the box instead of the provider.\n\n`operation` names an operation id (OpenAPI) or a tool (MCP) as `GET /api/connect/connectors/{connectorId}/operations` reports it. `runId` is the one field that changes WHICH connector answers: a run wired to a stand-in reaches the stand-in and never leaves the box, which is how an eval exercises a send without sending anything.\n\nA refusal comes back in TWO shapes on the same statuses: `{ ok: false, error }` when the executor completed and the provider or the broker said no, including a broker error thrown before execution completes. Signature and payload validation failures use the box’s `{ code, message }` envelope.",
53273
53436
  request: { body: jsonBody(named("ConnectExecuteRequest", executeRequestSchema), "The connector, the operation, and the args to run it with.") },
53274
53437
  responses: {
53275
53438
  200: jsonResponse(named("ConnectExecuteSuccess", connectExecuteSuccessSchema), "The operation’s own output, passed through untouched."),
53276
53439
  400: executeRefusal("The body failed its schema (`invalid_payload`), or the broker refused the arguments (`invalid_args`, `invalid_grant`)."),
53277
53440
  401: executeRefusal("The signature is missing or wrong (`access_denied`), or the connector has no usable authorization (`no_token`, `user_authorization_required`, `principal_required`)."),
53278
53441
  404: executeRefusal("No such connector on this box, or no such operation on it (`not_found`, `connector_installation_required`)."),
53442
+ 409: executeRefusal("The provider refused a guarded write: no rows matched (`precondition_failed`), or the requested row limit was exceeded (`cardinality_mismatch`). Terminal: change the guard or narrow the target before retrying."),
53279
53443
  502: executeRefusal("The provider call failed (`execute_failed`, `timeout`, `credential_error`, `invalid_output`). Upstream, not the box.")
53280
53444
  }
53281
53445
  }), async (c) => {
@@ -53291,7 +53455,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
53291
53455
  connectConnectorId: request.connector,
53292
53456
  connectOperationId: request.operation,
53293
53457
  payload: request.args,
53294
- ...request.runId !== void 0 ? { runId: request.runId } : {}
53458
+ ...request.runId !== void 0 ? { runId: request.runId } : {},
53459
+ ...request.idempotent !== void 0 ? { idempotent: request.idempotent } : {}
53295
53460
  });
53296
53461
  if (result.ok) return c.json({
53297
53462
  ok: true,
@@ -53299,7 +53464,8 @@ var createConnectTokenRouter = (opts, sharedContext) => {
53299
53464
  }, 200);
53300
53465
  return answerExecuteFailure(c, result.error);
53301
53466
  } catch (err) {
53302
- return answerBrokerRefusal(c, err);
53467
+ if (isBrokerError(err)) return answerExecuteFailure(c, err);
53468
+ throw err;
53303
53469
  }
53304
53470
  });
53305
53471
  app.openapi(createRoute({
@@ -65380,6 +65546,30 @@ var init_serial_execution_coordinator = __esmMin((() => {
65380
65546
  createSerialExecutionCoordinator = (gate) => {
65381
65547
  const held = /* @__PURE__ */ new Map();
65382
65548
  return {
65549
+ async beginStart(workflowName) {
65550
+ const owner = held.get(workflowName);
65551
+ if (!owner) return null;
65552
+ const abandon = () => {
65553
+ if (held.get(workflowName) === owner) held.delete(workflowName);
65554
+ };
65555
+ try {
65556
+ if (!await gate.markAttempt(workflowName, owner.token)) {
65557
+ abandon();
65558
+ return null;
65559
+ }
65560
+ } catch (err) {
65561
+ abandon();
65562
+ throw err;
65563
+ }
65564
+ return {
65565
+ noteStarted(runId) {
65566
+ if (held.get(workflowName) === owner) owner.runId = runId;
65567
+ },
65568
+ acknowledge: () => gate.acknowledge(workflowName, owner.token),
65569
+ requeue: (trigger) => gate.requeue(workflowName, trigger, owner.token),
65570
+ abandon
65571
+ };
65572
+ },
65383
65573
  async tryAdmit(workflowName, trigger) {
65384
65574
  const verdict = await gate.tryAdmit(workflowName, trigger);
65385
65575
  if (verdict.admitted) {
@@ -65420,26 +65610,34 @@ var init_serial_execution_coordinator = __esmMin((() => {
65420
65610
  held.delete(workflowName);
65421
65611
  },
65422
65612
  async requeue(workflowName, trigger) {
65423
- await gate.requeue(workflowName, trigger);
65613
+ const owner = held.get(workflowName);
65614
+ if (owner) await gate.requeue(workflowName, trigger, owner.token);
65424
65615
  }
65425
65616
  };
65426
65617
  };
65427
65618
  createSerialSweepSupport = (deps) => {
65428
65619
  const startAdmitted = async (name, trigger) => {
65620
+ const attempt = await deps.coordinator.beginStart(name);
65621
+ if (!attempt) return;
65429
65622
  let started;
65430
65623
  try {
65431
65624
  started = await deps.startNext(name, trigger);
65432
65625
  } catch (err) {
65433
65626
  try {
65434
- await deps.coordinator.requeue(name, trigger);
65627
+ await attempt.requeue(trigger);
65435
65628
  } catch (requeueErr) {
65436
65629
  console.warn(`[serial-execution] could not re-enqueue an unstarted trigger for "${name}": ${requeueErr instanceof Error ? requeueErr.message : String(requeueErr)}`);
65437
65630
  }
65438
- deps.coordinator.abandon(name);
65439
- console.warn(`[serial-execution] failed to start admitted trigger for "${name}", re-enqueued it and released its lock: ${err instanceof Error ? err.message : String(err)}`);
65631
+ attempt.abandon();
65632
+ console.warn(`[serial-execution] failed to start admitted trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
65440
65633
  return;
65441
65634
  }
65442
- deps.coordinator.noteStarted(name, started?.worldRunId);
65635
+ attempt.noteStarted(started?.worldRunId);
65636
+ try {
65637
+ await attempt.acknowledge();
65638
+ } catch (err) {
65639
+ console.warn(`[serial-execution] could not acknowledge started trigger for "${name}": ${err instanceof Error ? err.message : String(err)}`);
65640
+ }
65443
65641
  await deps.clearWaiting?.(name, trigger.pendingId);
65444
65642
  };
65445
65643
  return {
@@ -67254,9 +67452,12 @@ var init_event_forwarder = __esmMin((() => {
67254
67452
  * the row `claimed` while the attempt went ahead; a later swallowed `markStarted`
67255
67453
  * would then leave a `claimed` row with a real run behind it, the sweep would
67256
67454
  * release it, and the customer's workflow would run a second time. Failing here
67257
- * is instead the ordinary transient path the start and resume catches already
67258
- * take: nothing was attempted, so release the claim (best-effort) and bubble —
67259
- * the tick does not advance its cursor and the next one retries the item.
67455
+ * instead bubbles without releasing: a failed marker cannot establish ownership,
67456
+ * and deleting by pair could erase another writer's delivery. The tick holds its
67457
+ * cursor. If the row remains `claimed`, recovery waits for the takeover window
67458
+ * and consumes takeover budget, even for a transient marker failure. An applied
67459
+ * update whose response is lost instead leaves `starting` without an attempt;
67460
+ * this slice cannot resolve that unknown outcome (PLA-1395 investigation).
67260
67461
  *
67261
67462
  * Written at most once: a resume that misses falls through to the start below.
67262
67463
  */
@@ -67265,13 +67466,7 @@ var init_event_forwarder = __esmMin((() => {
67265
67466
  * one must give up where it stands. */
67266
67467
  const markAttempt = async () => {
67267
67468
  if (attemptMarked) return true;
67268
- let matched;
67269
- try {
67270
- matched = await deps.deliveries.markStarting(subscription.id, event.id);
67271
- } catch (err) {
67272
- await bestEffort(() => deps.deliveries.release(subscription.id, event.id), "release");
67273
- throw err;
67274
- }
67469
+ const matched = await deps.deliveries.markStarting(subscription.id, event.id);
67275
67470
  attemptMarked = matched;
67276
67471
  return matched;
67277
67472
  };
@@ -70958,7 +71153,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
70958
71153
  var plain_workflow_ts_default = "import { z } from \"zod\";\n\n/**\n * Workflow \"{{name}}\" — a durable pipeline.\n *\n * Each `\"use step\"` is a durable checkpoint: kill the runtime mid-run and it\n * resumes from the last completed step instead of replaying everything.\n */\n\n// THE contract — sibling exports next to the bare workflow function. The build\n// harvests these (by convention) into the manifest + a live validator. The\n// parameter type is DERIVED from `inputSchema` and the RETURN type from\n// `outputSchema` (both `z.infer`), so neither side can drift from the contract\n// the build harvests. Without the return annotation the schema is published to\n// Studio and the generated types and never compared to what the body returns\n// (PLA-1272).\nexport const inputSchema = z.object({\n message: z.string().describe(\"The input to process\"),\n});\n\nexport const outputSchema = z.object({\n message: z.string(),\n processed: z.boolean(),\n});\n\ntype {{Name}}Input = z.infer<typeof inputSchema>;\n\nexport async function {{export}}(input: {{Name}}Input): Promise<z.infer<typeof outputSchema>> {\n \"use workflow\";\n\n const result = await processMessage(input); // step 1 — durable checkpoint\n return result;\n}\n\nasync function processMessage(input: {{Name}}Input) {\n \"use step\";\n // Replace this with the deterministic work of your step (DB write, HTTP call,\n // an LLM call, …). Returning a value records it as the step's checkpoint.\n return { message: input.message, processed: true };\n}\n";
70959
71154
  //#endregion
70960
71155
  //#region src/services/scaffold/template-loader.ts
70961
- var SDK_TEMPLATE_VERSION = "0.4.0";
71156
+ var SDK_TEMPLATE_VERSION = "0.4.1";
70962
71157
  var PREFIX = "./templates/";
70963
71158
  var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
70964
71159
  "./templates/deep-agents/index.ts.tmpl": index_ts_default,
@@ -78802,6 +78997,7 @@ var generateConnectorTypes = (models) => {
78802
78997
  HEADER,
78803
78998
  "",
78804
78999
  ...AUGMENTED_MODULES.map((mod) => `import ${JSON.stringify(mod)};`),
79000
+ `import type { CallConnectorOptions } from ${JSON.stringify(AUGMENTED_MODULES[0])};`,
78805
79001
  "",
78806
79002
  augmentations.join("\n"),
78807
79003
  ""
@@ -78817,15 +79013,30 @@ function renderConnectorEntry(model) {
78817
79013
  ].join("\n");
78818
79014
  }
78819
79015
  /**
78820
- * One ` <op>(args: <type>): Promise<<out>>;` line (indent level 3) — or the
78821
- * THREE overload lines a `'when-requested'` raw response takes, because one
78822
- * signature there has two reachable outcomes.
79016
+ * One ` <op>(args: <type>, opts?: CallConnectorOptions): Promise<<out>>;` line
79017
+ * (indent level 3) — or the THREE overload lines a `'when-requested'` raw response
79018
+ * takes, because one signature there has two reachable outcomes.
78823
79019
  */
78824
79020
  function renderOperation(op) {
78825
79021
  if (takesEitherOutcome(op)) return renderEitherOutcome(op);
78826
- return ` ${propertyKey(op.name)}(${renderParam(op)}): Promise<${renderReturn(op)}>;`;
79022
+ return ` ${propertyKey(op.name)}(${renderParam(op)}${OPTS_PARAM}): Promise<${renderReturn(op)}>;`;
78827
79023
  }
78828
79024
  /**
79025
+ * The second parameter every generated signature carries (PLA-1349).
79026
+ *
79027
+ * `connection(id).<op>(args, opts)` has always forwarded a second argument at run
79028
+ * time (`call-connector.ts`), and no generated signature declared it — so
79029
+ * `idempotent` was reachable only through the untyped `.call(...)`, which is not
79030
+ * the path a creator is steered to. Typed as the whole options object rather than
79031
+ * one field, because PLA-1351's `echoPath` lands in exactly the same place.
79032
+ *
79033
+ * It is ALWAYS second, and `renderParam` is what guarantees that: an operation
79034
+ * with nothing to put in `args` still prints `args?: undefined` ahead of this. The
79035
+ * proxy is `(args?, opts?)` for every operation, so a signature that let `opts`
79036
+ * come first would hand the caller's options to the provider as arguments.
79037
+ */
79038
+ var OPTS_PARAM = ", opts?: CallConnectorOptions";
79039
+ /**
78829
79040
  * Does ONE signature of this operation have two reachable outcomes?
78830
79041
  *
78831
79042
  * Only `'when-requested'`, and only while the switch is actually IN its arguments
@@ -78865,7 +79076,7 @@ function takesEitherOutcome(op) {
78865
79076
  function renderEitherOutcome(op) {
78866
79077
  const receipt = printType(rawResponseReceiptJsonSchema(), 3);
78867
79078
  const response = op.outputSchema === void 0 ? "unknown" : printType(op.outputSchema, 3);
78868
- const signature = (presence, out) => ` ${propertyKey(op.name)}(args: ${printType(withKeyPresence(op.argsSchema, presence), 3)}): Promise<${out}>;`;
79079
+ const signature = (presence, out) => ` ${propertyKey(op.name)}(args: ${printType(withKeyPresence(op.argsSchema, presence), 3)}${OPTS_PARAM}): Promise<${out}>;`;
78869
79080
  return [
78870
79081
  signature("required", receipt),
78871
79082
  signature("forbidden", response),
@@ -78881,6 +79092,8 @@ function renderEitherOutcome(op) {
78881
79092
  */
78882
79093
  function withKeyPresence(schema, presence) {
78883
79094
  if (!isObject(schema)) return schema;
79095
+ const union = schema["anyOf"] ?? schema["oneOf"];
79096
+ if (Array.isArray(union)) return { anyOf: union.map((branch) => withKeyPresence(branch, presence)) };
78884
79097
  const properties = { ...isObject(schema["properties"]) ? schema["properties"] : {} };
78885
79098
  const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((k) => typeof k === "string") : []);
78886
79099
  if (presence === "required") required.add(RAW_RESPONSE_KEY_ARG);
@@ -78896,7 +79109,7 @@ function withKeyPresence(schema, presence) {
78896
79109
  }
78897
79110
  /** `args: <type>` for an operation with arguments, and nothing for one without. */
78898
79111
  function renderParam(op) {
78899
- return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "";
79112
+ return schemaHasProperties(op.argsSchema) ? `args: ${printType(op.argsSchema, 3)}` : "args?: undefined";
78900
79113
  }
78901
79114
  /**
78902
79115
  * What the operation ACTUALLY answers (PLA-1203).
@@ -79142,8 +79355,9 @@ function answeredResponseSchema(op, byId) {
79142
79355
  * BROKER-consumed arguments into ONE flat args object schema — exactly how the
79143
79356
  * broker executor reads the payload (`args[param.name]` for path/query, the
79144
79357
  * broker's own keys off the raw flags, the rest as the JSON body). A non-object
79145
- * request body (rare) is dropped from the flat shape; the dynamic form still sends
79146
- * it.
79358
+ * request body (rare) is dropped from the flat shape. Swagger's named body is
79359
+ * also offered as an optional argument for writes whose column names overlap
79360
+ * query filters.
79147
79361
  *
79148
79362
  * The three sources are merged in the order the executor reads them, and the
79149
79363
  * broker's come LAST because they are the only ones it will not put on the wire.
@@ -79167,19 +79381,44 @@ function operationArgsSchema(op, trustBodyRequired) {
79167
79381
  Object.assign(properties, bodyProperties);
79168
79382
  if (trustBodyRequired && Array.isArray(body["required"])) required.push(...body["required"].filter((k) => typeof k === "string" && !isServerGenerated(bodyProperties[k])));
79169
79383
  }
79384
+ const bodyRequired = [...required];
79385
+ const sharedRequired = [];
79386
+ const consumedNames = /* @__PURE__ */ new Set();
79387
+ if (op.bodyArgName !== void 0 && isObject(body)) {
79388
+ const namedBody = { ...body };
79389
+ if (!trustBodyRequired) delete namedBody["required"];
79390
+ else if (Array.isArray(body["required"]) && isObject(body["properties"])) {
79391
+ const bodyProperties = body["properties"];
79392
+ namedBody["required"] = body["required"].filter((key) => typeof key === "string" && !isServerGenerated(bodyProperties[key]));
79393
+ }
79394
+ properties[op.bodyArgName] = namedBody;
79395
+ }
79170
79396
  for (const param of op.parameters ?? []) {
79171
79397
  properties[param.name] = param.schema ?? {};
79172
- if (param.required) required.push(param.name);
79398
+ consumedNames.add(param.name);
79399
+ if (param.required) {
79400
+ required.push(param.name);
79401
+ sharedRequired.push(param.name);
79402
+ }
79173
79403
  }
79174
79404
  for (const arg of brokerConsumedArgs(op)) {
79175
79405
  properties[arg.name] = arg.schema;
79176
- if (arg.required) required.push(arg.name);
79406
+ consumedNames.add(arg.name);
79407
+ if (arg.required) {
79408
+ required.push(arg.name);
79409
+ sharedRequired.push(arg.name);
79410
+ }
79177
79411
  }
79178
- return {
79412
+ const flat = {
79179
79413
  type: "object",
79180
79414
  properties,
79181
79415
  ...required.length > 0 ? { required: [...new Set(required)] } : {}
79182
79416
  };
79417
+ if (op.bodyArgName === void 0 || bodyRequired.length === 0 || consumedNames.has(op.bodyArgName)) return flat;
79418
+ return { anyOf: [flat, {
79419
+ ...flat,
79420
+ required: [...new Set([...sharedRequired, op.bodyArgName])]
79421
+ }] };
79183
79422
  }
79184
79423
  /**
79185
79424
  * Whether a body property is one the SERVER produces rather than one the caller
@@ -79202,11 +79441,17 @@ function isServerGenerated(property) {
79202
79441
  }
79203
79442
  /** Whether an object schema declares ONE named property. */
79204
79443
  function schemaHasProperty(schema, name) {
79205
- return isObject(schema) && isObject(schema["properties"]) && name in schema["properties"];
79444
+ if (!isObject(schema)) return false;
79445
+ const union = schema["anyOf"] ?? schema["oneOf"];
79446
+ if (Array.isArray(union)) return union.some((branch) => schemaHasProperty(branch, name));
79447
+ return isObject(schema["properties"]) && name in schema["properties"];
79206
79448
  }
79207
79449
  /** Whether an object schema declares at least one property (else the op takes no args). */
79208
79450
  function schemaHasProperties(schema) {
79209
- return isObject(schema) && isObject(schema["properties"]) && Object.keys(schema["properties"]).length > 0;
79451
+ if (!isObject(schema)) return false;
79452
+ const union = schema["anyOf"] ?? schema["oneOf"];
79453
+ if (Array.isArray(union)) return union.some(schemaHasProperties);
79454
+ return isObject(schema["properties"]) && Object.keys(schema["properties"]).length > 0;
79210
79455
  }
79211
79456
  /**
79212
79457
  * Print a TypeScript type literal for a JSON Schema node. Handles the subset
@@ -81823,6 +82068,64 @@ var createDevCommand = (ctx) => defineCommand({
81823
82068
  })(args)
81824
82069
  });
81825
82070
  //#endregion
82071
+ //#region ../../libs/shared/format/src/lib/coding-agent-prompt.ts
82072
+ /**
82073
+ * The pasteable prompt, and nothing around it, with no "open your coding agent"
82074
+ * preamble. Each surface draws its own instruction: the CLI as a `note`, the
82075
+ * web as the label above a copy button.
82076
+ *
82077
+ * `null` is the empty workspace, which is what the web always passes: the
82078
+ * browser cannot see which piece the reader picked at `init`'s own prompt. That
82079
+ * costs nothing, because `stackbone-coder` detects the workspace on disk by
82080
+ * itself and adds to it rather than re-scaffolding.
82081
+ */
82082
+ function codingAgentPrompt(piece) {
82083
+ if (!piece) return "Use the stackbone-coder skill to build my first piece in this workspace: interview me about what it should do, then wire it up.";
82084
+ return `Use the stackbone-coder skill. The "${piece.name}" ${piece.kind} is already scaffolded in this workspace, so skip the scaffolding: interview me about what it should do, then wire it up.`;
82085
+ }
82086
+ //#endregion
82087
+ //#region ../../libs/shared/format/src/lib/coding-agents.ts
82088
+ var CODING_AGENT_LABELS = [
82089
+ {
82090
+ id: "claude-code",
82091
+ label: "Claude Code"
82092
+ },
82093
+ {
82094
+ id: "cursor",
82095
+ label: "Cursor"
82096
+ },
82097
+ {
82098
+ id: "github-copilot",
82099
+ label: "GitHub Copilot (VS Code)"
82100
+ },
82101
+ {
82102
+ id: "gemini-cli",
82103
+ label: "Gemini CLI"
82104
+ },
82105
+ {
82106
+ id: "windsurf",
82107
+ label: "Windsurf"
82108
+ },
82109
+ {
82110
+ id: "cline",
82111
+ label: "Cline"
82112
+ },
82113
+ {
82114
+ id: "codex",
82115
+ label: "Codex CLI"
82116
+ }
82117
+ ];
82118
+ /**
82119
+ * Throws rather than falling back to the id: a registry row whose label is
82120
+ * missing here is a mistake to fix at build time, not a menu entry reading
82121
+ * `github-copilot`.
82122
+ */
82123
+ var codingAgentLabel = (id) => {
82124
+ const found = CODING_AGENT_LABELS.find((agent) => agent.id === id);
82125
+ if (!found) throw new Error(`no label for coding agent "${id}"`);
82126
+ return found.label;
82127
+ };
82128
+ //#endregion
81826
82129
  //#region src/services/coding-agents/registry.ts
81827
82130
  /**
81828
82131
  * The one place that knows which coding agents Stackbone supports and what
@@ -81831,6 +82134,10 @@ var createDevCommand = (ctx) => defineCommand({
81831
82134
  * that pre-ticks the menu, and the MCP config writer — so adding an agent is a
81832
82135
  * row here and nothing else.
81833
82136
  *
82137
+ * The one thing a row does NOT own is its label: that is in `@stackbone/format`,
82138
+ * because the browser renders the same list and cannot import this file — it
82139
+ * resolves paths against `homedir()` and reads `process.platform`.
82140
+ *
81834
82141
  * The four DIFFERENT MCP entry shapes below are the reason this is data rather
81835
82142
  * than a switch. There is no convention: `mcpServers` vs `servers`, `url` vs
81836
82143
  * `httpUrl` vs `serverUrl`, `http` vs `streamableHttp`, JSON vs TOML. Each was
@@ -81870,7 +82177,7 @@ var CODEX_MCP = {
81870
82177
  var CODING_AGENTS = [
81871
82178
  {
81872
82179
  id: "claude-code",
81873
- label: "Claude Code",
82180
+ label: codingAgentLabel("claude-code"),
81874
82181
  skillsId: "claude-code",
81875
82182
  skillsDir: ".claude",
81876
82183
  detect: [".claude", "~/.claude"],
@@ -81884,7 +82191,7 @@ var CODING_AGENTS = [
81884
82191
  },
81885
82192
  {
81886
82193
  id: "cursor",
81887
- label: "Cursor",
82194
+ label: codingAgentLabel("cursor"),
81888
82195
  skillsId: "cursor",
81889
82196
  skillsDir: ".agents",
81890
82197
  detect: [".cursor", "~/.cursor"],
@@ -81898,7 +82205,7 @@ var CODING_AGENTS = [
81898
82205
  },
81899
82206
  {
81900
82207
  id: "github-copilot",
81901
- label: "GitHub Copilot (VS Code)",
82208
+ label: codingAgentLabel("github-copilot"),
81902
82209
  skillsId: "github-copilot",
81903
82210
  skillsDir: ".agents",
81904
82211
  detect: [".vscode"],
@@ -81912,7 +82219,7 @@ var CODING_AGENTS = [
81912
82219
  },
81913
82220
  {
81914
82221
  id: "gemini-cli",
81915
- label: "Gemini CLI",
82222
+ label: codingAgentLabel("gemini-cli"),
81916
82223
  skillsId: "gemini-cli",
81917
82224
  skillsDir: ".agents",
81918
82225
  detect: [".gemini", "~/.gemini"],
@@ -81926,7 +82233,7 @@ var CODING_AGENTS = [
81926
82233
  },
81927
82234
  {
81928
82235
  id: "windsurf",
81929
- label: "Windsurf",
82236
+ label: codingAgentLabel("windsurf"),
81930
82237
  skillsId: "windsurf",
81931
82238
  skillsDir: ".windsurf",
81932
82239
  detect: [".windsurf", "~/.codeium"],
@@ -81944,7 +82251,7 @@ var CODING_AGENTS = [
81944
82251
  },
81945
82252
  {
81946
82253
  id: "cline",
81947
- label: "Cline",
82254
+ label: codingAgentLabel("cline"),
81948
82255
  skillsId: "cline",
81949
82256
  skillsDir: ".agents",
81950
82257
  detect: [
@@ -81965,7 +82272,7 @@ var CODING_AGENTS = [
81965
82272
  },
81966
82273
  {
81967
82274
  id: "codex",
81968
- label: "Codex CLI",
82275
+ label: codingAgentLabel("codex"),
81969
82276
  skillsId: "codex",
81970
82277
  skillsDir: ".agents",
81971
82278
  detect: [".codex", "~/.codex"],
@@ -82642,22 +82949,6 @@ var createHitlCommand = defineGroup({
82642
82949
  }
82643
82950
  });
82644
82951
  //#endregion
82645
- //#region ../../libs/shared/format/src/lib/coding-agent-prompt.ts
82646
- /**
82647
- * The pasteable prompt, and nothing around it, with no "open your coding agent"
82648
- * preamble. Each surface draws its own instruction: the CLI as a `note`, the
82649
- * web as the label above a copy button.
82650
- *
82651
- * `null` is the empty workspace, which is what the web always passes: the
82652
- * browser cannot see which piece the reader picked at `init`'s own prompt. That
82653
- * costs nothing, because `stackbone-coder` detects the workspace on disk by
82654
- * itself and adds to it rather than re-scaffolding.
82655
- */
82656
- function codingAgentPrompt(piece) {
82657
- if (!piece) return "Use the stackbone-coder skill to build my first piece in this workspace: interview me about what it should do, then wire it up.";
82658
- return `Use the stackbone-coder skill. The "${piece.name}" ${piece.kind} is already scaffolded in this workspace, so skip the scaffolding: interview me about what it should do, then wire it up.`;
82659
- }
82660
- //#endregion
82661
82952
  //#region src/core/npm-exec.ts
82662
82953
  var execFileAsync = promisify(execFile);
82663
82954
  var NEEDS_SHELL = process.platform === "win32";