deepline 0.3.87 → 0.3.89

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 (69) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +532 -2
  2. package/dist/bundling-sources/sdk/src/config.ts +77 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +76 -30
  4. package/dist/bundling-sources/sdk/src/index.ts +6 -0
  5. package/dist/bundling-sources/sdk/src/play.ts +15 -1
  6. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  7. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +83 -4
  9. package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
  10. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +263 -0
  11. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +9 -1
  12. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +6 -11
  14. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +90 -12
  15. package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +78 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/checkpoint-boundaries.ts +70 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +407 -53
  18. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +1 -32
  19. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +57 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +20 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +6 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +7 -1
  23. package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +189 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +28 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +23 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +1 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +18 -4
  29. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +68 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +2 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +1 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +10 -8
  33. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +20 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +6 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +24 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +30 -1
  37. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +6 -3
  38. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +152 -17
  39. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +19 -4
  40. package/dist/bundling-sources/shared_libs/plays/contracts.ts +6 -14
  41. package/dist/bundling-sources/shared_libs/plays/core.ts +11 -6
  42. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +16 -11
  43. package/dist/bundling-sources/shared_libs/plays/resolve-static-pipeline.ts +1 -1
  44. package/dist/bundling-sources/shared_libs/plays/slug.ts +115 -0
  45. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +44 -14
  46. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +877 -0
  47. package/dist/bundling-sources/shared_libs/product-notifications/configuration-draft.ts +456 -0
  48. package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +560 -0
  49. package/dist/bundling-sources/shared_libs/product-notifications/content.ts +28 -0
  50. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +59 -0
  51. package/dist/bundling-sources/shared_libs/product-notifications/email.ts +246 -0
  52. package/dist/bundling-sources/shared_libs/product-notifications/evaluation.ts +280 -0
  53. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +433 -0
  54. package/dist/bundling-sources/shared_libs/product-notifications/legacy-slack.ts +80 -0
  55. package/dist/bundling-sources/shared_libs/product-notifications/workspace.ts +38 -0
  56. package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
  57. package/dist/cli/index.js +1065 -190
  58. package/dist/cli/index.mjs +1052 -177
  59. package/dist/{compiler-manifest-Dbov16F7.d.mts → compiler-manifest-DsLbQaew.d.mts} +132 -8
  60. package/dist/{compiler-manifest-Dbov16F7.d.ts → compiler-manifest-DsLbQaew.d.ts} +132 -8
  61. package/dist/index.d.mts +833 -11
  62. package/dist/index.d.ts +833 -11
  63. package/dist/index.js +467 -38
  64. package/dist/index.mjs +467 -38
  65. package/dist/install-integrity.json +20 -2
  66. package/dist/plays/bundle-play-file.d.mts +8 -3
  67. package/dist/plays/bundle-play-file.d.ts +8 -3
  68. package/dist/plays/bundle-play-file.mjs +100 -20
  69. package/package.json +1 -1
@@ -572,6 +572,7 @@ var HOST_URL_ENV = "DEEPLINE_HOST_URL";
572
572
  var API_KEY_ENV = "DEEPLINE_API_KEY";
573
573
  var ACTIVE_ORG_ID_ENV = "DEEPLINE_ACTIVE_ORG_ID";
574
574
  var ACTIVE_ORG_NAME_ENV = "DEEPLINE_ACTIVE_ORG_NAME";
575
+ var FEATURES_ENV = "DEEPLINE_FEATURES";
575
576
  var PROD_URL = "https://code.deepline.com";
576
577
  var DEFAULT_TIMEOUT = 6e4;
577
578
  var DEFAULT_MAX_RETRIES = 3;
@@ -596,8 +597,40 @@ var CLI_ENV_ALLOWED_KEYS = /* @__PURE__ */ new Set([
596
597
  HOST_URL_ENV,
597
598
  API_KEY_ENV,
598
599
  ACTIVE_ORG_ID_ENV,
599
- ACTIVE_ORG_NAME_ENV
600
+ ACTIVE_ORG_NAME_ENV,
601
+ FEATURES_ENV
600
602
  ]);
