deepline 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -266,7 +266,7 @@ function saveProjectDeeplineEnvValues(baseUrl, values, startDir = projectEnvStar
266
266
  }
267
267
 
268
268
  // src/version.ts
269
- var SDK_VERSION = "0.1.21";
269
+ var SDK_VERSION = "0.1.23";
270
270
  var SDK_API_CONTRACT = "2026-05-runs-v2";
271
271
 
272
272
  // ../shared_libs/play-runtime/coordinator-headers.ts
@@ -554,11 +554,24 @@ function sleep(ms) {
554
554
  // src/client.ts
555
555
  var TERMINAL_PLAY_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
556
556
  var INCLUDE_TOOL_METADATA_HEADER = "x-deepline-include-tool-metadata";
557
+ var COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1e3];
558
+ function sleep2(ms) {
559
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
560
+ }
561
+ function isTransientCompileManifestError(error) {
562
+ if (error instanceof DeeplineError && typeof error.statusCode === "number") {
563
+ return error.statusCode === 408 || error.statusCode === 425 || error.statusCode === 499 || error.statusCode >= 500 && error.statusCode < 600;
564
+ }
565
+ const message = error instanceof Error ? error.message : String(error);
566
+ return /fetch failed|connection (?:closed|reset|terminated)|socket hang up|econnreset|etimedout|eai_again|abort/i.test(
567
+ message
568
+ );
569
+ }
557
570
  function isRecord(value) {
558
571
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
559
572
  }
