deepline 0.1.220 → 0.1.221

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.
@@ -108,10 +108,10 @@ export const SDK_RELEASE = {
108
108
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
109
109
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
110
110
  // automatically without blocking their current command.
111
- version: '0.1.220',
111
+ version: '0.1.221',
112
112
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
113
113
  supportPolicy: {
114
- latest: '0.1.220',
114
+ latest: '0.1.221',
115
115
  minimumSupported: '0.1.53',
116
116
  deprecatedBelow: '0.1.219',
117
117
  commandMinimumSupported: [
@@ -997,6 +997,27 @@ export interface PlayCheckResult {
997
997
  summary?: string;
998
998
  artifactHash?: string | null;
999
999
  graphHash?: string | null;
1000
+ /** SHA-256 of the exact source bytes checked by Deepline. */
1001
+ sourceHash?: string | null;
1002
+ /** Enforceable byte budgets measured during cloud preflight. */
1003
+ limits?: {
1004
+ revisionStorage: {
1005
+ usedBytes: number;
1006
+ limitBytes: number;
1007
+ withinLimit: boolean;
1008
+ breakdown: {
1009
+ sourceCodeBytes: number;
1010
+ staticPipelineBytes: number;
1011
+ bindingsBytes: number;
1012
+ descriptionBytes: number;
1013
+ };
1014
+ };
1015
+ bundle: {
1016
+ usedBytes: number;
1017
+ limitBytes: number;
1018
+ withinLimit: boolean;
1019
+ };
1020
+ };
1000
1021
  }
1001
1022
 
1002
1023
  /** Severity of a {@link PlayCheckIssue}. `error` fails the check; `warning` does not. */
@@ -8741,6 +8741,7 @@ export class PlayContextImpl {
8741
8741
  const httpFailureAttempt = httpFailureAttempts.next({
8742
8742
  toolId,
8743
8743
  status: response.status,
8744
+ bodyText: text,
8744
8745
  transientHttpRetrySafe: retrySafeTransientHttp,
8745
8746
  });
8746
8747
  const failure = classifyToolExecuteHttpFailure({
@@ -5,6 +5,14 @@ import {
5
5
  } from './tool-http-errors';
6
6
 
7
7
  export const TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS = 2;
8
+ // A provider may need to finish the original request before it can replay the
9
+ // result for this key. With no Retry-After signal, these two polls give the
10
+ // original request ten seconds to finish without repeatedly hammering it.
11
+ export const TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS = [
12
+ 5_000, 5_000,
13
+ ] as const;
14
+ export const TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_MAX_ATTEMPTS =
15
+ TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS.length + 1;
8
16
  export const TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS = 8;
9
17
  export const TOOL_EXECUTE_BARE_RATE_LIMIT_MAX_ATTEMPTS = 2;
10
18
  export const TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS = 3;
@@ -13,6 +21,13 @@ export const TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS = 1_000;
13
21
  export const TOOL_EXECUTE_RETRY_DELAY_MAX_MS = 5_000;
14
22
  export const TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS = 60_000;
15
23
  export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
24
+ /**
25
+ * A provider/action-local outcome contract emits this only when a provider has
26
+ * explicitly said that the same idempotency key is still executing. It is not
27
+ * a generic HTTP 409 policy.
28
+ */
29
+ export const TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_CODE =
30
+ 'UPSTREAM_IDEMPOTENCY_IN_PROGRESS';
16
31
 
17
32
  export class ToolExecuteAuthScopeChangedError extends Error {
18
33
  readonly code = TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE;
@@ -29,6 +44,7 @@ export type ToolExecuteHttpRetryDecision = {
29
44
  attemptCap: number;
30
45
  reason:
31
46
  | 'rate_limit'
47
+ | 'idempotency_in_progress'
32
48
  | 'retry_safe_transient_5xx'
33
49
  | 'unsafe_transient_5xx'
34
50
  | 'hard_billing_error'
@@ -49,6 +65,7 @@ export type ToolExecuteHttpFailureAttemptTracker = {
49
65
  next(input: {
50
66
  toolId: string;
51
67
  status: number;
68
+ bodyText?: string;
52
69
  transientHttpRetrySafe?: boolean;
53
70
  }): number;
54
71
  };
@@ -65,6 +82,29 @@ function parseJsonObject(text: string): Record<string, unknown> | null {
65
82
  }
66
83
  }
67
84
 
85
+ function isIdempotencyInProgressResponse(input: {
86
+ status: number;
87
+ bodyText: string;
88
+ }): boolean {
89
+ if (input.status !== 409) return false;
90
+ const body = parseJsonObject(input.bodyText);
91
+ return (
92
+ body?.code === TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_CODE &&
93
+ body.error_category === 'idempotency_in_progress'
94
+ );
95
+ }
96
+
97
+ function idempotencyInProgressRetryDelayMs(attempt: number): number {
98
+ return (
99
+ TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS[
100
+ Math.min(
101
+ Math.max(0, attempt - 1),
102
+ TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS.length - 1,
103
+ )
104
+ ] ?? TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS
105
+ );
106
+ }
107
+
68
108
  export function parseToolExecuteAuthScopeChangedError(input: {
69
109
  status: number;
70
110
  bodyText: string;
@@ -84,6 +124,7 @@ export function parseToolExecuteAuthScopeChangedError(input: {
84
124
 
85
125
  function decideToolExecuteHttpRetry(input: {
86
126
  status: number;
127
+ idempotencyInProgress?: boolean;
87
128
  hardBillingFailure?: boolean;
88
129
  hasRetryAfterHeader?: boolean;
89
130
  transientHttpRetrySafe?: boolean;
@@ -110,6 +151,13 @@ function decideToolExecuteHttpRetry(input: {
110
151
  reason: 'rate_limit',
111
152
  };
112
153
  }
154
+ if (input.idempotencyInProgress) {
155
+ return {
156
+ retryable: true,
157
+ attemptCap: TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_MAX_ATTEMPTS,
158
+ reason: 'idempotency_in_progress',
159
+ };
160
+ }
113
161
  if (input.status >= 500 && input.status < 600) {
114
162
  if (!input.transientHttpRetrySafe) {
115
163
  return {
@@ -137,6 +185,7 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
137
185
  number
138
186
  > = {
139
187
  rate_limit: 0,
188
+ idempotency_in_progress: 0,
140
189
  retry_safe_transient_5xx: 0,
141
190
  unsafe_transient_5xx: 0,
142
191
  hard_billing_error: 0,
@@ -147,6 +196,10 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
147
196
  next(input) {
148
197
  const decision = decideToolExecuteHttpRetry({
149
198
  status: input.status,
199
+ idempotencyInProgress: isIdempotencyInProgressResponse({
200
+ status: input.status,
201
+ bodyText: input.bodyText ?? '',
202
+ }),
150
203
  hasRetryAfterHeader: true,
151
204
  transientHttpRetrySafe: input.transientHttpRetrySafe === true,
152
205
  });
@@ -186,8 +239,10 @@ export function classifyToolExecuteHttpFailure(input: {
186
239
  const hasRetryAfterHeader =
187
240
  typeof input.retryAfterHeader === 'string' &&
188
241
  input.retryAfterHeader.trim().length > 0;
242
+ const idempotencyInProgress = isIdempotencyInProgressResponse(input);
189
243
  const initialRetryDecision = decideToolExecuteHttpRetry({
190
244
  status: input.status,
245
+ idempotencyInProgress,
191
246
  hasRetryAfterHeader,
192
247
  transientHttpRetrySafe,
193
248
  });
@@ -208,6 +263,7 @@ export function classifyToolExecuteHttpFailure(input: {
208
263
  const hardBillingFailure = isHardBillingToolHttpError(error);
209
264
  const retryDecision = decideToolExecuteHttpRetry({
210
265
  status: input.status,
266
+ idempotencyInProgress,
211
267
  hardBillingFailure,
212
268
  hasRetryAfterHeader,
213
269
  transientHttpRetrySafe,
@@ -238,9 +294,12 @@ export function classifyToolExecuteHttpFailure(input: {
238
294
  TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS * input.attempt,
239
295
  ),
240
296
  )
241
- : retryAfterMs > 0
242
- ? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
243
- : TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
297
+ : retryDecision.reason === 'idempotency_in_progress' &&
298
+ !hasRetryAfterHeader
299
+ ? idempotencyInProgressRetryDelayMs(input.attempt)
300
+ : retryAfterMs > 0
301
+ ? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
302
+ : TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
244
303
  return {
245
304
  ...retryDecision,
246
305
  error,
package/dist/cli/index.js CHANGED
@@ -625,10 +625,10 @@ var SDK_RELEASE = {
625
625
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
626
626
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
627
627
  // automatically without blocking their current command.
628
- version: "0.1.220",
628
+ version: "0.1.221",
629
629
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
630
630
  supportPolicy: {
631
- latest: "0.1.220",
631
+ latest: "0.1.221",
632
632
  minimumSupported: "0.1.53",
633
633
  deprecatedBelow: "0.1.219",
634
634
  commandMinimumSupported: [
@@ -14648,6 +14648,73 @@ function parsePlayCheckOptions(args) {
14648
14648
  const jsonOutput = argsWantJson(args);
14649
14649
  return { target, jsonOutput };
14650
14650
  }
14651
+ function parsePlayPublishOptions(args) {
14652
+ const target = args[0];
14653
+ if (!target) {
14654
+ throw new Error(
14655
+ "Usage: deepline plays publish <play-file.ts|play-name> [--expected-artifact <hash>] [--dry-run] [--latest|--revision-id <id>] [--json]"
14656
+ );
14657
+ }
14658
+ let revisionId;
14659
+ let useLatest = false;
14660
+ let expectedArtifactHash;
14661
+ let dryRun = false;
14662
+ for (let index = 1; index < args.length; index += 1) {
14663
+ const arg = args[index];
14664
+ if (arg === "--revision-id") {
14665
+ revisionId = args[++index];
14666
+ if (!revisionId) throw new Error("--revision-id requires a revision id.");
14667
+ continue;
14668
+ }
14669
+ if (arg === "--latest") {
14670
+ useLatest = true;
14671
+ continue;
14672
+ }
14673
+ if (arg === "--expected-artifact") {
14674
+ expectedArtifactHash = args[++index];
14675
+ if (!expectedArtifactHash) {
14676
+ throw new Error("--expected-artifact requires an artifact hash.");
14677
+ }
14678
+ continue;
14679
+ }
14680
+ if (arg.startsWith("--expected-artifact=")) {
14681
+ expectedArtifactHash = arg.slice("--expected-artifact=".length);
14682
+ if (!expectedArtifactHash) {
14683
+ throw new Error("--expected-artifact requires an artifact hash.");
14684
+ }
14685
+ continue;
14686
+ }
14687
+ if (arg === "--dry-run") {
14688
+ dryRun = true;
14689
+ continue;
14690
+ }
14691
+ if (arg === "--json") continue;
14692
+ throw new Error(`Unknown plays publish option: ${arg}`);
14693
+ }
14694
+ if (revisionId && useLatest) {
14695
+ throw new Error("Choose only one live target: --latest or --revision-id.");
14696
+ }
14697
+ return {
14698
+ target,
14699
+ revisionId,
14700
+ useLatest,
14701
+ expectedArtifactHash,
14702
+ dryRun,
14703
+ jsonOutput: argsWantJson(args)
14704
+ };
14705
+ }
14706
+ function formatByteBudget(usedBytes, limitBytes) {
14707
+ return `${usedBytes.toLocaleString()} B / ${limitBytes.toLocaleString()} B`;
14708
+ }
14709
+ function printPlayCheckLimits(limits) {
14710
+ if (!limits) return;
14711
+ console.log(
14712
+ ` revision storage: ${formatByteBudget(limits.revisionStorage.usedBytes, limits.revisionStorage.limitBytes)}`
14713
+ );
14714
+ console.log(
14715
+ ` bundle: ${formatByteBudget(limits.bundle.usedBytes, limits.bundle.limitBytes)}`
14716
+ );
14717
+ }
14651
14718
  function shouldUseLocalOnlyPlayCheck() {
14652
14719
  const value = process.env.DEEPLINE_PLAY_CHECK_LOCAL_ONLY?.trim().toLowerCase();
14653
14720
  return value === "1" || value === "true" || value === "yes" || value === "on";
@@ -14960,7 +15027,11 @@ async function handlePlayCheck(args) {
14960
15027
  );
14961
15028
  } else {
14962
15029
  console.log(`\u2713 ${playName} passed local play check`);
15030
+ console.log(` source: ${graph.root.artifact.sourceHash.slice(0, 12)}`);
14963
15031
  console.log(` artifact: ${result2.artifactHash.slice(0, 12)}`);
15032
+ console.log(
15033
+ ` publish: deepline plays publish ${shellQuote2(options.target)} --expected-artifact ${result2.artifactHash}`
15034
+ );
14964
15035
  printToolGetterHints(result2.toolGetterHints);
14965
15036
  }
14966
15037
  return 0;
@@ -15002,6 +15073,15 @@ async function handlePlayCheck(args) {
15002
15073
  if (enrichedResult.artifactHash) {
15003
15074
  console.log(` artifact: ${enrichedResult.artifactHash.slice(0, 12)}`);
15004
15075
  }
15076
+ if (enrichedResult.sourceHash) {
15077
+ console.log(` source: ${enrichedResult.sourceHash.slice(0, 12)}`);
15078
+ }
15079
+ printPlayCheckLimits(enrichedResult.limits);
15080
+ if (enrichedResult.artifactHash) {
15081
+ console.log(
15082
+ ` publish: deepline plays publish ${shellQuote2(options.target)} --expected-artifact ${enrichedResult.artifactHash}`
15083
+ );
15084
+ }
15005
15085
  printPlayTriggers(enrichedResult.triggers);
15006
15086
  printRecognizedSummary(enrichedResult.recognized);
15007
15087
  printPlayCheckIssues(enrichedResult.issues, (line) => console.log(line));
@@ -16358,28 +16438,15 @@ async function handlePlayDescribe(args) {
16358
16438
  return 0;
16359
16439
  }
16360
16440
  async function handlePlayPublish(args) {
16361
- const playName = args[0];
16362
- if (!playName) {
16363
- console.error(
16364
- "Usage: deepline plays publish <play-file.ts|play-name> [--latest|--revision-id <id>] [--json]"
16365
- );
16366
- return 1;
16367
- }
16368
- let revisionId;
16369
- let useLatest = false;
16370
- for (let index = 1; index < args.length; index += 1) {
16371
- const arg = args[index];
16372
- if (arg === "--revision-id" && args[index + 1]) {
16373
- revisionId = args[++index];
16374
- }
16375
- if (arg === "--latest") {
16376
- useLatest = true;
16377
- }
16378
- }
16379
- if (revisionId && useLatest) {
16380
- console.error("Choose only one live target: --latest or --revision-id.");
16381
- return 1;
16441
+ let options;
16442
+ try {
16443
+ options = parsePlayPublishOptions(args);
16444
+ } catch (error) {
16445
+ console.error(error instanceof Error ? error.message : String(error));
16446
+ return 2;
16382
16447
  }
16448
+ const { target: playName, useLatest } = options;
16449
+ let revisionId = options.revisionId;
16383
16450
  const client2 = new DeeplineClient();
16384
16451
  if (isFileTarget(playName)) {
16385
16452
  if (revisionId || useLatest) {
@@ -16396,6 +16463,66 @@ async function handlePlayPublish(args) {
16396
16463
  () => collectBundledPlayGraph((0, import_node_path11.resolve)(playName))
16397
16464
  );
16398
16465
  assertBundledPlayGraphDescriptions(graph);
16466
+ } catch (error) {
16467
+ console.error(error instanceof Error ? error.message : String(error));
16468
+ return 1;
16469
+ }
16470
+ const rootPlayName = graph.root.playName ?? extractPlayName(graph.root.sourceCode, graph.root.filePath);
16471
+ const artifactHash = graph.root.artifact.artifactHash;
16472
+ if (options.expectedArtifactHash && options.expectedArtifactHash !== artifactHash) {
16473
+ const result3 = {
16474
+ ok: false,
16475
+ code: "PLAY_ARTIFACT_HASH_MISMATCH",
16476
+ message: "Refusing to publish: the local file no longer produces the artifact checked earlier.",
16477
+ expectedArtifactHash: options.expectedArtifactHash,
16478
+ currentArtifactHash: artifactHash,
16479
+ next: `deepline plays check ${shellQuote2(playName)}`
16480
+ };
16481
+ if (options.jsonOutput) {
16482
+ process.stdout.write(`${JSON.stringify(result3)}
16483
+ `);
16484
+ } else {
16485
+ console.error(result3.message);
16486
+ console.error(` checked artifact: ${result3.expectedArtifactHash}`);
16487
+ console.error(` current artifact: ${result3.currentArtifactHash}`);
16488
+ console.error(` run: ${result3.next}`);
16489
+ }
16490
+ return 7;
16491
+ }
16492
+ const dryRun = {
16493
+ ok: true,
16494
+ dryRun: true,
16495
+ name: rootPlayName,
16496
+ sourcePath: graph.root.filePath,
16497
+ sourceHash: graph.root.artifact.sourceHash,
16498
+ artifactHash,
16499
+ graphHash: graph.root.artifact.graphHash,
16500
+ importedPlayCount: Math.max(0, graph.nodes.size - 1),
16501
+ plannedMutation: "register revision and promote it live",
16502
+ expectedArtifactHash: options.expectedArtifactHash ?? null
16503
+ };
16504
+ if (options.dryRun) {
16505
+ if (options.jsonOutput) {
16506
+ process.stdout.write(`${JSON.stringify(dryRun)}
16507
+ `);
16508
+ } else {
16509
+ console.log(`Dry run: ${rootPlayName}`);
16510
+ console.log(` source: ${dryRun.sourceHash}`);
16511
+ console.log(` artifact: ${dryRun.artifactHash}`);
16512
+ console.log(` would: ${dryRun.plannedMutation}`);
16513
+ }
16514
+ return 0;
16515
+ }
16516
+ let previousLive = null;
16517
+ try {
16518
+ previousLive = (await client2.getPlay(rootPlayName)).play.liveRevision ?? null;
16519
+ } catch (error) {
16520
+ if (!(error instanceof DeeplineError) || error.statusCode !== 404) {
16521
+ console.error(error instanceof Error ? error.message : String(error));
16522
+ return 5;
16523
+ }
16524
+ }
16525
+ try {
16399
16526
  await traceCliSpan(
16400
16527
  "cli.play_publish_compile_manifests",
16401
16528
  { targetKind: "file", nodeCount: graph.nodes.size },
@@ -16410,13 +16537,12 @@ async function handlePlayPublish(args) {
16410
16537
  console.error(error instanceof Error ? error.message : String(error));
16411
16538
  return 1;
16412
16539
  }
16413
- const rootPlayName = graph.root.playName ?? extractPlayName(graph.root.sourceCode, graph.root.filePath);
16414
16540
  const published = await traceCliSpan(
16415
16541
  "cli.play_publish_register_root",
16416
16542
  {
16417
16543
  targetKind: "file",
16418
16544
  playName: rootPlayName,
16419
- artifactHash: graph.root.artifact.artifactHash
16545
+ artifactHash
16420
16546
  },
16421
16547
  () => client2.registerPlayArtifact({
16422
16548
  name: rootPlayName,
@@ -16428,17 +16554,63 @@ async function handlePlayPublish(args) {
16428
16554
  publish: true
16429
16555
  })
16430
16556
  );
16431
- process.stdout.write(
16432
- `${JSON.stringify({
16433
- success: true,
16434
- name: rootPlayName,
16435
- liveVersion: published.version ?? null,
16436
- revisionId: published.revisionId ?? null,
16437
- triggerMetadata: published.triggerMetadata ?? null,
16438
- triggerBindings: published.triggerBindings ?? []
16439
- })}
16440
- `
16441
- );
16557
+ const result2 = {
16558
+ success: true,
16559
+ name: rootPlayName,
16560
+ sourcePath: graph.root.filePath,
16561
+ sourceHash: graph.root.artifact.sourceHash,
16562
+ artifactHash,
16563
+ liveVersion: published.version ?? null,
16564
+ revisionId: published.revisionId ?? null,
16565
+ previousLiveVersion: previousLive?.version ?? null,
16566
+ previousArtifactHash: previousLive?.artifactHash ?? null,
16567
+ sourceBytes: {
16568
+ before: previousLive?.sourceCode ? Buffer.byteLength(previousLive.sourceCode, "utf8") : null,
16569
+ after: Buffer.byteLength(graph.root.sourceCode, "utf8")
16570
+ },
16571
+ triggerMetadata: published.triggerMetadata ?? null,
16572
+ triggerBindings: published.triggerBindings ?? []
16573
+ };
16574
+ if (options.jsonOutput) {
16575
+ process.stdout.write(`${JSON.stringify(result2)}
16576
+ `);
16577
+ } else {
16578
+ console.log(
16579
+ `\u2713 Published ${rootPlayName} as v${result2.liveVersion ?? "?"}`
16580
+ );
16581
+ console.log(` source: ${result2.sourceHash.slice(0, 12)}`);
16582
+ console.log(` artifact: ${result2.artifactHash.slice(0, 12)}`);
16583
+ if (result2.previousLiveVersion !== null) {
16584
+ console.log(
16585
+ ` previous live: v${result2.previousLiveVersion} (${result2.previousArtifactHash?.slice(0, 12) ?? "unknown artifact"})`
16586
+ );
16587
+ }
16588
+ if (result2.sourceBytes.before !== null) {
16589
+ console.log(
16590
+ ` source bytes: ${result2.sourceBytes.before.toLocaleString()} \u2192 ${result2.sourceBytes.after.toLocaleString()}`
16591
+ );
16592
+ }
16593
+ console.log(
16594
+ ` revisions: deepline plays versions --name ${rootPlayName}`
16595
+ );
16596
+ }
16597
+ return 0;
16598
+ }
16599
+ if (options.expectedArtifactHash) {
16600
+ console.error("--expected-artifact only applies to a local play file.");
16601
+ return 2;
16602
+ }
16603
+ if (options.dryRun) {
16604
+ const result2 = {
16605
+ ok: true,
16606
+ dryRun: true,
16607
+ name: playName,
16608
+ plannedMutation: "promote a saved revision live",
16609
+ revisionId: revisionId ?? null,
16610
+ latest: useLatest
16611
+ };
16612
+ process.stdout.write(`${JSON.stringify(result2)}
16613
+ `);
16442
16614
  return 0;
16443
16615
  }
16444
16616
  const resolvedName = parseReferencedPlayTarget2(playName).playName;
@@ -16802,7 +16974,9 @@ Notes:
16802
16974
  Mutates cloud state. For a saved play, --latest or --revision-id promotes an
16803
16975
  existing saved revision live.
16804
16976
  Local .play.ts publishing bundles the file, validates it, saves a revision,
16805
- and promotes that revision live.
16977
+ and promotes that revision live. Use --expected-artifact with the hash from
16978
+ plays check to refuse publishing bytes that changed after validation.
16979
+ --dry-run bundles only and prints the exact artifact that would be published.
16806
16980
  Running a local file with plays run does not publish it.
16807
16981
 
16808
16982
  Examples:
@@ -16810,27 +16984,40 @@ Examples:
16810
16984
  deepline plays set-live my-play --revision-id <revision-id> --json
16811
16985
  deepline plays set-live my.play.ts --json
16812
16986
  deepline plays publish my.play.ts --json
16987
+ deepline plays check my.play.ts
16988
+ deepline plays publish my.play.ts --expected-artifact <artifact-hash>
16989
+ deepline plays publish my.play.ts --dry-run --json
16813
16990
  `
16814
16991
  );
16815
16992
  addPublishHelp(
16816
16993
  play.command("publish <target>").description(
16817
16994
  "Promote a saved play revision or publish a local play file."
16818
16995
  )
16819
- ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
16996
+ ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option(
16997
+ "--expected-artifact <hash>",
16998
+ "Require the local file to produce this checked artifact hash"
16999
+ ).option("--dry-run", "Bundle and show the planned publish without mutation").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
16820
17000
  process.exitCode = await handlePlayPublish([
16821
17001
  target,
16822
17002
  ...options.latest ? ["--latest"] : [],
16823
17003
  ...options.revisionId ? ["--revision-id", options.revisionId] : [],
17004
+ ...options.expectedArtifact ? ["--expected-artifact", options.expectedArtifact] : [],
17005
+ ...options.dryRun ? ["--dry-run"] : [],
16824
17006
  ...options.json ? ["--json"] : []
16825
17007
  ]);
16826
17008
  });
16827
17009
  addPublishHelp(
16828
17010
  play.command("set-live <target>").description("Promote a saved revision or publish a local play file.")
16829
- ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
17011
+ ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option(
17012
+ "--expected-artifact <hash>",
17013
+ "Require the local file to produce this checked artifact hash"
17014
+ ).option("--dry-run", "Bundle and show the planned publish without mutation").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
16830
17015
  process.exitCode = await handlePlayPublish([
16831
17016
  target,
16832
17017
  ...options.latest ? ["--latest"] : [],
16833
17018
  ...options.revisionId ? ["--revision-id", options.revisionId] : [],
17019
+ ...options.expectedArtifact ? ["--expected-artifact", options.expectedArtifact] : [],
17020
+ ...options.dryRun ? ["--dry-run"] : [],
16834
17021
  ...options.json ? ["--json"] : []
16835
17022
  ]);
16836
17023
  });
@@ -610,10 +610,10 @@ var SDK_RELEASE = {
610
610
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
611
611
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
612
612
  // automatically without blocking their current command.
613
- version: "0.1.220",
613
+ version: "0.1.221",
614
614
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
615
615
  supportPolicy: {
616
- latest: "0.1.220",
616
+ latest: "0.1.221",
617
617
  minimumSupported: "0.1.53",
618
618
  deprecatedBelow: "0.1.219",
619
619
  commandMinimumSupported: [
@@ -14677,6 +14677,73 @@ function parsePlayCheckOptions(args) {
14677
14677
  const jsonOutput = argsWantJson(args);
14678
14678
  return { target, jsonOutput };
14679
14679
  }
14680
+ function parsePlayPublishOptions(args) {
14681
+ const target = args[0];
14682
+ if (!target) {
14683
+ throw new Error(
14684
+ "Usage: deepline plays publish <play-file.ts|play-name> [--expected-artifact <hash>] [--dry-run] [--latest|--revision-id <id>] [--json]"
14685
+ );
14686
+ }
14687
+ let revisionId;
14688
+ let useLatest = false;
14689
+ let expectedArtifactHash;
14690
+ let dryRun = false;
14691
+ for (let index = 1; index < args.length; index += 1) {
14692
+ const arg = args[index];
14693
+ if (arg === "--revision-id") {
14694
+ revisionId = args[++index];
14695
+ if (!revisionId) throw new Error("--revision-id requires a revision id.");
14696
+ continue;
14697
+ }
14698
+ if (arg === "--latest") {
14699
+ useLatest = true;
14700
+ continue;
14701
+ }
14702
+ if (arg === "--expected-artifact") {
14703
+ expectedArtifactHash = args[++index];
14704
+ if (!expectedArtifactHash) {
14705
+ throw new Error("--expected-artifact requires an artifact hash.");
14706
+ }
14707
+ continue;
14708
+ }
14709
+ if (arg.startsWith("--expected-artifact=")) {
14710
+ expectedArtifactHash = arg.slice("--expected-artifact=".length);
14711
+ if (!expectedArtifactHash) {
14712
+ throw new Error("--expected-artifact requires an artifact hash.");
14713
+ }
14714
+ continue;
14715
+ }
14716
+ if (arg === "--dry-run") {
14717
+ dryRun = true;
14718
+ continue;
14719
+ }
14720
+ if (arg === "--json") continue;
14721
+ throw new Error(`Unknown plays publish option: ${arg}`);
14722
+ }
14723
+ if (revisionId && useLatest) {
14724
+ throw new Error("Choose only one live target: --latest or --revision-id.");
14725
+ }
14726
+ return {
14727
+ target,
14728
+ revisionId,
14729
+ useLatest,
14730
+ expectedArtifactHash,
14731
+ dryRun,
14732
+ jsonOutput: argsWantJson(args)
14733
+ };
14734
+ }
14735
+ function formatByteBudget(usedBytes, limitBytes) {
14736
+ return `${usedBytes.toLocaleString()} B / ${limitBytes.toLocaleString()} B`;
14737
+ }
14738
+ function printPlayCheckLimits(limits) {
14739
+ if (!limits) return;
14740
+ console.log(
14741
+ ` revision storage: ${formatByteBudget(limits.revisionStorage.usedBytes, limits.revisionStorage.limitBytes)}`
14742
+ );
14743
+ console.log(
14744
+ ` bundle: ${formatByteBudget(limits.bundle.usedBytes, limits.bundle.limitBytes)}`
14745
+ );
14746
+ }
14680
14747
  function shouldUseLocalOnlyPlayCheck() {
14681
14748
  const value = process.env.DEEPLINE_PLAY_CHECK_LOCAL_ONLY?.trim().toLowerCase();
14682
14749
  return value === "1" || value === "true" || value === "yes" || value === "on";
@@ -14989,7 +15056,11 @@ async function handlePlayCheck(args) {
14989
15056
  );
14990
15057
  } else {
14991
15058
  console.log(`\u2713 ${playName} passed local play check`);
15059
+ console.log(` source: ${graph.root.artifact.sourceHash.slice(0, 12)}`);
14992
15060
  console.log(` artifact: ${result2.artifactHash.slice(0, 12)}`);
15061
+ console.log(
15062
+ ` publish: deepline plays publish ${shellQuote2(options.target)} --expected-artifact ${result2.artifactHash}`
15063
+ );
14993
15064
  printToolGetterHints(result2.toolGetterHints);
14994
15065
  }
14995
15066
  return 0;
@@ -15031,6 +15102,15 @@ async function handlePlayCheck(args) {
15031
15102
  if (enrichedResult.artifactHash) {
15032
15103
  console.log(` artifact: ${enrichedResult.artifactHash.slice(0, 12)}`);
15033
15104
  }
15105
+ if (enrichedResult.sourceHash) {
15106
+ console.log(` source: ${enrichedResult.sourceHash.slice(0, 12)}`);
15107
+ }
15108
+ printPlayCheckLimits(enrichedResult.limits);
15109
+ if (enrichedResult.artifactHash) {
15110
+ console.log(
15111
+ ` publish: deepline plays publish ${shellQuote2(options.target)} --expected-artifact ${enrichedResult.artifactHash}`
15112
+ );
15113
+ }
15034
15114
  printPlayTriggers(enrichedResult.triggers);
15035
15115
  printRecognizedSummary(enrichedResult.recognized);
15036
15116
  printPlayCheckIssues(enrichedResult.issues, (line) => console.log(line));
@@ -16387,28 +16467,15 @@ async function handlePlayDescribe(args) {
16387
16467
  return 0;
16388
16468
  }
16389
16469
  async function handlePlayPublish(args) {
16390
- const playName = args[0];
16391
- if (!playName) {
16392
- console.error(
16393
- "Usage: deepline plays publish <play-file.ts|play-name> [--latest|--revision-id <id>] [--json]"
16394
- );
16395
- return 1;
16396
- }
16397
- let revisionId;
16398
- let useLatest = false;
16399
- for (let index = 1; index < args.length; index += 1) {
16400
- const arg = args[index];
16401
- if (arg === "--revision-id" && args[index + 1]) {
16402
- revisionId = args[++index];
16403
- }
16404
- if (arg === "--latest") {
16405
- useLatest = true;
16406
- }
16407
- }
16408
- if (revisionId && useLatest) {
16409
- console.error("Choose only one live target: --latest or --revision-id.");
16410
- return 1;
16470
+ let options;
16471
+ try {
16472
+ options = parsePlayPublishOptions(args);
16473
+ } catch (error) {
16474
+ console.error(error instanceof Error ? error.message : String(error));
16475
+ return 2;
16411
16476
  }
16477
+ const { target: playName, useLatest } = options;
16478
+ let revisionId = options.revisionId;
16412
16479
  const client2 = new DeeplineClient();
16413
16480
  if (isFileTarget(playName)) {
16414
16481
  if (revisionId || useLatest) {
@@ -16425,6 +16492,66 @@ async function handlePlayPublish(args) {
16425
16492
  () => collectBundledPlayGraph(resolve8(playName))
16426
16493
  );
16427
16494
  assertBundledPlayGraphDescriptions(graph);
16495
+ } catch (error) {
16496
+ console.error(error instanceof Error ? error.message : String(error));
16497
+ return 1;
16498
+ }
16499
+ const rootPlayName = graph.root.playName ?? extractPlayName(graph.root.sourceCode, graph.root.filePath);
16500
+ const artifactHash = graph.root.artifact.artifactHash;
16501
+ if (options.expectedArtifactHash && options.expectedArtifactHash !== artifactHash) {
16502
+ const result3 = {
16503
+ ok: false,
16504
+ code: "PLAY_ARTIFACT_HASH_MISMATCH",
16505
+ message: "Refusing to publish: the local file no longer produces the artifact checked earlier.",
16506
+ expectedArtifactHash: options.expectedArtifactHash,
16507
+ currentArtifactHash: artifactHash,
16508
+ next: `deepline plays check ${shellQuote2(playName)}`
16509
+ };
16510
+ if (options.jsonOutput) {
16511
+ process.stdout.write(`${JSON.stringify(result3)}
16512
+ `);
16513
+ } else {
16514
+ console.error(result3.message);
16515
+ console.error(` checked artifact: ${result3.expectedArtifactHash}`);
16516
+ console.error(` current artifact: ${result3.currentArtifactHash}`);
16517
+ console.error(` run: ${result3.next}`);
16518
+ }
16519
+ return 7;
16520
+ }
16521
+ const dryRun = {
16522
+ ok: true,
16523
+ dryRun: true,
16524
+ name: rootPlayName,
16525
+ sourcePath: graph.root.filePath,
16526
+ sourceHash: graph.root.artifact.sourceHash,
16527
+ artifactHash,
16528
+ graphHash: graph.root.artifact.graphHash,
16529
+ importedPlayCount: Math.max(0, graph.nodes.size - 1),
16530
+ plannedMutation: "register revision and promote it live",
16531
+ expectedArtifactHash: options.expectedArtifactHash ?? null
16532
+ };
16533
+ if (options.dryRun) {
16534
+ if (options.jsonOutput) {
16535
+ process.stdout.write(`${JSON.stringify(dryRun)}
16536
+ `);
16537
+ } else {
16538
+ console.log(`Dry run: ${rootPlayName}`);
16539
+ console.log(` source: ${dryRun.sourceHash}`);
16540
+ console.log(` artifact: ${dryRun.artifactHash}`);
16541
+ console.log(` would: ${dryRun.plannedMutation}`);
16542
+ }
16543
+ return 0;
16544
+ }
16545
+ let previousLive = null;
16546
+ try {
16547
+ previousLive = (await client2.getPlay(rootPlayName)).play.liveRevision ?? null;
16548
+ } catch (error) {
16549
+ if (!(error instanceof DeeplineError) || error.statusCode !== 404) {
16550
+ console.error(error instanceof Error ? error.message : String(error));
16551
+ return 5;
16552
+ }
16553
+ }
16554
+ try {
16428
16555
  await traceCliSpan(
16429
16556
  "cli.play_publish_compile_manifests",
16430
16557
  { targetKind: "file", nodeCount: graph.nodes.size },
@@ -16439,13 +16566,12 @@ async function handlePlayPublish(args) {
16439
16566
  console.error(error instanceof Error ? error.message : String(error));
16440
16567
  return 1;
16441
16568
  }
16442
- const rootPlayName = graph.root.playName ?? extractPlayName(graph.root.sourceCode, graph.root.filePath);
16443
16569
  const published = await traceCliSpan(
16444
16570
  "cli.play_publish_register_root",
16445
16571
  {
16446
16572
  targetKind: "file",
16447
16573
  playName: rootPlayName,
16448
- artifactHash: graph.root.artifact.artifactHash
16574
+ artifactHash
16449
16575
  },
16450
16576
  () => client2.registerPlayArtifact({
16451
16577
  name: rootPlayName,
@@ -16457,17 +16583,63 @@ async function handlePlayPublish(args) {
16457
16583
  publish: true
16458
16584
  })
16459
16585
  );
16460
- process.stdout.write(
16461
- `${JSON.stringify({
16462
- success: true,
16463
- name: rootPlayName,
16464
- liveVersion: published.version ?? null,
16465
- revisionId: published.revisionId ?? null,
16466
- triggerMetadata: published.triggerMetadata ?? null,
16467
- triggerBindings: published.triggerBindings ?? []
16468
- })}
16469
- `
16470
- );
16586
+ const result2 = {
16587
+ success: true,
16588
+ name: rootPlayName,
16589
+ sourcePath: graph.root.filePath,
16590
+ sourceHash: graph.root.artifact.sourceHash,
16591
+ artifactHash,
16592
+ liveVersion: published.version ?? null,
16593
+ revisionId: published.revisionId ?? null,
16594
+ previousLiveVersion: previousLive?.version ?? null,
16595
+ previousArtifactHash: previousLive?.artifactHash ?? null,
16596
+ sourceBytes: {
16597
+ before: previousLive?.sourceCode ? Buffer.byteLength(previousLive.sourceCode, "utf8") : null,
16598
+ after: Buffer.byteLength(graph.root.sourceCode, "utf8")
16599
+ },
16600
+ triggerMetadata: published.triggerMetadata ?? null,
16601
+ triggerBindings: published.triggerBindings ?? []
16602
+ };
16603
+ if (options.jsonOutput) {
16604
+ process.stdout.write(`${JSON.stringify(result2)}
16605
+ `);
16606
+ } else {
16607
+ console.log(
16608
+ `\u2713 Published ${rootPlayName} as v${result2.liveVersion ?? "?"}`
16609
+ );
16610
+ console.log(` source: ${result2.sourceHash.slice(0, 12)}`);
16611
+ console.log(` artifact: ${result2.artifactHash.slice(0, 12)}`);
16612
+ if (result2.previousLiveVersion !== null) {
16613
+ console.log(
16614
+ ` previous live: v${result2.previousLiveVersion} (${result2.previousArtifactHash?.slice(0, 12) ?? "unknown artifact"})`
16615
+ );
16616
+ }
16617
+ if (result2.sourceBytes.before !== null) {
16618
+ console.log(
16619
+ ` source bytes: ${result2.sourceBytes.before.toLocaleString()} \u2192 ${result2.sourceBytes.after.toLocaleString()}`
16620
+ );
16621
+ }
16622
+ console.log(
16623
+ ` revisions: deepline plays versions --name ${rootPlayName}`
16624
+ );
16625
+ }
16626
+ return 0;
16627
+ }
16628
+ if (options.expectedArtifactHash) {
16629
+ console.error("--expected-artifact only applies to a local play file.");
16630
+ return 2;
16631
+ }
16632
+ if (options.dryRun) {
16633
+ const result2 = {
16634
+ ok: true,
16635
+ dryRun: true,
16636
+ name: playName,
16637
+ plannedMutation: "promote a saved revision live",
16638
+ revisionId: revisionId ?? null,
16639
+ latest: useLatest
16640
+ };
16641
+ process.stdout.write(`${JSON.stringify(result2)}
16642
+ `);
16471
16643
  return 0;
16472
16644
  }
16473
16645
  const resolvedName = parseReferencedPlayTarget2(playName).playName;
@@ -16831,7 +17003,9 @@ Notes:
16831
17003
  Mutates cloud state. For a saved play, --latest or --revision-id promotes an
16832
17004
  existing saved revision live.
16833
17005
  Local .play.ts publishing bundles the file, validates it, saves a revision,
16834
- and promotes that revision live.
17006
+ and promotes that revision live. Use --expected-artifact with the hash from
17007
+ plays check to refuse publishing bytes that changed after validation.
17008
+ --dry-run bundles only and prints the exact artifact that would be published.
16835
17009
  Running a local file with plays run does not publish it.
16836
17010
 
16837
17011
  Examples:
@@ -16839,27 +17013,40 @@ Examples:
16839
17013
  deepline plays set-live my-play --revision-id <revision-id> --json
16840
17014
  deepline plays set-live my.play.ts --json
16841
17015
  deepline plays publish my.play.ts --json
17016
+ deepline plays check my.play.ts
17017
+ deepline plays publish my.play.ts --expected-artifact <artifact-hash>
17018
+ deepline plays publish my.play.ts --dry-run --json
16842
17019
  `
16843
17020
  );
16844
17021
  addPublishHelp(
16845
17022
  play.command("publish <target>").description(
16846
17023
  "Promote a saved play revision or publish a local play file."
16847
17024
  )
16848
- ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
17025
+ ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option(
17026
+ "--expected-artifact <hash>",
17027
+ "Require the local file to produce this checked artifact hash"
17028
+ ).option("--dry-run", "Bundle and show the planned publish without mutation").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
16849
17029
  process.exitCode = await handlePlayPublish([
16850
17030
  target,
16851
17031
  ...options.latest ? ["--latest"] : [],
16852
17032
  ...options.revisionId ? ["--revision-id", options.revisionId] : [],
17033
+ ...options.expectedArtifact ? ["--expected-artifact", options.expectedArtifact] : [],
17034
+ ...options.dryRun ? ["--dry-run"] : [],
16853
17035
  ...options.json ? ["--json"] : []
16854
17036
  ]);
16855
17037
  });
16856
17038
  addPublishHelp(
16857
17039
  play.command("set-live <target>").description("Promote a saved revision or publish a local play file.")
16858
- ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
17040
+ ).option("--latest", "Promote the newest saved revision").option("--revision-id <id>", "Revision to promote").option(
17041
+ "--expected-artifact <hash>",
17042
+ "Require the local file to produce this checked artifact hash"
17043
+ ).option("--dry-run", "Bundle and show the planned publish without mutation").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (target, options) => {
16859
17044
  process.exitCode = await handlePlayPublish([
16860
17045
  target,
16861
17046
  ...options.latest ? ["--latest"] : [],
16862
17047
  ...options.revisionId ? ["--revision-id", options.revisionId] : [],
17048
+ ...options.expectedArtifact ? ["--expected-artifact", options.expectedArtifact] : [],
17049
+ ...options.dryRun ? ["--dry-run"] : [],
16863
17050
  ...options.json ? ["--json"] : []
16864
17051
  ]);
16865
17052
  });
package/dist/index.d.mts CHANGED
@@ -945,6 +945,27 @@ interface PlayCheckResult {
945
945
  summary?: string;
946
946
  artifactHash?: string | null;
947
947
  graphHash?: string | null;
948
+ /** SHA-256 of the exact source bytes checked by Deepline. */
949
+ sourceHash?: string | null;
950
+ /** Enforceable byte budgets measured during cloud preflight. */
951
+ limits?: {
952
+ revisionStorage: {
953
+ usedBytes: number;
954
+ limitBytes: number;
955
+ withinLimit: boolean;
956
+ breakdown: {
957
+ sourceCodeBytes: number;
958
+ staticPipelineBytes: number;
959
+ bindingsBytes: number;
960
+ descriptionBytes: number;
961
+ };
962
+ };
963
+ bundle: {
964
+ usedBytes: number;
965
+ limitBytes: number;
966
+ withinLimit: boolean;
967
+ };
968
+ };
948
969
  }
949
970
  /** Severity of a {@link PlayCheckIssue}. `error` fails the check; `warning` does not. */
950
971
  type PlayCheckIssueSeverity = 'error' | 'warning';
package/dist/index.d.ts CHANGED
@@ -945,6 +945,27 @@ interface PlayCheckResult {
945
945
  summary?: string;
946
946
  artifactHash?: string | null;
947
947
  graphHash?: string | null;
948
+ /** SHA-256 of the exact source bytes checked by Deepline. */
949
+ sourceHash?: string | null;
950
+ /** Enforceable byte budgets measured during cloud preflight. */
951
+ limits?: {
952
+ revisionStorage: {
953
+ usedBytes: number;
954
+ limitBytes: number;
955
+ withinLimit: boolean;
956
+ breakdown: {
957
+ sourceCodeBytes: number;
958
+ staticPipelineBytes: number;
959
+ bindingsBytes: number;
960
+ descriptionBytes: number;
961
+ };
962
+ };
963
+ bundle: {
964
+ usedBytes: number;
965
+ limitBytes: number;
966
+ withinLimit: boolean;
967
+ };
968
+ };
948
969
  }
949
970
  /** Severity of a {@link PlayCheckIssue}. `error` fails the check; `warning` does not. */
950
971
  type PlayCheckIssueSeverity = 'error' | 'warning';
package/dist/index.js CHANGED
@@ -424,10 +424,10 @@ var SDK_RELEASE = {
424
424
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
425
425
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
426
426
  // automatically without blocking their current command.
427
- version: "0.1.220",
427
+ version: "0.1.221",
428
428
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
429
429
  supportPolicy: {
430
- latest: "0.1.220",
430
+ latest: "0.1.221",
431
431
  minimumSupported: "0.1.53",
432
432
  deprecatedBelow: "0.1.219",
433
433
  commandMinimumSupported: [
package/dist/index.mjs CHANGED
@@ -354,10 +354,10 @@ var SDK_RELEASE = {
354
354
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
355
355
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
356
356
  // automatically without blocking their current command.
357
- version: "0.1.220",
357
+ version: "0.1.221",
358
358
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
359
359
  supportPolicy: {
360
- latest: "0.1.220",
360
+ latest: "0.1.221",
361
361
  minimumSupported: "0.1.53",
362
362
  deprecatedBelow: "0.1.219",
363
363
  commandMinimumSupported: [
@@ -353,6 +353,17 @@ body {
353
353
  border: 1px solid var(--red);
354
354
  background: rgba(248,81,73,0.06);
355
355
  }
356
+ .tool-detail-toggle {
357
+ margin-top: 8px;
358
+ background: var(--bg-tertiary);
359
+ border: 1px solid var(--border);
360
+ border-radius: 4px;
361
+ color: var(--blue);
362
+ cursor: pointer;
363
+ font-size: 12px;
364
+ padding: 4px 8px;
365
+ }
366
+ .tool-detail-toggle:hover { border-color: var(--blue); }
356
367
 
357
368
  /* Result card */
358
369
  .result-card {
@@ -1334,7 +1334,7 @@ function renderTimeline(timeline) {
1334
1334
  <div class="tool-detail" id="detail-${i}">
1335
1335
  <div class="tool-detail-section">
1336
1336
  <div class="label">Input</div>
1337
- <pre>${esc(formatInput(e.tool, e.input))}</pre>
1337
+ ${formatToolInputHtml(e.tool, e.input, i)}
1338
1338
  </div>
1339
1339
  ${e.result_content != null ? `<div class="tool-detail-section">
1340
1340
  <div class="label">Result</div>
@@ -1347,8 +1347,17 @@ function renderTimeline(timeline) {
1347
1347
  return html;
1348
1348
  }
1349
1349
 
1350
+ function isShellCommandTool(tool) {
1351
+ const normalized = String(tool || '').toLowerCase().replace(/[^a-z]/g, '');
1352
+ return ['bash', 'exec', 'execcommand', 'runcommand', 'shell'].includes(normalized);
1353
+ }
1354
+
1355
+ function shellCommand(input) {
1356
+ return input.command || input.cmd || input.CommandLine || null;
1357
+ }
1358
+
1350
1359
  function formatInput(tool, input) {
1351
- if (tool === 'Bash') return input.command || JSON.stringify(input, null, 2);
1360
+ if (isShellCommandTool(tool)) return shellCommand(input) || JSON.stringify(input, null, 2);
1352
1361
  if (tool === 'Write') {
1353
1362
  let s = 'file: ' + (input.file_path || input.path || '') + '\n\n';
1354
1363
  if (input.content != null) s += '--- content ---\n' + String(input.content);
@@ -1364,6 +1373,39 @@ function formatInput(tool, input) {
1364
1373
  return JSON.stringify(input, null, 2);
1365
1374
  }
1366
1375
 
1376
+ function isLongShellCommand(command) {
1377
+ return command.length > 600 || command.split('\n').length > 10;
1378
+ }
1379
+
1380
+ function shellCommandPreview(command) {
1381
+ const lines = command.split('\n');
1382
+ let preview = lines.slice(0, 10).join('\n');
1383
+ if (preview.length > 600) preview = preview.slice(0, 600);
1384
+ return preview + '\n...[full command hidden]';
1385
+ }
1386
+
1387
+ function formatToolInputHtml(tool, input, entryIndex) {
1388
+ const formatted = formatInput(tool, input);
1389
+ if (!isShellCommandTool(tool) || !isLongShellCommand(formatted)) {
1390
+ return `<pre>${esc(formatted)}</pre>`;
1391
+ }
1392
+ const previewId = `command-preview-${entryIndex}`;
1393
+ const fullId = `command-full-${entryIndex}`;
1394
+ return `<pre id="${previewId}">${esc(shellCommandPreview(formatted))}</pre>
1395
+ <button class="tool-detail-toggle" onclick="toggleCommandInput('${previewId}', '${fullId}', this)">Show full command</button>
1396
+ <pre id="${fullId}" hidden>${esc(formatted)}</pre>`;
1397
+ }
1398
+
1399
+ function toggleCommandInput(previewId, fullId, toggle) {
1400
+ const preview = document.getElementById(previewId);
1401
+ const full = document.getElementById(fullId);
1402
+ if (!preview || !full) return;
1403
+ const showFull = full.hidden;
1404
+ full.hidden = !showFull;
1405
+ preview.hidden = showFull;
1406
+ toggle.textContent = showFull ? 'Show less' : 'Show full command';
1407
+ }
1408
+
1367
1409
  function toggleReasoning(id, toggle) {
1368
1410
  const el = document.getElementById(id);
1369
1411
  const collapsed = el.classList.toggle('collapsed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.220",
3
+ "version": "0.1.221",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {