deepline 0.1.146 → 0.1.148

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.
@@ -38,6 +38,8 @@ import type {
38
38
  ToolResultTargetAccessor,
39
39
  ToolResultTargetMetadata,
40
40
  } from './tool-result-types';
41
+ import { createPlayDataset, type PlayDataset } from '../plays/dataset';
42
+ import { normalizeTableNamespace, sha256Hex } from '../plays/row-identity';
41
43
 
42
44
  type PathSegment = string | number | '*';
43
45
 
@@ -737,12 +739,22 @@ function resolveListRows(
737
739
  );
738
740
  let resolvedPath: string | null = null;
739
741
  let rows: Record<string, unknown>[] | null = null;
742
+ let emptyMatch: { path: string; rows: Record<string, unknown>[] } | null =
743
+ null;
740
744
  for (const candidate of candidates) {
741
745
  rows = normalizeRows(getAtPath(result, candidate));
742
- if (rows) {
746
+ if (!rows) {
747
+ continue;
748
+ }
749
+ if (rows.length > 0) {
743
750
  resolvedPath = candidate;
744
751
  break;
745
752
  }
753
+ emptyMatch ??= { path: candidate, rows };
754
+ }
755
+ if (!rows && emptyMatch) {
756
+ resolvedPath = emptyMatch.path;
757
+ rows = emptyMatch.rows;
746
758
  }
747
759
  if (!rows) continue;
748
760
  const storedPath = resolvedPath ?? path;
@@ -751,7 +763,12 @@ function resolveListRows(
751
763
  .filter(Boolean)
752
764
  .at(-1)
753
765
  ?.replace(/\[\d+\]$/, '');
754
- lists[name || storedPath] = { path: storedPath, rows };
766
+ const listName = name || storedPath;
767
+ const existing = lists[listName];
768
+ if (existing?.rows.length && rows.length === 0) {
769
+ continue;
770
+ }
771
+ lists[listName] = { path: storedPath, rows };
755
772
  }
756
773
  return lists;
757
774
  }