603
+ function featureEnvValues(data) {
604
+ if (!Array.isArray(data.features)) return {};
605
+ return {
606
+ [FEATURES_ENV]: data.features.filter(
607
+ (key) => typeof key === "string" && key.trim() !== ""
608
+ ).map((key) => key.trim()).join(",")
609
+ };
610
+ }
611
+ function storedHostApiKey(baseUrl) {
612
+ return loadCliEnv(normalizeBaseUrl(baseUrl) || baseUrl)[API_KEY_ENV] ?? "";
613
+ }
614
+ function storedHostFeatures(baseUrl) {
615
+ return loadCliEnv(normalizeBaseUrl(baseUrl) || baseUrl)[FEATURES_ENV];
616
+ }
617
+ function envHasFeature(env, feature) {
618
+ return (env[FEATURES_ENV] ?? "").split(",").map((key) => key.trim()).includes(feature);
619
+ }
620
+ function cliFeatureEnabled(feature) {
621
+ const processEnv = process.env;
622
+ if (envHasFeature(processEnv, feature)) return true;
623
+ if (processEnv[API_KEY_ENV]?.trim()) return false;
624
+ try {
625
+ const baseUrl = resolveBaseUrlOnly();
626
+ const projectEnv = loadProjectEnvCandidates().find(
627
+ ({ env }) => normalizeBaseUrl(env[HOST_URL_ENV] ?? "") === baseUrl && Boolean(env[API_KEY_ENV])
628
+ );
629
+ return envHasFeature(projectEnv?.env ?? loadCliEnv(baseUrl), feature);
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
601
634
  function baseUrlSlug(baseUrl) {
602
635
  let url;
603
636
  try {
@@ -797,6 +830,10 @@ function mergePersistedEnvValues(existing, values) {
797
830
  if (nextApiKey !== void 0 && nextApiKey !== existing[API_KEY_ENV] && values[ACTIVE_ORG_ID_ENV] === void 0) {
798
831
  delete merged[ACTIVE_ORG_ID_ENV];
799
832
  delete merged[ACTIVE_ORG_NAME_ENV];
833
+ delete merged[FEATURES_ENV];
834
+ }
835
+ if (values[FEATURES_ENV] === "") {
836
+ delete merged[FEATURES_ENV];
800
837
  }
801
838
  return merged;
802
839
  }
@@ -920,7 +957,10 @@ function resolveConfig(options) {
920
957
  apiKey,
921
958
  baseUrl,
922
959
  timeout: options?.timeout ?? DEFAULT_TIMEOUT,
923
- maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES
960
+ maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
961
+ // Pass through untouched: the transport owns case-insensitive merging so
962
+ // there is exactly one place that decides which header wins.
963
+ ...options?.headers ? { headers: { ...options.headers } } : {}
924
964
  };
925
965
  }
926
966
  function mergeProjectEnvFile(filePath, values) {
@@ -1210,7 +1250,7 @@ var SDK_RELEASE = {
1210
1250
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1211
1251
  // getters keep their established compatibility behavior.
1212
1252
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1213
- version: "0.3.87",
1253
+ version: "0.3.89",
1214
1254
  updateSummary: "Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.",
1215
1255
  packageCapabilities: {
1216
1256
  updatePreferences: 1
@@ -1764,11 +1804,33 @@ function normalizeRequestAbortError(error, input2) {
1764
1804
  }
1765
1805
  return normalized;
1766
1806
  }
1807
+ function applyExplicitHeaders(headers, explicit) {
1808
+ if (!explicit) return headers;
1809
+ for (const [rawName, value] of Object.entries(explicit)) {
1810
+ const name = rawName.trim().toLowerCase();
1811
+ if (!name) continue;
1812
+ for (const existing of Object.keys(headers)) {
1813
+ if (existing.toLowerCase() === name) delete headers[existing];
1814
+ }
1815
+ headers[name] = value;
1816
+ }
1817
+ return headers;
1818
+ }
1767
1819
  function isAbortLikeError(error) {
1768
1820
  const name = error.name.toLowerCase();
1769
1821
  const message = error.message.toLowerCase();
1770
1822
  return name === "aborterror" || message.includes("operation was aborted") || message === "aborted";
1771
1823
  }
1824
+ function withErrorCause(error, cause) {
1825
+ if (cause !== null && cause !== void 0) {
1826
+ Object.defineProperty(error, "cause", {
1827
+ value: cause,
1828
+ configurable: true,
1829
+ writable: true
1830
+ });
1831
+ }
1832
+ return error;
1833
+ }
1772
1834
  function mapNetworkError(baseUrl, error, targetLabel2) {
1773
1835
  const tagged = error;
1774
1836
  const timeoutMs = tagged?.[REQUEST_TIMEOUT_MARKER];
@@ -1885,7 +1947,7 @@ var HttpClient = class {
1885
1947
  if (coordinatorInternalToken?.trim() && (coordinatorUrl?.trim() || workerCallbackUrl?.trim() || runtimeTestFault?.trim() || absurdReleaseOverride?.trim() || preferredReceiptGatewayMachineId?.trim())) {
1886
1948
  headers[COORDINATOR_INTERNAL_TOKEN_HEADER] = coordinatorInternalToken.trim();
1887
1949
  }
1888
- return headers;
1950
+ return applyExplicitHeaders(headers, this.config.headers);
1889
1951
  }
1890
1952
  /**
1891
1953
  * Send an HTTP request with automatic retries and error handling.
@@ -2039,32 +2101,38 @@ var HttpClient = class {
2039
2101
  );
2040
2102
  if (options?.toolId) {
2041
2103
  const code = mappedNetworkError.code;
2042
- throw new ToolExecutionError(
2043
- withCoworkNetworkHint(mappedNetworkError.message),
2044
- {
2045
- toolId: options.toolId,
2046
- provider: providerLabelFromToolId(options.toolId),
2047
- operation: options.toolId,
2048
- code,
2049
- origin: "deepline",
2050
- category: "network",
2051
- // A direct client timeout has ambiguous delivery. Fail closed unless
2052
- // the server returns the authoritative action delivery contract.
2053
- retryable: false,
2054
- statusCode: null,
2055
- requestId: null,
2056
- retryAfterMs: null,
2057
- networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
2058
- networkScope: "client_to_deepline",
2059
- details: mappedNetworkError.details
2060
- }
2104
+ throw withErrorCause(
2105
+ new ToolExecutionError(
2106
+ withCoworkNetworkHint(mappedNetworkError.message),
2107
+ {
2108
+ toolId: options.toolId,
2109
+ provider: providerLabelFromToolId(options.toolId),
2110
+ operation: options.toolId,
2111
+ code,
2112
+ origin: "deepline",
2113
+ category: "network",
2114
+ // A direct client timeout has ambiguous delivery. Fail closed unless
2115
+ // the server returns the authoritative action delivery contract.
2116
+ retryable: false,
2117
+ statusCode: null,
2118
+ requestId: null,
2119
+ retryAfterMs: null,
2120
+ networkKind: code === "NETWORK_TIMEOUT" ? "timeout" : code === "NETWORK_ABORTED" ? "unknown" : "unavailable",
2121
+ networkScope: "client_to_deepline",
2122
+ details: mappedNetworkError.details
2123
+ }
2124
+ ),
2125
+ lastError
2061
2126
  );
2062
2127
  }
2063
- throw new DeeplineError(
2064
- withCoworkNetworkHint(mappedNetworkError.message),
2065
- void 0,
2066
- mappedNetworkError.code,
2067
- mappedNetworkError.details
2128
+ throw withErrorCause(
2129
+ new DeeplineError(
2130
+ withCoworkNetworkHint(mappedNetworkError.message),
2131
+ void 0,
2132
+ mappedNetworkError.code,
2133
+ mappedNetworkError.details
2134
+ ),
2135
+ lastError
2068
2136
  );
2069
2137
  }
2070
2138
  /**
@@ -2438,6 +2506,13 @@ var PRODUCT_NOTIFICATION_DELIVERY_LEASE_MS = 2 * 6e4;
2438
2506
  var PRODUCT_NOTIFICATION_SUCCESS_TTL_MS = 15 * 6e4;
2439
2507
  var PRODUCT_NOTIFICATION_FAILURE_TTL_MS = 24 * 60 * 6e4;
2440
2508
 
2509
+ // ../product-notifications/configuration.ts
2510
+ var PRODUCT_NOTIFICATION_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2511
+ function normalizeProductNotificationEmail(value) {
2512
+ const email = typeof value === "string" ? value.trim().toLowerCase() : "";
2513
+ return email && email.length <= 320 && PRODUCT_NOTIFICATION_EMAIL_PATTERN.test(email) ? email : null;
2514
+ }
2515
+
2441
2516
  // src/stream-reconnect.ts
2442
2517
  var STREAM_RECONNECT_BASE_DELAY_MS = 500;
2443
2518
  var STREAM_RECONNECT_MAX_DELAY_MS = 15e3;
@@ -2966,7 +3041,7 @@ function isRecord4(value) {
2966
3041
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2967
3042
  }
2968
3043
  function isPlayActivityObservation(value) {
2969
- if (!isRecord4(value) || value.schemaVersion !== 1 || typeof value.activityId !== "string" || !value.activityId.trim() || typeof value.observedAt !== "number" || !Number.isFinite(value.observedAt) || !isRecord4(value.target) || !isRecord4(value.state)) {
3044
+ if (!isRecord4(value) || value.schemaVersion !== void 0 && value.schemaVersion !== 1 || typeof value.activityId !== "string" || !value.activityId.trim() || typeof value.observedAt !== "number" || !Number.isFinite(value.observedAt) || !isRecord4(value.target) || !isRecord4(value.state)) {
2970
3045
  return false;
2971
3046
  }
2972
3047
  const targetKinds = /* @__PURE__ */ new Set([
@@ -3073,7 +3148,6 @@ function projectPlayRunActivity(input2) {
3073
3148
  if (input2.waitKind === "detached_runner") {
3074
3149
  return projection(
3075
3150
  {
3076
- schemaVersion: 1,
3077
3151
  activityId: "runner:managed",
3078
3152
  target: {
3079
3153
  kind: "runner",
@@ -3089,7 +3163,6 @@ function projectPlayRunActivity(input2) {
3089
3163
  if (input2.waitKind === "sleep") {
3090
3164
  return projection(
3091
3165
  {
3092
- schemaVersion: 1,
3093
3166
  activityId: "run:sleep",
3094
3167
  target: {
3095
3168
  kind: "step",
@@ -3109,7 +3182,6 @@ function projectPlayRunActivity(input2) {
3109
3182
  if (input2.waitKind === "integration_event" || input2.waitKind === "integration_event_batch") {
3110
3183
  return projection(
3111
3184
  {
3112
- schemaVersion: 1,
3113
3185
  activityId: `event:${input2.activeNodeId ?? "run"}`,
3114
3186
  stepId: input2.activeNodeId ?? void 0,
3115
3187
  target: {
@@ -3133,7 +3205,6 @@ function projectPlayRunActivity(input2) {
3133
3205
  if (normalizedStatus === "waiting") {
3134
3206
  return projection(
3135
3207
  {
3136
- schemaVersion: 1,
3137
3208
  activityId: "run:legacy-wait",
3138
3209
  target: {
3139
3210
  kind: "step",
@@ -3160,7 +3231,6 @@ function projectPlayRunActivity(input2) {
3160
3231
  const datasetProgress = datasetStep?.progress ?? null;
3161
3232
  return projection(
3162
3233
  {
3163
- schemaVersion: 1,
3164
3234
  activityId: `dataset:${pendingDataset.datasetId ?? pendingDataset.tableNamespace}`,
3165
3235
  target: {
3166
3236
  kind: "dataset",
@@ -3188,7 +3258,6 @@ function projectPlayRunActivity(input2) {
3188
3258
  const provider = toolId?.split(/[._]/)[0]?.trim() || null;
3189
3259
  return projection(
3190
3260
  {
3191
- schemaVersion: 1,
3192
3261
  activityId: `step:${activeStep.nodeId}`,
3193
3262
  stepId: activeStep.nodeId,
3194
3263
  target: toolId && provider ? {
@@ -3213,7 +3282,6 @@ function projectPlayRunActivity(input2) {
3213
3282
  if (normalizedStatus === "queued") {
3214
3283
  return projection(
3215
3284
  {
3216
- schemaVersion: 1,
3217
3285
  activityId: "capacity:worker",
3218
3286
  target: { kind: "capacity", pool: "worker" },
3219
3287
  state: { kind: "queued", reason: "capacity" },
@@ -3223,7 +3291,6 @@ function projectPlayRunActivity(input2) {
3223
3291
  );
3224
3292
  }
3225
3293
  const fallback = {
3226
- schemaVersion: 1,
3227
3294
  activityId: "run:legacy",
3228
3295
  target: {
3229
3296
  kind: "step",
@@ -4483,6 +4550,42 @@ function providerMetaFromToolResponseRawV2(rawV2, view) {
4483
4550
  }
4484
4551
 
4485
4552
  // src/client.ts
4553
+ var MAX_NOTIFICATION_GROUP_CREATE_TARGETS = 50;
4554
+ function parseNotificationGroupTarget(value) {
4555
+ const normalized = value.trim();
4556
+ if (normalized.toLowerCase().startsWith("email:")) {
4557
+ const email = normalizeProductNotificationEmail(normalized.slice(6));
4558
+ if (!email) {
4559
+ throw new Error(
4560
+ `Invalid email notification target "${value}". Use --to email:name@example.com.`
4561
+ );
4562
+ }
4563
+ return { kind: "email", email };
4564
+ }
4565
+ const slack = /^slack:(.+)$/i.exec(normalized)?.[1]?.trim();
4566
+ if (!slack) {
4567
+ const provider = normalized.split(":", 1)[0] || normalized;
4568
+ throw new Error(
4569
+ `Unsupported notification destination "${provider}". Use email: or slack:.`
4570
+ );
4571
+ }
4572
+ const member = /^member:([UW][A-Z0-9]{8,})$/i.exec(slack);
4573
+ if (member?.[1]) {
4574
+ return { kind: "slack", memberId: member[1].toUpperCase() };
4575
+ }
4576
+ if (slack.toLowerCase().startsWith("member:")) {
4577
+ throw new Error(
4578
+ "Slack member IDs must look like slack:member:U0123456789."
4579
+ );
4580
+ }
4581
+ if (!slack.replace(/^#/, "").trim()) {
4582
+ throw new Error("Use --to slack:#channel.");
4583
+ }
4584
+ return { kind: "slack", channel: slack };
4585
+ }
4586
+ function notificationObjectKey(kind) {
4587
+ return `${kind}.${globalThis.crypto.randomUUID()}`;
4588
+ }
4486
4589
  var TERMINAL_PLAY_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
4487
4590
  var INCLUDE_TOOL_METADATA_HEADER = "x-deepline-include-tool-metadata";
4488
4591
  var EXECUTE_RESPONSE_CONTRACT_HEADER = "x-deepline-execute-response-contract";
@@ -5052,6 +5155,10 @@ var DeeplineClient = class {
5052
5155
  audit: (options2) => this.auditMonitors(options2),
5053
5156
  repair: (options2) => this.repairMonitors(options2),
5054
5157
  health: (options2) => this.readMonitorsHealth(options2),
5158
+ batch: {
5159
+ submit: (options2) => this.submitMonitorBatch(options2),
5160
+ get: (operationId, options2) => this.getMonitorBatch(operationId, options2)
5161
+ },
5055
5162
  fleets: {
5056
5163
  sync: (definitionOrId, options2) => this.syncMonitorFleet(definitionOrId, options2),
5057
5164
  get: (fleetId, options2) => fleetId === void 0 ? this.listMonitorFleets() : this.getMonitorFleet(fleetId, options2),
@@ -5184,6 +5291,16 @@ var DeeplineClient = class {
5184
5291
  (secret) => secret.name === normalized && secret.status === "active" && secret.hasValue
5185
5292
  ) ?? null;
5186
5293
  }
5294
+ /** Check effective scope and active-version health without reading the value.
5295
+ * Without playName, checks the organization-scoped secret only. With a Play
5296
+ * name, an active Play secret takes precedence; a deleted/disabled Play
5297
+ * secret falls back to the organization-scoped secret.
5298
+ */
5299
+ async inspectSecret(name, options = {}) {
5300
+ const params = new URLSearchParams({ name: name.trim().toUpperCase() });
5301
+ if (options.playName !== void 0) params.set("play", options.playName);
5302
+ return this.http.get(`/api/v2/secrets?${params.toString()}`);
5303
+ }
5187
5304
  // ——————————————————————————————————————————————————————————
5188
5305
  // Tools
5189
5306
  // ——————————————————————————————————————————————————————————
@@ -5275,6 +5392,10 @@ var DeeplineClient = class {
5275
5392
  `/api/v2/integrations/${encodeURIComponent(toolId)}/get`,
5276
5393
  {
5277
5394
  method: "GET",
5395
+ // Tool metadata is a read-only catalog lookup. Keep the metadata
5396
+ // header while opting into the same transient edge-response retries as
5397
+ // other GET catalog methods such as searchTools().
5398
+ retryApiErrors: true,
5278
5399
  headers: {
5279
5400
  "x-deepline-tool-meta-only": "1"
5280
5401
  }
@@ -6730,6 +6851,197 @@ var DeeplineClient = class {
6730
6851
  async getNotifications() {
6731
6852
  return this.http.get("/api/v2/notifications");
6732
6853
  }
6854
+ /** Read the group-based product-notification configuration and rule catalog. */
6855
+ async getNotificationConfiguration() {
6856
+ return this.http.get("/api/v2/notifications/configuration");
6857
+ }
6858
+ /** Atomically apply one retry-safe desired configuration change. */
6859
+ async changeNotificationConfiguration(expectedWorkspaceVersion, change) {
6860
+ return this.http.put("/api/v2/notifications/configuration", {
6861
+ expectedWorkspaceVersion,
6862
+ change
6863
+ });
6864
+ }
6865
+ /** Create one notification group and its destinations as a retry-safe unit. */
6866
+ async createNotificationGroup(input2) {
6867
+ const name = input2.name.trim().replace(/\s+/g, " ");
6868
+ if (!name) throw new Error("Notification group name is required.");
6869
+ if (input2.targets.length < 1 || input2.targets.length > MAX_NOTIFICATION_GROUP_CREATE_TARGETS) {
6870
+ throw new Error(
6871
+ `Choose between 1 and ${MAX_NOTIFICATION_GROUP_CREATE_TARGETS} notification targets.`
6872
+ );
6873
+ }
6874
+ const targets = input2.targets.map(parseNotificationGroupTarget);
6875
+ const legacyEvents = input2.legacyEvents ?? [];
6876
+ const legacyTarget = targets.length === 1 && targets[0]?.kind === "slack" && legacyEvents.length > 0 ? targets[0] : null;
6877
+ if (legacyEvents.length > 0 && !legacyTarget) {
6878
+ throw new Error(
6879
+ "--for is supported only by the legacy one-Slack-target form. Create the group first, then add rules in notification configuration."
6880
+ );
6881
+ }
6882
+ if (!legacyTarget && name.toLowerCase() === "default") {
6883
+ throw new Error(
6884
+ "Default is reserved for the workspace default notification group."
6885
+ );
6886
+ }
6887
+ const normalizedTargets = targets.map(
6888
+ (target) => target.kind === "email" ? `email:${target.email}` : "memberId" in target ? `slack:member:${target.memberId}` : `slack:${target.channel}`
6889
+ );
6890
+ if (input2.dryRun) {
6891
+ return {
6892
+ mode: "dry_run",
6893
+ compatibilityMode: legacyTarget !== null,
6894
+ name: legacyTarget ? input2.name : name,
6895
+ targets: normalizedTargets,
6896
+ legacyEvents
6897
+ };
6898
+ }
6899
+ if (legacyTarget) {
6900
+ const notification = await this.createNotification({
6901
+ name: input2.name,
6902
+ provider: "slack",
6903
+ ..."channel" in legacyTarget ? { channel: legacyTarget.channel } : { memberId: legacyTarget.memberId },
6904
+ eventTypes: legacyEvents
6905
+ });
6906
+ return { mode: "legacy", notification };
6907
+ }
6908
+ const current = await this.getNotificationConfiguration();
6909
+ const existingGroup = current.configuration.groups.find(
6910
+ (candidate) => candidate.name.toLowerCase() === name.toLowerCase()
6911
+ );
6912
+ const groupKey = existingGroup?.groupKey ?? notificationObjectKey("group");
6913
+ const group = existingGroup ?? {
6914
+ groupKey,
6915
+ name,
6916
+ enabled: true,
6917
+ deliveryTiming: { mode: "immediate" },
6918
+ source: "workspace",
6919
+ configMode: "custom",
6920
+ version: 0
6921
+ };
6922
+ const destinations = current.configuration.destinations.filter(
6923
+ (destination) => destination.groupKey === groupKey
6924
+ );
6925
+ const emails = [
6926
+ ...new Set(
6927
+ targets.flatMap(
6928
+ (target) => target.kind === "email" ? [target.email] : []
6929
+ )
6930
+ )
6931
+ ].sort();
6932
+ const existingEmails = new Set(
6933
+ destinations.flatMap(
6934
+ (destination) => destination.settings.kind === "email_addresses" ? destination.settings.to : []
6935
+ )
6936
+ );
6937
+ const newEmails = emails.filter((email) => !existingEmails.has(email));
6938
+ if (newEmails.length > 0) {
6939
+ destinations.push({
6940
+ destinationKey: notificationObjectKey("destination"),
6941
+ groupKey,
6942
+ name: "Email recipients",
6943
+ enabled: true,
6944
+ source: "workspace",
6945
+ configMode: "custom",
6946
+ version: 0,
6947
+ status: "active",
6948
+ settings: { kind: "email_addresses", to: newEmails }
6949
+ });
6950
+ }
6951
+ const slackTargets = /* @__PURE__ */ new Map();
6952
+ for (const target of targets) {
6953
+ if (target.kind !== "slack") continue;
6954
+ const targetKey = "memberId" in target ? `member:${target.memberId}` : `channel:${target.channel.replace(/^#/, "").toLowerCase()}`;
6955
+ slackTargets.set(targetKey, target);
6956
+ }
6957
+ const connectionId = current.slackConnection.connectionId;
6958
+ if (slackTargets.size > 0 && !connectionId) {
6959
+ throw new Error(
6960
+ "Slack is not connected to this workspace. Connect it in Dashboard \u2192 Integrations, then retry."
6961
+ );
6962
+ }
6963
+ const resolvedSlackTargets = await Promise.all(
6964
+ [...slackTargets.values()].map(async (target) => {
6965
+ if ("memberId" in target) {
6966
+ return {
6967
+ targetKind: "member",
6968
+ targetId: target.memberId,
6969
+ targetName: target.memberId
6970
+ };
6971
+ }
6972
+ const channel = target.channel;
6973
+ const requested = channel.replace(/^#/, "");
6974
+ const response = await this.listNotificationChannels(channel);
6975
+ const matches = response.channels.filter(
6976
+ (candidate) => candidate.id === requested || candidate.name.toLowerCase() === requested.toLowerCase()
6977
+ );
6978
+ if (matches.length !== 1) {
6979
+ throw new Error(
6980
+ matches.length === 0 ? `Slack channel ${channel} was not found or is not visible to the Deepline app.` : `Slack channel ${channel} is ambiguous. Use its channel ID.`
6981
+ );
6982
+ }
6983
+ return {
6984
+ targetKind: "channel",
6985
+ targetId: matches[0].id,
6986
+ targetName: matches[0].name
6987
+ };
6988
+ })
6989
+ );
6990
+ const uniqueSlackTargets = new Map(
6991
+ resolvedSlackTargets.map((target) => [
6992
+ `${target.targetKind}:${target.targetId}`,
6993
+ target
6994
+ ])
6995
+ );
6996
+ for (const target of uniqueSlackTargets.values()) {
6997
+ if (destinations.some(
6998
+ (destination) => destination.settings.kind === "slack" && destination.settings.targetKind === target.targetKind && destination.settings.targetId === target.targetId
6999
+ )) {
7000
+ continue;
7001
+ }
7002
+ destinations.push({
7003
+ destinationKey: notificationObjectKey("destination"),
7004
+ groupKey,
7005
+ name: `${target.targetKind === "channel" ? "#" : "@"}${target.targetName}`,
7006
+ enabled: true,
7007
+ source: "workspace",
7008
+ configMode: "custom",
7009
+ version: 0,
7010
+ status: "pending_verification",
7011
+ settings: {
7012
+ kind: "slack",
7013
+ ...target,
7014
+ oauthConnectionId: connectionId
7015
+ }
7016
+ });
7017
+ }
7018
+ const draft = {
7019
+ group,
7020
+ destinations,
7021
+ rules: current.configuration.rules.filter(
7022
+ (rule) => rule.groupKey === groupKey
7023
+ )
7024
+ };
7025
+ const result = await this.changeNotificationConfiguration(
7026
+ current.configuration.workspaceVersion,
7027
+ { kind: "save_group", draft }
7028
+ );
7029
+ const storedGroup = result.configuration.groups.find(
7030
+ (candidate) => candidate.groupKey === groupKey
7031
+ );
7032
+ if (!storedGroup) {
7033
+ throw new Error("Notification group creation returned no saved group.");
7034
+ }
7035
+ return {
7036
+ mode: "native",
7037
+ groupKey,
7038
+ targets: normalizedTargets,
7039
+ group: storedGroup,
7040
+ destinations: result.configuration.destinations.filter(
7041
+ (destination) => destination.groupKey === groupKey
7042
+ )
7043
+ };
7044
+ }
6733
7045
  /** List Slack channels available to an already-connected Slack integration. */
6734
7046
  async listNotificationChannels(query) {
6735
7047
  const suffix = query ? `?search=${encodeURIComponent(query)}` : "";
@@ -7274,6 +7586,78 @@ var DeeplineClient = class {
7274
7586
  { method: "GET" }
7275
7587
  );
7276
7588
  }
7589
+ /** Submit durable monitor work; transport retries use the same idempotency key. */
7590
+ async submitMonitorBatch(options) {
7591
+ const idempotencyKey = options.idempotencyKey.trim();
7592
+ if (!idempotencyKey) {
7593
+ throw new DeeplineError(
7594
+ "Monitor batch idempotencyKey is required.",
7595
+ void 0,
7596
+ "INVALID_MONITOR_BATCH_IDEMPOTENCY_KEY"
7597
+ );
7598
+ }
7599
+ if (options.kind === "deploy") {
7600
+ if (!options.manifestNdjson.trim()) {
7601
+ throw new DeeplineError(
7602
+ "Monitor batch deploy requires a non-empty NDJSON manifest.",
7603
+ void 0,
7604
+ "INVALID_MONITOR_BATCH_MANIFEST"
7605
+ );
7606
+ }
7607
+ return this.http.request(
7608
+ "/api/v2/monitors/batch",
7609
+ {
7610
+ method: "POST",
7611
+ body: {
7612
+ kind: "deploy",
7613
+ manifest_ndjson: options.manifestNdjson,
7614
+ dry_run: options.dryRun === true,
7615
+ idempotency_key: idempotencyKey
7616
+ },
7617
+ headers: { "Idempotency-Key": idempotencyKey },
7618
+ maxRetries: 0,
7619
+ exactUrlOnly: true
7620
+ }
7621
+ );
7622
+ }
7623
+ const scope = options.scope.kind === "keys" ? { kind: "keys", monitor_keys: options.scope.monitorKeys } : options.scope.kind === "fleet" ? { kind: "fleet", fleet_id: options.scope.fleetId } : { kind: "target", target: options.scope.target };
7624
+ return this.http.request(
7625
+ "/api/v2/monitors/batch",
7626
+ {
7627
+ method: "POST",
7628
+ body: {
7629
+ kind: options.kind,
7630
+ scope,
7631
+ dry_run: options.dryRun === true,
7632
+ idempotency_key: idempotencyKey,
7633
+ ...options.deactivationReason === void 0 ? {} : { deactivation_reason: options.deactivationReason }
7634
+ },
7635
+ headers: { "Idempotency-Key": idempotencyKey },
7636
+ // Durable submission is itself idempotent, but an ambiguous local retry
7637
+ // must retain the exact key and never turn a CLI attempt into two runs.
7638
+ maxRetries: 0,
7639
+ exactUrlOnly: true
7640
+ }
7641
+ );
7642
+ }
7643
+ /** Get durable batch progress and a bounded retained evidence page. */
7644
+ async getMonitorBatch(operationId, options) {
7645
+ const runId = operationId.trim();
7646
+ if (!runId) {
7647
+ throw new DeeplineError(
7648
+ "Monitor batch operation id is required.",
7649
+ void 0,
7650
+ "INVALID_MONITOR_BATCH_OPERATION_ID"
7651
+ );
7652
+ }
7653
+ const params = new URLSearchParams();
7654
+ if (options?.cursor) params.set("cursor", options.cursor);
7655
+ const suffix = params.size > 0 ? `?${params}` : "";
7656
+ return this.http.request(
7657
+ `/api/v2/monitors/batch/${encodeURIComponent(runId)}${suffix}`,
7658
+ { method: "GET" }
7659
+ );
7660
+ }
7277
7661
  /**
7278
7662
  * The deployable monitor tools catalog. Pass a tool id (positional or
7279
7663
  * `{ tool }`) to describe one tool's full payload/stream contract, or no tool
@@ -8591,7 +8975,9 @@ function saveEnvValues(values, baseUrl, scope) {
8591
8975
  ...values[HOST_URL_ENV] ? { [HOST_URL_ENV]: values[HOST_URL_ENV] } : {},
8592
8976
  ...values[API_KEY_ENV] ? { [API_KEY_ENV]: values[API_KEY_ENV] } : {},
8593
8977
  ...values[ACTIVE_ORG_ID_ENV] ? { [ACTIVE_ORG_ID_ENV]: values[ACTIVE_ORG_ID_ENV] } : {},
8594
- ...Object.prototype.hasOwnProperty.call(values, ACTIVE_ORG_NAME_ENV) ? { [ACTIVE_ORG_NAME_ENV]: values[ACTIVE_ORG_NAME_ENV] } : {}
8978
+ ...Object.prototype.hasOwnProperty.call(values, ACTIVE_ORG_NAME_ENV) ? { [ACTIVE_ORG_NAME_ENV]: values[ACTIVE_ORG_NAME_ENV] } : {},
8979
+ // An explicit empty list must reach the store: it clears a stale line.
8980
+ ...Object.prototype.hasOwnProperty.call(values, FEATURES_ENV) ? { [FEATURES_ENV]: values[FEATURES_ENV] } : {}
8595
8981
  };
8596
8982
  if (scope === "folder") {
8597
8983
  saveProjectDeeplineEnvValues(filtered);
@@ -8605,7 +8991,8 @@ function activeOrgEnvValues(data) {
8605
8991
  const orgName = typeof data.org_name === "string" ? data.org_name.trim() : "";
8606
8992
  return {
8607
8993
  [ACTIVE_ORG_ID_ENV]: orgId,
8608
- [ACTIVE_ORG_NAME_ENV]: orgName
8994
+ [ACTIVE_ORG_NAME_ENV]: orgName,
8995
+ ...featureEnvValues(data)
8609
8996
  };
8610
8997
  }
8611
8998
  async function httpJson(method, url, apiKey, body, timeoutMs = AUTH_HTTP_TIMEOUT_MS) {
@@ -9130,6 +9517,8 @@ async function handleStatus(args) {
9130
9517
  savedApiKeyPath = envFilePath(baseUrl);
9131
9518
  }
9132
9519
  }
9520
+ } else if (resolvedAuthScope === "folder" || storedHostApiKey(baseUrl) === apiKey) {
9521
+ saveEnvValues(featureEnvValues(data), baseUrl, resolvedAuthScope);
9133
9522
  }
9134
9523
  printCommandEnvelope(
9135
9524
  {
@@ -10334,12 +10723,12 @@ async function handleBuyCredits(creditsRaw, options) {
10334
10723
  );
10335
10724
  }
10336
10725
  async function handleTargetPlan(planSku, options) {
10337
- if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "team-v1") {
10726
+ if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "growth-v1" && planSku !== "growth-canary-v1" && planSku !== "team-v1") {
10338
10727
  reportBillingFailure(
10339
10728
  {
10340
10729
  exitCode: 2,
10341
10730
  code: "INVALID_PLAN",
10342
- message: "Plan must be payg-v1, builder-v1, or team-v1."
10731
+ message: "Plan must be payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1."
10343
10732
  },
10344
10733
  options
10345
10734
  );
@@ -10600,7 +10989,7 @@ Examples:
10600
10989
  "Disable automatic recharge without clearing saved amounts."
10601
10990
  ).option("--idempotency-key <key>", "Stable retry key").option("--dry-run", "Print the planned update without applying it").option("--json", "Emit JSON output").action(handleAutoRechargeOff)
10602
10991
  );
10603
- billing.command("change-plan").description("Start or change the target billing plan.").argument("<plan_sku>", "payg-v1, builder-v1, or team-v1").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlan);
10992
+ billing.command("change-plan").description("Start or change the target billing plan.").argument("<plan_sku>", "payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlan);
10604
10993
  billing.command("cancel-plan").description("Cancel a target subscription at period end, or undo it.").option("--undo", "Undo a pending period-end cancellation").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlanCancellation);
10605
10994
  billing.command("portal").description("Open the Stripe-hosted billing recovery portal.").option("--no-open", "Print the URL without opening a browser").option("--json", "Emit JSON output").action(handleTargetPortal);
10606
10995
  billing.command("plans").description("Show published plans and the plan you are on.").addHelpText(
@@ -10894,6 +11283,12 @@ function mergeCanonicalRowsInfo(left, right) {
10894
11283
  const exportUnavailableReason = left.exportUnavailableReason === "shared_table_namespace" || right.exportUnavailableReason === "shared_table_namespace" ? "shared_table_namespace" : left.exportUnavailableReason ?? right.exportUnavailableReason;
10895
11284
  const unavailableSource = left.exportUnavailableReason === exportUnavailableReason ? left : right.exportUnavailableReason === exportUnavailableReason ? right : null;
10896
11285
  const columns = preferred.columns.length > 0 ? preferred.columns : fallback.columns;
11286
+ const recoveredReturnPaths2 = [
11287
+ .../* @__PURE__ */ new Set([
11288
+ ...left.recoveredReturnPaths ?? [],
11289
+ ...right.recoveredReturnPaths ?? []
11290
+ ])
11291
+ ];
10897
11292
  return {
10898
11293
  ...fallback,
10899
11294
  ...preferred,
@@ -10906,6 +11301,7 @@ function mergeCanonicalRowsInfo(left, right) {
10906
11301
  datasetId: preferred.datasetId ?? fallback.datasetId,
10907
11302
  tableNamespace: preferred.tableNamespace ?? fallback.tableNamespace,
10908
11303
  ...left.recovered || right.recovered ? { recovered: true } : {},
11304
+ ...recoveredReturnPaths2.length ? { recoveredReturnPaths: recoveredReturnPaths2 } : {},
10909
11305
  ...exportUnavailableReason ? {
10910
11306
  exportUnavailableReason,
10911
11307
  ...unavailableSource?.exportUnavailableMessage ? {
@@ -10966,6 +11362,16 @@ function isSerializedDataset(value) {
10966
11362
  function isPackagedDatasetOutput(value) {
10967
11363
  return isRecord8(value) && value.kind === "dataset" && isRecord8(value.preview) && Array.isArray(value.preview.rows);
10968
11364
  }
11365
+ function recoveredReturnPaths(value) {
11366
+ if (!Array.isArray(value)) return [];
11367
+ return [
11368
+ ...new Set(
11369
+ value.filter(
11370
+ (path) => typeof path === "string" && /^result\.[A-Za-z_$][A-Za-z0-9_$]*$/.test(path)
11371
+ ).slice(0, 200)
11372
+ )
11373
+ ];
11374
+ }
10969
11375
  function pathParts(path) {
10970
11376
  return path.split(".").map((part) => part.trim()).filter(Boolean);
10971
11377
  }
@@ -11062,6 +11468,11 @@ function canonicalRowsInfoFromCandidate(input2) {
11062
11468
  datasetId: typeof candidate.value.datasetId === "string" ? candidate.value.datasetId : null,
11063
11469
  tableNamespace: typeof candidate.value.tableNamespace === "string" ? candidate.value.tableNamespace : null,
11064
11470
  ...candidate.value.recovered === true ? { recovered: true } : {},
11471
+ ...recoveredReturnPaths(candidate.value.recoveredReturnPaths).length ? {
11472
+ recoveredReturnPaths: recoveredReturnPaths(
11473
+ candidate.value.recoveredReturnPaths
11474
+ )
11475
+ } : {},
11065
11476
  ...isRecord8(candidate.value.exportUnavailable) && (candidate.value.exportUnavailable.reason === "empty_dataset" || candidate.value.exportUnavailable.reason === "shared_table_namespace") ? {
11066
11477
  exportUnavailableReason: candidate.value.exportUnavailable.reason,
11067
11478
  ...typeof candidate.value.exportUnavailable.message === "string" ? {
@@ -15217,6 +15628,82 @@ var PLAY_AUTHORING_FIELD_REGISTRY = {
15217
15628
  description: "Unsupported legacy child-workflow timeout.",
15218
15629
  errorMessage: "ctx.runPlay timeoutMs is unsupported because child Plays execute inline."
15219
15630
  },
15631
+ "ctx.runPlayAsync.key": {
15632
+ schema: Type.String({ minLength: 1 }),
15633
+ fixtures: {
15634
+ valid: "request-approval",
15635
+ invalid: "",
15636
+ absent: void 0,
15637
+ unresolved: { expression: "callKey" },
15638
+ edition1: "request-approval"
15639
+ },
15640
+ referenceType: "string",
15641
+ required: true,
15642
+ resolution: "runtime-dynamic",
15643
+ issueCode: "play_authoring_run_play_option_invalid",
15644
+ description: "Stable identity for one independent child Play Run.",
15645
+ errorMessage: "ctx.runPlayAsync key must be a non-empty string."
15646
+ },
15647
+ "ctx.runPlayAsync.playRef": {
15648
+ schema: Type.Union([
15649
+ Type.String({ minLength: 1 }),
15650
+ Type.Union([
15651
+ Type.Object(
15652
+ { playName: Type.String({ minLength: 1 }) },
15653
+ { additionalProperties: true }
15654
+ ),
15655
+ Type.Object(
15656
+ { name: Type.String({ minLength: 1 }) },
15657
+ { additionalProperties: true }
15658
+ )
15659
+ ])
15660
+ ]),
15661
+ fixtures: {
15662
+ valid: "request-approval",
15663
+ invalid: {},
15664
+ absent: void 0,
15665
+ unresolved: { expression: "playRef" },
15666
+ edition1: "request-approval"
15667
+ },
15668
+ referenceType: "string | PlayReferenceLike",
15669
+ required: true,
15670
+ resolution: "runtime-dynamic",
15671
+ issueCode: "play_authoring_run_play_option_invalid",
15672
+ description: "Independently published scalar child Play name or strongly typed definition handle.",
15673
+ errorMessage: "ctx.runPlayAsync playRef must be a non-empty Play name or definition handle."
15674
+ },
15675
+ "ctx.runPlayAsync.input": {
15676
+ schema: Type.Record(Type.String(), Type.Unknown()),
15677
+ fixtures: {
15678
+ valid: { draftId: "draft-1" },
15679
+ invalid: "draft-1",
15680
+ absent: void 0,
15681
+ unresolved: "childInput",
15682
+ edition1: {}
15683
+ },
15684
+ referenceType: "Record<string, unknown>",
15685
+ required: true,
15686
+ resolution: "runtime-dynamic",
15687
+ issueCode: "play_authoring_run_play_option_invalid",
15688
+ description: "Scalar input object submitted to the independent child Run.",
15689
+ errorMessage: "ctx.runPlayAsync input must be an object."
15690
+ },
15691
+ "ctx.runPlayAsync.options.description": {
15692
+ schema: Type.String({ minLength: 1 }),
15693
+ fixtures: {
15694
+ valid: "Request approval.",
15695
+ invalid: "",
15696
+ absent: void 0,
15697
+ unresolved: { expression: "description" },
15698
+ edition1: "Request approval."
15699
+ },
15700
+ referenceType: "string",
15701
+ required: true,
15702
+ resolution: "runtime-dynamic",
15703
+ issueCode: "play_authoring_run_play_option_invalid",
15704
+ description: "Non-empty purpose for one independent child Play Run.",
15705
+ errorMessage: "ctx.runPlayAsync options.description must be non-empty."
15706
+ },
15220
15707
  "runtime.timeout": {
15221
15708
  schema: Type.String({ pattern: "^\\d+\\s*[mh]$" }),
15222
15709
  fixtures: {
@@ -15504,6 +15991,10 @@ var PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
15504
15991
  `export type CsvOptions = { description?: ${cloudReferenceType("ctx.csv.options.description")}; columns?: ${cloudReferenceType("ctx.csv.options.columns")}; rename?: ${cloudReferenceType("ctx.csv.options.rename")}; required?: ${cloudReferenceType("ctx.csv.options.required")} };`,
15505
15992
  "export type PlayCallExecution = 'inline';",
15506
15993
  `export type PlayCallOptions = { description: ${cloudReferenceType("ctx.runPlay.options.description")}; execution?: ${cloudReferenceType("ctx.runPlay.options.execution")}; timeoutMs?: ${cloudReferenceType("ctx.runPlay.options.timeoutMs")} };`,
15994
+ `export type PlayAsyncCallOptions = { description: ${cloudReferenceType("ctx.runPlayAsync.options.description")} };`,
15995
+ "declare const PLAY_RUN_ID_BRAND: unique symbol;",
15996
+ "export type PlayRunId = string & { readonly [PLAY_RUN_ID_BRAND]: 'PlayRunId' };",
15997
+ "export type AsyncPlayRunRef = Readonly<{ runId: PlayRunId }>;",
15507
15998
  `export type RuntimeStepOptions = { semanticKey?: ${cloudReferenceType("ctx.step.semanticKey")}; staleAfterSeconds?: ${cloudReferenceType("ctx.step.staleAfterSeconds")} };`,
15508
15999
  `export type FetchOptions = { staleAfterSeconds?: ${cloudReferenceType("ctx.fetch.staleAfterSeconds")}; transient?: boolean };`,
15509
16000
  "export type PlayFetchResponse = { ok: boolean; status: number; statusText: string; url: string; headers: Record<string, string>; bodyText: string; json: unknown | null };",
@@ -15529,11 +16020,11 @@ var PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
15529
16020
  " withColumn<Name extends string, Value>(name: Name, definition: DatasetColumnDefinition<OutputRow, Value> & { readonly runIf: (row: OutputRow, index: number) => boolean | Promise<boolean> }): DatasetBuilder<InputRow, OutputRow & Record<Name, Value | null>>;",
15530
16021
  " withColumn<Name extends string, Value>(name: Name, definition: DatasetColumnDefinition<OutputRow, Value>): DatasetBuilder<InputRow, OutputRow & Record<Name, Value>>;",
15531
16022
  " withColumn<Name extends string, Value>(name: Name, resolver: StepResolver<OutputRow, Value> | RunnableColumnStepProgram<Value>, options: StepOptions<OutputRow, Value>): DatasetBuilder<InputRow, OutputRow & Record<Name, Value | null>>;",
15532
- " withColumns<Program extends StepProgram<OutputRow, object, unknown>>(program: Program): DatasetBuilder<InputRow, StepProgramOutput<Program>>;",
16023
+ " withColumns<ProgramOutput extends OutputRow, Return>(program: StepProgram<OutputRow, ProgramOutput, Return>): DatasetBuilder<InputRow, ProgramOutput>;",
15533
16024
  " step<Name extends string, Value>(name: Name, resolver: ColumnResolver<OutputRow, Value>): never;",
15534
16025
  " run(options?: DatasetRunOptions<InputRow>): Promise<PlayDataset<OutputRow>>;",
15535
16026
  "};",
15536
- "export type PlayReferenceLike = { readonly playName: string; readonly name?: string } | { readonly name: string; readonly playName?: string };",
16027
+ "export type PlayReferenceLike<TInput extends Record<string, unknown> = Record<string, unknown>> = ({ readonly playName: string; readonly name?: string } | { readonly name: string; readonly playName?: string }) & { readonly __inputType?: TInput };",
15537
16028
  "export interface DeeplinePlayRuntimeContext {",
15538
16029
  " csv<T = Record<string, unknown>>(path: string | CsvInput<T & object>, options?: CsvOptions): Promise<PlayDataset<T>>;",
15539
16030
  " dataset<TSource extends PlayDatasetInput<object>>(key: string, items: TSource): DatasetBuilder<PlayDatasetRow<TSource> & object, PlayDatasetRow<TSource> & object>;",
@@ -15547,6 +16038,8 @@ var PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
15547
16038
  " fetch(key: string, url: string | URL, init?: RequestInit & { headers?: HeadersInit & { 'Idempotency-Key'?: string; 'X-Idempotency-Key'?: string }; auth?: SecretAuthInput }, options?: FetchOptions): Promise<PlayFetchResponse>;",
15548
16039
  " secrets: { get(name: string): SecretPromise; bearer(secret: string | SecretPromise | SecretHandle): SecretAuth; header(header: string, secret: string | SecretPromise | SecretHandle): SecretAuth };",
15549
16040
  ` runPlay<TOutput = unknown>(key: string, playRef: ${cloudReferenceType("ctx.runPlay.playRef")}, input: ${cloudReferenceType("ctx.runPlay.input")}, options: PlayCallOptions): Promise<TOutput>;`,
16041
+ " runPlayAsync<TInput extends Record<string, unknown>>(key: string, playRef: PlayReferenceLike<TInput>, input: NoInfer<TInput>, options: PlayAsyncCallOptions): Promise<AsyncPlayRunRef>;",
16042
+ " runPlayAsync(key: string, playRef: string, input: Record<string, unknown>, options: PlayAsyncCallOptions): Promise<AsyncPlayRunRef>;",
15550
16043
  " log(message: string, options?: { level?: 'debug' | 'info' | 'warn' | 'error' }): void;",
15551
16044
  ` sleep(ms: ${cloudReferenceType("ctx.sleep.ms")}): Promise<void>;`,
15552
16045
  "}",
@@ -15821,7 +16314,7 @@ function collectInlineSecretFindings(sourceCode) {
15821
16314
  var MAX_PLAY_BUNDLE_BYTES = 30 * 1024 * 1024;
15822
16315
 
15823
16316
  // ../plays/bundling/index.ts
15824
- var PLAY_BUNDLE_CACHE_VERSION = 36;
16317
+ var PLAY_BUNDLE_CACHE_VERSION = 37;
15825
16318
  var PLAY_ARTIFACT_CACHE_DIR = join7(
15826
16319
  tmpdir2(),
15827
16320
  `deepline-play-artifacts-v${PLAY_BUNDLE_CACHE_VERSION}`
@@ -16560,7 +17053,7 @@ var RunDataset = class _RunDataset {
16560
17053
  return this.#info.exportUnavailableMessage;
16561
17054
  }
16562
17055
  matches(path) {
16563
- return this.source === path || this.datasetId === path || this.tableNamespace === path;
17056
+ return this.source === path || this.datasetId === path || this.tableNamespace === path || this.#info.recoveredReturnPaths?.includes(path) === true;
16564
17057
  }
16565
17058
  /** Return a new Handle when its preview has been replaced by fetched rows. */
16566
17059
  withRows(info) {
@@ -17405,6 +17898,39 @@ var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17405
17898
  "--open",
17406
17899
  "--debug-map-latency"
17407
17900
  ]);
17901
+ var PLAY_RUN_HEADER_FLAG_USAGE = `--header requires "<name>: <value>", for example: --header 'x-deepline-absurd-release: 2026-09-07.1'`;
17902
+ var HTTP_HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/;
17903
+ function parsePlayRunHeaderFlag(raw) {
17904
+ const separator = raw.indexOf(":");
17905
+ if (separator < 0) {
17906
+ throw new Error(
17907
+ `${PLAY_RUN_HEADER_FLAG_USAGE}. Got ${JSON.stringify(raw)} with no ":" separator.`
17908
+ );
17909
+ }
17910
+ const name = raw.slice(0, separator).trim().toLowerCase();
17911
+ const value = raw.slice(separator + 1).trim();
17912
+ if (!name) {
17913
+ throw new Error(
17914
+ `${PLAY_RUN_HEADER_FLAG_USAGE}. Got ${JSON.stringify(raw)} with an empty header name.`
17915
+ );
17916
+ }
17917
+ if (!HTTP_HEADER_NAME_PATTERN.test(name)) {
17918
+ throw new Error(
17919
+ `${PLAY_RUN_HEADER_FLAG_USAGE}. ${JSON.stringify(name)} is not a valid HTTP header name.`
17920
+ );
17921
+ }
17922
+ if (!value) {
17923
+ throw new Error(
17924
+ `${PLAY_RUN_HEADER_FLAG_USAGE}. Header ${JSON.stringify(name)} was given an empty value.`
17925
+ );
17926
+ }
17927
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
17928
+ throw new Error(
17929
+ `${PLAY_RUN_HEADER_FLAG_USAGE}. Header ${JSON.stringify(name)} contains a control character.`
17930
+ );
17931
+ }
17932
+ return { name, value };
17933
+ }
17408
17934
  function isBarePlayRunDebugFlag(args, index) {
17409
17935
  return args[index] === "--debug" && (args[index + 1] === void 0 || args[index + 1].startsWith("--"));
17410
17936
  }
@@ -19018,6 +19544,9 @@ function assertPlayWaitNotTimedOut(input2) {
19018
19544
  }
19019
19545
  }
19020
19546
  var PLAY_DURABLE_STATUS_PROBE_INTERVAL_MS = 15e3;
19547
+ function isAdmittedRunVisibilityDelay(error) {
19548
+ return error instanceof DeeplineError && error.statusCode === 404 && error.message.trim() === "Workflow run not found.";
19549
+ }
19021
19550
  async function waitForPlayCompletionByStream(input2) {
19022
19551
  let lastPhase = null;
19023
19552
  let reconnectAttempt = 0;
@@ -19031,7 +19560,7 @@ async function waitForPlayCompletionByStream(input2) {
19031
19560
  billing: false
19032
19561
  });
19033
19562
  } catch (error) {
19034
- if (isTransientPlayStreamError(error)) {
19563
+ if (isTransientPlayStreamError(error) || isAdmittedRunVisibilityDelay(error)) {
19035
19564
  return null;
19036
19565
  }
19037
19566
  throw error;
@@ -19142,7 +19671,7 @@ async function waitForPlayCompletionByStream(input2) {
19142
19671
  if (timedOut) {
19143
19672
  return { kind: "timed_out", sawEvent };
19144
19673
  }
19145
- if (!isTransientPlayStreamError(error)) {
19674
+ if (!isTransientPlayStreamError(error) && !isAdmittedRunVisibilityDelay(error)) {
19146
19675
  throw error;
19147
19676
  }
19148
19677
  return {
@@ -21378,7 +21907,7 @@ function writeStartedPlayRun(input2) {
21378
21907
  );
21379
21908
  }
21380
21909
  function parsePlayRunOptions(args) {
21381
- const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--profile <id>] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--profile <id>] [--live|--latest|--revision-id <id>] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--json] [--full] [--<input> value]\n Runs use the Absurd CJS runtime.\n Unknown --<input> value flags, such as --limit 5, are passed into play input.\nRun `deepline plays run --help` for idempotent call caching and ctx.dataset guidance.";
21910
+ const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--profile <id>] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--full] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--profile <id>] [--live|--latest|--revision-id <id>] [--run-id-file <path>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--open] [--json] [--full] [--<input> value]\n Runs use the Absurd CJS runtime.\n --header '<name>: <value>' may repeat; each is sent on every request for this run.\n Unknown --<input> value flags, such as --limit 5, are passed into play input.\nRun `deepline plays run --help` for idempotent call caching and ctx.dataset guidance.";
21382
21911
  let filePath = null;
21383
21912
  let playName = null;
21384
21913
  let input2 = null;
@@ -21399,8 +21928,20 @@ function parsePlayRunOptions(args) {
21399
21928
  let profile = null;
21400
21929
  let fixtureBehavior = null;
21401
21930
  let runIdFile = null;
21931
+ let headers = null;
21402
21932
  for (let index = 0; index < args.length; index += 1) {
21403
21933
  const arg = args[index];
21934
+ if (arg === "--header" || arg.startsWith("--header=")) {
21935
+ const raw = arg === "--header" ? args[index + 1] : arg.slice("--header=".length);
21936
+ if (raw === void 0 || arg === "--header" && raw.startsWith("--")) {
21937
+ throw new Error(`${PLAY_RUN_HEADER_FLAG_USAGE}. No value was given.`);
21938
+ }
21939
+ const header = parsePlayRunHeaderFlag(raw);
21940
+ headers ??= {};
21941
+ headers[header.name] = header.value;
21942
+ if (arg === "--header") index += 1;
21943
+ continue;
21944
+ }
21404
21945
  if (arg === "--file" && args[index + 1]) {
21405
21946
  filePath = args[++index];
21406
21947
  continue;
@@ -21639,7 +22180,8 @@ function parsePlayRunOptions(args) {
21639
22180
  profile,
21640
22181
  debugMapLatency,
21641
22182
  fixtureBehavior,
21642
- runIdFile
22183
+ runIdFile,
22184
+ headers
21643
22185
  };
21644
22186
  }
21645
22187
  function parsePlayCheckOptions(args) {
@@ -22395,11 +22937,14 @@ function printPlayCheckOutcomes(outcomes, target) {
22395
22937
  printPlayCheckOutcome(outcome, target, `[${outcome.exportName}] `);
22396
22938
  }
22397
22939
  }
22940
+ function playRunClientOptions(options) {
22941
+ return options.headers ? { headers: options.headers } : void 0;
22942
+ }
22398
22943
  async function handleFileBackedRun(options, hooks) {
22399
22944
  if (options.target.kind !== "file") {
22400
22945
  throw new Error("Expected a file-backed play run target.");
22401
22946
  }
22402
- const client2 = new DeeplineClient();
22947
+ const client2 = new DeeplineClient(playRunClientOptions(options));
22403
22948
  const progress = getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
22404
22949
  const absolutePlayPath = resolve11(options.target.path);
22405
22950
  progress.phase("compiling play");
@@ -22607,7 +23152,7 @@ async function handleNamedRun(options, hooks) {
22607
23152
  if (options.target.kind !== "name") {
22608
23153
  throw new Error("Expected a named play run target.");
22609
23154
  }
22610
- const client2 = new DeeplineClient();
23155
+ const client2 = new DeeplineClient(playRunClientOptions(options));
22611
23156
  const progress = getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
22612
23157
  const playName = options.target.name;
22613
23158
  const runtimeInput = options.input ? { ...options.input } : {};
@@ -32702,7 +33247,7 @@ function renderDeployedListText(payload, requestedStatus) {
32702
33247
  }
32703
33248
  const applied = asString(payload.status_filter_applied) ?? requestedStatus ?? "active";
32704
33249
  lines.push(
32705
- `Status filter: ${applied}${requestedStatus === void 0 && applied === "active" ? " (default)" : ""} \u2014 use --status all to include disabled monitors.`
33250
+ `Status filter: ${applied}${requestedStatus === void 0 && applied === "active" ? " (default)" : ""} \u2014 use --status all to include inactive monitors.`
32706
33251
  );
32707
33252
  const summary = Array.isArray(payload.status_summary) ? payload.status_summary.flatMap((raw) => {
32708
33253
  const item = asRecord2(raw);
@@ -32710,7 +33255,7 @@ function renderDeployedListText(payload, requestedStatus) {
32710
33255
  const count = item ? asFiniteNumber(item.count) : void 0;
32711
33256
  return status && count !== void 0 ? [`${status}: ${count}`] : [];
32712
33257
  }).join(" ") : "";
32713
- if (summary) lines.push(`All lifecycle states: ${summary}`);
33258
+ if (summary) lines.push(`All monitor states: ${summary}`);
32714
33259
  if (payload.has_more === true || payload.is_truncated === true) {
32715
33260
  const nextCursor = asString(payload.next_cursor);
32716
33261
  lines.push(
@@ -33799,6 +34344,9 @@ async function handleMonitorsReactivate(key, options) {
33799
34344
  }
33800
34345
  var MONITOR_VERB_POLL_INTERVAL_MS = 2e3;
33801
34346
  function monitorHealthLine(health) {
34347
+ if (health.refresh?.state === "running") {
34348
+ return `refreshing whole scope \xB7 scanned ${health.scanned} so far`;
34349
+ }
33802
34350
  return [
33803
34351
  `converged ${health.converged}/${health.scanned}`,
33804
34352
  `needs_repair ${health.needs_repair}`,
@@ -33837,7 +34385,9 @@ async function watchMonitorHealth(input2) {
33837
34385
  let last = await input2.client.monitors.health(scope);
33838
34386
  input2.onProgress(last);
33839
34387
  while (Date.now() < deadlineAt) {
33840
- if (last.needs_repair === 0 && last.observed.unknown === 0) return last;
34388
+ if (last.refresh?.state !== "running" && last.needs_repair === 0 && last.observed.unknown === 0) {
34389
+ return last;
34390
+ }
33841
34391
  await new Promise(
33842
34392
  (resolve21) => setTimeout(resolve21, MONITOR_VERB_POLL_INTERVAL_MS)
33843
34393
  );
@@ -33847,11 +34397,21 @@ async function watchMonitorHealth(input2) {
33847
34397
  return last;
33848
34398
  }
33849
34399
  function monitorHealthText(health) {
33850
- const partial = health.truncated ? " (partial \u2014 scope larger than one read)" : "";
34400
+ const partial = health.refresh?.state === "running" ? " (refreshing \u2014 counts below are not final)" : health.truncated ? " (partial \u2014 scope larger than one read)" : "";
33851
34401
  const lines = [
33852
34402
  `scope: ${health.scope.fleet_id ?? "workspace"}${partial}`,
33853
34403
  monitorHealthLine(health)
33854
34404
  ];
34405
+ if ((health.batch_operations?.length ?? 0) > 0) {
34406
+ lines.push("");
34407
+ lines.push("batch work in progress:");
34408
+ for (const operation of health.batch_operations ?? []) {
34409
+ const retry = operation.next_attempt_at === void 0 ? "" : ` \xB7 next ${new Date(operation.next_attempt_at).toISOString()}`;
34410
+ lines.push(
34411
+ ` ${operation.kind} ${operation.run_id} \xB7 ${operation.state}${retry}`
34412
+ );
34413
+ }
34414
+ }
33855
34415
  if (health.deactivation_reasons.length > 0) {
33856
34416
  lines.push("");
33857
34417
  lines.push("deactivated by Deepline:");
@@ -33924,78 +34484,143 @@ async function handleMonitorsAudit(options) {
33924
34484
  );
33925
34485
  }
33926
34486
  async function handleMonitorsRepair(options) {
33927
- const client2 = new DeeplineClient();
34487
+ return await handleMonitorBatch("repair", {
34488
+ ...options,
34489
+ ...options.fleet?.trim() ? {} : { target: "needs-repair" }
34490
+ });
34491
+ }
34492
+ async function handleMonitorsHealth(options) {
33928
34493
  const fleetId = options.fleet?.trim() || void 0;
33929
- const scope = fleetId ? { fleetId } : {};
33930
- const result = await client2.monitors.repair({
33931
- ...scope,
33932
- ...options.dryRun ? { dryRun: true } : {}
34494
+ const health = await new DeeplineClient().monitors.health(
34495
+ fleetId ? { fleetId } : {}
34496
+ );
34497
+ printCommandEnvelope(health, {
34498
+ json: options.json,
34499
+ text: monitorHealthText(health)
33933
34500
  });
33934
- if (options.dryRun) {
33935
- assertMonitorDryRunAcknowledged(
33936
- result,
33937
- { command: "monitors repair", mutation: "repair" }
34501
+ }
34502
+ function monitorBatchScope(options, kind) {
34503
+ const selectors = [
34504
+ options.target ? "target" : null,
34505
+ options.fleet?.trim() ? "fleet" : null,
34506
+ options.keys?.trim() ? "keys" : null
34507
+ ].filter((value) => value !== null);
34508
+ if (selectors.length === 0 && kind === "audit") {
34509
+ return { kind: "target", target: "all" };
34510
+ }
34511
+ if (selectors.length !== 1) {
34512
+ throw new MonitorsUsageError(
34513
+ `monitors batch ${kind} needs exactly one selector: --target, --fleet, or --keys.`
33938
34514
  );
33939
34515
  }
33940
- let health = result.health;
33941
- if (options.watch && !options.dryRun) {
33942
- const progress = monitorVerbProgressReporter();
33943
- try {
33944
- health = await watchMonitorHealth({
33945
- client: client2,
33946
- ...scope,
33947
- timeoutMs: fleetTimeoutMs(options.timeout),
33948
- onProgress: progress.report
33949
- });
33950
- } finally {
33951
- progress.finish();
33952
- }
34516
+ if (options.target) return { kind: "target", target: options.target };
34517
+ if (options.fleet?.trim())
34518
+ return { kind: "fleet", fleetId: options.fleet.trim() };
34519
+ const monitorKeys = (options.keys ?? "").split(",").map((key) => key.trim()).filter(Boolean);
34520
+ if (monitorKeys.length === 0) {
34521
+ throw new MonitorsUsageError(
34522
+ "--keys needs one or more comma-separated monitor keys."
34523
+ );
33953
34524
  }
33954
- const verb = options.dryRun ? "would queue" : "queued";
33955
- const lines = [
33956
- `${verb} ${result.repair.queued} of ${result.repair.scanned} monitors`
33957
- ];
33958
- for (const item of result.repair.plan.slice(0, REPAIR_PLAN_PREVIEW)) {
33959
- const why = item.deactivation_reason ? ` \xB7 ${item.deactivation_reason}` : "";
33960
- lines.push(
33961
- ` ${item.action.padEnd(6)} ${item.monitor_key} desired ${item.desired} \xB7 observed ${item.observed}${why}`
34525
+ return { kind: "keys", monitorKeys };
34526
+ }
34527
+ function monitorBatchIdempotencyKey(kind, value) {
34528
+ return value?.trim() || `monitor-batch-${kind}-${randomUUID5()}`;
34529
+ }
34530
+ async function handleMonitorBatchDeploy(options) {
34531
+ if (!options.file?.trim()) {
34532
+ throw new MonitorsUsageError(
34533
+ "monitors batch deploy requires --file <monitors.ndjson>."
33962
34534
  );
33963
34535
  }
33964
- if (result.repair.plan.length > REPAIR_PLAN_PREVIEW) {
33965
- lines.push(` \u2026 ${result.repair.plan.length - REPAIR_PLAN_PREVIEW} more`);
34536
+ if (options.dryRun !== true && options.yes !== true) {
34537
+ throw new MonitorsUsageError(
34538
+ "monitors batch deploy changes provider monitors asynchronously. Preview with --dry-run, then confirm with --yes."
34539
+ );
33966
34540
  }
33967
- if (result.repair.unaudited.length > 0) {
33968
- lines.push(
33969
- `${result.repair.unaudited.length} monitors have never been observed \u2014 run 'deepline monitors audit' first.`
34541
+ let manifestNdjson;
34542
+ try {
34543
+ manifestNdjson = readFileSync11(resolve14(options.file), "utf8");
34544
+ } catch (error) {
34545
+ throw new MonitorsUsageError(
34546
+ `Could not read monitor batch manifest '${options.file}': ${error instanceof Error ? error.message : String(error)}`
33970
34547
  );
33971
34548
  }
33972
- if (result.repair.unowned.length > 0) {
33973
- lines.push(
33974
- `${result.repair.unowned.length} monitors disagree but no fleet owns them; repair those with 'deepline monitors deploy' or 'deepline monitors delete'.`
34549
+ if (!manifestNdjson.trim()) {
34550
+ throw new MonitorsUsageError("Monitor batch manifest is empty.");
34551
+ }
34552
+ const idempotencyKey = monitorBatchIdempotencyKey(
34553
+ "deploy",
34554
+ options.idempotencyKey
34555
+ );
34556
+ const result = await new DeeplineClient().monitors.batch.submit({
34557
+ kind: "deploy",
34558
+ manifestNdjson,
34559
+ dryRun: options.dryRun === true,
34560
+ idempotencyKey
34561
+ });
34562
+ const operation = result.operation;
34563
+ printCommandEnvelope(
34564
+ { ...result, idempotency_key: idempotencyKey },
34565
+ {
34566
+ json: options.json,
34567
+ text: [
34568
+ `${result.rejoined ? "rejoined" : "submitted"} deploy batch ${operation.runId}`,
34569
+ `state ${operation.state} \xB7 retry key ${idempotencyKey}`,
34570
+ `inspect: deepline monitors batch get ${operation.runId} --json`
34571
+ ].join("\n") + "\n"
34572
+ }
34573
+ );
34574
+ }
34575
+ async function handleMonitorBatch(kind, options) {
34576
+ if ((kind === "deactivate" || kind === "reactivate" || kind === "repair") && options.dryRun !== true && options.yes !== true) {
34577
+ throw new MonitorsUsageError(
34578
+ `monitors batch ${kind} changes provider monitors asynchronously. Preview with --dry-run, then confirm with --yes.`
33975
34579
  );
33976
34580
  }
33977
- if (result.repair.fenced.length > 0) {
33978
- lines.push(
33979
- `${result.repair.fenced.length} monitors have a provider operation in flight; Deepline is finishing those and repair left them alone.`
34581
+ if (kind === "deactivate" && !options.reason?.trim()) {
34582
+ throw new MonitorsUsageError(
34583
+ "monitors batch deactivate requires --reason (for example insufficient_balance or billing_failure)."
33980
34584
  );
33981
34585
  }
33982
- lines.push("");
33983
- lines.push(monitorHealthText(health));
34586
+ const idempotencyKey = monitorBatchIdempotencyKey(
34587
+ kind,
34588
+ options.idempotencyKey
34589
+ );
34590
+ const result = await new DeeplineClient().monitors.batch.submit({
34591
+ kind,
34592
+ scope: monitorBatchScope(options, kind),
34593
+ dryRun: options.dryRun === true,
34594
+ ...options.reason?.trim() ? { deactivationReason: options.reason.trim() } : {},
34595
+ idempotencyKey
34596
+ });
34597
+ const operation = result.operation;
34598
+ const text = [
34599
+ `${result.rejoined ? "rejoined" : "submitted"} ${operation.kind} batch ${operation.runId}`,
34600
+ `state ${operation.state} \xB7 retry key ${idempotencyKey}`,
34601
+ `inspect: deepline monitors batch get ${operation.runId} --json`
34602
+ ].join("\n");
33984
34603
  printCommandEnvelope(
33985
- { ...result, health },
33986
- { json: options.json, text: lines.join("\n") }
34604
+ { ...result, idempotency_key: idempotencyKey },
34605
+ { json: options.json, text: `${text}
34606
+ ` }
33987
34607
  );
33988
34608
  }
33989
- var REPAIR_PLAN_PREVIEW = 20;
33990
- async function handleMonitorsHealth(options) {
33991
- const fleetId = options.fleet?.trim() || void 0;
33992
- const health = await new DeeplineClient().monitors.health(
33993
- fleetId ? { fleetId } : {}
33994
- );
33995
- printCommandEnvelope(health, {
33996
- json: options.json,
33997
- text: monitorHealthText(health)
34609
+ async function handleMonitorBatchGet(operationId, options) {
34610
+ const result = await new DeeplineClient().monitors.batch.get(operationId, {
34611
+ ...options.cursor ? { cursor: options.cursor } : {}
33998
34612
  });
34613
+ const summary = result.operation.summary ?? {};
34614
+ const text = [
34615
+ `${result.operation.kind} batch ${result.operation.runId}`,
34616
+ `state ${result.operation.state}`,
34617
+ `progress ${JSON.stringify(summary)}`,
34618
+ ...result.results ? [
34619
+ `results ${result.results.page.length}${result.results.isDone ? "" : " (more; pass --cursor)"}`
34620
+ ] : []
34621
+ ].join("\n");
34622
+ printCommandEnvelope(result, { json: options.json, text: `${text}
34623
+ ` });
33999
34624
  }
34000
34625
  function registerMonitorsCommands(program) {
34001
34626
  const monitors = program.command("monitors").description("Discover, deploy, and manage Deepline monitors.").showSuggestionAfterError(true).addHelpText(
@@ -34060,25 +34685,29 @@ Examples:
34060
34685
  withJsonOption(
34061
34686
  monitors.command("repair").description(
34062
34687
  "Converge the monitors whose desired and observed states disagree."
34063
- ).option("--fleet <fleet-id>", "Repair one fleet instead of every monitor").option("--dry-run", "Show what would be queued without queueing it").option("--watch", "Keep polling health until the scope settles").option(
34064
- "--timeout <duration>",
34065
- "How long --watch waits, e.g. 90s, 5m, 1h (default 10m)"
34688
+ ).option("--fleet <fleet-id>", "Repair one fleet instead of every monitor").option(
34689
+ "--dry-run",
34690
+ "Record the bounded repair plan without provider changes"
34691
+ ).option("--yes", "Confirm the asynchronous provider repair").option(
34692
+ "--idempotency-key <key>",
34693
+ "Stable retry key (generated when omitted)"
34066
34694
  ).addHelpText(
34067
34695
  "after",
34068
34696
  `
34069
34697
  Notes:
34070
- Queues the monitors that disagree with the provider onto the same convergence
34071
- loop a fleet sync uses; the provider work happens on Deepline's own schedule.
34072
- Idempotent \u2014 the queue IS the disagreement, so a repeat run queues only what
34073
- is still wrong.
34698
+ This is the short spelling of 'monitors batch repair --target needs-repair'.
34699
+ It handles both direct monitors and Fleet members through one durable
34700
+ service-account worker. An active historical monitor without an upstream id
34701
+ is selected for repair; current Convex writes cannot create that state.
34074
34702
 
34075
- Monitors nobody has observed are never repaired: run 'deepline monitors audit'
34076
- first. Monitors no fleet owns are reported rather than queued; deploy or delete
34077
- those directly.
34703
+ It returns a run id immediately. Inspect retained results with
34704
+ 'deepline monitors batch get <run-id> --json'. Reuse --idempotency-key after
34705
+ a lost response to rejoin the exact run without another provider request.
34078
34706
 
34079
34707
  Examples:
34080
34708
  deepline monitors repair --dry-run
34081
- deepline monitors repair --fleet accounts-eu --watch
34709
+ deepline monitors repair --yes
34710
+ deepline monitors repair --fleet accounts-eu --yes --idempotency-key repair-eu-2026-09-09
34082
34711
  `
34083
34712
  )
34084
34713
  ).action(monitorsAction(handleMonitorsRepair));
@@ -34101,6 +34730,75 @@ Examples:
34101
34730
  `
34102
34731
  )
34103
34732
  ).action(monitorsAction(handleMonitorsHealth));
34733
+ const batch = monitors.command("batch").description("Submit or inspect durable bulk monitor operations.");
34734
+ const batchSelectorOptions = (command) => command.option(
34735
+ "--target <target>",
34736
+ "One of: all, all-active, billing-suspended, needs-repair"
34737
+ ).option("--fleet <fleet-id>", "Select one Fleet").option("--keys <keys>", "Up to 100 comma-separated monitor keys").option(
34738
+ "--dry-run",
34739
+ "Record the same bounded plan without provider changes"
34740
+ ).option(
34741
+ "--idempotency-key <key>",
34742
+ "Stable retry key (generated when omitted)"
34743
+ );
34744
+ withJsonOption(
34745
+ batch.command("deploy").description(
34746
+ "Durably create complete NDJSON monitor definitions through provider bulk create."
34747
+ ).requiredOption(
34748
+ "--file <path>",
34749
+ "NDJSON file of complete monitor definitions"
34750
+ ).option("--dry-run", "Validate and record the durable deploy plan").option("--yes", "Confirm asynchronous provider creation").option(
34751
+ "--idempotency-key <key>",
34752
+ "Stable retry key (generated when omitted)"
34753
+ )
34754
+ ).action(
34755
+ monitorsAction(
34756
+ (options) => handleMonitorBatchDeploy(options)
34757
+ )
34758
+ );
34759
+ withJsonOption(
34760
+ batchSelectorOptions(
34761
+ batch.command("audit").description("Durably audit a selection; defaults to --target all.")
34762
+ )
34763
+ ).action(
34764
+ monitorsAction(
34765
+ (options) => handleMonitorBatch("audit", options)
34766
+ )
34767
+ );
34768
+ withJsonOption(
34769
+ batchSelectorOptions(
34770
+ batch.command("repair").description(
34771
+ "Durably re-audit and converge a selection through the existing lifecycle fences."
34772
+ )
34773
+ ).option("--yes", "Confirm the asynchronous provider repair")
34774
+ ).action(
34775
+ monitorsAction(
34776
+ (options) => handleMonitorBatch("repair", options)
34777
+ )
34778
+ );
34779
+ withJsonOption(
34780
+ batchSelectorOptions(
34781
+ batch.command("deactivate").description(
34782
+ "Durably deactivate a selection through provider batch deletes."
34783
+ )
34784
+ ).requiredOption("--reason <reason>", "Declared deactivation reason").option("--yes", "Confirm the asynchronous provider deactivation")
34785
+ ).action(
34786
+ monitorsAction(
34787
+ (options) => handleMonitorBatch("deactivate", options)
34788
+ )
34789
+ );
34790
+ withJsonOption(
34791
+ batchSelectorOptions(
34792
+ batch.command("reactivate").description("Durably reactivate a selection after a suspension.")
34793
+ ).option("--yes", "Confirm provider reactivation and possible billing")
34794
+ ).action(
34795
+ monitorsAction(
34796
+ (options) => handleMonitorBatch("reactivate", options)
34797
+ )
34798
+ );
34799
+ withJsonOption(
34800
+ batch.command("get <operation-id>").description("Read batch progress and one bounded evidence page.").option("--cursor <cursor>", "Read the next retained evidence page")
34801
+ ).action(monitorsAction(handleMonitorBatchGet));
34104
34802
  withJsonOption(
34105
34803
  monitors.command("status").description("Check whether you have access to Deepline monitors.").addHelpText(
34106
34804
  "after",
@@ -34152,8 +34850,9 @@ Examples:
34152
34850
  "after",
34153
34851
  `
34154
34852
  Notes:
34155
- Read-only. --status filters by monitor status: active (default), disabled, or
34156
- all. The output echoes the status filter that was applied. The response reports
34853
+ Read-only. A monitor state is active or inactive; provider disagreement is
34854
+ reported separately by audit/health. The output echoes the state filter that
34855
+ was applied. The response reports
34157
34856
  total (the true registry count, not the page size), returned, has_more, and
34158
34857
  next_cursor. When has_more is true, page with --cursor <next_cursor> until it
34159
34858
  is false \u2014 a "no matching
@@ -34171,7 +34870,7 @@ Examples:
34171
34870
  `
34172
34871
  ).option(
34173
34872
  "--status <status>",
34174
- "Filter by monitor status: active (default), disabled, or all"
34873
+ "Filter by monitor state: active (default), inactive, or all"
34175
34874
  ).option("--limit <n>", "Limit the number of deployed monitors returned").option(
34176
34875
  "--cursor <cursor>",
34177
34876
  "Page past a truncated result using the next_cursor from a prior list response"
@@ -34564,7 +35263,7 @@ Example:
34564
35263
  const deployed = withJsonOption(
34565
35264
  monitors.command("deployed", { hidden: true }).description("Alias of `monitors list` (plus get/update/delete aliases).").option(
34566
35265
  "--status <status>",
34567
- "Filter by monitor status: active (default), disabled, or all"
35266
+ "Filter by monitor state: active (default), inactive, or all"
34568
35267
  ).option("--limit <n>", "Limit the number of deployed monitors returned").option(
34569
35268
  "--cursor <cursor>",
34570
35269
  "Page past a truncated result using the next_cursor from a prior list response"
@@ -34712,7 +35411,8 @@ function organizationAuthValues(input2) {
34712
35411
  [HOST_URL_ENV]: input2.baseUrl,
34713
35412
  [API_KEY_ENV]: input2.apiKey,
34714
35413
  [ACTIVE_ORG_ID_ENV]: input2.orgId,
34715
- [ACTIVE_ORG_NAME_ENV]: input2.orgName
35414
+ [ACTIVE_ORG_NAME_ENV]: input2.orgName,
35415
+ ...featureEnvValues(input2)
34716
35416
  };
34717
35417
  }
34718
35418
  function organizationCommandContext(input2) {
@@ -34928,6 +35628,7 @@ async function handleOrgSwitch(selection, options) {
34928
35628
  if (!selection && !options.orgId && authScope === "folder" && provenance.scope === "global" && provenance.project.state === "project") {
34929
35629
  const commandContext = resolveCliCommandContext(config);
34930
35630
  const activeOrganization = commandContext.active_organization;
35631
+ const pinnedFeatures = storedHostFeatures(config.baseUrl);
34931
35632
  const project_env_paths2 = saveProjectDeeplineEnvValues(
34932
35633
  {
34933
35634
  [HOST_URL_ENV]: config.baseUrl,
@@ -34935,7 +35636,8 @@ async function handleOrgSwitch(selection, options) {
34935
35636
  ...activeOrganization ? {
34936
35637
  [ACTIVE_ORG_ID_ENV]: activeOrganization.org_id,
34937
35638
  [ACTIVE_ORG_NAME_ENV]: activeOrganization.org_name ?? ""
34938
- } : {}
35639
+ } : {},
35640
+ ...pinnedFeatures !== void 0 ? { [FEATURES_ENV]: pinnedFeatures } : {}
34939
35641
  },
34940
35642
  provenance.project.dir
34941
35643
  );
@@ -35015,11 +35717,15 @@ async function handleOrgSwitch(selection, options) {
35015
35717
  const authTarget = resolveOrgSwitchAuthTarget(authScope, config);
35016
35718
  if (target.is_current) {
35017
35719
  let project_env_paths2 = [];
35720
+ const features = await http.post("/api/v2/auth/cli/status", {
35721
+ api_key: config.apiKey
35722
+ }).then((status) => status.features).catch(() => void 0);
35018
35723
  const authValues2 = organizationAuthValues({
35019
35724
  baseUrl: config.baseUrl,
35020
35725
  apiKey: config.apiKey,
35021
35726
  orgId: target.org_id,
35022
- orgName: target.name
35727
+ orgName: target.name,
35728
+ features
35023
35729
  });
35024
35730
  if (authTarget.kind === "folder") {
35025
35731
  project_env_paths2 = saveProjectDeeplineEnvValues(authValues2);
@@ -35086,7 +35792,8 @@ async function handleOrgSwitch(selection, options) {
35086
35792
  baseUrl: config.baseUrl,
35087
35793
  apiKey: switched.api_key,
35088
35794
  orgId: switched.org_id,
35089
- orgName: switched.org_name
35795
+ orgName: switched.org_name,
35796
+ features: switched.features
35090
35797
  });
35091
35798
  if (authTarget.kind === "folder") {
35092
35799
  project_env_paths = saveProjectDeeplineEnvValues(authValues);
@@ -35158,14 +35865,20 @@ async function handleOrgCreate(name, options) {
35158
35865
  const identity = await fetchWorkspaceCreationIdentity(http, config.apiKey);
35159
35866
  let created;
35160
35867
  let workspaceApiKey;
35868
+ let workspaceFeatures;
35161
35869
  let pendingIntentPath = null;
35162
35870
  if (!identity.orgId) {
35163
35871
  const firstWorkspace = await http.post("/api/v2/auth/cli/org-create", {
35164
35872
  api_key: config.apiKey,
35165
35873
  name: normalizedName
35166
35874
  });
35167
- const { api_key: apiKey, ...publicFirstWorkspace } = firstWorkspace;
35875
+ const {
35876
+ api_key: apiKey,
35877
+ features,
35878
+ ...publicFirstWorkspace
35879
+ } = firstWorkspace;
35168
35880
  workspaceApiKey = apiKey;
35881
+ workspaceFeatures = features;
35169
35882
  created = publicFirstWorkspace;
35170
35883
  } else {
35171
35884
  const pending = loadOrCreatePendingOrgCreate({
@@ -35186,6 +35899,7 @@ async function handleOrgCreate(name, options) {
35186
35899
  org_id: workspace.org_id
35187
35900
  });
35188
35901
  workspaceApiKey = switched.api_key;
35902
+ workspaceFeatures = switched.features;
35189
35903
  created = { ...workspace };
35190
35904
  pendingIntentPath = pending.path;
35191
35905
  }
@@ -35193,7 +35907,8 @@ async function handleOrgCreate(name, options) {
35193
35907
  baseUrl: config.baseUrl,
35194
35908
  apiKey: workspaceApiKey,
35195
35909
  orgId: created.org_id,
35196
- orgName: created.org_name
35910
+ orgName: created.org_name,
35911
+ features: workspaceFeatures
35197
35912
  });
35198
35913
  saveHostEnvValues(config.baseUrl, authValues);
35199
35914
  if (pendingIntentPath) unlinkSync(pendingIntentPath);
@@ -35390,6 +36105,7 @@ Examples:
35390
36105
  }
35391
36106
 
35392
36107
  // src/cli/commands/git.ts
36108
+ var GIT_DEPLOY_FEATURE = "github-deploy";
35393
36109
  function renderProject(project) {
35394
36110
  const root = project.rootDirectory ? ` (${project.rootDirectory})` : "";
35395
36111
  const flags = [
@@ -35642,7 +36358,7 @@ async function handleDeploymentGet(id, options) {
35642
36358
  );
35643
36359
  }
35644
36360
  function registerGitCommands(program) {
35645
- const git = program.command("git").alias("projects").description(
36361
+ const git = program.command("git", { hidden: !cliFeatureEnabled(GIT_DEPLOY_FEATURE) }).alias("projects").description(
35646
36362
  "Git deploys: link repositories to this workspace and deploy plays on push."
35647
36363
  ).addHelpText(
35648
36364
  "after",
@@ -35963,18 +36679,25 @@ async function handleList2(options) {
35963
36679
  async function handleCheck(nameInput, options) {
35964
36680
  const name = normalizeSecretName(nameInput);
35965
36681
  const client2 = new DeeplineClient();
35966
- const secret = await client2.checkSecret(name);
36682
+ const check = options.play !== void 0 ? await client2.inspectSecret(name, { playName: options.play }) : null;
36683
+ const secret = check ? check.secret : await client2.checkSecret(name);
36684
+ const ok = check ? check.ok : Boolean(secret);
36685
+ const quotedPlay = `'${(options.play ?? "PLAY").replace(/'/g, `'"'"'`)}'`;
36686
+ const scopeHint = check ? secret?.scope === "play" ? check.reason === "disabled" ? `The Play-scoped secret is disabled and no active org fallback is available. Restore it with: deepline secrets set ${name} --scope play --play ${quotedPlay}` : `The active Play-scoped secret takes precedence over the org secret.${ok ? "" : ` Restore it with: deepline secrets set ${name} --scope play --play ${quotedPlay}`}` : `No Play override.${ok ? " The org secret is available." : ` Set or rotate the org secret with: deepline secrets set ${name}`}` : `Metadata only. Verify effective scope and active version with: deepline secrets check ${name} --play <play-name>`;
35967
36687
  printCommandEnvelope(
35968
36688
  {
35969
- ok: Boolean(secret),
36689
+ ok,
35970
36690
  name,
35971
36691
  secret: secret ?? null,
36692
+ check: check ? { playName: check.playName, reason: check.reason } : null,
36693
+ scopeHint,
35972
36694
  render: {
35973
36695
  sections: [
35974
36696
  {
35975
36697
  title: "secret check",
35976
36698
  lines: [
35977
- secret ? `${name}: active \u2014 ${renderSecret(secret)}` : `${name}: missing, disabled, or empty`
36699
+ check ? `${name}: ${check.reason} for ${check.playName}${secret ? ` (${secret.scope})` : ""}` : secret ? `${name}: active metadata \u2014 ${renderSecret(secret)}` : `${name}: missing, disabled, or empty`,
36700
+ scopeHint
35978
36701
  ]
35979
36702
  }
35980
36703
  ]
@@ -35982,7 +36705,7 @@ async function handleCheck(nameInput, options) {
35982
36705
  },
35983
36706
  { json: options.json }
35984
36707
  );
35985
- if (!secret) process.exitCode = 4;
36708
+ if (!ok) process.exitCode = 4;
35986
36709
  }
35987
36710
  async function handleSet2(nameInput, forbidden, options) {
35988
36711
  preventShellHistoryLeak(forbidden);
@@ -36036,6 +36759,7 @@ Notes:
36036
36759
  Examples:
36037
36760
  deepline secrets list
36038
36761
  deepline secrets check HUBSPOT_TOKEN
36762
+ deepline secrets check HUBSPOT_TOKEN --play hubspot-sync
36039
36763
  deepline secrets set HUBSPOT_TOKEN
36040
36764
  printf %s "$HUBSPOT_TOKEN" | deepline secrets set HUBSPOT_TOKEN --value-stdin
36041
36765
  deepline secrets set HUBSPOT_TOKEN --from-env HUBSPOT_TOKEN
@@ -36045,7 +36769,12 @@ Examples:
36045
36769
  secrets.command("list").description("List secret metadata only.").option("--json", "Emit JSON output").action(async (options) => {
36046
36770
  await handleList2(options);
36047
36771
  });
36048
- secrets.command("check").description("Check whether a secret exists and is active.").argument("<name>", "Secret name").option("--json", "Emit JSON output").action(async (name, options) => {
36772
+ secrets.command("check").description(
36773
+ "Check secret metadata, or effective availability with --play."
36774
+ ).argument("<name>", "Secret name").option(
36775
+ "--play <name>",
36776
+ "Validate the effective secret for a bare Play name and active version"
36777
+ ).option("--json", "Emit JSON output").action(async (name, options) => {
36049
36778
  await handleCheck(name, options);
36050
36779
  });
36051
36780
  secrets.command("set").description("Set or rotate a secret without exposing its value in argv.").argument("<name>", "Secret name").argument("[forbidden...]", "Do not pass secret values here").option("--json", "Emit JSON output").option("--scope <scope>", "Secret scope: org or play", "org").option("--play <name>", "Play name for play-scoped secrets").option("--value-stdin", "Read the value from piped stdin").option("--from-env <name>", "Read the value from an environment variable").option("--from-file <path>", "Read the value from a file").action(
@@ -36438,6 +37167,20 @@ function classifyNetworkFailure(error) {
36438
37167
  }
36439
37168
  return "network_error";
36440
37169
  }
37170
+ function classifyNetworkCauseCategory(error) {
37171
+ switch (classifyNetworkFailure(error)) {
37172
+ case "network_dns_resolution_failed":
37173
+ return "dns";
37174
+ case "network_ssl_error":
37175
+ return "tls";
37176
+ case "network_connection_reset":
37177
+ return "reset";
37178
+ case "network_timeout":
37179
+ return "connect_timeout";
37180
+ default:
37181
+ return "unknown";
37182
+ }
37183
+ }
36441
37184
  function isNetworkFailure(error) {
36442
37185
  if (!(error instanceof Error)) return false;
36443
37186
  if (error instanceof DeeplineError && error.statusCode) return false;
@@ -36607,6 +37350,7 @@ function buildFailureReport(input2) {
36607
37350
  });
36608
37351
  const code = failureCode({ exitCode: input2.exitCode, error: input2.error });
36609
37352
  const errorBody = input2.error === void 0 ? `SDK CLI command exited ${input2.exitCode ?? "unknown"}` : errorMessage3(input2.error);
37353
+ const networkCauseCategory = input2.error !== void 0 && isNetworkFailure(input2.error) ? classifyNetworkCauseCategory(input2.error) : null;
36610
37354
  return {
36611
37355
  command: sanitizeCommand(input2.argv),
36612
37356
  subcommand: subcommandFromArgv(input2.argv),
@@ -36628,7 +37372,8 @@ function buildFailureReport(input2) {
36628
37372
  failure_code: code,
36629
37373
  failure_stage: input2.error === void 0 ? "command_exit" : "cli_main",
36630
37374
  duration_ms: durationMs,
36631
- ...input2.exitCode !== null ? { exit_code: input2.exitCode } : {}
37375
+ ...input2.exitCode !== null ? { exit_code: input2.exitCode } : {},
37376
+ ...networkCauseCategory ? { network_cause_category: networkCauseCategory } : {}
36632
37377
  }
36633
37378
  };
36634
37379
  }
@@ -38333,6 +39078,9 @@ Examples:
38333
39078
  });
38334
39079
  }
38335
39080
 
39081
+ // src/cli/commands/settings.ts
39082
+ import { readFileSync as readFileSync20 } from "fs";
39083
+
38336
39084
  // src/cli/update-preferences.ts
38337
39085
  import {
38338
39086
  existsSync as existsSync16,
@@ -40047,21 +40795,104 @@ function displaySlackTarget(target) {
40047
40795
  function collectEvent(value, previous = []) {
40048
40796
  return [...previous, value];
40049
40797
  }
40798
+ function collectNotificationTarget(value, previous = []) {
40799
+ return [...previous, value];
40800
+ }
40801
+ async function readNotificationConfigurationChange(source, read = (path, encoding) => readFileSync20(path, encoding)) {
40802
+ const label = source === "-" ? "stdin" : source;
40803
+ let text;
40804
+ try {
40805
+ text = await read(source === "-" ? 0 : source, "utf8");
40806
+ } catch (error) {
40807
+ throw new Error(
40808
+ `Could not read notification configuration from ${label}: ${error instanceof Error ? error.message : String(error)}`
40809
+ );
40810
+ }
40811
+ let parsed;
40812
+ try {
40813
+ parsed = JSON.parse(text);
40814
+ } catch (error) {
40815
+ throw new Error(
40816
+ `Notification configuration from ${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
40817
+ );
40818
+ }
40819
+ if (typeof parsed.expectedWorkspaceVersion !== "number" || !Number.isInteger(parsed.expectedWorkspaceVersion) || !parsed.change || typeof parsed.change !== "object") {
40820
+ throw new Error(
40821
+ `Notification configuration from ${label} must contain an integer expectedWorkspaceVersion and a change object.`
40822
+ );
40823
+ }
40824
+ return {
40825
+ expectedWorkspaceVersion: parsed.expectedWorkspaceVersion,
40826
+ change: parsed.change
40827
+ };
40828
+ }
40050
40829
  function registerNotificationCommands(program) {
40051
- const notifications = program.command("notifications").description("Choose which Play outcomes notify which people and channels.").addHelpText(
40830
+ const notifications = program.command("notifications").description("Configure notification groups, destinations, and rules.").addHelpText(
40052
40831
  "after",
40053
40832
  `
40054
40833
  Examples:
40834
+ deepline notifications add oncall --to email:oncall@example.com --to slack:#oncall
40055
40835
  deepline notifications events
40056
40836
  deepline notifications slack channels --search pipeline
40057
40837
  deepline notifications add pipeline-watchdog --to slack:#pipeline-alerts --for play.cron.failed
40058
40838
  deepline notifications add owner-alerts --to slack:member:U0123456789 --for play.cron.failed
40059
40839
  deepline notifications test pipeline-watchdog
40060
40840
 
40061
- Slack connections are managed in Dashboard \u2192 Integrations. This command only
40062
- chooses the connected Slack channel or member and the events it receives.
40841
+ Each named notification is a group. Repeat --to to give it email and Slack
40842
+ destinations. Add rules from the dashboard or notification configuration. The
40843
+ existing one-Slack-target form with --for remains supported and creates the
40844
+ equivalent compatibility group and rules. Slack connections are managed in
40845
+ Dashboard \u2192 Integrations.
40846
+ `
40847
+ );
40848
+ const configuration = notifications.command("configuration").description("Read or atomically update notification groups and rules.").addHelpText(
40849
+ "after",
40850
+ `
40851
+ Examples:
40852
+ deepline notifications configuration get --json
40853
+ deepline notifications configuration apply change.json --dry-run --json
40854
+ jq '{expectedWorkspaceVersion: .configuration.workspaceVersion, change: {kind: "delete_group", groupKey: "oncall"}}' current.json | deepline notifications configuration apply - --json
40063
40855
  `
40064
40856
  );
40857
+ configuration.command("get").description("Show the effective group-based notification configuration.").option("--json", "Emit JSON output").action(async (options) => {
40858
+ const result = await new DeeplineClient().getNotificationConfiguration();
40859
+ printCommandEnvelope(
40860
+ {
40861
+ ...result,
40862
+ render: {
40863
+ sections: [
40864
+ {
40865
+ title: "notification groups",
40866
+ lines: result.configuration.groups.length ? result.configuration.groups.map(
40867
+ (group) => `${group.name}: ${group.enabled ? "active" : "paused"}`
40868
+ ) : ["No notification groups are configured."]
40869
+ }
40870
+ ]
40871
+ }
40872
+ },
40873
+ { json: options.json }
40874
+ );
40875
+ });
40876
+ configuration.command("apply").argument(
40877
+ "<file>",
40878
+ "JSON file, or - for stdin, containing expectedWorkspaceVersion and one desired change"
40879
+ ).description(
40880
+ "Atomically apply a configuration draft exported by an agent or UI."
40881
+ ).option("--dry-run", "Validate and display the file without saving it").option("--json", "Emit JSON output").action(async (file, options) => {
40882
+ const input2 = await readNotificationConfigurationChange(file);
40883
+ if (options.dryRun) {
40884
+ printCommandEnvelope(
40885
+ { dryRun: true, ...input2 },
40886
+ { json: options.json }
40887
+ );
40888
+ return;
40889
+ }
40890
+ const result = await new DeeplineClient().changeNotificationConfiguration(
40891
+ input2.expectedWorkspaceVersion,
40892
+ input2.change
40893
+ );
40894
+ printCommandEnvelope(result, { json: options.json });
40895
+ });
40065
40896
  notifications.command("events").description("List the Play events available to a notification.").option("--json", "Emit JSON output").action(async (options) => {
40066
40897
  const settings = await new DeeplineClient().getNotifications();
40067
40898
  printCommandEnvelope(
@@ -40117,42 +40948,82 @@ chooses the connected Slack channel or member and the events it receives.
40117
40948
  );
40118
40949
  printCommandEnvelope({ notification: rule }, { json: options.json });
40119
40950
  });
40120
- notifications.command("add").argument("<name>", "Short stable name, for example pipeline-watchdog").requiredOption(
40951
+ notifications.command("add").argument("<name>", "Group name, for example oncall").requiredOption(
40121
40952
  "--to <target>",
40122
- "Target, for example slack:#pipeline-alerts or slack:member:U0123456789"
40953
+ "Target; repeat email:name@example.com, slack:#channel, or slack:member:U0123456789",
40954
+ collectNotificationTarget,
40955
+ []
40123
40956
  ).option(
40124
40957
  "--for <event>",
40125
- "Event ID; repeat for more events. Run notifications events to list them",
40958
+ "Legacy one-Slack-target event ID; repeat for more events",
40126
40959
  collectEvent,
40127
40960
  []
40128
40961
  ).option("--dry-run", "Describe the notification without saving it").option("--json", "Emit JSON output").action(
40129
40962
  async (name, options) => {
40130
- if (!options.for.length) {
40131
- throw new Error("Choose at least one event with --for <event>.");
40963
+ const result = await new DeeplineClient().createNotificationGroup({
40964
+ name,
40965
+ targets: options.to,
40966
+ legacyEvents: options.for,
40967
+ dryRun: options.dryRun
40968
+ });
40969
+ if (result.mode === "dry_run") {
40970
+ printCommandEnvelope(
40971
+ result.compatibilityMode ? {
40972
+ dryRun: true,
40973
+ name: result.name,
40974
+ target: options.to[0],
40975
+ eventTypes: result.legacyEvents
40976
+ } : {
40977
+ dryRun: true,
40978
+ group: result.name,
40979
+ targets: result.targets,
40980
+ compatibilityMode: false
40981
+ },
40982
+ { json: options.json }
40983
+ );
40984
+ return;
40132
40985
  }
40133
- const target = parseSlackTarget(options.to);
40134
- if (options.dryRun) {
40986
+ if (result.mode === "legacy") {
40135
40987
  printCommandEnvelope(
40136
- { dryRun: true, name, target: options.to, eventTypes: options.for },
40988
+ {
40989
+ notification: result.notification,
40990
+ render: {
40991
+ sections: [
40992
+ {
40993
+ title: "notification saved",
40994
+ lines: [name]
40995
+ }
40996
+ ],
40997
+ actions: [
40998
+ {
40999
+ label: "Send a test",
41000
+ command: `deepline notifications test ${name}`
41001
+ }
41002
+ ]
41003
+ }
41004
+ },
40137
41005
  { json: options.json }
40138
41006
  );
40139
41007
  return;
40140
41008
  }
40141
- const result = await new DeeplineClient().createNotification({
40142
- name,
40143
- provider: "slack",
40144
- ...target,
40145
- eventTypes: options.for
40146
- });
40147
41009
  printCommandEnvelope(
40148
41010
  {
40149
- notification: result,
41011
+ ...result,
40150
41012
  render: {
40151
- sections: [{ title: "notification saved", lines: [name] }],
41013
+ sections: [
41014
+ {
41015
+ title: "notification group saved",
41016
+ lines: [
41017
+ result.group.name,
41018
+ `${result.destinations.length} destination${result.destinations.length === 1 ? "" : "s"}`,
41019
+ "0 rules"
41020
+ ]
41021
+ }
41022
+ ],
40152
41023
  actions: [
40153
41024
  {
40154
- label: "Send a test",
40155
- command: `deepline notifications test ${name}`
41025
+ label: "View configuration",
41026
+ command: "deepline notifications configuration get"
40156
41027
  }
40157
41028
  ]
40158
41029
  }
@@ -40270,7 +41141,7 @@ import {
40270
41141
  chmodSync,
40271
41142
  existsSync as existsSync19,
40272
41143
  mkdtempSync,
40273
- readFileSync as readFileSync20,
41144
+ readFileSync as readFileSync21,
40274
41145
  writeFileSync as writeFileSync20
40275
41146
  } from "fs";
40276
41147
  import { tmpdir as tmpdir5 } from "os";
@@ -41139,6 +42010,9 @@ async function findPlayForToolId(client2, toolId) {
41139
42010
  const plays = await client2.searchPlays({ query: requested, compact: true });
41140
42011
  return plays.find((play) => playIdentifiers(play).includes(requested)) ?? null;
41141
42012
  }
42013
+ function isMissingToolMetadataError(error) {
42014
+ return error instanceof DeeplineError && error.statusCode === 404;
42015
+ }
41142
42016
  function playAliasToolErrorMessage(toolId, play) {
41143
42017
  const playName = play.reference ?? play.name;
41144
42018
  return `${toolId} is a play, not a tool.
@@ -41420,10 +42294,12 @@ async function getTool(toolId, options = {}) {
41420
42294
  try {
41421
42295
  tool = await client2.getTool(toolId);
41422
42296
  } catch (error) {
41423
- const play = await findPlayForToolId(client2, toolId);
41424
- if (play) {
41425
- printPlayAliasToolError(toolId, play);
41426
- return 2;
42297
+ if (isMissingToolMetadataError(error)) {
42298
+ const play = await findPlayForToolId(client2, toolId);
42299
+ if (play) {
42300
+ printPlayAliasToolError(toolId, play);
42301
+ return 2;
42302
+ }
41427
42303
  }
41428
42304
  throw error;
41429
42305
  }
@@ -42411,7 +43287,7 @@ function readJsonArgument(raw, flagName) {
42411
43287
  throw new Error(`Invalid ${flagName} value: empty @file path.`);
42412
43288
  }
42413
43289
  try {
42414
- return readFileSync20(resolveAtFilePath(filePath), "utf8").replace(
43290
+ return readFileSync21(resolveAtFilePath(filePath), "utf8").replace(
42415
43291
  /^\uFEFF/,
42416
43292
  ""
42417
43293
  );
@@ -42762,19 +43638,18 @@ async function executeTool(args) {
42762
43638
  try {
42763
43639
  metadata = await client2.getTool(parsed.toolId);
42764
43640
  } catch (error) {
42765
- const play = await findPlayForToolId(client2, parsed.toolId);
42766
- if (play) {
42767
- if (argsWantJson(args)) {
42768
- printJsonError(
42769
- new Error(playAliasToolErrorMessage(parsed.toolId, play))
42770
- );
42771
- } else {
42772
- printPlayAliasToolError(parsed.toolId, play);
43641
+ if (isMissingToolMetadataError(error)) {
43642
+ const play = await findPlayForToolId(client2, parsed.toolId);
43643
+ if (play) {
43644
+ if (argsWantJson(args)) {
43645
+ printJsonError(
43646
+ new Error(playAliasToolErrorMessage(parsed.toolId, play))
43647
+ );
43648
+ } else {
43649
+ printPlayAliasToolError(parsed.toolId, play);
43650
+ }
43651
+ return 2;
42773
43652
  }
42774
- return 2;
42775
- }
42776
- if (error instanceof DeeplineError) {
42777
- throw error;
42778
43653
  }
42779
43654
  throw error;
42780
43655
  }