560
573
  function normalizePlayStatus(raw) {
561
- const status = typeof raw.status === "string" ? raw.status : typeof raw.temporalStatus === "string" ? mapLegacyTemporalStatus(raw.temporalStatus) : "running";
574
+ const status = typeof raw.status === "string" ? raw.status : "running";
562
575
  const runId = typeof raw.runId === "string" ? raw.runId : typeof raw.workflowId === "string" ? raw.workflowId : "";
563
576
  return {
564
577
  ...raw,
@@ -566,23 +579,6 @@ function normalizePlayStatus(raw) {
566
579
  status
567
580
  };
568
581
  }
569
- function mapLegacyTemporalStatus(status) {
570
- switch (status.trim().toUpperCase()) {
571
- case "PENDING":
572
- return "queued";
573
- case "COMPLETED":
574
- return "completed";
575
- case "FAILED":
576
- return "failed";
577
- case "CANCELLED":
578
- case "TERMINATED":
579
- case "TIMED_OUT":
580
- return "cancelled";
581
- case "RUNNING":
582
- default:
583
- return "running";
584
- }
585
- }
586
582
  function decodeBase64Bytes(value) {
587
583
  const binary = atob(value);
588
584
  const bytes = new Uint8Array(binary.length);
@@ -591,6 +587,79 @@ function decodeBase64Bytes(value) {
591
587
  }
592
588
  return bytes;
593
589
  }
590
+ function readStringArray(value) {
591
+ return Array.isArray(value) ? value.filter((line) => typeof line === "string") : [];
592
+ }
593
+ function getPlayLiveEventPayload(event) {
594
+ return event.payload && typeof event.payload === "object" ? event.payload : {};
595
+ }
596
+ function normalizeLiveStatus(value) {
597
+ if (value === "queued" || value === "running" || value === "waiting" || value === "completed" || value === "failed" || value === "cancelled") {
598
+ return value;
599
+ }
600
+ return null;
601
+ }
602
+ function updatePlayLiveStatusState(state, event) {
603
+ const payload = getPlayLiveEventPayload(event);
604
+ if (event.type === "play.run.log") {
605
+ state.logs.push(...readStringArray(payload.lines));
606
+ return null;
607
+ }
608
+ if (event.type !== "play.run.snapshot" && event.type !== "play.run.status" && event.type !== "play.run.final_status") {
609
+ return null;
610
+ }
611
+ const runId = typeof payload.runId === "string" && payload.runId ? payload.runId : state.runId;
612
+ const status = normalizeLiveStatus(payload.status) ?? state.status;
613
+ const logs = readStringArray(payload.logs);
614
+ if (logs.length > 0 || event.type === "play.run.snapshot") {
615
+ state.logs = logs;
616
+ }
617
+ if ("result" in payload) {
618
+ state.result = payload.result;
619
+ }
620
+ if (typeof payload.error === "string" && payload.error.trim()) {
621
+ state.error = payload.error;
622
+ }
623
+ state.runId = runId;
624
+ state.status = status;
625
+ const progressRecord = payload.progress && typeof payload.progress === "object" && !Array.isArray(payload.progress) ? payload.progress : {};
626
+ const next = {
627
+ ...payload,
628
+ runId,
629
+ status,
630
+ progress: {
631
+ ...progressRecord,
632
+ status: typeof progressRecord.status === "string" ? progressRecord.status : status,
633
+ logs: state.logs,
634
+ ...state.error ? { error: state.error } : {}
635
+ },
636
+ ..."result" in state ? { result: state.result } : {}
637
+ };
638
+ state.latest = next;
639
+ return next;
640
+ }
641
+ function playRunResultFromStatus(status, startedAt, fallbackRunId) {
642
+ return {
643
+ success: status.status === "completed",
644
+ runId: status.runId || fallbackRunId,
645
+ result: status.result,
646
+ logs: status.progress?.logs ?? [],
647
+ durationMs: Date.now() - startedAt,
648
+ error: status.progress?.error ?? (status.status !== "completed" ? status.status : void 0)
649
+ };
650
+ }
651
+ function playRunStatusFromState(state) {
652
+ return {
653
+ runId: state.runId,
654
+ status: state.status,
655
+ progress: {
656
+ status: state.status,
657
+ logs: state.logs,
658
+ ...state.error ? { error: state.error } : {}
659
+ },
660
+ ..."result" in state ? { result: state.result } : {}
661
+ };
662
+ }
594
663
  var DeeplineClient = class {
595
664
  http;
596
665
  config;
@@ -700,7 +769,7 @@ var DeeplineClient = class {
700
769
  /**
701
770
  * Search available tools using Deepline's ranked backend search.
702
771
  *
703
- * This is the same discovery surface used by the legacy CLI: it ranks across
772
+ * This is the same discovery surface used by the CLI: it ranks across
704
773
  * tool metadata, categories, agent guidance, and input schema fields.
705
774
  */
706
775
  async searchTools(options = {}) {
@@ -793,7 +862,7 @@ var DeeplineClient = class {
793
862
  * `progress.logs`; they are not part of the user output object.
794
863
  *
795
864
  * @param request - Play run configuration (name, code, input, etc.)
796
- * @returns Workflow metadata including the `workflowId` for status polling
865
+ * @returns Run metadata including the public `workflowId`
797
866
  *
798
867
  * @example
799
868
  * ```typescript
@@ -903,8 +972,22 @@ var DeeplineClient = class {
903
972
  });
904
973
  }
905
974
  async compilePlayManifest(input) {
906
- const response = await this.http.post("/api/v2/plays/compile-manifest", input);
907
- return response.compilerManifest;
975
+ const retryDelays = COMPILE_MANIFEST_RETRY_DELAYS_MS.slice(
976
+ 0,
977
+ Math.max(0, this.config.maxRetries)
978
+ );
979
+ for (let attempt = 0; ; attempt += 1) {
980
+ try {
981
+ const response = await this.http.post("/api/v2/plays/compile-manifest", input);
982
+ return response.compilerManifest;
983
+ } catch (error) {
984
+ const delayMs = retryDelays[attempt];
985
+ if (delayMs === void 0 || !isTransientCompileManifestError(error)) {
986
+ throw error;
987
+ }
988
+ await sleep2(delayMs);
989
+ }
990
+ }
908
991
  }
909
992
  /**
910
993
  * Check a bundled play artifact against the server's current play compiler.
@@ -1085,9 +1168,6 @@ var DeeplineClient = class {
1085
1168
  * Internal/advanced primitive. Public callers should usually prefer
1086
1169
  * {@link runPlay}, {@link PlayJob.get}, or `deepline play run --watch`.
1087
1170
  *
1088
- * Poll this method until `status` reaches a terminal state:
1089
- * `'completed'`, `'failed'`, or `'cancelled'`.
1090
- *
1091
1171
  * @param workflowId - Play-run id from {@link startPlayRun}
1092
1172
  * @returns Current status with progress logs and partial results
1093
1173
  *
@@ -1109,35 +1189,11 @@ var DeeplineClient = class {
1109
1189
  );
1110
1190
  return normalizePlayStatus(response);
1111
1191
  }
1112
- /**
1113
- * Get the lightweight tail-polling status for a play execution.
1114
- *
1115
- * This is intentionally smaller than {@link getPlayStatus}: it returns the
1116
- * fields needed for CLI log tailing while the run is in flight, without
1117
- * forcing the API to rebuild final result views on every poll. Call
1118
- * {@link getPlayStatus} once after a terminal state for the full result.
1119
- */
1120
- async getPlayTailStatus(workflowId, options) {
1121
- const params = new URLSearchParams({ mode: "tail" });
1122
- if (typeof options?.afterLogIndex === "number") {
1123
- params.set("afterLogIndex", String(options.afterLogIndex));
1124
- }
1125
- if (typeof options?.waitMs === "number") {
1126
- params.set("waitMs", String(options.waitMs));
1127
- }
1128
- if (options?.terminalOnly) {
1129
- params.set("terminalOnly", "true");
1130
- }
1131
- const response = await this.http.get(
1132
- `/api/v2/plays/run/${encodeURIComponent(workflowId)}?${params.toString()}`
1133
- );
1134
- return normalizePlayStatus(response);
1135
- }
1136
1192
  /**
1137
1193
  * Stream semantic play-run events using the same SSE feed as the dashboard.
1138
1194
  *
1139
- * Consumers should still keep a polling fallback: SSE is the fast live-update
1140
- * transport, while the status endpoints remain the authoritative recovery path.
1195
+ * The server emits a canonical `play.run.snapshot` event first for every
1196
+ * connection, then incremental live events until terminal state or reconnect.
1141
1197
  */
1142
1198
  async *streamPlayRunEvents(workflowId, options) {
1143
1199
  const headers = options?.lastEventId && options.lastEventId.trim() ? { "Last-Event-ID": options.lastEventId.trim() } : void 0;
@@ -1157,7 +1213,7 @@ var DeeplineClient = class {
1157
1213
  *
1158
1214
  * Sends a stop request for the run.
1159
1215
  *
1160
- * @param workflowId - Temporal workflow ID to cancel
1216
+ * @param workflowId - Public Deepline play-run id to cancel
1161
1217
  *
1162
1218
  * @example
1163
1219
  * ```typescript
@@ -1173,7 +1229,7 @@ var DeeplineClient = class {
1173
1229
  /**
1174
1230
  * Stop a running play execution, including open HITL waits.
1175
1231
  *
1176
- * @param workflowId - Temporal workflow ID to stop
1232
+ * @param workflowId - Public Deepline play-run id to stop
1177
1233
  * @param options.reason - Optional audit/debug reason
1178
1234
  */
1179
1235
  async stopPlay(workflowId, options) {
@@ -1245,32 +1301,42 @@ var DeeplineClient = class {
1245
1301
  );
1246
1302
  return response.runs ?? [];
1247
1303
  }
1248
- /**
1249
- * Fetch the lightweight tail status for a run using the public runs resource model.
1250
- *
1251
- * This is the SDK equivalent of:
1252
- *
1253
- * ```bash
1254
- * deepline runs tail <run-id> --json
1255
- * ```
1256
- */
1304
+ /** Read the canonical run stream and return the latest run snapshot. */
1257
1305
  async tailRun(runId, options) {
1258
- const afterLogIndex = typeof options?.afterLogIndex === "number" ? options.afterLogIndex : typeof options?.cursor === "number" ? options.cursor : typeof options?.cursor === "string" && options.cursor.trim() ? Number(options.cursor) : void 0;
1259
- const params = new URLSearchParams();
1260
- if (Number.isFinite(afterLogIndex)) {
1261
- params.set("afterLogIndex", String(Number(afterLogIndex)));
1306
+ const state = {
1307
+ runId,
1308
+ status: "running",
1309
+ logs: [],
1310
+ latest: null
1311
+ };
1312
+ let terminal = false;
1313
+ for await (const event of this.streamPlayRunEvents(runId, {
1314
+ mode: "cli",
1315
+ signal: options?.signal
1316
+ })) {
1317
+ const status = updatePlayLiveStatusState(state, event);
1318
+ if (!status) {
1319
+ continue;
1320
+ }
1321
+ terminal = TERMINAL_PLAY_STATUSES.has(status.status);
1322
+ if (terminal) {
1323
+ break;
1324
+ }
1262
1325
  }
1263
- if (typeof options?.waitMs === "number") {
1264
- params.set("waitMs", String(options.waitMs));
1326
+ if (terminal && state.latest) {
1327
+ return await this.getRunStatus(state.latest.runId || runId).catch(
1328
+ () => state.latest ?? playRunStatusFromState(state)
1329
+ );
1265
1330
  }
1266
- if (options?.terminalOnly) {
1267
- params.set("terminalOnly", "true");
1331
+ if (state.latest) {
1332
+ return state.latest;
1268
1333
  }
1269
- const suffix = params.toString() ? `?${params.toString()}` : "";
1270
- const response = await this.http.get(
1271
- `/api/v2/runs/${encodeURIComponent(runId)}/tail${suffix}`
1334
+ throw new DeeplineError(
1335
+ `Run stream for ${runId} ended before the initial snapshot.`,
1336
+ void 0,
1337
+ "PLAY_RUN_STREAM_EMPTY",
1338
+ { runId }
1272
1339
  );
1273
- return normalizePlayStatus(response);
1274
1340
  }
1275
1341
  /**
1276
1342
  * Fetch persisted logs for a run using the public runs resource model.
@@ -1427,11 +1493,11 @@ var DeeplineClient = class {
1427
1493
  // Plays — high-level orchestration
1428
1494
  // ——————————————————————————————————————————————————————————
1429
1495
  /**
1430
- * Run a play end-to-end: submit, poll until terminal, return result.
1496
+ * Run a play end-to-end: submit, stream until terminal, return result.
1431
1497
  *
1432
1498
  * This is the highest-level play execution method. It submits the play,
1433
- * polls for status updates, and returns a structured result with logs
1434
- * and timing. Supports cancellation via `AbortSignal`.
1499
+ * reads the canonical run stream for status updates, and returns a structured
1500
+ * result with logs and timing. Supports cancellation via `AbortSignal`.
1435
1501
  *
1436
1502
  * @param code - Source string fallback; pass the bundled artifact in `options.artifact`
1437
1503
  * @param csvPath - Input CSV path, or `null`
@@ -1447,7 +1513,6 @@ var DeeplineClient = class {
1447
1513
  * const logs = status.progress?.logs ?? [];
1448
1514
  * console.log(`[${status.status}] ${logs.length} log lines`);
1449
1515
  * },
1450
- * pollIntervalMs: 1000,
1451
1516
  * });
1452
1517
  *
1453
1518
  * if (result.success) {
@@ -1478,33 +1543,53 @@ var DeeplineClient = class {
1478
1543
  packagedFiles: options?.packagedFiles,
1479
1544
  force: options?.force
1480
1545
  });
1481
- const pollInterval = options?.pollIntervalMs ?? 500;
1482
1546
  const start = Date.now();
1483
- while (true) {
1547
+ const state = {
1548
+ runId: workflowId,
1549
+ status: "running",
1550
+ logs: [],
1551
+ latest: null
1552
+ };
1553
+ if (options?.signal?.aborted) {
1554
+ await this.cancelPlay(workflowId);
1555
+ return {
1556
+ success: false,
1557
+ runId: workflowId,
1558
+ logs: [],
1559
+ durationMs: Date.now() - start,
1560
+ error: "Cancelled by user"
1561
+ };
1562
+ }
1563
+ for await (const event of this.streamPlayRunEvents(workflowId, {
1564
+ mode: "cli",
1565
+ signal: options?.signal
1566
+ })) {
1484
1567
  if (options?.signal?.aborted) {
1485
1568
  await this.cancelPlay(workflowId);
1486
1569
  return {
1487
1570
  success: false,
1488
1571
  runId: workflowId,
1489
- logs: [],
1572
+ logs: state.logs,
1490
1573
  durationMs: Date.now() - start,
1491
1574
  error: "Cancelled by user"
1492
1575
  };
1493
1576
  }
1494
- const status = await this.getPlayStatus(workflowId);
1577
+ const status = updatePlayLiveStatusState(state, event);
1578
+ if (!status) {
1579
+ continue;
1580
+ }
1495
1581
  options?.onProgress?.(status);
1496
1582
  if (TERMINAL_PLAY_STATUSES.has(status.status)) {
1497
- return {
1498
- success: status.status === "completed",
1499
- runId: status.runId || workflowId,
1500
- result: status.result,
1501
- logs: status.progress?.logs ?? [],
1502
- durationMs: Date.now() - start,
1503
- error: status.progress?.error ?? (status.status !== "completed" ? status.status : void 0)
1504
- };
1583
+ const finalStatus = await this.getPlayStatus(status.runId || workflowId).catch(() => status);
1584
+ return playRunResultFromStatus(finalStatus, start, workflowId);
1505
1585
  }
1506
- await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
1507
1586
  }
1587
+ throw new DeeplineError(
1588
+ `Run stream for ${workflowId} ended before the run reached a terminal state.`,
1589
+ void 0,
1590
+ "PLAY_RUN_STREAM_ENDED",
1591
+ { runId: workflowId, workflowId }
1592
+ );
1508
1593
  }
1509
1594
  // ——————————————————————————————————————————————————————————
1510
1595
  // Health
@@ -1590,6 +1675,7 @@ var import_node_path2 = require("path");
1590
1675
  var import_node_child_process = require("child_process");
1591
1676
  var import_sync = require("csv-parse/sync");
1592
1677
  var import_sync2 = require("csv-stringify/sync");
1678
+ var BROWSER_FOCUS_COOLDOWN_MS = 3e4;
1593
1679
  function getAuthedHttpClient() {
1594
1680
  const config = resolveConfig();
1595
1681
  return { config, http: new HttpClient(config) };
@@ -1601,12 +1687,215 @@ async function writeOutputFile(filename, content) {
1601
1687
  await (0, import_promises.writeFile)(fullPath, content, "utf-8");
1602
1688
  return fullPath;
1603
1689
  }
1690
+ function browserFocusStateFile() {
1691
+ const homeDir = process.env.HOME || (0, import_node_os2.homedir)();
1692
+ return (0, import_node_path2.join)(
1693
+ homeDir,
1694
+ ".local",
1695
+ "deepline",
1696
+ "runtime",
1697
+ "state",
1698
+ "browser-focus.json"
1699
+ );
1700
+ }
1701
+ function claimBrowserFocus(now = Date.now()) {
1702
+ const statePath = browserFocusStateFile();
1703
+ try {
1704
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(statePath), { recursive: true });
1705
+ let lastFocusedAt = 0;
1706
+ if ((0, import_node_fs2.existsSync)(statePath)) {
1707
+ const payload = JSON.parse((0, import_node_fs2.readFileSync)(statePath, "utf-8"));
1708
+ const value = payload.lastFocusedAt ?? payload.last_focused_at;
1709
+ if (typeof value === "number" && Number.isFinite(value)) {
1710
+ lastFocusedAt = value;
1711
+ }
1712
+ }
1713
+ if (lastFocusedAt > now) {
1714
+ lastFocusedAt = 0;
1715
+ }
1716
+ if (now - lastFocusedAt < BROWSER_FOCUS_COOLDOWN_MS) {
1717
+ return false;
1718
+ }
1719
+ (0, import_node_fs2.writeFileSync)(statePath, JSON.stringify({ lastFocusedAt: now }), "utf-8");
1720
+ return true;
1721
+ } catch {
1722
+ return true;
1723
+ }
1724
+ }
1725
+ function extractUrlHost(raw) {
1726
+ try {
1727
+ const parsed = new URL(raw.includes("://") ? raw : `https://${raw}`);
1728
+ return parsed.port ? `${parsed.hostname.toLowerCase()}:${parsed.port}` : parsed.hostname.toLowerCase();
1729
+ } catch {
1730
+ return "";
1731
+ }
1732
+ }
1733
+ function browserAppNameFromBundleId(bundleId) {
1734
+ const names = {
1735
+ "com.google.chrome": "Google Chrome",
1736
+ "com.google.chrome.canary": "Google Chrome Canary",
1737
+ "com.microsoft.edgemac": "Microsoft Edge",
1738
+ "com.brave.browser": "Brave Browser",
1739
+ "com.operasoftware.opera": "Opera",
1740
+ "com.operasoftware.operagx": "Opera GX",
1741
+ "com.vivaldi.vivaldi": "Vivaldi",
1742
+ "company.thebrowser.browser": "Arc",
1743
+ "com.apple.safari": "Safari"
1744
+ };
1745
+ return names[bundleId.toLowerCase()] ?? "";
1746
+ }
1747
+ function readDefaultMacBrowserBundleId() {
1748
+ try {
1749
+ const output = (0, import_node_child_process.execFileSync)(
1750
+ "/usr/bin/defaults",
1751
+ [
1752
+ "read",
1753
+ `${(0, import_node_os2.homedir)()}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist`,
1754
+ "LSHandlers"
1755
+ ],
1756
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1757
+ );
1758
+ const httpsMatch = output.match(
1759
+ /LSHandlerURLScheme\s*=\s*https;[\s\S]*?LSHandlerRole(?:All|Viewer|Editor)\s*=\s*"([^"]+)"/
1760
+ );
1761
+ const httpMatch = output.match(
1762
+ /LSHandlerURLScheme\s*=\s*http;[\s\S]*?LSHandlerRole(?:All|Viewer|Editor)\s*=\s*"([^"]+)"/
1763
+ );
1764
+ return (httpsMatch?.[1] ?? httpMatch?.[1] ?? "").trim();
1765
+ } catch {
1766
+ return "";
1767
+ }
1768
+ }
1769
+ function browserStrategyForBundleId(bundleId) {
1770
+ const normalized = bundleId.toLowerCase();
1771
+ if ((/* @__PURE__ */ new Set([
1772
+ "com.google.chrome",
1773
+ "com.google.chrome.canary",
1774
+ "com.microsoft.edgemac",
1775
+ "com.brave.browser",
1776
+ "com.operasoftware.opera",
1777
+ "com.operasoftware.operagx",
1778
+ "com.vivaldi.vivaldi",
1779
+ "company.thebrowser.browser"
1780
+ ])).has(normalized)) {
1781
+ return "chromium";
1782
+ }
1783
+ return normalized === "com.apple.safari" ? "safari" : "fallback";
1784
+ }
1785
+ function runAppleScript(script, args) {
1786
+ const result = (0, import_node_child_process.spawnSync)("osascript", ["-", ...args], {
1787
+ input: script,
1788
+ encoding: "utf-8",
1789
+ stdio: ["pipe", "ignore", "ignore"],
1790
+ timeout: 5e3
1791
+ });
1792
+ return result.status === 0;
1793
+ }
1794
+ function retargetChromiumMacos(appName, targetUrl, allowFocus) {
1795
+ const host = extractUrlHost(targetUrl);
1796
+ if (!host) return false;
1797
+ const escapedAppName = appName.replace(/"/g, '\\"');
1798
+ const activateBlock = allowFocus ? " activate\n" : "";
1799
+ const foundTabBlock = allowFocus ? " set active tab index of w to i\n set index of w to 1\n set URL of t to targetUrl\n" : " set URL of t to targetUrl\n";
1800
+ const newTabBlock = allowFocus ? " tell window 1\n make new tab with properties {URL:targetUrl}\n set active tab index to (count of tabs)\n set index to 1\n end tell\n" : " tell window 1\n make new tab with properties {URL:targetUrl}\n end tell\n";
1801
+ const script = `
1802
+ on run argv
1803
+ set targetUrl to item 1 of argv
1804
+ set targetHost to item 2 of argv
1805
+ tell application "${escapedAppName}"
1806
+ ${activateBlock} if (count of windows) is 0 then
1807
+ make new window
1808
+ end if
1809
+ set foundTab to false
1810
+ repeat with w in windows
1811
+ set tabCount to count of tabs of w
1812
+ repeat with i from 1 to tabCount
1813
+ set t to tab i of w
1814
+ if (URL of t) contains targetHost then
1815
+ ${foundTabBlock} set foundTab to true
1816
+ exit repeat
1817
+ end if
1818
+ end repeat
1819
+ if foundTab then exit repeat
1820
+ end repeat
1821
+ if not foundTab then
1822
+ ${newTabBlock} end if
1823
+ end tell
1824
+ end run
1825
+ `;
1826
+ return runAppleScript(script, [targetUrl, host]);
1827
+ }
1828
+ function retargetSafariMacos(appName, targetUrl, allowFocus) {
1829
+ const host = extractUrlHost(targetUrl);
1830
+ if (!host) return false;
1831
+ const escapedAppName = appName.replace(/"/g, '\\"');
1832
+ const activateBlock = allowFocus ? " activate\n" : "";
1833
+ const foundTabBlock = allowFocus ? " set current tab of w to t\n set index of w to 1\n set URL of t to targetUrl\n" : " set URL of t to targetUrl\n";
1834
+ const newTabBlock = allowFocus ? " tell window 1\n set current tab to (make new tab with properties {URL:targetUrl})\n set index to 1\n end tell\n" : " tell window 1\n make new tab with properties {URL:targetUrl}\n end tell\n";
1835
+ const script = `
1836
+ on run argv
1837
+ set targetUrl to item 1 of argv
1838
+ set targetHost to item 2 of argv
1839
+ tell application "${escapedAppName}"
1840
+ ${activateBlock} if (count of windows) is 0 then
1841
+ make new document
1842
+ end if
1843
+ set foundTab to false
1844
+ repeat with w in windows
1845
+ set tabCount to count of tabs of w
1846
+ repeat with i from 1 to tabCount
1847
+ set t to tab i of w
1848
+ if (URL of t) contains targetHost then
1849
+ ${foundTabBlock} set foundTab to true
1850
+ exit repeat
1851
+ end if
1852
+ end repeat
1853
+ if foundTab then exit repeat
1854
+ end repeat
1855
+ if not foundTab then
1856
+ ${newTabBlock} end if
1857
+ end tell
1858
+ end run
1859
+ `;
1860
+ return runAppleScript(script, [targetUrl, host]);
1861
+ }
1862
+ function openUrlMacos(targetUrl, allowFocus) {
1863
+ const defaultBundleId = readDefaultMacBrowserBundleId();
1864
+ const appName = defaultBundleId ? browserAppNameFromBundleId(defaultBundleId) : "";
1865
+ const strategy = browserStrategyForBundleId(defaultBundleId);
1866
+ if (appName && strategy === "chromium" && retargetChromiumMacos(appName, targetUrl, allowFocus)) {
1867
+ return true;
1868
+ }
1869
+ if (appName && strategy === "safari" && retargetSafariMacos(appName, targetUrl, allowFocus)) {
1870
+ return true;
1871
+ }
1872
+ if (!allowFocus) {
1873
+ return false;
1874
+ }
1875
+ try {
1876
+ (0, import_node_child_process.execFileSync)("open", [targetUrl], { stdio: "ignore" });
1877
+ return true;
1878
+ } catch {
1879
+ return false;
1880
+ }
1881
+ }
1604
1882
  function openInBrowser(url) {
1605
1883
  try {
1606
- if (process.platform === "darwin") (0, import_node_child_process.execSync)(`open "${url}"`, { stdio: "ignore" });
1607
- else if (process.platform === "win32")
1608
- (0, import_node_child_process.execSync)(`start "" "${url}"`, { stdio: "ignore" });
1609
- else (0, import_node_child_process.execSync)(`xdg-open "${url}"`, { stdio: "ignore" });
1884
+ const targetUrl = String(url || "").trim();
1885
+ if (!targetUrl) return;
1886
+ const allowFocus = claimBrowserFocus();
1887
+ if (process.platform === "darwin") {
1888
+ openUrlMacos(targetUrl, allowFocus);
1889
+ return;
1890
+ }
1891
+ if (!allowFocus) return;
1892
+ if (process.platform === "win32") {
1893
+ (0, import_node_child_process.execFileSync)("cmd.exe", ["/c", "start", "", targetUrl], {
1894
+ stdio: "ignore"
1895
+ });
1896
+ return;
1897
+ }
1898
+ (0, import_node_child_process.execFileSync)("xdg-open", [targetUrl], { stdio: "ignore" });
1610
1899
  } catch {
1611
1900
  }
1612
1901
  }
@@ -1751,7 +2040,7 @@ function buildCandidateUrls2(url) {
1751
2040
  return [url];
1752
2041
  }
1753
2042
  }
1754
- function sleep2(ms) {
2043
+ function sleep3(ms) {
1755
2044
  return new Promise((resolve8) => setTimeout(resolve8, ms));
1756
2045
  }
1757
2046
  function printDeeplineLogo() {
@@ -1834,7 +2123,7 @@ async function handleRegister(args) {
1834
2123
  return EXIT_AUTH;
1835
2124
  }
1836
2125
  if (s >= 500 || s === 0 || s === 400) {
1837
- await sleep2(2e3);
2126
+ await sleep3(2e3);
1838
2127
  continue;
1839
2128
  }
1840
2129
  if (s >= 400) {
@@ -1858,7 +2147,7 @@ async function handleRegister(args) {
1858
2147
  console.log("That approval link expired. Please run: deepline auth register");
1859
2148
  return EXIT_AUTH;
1860
2149
  }
1861
- await sleep2(2e3);
2150
+ await sleep3(2e3);
1862
2151
  }
1863
2152
  }
1864
2153
  async function handleWait(args) {
@@ -1895,7 +2184,7 @@ async function handleWait(args) {
1895
2184
  return EXIT_AUTH;
1896
2185
  }
1897
2186
  if (status >= 500 || status === 0 || status === 400) {
1898
- await sleep2(2e3);
2187
+ await sleep3(2e3);
1899
2188
  continue;
1900
2189
  }
1901
2190
  if (status >= 400) {
@@ -1919,7 +2208,7 @@ async function handleWait(args) {
1919
2208
  console.error("That approval link expired. Run: deepline auth register");
1920
2209
  return EXIT_AUTH;
1921
2210
  }
1922
- await sleep2(2e3);
2211
+ await sleep3(2e3);
1923
2212
  }
1924
2213
  console.error("Still pending. Approve the browser link, then run: deepline auth wait");
1925
2214
  return EXIT_AUTH;
@@ -3862,13 +4151,6 @@ var PLAY_DEDUP_BACKENDS = {
3862
4151
 
3863
4152
  // ../shared_libs/play-runtime/profiles.ts
3864
4153
  var PLAY_EXECUTION_PROFILES = {
3865
- legacy: {
3866
- id: "legacy",
3867
- scheduler: PLAY_SCHEDULER_BACKENDS.temporal,
3868
- runner: PLAY_RUNTIME_BACKENDS.daytona,
3869
- dedup: PLAY_DEDUP_BACKENDS.inMemory,
3870
- label: "Daytona + Temporal (production today)"
3871
- },
3872
4154
  workers_edge: {
3873
4155
  id: "workers_edge",
3874
4156
  scheduler: PLAY_SCHEDULER_BACKENDS.cfWorkflows,
@@ -4919,7 +5201,7 @@ function formatTimestamp(value) {
4919
5201
  function formatRunLine(run) {
4920
5202
  return `${run.workflowId} ${run.status} ${formatTimestamp(run.startTime)}`;
4921
5203
  }
4922
- function isTransientPlayStatusPollError(error) {
5204
+ function isTransientPlayStreamError(error) {
4923
5205
  if (error instanceof DeeplineError && typeof error.statusCode === "number") {
4924
5206
  return error.statusCode >= 500 && error.statusCode < 600;
4925
5207
  }
@@ -4928,12 +5210,6 @@ function isTransientPlayStatusPollError(error) {
4928
5210
  text
4929
5211
  );
4930
5212
  }
4931
- function isTerminalPlayStatusPollError(input) {
4932
- if (input.error instanceof DeeplineError && input.error.statusCode === 404 && input.hasSeenRun) {
4933
- return true;
4934
- }
4935
- return false;
4936
- }
4937
5213
  var TERMINAL_PLAY_STATUSES2 = /* @__PURE__ */ new Set([
4938
5214
  "completed",
4939
5215
  "failed",
@@ -4999,6 +5275,12 @@ function buildPlayDashboardUrl(baseUrl, playName) {
4999
5275
  const encodedPlayName = encodeURIComponent(playName);
5000
5276
  return `${trimmedBase}/dashboard/plays/${encodedPlayName}`;
5001
5277
  }
5278
+ function openPlayDashboard(input) {
5279
+ if (input.jsonOutput || input.noOpen || !input.dashboardUrl) {
5280
+ return;
5281
+ }
5282
+ openInBrowser(input.dashboardUrl);
5283
+ }
5002
5284
  function getDashboardUrlFromLiveEvent(event) {
5003
5285
  const dashboardUrl = getEventPayload(event).dashboardUrl;
5004
5286
  return typeof dashboardUrl === "string" && dashboardUrl.trim() ? dashboardUrl.trim() : null;
@@ -5035,6 +5317,68 @@ function assertPlayWaitNotTimedOut(input) {
5035
5317
  );
5036
5318
  }
5037
5319
  }
5320
+ async function waitForPlayCompletionByStream(input) {
5321
+ const controller = new AbortController();
5322
+ let timedOut = false;
5323
+ let lastPhase = null;
5324
+ const timeout = input.waitTimeoutMs === null ? null : setTimeout(
5325
+ () => {
5326
+ timedOut = true;
5327
+ controller.abort();
5328
+ },
5329
+ Math.max(1, input.waitTimeoutMs - (Date.now() - input.startedAt))
5330
+ );
5331
+ try {
5332
+ for await (const event of input.client.streamPlayRunEvents(
5333
+ input.workflowId,
5334
+ { signal: controller.signal }
5335
+ )) {
5336
+ assertPlayWaitNotTimedOut({ ...input, lastPhase });
5337
+ const phase = describeLiveEventPhase(event);
5338
+ if (phase) {
5339
+ lastPhase = phase;
5340
+ input.progress.phase(phase);
5341
+ }
5342
+ printPlayLogLines({
5343
+ lines: getLogLinesFromLiveEvent(event),
5344
+ status: null,
5345
+ jsonOutput: input.jsonOutput,
5346
+ emitLogs: input.emitLogs,
5347
+ state: input.state,
5348
+ progress: input.progress
5349
+ });
5350
+ const status = getStatusFromLiveEvent(event);
5351
+ if (status && TERMINAL_PLAY_STATUSES2.has(status)) {
5352
+ const finalStatus = await input.client.getPlayStatus(input.workflowId, {
5353
+ billing: false
5354
+ });
5355
+ if (TERMINAL_PLAY_STATUSES2.has(finalStatus.status)) {
5356
+ return finalStatus;
5357
+ }
5358
+ }
5359
+ }
5360
+ } catch (error) {
5361
+ if (timedOut) {
5362
+ assertPlayWaitNotTimedOut({ ...input, lastPhase });
5363
+ }
5364
+ throw error;
5365
+ } finally {
5366
+ if (timeout) {
5367
+ clearTimeout(timeout);
5368
+ }
5369
+ }
5370
+ const phaseSuffix = lastPhase && lastPhase.trim() ? ` (last observed phase: ${lastPhase.trim()})` : "";
5371
+ throw new DeeplineError(
5372
+ `Play live stream ended before the run reached a terminal state runId=${input.workflowId}${phaseSuffix}.`,
5373
+ void 0,
5374
+ "PLAY_LIVE_STREAM_ENDED",
5375
+ {
5376
+ runId: input.workflowId,
5377
+ workflowId: input.workflowId,
5378
+ ...lastPhase ? { phase: lastPhase } : {}
5379
+ }
5380
+ );
5381
+ }
5038
5382
  async function startAndWaitForPlayCompletionByStream(input) {
5039
5383
  const startedAt = Date.now();
5040
5384
  const state = {
@@ -5068,6 +5412,11 @@ async function startAndWaitForPlayCompletionByStream(input) {
5068
5412
  const workflowId = lastKnownWorkflowId || "pending";
5069
5413
  if (workflowId !== "pending" && !emittedDashboardUrl) {
5070
5414
  const dashboardUrl = getDashboardUrlFromLiveEvent(event) ?? buildPlayDashboardUrl(input.client.baseUrl, input.playName);
5415
+ openPlayDashboard({
5416
+ dashboardUrl,
5417
+ jsonOutput: input.jsonOutput,
5418
+ noOpen: input.noOpen
5419
+ });
5071
5420
  if (!input.jsonOutput) {
5072
5421
  writeStartedPlayRun({
5073
5422
  runId: workflowId,
@@ -5123,21 +5472,21 @@ async function startAndWaitForPlayCompletionByStream(input) {
5123
5472
  lastPhase
5124
5473
  });
5125
5474
  }
5126
- if (lastKnownWorkflowId && isTransientPlayStatusPollError(error)) {
5475
+ if (lastKnownWorkflowId && isTransientPlayStreamError(error)) {
5127
5476
  if (timeout) {
5128
5477
  clearTimeout(timeout);
5129
5478
  }
5130
5479
  const reason = error instanceof Error ? error.message : String(error);
5131
5480
  if (!input.jsonOutput) {
5132
5481
  process.stderr.write(
5133
- `[play watch] start stream failed after run ${lastKnownWorkflowId}; falling back to polling (${reason})
5482
+ `[play watch] start stream failed after run ${lastKnownWorkflowId}; reconnecting to run stream (${reason})
5134
5483
  `
5135
5484
  );
5136
5485
  }
5137
5486
  recordCliTrace({
5138
- phase: "cli.play_start_stream_fallback",
5487
+ phase: "cli.play_start_stream_reconnect",
5139
5488
  ms: Date.now() - startedAt,
5140
- ok: false,
5489
+ ok: true,
5141
5490
  playName: input.playName,
5142
5491
  workflowId: lastKnownWorkflowId,
5143
5492
  eventCount,
@@ -5145,10 +5494,9 @@ async function startAndWaitForPlayCompletionByStream(input) {
5145
5494
  lastPhase,
5146
5495
  reason
5147
5496
  });
5148
- return waitForPlayCompletionByPolling({
5497
+ return waitForPlayCompletionByStream({
5149
5498
  client: input.client,
5150
5499
  workflowId: lastKnownWorkflowId,
5151
- pollIntervalMs: 500,
5152
5500
  jsonOutput: input.jsonOutput,
5153
5501
  emitLogs: input.emitLogs,
5154
5502
  waitTimeoutMs: input.waitTimeoutMs,
@@ -5166,13 +5514,13 @@ async function startAndWaitForPlayCompletionByStream(input) {
5166
5514
  if (lastKnownWorkflowId) {
5167
5515
  if (!input.jsonOutput) {
5168
5516
  input.progress.writeLine(
5169
- `[play watch] start stream ended after run ${lastKnownWorkflowId}; falling back to polling`
5517
+ `[play watch] start stream ended after run ${lastKnownWorkflowId}; reconnecting to run stream`
5170
5518
  );
5171
5519
  }
5172
5520
  recordCliTrace({
5173
- phase: "cli.play_start_stream_fallback",
5521
+ phase: "cli.play_start_stream_reconnect",
5174
5522
  ms: Date.now() - startedAt,
5175
- ok: false,
5523
+ ok: true,
5176
5524
  playName: input.playName,
5177
5525
  workflowId: lastKnownWorkflowId,
5178
5526
  eventCount,
@@ -5180,10 +5528,9 @@ async function startAndWaitForPlayCompletionByStream(input) {
5180
5528
  lastPhase,
5181
5529
  reason: "stream ended before terminal event"
5182
5530
  });
5183
- return waitForPlayCompletionByPolling({
5531
+ return waitForPlayCompletionByStream({
5184
5532
  client: input.client,
5185
5533
  workflowId: lastKnownWorkflowId,
5186
- pollIntervalMs: 500,
5187
5534
  jsonOutput: input.jsonOutput,
5188
5535
  emitLogs: input.emitLogs,
5189
5536
  waitTimeoutMs: input.waitTimeoutMs,
@@ -5204,72 +5551,6 @@ async function startAndWaitForPlayCompletionByStream(input) {
5204
5551
  }
5205
5552
  );
5206
5553
  }
5207
- async function waitForPlayCompletionByPolling(input) {
5208
- let lastTransientPollWarningAt = 0;
5209
- let hasSeenRun = false;
5210
- while (true) {
5211
- assertPlayWaitNotTimedOut(input);
5212
- let status;
5213
- try {
5214
- status = await input.client.getPlayTailStatus(input.workflowId, {
5215
- afterLogIndex: input.state.lastLogIndex,
5216
- // Keep the server-side tail wait close to the caller's requested poll
5217
- // cadence. A long wait makes tiny remote runs look slow whenever the
5218
- // terminal update lands just after the held request starts.
5219
- waitMs: Math.max(50, Math.min(input.pollIntervalMs, 1e3))
5220
- });
5221
- } catch (error) {
5222
- if (isTerminalPlayStatusPollError({ error, hasSeenRun })) {
5223
- throw new DeeplineError(
5224
- `Play run ${input.workflowId} no longer exists on the server (404). The run was deleted or the backend lost it.`,
5225
- 404,
5226
- "PLAY_RUN_NOT_FOUND"
5227
- );
5228
- }
5229
- if (!isTransientPlayStatusPollError(error)) {
5230
- throw error;
5231
- }
5232
- const now = Date.now();
5233
- if (now - lastTransientPollWarningAt >= 3e4) {
5234
- const message = error instanceof Error ? error.message : String(error);
5235
- input.progress.writeLine(
5236
- `[play tail] transient status poll failed; retrying: ${message}`
5237
- );
5238
- lastTransientPollWarningAt = now;
5239
- }
5240
- await new Promise(
5241
- (resolvePromise) => setTimeout(resolvePromise, input.pollIntervalMs)
5242
- );
5243
- continue;
5244
- }
5245
- hasSeenRun = true;
5246
- const logs = status.progress?.logs ?? [];
5247
- input.progress.phase(status.status);
5248
- printPlayLogLines({
5249
- lines: logs.slice(input.state.lastLogIndex),
5250
- status,
5251
- jsonOutput: input.jsonOutput,
5252
- emitLogs: input.emitLogs,
5253
- state: input.state,
5254
- progress: input.progress
5255
- });
5256
- if (TERMINAL_PLAY_STATUSES2.has(status.status)) {
5257
- return status.result !== void 0 ? status : await input.client.getPlayStatus(input.workflowId, { billing: false });
5258
- }
5259
- const authoritativeStatus = await input.client.getPlayStatus(
5260
- input.workflowId,
5261
- { billing: false }
5262
- );
5263
- if (TERMINAL_PLAY_STATUSES2.has(authoritativeStatus.status)) {
5264
- return authoritativeStatus;
5265
- }
5266
- if ((status.progress?.logs ?? []).length === input.state.lastLogIndex) {
5267
- await new Promise(
5268
- (resolvePromise) => setTimeout(resolvePromise, input.pollIntervalMs)
5269
- );
5270
- }
5271
- }
5272
- }
5273
5554
  function formatInteger(value) {
5274
5555
  return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : String(value ?? "-");
5275
5556
  }
@@ -5410,7 +5691,6 @@ function buildRunWarnings(status, rowsInfo) {
5410
5691
  function buildRunNextCommands(runId, rowsInfo) {
5411
5692
  const commands = {
5412
5693
  get: `deepline runs get ${runId} --json`,
5413
- tail: `deepline runs tail ${runId} --json`,
5414
5694
  stop: `deepline runs stop ${runId} --reason "stale lock" --json`,
5415
5695
  logs: `deepline runs logs ${runId} --out run.log --json`
5416
5696
  };
@@ -5526,10 +5806,9 @@ function normalizeErrorsForEnvelope(status, error) {
5526
5806
  }
5527
5807
  function normalizeLogsForEnvelope(status) {
5528
5808
  const logs = Array.isArray(status.progress?.logs) ? status.progress.logs : [];
5529
- const offset = typeof status.progress?.logOffset === "number" && Number.isFinite(status.progress.logOffset) ? Math.max(0, Math.trunc(status.progress.logOffset)) : 0;
5530
- const totalCount = offset + logs.length;
5809
+ const totalCount = logs.length;
5531
5810
  const entries = logs.slice(Math.max(0, logs.length - RUN_LOG_PREVIEW_LIMIT));
5532
- const firstSequence = entries.length === 0 ? null : offset + logs.length - entries.length + 1;
5811
+ const firstSequence = entries.length === 0 ? null : logs.length - entries.length + 1;
5533
5812
  const lastSequence = totalCount === 0 ? null : totalCount;
5534
5813
  return {
5535
5814
  totalCount,
@@ -5775,7 +6054,6 @@ function writeStartedPlayRun(input) {
5775
6054
  workflowId: input.runId,
5776
6055
  name: input.playName,
5777
6056
  status: input.status ?? "started",
5778
- statusUrl: input.statusUrl,
5779
6057
  dashboardUrl: input.dashboardUrl
5780
6058
  };
5781
6059
  if (input.jsonOutput) {
@@ -5787,7 +6065,7 @@ function writeStartedPlayRun(input) {
5787
6065
  `Started ${input.playName}`,
5788
6066
  ` run id: ${input.runId}`,
5789
6067
  ` get status: deepline runs get ${input.runId} --json`,
5790
- ` tail logs: deepline runs tail ${input.runId} --json`,
6068
+ ` logs: deepline runs logs ${input.runId} --json`,
5791
6069
  ` stop run: deepline runs stop ${input.runId} --reason "stale lock" --json`,
5792
6070
  ` result JSON: deepline runs get ${input.runId} --json`
5793
6071
  ];
@@ -5802,7 +6080,7 @@ function writeStartedPlayRun(input) {
5802
6080
  console.log(output);
5803
6081
  }
5804
6082
  function parsePlayRunOptions(args) {
5805
- const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--live|--latest|--revision-id <id>] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--json] [--<input> value]";
6083
+ const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--live|--latest|--revision-id <id>] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--no-open] [--json] [--<input> value]\nRun `deepline plays run --help` for idempotency, tool call id, and ctx.map guidance.";
5806
6084
  let filePath = null;
5807
6085
  let playName = null;
5808
6086
  let input = null;
@@ -5812,8 +6090,8 @@ function parsePlayRunOptions(args) {
5812
6090
  let jsonOutput = watch ? args.includes("--json") : argsWantJson(args);
5813
6091
  const emitLogs = !jsonOutput || args.includes("--logs");
5814
6092
  const force = args.includes("--force");
6093
+ const noOpen = args.includes("--no-open");
5815
6094
  let outPath = null;
5816
- let pollIntervalMs = 500;
5817
6095
  let waitTimeoutMs = null;
5818
6096
  for (let index = 0; index < args.length; index += 1) {
5819
6097
  const arg = args[index];
@@ -5845,15 +6123,16 @@ function parsePlayRunOptions(args) {
5845
6123
  outPath = (0, import_node_path8.resolve)(args[++index]);
5846
6124
  continue;
5847
6125
  }
5848
- if ((arg === "--poll-interval-ms" || arg === "--interval-ms") && args[index + 1]) {
5849
- pollIntervalMs = parsePositiveInteger2(args[++index], arg);
5850
- continue;
6126
+ if (arg === "--poll-interval-ms" || arg === "--interval-ms") {
6127
+ throw new Error(
6128
+ `${arg} was removed. --watch uses the canonical run stream directly.`
6129
+ );
5851
6130
  }
5852
6131
  if ((arg === "--tail-timeout-ms" || arg === "--timeout-ms") && args[index + 1]) {
5853
6132
  waitTimeoutMs = parsePositiveInteger2(args[++index], arg);
5854
6133
  continue;
5855
6134
  }
5856
- if (arg === "--json" || arg === "--wait" || arg === "--tail" || arg === "--watch" || arg === "--logs" || arg === "--force") {
6135
+ if (arg === "--json" || arg === "--wait" || arg === "--tail" || arg === "--watch" || arg === "--logs" || arg === "--force" || arg === "--no-open") {
5857
6136
  if (arg === "--watch") {
5858
6137
  continue;
5859
6138
  }
@@ -5922,10 +6201,10 @@ function parsePlayRunOptions(args) {
5922
6201
  watch,
5923
6202
  emitLogs,
5924
6203
  jsonOutput,
5925
- pollIntervalMs,
5926
6204
  waitTimeoutMs,
5927
6205
  force,
5928
- outPath
6206
+ outPath,
6207
+ noOpen
5929
6208
  };
5930
6209
  }
5931
6210
  function parsePlayCheckOptions(args) {
@@ -6100,6 +6379,7 @@ async function handleFileBackedRun(options) {
6100
6379
  jsonOutput: options.jsonOutput,
6101
6380
  emitLogs: options.emitLogs,
6102
6381
  waitTimeoutMs: options.waitTimeoutMs,
6382
+ noOpen: options.noOpen,
6103
6383
  progress
6104
6384
  })
6105
6385
  );
@@ -6127,14 +6407,19 @@ async function handleFileBackedRun(options) {
6127
6407
  () => client.startPlayRun(startRequest)
6128
6408
  );
6129
6409
  const dashboardUrl = buildPlayDashboardUrl(client.baseUrl, playName);
6130
- progress.phase(`loading play on ${dashboardUrl}`);
6410
+ const resolvedDashboardUrl = started.dashboardUrl ?? dashboardUrl;
6411
+ openPlayDashboard({
6412
+ dashboardUrl: resolvedDashboardUrl,
6413
+ jsonOutput: options.jsonOutput,
6414
+ noOpen: options.noOpen
6415
+ });
6416
+ progress.phase(`loading play on ${resolvedDashboardUrl}`);
6131
6417
  progress.complete();
6132
6418
  writeStartedPlayRun({
6133
6419
  runId: started.workflowId,
6134
6420
  playName,
6135
6421
  status: started.status,
6136
- statusUrl: started.statusUrl,
6137
- dashboardUrl: started.dashboardUrl ?? dashboardUrl,
6422
+ dashboardUrl: resolvedDashboardUrl,
6138
6423
  jsonOutput: options.jsonOutput,
6139
6424
  progress
6140
6425
  });
@@ -6239,6 +6524,7 @@ async function handleNamedRun(options) {
6239
6524
  jsonOutput: options.jsonOutput,
6240
6525
  emitLogs: options.emitLogs,
6241
6526
  waitTimeoutMs: options.waitTimeoutMs,
6527
+ noOpen: options.noOpen,
6242
6528
  progress
6243
6529
  })
6244
6530
  );
@@ -6269,14 +6555,19 @@ async function handleNamedRun(options) {
6269
6555
  client.baseUrl,
6270
6556
  playName
6271
6557
  );
6272
- progress.phase(`loading play on ${dashboardUrl}`);
6558
+ const resolvedDashboardUrl = started.dashboardUrl ?? dashboardUrl;
6559
+ openPlayDashboard({
6560
+ dashboardUrl: resolvedDashboardUrl,
6561
+ jsonOutput: options.jsonOutput,
6562
+ noOpen: options.noOpen
6563
+ });
6564
+ progress.phase(`loading play on ${resolvedDashboardUrl}`);
6273
6565
  progress.complete();
6274
6566
  writeStartedPlayRun({
6275
6567
  runId: started.workflowId,
6276
6568
  playName: started.name ?? playName,
6277
6569
  status: started.status,
6278
- statusUrl: started.statusUrl,
6279
- dashboardUrl: started.dashboardUrl ?? dashboardUrl,
6570
+ dashboardUrl: resolvedDashboardUrl,
6280
6571
  jsonOutput: options.jsonOutput,
6281
6572
  progress
6282
6573
  });
@@ -6319,7 +6610,7 @@ function parseRunIdPositional(args, usage) {
6319
6610
  }
6320
6611
  continue;
6321
6612
  }
6322
- if ((arg === "--out" || arg === "--cursor" || arg === "--reason" || arg === "--interval-ms" || arg === "--poll-interval-ms") && args[index + 1]) {
6613
+ if ((arg === "--out" || arg === "--reason") && args[index + 1]) {
6323
6614
  index += 1;
6324
6615
  continue;
6325
6616
  }
@@ -6404,7 +6695,7 @@ async function handleRunsList(args) {
6404
6695
  return 0;
6405
6696
  }
6406
6697
  async function handleRunTail(args) {
6407
- const usage = "Usage: deepline runs tail <run-id> [--json] [--compact] [--cursor <cursor>]";
6698
+ const usage = "Usage: deepline runs tail <run-id> [--json] [--compact]";
6408
6699
  let runId;
6409
6700
  try {
6410
6701
  runId = parseRunIdPositional(args, usage);
@@ -6412,20 +6703,19 @@ async function handleRunTail(args) {
6412
6703
  console.error(error instanceof Error ? error.message : usage);
6413
6704
  return 1;
6414
6705
  }
6415
- const client = new DeeplineClient();
6416
- let afterLogIndex;
6417
6706
  for (let index = 0; index < args.length; index += 1) {
6418
6707
  const arg = args[index];
6419
- if (arg === "--cursor" && args[index + 1]) {
6420
- const parsed = Number(args[++index]);
6421
- if (Number.isInteger(parsed) && parsed >= 0) {
6422
- afterLogIndex = parsed;
6423
- }
6708
+ if (arg === "--cursor") {
6709
+ console.error("--cursor was removed. deepline runs tail reads the canonical run stream.");
6710
+ return 1;
6711
+ }
6712
+ if (arg.startsWith("--") && arg !== "--json" && arg !== "--compact") {
6713
+ console.error(`${arg} is not supported by deepline runs tail.`);
6714
+ return 1;
6424
6715
  }
6425
6716
  }
6426
- const status = await client.runs.tail(runId, {
6427
- ...afterLogIndex !== void 0 ? { afterLogIndex } : {}
6428
- });
6717
+ const client = new DeeplineClient();
6718
+ const status = await client.runs.tail(runId);
6429
6719
  writePlayResult(status, argsWantJson(args));
6430
6720
  return status.status === "failed" ? 1 : 0;
6431
6721
  }
@@ -6966,8 +7256,9 @@ function registerPlayCommands(program) {
6966
7256
  "after",
6967
7257
  `
6968
7258
  Concepts:
6969
- Plays are durable Deepline cloud workflows. Local .play.ts files are bundled locally,
6970
- then validated and executed in Deepline cloud.
7259
+ Plays are durable cloud workflows.
7260
+ Stable ctx.tools.execute({ id, tool, input }) calls are replay-safe.
7261
+ ctx.map adds row keys and row progress.
6971
7262
 
6972
7263
  Common commands:
6973
7264
  deepline plays search email --json
@@ -6998,17 +7289,42 @@ Examples:
6998
7289
  "after",
6999
7290
  `
7000
7291
  Notes:
7001
- Local play files are bundled locally, then validated and executed in Deepline cloud.
7002
- Named plays run the stored live cloud revision.
7003
- Unknown --foo and --foo.bar flags are treated as play input args.
7004
- File-like input args accept local paths; the CLI stages those files before submit.
7005
- Run performs server preflight automatically. Use \`deepline plays check <file>\`
7006
- to validate without starting a run.
7292
+ Local files are bundled, preflighted, then run in Deepline cloud.
7293
+ Named plays run the live saved revision.
7294
+ Unknown --foo and --foo.bar flags are treated as play input args.
7295
+ File args accept local paths; the CLI stages files before submit.
7296
+ --watch prints logs, previews, stats, and next commands.
7297
+ The play page opens in your browser as soon as the run starts; use --no-open
7298
+ to only print the URL.
7299
+ --force supersedes active runs; it does not bypass completed reuse.
7300
+
7301
+ Idempotent execution:
7302
+ Stable tool call ids are the reuse key:
7303
+
7304
+ await ctx.tools.execute({ id: 'company_lookup', tool, input });
7305
+
7306
+ For rows, use ctx.map plus a stable row key:
7307
+
7308
+ const rows = await ctx
7309
+ .map('companies_v1', companies)
7310
+ .step('cto', (row, ctx) => ctx.tools.execute({
7311
+ id: 'find_cto',
7312
+ tool: 'apollo_search_people_with_match',
7313
+ input: { q_organization_domains_list: [row.domain], per_page: 1 },
7314
+ }))
7315
+ .run({ key: 'domain' });
7316
+
7317
+ Reuse needs the same play, tool id, map name, row key, and compatible logic.
7318
+ To refresh, change the id/map key or set staleAfterSeconds:
7319
+
7320
+ .run({ key: 'domain', staleAfterSeconds: 86400 })
7007
7321
 
7008
7322
  Examples:
7009
7323
  deepline plays run my.play.ts --input '{"domain":"stripe.com"}' --watch
7010
7324
  deepline plays run person-linkedin-to-email --input '{"linkedin_url":"..."}' --watch
7011
7325
  deepline plays run enrich.play.ts --csv leads.csv --watch --out leads-enriched.csv
7326
+ deepline plays run cto-search.play.ts --limit 5 --watch
7327
+ deepline runs get <run-id>
7012
7328
  `
7013
7329
  ).option("--file <path>", "Local play file to run").option("--name <name>", "Saved play name to run").option("-i, --input <json>", "Input JSON object or @file path").option("--live", "Run the current live revision explicitly").option("--latest", "Run the newest saved revision, even if it is not live").option(
7014
7330
  "--revision-id <id>",
@@ -7019,7 +7335,7 @@ Examples:
7019
7335
  ).option("--watch", "Stream logs until completion").option(
7020
7336
  "--logs",
7021
7337
  "When output is non-interactive, stream play logs to stderr while waiting"
7022
- ).option("--poll-interval-ms <ms>", "Polling interval while tailing").option("--tail-timeout-ms <ms>", "Timeout while tailing").option("--force", "Supersede any active runs for this play").option("--json", "Emit JSON output").action(async (target, options, command) => {
7338
+ ).option("--tail-timeout-ms <ms>", "Timeout while watching the run stream").option("--force", "Supersede any active runs for this play").option("--no-open", "Print the play page URL without opening a browser").option("--json", "Emit JSON output").action(async (target, options, command) => {
7023
7339
  const passthroughArgs = [...command.args];
7024
7340
  const explicitTarget = options.file || options.name;
7025
7341
  const targetIsInputFlag = typeof target === "string" && target.startsWith("--");
@@ -7041,9 +7357,9 @@ Examples:
7041
7357
  ...options.out ? ["--out", options.out] : [],
7042
7358
  ...options.watch ? ["--watch"] : [],
7043
7359
  ...options.logs ? ["--logs"] : [],
7044
- ...options.pollIntervalMs ? ["--poll-interval-ms", options.pollIntervalMs] : [],
7045
7360
  ...options.tailTimeoutMs ? ["--tail-timeout-ms", options.tailTimeoutMs] : [],
7046
7361
  ...options.force ? ["--force"] : [],
7362
+ ...options.open === false ? ["--no-open"] : [],
7047
7363
  ...options.json ? ["--json"] : [],
7048
7364
  ...passthroughArgs
7049
7365
  ]);
@@ -7150,12 +7466,11 @@ Examples:
7150
7466
  ...options.json ? ["--json"] : []
7151
7467
  ]);
7152
7468
  });
7153
- runs.command("tail <runId>").description("Tail or fetch recent live events for a play run.").option("--json", "Emit JSON output. Also automatic when stdout is piped").option("--compact", "Drop verbose fields from JSON output").option("--cursor <cursor>", "Resume after a previously returned event cursor").action(async (runId, options) => {
7469
+ runs.command("tail <runId>").description("Read the canonical live stream for a play run.").option("--json", "Emit JSON output. Also automatic when stdout is piped").option("--compact", "Drop verbose fields from JSON output").action(async (runId, options) => {
7154
7470
  process.exitCode = await handleRunTail([
7155
7471
  runId,
7156
7472
  ...options.json ? ["--json"] : [],
7157
- ...options.compact ? ["--compact"] : [],
7158
- ...options.cursor ? ["--cursor", options.cursor] : []
7473
+ ...options.compact ? ["--compact"] : []
7159
7474
  ]);
7160
7475
  });
7161
7476
  runs.command("logs <runId>").description("Fetch persisted logs for a play run.").option("--limit <count>", "Maximum recent log lines to print without --out", "200").option("--out <path>", "Write the full persisted log stream to a file").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (runId, options) => {