@@ -935,10 +952,26 @@ function buildExtractedAccessors(
935
952
  function buildListAccessors(
936
953
  resolved: Record<string, { path: string; rows: Record<string, unknown>[] }>,
937
954
  lists: Record<string, ToolResultListMetadata>,
955
+ toolId: string,
956
+ executionDiscriminator: string,
938
957
  ): Record<string, ToolResultListAccessor> {
939
958
  return Object.fromEntries(
940
959
  Object.entries(lists).map(([name, metadata]) => {
941
960
  const rows = resolved[name]?.rows ?? [];
961
+ const datasetDiscriminator = `${executionDiscriminator}:${listRowsFingerprint(rows)}`;
962
+ const dataset = createPlayDataset(rows, {
963
+ kind: 'csv',
964
+ sourceLabel: metadata.path,
965
+ tableNamespace: listTableNamespace(
966
+ toolId,
967
+ name,
968
+ metadata.path,
969
+ datasetDiscriminator,
970
+ ),
971
+ datasetId: `tool-list:${sha256Hex(
972
+ `${toolId}:${metadata.path}:${datasetDiscriminator}`,
973
+ )}`,
974
+ });
942
975
  const accessor = {
943
976
  path: metadata.path,
944
977
  count: metadata.count,
@@ -946,7 +979,7 @@ function buildListAccessors(
946
979
  } as ToolResultListAccessor;
947
980
  Object.defineProperty(accessor, 'get', {
948
981
  value() {
949
- return rows;
982
+ return dataset;
950
983
  },
951
984
  enumerable: false,
952
985
  });
@@ -955,6 +988,44 @@ function buildListAccessors(
955
988
  );
956
989
  }
957
990
 
991
+ function listRowsFingerprint(rows: readonly Record<string, unknown>[]): string {
992
+ try {
993
+ return sha256Hex(
994
+ JSON.stringify(
995
+ {
996
+ count: rows.length,
997
+ rows,
998
+ },
999
+ (_key, value) => (typeof value === 'bigint' ? value.toString() : value),
1000
+ ),
1001
+ ).slice(0, 12);
1002
+ } catch {
1003
+ return sha256Hex(String(rows.length)).slice(0, 12);
1004
+ }
1005
+ }
1006
+
1007
+ function listTableNamespace(
1008
+ toolId: string,
1009
+ name: string,
1010
+ path: string,
1011
+ discriminator: string,
1012
+ ): string {
1013
+ const raw = `${toolId}_${name || path || 'rows'}_${sha256Hex(discriminator).slice(0, 10)}`;
1014
+ try {
1015
+ return normalizeTableNamespace(raw);
1016
+ } catch {
1017
+ const hash = sha256Hex(raw).slice(0, 10);
1018
+ const leaf = name || path.split('.').filter(Boolean).at(-1) || 'rows';
1019
+ let prefix = 'rows';
1020
+ try {
1021
+ prefix = normalizeTableNamespace(leaf).slice(0, 52) || 'rows';
1022
+ } catch {
1023
+ prefix = 'rows';
1024
+ }
1025
+ return normalizeTableNamespace(`${prefix}_${hash}`);
1026
+ }
1027
+ }
1028
+
958
1029
  export function createToolExecuteResult<TResult = unknown>(input: {
959
1030
  status: string;
960
1031
  jobId?: string;
@@ -994,7 +1065,12 @@ export function createToolExecuteResult<TResult = unknown>(input: {
994
1065
  ...(result.meta ? { meta: result.meta } : {}),
995
1066
  };
996
1067
  const extractedValues = buildExtractedAccessors(targets);
997
- const extractedLists = buildListAccessors(resolvedLists, lists);
1068
+ const extractedLists = buildListAccessors(
1069
+ resolvedLists,
1070
+ lists,
1071
+ input.metadata.toolId,
1072
+ input.jobId ?? input.execution.cacheKey ?? 'inline',
1073
+ );
998
1074
  const wrapper = {
999
1075
  status: input.status,
1000
1076
  ...(input.jobId ? { job_id: input.jobId } : {}),
@@ -1063,7 +1139,7 @@ export function readValue(
1063
1139
  export function readList(
1064
1140
  result: ToolExecuteResult,
1065
1141
  selector?: readonly string[] | string,
1066
- ): Record<string, unknown>[] {
1142
+ ): PlayDataset<Record<string, unknown>> | Record<string, unknown>[] {
1067
1143
  if (selector) {
1068
1144
  const paths = Array.isArray(selector) ? selector : [selector];
1069
1145
  const found = findFirstTargetByPath(resultRootOf(result), paths)?.value;
package/dist/cli/index.js CHANGED
@@ -619,10 +619,11 @@ var SDK_RELEASE = {
619
619
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
620
620
  // the SDK enrich generator's one-second stale policy.
621
621
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
622
- version: "0.1.146",
623
- apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
622
+ // 0.1.111 ships dataset-native tool list getters and result row datasets.
623
+ version: "0.1.148",
624
+ apiContract: "2026-06-dataset-handle-results-hard-cutover",
624
625
  supportPolicy: {
625
- latest: "0.1.146",
626
+ latest: "0.1.148",
626
627
  minimumSupported: "0.1.53",
627
628
  deprecatedBelow: "0.1.53",
628
629
  commandMinimumSupported: [
@@ -633,52 +634,79 @@ var SDK_RELEASE = {
633
634
  },
634
635
  {
635
636
  command: "plays",
636
- minimumSupported: "0.1.110",
637
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
637
+ minimumSupported: "0.1.111",
638
+ reason: "Play file commands now use dataset-native list getters and result row datasets."
638
639
  },
639
640
  {
640
641
  command: "plays run",
641
- minimumSupported: "0.1.110",
642
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
642
+ minimumSupported: "0.1.111",
643
+ reason: "Play run results now promote row-shaped outputs into dataset handles for safe export."
643
644
  },
644
645
  {
645
646
  command: "run",
646
647
  displayCommand: "plays run",
647
- minimumSupported: "0.1.110",
648
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
648
+ minimumSupported: "0.1.111",
649
+ reason: "Play run results now promote row-shaped outputs into dataset handles for safe export."
649
650
  },
650
651
  {
651
652
  command: "plays check",
652
- minimumSupported: "0.1.110",
653
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
653
+ minimumSupported: "0.1.111",
654
+ reason: "Play file checks now validate dataset-native list getter authoring."
654
655
  },
655
656
  {
656
657
  command: "check",
657
658
  displayCommand: "plays check",
658
- minimumSupported: "0.1.110",
659
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
659
+ minimumSupported: "0.1.111",
660
+ reason: "Play file checks now validate dataset-native list getter authoring."
660
661
  },
661
662
  {
662
663
  command: "plays publish",
663
- minimumSupported: "0.1.110",
664
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
664
+ minimumSupported: "0.1.111",
665
+ reason: "Published play artifacts now target dataset-native list getters and result row datasets."
665
666
  },
666
667
  {
667
668
  command: "publish",
668
669
  displayCommand: "plays publish",
669
- minimumSupported: "0.1.110",
670
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
670
+ minimumSupported: "0.1.111",
671
+ reason: "Published play artifacts now target dataset-native list getters and result row datasets."
671
672
  },
672
673
  {
673
674
  command: "plays set-live",
674
- minimumSupported: "0.1.110",
675
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
675
+ minimumSupported: "0.1.111",
676
+ reason: "Published play artifacts now target dataset-native list getters and result row datasets."
676
677
  },
677
678
  {
678
679
  command: "set-live",
679
680
  displayCommand: "plays set-live",
680
- minimumSupported: "0.1.110",
681
- reason: "Play file commands now require top-level definePlay descriptions so agents and play surfaces can explain local plays."
681
+ minimumSupported: "0.1.111",
682
+ reason: "Published play artifacts now target dataset-native list getters and result row datasets."
683
+ },
684
+ {
685
+ command: "runs",
686
+ minimumSupported: "0.1.111",
687
+ reason: "Run result rows now render as dataset handles with explicit export commands."
688
+ },
689
+ {
690
+ command: "runs get",
691
+ minimumSupported: "0.1.111",
692
+ reason: "Run result rows now render as dataset handles with explicit export commands."
693
+ },
694
+ {
695
+ command: "get",
696
+ displayCommand: "runs get",
697
+ minimumSupported: "0.1.111",
698
+ reason: "Run result rows now render as dataset handles with explicit export commands."
699
+ },
700
+ {
701
+ command: "runs export",
702
+ minimumSupported: "0.1.111",
703
+ reason: "Run result row datasets now use the dataset-handle export contract."
704
+ },
705
+ {
706
+ command: "export",
707
+ displayCommand: "runs export",
708
+ minimumSupported: "0.1.111",
709
+ reason: "Run result row datasets now use the dataset-handle export contract."
682
710
  }
683
711
  ],
684
712
  autoUpdatePatchLag: 2
@@ -8302,9 +8330,10 @@ function generateProviderSourceBlock(input2) {
8302
8330
  input: ${inputName},
8303
8331
  description: ${jsString(`Seed ${input2.entity} rows from ${input2.provider}.`)},
8304
8332
  });
8305
- // extractedLists.${getter}.get() returns provider-shaped rows. Do not assume canonical fields;
8333
+ // extractedLists.${getter}.get() returns a dataset handle of provider-shaped rows. Do not assume canonical fields;
8306
8334
  // inspect source_${input2.index}.extractedLists.${getter}.keys or the provider output schema, then map explicitly below.
8307
- const sourceRows_${input2.index} = ${accessorExpression(`source_${input2.index}.extractedLists`, getter)}.get() as ${input2.collectionType}[];`;
8335
+ const sourceRowsDataset_${input2.index} = ${accessorExpression(`source_${input2.index}.extractedLists`, getter)}.get();
8336
+ const sourceRows_${input2.index} = (await sourceRowsDataset_${input2.index}.peek(limit)) as ${input2.collectionType}[];`;
8308
8337
  }
8309
8338
  function generateProviderSourceRowsBlock(input2) {
8310
8339
  const blocks = input2.source.values.map(
@@ -10990,6 +11019,9 @@ function formatPlayLogLine(line, status, state) {
10990
11019
  }
10991
11020
  return `${prefix}${message}`;
10992
11021
  }
11022
+ function isDatasetResultEnvelope(value) {
11023
+ return value !== null && typeof value === "object" && !Array.isArray(value) && value.kind === "dataset";
11024
+ }
10993
11025
  function compactReturnValue(value, depth = 0) {
10994
11026
  if (depth >= 4) {
10995
11027
  return value && typeof value === "object" ? "[Object]" : value;
@@ -11001,6 +11033,7 @@ function compactReturnValue(value, depth = 0) {
11001
11033
  if (!value || typeof value !== "object") {
11002
11034
  return value;
11003
11035
  }
11036
+ const isDatasetEnvelope = isDatasetResultEnvelope(value);
11004
11037
  const output2 = {};
11005
11038
  for (const [key, entry] of Object.entries(value)) {
11006
11039
  if (depth === 0 && key === "_metadata") {
@@ -11009,13 +11042,41 @@ function compactReturnValue(value, depth = 0) {
11009
11042
  if (BULKY_RETURN_KEYS.has(key)) {
11010
11043
  continue;
11011
11044
  }
11012
- if (key === "access") {
11045
+ if (isDatasetEnvelope && key === "access") {
11046
+ continue;
11047
+ }
11048
+ if (isDatasetEnvelope && (key === "queryDatasetCommand" || key === "cliCommand")) {
11049
+ continue;
11050
+ }
11051
+ if (isDatasetEnvelope && key === "slowExportAsCsvCommand") {
11052
+ output2.fullExportCommand = entry;
11013
11053
  continue;
11014
11054
  }
11015
11055
  output2[key] = compactReturnValue(entry, depth + 1);
11016
11056
  }
11017
11057
  return output2;
11018
11058
  }
11059
+ function defaultResultForEnvelope(value, depth = 0) {
11060
+ if (depth > 20 || value == null || typeof value !== "object") {
11061
+ return value;
11062
+ }
11063
+ if (Array.isArray(value)) {
11064
+ return value.map((entry) => defaultResultForEnvelope(entry, depth + 1));
11065
+ }
11066
+ const isDatasetEnvelope = isDatasetResultEnvelope(value);
11067
+ const output2 = {};
11068
+ for (const [key, entry] of Object.entries(value)) {
11069
+ if (isDatasetEnvelope && (key === "queryDatasetCommand" || key === "cliCommand")) {
11070
+ continue;
11071
+ }
11072
+ if (isDatasetEnvelope && key === "slowExportAsCsvCommand") {
11073
+ output2.fullExportCommand = entry;
11074
+ continue;
11075
+ }
11076
+ output2[key] = defaultResultForEnvelope(entry, depth + 1);
11077
+ }
11078
+ return output2;
11079
+ }
11019
11080
  function formatJsonPreview(value) {
11020
11081
  const json = JSON.stringify(value, null, 2);
11021
11082
  if (!json || json === "{}") {
@@ -11059,9 +11120,6 @@ function collectDatasetHandleLines(value, path = "result", datasetStats) {
11059
11120
  if (datasetStats && (datasetStats.source === path || datasetStats.source === record.path)) {
11060
11121
  lines2.push(...formatDatasetStatsLines(datasetStats.stats, " "));
11061
11122
  }
11062
- if (typeof record.queryDatasetCommand === "string") {
11063
- lines2.push(` query dataset: ${record.queryDatasetCommand}`);
11064
- }
11065
11123
  if (typeof record.slowExportAsCsvCommand === "string") {
11066
11124
  lines2.push(` export CSV: ${record.slowExportAsCsvCommand}`);
11067
11125
  }
@@ -11472,7 +11530,7 @@ function compactPlayStatus(status) {
11472
11530
  logs: normalizeLogsForEnvelope(status),
11473
11531
  ...displayError ? { error: displayError } : {},
11474
11532
  ...warnings.length > 0 ? { warnings } : {},
11475
- ...result !== void 0 ? { result } : {},
11533
+ ...result !== void 0 ? { result: defaultResultForEnvelope(result) } : {},
11476
11534
  ...billing ? { billing } : {},
11477
11535
  next: buildRunNextCommands(status)
11478
11536
  };
@@ -20415,16 +20473,24 @@ function tryConvertToList(payload, options) {
20415
20473
  (entry) => typeof entry === "string" && entry.trim().length > 0
20416
20474
  ) : [];
20417
20475
  if (listExtractorPaths.length > 0) {
20476
+ let emptyMatch = null;
20418
20477
  for (const root of candidateRoots(payload)) {
20419
20478
  for (const extractorPath of listExtractorPaths) {
20420
20479
  const resolved = getByDottedPath(root.value, extractorPath);
20421
20480
  const rows = normalizeRows(resolved);
20422
- if (rows && rows.length > 0) {
20423
- const sourcePath = root.path ? `${root.path}.${extractorPath}` : extractorPath;
20481
+ if (!rows) {
20482
+ continue;
20483
+ }
20484
+ const sourcePath = root.path ? `${root.path}.${extractorPath}` : extractorPath;
20485
+ if (rows.length > 0) {
20424
20486
  return { rows, strategy: "configured_paths", sourcePath };
20425
20487
  }
20488
+ emptyMatch ??= { rows, strategy: "configured_paths", sourcePath };
20426
20489
  }
20427
20490
  }
20491
+ if (emptyMatch) {
20492
+ return emptyMatch;
20493
+ }
20428
20494
  }
20429
20495
  for (const root of candidateRoots(payload)) {
20430
20496
  const candidate = findBestArrayCandidate(root.value, root.path ?? "");
@@ -20448,9 +20514,9 @@ function writeJsonOutputFile(payload, stem) {
20448
20514
  (0, import_node_fs12.writeFileSync)(outputPath, JSON.stringify(payload, null, 2), "utf-8");
20449
20515
  return outputPath;
20450
20516
  }
20451
- function writeCsvOutputFile(rows, stem) {
20452
- const outputDir = ensureOutputDir();
20453
- const outputPath = (0, import_node_path14.join)(outputDir, `${stem}_${Date.now()}.csv`);
20517
+ function writeCsvOutputFile(rows, stem, options) {
20518
+ const outputPath = options?.outPath ? options.outPath : (0, import_node_path14.join)(ensureOutputDir(), `${stem}_${Date.now()}.csv`);
20519
+ (0, import_node_fs12.mkdirSync)((0, import_node_path14.dirname)(outputPath), { recursive: true });
20454
20520
  const seen = /* @__PURE__ */ new Set();
20455
20521
  const columns = [];
20456
20522
  for (const row of rows) {
@@ -20979,6 +21045,7 @@ Examples:
20979
21045
  deepline tools execute hunter_email_verifier --input '{"email":"a@b.com"}'
20980
21046
  deepline tools execute hunter_email_verifier -p email=a@b.com
20981
21047
  deepline tools execute test_rate_limit --input '{"key":"smoke"}' --json | jq '.status'
21048
+ deepline tools execute free_simple_company_search --input '{"sql":"SELECT company_name FROM companies LIMIT 10"}' --out companies.csv
20982
21049
  `
20983
21050
  ).option(
20984
21051
  "-p, --param <key=value>",
@@ -21000,6 +21067,9 @@ Examples:
21000
21067
  ).option(
21001
21068
  "--output-format <format>",
21002
21069
  "Output format: auto, csv, csv_file, json, or json_file"
21070
+ ).option(
21071
+ "-o, --out <path>",
21072
+ "Write row-shaped tool output to this CSV path"
21003
21073
  ).option(
21004
21074
  "--no-preview",
21005
21075
  "Only print the extracted output path when applicable"
@@ -21012,6 +21082,7 @@ Examples:
21012
21082
  ...options.input ? ["--input", options.input] : [],
21013
21083
  ...options.payload ? ["--payload", options.payload] : [],
21014
21084
  ...options.outputFormat ? ["--output-format", options.outputFormat] : [],
21085
+ ...options.out ? ["--out", options.out] : [],
21015
21086
  ...options.preview === false ? ["--no-preview"] : []
21016
21087
  ];
21017
21088
  process.exitCode = await executeTool(args);
@@ -21462,7 +21533,7 @@ function toolMetadataJsonForDescribe(tool, requestedToolId) {
21462
21533
  invalidGetterHint: "If TypeScript says an extractedValues/extractedLists property does not exist, that field is not a declared Deepline getter.",
21463
21534
  observeActualShape: observedOutputCommand,
21464
21535
  observedOutput: observedOutputCommand,
21465
- forPlayGetterBugs: "Run a tiny play, inspect `deepline runs get <run-id> --full --json`, and export returned dataset handles with `deepline runs export`. Backing tables exist only for ctx.map(...).run(...) stages that actually executed.",
21536
+ forPlayGetterBugs: "Run a tiny play, inspect returned handles with `deepline runs get <run-id> --full --json`, then export row datasets with `deepline runs export`.",
21466
21537
  executeOutputFields: "tools execute JSON may include output_preview for this direct probe only; play debugging uses run output and returned dataset handles."
21467
21538
  },
21468
21539
  ...starterScript ? { starterScript } : {}
@@ -21669,12 +21740,13 @@ function parseExecuteOptions(args) {
21669
21740
  const toolId = args[0];
21670
21741
  if (!toolId) {
21671
21742
  throw new Error(
21672
- `Usage: deepline tools execute <toolId> [--param key=value ...] [--input '{"k":"v"}'] [--output-format auto|csv|csv_file|json|json_file] [--no-preview]`
21743
+ `Usage: deepline tools execute <toolId> [--param key=value ...] [--input '{"k":"v"}'] [--out rows.csv] [--output-format auto|csv|csv_file|json|json_file] [--no-preview]`
21673
21744
  );
21674
21745
  }
21675
21746
  const params = {};
21676
21747
  let outputFormat = "auto";
21677
21748
  let noPreview = false;
21749
+ let outPath = null;
21678
21750
  for (let index = 1; index < args.length; index += 1) {
21679
21751
  const arg = args[index];
21680
21752
  if ((arg === "--param" || arg === "-p") && args[index + 1]) {
@@ -21706,9 +21778,13 @@ function parseExecuteOptions(args) {
21706
21778
  noPreview = true;
21707
21779
  continue;
21708
21780
  }
21781
+ if ((arg === "--out" || arg === "-o") && args[index + 1]) {
21782
+ outPath = (0, import_node_path15.resolve)(args[++index]);
21783
+ continue;
21784
+ }
21709
21785
  throw new Error(`Unknown option: ${arg}`);
21710
21786
  }
21711
- return { toolId, params, outputFormat, noPreview };
21787
+ return { toolId, params, outputFormat, noPreview, outPath };
21712
21788
  }
21713
21789
  function safeFileStem(value) {
21714
21790
  return value.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "tool";
@@ -21752,9 +21828,14 @@ export default definePlay(${JSON.stringify(playName)}, async (ctx) => {
21752
21828
  });
21753
21829
 
21754
21830
  const list = Object.values(result.extractedLists)[0];
21755
- const rows = (list?.get() ?? []).slice(0, 100);
21831
+ if (!list) {
21832
+ throw new Error('Expected ${input2.toolId} to expose an extracted list getter.');
21833
+ }
21834
+ const rows = list.get();
21756
21835
  // ${sampleRows}
21757
21836
  // columns: ${columns}
21837
+ // extractedLists.<name>.get() returns a dataset handle for row-shaped list outputs.
21838
+ // Pass it to ctx.dataset; use rows.peek/count/materialize only for deliberate bounded inspection.
21758
21839
  // .withColumn('email_waterfall', (row, rowCtx) => rowCtx.runPlay('name_domain_email', 'name-and-domain-to-email', { first_name: String(row.first_name ?? ''), last_name: String(row.last_name ?? ''), domain: String(row.domain ?? '') }, { description: 'Resolve email.' }))
21759
21840
  // .withColumn('phone_waterfall', (row, rowCtx) => rowCtx.runPlay('contact_phone', 'contact-to-phone', { first_name: String(row.first_name ?? ''), last_name: String(row.last_name ?? ''), email: String(row.email ?? '') }, { description: 'Resolve phone.' }))
21760
21841
  // ctx.dataset is idempotent by dataset key + row key; reruns reuse completed rows.
@@ -21814,7 +21895,7 @@ function buildToolExecuteBaseEnvelope(input2) {
21814
21895
  ...summaryEntries.length > 0 ? { summary: input2.summary } : {},
21815
21896
  next: {
21816
21897
  inspect: inspectCommand,
21817
- playDebugging: "When fixing a play getter, inspect the actual play run table with runs get / inspect rows; do not copy CLI preview paths blindly.",
21898
+ playDebugging: "When fixing a play getter, inspect returned dataset handles with runs get and export rows with runs export; do not copy CLI preview paths blindly.",
21818
21899
  ...input2.listConversion ? {
21819
21900
  expandToPlay: "Use stable map and step keys so reruns are idempotent: completed rows are reused, and only missing or stale work runs again.",
21820
21901
  listSourcePath: input2.listConversion.sourcePath ?? "auto-detected list in the CLI response preview"
@@ -21826,7 +21907,7 @@ function buildToolExecuteBaseEnvelope(input2) {
21826
21907
  title: "output",
21827
21908
  lines: [
21828
21909
  `${input2.listConversion.rows.length} row(s) extracted from ${input2.listConversion.sourcePath ?? "auto-detected list"}`,
21829
- "paths above are observed from this execute response; use run table rows to debug play getters",
21910
+ "paths above are observed from this execute response; use runs get and runs export to validate play output",
21830
21911
  `columns: ${JSON.stringify(Object.keys(input2.listConversion.rows[0] ?? {}))}`,
21831
21912
  `preview: ${JSON.stringify(input2.listConversion.rows.slice(0, 5))}`
21832
21913
  ]
@@ -21906,7 +21987,7 @@ async function executeTool(args) {
21906
21987
  listConversion,
21907
21988
  summary
21908
21989
  });
21909
- if (parsed.outputFormat === "json" || parsed.outputFormat === "auto" && shouldEmitJson()) {
21990
+ if (!parsed.outPath && (parsed.outputFormat === "json" || parsed.outputFormat === "auto" && shouldEmitJson())) {
21910
21991
  printCommandEnvelope(baseEnvelope, { json: true });
21911
21992
  return 0;
21912
21993
  }
@@ -21928,6 +22009,17 @@ async function executeTool(args) {
21928
22009
  return 0;
21929
22010
  }
21930
22011
  if (!listConversion) {
22012
+ if (parsed.outPath) {
22013
+ const error = new Error(
22014
+ `${parsed.toolId} did not expose row-shaped output to write as CSV. Re-run with --json to inspect the raw tool response.`
22015
+ );
22016
+ if (argsWantJson(args)) {
22017
+ printJsonError(error);
22018
+ } else {
22019
+ console.error(error.message);
22020
+ }
22021
+ return 1;
22022
+ }
21931
22023
  if (parsed.outputFormat === "csv" || parsed.outputFormat === "csv_file") {
21932
22024
  const jsonPath = writeJsonOutputFile(
21933
22025
  rawResponse,
@@ -21949,7 +22041,8 @@ async function executeTool(args) {
21949
22041
  }
21950
22042
  const csv = writeCsvOutputFile(
21951
22043
  listConversion.rows,
21952
- `${parsed.toolId}_output`
22044
+ `${parsed.toolId}_output`,
22045
+ parsed.outPath ? { outPath: parsed.outPath } : void 0
21953
22046
  );
21954
22047
  const seededScript = seedToolListScript({
21955
22048
  toolId: parsed.toolId,
@@ -22022,6 +22115,27 @@ async function executeTool(args) {
22022
22115
  );
22023
22116
  return 0;
22024
22117
  }
22118
+ if (parsed.outPath) {
22119
+ printCommandEnvelope(
22120
+ {
22121
+ ...materializedEnvelope,
22122
+ csv_path: csv.path,
22123
+ row_count: csv.rowCount,
22124
+ row_count_returned: csv.rowCount,
22125
+ columns: csv.columns,
22126
+ preview: csv.preview,
22127
+ list_strategy: listConversion.strategy,
22128
+ list_source_path: listConversion.sourcePath,
22129
+ summary
22130
+ },
22131
+ {
22132
+ json: argsWantJson(args) || shouldEmitJson(),
22133
+ text: `Wrote ${csv.rowCount} row(s) to ${csv.path}
22134
+ `
22135
+ }
22136
+ );
22137
+ return 0;
22138
+ }
22025
22139
  if (parsed.noPreview) {
22026
22140
  printCommandEnvelope(
22027
22141
  {
@@ -23563,6 +23677,40 @@ function isCi() {
23563
23677
  function shouldSkipSelfUpdate() {
23564
23678
  return envTruthy("DEEPLINE_SKIP_SELF_UPDATE") || envTruthy("DEEPLINE_NO_AUTO_UPDATE") || envTruthy("DEEPLINE_SKIP_SDK_AUTO_UPDATE") || envTruthy("DEEPLINE_DISABLE_AUTO_UPDATE") || isCi();
23565
23679
  }
23680
+ function parseSemver(version) {
23681
+ const trimmed = version?.trim();
23682
+ if (!trimmed) return null;
23683
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(
23684
+ trimmed
23685
+ );
23686
+ if (!match) return null;
23687
+ return {
23688
+ major: Number(match[1]),
23689
+ minor: Number(match[2]),
23690
+ patch: Number(match[3]),
23691
+ prerelease: match[4] ?? ""
23692
+ };
23693
+ }
23694
+ function compareSemver(left, right) {
23695
+ const a = parseSemver(left);
23696
+ const b = parseSemver(right);
23697
+ if (!a || !b) {
23698
+ return left.localeCompare(right);
23699
+ }
23700
+ for (const key of ["major", "minor", "patch"]) {
23701
+ if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
23702
+ }
23703
+ if (a.prerelease === b.prerelease) return 0;
23704
+ if (!a.prerelease) return 1;
23705
+ if (!b.prerelease) return -1;
23706
+ return a.prerelease.localeCompare(b.prerelease);
23707
+ }
23708
+ function isDowngradeAutoUpdateResponse(response) {
23709
+ const target = response?.latest?.trim();
23710
+ const current = response?.current?.trim() || SDK_VERSION;
23711
+ if (!target) return false;
23712
+ return compareSemver(target, current) < 0;
23713
+ }
23566
23714
  function relaunchCurrentCommand(plan) {
23567
23715
  return new Promise((resolve13) => {
23568
23716
  const command = plan.kind === "python-sidecar" ? plan.sidecarPath : process.execPath;
@@ -23590,6 +23738,15 @@ async function maybeAutoUpdateAndRelaunch(response) {
23590
23738
  if (!response || !autoUpdate?.should_auto_update || shouldSkipSelfUpdate()) {
23591
23739
  return false;
23592
23740
  }
23741
+ if (isDowngradeAutoUpdateResponse(response)) {
23742
+ const target = response.latest;
23743
+ const current = response.current?.trim() || SDK_VERSION;
23744
+ process.stderr.write(
23745
+ `Deepline SDK/CLI auto-update refused: server advertised older ${target} than current ${current}. Continuing without mutating the CLI.
23746
+ `
23747
+ );
23748
+ return false;
23749
+ }
23593
23750
  const packageSpec = response.latest ? `deepline@${response.latest}` : void 0;
23594
23751
  const plan = resolveUpdatePlan({ packageSpec });
23595
23752
  if (plan.kind === "source") {