deepline 0.1.109 → 0.1.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cli/index.js +2226 -1271
  2. package/dist/cli/index.mjs +2303 -1355
  3. package/dist/index.d.mts +21 -14
  4. package/dist/index.d.ts +21 -14
  5. package/dist/index.js +97 -23
  6. package/dist/index.mjs +97 -23
  7. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +42 -20
  8. package/dist/repo/apps/play-runner-workers/src/entry.ts +135 -45
  9. package/dist/repo/apps/play-runner-workers/src/runtime/receipts.ts +18 -27
  10. package/dist/repo/apps/play-runner-workers/src/workflow-instance-create.ts +44 -0
  11. package/dist/repo/apps/play-runner-workers/src/workflow-retry.ts +7 -11
  12. package/dist/repo/sdk/src/client.ts +35 -12
  13. package/dist/repo/sdk/src/errors.ts +2 -2
  14. package/dist/repo/sdk/src/http.ts +87 -7
  15. package/dist/repo/sdk/src/play.ts +1 -1
  16. package/dist/repo/sdk/src/plays/bundle-play-file.ts +5 -1
  17. package/dist/repo/sdk/src/release.ts +13 -10
  18. package/dist/repo/sdk/src/tool-output.ts +2 -2
  19. package/dist/repo/sdk/src/types.ts +9 -6
  20. package/dist/repo/shared_libs/play-runtime/fullenrich-batching.ts +229 -0
  21. package/dist/repo/shared_libs/play-runtime/governor/policy.ts +1 -1
  22. package/dist/repo/shared_libs/play-runtime/play-runtime-batching-registry.ts +20 -0
  23. package/dist/repo/shared_libs/play-runtime/run-failure.ts +20 -12
  24. package/dist/repo/shared_libs/play-runtime/run-ledger.ts +107 -56
  25. package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +4 -2
  26. package/dist/repo/shared_libs/play-runtime/secret-redaction.ts +15 -0
  27. package/dist/repo/shared_libs/play-runtime/work-receipts.ts +1 -0
  28. package/dist/repo/shared_libs/plays/bundling/index.ts +69 -11
  29. package/dist/repo/shared_libs/plays/static-pipeline.ts +1 -3
  30. package/dist/repo/shared_libs/security/outbound-url-policy.ts +238 -0
  31. package/dist/repo/shared_libs/security/safe-fetch.ts +118 -0
  32. package/dist/viewer/viewer.css +617 -0
  33. package/dist/viewer/viewer.js +1496 -0
  34. package/package.json +5 -1
@@ -1,8 +1,165 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli/proxy-env.ts
4
+ import { Agent, Dispatcher, ProxyAgent, setGlobalDispatcher } from "undici";
5
+ function defaultPort(protocol) {
6
+ return protocol === "https:" ? "443" : "80";
7
+ }
8
+ function proxyEnvValue(name) {
9
+ return process.env[name]?.trim() ?? "";
10
+ }
11
+ function firstProxyEnv(...names) {
12
+ for (const name of names) {
13
+ const value = proxyEnvValue(name);
14
+ if (value) {
15
+ return value;
16
+ }
17
+ }
18
+ return "";
19
+ }
20
+ function ipv4ToNumber(value) {
21
+ const parts = value.split(".");
22
+ if (parts.length !== 4) {
23
+ return null;
24
+ }
25
+ let result = 0;
26
+ for (const part of parts) {
27
+ if (!/^\d+$/.test(part)) {
28
+ return null;
29
+ }
30
+ const parsed = Number(part);
31
+ if (parsed < 0 || parsed > 255) {
32
+ return null;
33
+ }
34
+ result = (result << 8) + parsed;
35
+ }
36
+ return result >>> 0;
37
+ }
38
+ function ipv4MatchesCidr(hostname3, cidr) {
39
+ const [range, bitsRaw] = cidr.split("/");
40
+ const hostValue = ipv4ToNumber(hostname3);
41
+ const rangeValue = range ? ipv4ToNumber(range) : null;
42
+ const bits = Number(bitsRaw);
43
+ if (hostValue === null || rangeValue === null || !Number.isInteger(bits) || bits < 0 || bits > 32) {
44
+ return false;
45
+ }
46
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
47
+ return (hostValue & mask) === (rangeValue & mask);
48
+ }
49
+ function splitNoProxyHostPort(entry) {
50
+ const bracketed = entry.match(/^\[([^\]]+)\](?::(\d+))?$/);
51
+ if (bracketed) {
52
+ return { host: bracketed[1].toLowerCase(), port: bracketed[2] ?? null };
53
+ }
54
+ const portMatch = entry.match(/^([^:]+):(\d+)$/);
55
+ if (portMatch) {
56
+ return { host: portMatch[1].toLowerCase(), port: portMatch[2] ?? null };
57
+ }
58
+ return { host: entry.toLowerCase(), port: null };
59
+ }
60
+ function noProxyMatches(url, noProxy) {
61
+ const hostname3 = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
62
+ const port = url.port || defaultPort(url.protocol);
63
+ for (const rawEntry of noProxy.split(/[,\s]+/)) {
64
+ const entry = rawEntry.trim().toLowerCase();
65
+ if (!entry) {
66
+ continue;
67
+ }
68
+ if (entry === "*") {
69
+ return true;
70
+ }
71
+ const { host, port: entryPort } = splitNoProxyHostPort(entry);
72
+ if (entryPort && entryPort !== port) {
73
+ continue;
74
+ }
75
+ if (host.includes("/") && ipv4MatchesCidr(hostname3, host)) {
76
+ return true;
77
+ }
78
+ if (host.startsWith("*.")) {
79
+ const suffix = host.slice(2);
80
+ if (hostname3.endsWith(`.${suffix}`)) {
81
+ return true;
82
+ }
83
+ continue;
84
+ }
85
+ if (host.startsWith(".")) {
86
+ const suffix = host.slice(1);
87
+ if (hostname3 === suffix || hostname3.endsWith(host)) {
88
+ return true;
89
+ }
90
+ continue;
91
+ }
92
+ if (hostname3 === host || hostname3.endsWith(`.${host}`)) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ var EnvProxyDispatcher = class extends Dispatcher {
99
+ directAgent = new Agent();
100
+ httpProxyAgent;
101
+ httpsProxyAgent;
102
+ noProxy;
103
+ constructor(options) {
104
+ super();
105
+ this.httpProxyAgent = options.httpProxy ? new ProxyAgent(options.httpProxy) : null;
106
+ this.httpsProxyAgent = options.httpsProxy ? new ProxyAgent(options.httpsProxy) : this.httpProxyAgent;
107
+ this.noProxy = options.noProxy;
108
+ }
109
+ dispatch(options, handler) {
110
+ const origin = new URL(String(options.origin));
111
+ if (this.noProxy && noProxyMatches(origin, this.noProxy)) {
112
+ return this.directAgent.dispatch(options, handler);
113
+ }
114
+ const proxyAgent = origin.protocol === "https:" ? this.httpsProxyAgent : this.httpProxyAgent;
115
+ return (proxyAgent ?? this.directAgent).dispatch(options, handler);
116
+ }
117
+ close(callback) {
118
+ const promise = Promise.all([
119
+ this.directAgent.close(),
120
+ this.httpProxyAgent?.close(),
121
+ this.httpsProxyAgent && this.httpsProxyAgent !== this.httpProxyAgent ? this.httpsProxyAgent.close() : void 0
122
+ ]).then(() => void 0);
123
+ if (callback) {
124
+ promise.then(callback, callback);
125
+ return;
126
+ }
127
+ return promise;
128
+ }
129
+ destroy(errorOrCallback, callback) {
130
+ const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null;
131
+ const done = typeof errorOrCallback === "function" ? errorOrCallback : callback;
132
+ const promise = Promise.all([
133
+ this.directAgent.destroy(error),
134
+ this.httpProxyAgent?.destroy(error),
135
+ this.httpsProxyAgent && this.httpsProxyAgent !== this.httpProxyAgent ? this.httpsProxyAgent.destroy(error) : void 0
136
+ ]).then(() => void 0);
137
+ if (done) {
138
+ promise.then(done, done);
139
+ return;
140
+ }
141
+ return promise;
142
+ }
143
+ };
144
+ function configureProxyFromEnv() {
145
+ const httpProxy = firstProxyEnv("HTTP_PROXY", "http_proxy");
146
+ const httpsProxy = firstProxyEnv("HTTPS_PROXY", "https_proxy") || httpProxy;
147
+ if (!httpProxy && !httpsProxy) {
148
+ return;
149
+ }
150
+ setGlobalDispatcher(
151
+ new EnvProxyDispatcher({
152
+ httpProxy,
153
+ httpsProxy,
154
+ noProxy: firstProxyEnv("NO_PROXY", "no_proxy")
155
+ })
156
+ );
157
+ }
158
+ configureProxyFromEnv();
159
+
3
160
  // src/cli/index.ts
4
161
  import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile6 } from "fs/promises";
5
- import { join as join15 } from "path";
162
+ import { join as join16 } from "path";
6
163
  import { tmpdir as tmpdir5 } from "os";
7
164
  import { Command as Command3 } from "commander";
8
165
 
@@ -223,10 +380,10 @@ var SDK_RELEASE = {
223
380
  // skill on the sdk sync surface, and the people-search-to-email prebuilt.
224
381
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
225
382
  // the SDK enrich generator's one-second stale policy.
226
- version: "0.1.109",
383
+ version: "0.1.110",
227
384
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
228
385
  supportPolicy: {
229
- latest: "0.1.109",
386
+ latest: "0.1.110",
230
387
  minimumSupported: "0.1.53",
231
388
  deprecatedBelow: "0.1.53",
232
389
  commandMinimumSupported: [
@@ -253,6 +410,7 @@ var SYNTHETIC_RUN_HEADER = "x-deepline-synthetic-run";
253
410
 
254
411
  // src/http.ts
255
412
  var MAX_DIAGNOSTIC_HEADER_LENGTH = 120;
413
+ var COWORK_NETWORK_HINT = "Claude Cowork appears to be running Deepline in a network-restricted sandbox. In Claude Desktop, open Settings > Capabilities, turn on Allow network egress, and set Domain allowlist to All domains for the Cowork session.";
256
414
  var HttpClient = class {
257
415
  constructor(config) {
258
416
  this.config = config;
@@ -288,6 +446,7 @@ var HttpClient = class {
288
446
  "User-Agent": `deepline-ts-sdk/${SDK_VERSION}`,
289
447
  "X-Deepline-Client-Family": "sdk",
290
448
  "X-Deepline-CLI-Family": "sdk",
449
+ "X-Deepline-Agent-Runtime": detectAgentRuntime(),
291
450
  "X-Deepline-CLI-Version": SDK_VERSION,
292
451
  "X-Deepline-SDK-Version": SDK_VERSION,
293
452
  "X-Deepline-API-Contract": SDK_API_CONTRACT,
@@ -390,12 +549,13 @@ var HttpClient = class {
390
549
  parsed = body;
391
550
  }
392
551
  if (!response.ok) {
552
+ const retryableApiError = options?.retryApiErrors === true && isRetryableApiErrorResponse(response.status);
393
553
  const htmlError = detectHtmlErrorBody(
394
554
  body,
395
555
  response.headers.get("content-type")
396
556
  );
397
557
  if (htmlError) {
398
- throw new DeeplineError(
558
+ lastError = new DeeplineError(
399
559
  htmlError.message(response.status),
400
560
  response.status,
401
561
  "API_ERROR",
@@ -405,12 +565,23 @@ var HttpClient = class {
405
565
  ...htmlError.workerThrewException ? { workerThrewException: true } : {}
406
566
  }
407
567
  );
568
+ if (retryableApiError && attempt < this.config.maxRetries) {
569
+ retryAfterDelayMs = parseOptionalRetryAfter(response);
570
+ break;
571
+ }
572
+ throw lastError;
408
573
  }
409
574
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
410
575
  const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
411
- throw new DeeplineError(msg, response.status, "API_ERROR", {
576
+ const apiErrorCode = errorValue && typeof errorValue === "object" && typeof errorValue.code === "string" ? errorValue.code : "API_ERROR";
577
+ lastError = new DeeplineError(msg, response.status, apiErrorCode, {
412
578
  response: parsed
413
579
  });
580
+ if (retryableApiError && attempt < this.config.maxRetries) {
581
+ retryAfterDelayMs = parseOptionalRetryAfter(response);
582
+ break;
583
+ }
584
+ throw lastError;
414
585
  }
415
586
  return parsed;
416
587
  } catch (error) {
@@ -430,8 +601,8 @@ var HttpClient = class {
430
601
  if (lastError instanceof DeeplineError) {
431
602
  throw lastError;
432
603
  }
433
- const errorMessage3 = lastError?.message ? `Unable to connect to ${baseUrl}. ${lastError.message}` : `Unable to connect to ${baseUrl}. Is the computer able to access the url?`;
434
- throw new DeeplineError(errorMessage3);
604
+ const errorMessage4 = lastError?.message ? `Unable to connect to ${baseUrl}. ${lastError.message}` : `Unable to connect to ${baseUrl}. Is the computer able to access the url?`;
605
+ throw new DeeplineError(withCoworkNetworkHint(errorMessage4));
435
606
  }
436
607
  /**
437
608
  * Send a GET request.
@@ -503,7 +674,9 @@ var HttpClient = class {
503
674
  }
504
675
  }
505
676
  throw new DeeplineError(
506
- lastError?.message ? `Unable to stream from ${this.config.baseUrl}. ${lastError.message}` : `Unable to stream from ${this.config.baseUrl}.`
677
+ withCoworkNetworkHint(
678
+ lastError?.message ? `Unable to stream from ${this.config.baseUrl}. ${lastError.message}` : `Unable to stream from ${this.config.baseUrl}.`
679
+ )
507
680
  );
508
681
  }
509
682
  /**
@@ -513,11 +686,12 @@ var HttpClient = class {
513
686
  * @param path - API path
514
687
  * @param body - Request body (will be JSON-serialized)
515
688
  */
516
- async post(path, body, headers) {
689
+ async post(path, body, headers, options) {
517
690
  return this.request(path, {
518
691
  method: "POST",
519
692
  body,
520
- headers
693
+ headers,
694
+ ...options
521
695
  });
522
696
  }
523
697
  async postFormData(path, formData, headers) {
@@ -558,6 +732,9 @@ function parseResponseBody(body) {
558
732
  return body;
559
733
  }
560
734
  }
735
+ function isRetryableApiErrorResponse(status) {
736
+ return status === 408 || status === 425 || status >= 500 && status < 600;
737
+ }
561
738
  function detectHtmlErrorBody(body, contentType) {
562
739
  const trimmed = body.trim();
563
740
  const lower = trimmed.toLowerCase();
@@ -597,6 +774,9 @@ function apiErrorMessage(parsed, status) {
597
774
  return `HTTP ${status}`;
598
775
  }
599
776
  function parseRetryAfter(response) {
777
+ return parseOptionalRetryAfter(response) ?? 5e3;
778
+ }
779
+ function parseOptionalRetryAfter(response) {
600
780
  const header = response.headers.get("retry-after");
601
781
  if (header) {
602
782
  const seconds = Number(header);
@@ -604,7 +784,7 @@ function parseRetryAfter(response) {
604
784
  return seconds * 1e3;
605
785
  }
606
786
  }
607
- return 5e3;
787
+ return null;
608
788
  }
609
789
  function buildCandidateUrls(url) {
610
790
  try {
@@ -661,6 +841,39 @@ function decodeSseFrame(frame) {
661
841
  function sleep(ms) {
662
842
  return new Promise((resolve16) => setTimeout(resolve16, ms));
663
843
  }
844
+ function isTruthyEnv(name) {
845
+ const normalized = process.env[name]?.trim().toLowerCase();
846
+ return ["1", "true", "yes", "on"].includes(normalized ?? "");
847
+ }
848
+ function isCoworkLikeSandbox() {
849
+ const pluginMode = isTruthyEnv("DEEPLINE_PLUGIN_MODE");
850
+ const claudeRemote = isTruthyEnv("CLAUDE_CODE_REMOTE");
851
+ const projectDir = Boolean(process.env.CLAUDE_PROJECT_DIR?.trim());
852
+ const pluginRoot = Boolean(process.env.DEEPLINE_PLUGIN_ROOT?.trim());
853
+ const home = process.env.HOME?.trim() ?? "";
854
+ const sessionHome = home.startsWith("/sessions/");
855
+ return (pluginMode || pluginRoot) && (claudeRemote || projectDir || sessionHome);
856
+ }
857
+ function detectAgentRuntime() {
858
+ if (process.env.CODEX_THREAD_ID?.trim()) return "codex";
859
+ if (isCoworkLikeSandbox()) return "claude_cowork";
860
+ if (process.env.CLAUDECODE?.trim() === "1") return "claude_code";
861
+ if (process.env.CLINE_ACTIVE?.trim().toLowerCase() === "true") return "cline";
862
+ if (process.env.CURSOR_TRACE_ID?.trim() || process.env.CURSOR_AGENT?.trim()) {
863
+ return "cursor";
864
+ }
865
+ if (process.env.WINDSURF?.trim() || process.env.CASCADE?.trim()) {
866
+ return "windsurf";
867
+ }
868
+ return "unknown";
869
+ }
870
+ function withCoworkNetworkHint(message) {
871
+ if (!isCoworkLikeSandbox() || message.includes(COWORK_NETWORK_HINT)) {
872
+ return message;
873
+ }
874
+ return `${message}
875
+ ${COWORK_NETWORK_HINT}`;
876
+ }
664
877
 
665
878
  // src/stream-reconnect.ts
666
879
  var STREAM_RECONNECT_BASE_DELAY_MS = 500;
@@ -1937,12 +2150,12 @@ var DeeplineClient = class {
1937
2150
  * Returns everything from {@link ToolDefinition} plus pricing info, sample
1938
2151
  * inputs/outputs, failure modes, and cost estimates.
1939
2152
  *
1940
- * @param toolId - Tool identifier (e.g. `"apollo_people_search"`)
2153
+ * @param toolId - Tool identifier (e.g. `"dropleads_search_people"`)
1941
2154
  * @returns Full tool metadata
1942
2155
  *
1943
2156
  * @example
1944
2157
  * ```typescript
1945
- * const meta = await client.getTool('apollo_people_search');
2158
+ * const meta = await client.getTool('dropleads_search_people');
1946
2159
  * console.log(`Cost: ${meta.estimatedCreditsRange} credits`);
1947
2160
  * console.log(`Input schema:`, meta.inputSchema);
1948
2161
  * ```
@@ -1974,7 +2187,8 @@ var DeeplineClient = class {
1974
2187
  return this.http.post(
1975
2188
  `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
1976
2189
  { payload: input2 },
1977
- headers
2190
+ headers,
2191
+ { forbiddenAsApiError: true }
1978
2192
  );
1979
2193
  }
1980
2194
  /**
@@ -2056,7 +2270,7 @@ var DeeplineClient = class {
2056
2270
  ...request.force ? { force: true } : {},
2057
2271
  ...typeof request.waitForCompletionMs === "number" ? { waitForCompletionMs: request.waitForCompletionMs } : {},
2058
2272
  // Profile selection is the API's job, not the CLI's. The server
2059
- // hardcodes workers_edge as the default; tests that want a
2273
+ // defaults to workers_edge; tests and runtime probes that want a
2060
2274
  // different profile pass `request.profile` explicitly.
2061
2275
  ...request.profile ? { profile: request.profile } : {}
2062
2276
  }
@@ -2121,10 +2335,15 @@ var DeeplineClient = class {
2121
2335
  sourceFiles: input2.sourceFiles,
2122
2336
  artifact: input2.artifact
2123
2337
  });
2124
- return this.http.post("/api/v2/plays/artifacts", {
2125
- ...input2,
2126
- compilerManifest
2127
- });
2338
+ return this.http.post(
2339
+ "/api/v2/plays/artifacts",
2340
+ {
2341
+ ...input2,
2342
+ compilerManifest
2343
+ },
2344
+ void 0,
2345
+ { forbiddenAsApiError: true, retryApiErrors: true }
2346
+ );
2128
2347
  }
2129
2348
  /**
2130
2349
  * Register multiple bundled play artifacts in one request.
@@ -2134,7 +2353,12 @@ var DeeplineClient = class {
2134
2353
  */
2135
2354
  async registerPlayArtifacts(artifacts) {
2136
2355
  if (artifacts.length === 0) {
2137
- return this.http.post("/api/v2/plays/artifacts", { artifacts });
2356
+ return this.http.post(
2357
+ "/api/v2/plays/artifacts",
2358
+ { artifacts },
2359
+ void 0,
2360
+ { forbiddenAsApiError: true, retryApiErrors: true }
2361
+ );
2138
2362
  }
2139
2363
  const compiledArtifacts = await mapWithConcurrency(
2140
2364
  artifacts,
@@ -2152,9 +2376,14 @@ var DeeplineClient = class {
2152
2376
  const responses = [];
2153
2377
  for (const chunk of chunkRegisterPlayArtifacts(compiledArtifacts)) {
2154
2378
  responses.push(
2155
- await this.http.post("/api/v2/plays/artifacts", {
2156
- artifacts: chunk
2157
- })
2379
+ await this.http.post(
2380
+ "/api/v2/plays/artifacts",
2381
+ {
2382
+ artifacts: chunk
2383
+ },
2384
+ void 0,
2385
+ { forbiddenAsApiError: true, retryApiErrors: true }
2386
+ )
2158
2387
  );
2159
2388
  }
2160
2389
  return {
@@ -2959,7 +3188,9 @@ var DeeplineClient = class {
2959
3188
  const encodedName = encodeURIComponent(name);
2960
3189
  return this.http.post(
2961
3190
  `/api/v2/plays/${encodedName}/live`,
2962
- request
3191
+ request,
3192
+ void 0,
3193
+ { forbiddenAsApiError: true }
2963
3194
  );
2964
3195
  }
2965
3196
  /**
@@ -3542,11 +3773,53 @@ function sleep3(ms) {
3542
3773
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
3543
3774
  }
3544
3775
  function collectLocalEnvInfo() {
3545
- return {
3776
+ const info = {
3546
3777
  os: `${process.platform} ${process.arch}`,
3547
3778
  node_version: process.version,
3548
- home_dir: homedir3()
3779
+ home_dir: homedir3(),
3780
+ agent_runtime: detectAgentRuntime2()
3549
3781
  };
3782
+ const env = process.env;
3783
+ const optional = {
3784
+ claude_code_remote: env.CLAUDE_CODE_REMOTE,
3785
+ deepline_plugin_mode: env.DEEPLINE_PLUGIN_MODE
3786
+ };
3787
+ for (const [key, value] of Object.entries(optional)) {
3788
+ if (value?.trim()) info[key] = value.trim();
3789
+ }
3790
+ if (env.CLAUDE_PROJECT_DIR?.trim()) {
3791
+ info.claude_project_dir_present = "true";
3792
+ }
3793
+ if (env.DEEPLINE_PLUGIN_ROOT?.trim()) {
3794
+ info.deepline_plugin_root_present = "true";
3795
+ }
3796
+ if (env.DEEPLINE_PLUGIN_SKILLS_DIR?.trim()) {
3797
+ info.deepline_plugin_skills_dir_present = "true";
3798
+ }
3799
+ if (info.home_dir.startsWith("/sessions/")) {
3800
+ info.home_scope = "sessions";
3801
+ }
3802
+ return info;
3803
+ }
3804
+ function detectAgentRuntime2() {
3805
+ if (process.env.CODEX_THREAD_ID?.trim()) return "codex";
3806
+ const pluginMode = process.env.DEEPLINE_PLUGIN_MODE?.trim().toLowerCase();
3807
+ const claudeRemote = process.env.CLAUDE_CODE_REMOTE?.trim().toLowerCase();
3808
+ const sessionHome = (process.env.HOME?.trim() || homedir3()).startsWith(
3809
+ "/sessions/"
3810
+ );
3811
+ if (["1", "true", "yes", "on"].includes(pluginMode ?? "") && (["1", "true", "yes", "on"].includes(claudeRemote ?? "") || Boolean(process.env.CLAUDE_PROJECT_DIR?.trim()) || sessionHome)) {
3812
+ return "claude_cowork";
3813
+ }
3814
+ if (process.env.CLAUDECODE?.trim() === "1") return "claude_code";
3815
+ if (process.env.CLINE_ACTIVE?.trim().toLowerCase() === "true") return "cline";
3816
+ if (process.env.CURSOR_TRACE_ID?.trim() || process.env.CURSOR_AGENT?.trim()) {
3817
+ return "cursor";
3818
+ }
3819
+ if (process.env.WINDSURF?.trim() || process.env.CASCADE?.trim()) {
3820
+ return "windsurf";
3821
+ }
3822
+ return "unknown";
3550
3823
  }
3551
3824
  function readCsvRows(csvPath) {
3552
3825
  const raw = readFileSync3(resolve2(csvPath), "utf-8");
@@ -3814,7 +4087,14 @@ function saveEnvValues(values, baseUrl) {
3814
4087
  }
3815
4088
  async function httpJson(method, url, apiKey, body) {
3816
4089
  const headers = {
3817
- "Content-Type": "application/json"
4090
+ "Content-Type": "application/json",
4091
+ "User-Agent": `deepline-ts-sdk/${SDK_VERSION}`,
4092
+ "X-Deepline-Client-Family": "sdk",
4093
+ "X-Deepline-CLI-Family": "sdk",
4094
+ "X-Deepline-Agent-Runtime": detectAgentRuntime2(),
4095
+ "X-Deepline-CLI-Version": SDK_VERSION,
4096
+ "X-Deepline-SDK-Version": SDK_VERSION,
4097
+ "X-Deepline-API-Contract": SDK_API_CONTRACT
3818
4098
  };
3819
4099
  if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
3820
4100
  let response = null;
@@ -4672,7 +4952,7 @@ async function handlePlans(options) {
4672
4952
  );
4673
4953
  const activePlan = payload.active_plan ?? {};
4674
4954
  const plans = Array.isArray(payload.plans) ? payload.plans : [];
4675
- const metrics = Array.isArray(payload.metrics) ? payload.metrics : [];
4955
+ const meters = Array.isArray(payload.meters) ? payload.meters : Array.isArray(payload.metrics) ? payload.metrics : [];
4676
4956
  const lines = [
4677
4957
  `Catalog: ${payload.catalog_id ?? "(unknown)"} (version ${payload.catalog_version ?? "(unknown)"})`,
4678
4958
  `Active plan: ${activePlan.public_name ?? "(unknown)"} (${activePlan.plan_version_id ?? "(unknown)"})`,
@@ -4683,10 +4963,10 @@ async function handlePlans(options) {
4683
4963
  (plan) => `${plan.public_name ?? "(unknown)"} (${plan.plan_version_id ?? "(unknown)"}) | ${planPriceText(plan.price_usd, plan.price_interval)} | ${plan.monthly_grant_credits ?? 0} credits/cycle | ${planRolloverText(plan.rollover)} | ${plan.acquirable ? "acquirable" : "not acquirable"}`
4684
4964
  )
4685
4965
  ],
4686
- ...metrics.length === 0 ? ["Metrics: none"] : [
4966
+ ...meters.length === 0 ? ["Metrics: none"] : [
4687
4967
  "Metrics:",
4688
- ...metrics.map(
4689
- (metric) => `${metric.id ?? "(unknown)"} | ${metric.name ?? "(unknown)"}`
4968
+ ...meters.map(
4969
+ (meter) => `${meter.id ?? "(unknown)"} | ${meter.name ?? "(unknown)"}`
4690
4970
  )
4691
4971
  ]
4692
4972
  ];
@@ -7143,17 +7423,58 @@ async function analyzeSourceGraph(entryFile, adapter) {
7143
7423
  }
7144
7424
  async function computeWorkersHarnessFingerprintWithAdapter(adapter) {
7145
7425
  const { readdir } = await import("fs/promises");
7426
+ const addFilePart = async (parts2, rootDir, filePath) => {
7427
+ const contents = await readFile(filePath, "utf-8");
7428
+ parts2.push({
7429
+ name: `${basename(rootDir)}:${filePath.slice(rootDir.length + 1)}`,
7430
+ hash: sha256(contents)
7431
+ });
7432
+ };
7433
+ const collectTopLevelTsFiles = async (rootDir, parts2) => {
7434
+ if (!await fileExists(rootDir)) return;
7435
+ const entries2 = await readdir(rootDir, { withFileTypes: true });
7436
+ const tsFiles2 = entries2.filter((entry) => entry.isFile() && /\.[cm]?ts$/.test(entry.name)).map((entry) => entry.name).sort();
7437
+ for (const name of tsFiles2) {
7438
+ await addFilePart(parts2, rootDir, join4(rootDir, name));
7439
+ }
7440
+ };
7441
+ const collectIntegrationBatchingFiles = async (rootDir, parts2) => {
7442
+ if (!await fileExists(rootDir)) return;
7443
+ const entries2 = await readdir(rootDir, { withFileTypes: true });
7444
+ const filePaths = [];
7445
+ for (const entry of entries2) {
7446
+ if (entry.isFile() && (entry.name === "play-runtime-batching-registry.ts" || /^batching.*\.ts$/.test(entry.name))) {
7447
+ filePaths.push(join4(rootDir, entry.name));
7448
+ }
7449
+ if (entry.isDirectory()) {
7450
+ const batchingFile = join4(rootDir, entry.name, "batching.ts");
7451
+ if (await fileExists(batchingFile)) {
7452
+ filePaths.push(batchingFile);
7453
+ }
7454
+ }
7455
+ }
7456
+ for (const filePath of filePaths.sort()) {
7457
+ await addFilePart(parts2, rootDir, filePath);
7458
+ }
7459
+ };
7146
7460
  const entries = await readdir(adapter.workersHarnessFilesDir, {
7147
7461
  withFileTypes: true
7148
7462
  });
7149
7463
  const tsFiles = entries.filter((e) => e.isFile() && /\.[cm]?ts$/.test(e.name)).map((e) => e.name).sort();
7150
7464
  const parts = [];
7151
7465
  for (const name of tsFiles) {
7152
- const contents = await readFile(
7153
- join4(adapter.workersHarnessFilesDir, name),
7154
- "utf-8"
7466
+ await addFilePart(
7467
+ parts,
7468
+ adapter.workersHarnessFilesDir,
7469
+ join4(adapter.workersHarnessFilesDir, name)
7155
7470
  );
7156
- parts.push({ name, hash: sha256(contents) });
7471
+ }
7472
+ for (const dir of adapter.workersRuntimeFingerprintDirs ?? []) {
7473
+ if (basename(dir) === "integrations") {
7474
+ await collectIntegrationBatchingFiles(dir, parts);
7475
+ } else {
7476
+ await collectTopLevelTsFiles(dir, parts);
7477
+ }
7157
7478
  }
7158
7479
  return sha256(JSON.stringify(parts));
7159
7480
  }
@@ -8017,6 +8338,9 @@ function createSdkPlayBundlingAdapter() {
8017
8338
  sdkWorkersEntryFile: SDK_WORKERS_ENTRY_FILE,
8018
8339
  workersHarnessEntryFile: WORKERS_HARNESS_ENTRY_FILE,
8019
8340
  workersHarnessFilesDir: WORKERS_HARNESS_FILES_DIR,
8341
+ workersRuntimeFingerprintDirs: [
8342
+ resolve8(PROJECT_ROOT, "shared_libs", "play-runtime")
8343
+ ],
8020
8344
  discoverPackagedLocalFiles,
8021
8345
  warnAboutNonDevelopmentBundling
8022
8346
  };
@@ -8027,7 +8351,8 @@ async function bundlePlayFile2(filePath, options = {}) {
8027
8351
  exportName: options.exportName,
8028
8352
  adapter: createSdkPlayBundlingAdapter()
8029
8353
  });
8030
- if (result.success) validatePlaySourceFilesHaveNoInlineSecrets(result.sourceFiles);
8354
+ if (result.success)
8355
+ validatePlaySourceFilesHaveNoInlineSecrets(result.sourceFiles);
8031
8356
  return result;
8032
8357
  }
8033
8358
 
@@ -8209,17 +8534,17 @@ function templateExample(template) {
8209
8534
  case "people-list":
8210
8535
  return "deepline plays bootstrap people-list --from provider:dropleads_search_people --out people.play.ts";
8211
8536
  case "company-list":
8212
- return "deepline plays bootstrap company-list --from provider:apollo_company_search --out companies.play.ts";
8537
+ return "deepline plays bootstrap company-list --from provider:crustdata_companydb_search --out companies.play.ts";
8213
8538
  case "people-email":
8214
8539
  return "deepline plays bootstrap people-email --from csv:data/leads.csv --using play:prebuilt/name-and-domain-to-email-waterfall --out email-flow.play.ts";
8215
8540
  case "people-phone":
8216
8541
  return "deepline plays bootstrap people-phone --from csv:data/vp_contacts.csv --using play:prebuilt/person-to-phone --out phone-flow.play.ts";
8217
8542
  case "company-people":
8218
- return "deepline plays bootstrap company-people --from provider:apollo_company_search --using play:prebuilt/company-to-contact --out company-people.play.ts";
8543
+ return "deepline plays bootstrap company-people --from provider:crustdata_companydb_search --using play:prebuilt/company-to-contact --out company-people.play.ts";
8219
8544
  case "company-people-email":
8220
- return "deepline plays bootstrap company-people-email --from provider:apollo_company_search --people play:prebuilt/company-to-contact --email providers:hunter_email_finder,leadmagic_email_finder --out account-emails.play.ts";
8545
+ return "deepline plays bootstrap company-people-email --from provider:crustdata_companydb_search --people play:prebuilt/company-to-contact --email providers:hunter_email_finder,leadmagic_email_finder --out account-emails.play.ts";
8221
8546
  case "company-people-phone":
8222
- return "deepline plays bootstrap company-people-phone --from provider:apollo_company_search --people play:prebuilt/company-to-contact --phone providers:ai_ark_mobile_phone_finder --out account-phones.play.ts";
8547
+ return "deepline plays bootstrap company-people-phone --from provider:crustdata_companydb_search --people play:prebuilt/company-to-contact --phone providers:ai_ark_mobile_phone_finder --out account-phones.play.ts";
8223
8548
  }
8224
8549
  }
8225
8550
  var PLAY_BOOTSTRAP_STAGE_NAMES = [
@@ -9521,8 +9846,8 @@ Examples:
9521
9846
  deepline plays run email-flow.play.ts --input '{"limit":5}' --watch
9522
9847
 
9523
9848
  deepline plays bootstrap people-email --from provider:dropleads_search_people --using providers:hunter_email_finder,leadmagic_email_finder --limit 5 --out prospecting.play.ts
9524
- deepline plays bootstrap company-people-email --from provider:apollo_company_search --people play:prebuilt/company-to-contact --email play:prebuilt/name-and-domain-to-email-waterfall --limit 5 --out account-contacts.play.ts
9525
- deepline plays bootstrap company-list --from provider:apollo_company_search --limit 5 --out companies.play.ts
9849
+ deepline plays bootstrap company-people-email --from provider:crustdata_companydb_search --people play:prebuilt/company-to-contact --email play:prebuilt/name-and-domain-to-email-waterfall --limit 5 --out account-contacts.play.ts
9850
+ deepline plays bootstrap company-list --from provider:crustdata_companydb_search --limit 5 --out companies.play.ts
9526
9851
  `
9527
9852
  ).option("--name <name>", "Generated play name").option(
9528
9853
  "--from <ref>",
@@ -9701,11 +10026,11 @@ function createCliProgress(enabled) {
9701
10026
 
9702
10027
  // src/cli/trace.ts
9703
10028
  var cliTraceStartedAt = Date.now();
9704
- function isTruthyEnv(value) {
10029
+ function isTruthyEnv2(value) {
9705
10030
  return value === "1" || value === "true" || value === "yes";
9706
10031
  }
9707
10032
  function isCliTraceEnabled() {
9708
- return isTruthyEnv(process.env.DEEPLINE_CLI_TRACE);
10033
+ return isTruthyEnv2(process.env.DEEPLINE_CLI_TRACE);
9709
10034
  }
9710
10035
  function recordCliTrace(event) {
9711
10036
  if (!isCliTraceEnabled()) {
@@ -12825,7 +13150,7 @@ function writeStartedPlayRun(input2) {
12825
13150
  );
12826
13151
  }
12827
13152
  function parsePlayRunOptions(args) {
12828
- const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--profile <id>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--profile <id>] [--live|--latest|--revision-id <id>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--no-open] [--json] [--full] [--<input> value]\n --profile defaults to workers_edge; hatchet is the durable Daytona-backed preview runtime.\n Unknown --<input> value flags, such as --limit 5, are passed into play input.\nRun `deepline plays run --help` for idempotency, tool call id, and ctx.dataset guidance.";
13153
+ const usage = "Usage: deepline plays run <play-name> [--input '{...}'] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run <play-file.ts> [--input '{...}'] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run --file <play-file.ts> [--input '{...}'] [--profile <id>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--full] [--<input> value]\n deepline plays run --name <name> [--input '{...}'] [--profile <id>] [--live|--latest|--revision-id <id>] [--no-wait] [--tail-timeout-ms 30000] [--force] [--no-open] [--json] [--full] [--<input> value]\n --profile defaults to workers_edge; pass hatchet explicitly for Hatchet runtime comparison.\n Unknown --<input> value flags, such as --limit 5, are passed into play input.\nRun `deepline plays run --help` for idempotency, tool call id, and ctx.dataset guidance.";
12829
13154
  let filePath = null;
12830
13155
  let playName = null;
12831
13156
  let input2 = null;
@@ -14164,7 +14489,7 @@ function playRunCommand(play, options) {
14164
14489
  return `deepline plays run ${target} --input '{...}' --watch`;
14165
14490
  }
14166
14491
  function summarizePlayListItemForCli(play, options) {
14167
- const aliases = play.aliases?.length ? play.aliases : [play.name];
14492
+ const aliases = play.aliases ?? [];
14168
14493
  const csvInput = playSchemaMetadata(play.inputSchema, "csvInput");
14169
14494
  const rowOutputSchema = playSchemaMetadata(
14170
14495
  play.outputSchema,
@@ -14619,8 +14944,8 @@ Idempotent execution:
14619
14944
  .dataset('companies_v1', companies)
14620
14945
  .withColumn('cto', (row, ctx) => ctx.tools.execute({
14621
14946
  id: 'find_cto',
14622
- tool: 'apollo_search_people_with_match',
14623
- input: { q_organization_domains_list: [row.domain], per_page: 1 },
14947
+ tool: 'dropleads_search_people',
14948
+ input: { filters: { companyDomains: [row.domain], jobTitles: ['CTO'] }, pagination: { page: 1, limit: 1 } },
14624
14949
  }))
14625
14950
  .run({ key: 'domain' });
14626
14951
 
@@ -14766,14 +15091,12 @@ Examples:
14766
15091
  ]);
14767
15092
  });
14768
15093
  addPlaySearchCommand(play.command("search <query>"));
14769
- play.command("grep <query>").description(
14770
- "Literal grep over play names, aliases, schemas, and descriptions."
14771
- ).addHelpText(
15094
+ play.command("grep <query>").description("Literal grep over play names, schemas, and descriptions.").addHelpText(
14772
15095
  "after",
14773
15096
  `
14774
15097
  Notes:
14775
15098
  Literal registry filtering. Terms are matched case-insensitively against play
14776
- names, references, display names, aliases, ownership, and schemas. Use
15099
+ names, references, display names, ownership, and schemas. Use
14777
15100
  --mode phrase for exact phrase matching, --mode any for OR, and the default
14778
15101
  --mode all for AND.
14779
15102
 
@@ -14794,7 +15117,7 @@ Examples:
14794
15117
  "after",
14795
15118
  `
14796
15119
  Notes:
14797
- Compact contract read for schemas, aliases, examples, ownership, and the run command.
15120
+ Compact contract read for schemas, examples, ownership, and the run command.
14798
15121
  Use \`get\` when you need full metadata, revisions, source fields, or latest runs.
14799
15122
 
14800
15123
  Examples:
@@ -16818,7 +17141,8 @@ async function buildPlanArgs(args) {
16818
17141
  "--name",
16819
17142
  "--rows",
16820
17143
  "--with-force",
16821
- "--timeout"
17144
+ "--timeout",
17145
+ "--profile"
16822
17146
  ]);
16823
17147
  const localBooleanOptions = /* @__PURE__ */ new Set([
16824
17148
  "--dry-run",
@@ -17958,6 +18282,9 @@ function registerEnrichCommand(program) {
17958
18282
  ).option("--in-place", "Write enriched output back to the input CSV.").option(
17959
18283
  "--timeout <SECONDS>",
17960
18284
  "API read timeout for enrich status/API requests."
18285
+ ).option(
18286
+ "--profile <id>",
18287
+ "Internal/testing: override the execution profile for the generated play run."
17961
18288
  ).action(async (options, _command) => {
17962
18289
  if (options.inPlace && options.dryRun) {
17963
18290
  throw new Error("--in-place is not supported with --dry-run.");
@@ -18028,6 +18355,9 @@ function registerEnrichCommand(program) {
18028
18355
  JSON.stringify(runtimeInput),
18029
18356
  "--watch"
18030
18357
  ];
18358
+ if (options.profile) {
18359
+ runArgs.push("--profile", options.profile);
18360
+ }
18031
18361
  if (options.noOpen) {
18032
18362
  runArgs.push("--no-open");
18033
18363
  }
@@ -18174,752 +18504,618 @@ Examples:
18174
18504
  ).argument("<text>", "Feedback text").option("--command <command>", "Command that reproduced the issue").option("--payload <payload>", "JSON or plain-text payload for the repro").option("--json", "Emit JSON output").action(handleFeedback);
18175
18505
  }
18176
18506
 
18177
- // src/cli/commands/legacy-noops.ts
18178
- var SESSION_SUBCOMMANDS = [
18179
- "start",
18180
- "status",
18181
- "output",
18182
- "alert",
18183
- "usage",
18184
- "limit",
18185
- "render",
18186
- "send"
18187
- ];
18188
- var BACKEND_SUBCOMMANDS = [
18189
- "start",
18190
- "stop",
18191
- "status",
18192
- "refresh-runtime",
18193
- "sync-runtime"
18194
- ];
18195
- function legacyNoopEnvelope(input2) {
18196
- const command = [
18197
- "deepline",
18198
- input2.family,
18199
- input2.subcommand
18200
- ].filter(Boolean).join(" ");
18201
- const subject = input2.family === "session" ? "Legacy Session UI/playground command" : "Legacy local playground backend command";
18202
- const note = input2.family === "session" ? "The SDK CLI does not manage the legacy Python Session UI. This command is accepted for backwards compatibility and intentionally does nothing." : "The SDK CLI does not start or stop the legacy Python local playground backend. This command is accepted for backwards compatibility and intentionally does nothing.";
18507
+ // src/cli/commands/sessions.ts
18508
+ import {
18509
+ existsSync as existsSync8,
18510
+ mkdirSync as mkdirSync4,
18511
+ readdirSync as readdirSync2,
18512
+ readFileSync as readFileSync7,
18513
+ statSync as statSync3,
18514
+ writeFileSync as writeFileSync8
18515
+ } from "fs";
18516
+ import { homedir as homedir5, platform } from "os";
18517
+ import { basename as basename4, dirname as dirname9, join as join9, resolve as resolve12 } from "path";
18518
+ import { gzipSync } from "zlib";
18519
+ import { randomUUID } from "crypto";
18520
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18521
+ var UUID_IN_TEXT_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
18522
+ var MAX_SESSION_UPLOAD_BYTES = 35e5;
18523
+ var MAX_DIRECT_SESSION_DECODED_BYTES = 50 * 1024 * 1024;
18524
+ var CHUNK_SIZE_BYTES = 25e5;
18525
+ var MAX_EVENT_STRING_CHARS = 8e3;
18526
+ var MAX_EVENT_LIST_ITEMS = 40;
18527
+ var MAX_EVENT_OBJECT_KEYS = 80;
18528
+ var TRUNCATION_MARKER = "...[truncated]";
18529
+ var NOISE_EVENT_TYPES = /* @__PURE__ */ new Set(["progress", "file-history-snapshot"]);
18530
+ function homeDir() {
18531
+ return process.env.HOME?.trim() || homedir5();
18532
+ }
18533
+ function detectShellContext() {
18534
+ const shellPath = process.env.SHELL?.trim() || process.env.ComSpec?.trim() || process.env.COMSPEC?.trim() || "";
18203
18535
  return {
18204
- ok: true,
18205
- noop: true,
18206
- command,
18207
- compatibility: {
18208
- family: "legacy_python_cli",
18209
- sdk_behavior: "noop"
18210
- },
18211
- render: {
18212
- sections: [
18213
- {
18214
- title: subject,
18215
- lines: [note]
18216
- }
18217
- ]
18218
- }
18536
+ shell: shellPath ? basename4(shellPath).replace(/\.exe$/i, "") : "unknown",
18537
+ shell_path: shellPath || null,
18538
+ os: platform(),
18539
+ cwd: process.cwd()
18219
18540
  };
18220
18541
  }
18221
- function printLegacyNoop(input2) {
18222
- printCommandEnvelope(legacyNoopEnvelope(input2), { json: input2.options.json });
18542
+ function claudeProjectsRoot() {
18543
+ return join9(homeDir(), ".claude", "projects");
18223
18544
  }
18224
- function legacySubcommandFromArgv(family) {
18225
- const args = process.argv.slice(2);
18226
- const familyIndex = args.indexOf(family);
18227
- const nextToken = familyIndex >= 0 ? args[familyIndex + 1] : void 0;
18228
- return nextToken && !nextToken.startsWith("-") ? nextToken : void 0;
18545
+ function codexSessionsRoot() {
18546
+ return join9(homeDir(), ".codex", "sessions");
18229
18547
  }
18230
- function addLegacyNoopSubcommand(parent, family, subcommand, description) {
18231
- parent.command(subcommand).description(description).allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").action((_args, options) => {
18232
- printLegacyNoop({ family, subcommand, options });
18233
- });
18548
+ function listClaudeSessionFiles() {
18549
+ const root = claudeProjectsRoot();
18550
+ if (!existsSync8(root)) return [];
18551
+ const projectDirs = readDirectoryNames(root);
18552
+ const files = [];
18553
+ for (const projectDir of projectDirs) {
18554
+ const fullProjectDir = join9(root, projectDir);
18555
+ for (const fileName of readDirectoryNames(fullProjectDir)) {
18556
+ if (fileName.endsWith(".jsonl")) {
18557
+ const filePath = join9(fullProjectDir, fileName);
18558
+ const sessionId = sessionIdFromClaudeFilePath(filePath);
18559
+ const stat4 = statIfReadable(filePath);
18560
+ if (sessionId && stat4) {
18561
+ files.push({
18562
+ agent: "claude",
18563
+ sessionId,
18564
+ filePath,
18565
+ mtimeMs: stat4.mtimeMs
18566
+ });
18567
+ }
18568
+ }
18569
+ }
18570
+ }
18571
+ return files;
18234
18572
  }
18235
- function registerLegacyNoopCommands(program) {
18236
- const session = program.command("session").description(
18237
- "Compatibility no-ops for legacy Python Session UI commands."
18238
- ).allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").addHelpText(
18239
- "after",
18240
- `
18241
- Notes:
18242
- The SDK CLI accepts singular session commands from older agent skills so they
18243
- do not fail, but it does not manage the legacy Python Session UI.
18244
- Use "deepline sessions send" or "deepline sessions render" for real SDK
18245
- transcript workflows.
18246
-
18247
- Examples:
18248
- deepline session start --steps '["Inspect CSV","Run pilot"]'
18249
- deepline session status --message "Running pilot"
18250
- deepline session output --csv ./results.csv --label "Results"
18251
- `
18252
- ).action((args, options) => {
18253
- void args;
18254
- printLegacyNoop({
18255
- family: "session",
18256
- subcommand: legacySubcommandFromArgv("session"),
18257
- options
18258
- });
18259
- });
18260
- for (const subcommand of SESSION_SUBCOMMANDS) {
18261
- addLegacyNoopSubcommand(
18262
- session,
18263
- "session",
18264
- subcommand,
18265
- `Accept legacy "deepline session ${subcommand}" as an SDK no-op.`
18266
- );
18267
- }
18268
- const backend = program.command("backend").description(
18269
- "Compatibility no-ops for legacy Python local backend commands."
18270
- ).allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").addHelpText(
18271
- "after",
18272
- `
18273
- Notes:
18274
- The SDK CLI uses the configured Deepline host and V2 play runtime. It does not
18275
- start, stop, or refresh the legacy Python local playground backend.
18276
-
18277
- Examples:
18278
- deepline backend start
18279
- deepline backend stop --just-backend
18280
- deepline backend status --json
18281
- `
18282
- ).action((args, options) => {
18283
- void args;
18284
- printLegacyNoop({
18285
- family: "backend",
18286
- subcommand: legacySubcommandFromArgv("backend"),
18287
- options
18288
- });
18289
- });
18290
- for (const subcommand of BACKEND_SUBCOMMANDS) {
18291
- addLegacyNoopSubcommand(
18292
- backend,
18293
- "backend",
18294
- subcommand,
18295
- `Accept legacy "deepline backend ${subcommand}" as an SDK no-op.`
18296
- );
18573
+ function listCodexSessionFiles() {
18574
+ const root = codexSessionsRoot();
18575
+ if (!existsSync8(root)) return [];
18576
+ const files = [];
18577
+ for (const filePath of listJsonlFilesRecursive(root, 5)) {
18578
+ const stat4 = statIfReadable(filePath);
18579
+ if (!stat4) continue;
18580
+ const sessionId = sessionIdFromCodexFilePath(filePath) ?? readCodexSessionId(filePath);
18581
+ if (!sessionId) continue;
18582
+ files.push({ agent: "codex", sessionId, filePath, mtimeMs: stat4.mtimeMs });
18297
18583
  }
18584
+ return files;
18298
18585
  }
18299
-
18300
- // src/cli/commands/org.ts
18301
- async function fetchOrganizations(http, apiKey) {
18302
- return http.post("/api/v2/auth/cli/organizations", { api_key: apiKey });
18586
+ function listSessionFiles(agent) {
18587
+ const files = [];
18588
+ if (agent === "auto" || agent === "claude") {
18589
+ files.push(...listClaudeSessionFiles());
18590
+ }
18591
+ if (agent === "auto" || agent === "codex") {
18592
+ files.push(...listCodexSessionFiles());
18593
+ }
18594
+ return files;
18303
18595
  }
18304
- function orgListLines(orgs) {
18305
- return orgs.map((org, index) => {
18306
- const current = org.is_current ? " (current)" : "";
18307
- const role = org.role ? ` [${org.role}]` : "";
18308
- return `${index + 1}. ${org.name}${role}${current}`;
18309
- });
18596
+ function readDirectoryNames(dir) {
18597
+ try {
18598
+ return readdirSync2(dir);
18599
+ } catch {
18600
+ return [];
18601
+ }
18310
18602
  }
18311
- async function handleOrgList(options) {
18312
- const config = resolveConfig();
18313
- const http = new HttpClient(config);
18314
- const payload = await fetchOrganizations(http, config.apiKey);
18315
- printCommandEnvelope(
18316
- {
18317
- ...payload,
18318
- render: {
18319
- sections: [
18320
- {
18321
- title: "Your organizations:",
18322
- lines: orgListLines(payload.organizations)
18323
- }
18324
- ]
18603
+ function listJsonlFilesRecursive(root, maxDepth) {
18604
+ const files = [];
18605
+ function visit(dir, depth) {
18606
+ if (depth > maxDepth) return;
18607
+ let entries;
18608
+ try {
18609
+ entries = readdirSync2(dir, { withFileTypes: true });
18610
+ } catch {
18611
+ return;
18612
+ }
18613
+ for (const entry of entries) {
18614
+ const fullPath = join9(dir, entry.name);
18615
+ if (entry.isDirectory()) {
18616
+ visit(fullPath, depth + 1);
18617
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
18618
+ files.push(fullPath);
18325
18619
  }
18326
- },
18327
- { json: options.json }
18328
- );
18620
+ }
18621
+ }
18622
+ visit(root, 0);
18623
+ return files;
18329
18624
  }
18330
- async function handleOrgSwitch(selection, options) {
18331
- const config = resolveConfig();
18332
- const http = new HttpClient(config);
18333
- const payload = await fetchOrganizations(http, config.apiKey);
18334
- if (!selection && !options.orgId) {
18335
- printCommandEnvelope(
18336
- {
18337
- ...payload,
18338
- next: { switch: "deepline org switch <number>" },
18339
- render: {
18340
- sections: [
18341
- {
18342
- title: "Your organizations:",
18343
- lines: orgListLines(payload.organizations)
18344
- }
18345
- ],
18346
- actions: [{ label: "Run", command: "deepline org switch <number>" }]
18347
- }
18348
- },
18349
- { json: options.json }
18625
+ function statIfReadable(filePath) {
18626
+ try {
18627
+ return statSync3(filePath);
18628
+ } catch {
18629
+ return null;
18630
+ }
18631
+ }
18632
+ function newestSessionFile(agent) {
18633
+ let newest = null;
18634
+ for (const candidate of listSessionFiles(agent)) {
18635
+ if (!newest || candidate.mtimeMs > newest.mtimeMs) {
18636
+ newest = candidate;
18637
+ }
18638
+ }
18639
+ return newest;
18640
+ }
18641
+ function sessionIdFromClaudeFilePath(filePath) {
18642
+ return basename4(filePath, ".jsonl");
18643
+ }
18644
+ function sessionIdFromCodexFilePath(filePath) {
18645
+ const match = basename4(filePath, ".jsonl").match(UUID_IN_TEXT_RE);
18646
+ return match?.[0] ?? null;
18647
+ }
18648
+ function readCodexSessionId(filePath) {
18649
+ try {
18650
+ for (const line of normalizedJsonLines(readFileSync7(filePath)).slice(
18651
+ 0,
18652
+ 20
18653
+ )) {
18654
+ const parsed = parseJsonLine(line);
18655
+ if (!parsed || typeof parsed !== "object") continue;
18656
+ const record = parsed;
18657
+ const payload = record.payload && typeof record.payload === "object" ? record.payload : null;
18658
+ const id = record.type === "session_meta" && typeof payload?.id === "string" ? payload.id : null;
18659
+ if (id && UUID_RE.test(id)) return id;
18660
+ }
18661
+ } catch {
18662
+ return null;
18663
+ }
18664
+ return null;
18665
+ }
18666
+ function sessionSearchDescription(agent) {
18667
+ if (agent === "claude") return "~/.claude/projects/*/<session-id>.jsonl";
18668
+ if (agent === "codex") return "~/.codex/sessions/**/*.jsonl";
18669
+ return "~/.claude/projects/*/<session-id>.jsonl or ~/.codex/sessions/**/*.jsonl";
18670
+ }
18671
+ function parseSessionAgent(value) {
18672
+ const normalized = String(value || "auto").trim().toLowerCase();
18673
+ if (normalized === "auto" || normalized === "claude" || normalized === "codex") {
18674
+ return normalized;
18675
+ }
18676
+ throw new Error("Invalid --agent value. Expected auto, claude, or codex.");
18677
+ }
18678
+ function findSessionFile(sessionId, agent) {
18679
+ if (!UUID_RE.test(sessionId)) {
18680
+ throw new Error(
18681
+ "Invalid session ID format. Expected a UUID such as 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca."
18350
18682
  );
18351
- return;
18352
18683
  }
18353
- let target = payload.organizations.find(
18354
- (org) => org.org_id === options.orgId
18355
- );
18356
- if (!target && selection) {
18357
- const index = Number.parseInt(selection, 10);
18358
- if (Number.isFinite(index) && index >= 1 && index <= payload.organizations.length) {
18359
- target = payload.organizations[index - 1];
18360
- } else {
18361
- target = payload.organizations.find(
18362
- (org) => org.name === selection || org.org_id === selection
18684
+ for (const candidate of listSessionFiles(agent)) {
18685
+ if (candidate.sessionId === sessionId) {
18686
+ return candidate;
18687
+ }
18688
+ }
18689
+ return null;
18690
+ }
18691
+ function resolveSessionTargets(input2) {
18692
+ const agent = parseSessionAgent(input2.agent);
18693
+ const targets = [];
18694
+ if (input2.currentSession) {
18695
+ const currentFile = newestSessionFile(agent);
18696
+ if (!currentFile) {
18697
+ throw new Error(
18698
+ `No session files found in ${sessionSearchDescription(agent)}.`
18363
18699
  );
18364
18700
  }
18701
+ targets.push({
18702
+ sessionId: currentFile.sessionId,
18703
+ label: `${currentFile.agent}-session-${currentFile.sessionId}`,
18704
+ filePath: currentFile.filePath,
18705
+ agent: currentFile.agent
18706
+ });
18365
18707
  }
18366
- if (!target) {
18367
- throw new Error("Could not resolve the selected organization.");
18708
+ for (const [index, sessionId] of (input2.sessionIds ?? []).entries()) {
18709
+ const candidate = findSessionFile(sessionId, agent);
18710
+ if (!candidate) {
18711
+ throw new Error(
18712
+ `Session file not found in ${sessionSearchDescription(agent)}: ${sessionId}`
18713
+ );
18714
+ }
18715
+ targets.push({
18716
+ sessionId,
18717
+ label: input2.labels?.[index] ?? `${candidate.agent}-session-${sessionId}`,
18718
+ filePath: candidate.filePath,
18719
+ agent: candidate.agent
18720
+ });
18368
18721
  }
18369
- if (target.is_current) {
18370
- printCommandEnvelope(
18371
- {
18372
- ok: true,
18373
- unchanged: true,
18374
- organization: target,
18375
- render: {
18376
- sections: [
18377
- { title: "org switch", lines: [`Already on ${target.name}.`] }
18378
- ]
18379
- }
18380
- },
18381
- { json: options.json }
18382
- );
18383
- return;
18722
+ if (targets.length === 0) {
18723
+ throw new Error("One of --session-id or --current-session is required.");
18384
18724
  }
18385
- const switched = await http.post("/api/v2/auth/cli/switch", {
18386
- api_key: config.apiKey,
18387
- org_id: target.org_id
18388
- });
18389
- saveHostEnvValues(config.baseUrl, {
18390
- DEEPLINE_API_KEY: switched.api_key,
18391
- DEEPLINE_ACTIVE_ORG_ID: switched.org_id,
18392
- DEEPLINE_ACTIVE_ORG_NAME: switched.org_name
18393
- });
18394
- const { api_key: _apiKey, ...publicSwitched } = switched;
18395
- printCommandEnvelope(
18396
- {
18397
- ok: true,
18398
- host_env_path: hostEnvFilePath(config.baseUrl),
18399
- ...publicSwitched,
18400
- api_key_saved: true,
18401
- render: {
18402
- sections: [
18403
- {
18404
- title: "org switch",
18405
- lines: [
18406
- `Switched to ${switched.org_name}.`,
18407
- `Saved host auth in ${hostEnvFilePath(config.baseUrl)}`
18408
- ]
18409
- }
18410
- ]
18411
- }
18412
- },
18413
- { json: options.json }
18414
- );
18725
+ return targets;
18415
18726
  }
18416
- async function handleOrgCreate(name, options) {
18417
- const config = resolveConfig();
18418
- const http = new HttpClient(config);
18419
- const created = await http.post("/api/v2/auth/cli/org-create", {
18420
- api_key: config.apiKey,
18421
- name
18422
- });
18423
- saveHostEnvValues(config.baseUrl, {
18424
- DEEPLINE_API_KEY: created.api_key,
18425
- DEEPLINE_ACTIVE_ORG_ID: created.org_id,
18426
- DEEPLINE_ACTIVE_ORG_NAME: created.org_name
18427
- });
18428
- const { api_key: _apiKey, ...publicCreated } = created;
18429
- printCommandEnvelope(
18430
- {
18431
- ok: true,
18432
- ...publicCreated,
18433
- api_key_saved: true,
18434
- switched: true,
18435
- host_env_path: hostEnvFilePath(config.baseUrl),
18436
- render: {
18437
- sections: [
18438
- {
18439
- title: "org create",
18440
- lines: [
18441
- `Created organization: ${created.org_name}.`,
18442
- `Switched to ${created.org_name}.`,
18443
- `Saved host auth in ${hostEnvFilePath(config.baseUrl)}`
18444
- ]
18445
- }
18446
- ]
18447
- }
18448
- },
18449
- { json: options.json }
18450
- );
18727
+ function parseJsonLine(line) {
18728
+ try {
18729
+ return JSON.parse(line);
18730
+ } catch {
18731
+ return null;
18732
+ }
18451
18733
  }
18452
- function registerOrgCommands(program) {
18453
- const org = program.command("org").description("List, create, and switch organizations.").addHelpText(
18454
- "after",
18455
- `
18456
- Notes:
18457
- Organizations are workspaces. Switching organizations mutates the saved host
18458
- auth file so later CLI commands target the selected workspace.
18459
-
18460
- Examples:
18461
- deepline org list --json
18462
- deepline org create Acme --json
18463
- deepline org switch 2
18464
- deepline org switch --org-id org_123 --json
18465
- `
18466
- );
18467
- org.command("list").description("List your organizations.").addHelpText(
18468
- "after",
18469
- `
18470
- Notes:
18471
- Read-only. Marks the active organization when the server returns that metadata.
18472
-
18473
- Examples:
18474
- deepline org list
18475
- deepline org list --json
18476
- `
18477
- ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgList);
18478
- org.command("create <name>").description("Create a new organization and switch this CLI to it.").addHelpText(
18479
- "after",
18480
- `
18481
- Notes:
18482
- Mutates workspace state. The new organization is created for the current
18483
- authenticated user, then the returned API key is saved for this host so later
18484
- CLI commands target the new organization.
18485
-
18486
- Examples:
18487
- deepline org create Acme
18488
- deepline org create "Acme Sales" --json
18489
- `
18490
- ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgCreate);
18491
- org.command("switch [selection]").description(
18492
- "Switch to another organization and save the new API key in the host auth file."
18493
- ).addHelpText(
18494
- "after",
18495
- `
18496
- Notes:
18497
- Mutates the saved host auth file. Selection can be a list number, exact
18498
- organization name, or organization id. Without a selection, prints choices.
18499
-
18500
- Examples:
18501
- deepline org switch
18502
- deepline org switch 2
18503
- deepline org switch --org-id org_123 --json
18504
- `
18505
- ).option("--org-id <id>", "Switch using an explicit organization id").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgSwitch);
18506
- }
18507
-
18508
- // src/cli/commands/quickstart.ts
18509
- import { spawn, spawnSync } from "child_process";
18510
- import { randomBytes } from "crypto";
18511
- import { createServer } from "http";
18512
- var EXIT_OK2 = 0;
18513
- var EXIT_AUTH2 = 1;
18514
- var EXIT_SERVER3 = 2;
18515
- var MAX_PROMPT_LENGTH = 8e3;
18516
- var MAX_BODY_BYTES = 64 * 1024;
18517
- function shellQuote(arg) {
18518
- return `'${arg.replace(/'/g, `'\\''`)}'`;
18734
+ function normalizedJsonLines(raw) {
18735
+ return raw.toString("utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
18519
18736
  }
18520
- function hasClaudeBinary() {
18521
- try {
18522
- const result = spawnSync("claude", ["--version"], {
18523
- stdio: "ignore",
18524
- shell: process.platform === "win32"
18525
- });
18526
- return result.status === 0;
18527
- } catch {
18528
- return false;
18737
+ function stripNoiseEvents(raw) {
18738
+ const lines = [];
18739
+ for (const line of normalizedJsonLines(raw)) {
18740
+ const parsed = parseJsonLine(line);
18741
+ if (parsed && typeof parsed === "object" && NOISE_EVENT_TYPES.has(String(parsed.type ?? ""))) {
18742
+ continue;
18743
+ }
18744
+ lines.push(line);
18529
18745
  }
18746
+ return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
18747
+ ` : "", "utf8");
18530
18748
  }
18531
- function launchClaude(prompt) {
18532
- return new Promise((resolve16) => {
18533
- const child = spawn("claude", [prompt], {
18534
- stdio: "inherit",
18535
- shell: process.platform === "win32"
18536
- });
18537
- child.on("error", () => resolve16(EXIT_SERVER3));
18538
- child.on("close", (status) => resolve16(status ?? EXIT_OK2));
18539
- });
18540
- }
18541
- function readBody(req) {
18542
- return new Promise((resolve16, reject) => {
18543
- let raw = "";
18544
- req.setEncoding("utf8");
18545
- req.on("data", (chunk) => {
18546
- raw += chunk;
18547
- if (raw.length > MAX_BODY_BYTES) {
18548
- reject(new Error("Request body too large."));
18549
- req.destroy();
18550
- }
18551
- });
18552
- req.on("end", () => resolve16(raw));
18553
- req.on("error", reject);
18554
- });
18555
- }
18556
- function writeJson(res, status, payload) {
18557
- res.writeHead(status, { "content-type": "application/json" });
18558
- res.end(JSON.stringify(payload));
18749
+ function messageContentKey(value) {
18750
+ const message = value.message;
18751
+ if (!message || typeof message !== "object") return null;
18752
+ const content = message.content;
18753
+ if (typeof content === "string") return content;
18754
+ if (!Array.isArray(content)) return null;
18755
+ return content.map((block) => {
18756
+ if (!block || typeof block !== "object") return String(block);
18757
+ const record = block;
18758
+ const type = String(record.type ?? "");
18759
+ if (type === "tool_use") {
18760
+ return `tool_use:${String(record.name ?? "")}:${String(record.id ?? "")}`;
18761
+ }
18762
+ if (type === "tool_result") {
18763
+ return `tool_result:${String(record.tool_use_id ?? "")}`;
18764
+ }
18765
+ return String(record.text ?? type);
18766
+ }).join("\n");
18559
18767
  }
18560
- function startCallbackServer(input2) {
18561
- const server = createServer((req, res) => {
18562
- res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*");
18563
- res.setHeader("Vary", "Origin");
18564
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
18565
- res.setHeader("Access-Control-Allow-Headers", "content-type");
18566
- res.setHeader("Access-Control-Allow-Private-Network", "true");
18567
- res.setHeader("Access-Control-Max-Age", "600");
18568
- if (req.method === "OPTIONS") {
18569
- res.writeHead(204);
18570
- res.end();
18571
- return;
18768
+ function dedupConsecutiveEvents(raw) {
18769
+ const rawLines = normalizedJsonLines(raw);
18770
+ const parsedEvents = rawLines.map(parseJsonLine);
18771
+ const output2 = [];
18772
+ let index = 0;
18773
+ while (index < parsedEvents.length) {
18774
+ const event = parsedEvents[index];
18775
+ if (!event || typeof event !== "object") {
18776
+ output2.push(rawLines[index] ?? "");
18777
+ index += 1;
18778
+ continue;
18572
18779
  }
18573
- if (req.method !== "POST" || req.url !== "/submit") {
18574
- writeJson(res, 404, { error: "Not found." });
18575
- return;
18780
+ const record = event;
18781
+ const eventType = String(record.type ?? "");
18782
+ const eventKey = messageContentKey(record);
18783
+ if (!["user", "assistant"].includes(eventType) || !eventKey) {
18784
+ output2.push(rawLines[index] ?? "");
18785
+ index += 1;
18786
+ continue;
18576
18787
  }
18577
- void readBody(req).then((raw) => {
18578
- let body = null;
18579
- try {
18580
- body = JSON.parse(raw);
18581
- } catch {
18582
- body = null;
18583
- }
18584
- const state = typeof body?.state === "string" ? body.state : "";
18585
- const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
18586
- const workflowId = typeof body?.workflow_id === "string" && body.workflow_id.trim() ? body.workflow_id.trim() : null;
18587
- if (!state || state !== input2.state) {
18588
- writeJson(res, 403, { error: "Invalid quickstart state token." });
18589
- return;
18590
- }
18591
- if (!prompt) {
18592
- writeJson(res, 400, { error: "prompt is required." });
18593
- return;
18594
- }
18595
- if (prompt.length > MAX_PROMPT_LENGTH) {
18596
- writeJson(res, 400, {
18597
- error: `prompt exceeds ${MAX_PROMPT_LENGTH} characters.`
18598
- });
18599
- return;
18600
- }
18601
- writeJson(res, 200, { ok: true });
18602
- setImmediate(() => input2.onSelection({ prompt, workflowId }));
18603
- }).catch(() => {
18604
- writeJson(res, 400, { error: "Invalid request body." });
18605
- });
18606
- });
18607
- return new Promise((resolve16, reject) => {
18608
- server.once("error", reject);
18609
- server.listen(0, "127.0.0.1", () => {
18610
- const address = server.address();
18611
- if (!address || typeof address === "string") {
18612
- reject(new Error("Failed to bind quickstart callback server."));
18613
- return;
18788
+ let runCount = 1;
18789
+ let cursor = index + 1;
18790
+ while (cursor < parsedEvents.length) {
18791
+ const next = parsedEvents[cursor];
18792
+ if (!next || typeof next !== "object") break;
18793
+ const nextRecord = next;
18794
+ if (String(nextRecord.type ?? "") !== eventType || messageContentKey(nextRecord) !== eventKey) {
18795
+ break;
18614
18796
  }
18615
- resolve16({ server, port: address.port });
18616
- });
18617
- });
18618
- }
18619
- async function handleQuickstart(options) {
18620
- const jsonOutput = shouldEmitJson(options.json === true);
18621
- const baseUrl = autoDetectBaseUrl().replace(/\/$/, "");
18622
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
18623
- let apiKey = resolveApiKeyForBaseUrl(baseUrl);
18624
- if (!apiKey) {
18625
- if (interactive) {
18626
- console.error("Not connected yet \u2014 registering this device first.");
18627
- const registerExit = await handleRegister([]);
18628
- if (registerExit !== EXIT_OK2) return registerExit;
18629
- apiKey = resolveApiKeyForBaseUrl(baseUrl);
18797
+ runCount += 1;
18798
+ cursor += 1;
18630
18799
  }
18631
- if (!apiKey) {
18632
- console.error("Not connected. Run: deepline auth register");
18633
- return EXIT_AUTH2;
18800
+ if (runCount > 1) {
18801
+ record._repeat_count = runCount;
18802
+ record._repeat_summary = `${runCount} consecutive identical ${eventType} messages collapsed`;
18803
+ output2.push(JSON.stringify(record));
18804
+ index = cursor;
18805
+ continue;
18634
18806
  }
18807
+ output2.push(rawLines[index] ?? "");
18808
+ index += 1;
18635
18809
  }
18636
- const state = randomBytes(32).toString("hex");
18637
- let resolveSelection;
18638
- const selectionPromise = new Promise((resolve16) => {
18639
- resolveSelection = resolve16;
18640
- });
18641
- let callback;
18642
- try {
18643
- callback = await startCallbackServer({
18644
- state,
18645
- onSelection: (selection) => resolveSelection(selection)
18646
- });
18647
- } catch (error) {
18648
- console.error(
18649
- `Could not start the local quickstart listener: ${error instanceof Error ? error.message : String(error)}`
18650
- );
18651
- return EXIT_SERVER3;
18810
+ return Buffer.from(output2.length > 0 ? `${output2.join("\n")}
18811
+ ` : "", "utf8");
18812
+ }
18813
+ function compactEventValue(value) {
18814
+ if (typeof value === "string") {
18815
+ if (value.length <= MAX_EVENT_STRING_CHARS) return value;
18816
+ return `${value.slice(0, MAX_EVENT_STRING_CHARS - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`;
18652
18817
  }
18653
- const quickstartUrl = `${baseUrl}/cli/quickstart?cb=${callback.port}&state=${state}`;
18654
- console.error(" Opening the recipe picker in your browser.");
18655
- console.error(` If it didn't open, cmd+click: ${quickstartUrl}`);
18656
- if (options.open !== false) {
18657
- openInBrowser(quickstartUrl);
18818
+ if (Array.isArray(value)) {
18819
+ const compacted = value.slice(0, MAX_EVENT_LIST_ITEMS).map(compactEventValue);
18820
+ if (value.length > MAX_EVENT_LIST_ITEMS) {
18821
+ compacted.push(
18822
+ `${TRUNCATION_MARKER} ${value.length - MAX_EVENT_LIST_ITEMS} more item(s)`
18823
+ );
18824
+ }
18825
+ return compacted;
18658
18826
  }
18659
- const timeoutSeconds = Number.parseInt(options.timeout ?? "300", 10);
18660
- const timeoutMs = (Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds : 300) * 1e3;
18661
- const timeoutHandle = setTimeout(
18662
- () => resolveSelection("timeout"),
18663
- timeoutMs
18827
+ if (value && typeof value === "object") {
18828
+ const entries = Object.entries(value);
18829
+ const compacted = {};
18830
+ for (const [key, item] of entries.slice(0, MAX_EVENT_OBJECT_KEYS)) {
18831
+ compacted[key] = compactEventValue(item);
18832
+ }
18833
+ if (entries.length > MAX_EVENT_OBJECT_KEYS) {
18834
+ compacted._truncated_keys = entries.length - MAX_EVENT_OBJECT_KEYS;
18835
+ }
18836
+ return compacted;
18837
+ }
18838
+ return value;
18839
+ }
18840
+ function selectiveCompactToolResults(raw) {
18841
+ const lines = [];
18842
+ for (const line of normalizedJsonLines(raw)) {
18843
+ const parsed = parseJsonLine(line);
18844
+ if (!parsed || typeof parsed !== "object") {
18845
+ lines.push(line);
18846
+ continue;
18847
+ }
18848
+ const record = parsed;
18849
+ if (record.type === "user") {
18850
+ const message = record.message;
18851
+ const content = message && typeof message === "object" ? message.content : null;
18852
+ if (Array.isArray(content)) {
18853
+ message.content = content.map(
18854
+ (block) => block && typeof block === "object" && block.type === "tool_result" ? compactEventValue(block) : block
18855
+ );
18856
+ }
18857
+ }
18858
+ lines.push(JSON.stringify(record));
18859
+ }
18860
+ return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
18861
+ ` : "", "utf8");
18862
+ }
18863
+ function prepareSessionBuffer(raw) {
18864
+ return selectiveCompactToolResults(
18865
+ dedupConsecutiveEvents(stripNoiseEvents(raw))
18664
18866
  );
18665
- const progress = createCliProgress(!jsonOutput);
18666
- progress.phase("waiting for your pick in the browser (Ctrl+C to skip)");
18667
- const onSigint = () => resolveSelection("sigint");
18668
- process.once("SIGINT", onSigint);
18669
- const outcome = await selectionPromise;
18670
- clearTimeout(timeoutHandle);
18671
- process.removeListener("SIGINT", onSigint);
18672
- callback.server.close();
18673
- if (outcome === "sigint") {
18674
- progress.fail();
18675
- console.error("\nSkipped \u2014 run `deepline quickstart` anytime.");
18676
- return EXIT_OK2;
18867
+ }
18868
+ function buildSessionUploadContent(raw) {
18869
+ const prepared = prepareSessionBuffer(raw);
18870
+ const encoded = gzipSync(prepared).toString("base64");
18871
+ if (encoded.length <= MAX_SESSION_UPLOAD_BYTES && prepared.length <= MAX_DIRECT_SESSION_DECODED_BYTES) {
18872
+ return { encodedContent: encoded, needsChunking: false };
18677
18873
  }
18678
- if (outcome === "timeout") {
18679
- progress.fail();
18680
- console.error(
18681
- "Timed out waiting for a selection. Run: deepline quickstart"
18874
+ return { encodedContent: encoded, needsChunking: true };
18875
+ }
18876
+ async function uploadPayload(path, payload) {
18877
+ const { http } = getAuthedHttpClient();
18878
+ return await http.post(path, payload);
18879
+ }
18880
+ async function uploadChunkedSessions(sessions, options) {
18881
+ const uploadId = randomUUID();
18882
+ for (const session of sessions) {
18883
+ const bytes = Buffer.from(session.encodedContent, "base64");
18884
+ const chunks = [];
18885
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE_BYTES) {
18886
+ chunks.push(bytes.subarray(offset, offset + CHUNK_SIZE_BYTES));
18887
+ }
18888
+ process.stderr.write(
18889
+ `Uploading ${session.label} in ${chunks.length} chunk(s)...
18890
+ `
18682
18891
  );
18683
- return EXIT_AUTH2;
18892
+ for (const [index, chunk] of chunks.entries()) {
18893
+ await uploadPayload("/api/v2/cli/send-session/chunk", {
18894
+ upload_id: uploadId,
18895
+ session_id: session.sessionId,
18896
+ index,
18897
+ total_chunks: chunks.length,
18898
+ data: chunk.toString("base64")
18899
+ });
18900
+ }
18684
18901
  }
18685
- progress.complete();
18686
- const { prompt, workflowId } = outcome;
18687
- const claudeCommand = `claude ${shellQuote(prompt)}`;
18688
- const claudeAvailable = hasClaudeBinary();
18689
- const willLaunch = options.launch !== false && !jsonOutput && interactive && claudeAvailable;
18902
+ const response = await uploadPayload("/api/v2/cli/send-session/finalize", {
18903
+ upload_id: uploadId,
18904
+ session_ids: sessions.map((session) => session.sessionId),
18905
+ labels: sessions.map((session) => session.label),
18906
+ environments: sessions.map(() => detectShellContext())
18907
+ });
18690
18908
  printCommandEnvelope(
18691
18909
  {
18692
- status: "submitted",
18693
- workflowId,
18694
- prompt,
18695
- claudeCommand,
18696
- url: quickstartUrl,
18697
- launched: willLaunch,
18910
+ ...response,
18911
+ ok: true,
18912
+ uploaded: sessions.length,
18698
18913
  render: {
18699
18914
  sections: [
18700
18915
  {
18701
- title: "quickstart",
18702
- lines: [
18703
- ...workflowId ? [`Recipe: ${workflowId}`] : [],
18704
- willLaunch ? "Launching Claude Code with your recipe..." : claudeAvailable ? "Run the command below to start your recipe." : "Claude Code not found. Install it (https://code.claude.com/docs/en/overview), then run the command below."
18705
- ]
18916
+ title: "sessions send",
18917
+ lines: ["Session uploaded to #internal-reports (chunked)."]
18706
18918
  }
18707
- ],
18708
- actions: [{ label: "Run", command: claudeCommand }]
18919
+ ]
18709
18920
  }
18710
18921
  },
18711
- { json: jsonOutput }
18922
+ { json: options.json }
18712
18923
  );
18713
- if (willLaunch) {
18714
- return launchClaude(prompt);
18715
- }
18716
- return EXIT_OK2;
18717
- }
18718
- function registerQuickstartCommands(program) {
18719
- program.command("quickstart").description(
18720
- "Pick a starter recipe in the browser and launch it with Claude Code."
18721
- ).addHelpText(
18722
- "after",
18723
- `
18724
- Notes:
18725
- Opens the hosted recipe picker in your browser. The picker sends your
18726
- selection back to a temporary listener on 127.0.0.1, so the browser must run
18727
- on the same machine as the CLI. Once a recipe arrives, the CLI prints the
18728
- matching claude command and, when Claude Code is installed and the shell is
18729
- interactive, launches it directly. Press Ctrl+C while waiting to skip.
18730
-
18731
- Examples:
18732
- deepline quickstart
18733
- deepline quickstart --no-open
18734
- deepline quickstart --no-launch --json
18735
- deepline quickstart --timeout 120
18736
- `
18737
- ).option("--json", "Emit JSON output. Also automatic when stdout is piped").option("--no-open", "Do not open the browser; print the URL only").option("--no-launch", "Print the claude command instead of launching it").option(
18738
- "--timeout <seconds>",
18739
- "Maximum seconds to wait for a selection",
18740
- "300"
18741
- ).action(async (options) => {
18742
- process.exitCode = await handleQuickstart(options);
18743
- });
18744
18924
  }
18745
-
18746
- // src/cli/commands/secrets.ts
18747
- import { stdin as input, stdout as output } from "process";
18748
- var hiddenInputBuffer = "";
18749
- function normalizeSecretName(value) {
18750
- const normalized = value.trim().toUpperCase();
18751
- if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(normalized)) {
18752
- throw new Error(
18753
- "Secret names must be 2-64 characters and use uppercase letters, numbers, and underscores."
18925
+ async function handleSessionsSend(options) {
18926
+ if (options.file) {
18927
+ const filePath = resolve12(options.file);
18928
+ if (!existsSync8(filePath)) {
18929
+ throw new Error(`File not found: ${options.file}`);
18930
+ }
18931
+ const response2 = await uploadPayload("/api/v2/cli/send-session", {
18932
+ file: readFileSync7(filePath).toString("base64"),
18933
+ filename: basename4(filePath)
18934
+ });
18935
+ printCommandEnvelope(
18936
+ {
18937
+ ...response2,
18938
+ ok: true,
18939
+ filename: basename4(filePath),
18940
+ render: {
18941
+ sections: [
18942
+ {
18943
+ title: "sessions send",
18944
+ lines: [
18945
+ `File '${basename4(filePath)}' uploaded to #internal-reports.`
18946
+ ]
18947
+ }
18948
+ ]
18949
+ }
18950
+ },
18951
+ { json: options.json }
18754
18952
  );
18953
+ return;
18755
18954
  }
18756
- return normalized;
18757
- }
18758
- function renderSecret(secret) {
18759
- const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
18760
- return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
18761
- }
18762
- async function readHiddenLine(prompt) {
18763
- if (!input.isTTY || !output.isTTY) {
18764
- throw new Error(
18765
- "Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
18766
- );
18955
+ const targets = resolveSessionTargets({
18956
+ sessionIds: options.sessionId,
18957
+ labels: options.label,
18958
+ currentSession: options.currentSession,
18959
+ agent: options.agent
18960
+ });
18961
+ const built = targets.map((target) => {
18962
+ const upload = buildSessionUploadContent(readFileSync7(target.filePath));
18963
+ return { ...target, ...upload };
18964
+ });
18965
+ if (built.some((session) => session.needsChunking)) {
18966
+ await uploadChunkedSessions(built, options);
18967
+ return;
18767
18968
  }
18768
- output.write(prompt);
18769
- const previousRawMode = input.isRaw;
18770
- if (typeof input.setRawMode === "function") input.setRawMode(true);
18771
- let value = "";
18772
- input.resume();
18773
- return await new Promise((resolve16, reject) => {
18774
- let settled = false;
18775
- const cleanup = () => {
18776
- input.off("data", onData);
18777
- input.off("end", onEnd);
18778
- input.off("error", onError);
18779
- if (typeof input.setRawMode === "function") {
18780
- input.setRawMode(previousRawMode);
18969
+ const response = built.length === 1 && !options.label?.length ? await uploadPayload("/api/v2/cli/send-session", {
18970
+ session_id: built[0]?.sessionId,
18971
+ content: built[0]?.encodedContent,
18972
+ environment: detectShellContext()
18973
+ }) : await uploadPayload("/api/v2/cli/send-session", {
18974
+ sessions: built.map((session) => ({
18975
+ session_id: session.sessionId,
18976
+ content: session.encodedContent,
18977
+ label: session.label,
18978
+ environment: detectShellContext()
18979
+ })),
18980
+ environment: detectShellContext()
18981
+ });
18982
+ printCommandEnvelope(
18983
+ {
18984
+ ...response,
18985
+ ok: true,
18986
+ uploaded: built.length,
18987
+ render: {
18988
+ sections: [
18989
+ {
18990
+ title: "sessions send",
18991
+ lines: ["Session uploaded to #internal-reports."]
18992
+ }
18993
+ ]
18781
18994
  }
18782
- };
18783
- const finish = (line) => {
18784
- if (settled) return;
18785
- settled = true;
18786
- output.write("\n");
18787
- cleanup();
18788
- resolve16(line);
18789
- };
18790
- const fail = (error) => {
18791
- if (settled) return;
18792
- settled = true;
18793
- output.write("\n");
18794
- cleanup();
18795
- reject(error);
18796
- };
18797
- const processText = (text) => {
18798
- for (let index = 0; index < text.length; index++) {
18799
- const char = text[index];
18800
- const code = char.charCodeAt(0);
18801
- if (char === "\r" || char === "\n") {
18802
- hiddenInputBuffer = text.slice(index + 1);
18803
- finish(value);
18804
- return;
18805
- }
18806
- if (code === 3) {
18807
- fail(new Error("Secret input cancelled."));
18808
- return;
18809
- }
18810
- if (code === 8 || code === 127) {
18811
- value = value.slice(0, -1);
18812
- continue;
18813
- }
18814
- if (code >= 32) {
18815
- value += char;
18816
- }
18817
- }
18818
- hiddenInputBuffer = "";
18819
- };
18820
- const onData = (chunk) => {
18821
- processText(chunk.toString());
18822
- };
18823
- const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
18824
- const onError = (error) => fail(error);
18825
- input.on("data", onData);
18826
- input.once("end", onEnd);
18827
- input.once("error", onError);
18828
- if (hiddenInputBuffer) {
18829
- const buffered = hiddenInputBuffer;
18830
- hiddenInputBuffer = "";
18831
- processText(buffered);
18832
- }
18833
- });
18995
+ },
18996
+ { json: options.json }
18997
+ );
18834
18998
  }
18835
- async function readSecretValue() {
18836
- const first = await readHiddenLine("Secret value: ");
18837
- if (!first) throw new Error("Secret value is required.");
18838
- const second = await readHiddenLine("Confirm secret value: ");
18839
- if (first !== second) {
18840
- throw new Error("Secret values did not match.");
18841
- }
18842
- return first;
18999
+ function fallbackViewerAssets() {
19000
+ return {
19001
+ css: [
19002
+ "body{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin:0;padding:16px;background:#fafafa;color:#111}",
19003
+ ".section{background:#fff;border:1px solid #ddd;border-radius:8px;padding:12px;margin-bottom:12px}",
19004
+ ".section h2{margin:0 0 8px 0;font-size:14px}",
19005
+ "pre{margin:0;white-space:pre-wrap;word-break:break-word;background:#f6f8fa;border:1px solid #e3e5e8;border-radius:6px;padding:10px}"
19006
+ ].join(""),
19007
+ js: [
19008
+ "(() => {",
19009
+ 'const root=document.getElementById("main-content");',
19010
+ 'const raw=document.getElementById("raw-sessions");',
19011
+ "if(!root||!raw)return;",
19012
+ 'let sessions=[];try{sessions=JSON.parse(raw.textContent||"[]")}catch{}',
19013
+ 'root.innerHTML="";',
19014
+ "for(const session of sessions){",
19015
+ 'const section=document.createElement("section");section.className="section";',
19016
+ 'const title=document.createElement("h2");title.textContent=String(session.label||"session");',
19017
+ 'const pre=document.createElement("pre");',
19018
+ 'pre.textContent=(Array.isArray(session.events)?session.events:[]).map((event)=>JSON.stringify(event)).join("\\n");',
19019
+ "section.append(title,pre);root.appendChild(section);",
19020
+ "}",
19021
+ "})();"
19022
+ ].join("")
19023
+ };
18843
19024
  }
18844
- function preventShellHistoryLeak(forbidden) {
18845
- if (forbidden.length > 0) {
18846
- throw new Error(
18847
- "Do not pass secret values as command arguments. Run `deepline secrets set NAME` and enter the value at the hidden prompt."
18848
- );
19025
+ function loadViewerAssets() {
19026
+ const cliEntry = process.argv[1]?.trim() ? resolve12(process.argv[1]) : null;
19027
+ const candidateRoots2 = [
19028
+ ...cliEntry ? [
19029
+ join9(dirname9(dirname9(cliEntry)), "viewer"),
19030
+ join9(
19031
+ dirname9(dirname9(dirname9(cliEntry))),
19032
+ "src",
19033
+ "lib",
19034
+ "cli",
19035
+ "viewer"
19036
+ )
19037
+ ] : [],
19038
+ join9(process.cwd(), "src", "lib", "cli", "viewer")
19039
+ ];
19040
+ for (const root of candidateRoots2) {
19041
+ try {
19042
+ const cssPath = join9(root, "viewer.css");
19043
+ const jsPath = join9(root, "viewer.js");
19044
+ if (!existsSync8(cssPath) || !existsSync8(jsPath)) continue;
19045
+ return {
19046
+ css: readFileSync7(cssPath, "utf8"),
19047
+ js: readFileSync7(jsPath, "utf8")
19048
+ };
19049
+ } catch {
19050
+ continue;
19051
+ }
18849
19052
  }
19053
+ return fallbackViewerAssets();
18850
19054
  }
18851
- async function handleList(options) {
18852
- const client2 = new DeeplineClient();
18853
- const secrets = await client2.listSecrets();
18854
- printCommandEnvelope(
18855
- {
18856
- secrets,
18857
- count: secrets.length,
18858
- render: {
18859
- sections: [
18860
- {
18861
- title: "secrets",
18862
- lines: secrets.length ? secrets.map(renderSecret) : ["No play secrets are configured."]
18863
- }
18864
- ]
18865
- }
18866
- },
18867
- { json: options.json }
18868
- );
19055
+ function parsePreparedEvents(buffer) {
19056
+ return normalizedJsonLines(buffer).map((line) => {
19057
+ const parsed = parseJsonLine(line);
19058
+ return parsed ?? line;
19059
+ });
18869
19060
  }
18870
- async function handleCheck(nameInput, options) {
18871
- const name = normalizeSecretName(nameInput);
18872
- const client2 = new DeeplineClient();
18873
- const secret = await client2.checkSecret(name);
18874
- printCommandEnvelope(
18875
- {
18876
- ok: Boolean(secret),
18877
- name,
18878
- secret: secret ?? null,
18879
- render: {
18880
- sections: [
18881
- {
18882
- title: "secret check",
18883
- lines: [
18884
- secret ? `${name}: active` : `${name}: missing, disabled, or empty`
18885
- ]
18886
- }
18887
- ]
18888
- }
18889
- },
18890
- { json: options.json }
18891
- );
18892
- if (!secret) process.exitCode = 4;
19061
+ function escapeJsonForHtmlScript(json) {
19062
+ return json.replace(/</g, "\\u003c");
18893
19063
  }
18894
- async function handleSet(nameInput, forbidden, options) {
18895
- preventShellHistoryLeak(forbidden);
18896
- const name = normalizeSecretName(nameInput);
18897
- const scope = options.scope === "play" ? "play" : "org";
18898
- const playName = options.play?.trim();
18899
- if (scope === "play" && !playName) {
18900
- throw new Error("--play <name> is required when --scope play is used.");
19064
+ async function handleSessionsRender(options) {
19065
+ const targets = resolveSessionTargets({
19066
+ sessionIds: options.sessionId,
19067
+ labels: options.label,
19068
+ currentSession: options.currentSession,
19069
+ agent: options.agent
19070
+ });
19071
+ let outputPath = options.output ? resolve12(options.output) : "";
19072
+ if (!outputPath) {
19073
+ const outputDir = join9(process.cwd(), "deepline", "data");
19074
+ mkdirSync4(outputDir, { recursive: true });
19075
+ outputPath = join9(
19076
+ outputDir,
19077
+ targets.length > 1 ? "session-viewer.html" : `session-${targets[0]?.sessionId}.html`
19078
+ );
19079
+ } else {
19080
+ mkdirSync4(dirname9(outputPath), { recursive: true });
18901
19081
  }
18902
- const value = await readSecretValue();
18903
- const { http } = getAuthedHttpClient();
18904
- const response = await http.post(
18905
- "/api/v2/secrets",
18906
- {
18907
- name,
18908
- value,
18909
- scope,
18910
- ...playName ? { playName } : {}
18911
- }
18912
- );
18913
- const secret = response.secret;
19082
+ const sessions = targets.map((target) => ({
19083
+ label: target.label,
19084
+ events: parsePreparedEvents(
19085
+ prepareSessionBuffer(readFileSync7(target.filePath))
19086
+ )
19087
+ }));
19088
+ const { css, js } = loadViewerAssets();
19089
+ const refreshMeta = options.autoRefresh ? `<meta http-equiv="refresh" content="${Number.parseInt(options.autoRefresh, 10)}">` : "";
19090
+ const rawJson = escapeJsonForHtmlScript(JSON.stringify(sessions));
19091
+ const html = `<!DOCTYPE html>
19092
+ <html lang="en">
19093
+ <head>
19094
+ <meta charset="UTF-8">
19095
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19096
+ ${refreshMeta}
19097
+ <title>Session Viewer</title>
19098
+ <style>${css}</style>
19099
+ </head>
19100
+ <body>
19101
+ <div class="layout">
19102
+ <div class="main" id="main-content"></div>
19103
+ </div>
19104
+ <script type="application/json" id="raw-sessions">${rawJson}</script>
19105
+ <script>${js}</script>
19106
+ </body>
19107
+ </html>`;
19108
+ writeFileSync8(outputPath, html, "utf8");
18914
19109
  printCommandEnvelope(
18915
19110
  {
18916
19111
  ok: true,
18917
- secret,
19112
+ file: outputPath,
19113
+ session_count: targets.length,
18918
19114
  render: {
18919
19115
  sections: [
18920
19116
  {
18921
- title: "secret saved",
18922
- lines: [`${secret.name}: saved (${secret.scope})`]
19117
+ title: "sessions render",
19118
+ lines: [`Rendered session viewer: ${outputPath}`]
18923
19119
  }
18924
19120
  ]
18925
19121
  }
@@ -18927,346 +19123,759 @@ async function handleSet(nameInput, forbidden, options) {
18927
19123
  { json: options.json }
18928
19124
  );
18929
19125
  }
18930
- function registerSecretsCommands(program) {
18931
- const secrets = program.command("secrets").description("Manage play secrets without revealing values.").addHelpText(
19126
+ function collectOption(value, previous) {
19127
+ previous.push(value);
19128
+ return previous;
19129
+ }
19130
+ function registerSessionsCommands(program) {
19131
+ const sessions = program.command("sessions").description("Upload and render local agent session transcripts.").addHelpText(
18932
19132
  "after",
18933
19133
  `
18934
19134
  Notes:
18935
- Secret values are never accepted as command arguments, stdin pipes, env vars,
18936
- or files. Use deepline secrets set NAME and type the value at the hidden TTY
18937
- prompt. Agents can list/check metadata but should not enter secret values.
19135
+ Session commands operate on local Claude and Codex JSONL files under
19136
+ ~/.claude/projects and ~/.codex/sessions. send uploads a compacted transcript
19137
+ or file to Deepline. render writes a local HTML viewer.
18938
19138
 
18939
19139
  Examples:
18940
- deepline secrets list
18941
- deepline secrets check HUBSPOT_TOKEN
18942
- deepline secrets set HUBSPOT_TOKEN
19140
+ deepline sessions send --current-session --json
19141
+ deepline sessions send --agent codex --current-session --json
19142
+ deepline sessions send --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca
19143
+ deepline sessions render --current-session --output session.html
18943
19144
  `
18944
19145
  );
18945
- secrets.command("list").description("List secret metadata only.").option("--json", "Emit JSON output").action(async (options) => {
18946
- await handleList(options);
18947
- });
18948
- secrets.command("check").description("Check whether a secret exists and is active.").argument("<name>", "Secret name").option("--json", "Emit JSON output").action(async (name, options) => {
18949
- await handleCheck(name, options);
18950
- });
18951
- secrets.command("set").description("Set or rotate a secret through a hidden interactive prompt.").argument("<name>", "Secret name").argument("[forbidden...]", "Do not pass secret values here").option("--json", "Emit JSON output").option("--scope <scope>", "Secret scope: org or play", "org").option("--play <name>", "Play name for play-scoped secrets").action(
18952
- async (name, forbidden, options) => {
18953
- await handleSet(name, forbidden ?? [], options);
18954
- }
18955
- );
19146
+ registerSessionSendRenderCommands(sessions, "sessions");
18956
19147
  }
18957
-
18958
- // src/cli/commands/sessions.ts
18959
- import {
18960
- existsSync as existsSync8,
18961
- mkdirSync as mkdirSync4,
18962
- readdirSync as readdirSync2,
18963
- readFileSync as readFileSync7,
18964
- statSync as statSync3,
18965
- writeFileSync as writeFileSync8
18966
- } from "fs";
18967
- import { homedir as homedir5, platform } from "os";
18968
- import { basename as basename4, dirname as dirname9, join as join9, resolve as resolve12 } from "path";
18969
- import { gzipSync } from "zlib";
18970
- import { randomUUID } from "crypto";
18971
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18972
- var MAX_SESSION_UPLOAD_BYTES = 35e5;
18973
- var MAX_DIRECT_SESSION_DECODED_BYTES = 50 * 1024 * 1024;
18974
- var CHUNK_SIZE_BYTES = 25e5;
18975
- var MAX_EVENT_STRING_CHARS = 8e3;
18976
- var MAX_EVENT_LIST_ITEMS = 40;
18977
- var MAX_EVENT_OBJECT_KEYS = 80;
18978
- var TRUNCATION_MARKER = "...[truncated]";
18979
- var NOISE_EVENT_TYPES = /* @__PURE__ */ new Set(["progress", "file-history-snapshot"]);
18980
- function homeDir() {
18981
- return process.env.HOME?.trim() || homedir5();
19148
+ function registerSessionSendRenderCommands(parent, familyName) {
19149
+ parent.command("send").description("Upload session transcript(s) or a local file to Deepline.").addHelpText(
19150
+ "after",
19151
+ `
19152
+ Examples:
19153
+ deepline ${familyName} send --current-session --json
19154
+ deepline ${familyName} send --agent codex --current-session --json
19155
+ deepline ${familyName} send --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca --label "pilot run"
19156
+ deepline ${familyName} send --file ./debug.log --json
19157
+ `
19158
+ ).option(
19159
+ "--session-id <uuid>",
19160
+ "Claude or Codex session UUID. Repeat for multiple sessions.",
19161
+ collectOption,
19162
+ []
19163
+ ).option(
19164
+ "--label <label>",
19165
+ "Label for the preceding session id",
19166
+ collectOption,
19167
+ []
19168
+ ).option(
19169
+ "--agent <auto|claude|codex>",
19170
+ "Limit transcript discovery to one agent runtime",
19171
+ "auto"
19172
+ ).option("--current-session", "Use the newest local agent session JSONL").option("--file <path>", "Upload a raw local file instead of a session").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleSessionsSend);
19173
+ parent.command("render").description("Render local session transcript(s) to an HTML viewer.").addHelpText(
19174
+ "after",
19175
+ `
19176
+ Examples:
19177
+ deepline ${familyName} render --current-session
19178
+ deepline ${familyName} render --agent codex --current-session
19179
+ deepline ${familyName} render --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca --output session.html
19180
+ deepline ${familyName} render --current-session --auto-refresh 5 --json
19181
+ `
19182
+ ).option(
19183
+ "--session-id <uuid>",
19184
+ "Claude or Codex session UUID. Repeat for multiple sessions.",
19185
+ collectOption,
19186
+ []
19187
+ ).option(
19188
+ "--label <label>",
19189
+ "Label for the preceding session id",
19190
+ collectOption,
19191
+ []
19192
+ ).option(
19193
+ "--agent <auto|claude|codex>",
19194
+ "Limit transcript discovery to one agent runtime",
19195
+ "auto"
19196
+ ).option("--current-session", "Use the newest local agent session JSONL").option("--auto-refresh <seconds>", "Add a browser auto-refresh interval").option("-o, --output <path>", "Output HTML path").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleSessionsRender);
18982
19197
  }
18983
- function detectShellContext() {
18984
- const shellPath = process.env.SHELL?.trim() || process.env.ComSpec?.trim() || process.env.COMSPEC?.trim() || "";
19198
+
19199
+ // src/cli/commands/legacy-noops.ts
19200
+ var SESSION_SUBCOMMANDS = [
19201
+ "start",
19202
+ "status",
19203
+ "output",
19204
+ "alert",
19205
+ "usage",
19206
+ "limit"
19207
+ ];
19208
+ var BACKEND_SUBCOMMANDS = [
19209
+ "start",
19210
+ "stop",
19211
+ "status",
19212
+ "refresh-runtime",
19213
+ "sync-runtime"
19214
+ ];
19215
+ function legacyNoopEnvelope(input2) {
19216
+ const command = ["deepline", input2.family, input2.subcommand].filter(Boolean).join(" ");
19217
+ const subject = input2.family === "session" ? "Legacy Session UI/playground command" : "Legacy local playground backend command";
19218
+ const note = input2.family === "session" ? "The SDK CLI does not manage the legacy Python Session UI. This command is accepted for backwards compatibility and intentionally does nothing." : "The SDK CLI does not start or stop the legacy Python local playground backend. This command is accepted for backwards compatibility and intentionally does nothing.";
18985
19219
  return {
18986
- shell: shellPath ? basename4(shellPath).replace(/\.exe$/i, "") : "unknown",
18987
- shell_path: shellPath || null,
18988
- os: platform(),
18989
- cwd: process.cwd()
18990
- };
18991
- }
18992
- function claudeProjectsRoot() {
18993
- return join9(homeDir(), ".claude", "projects");
18994
- }
18995
- function listClaudeSessionFiles() {
18996
- const root = claudeProjectsRoot();
18997
- if (!existsSync8(root)) return [];
18998
- const projectDirs = readDirectoryNames(root);
18999
- const files = [];
19000
- for (const projectDir of projectDirs) {
19001
- const fullProjectDir = join9(root, projectDir);
19002
- for (const fileName of readDirectoryNames(fullProjectDir)) {
19003
- if (fileName.endsWith(".jsonl")) {
19004
- files.push(join9(fullProjectDir, fileName));
19005
- }
19220
+ ok: true,
19221
+ noop: true,
19222
+ command,
19223
+ compatibility: {
19224
+ family: "legacy_python_cli",
19225
+ sdk_behavior: "noop"
19226
+ },
19227
+ render: {
19228
+ sections: [
19229
+ {
19230
+ title: subject,
19231
+ lines: [note]
19232
+ }
19233
+ ]
19006
19234
  }
19007
- }
19008
- return files;
19009
- }
19010
- function readDirectoryNames(dir) {
19011
- try {
19012
- return readdirSync2(dir);
19013
- } catch {
19014
- return [];
19015
- }
19235
+ };
19016
19236
  }
19017
- function newestClaudeSessionFile() {
19018
- let newest = null;
19019
- for (const filePath of listClaudeSessionFiles()) {
19020
- try {
19021
- const stat4 = statSync3(filePath);
19022
- if (!newest || stat4.mtimeMs > newest.mtimeMs) {
19023
- newest = { filePath, mtimeMs: stat4.mtimeMs };
19024
- }
19025
- } catch {
19026
- continue;
19027
- }
19028
- }
19029
- return newest?.filePath ?? null;
19237
+ function printLegacyNoop(input2) {
19238
+ printCommandEnvelope(legacyNoopEnvelope(input2), { json: input2.options.json });
19030
19239
  }
19031
- function sessionIdFromFilePath(filePath) {
19032
- return basename4(filePath, ".jsonl");
19240
+ function legacySubcommandFromArgv(family) {
19241
+ const args = process.argv.slice(2);
19242
+ const familyIndex = args.indexOf(family);
19243
+ const nextToken = familyIndex >= 0 ? args[familyIndex + 1] : void 0;
19244
+ return nextToken && !nextToken.startsWith("-") ? nextToken : void 0;
19033
19245
  }
19034
- function findSessionFile(sessionId) {
19035
- if (!UUID_RE.test(sessionId)) {
19036
- throw new Error(
19037
- "Invalid session ID format. Expected a UUID such as 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca."
19038
- );
19039
- }
19040
- for (const filePath of listClaudeSessionFiles()) {
19041
- if (sessionIdFromFilePath(filePath) === sessionId) {
19042
- return filePath;
19043
- }
19044
- }
19045
- return null;
19246
+ function addLegacyNoopSubcommand(parent, family, subcommand, description) {
19247
+ parent.command(subcommand).description(description).allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").action((_args, options) => {
19248
+ printLegacyNoop({ family, subcommand, options });
19249
+ });
19046
19250
  }
19047
- function resolveSessionTargets(input2) {
19048
- const targets = [];
19049
- if (input2.currentSession) {
19050
- const currentFile = newestClaudeSessionFile();
19051
- if (!currentFile) {
19052
- throw new Error("No session files found in ~/.claude/projects/*/.");
19053
- }
19054
- const sessionId = sessionIdFromFilePath(currentFile);
19055
- targets.push({
19056
- sessionId,
19057
- label: `session-${sessionId}`,
19058
- filePath: currentFile
19251
+ function registerLegacyNoopCommands(program) {
19252
+ const session = program.command("session").description("Compatibility no-ops for legacy Python Session UI commands.").allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").addHelpText(
19253
+ "after",
19254
+ `
19255
+ Notes:
19256
+ The SDK CLI accepts singular session commands from older agent skills so they
19257
+ do not fail, but it does not manage the legacy Python Session UI.
19258
+ Use "deepline sessions send" or "deepline sessions render" for real SDK
19259
+ transcript workflows. "deepline session send" and "deepline session render"
19260
+ are accepted aliases for those real SDK workflows.
19261
+
19262
+ Examples:
19263
+ deepline session start --steps '["Inspect CSV","Run pilot"]'
19264
+ deepline session status --message "Running pilot"
19265
+ deepline session output --csv ./results.csv --label "Results"
19266
+ `
19267
+ ).action((args, options) => {
19268
+ void args;
19269
+ printLegacyNoop({
19270
+ family: "session",
19271
+ subcommand: legacySubcommandFromArgv("session"),
19272
+ options
19059
19273
  });
19274
+ });
19275
+ registerSessionSendRenderCommands(session, "session");
19276
+ for (const subcommand of SESSION_SUBCOMMANDS) {
19277
+ addLegacyNoopSubcommand(
19278
+ session,
19279
+ "session",
19280
+ subcommand,
19281
+ `Accept legacy "deepline session ${subcommand}" as an SDK no-op.`
19282
+ );
19060
19283
  }
19061
- for (const [index, sessionId] of (input2.sessionIds ?? []).entries()) {
19062
- const filePath = findSessionFile(sessionId);
19063
- if (!filePath) {
19064
- throw new Error(
19065
- `Session file not found: ~/.claude/projects/*/${sessionId}.jsonl`
19066
- );
19067
- }
19068
- targets.push({
19069
- sessionId,
19070
- label: input2.labels?.[index] ?? `session-${sessionId}`,
19071
- filePath
19284
+ const backend = program.command("backend").description(
19285
+ "Compatibility no-ops for legacy Python local backend commands."
19286
+ ).allowUnknownOption(true).allowExcessArguments(true).option("--json", "Emit JSON output").argument("[args...]").addHelpText(
19287
+ "after",
19288
+ `
19289
+ Notes:
19290
+ The SDK CLI uses the configured Deepline host and V2 play runtime. It does not
19291
+ start, stop, or refresh the legacy Python local playground backend.
19292
+
19293
+ Examples:
19294
+ deepline backend start
19295
+ deepline backend stop --just-backend
19296
+ deepline backend status --json
19297
+ `
19298
+ ).action((args, options) => {
19299
+ void args;
19300
+ printLegacyNoop({
19301
+ family: "backend",
19302
+ subcommand: legacySubcommandFromArgv("backend"),
19303
+ options
19072
19304
  });
19305
+ });
19306
+ for (const subcommand of BACKEND_SUBCOMMANDS) {
19307
+ addLegacyNoopSubcommand(
19308
+ backend,
19309
+ "backend",
19310
+ subcommand,
19311
+ `Accept legacy "deepline backend ${subcommand}" as an SDK no-op.`
19312
+ );
19073
19313
  }
19074
- if (targets.length === 0) {
19075
- throw new Error("One of --session-id or --current-session is required.");
19076
- }
19077
- return targets;
19078
19314
  }
19079
- function parseJsonLine(line) {
19080
- try {
19081
- return JSON.parse(line);
19082
- } catch {
19083
- return null;
19084
- }
19315
+
19316
+ // src/cli/commands/org.ts
19317
+ async function fetchOrganizations(http, apiKey) {
19318
+ return http.post("/api/v2/auth/cli/organizations", { api_key: apiKey });
19085
19319
  }
19086
- function normalizedJsonLines(raw) {
19087
- return raw.toString("utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
19320
+ function orgListLines(orgs) {
19321
+ return orgs.map((org, index) => {
19322
+ const current = org.is_current ? " (current)" : "";
19323
+ const role = org.role ? ` [${org.role}]` : "";
19324
+ return `${index + 1}. ${org.name}${role}${current}`;
19325
+ });
19088
19326
  }
19089
- function stripNoiseEvents(raw) {
19090
- const lines = [];
19091
- for (const line of normalizedJsonLines(raw)) {
19092
- const parsed = parseJsonLine(line);
19093
- if (parsed && typeof parsed === "object" && NOISE_EVENT_TYPES.has(String(parsed.type ?? ""))) {
19094
- continue;
19095
- }
19096
- lines.push(line);
19097
- }
19098
- return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
19099
- ` : "", "utf8");
19327
+ async function handleOrgList(options) {
19328
+ const config = resolveConfig();
19329
+ const http = new HttpClient(config);
19330
+ const payload = await fetchOrganizations(http, config.apiKey);
19331
+ printCommandEnvelope(
19332
+ {
19333
+ ...payload,
19334
+ render: {
19335
+ sections: [
19336
+ {
19337
+ title: "Your organizations:",
19338
+ lines: orgListLines(payload.organizations)
19339
+ }
19340
+ ]
19341
+ }
19342
+ },
19343
+ { json: options.json }
19344
+ );
19100
19345
  }
19101
- function messageContentKey(value) {
19102
- const message = value.message;
19103
- if (!message || typeof message !== "object") return null;
19104
- const content = message.content;
19105
- if (typeof content === "string") return content;
19106
- if (!Array.isArray(content)) return null;
19107
- return content.map((block) => {
19108
- if (!block || typeof block !== "object") return String(block);
19109
- const record = block;
19110
- const type = String(record.type ?? "");
19111
- if (type === "tool_use") {
19112
- return `tool_use:${String(record.name ?? "")}:${String(record.id ?? "")}`;
19113
- }
19114
- if (type === "tool_result") {
19115
- return `tool_result:${String(record.tool_use_id ?? "")}`;
19346
+ async function handleOrgSwitch(selection, options) {
19347
+ const config = resolveConfig();
19348
+ const http = new HttpClient(config);
19349
+ const payload = await fetchOrganizations(http, config.apiKey);
19350
+ if (!selection && !options.orgId) {
19351
+ printCommandEnvelope(
19352
+ {
19353
+ ...payload,
19354
+ next: { switch: "deepline org switch <number>" },
19355
+ render: {
19356
+ sections: [
19357
+ {
19358
+ title: "Your organizations:",
19359
+ lines: orgListLines(payload.organizations)
19360
+ }
19361
+ ],
19362
+ actions: [{ label: "Run", command: "deepline org switch <number>" }]
19363
+ }
19364
+ },
19365
+ { json: options.json }
19366
+ );
19367
+ return;
19368
+ }
19369
+ let target = payload.organizations.find(
19370
+ (org) => org.org_id === options.orgId
19371
+ );
19372
+ if (!target && selection) {
19373
+ const index = Number.parseInt(selection, 10);
19374
+ if (Number.isFinite(index) && index >= 1 && index <= payload.organizations.length) {
19375
+ target = payload.organizations[index - 1];
19376
+ } else {
19377
+ target = payload.organizations.find(
19378
+ (org) => org.name === selection || org.org_id === selection
19379
+ );
19116
19380
  }
19117
- return String(record.text ?? type);
19118
- }).join("\n");
19381
+ }
19382
+ if (!target) {
19383
+ throw new Error("Could not resolve the selected organization.");
19384
+ }
19385
+ if (target.is_current) {
19386
+ printCommandEnvelope(
19387
+ {
19388
+ ok: true,
19389
+ unchanged: true,
19390
+ organization: target,
19391
+ render: {
19392
+ sections: [
19393
+ { title: "org switch", lines: [`Already on ${target.name}.`] }
19394
+ ]
19395
+ }
19396
+ },
19397
+ { json: options.json }
19398
+ );
19399
+ return;
19400
+ }
19401
+ const switched = await http.post("/api/v2/auth/cli/switch", {
19402
+ api_key: config.apiKey,
19403
+ org_id: target.org_id
19404
+ });
19405
+ saveHostEnvValues(config.baseUrl, {
19406
+ DEEPLINE_API_KEY: switched.api_key,
19407
+ DEEPLINE_ACTIVE_ORG_ID: switched.org_id,
19408
+ DEEPLINE_ACTIVE_ORG_NAME: switched.org_name
19409
+ });
19410
+ const { api_key: _apiKey, ...publicSwitched } = switched;
19411
+ printCommandEnvelope(
19412
+ {
19413
+ ok: true,
19414
+ host_env_path: hostEnvFilePath(config.baseUrl),
19415
+ ...publicSwitched,
19416
+ api_key_saved: true,
19417
+ render: {
19418
+ sections: [
19419
+ {
19420
+ title: "org switch",
19421
+ lines: [
19422
+ `Switched to ${switched.org_name}.`,
19423
+ `Saved host auth in ${hostEnvFilePath(config.baseUrl)}`
19424
+ ]
19425
+ }
19426
+ ]
19427
+ }
19428
+ },
19429
+ { json: options.json }
19430
+ );
19119
19431
  }
19120
- function dedupConsecutiveEvents(raw) {
19121
- const rawLines = normalizedJsonLines(raw);
19122
- const parsedEvents = rawLines.map(parseJsonLine);
19123
- const output2 = [];
19124
- let index = 0;
19125
- while (index < parsedEvents.length) {
19126
- const event = parsedEvents[index];
19127
- if (!event || typeof event !== "object") {
19128
- output2.push(rawLines[index] ?? "");
19129
- index += 1;
19130
- continue;
19432
+ async function handleOrgCreate(name, options) {
19433
+ const config = resolveConfig();
19434
+ const http = new HttpClient(config);
19435
+ const created = await http.post("/api/v2/auth/cli/org-create", {
19436
+ api_key: config.apiKey,
19437
+ name
19438
+ });
19439
+ saveHostEnvValues(config.baseUrl, {
19440
+ DEEPLINE_API_KEY: created.api_key,
19441
+ DEEPLINE_ACTIVE_ORG_ID: created.org_id,
19442
+ DEEPLINE_ACTIVE_ORG_NAME: created.org_name
19443
+ });
19444
+ const { api_key: _apiKey, ...publicCreated } = created;
19445
+ printCommandEnvelope(
19446
+ {
19447
+ ok: true,
19448
+ ...publicCreated,
19449
+ api_key_saved: true,
19450
+ switched: true,
19451
+ host_env_path: hostEnvFilePath(config.baseUrl),
19452
+ render: {
19453
+ sections: [
19454
+ {
19455
+ title: "org create",
19456
+ lines: [
19457
+ `Created organization: ${created.org_name}.`,
19458
+ `Switched to ${created.org_name}.`,
19459
+ `Saved host auth in ${hostEnvFilePath(config.baseUrl)}`
19460
+ ]
19461
+ }
19462
+ ]
19463
+ }
19464
+ },
19465
+ { json: options.json }
19466
+ );
19467
+ }
19468
+ function registerOrgCommands(program) {
19469
+ const org = program.command("org").description("List, create, and switch organizations.").addHelpText(
19470
+ "after",
19471
+ `
19472
+ Notes:
19473
+ Organizations are workspaces. Switching organizations mutates the saved host
19474
+ auth file so later CLI commands target the selected workspace.
19475
+
19476
+ Examples:
19477
+ deepline org list --json
19478
+ deepline org create Acme --json
19479
+ deepline org switch 2
19480
+ deepline org switch --org-id org_123 --json
19481
+ `
19482
+ );
19483
+ org.command("list").description("List your organizations.").addHelpText(
19484
+ "after",
19485
+ `
19486
+ Notes:
19487
+ Read-only. Marks the active organization when the server returns that metadata.
19488
+
19489
+ Examples:
19490
+ deepline org list
19491
+ deepline org list --json
19492
+ `
19493
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgList);
19494
+ org.command("create <name>").description("Create a new organization and switch this CLI to it.").addHelpText(
19495
+ "after",
19496
+ `
19497
+ Notes:
19498
+ Mutates workspace state. The new organization is created for the current
19499
+ authenticated user, then the returned API key is saved for this host so later
19500
+ CLI commands target the new organization.
19501
+
19502
+ Examples:
19503
+ deepline org create Acme
19504
+ deepline org create "Acme Sales" --json
19505
+ `
19506
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgCreate);
19507
+ org.command("switch [selection]").description(
19508
+ "Switch to another organization and save the new API key in the host auth file."
19509
+ ).addHelpText(
19510
+ "after",
19511
+ `
19512
+ Notes:
19513
+ Mutates the saved host auth file. Selection can be a list number, exact
19514
+ organization name, or organization id. Without a selection, prints choices.
19515
+
19516
+ Examples:
19517
+ deepline org switch
19518
+ deepline org switch 2
19519
+ deepline org switch --org-id org_123 --json
19520
+ `
19521
+ ).option("--org-id <id>", "Switch using an explicit organization id").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleOrgSwitch);
19522
+ }
19523
+
19524
+ // src/cli/commands/quickstart.ts
19525
+ import { spawn, spawnSync } from "child_process";
19526
+ import { randomBytes } from "crypto";
19527
+ import { createServer } from "http";
19528
+ var EXIT_OK2 = 0;
19529
+ var EXIT_AUTH2 = 1;
19530
+ var EXIT_SERVER3 = 2;
19531
+ var MAX_PROMPT_LENGTH = 8e3;
19532
+ var MAX_BODY_BYTES = 64 * 1024;
19533
+ function shellQuote(arg) {
19534
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
19535
+ }
19536
+ function hasClaudeBinary() {
19537
+ try {
19538
+ const result = spawnSync("claude", ["--version"], {
19539
+ stdio: "ignore",
19540
+ shell: process.platform === "win32"
19541
+ });
19542
+ return result.status === 0;
19543
+ } catch {
19544
+ return false;
19545
+ }
19546
+ }
19547
+ function launchClaude(prompt) {
19548
+ return new Promise((resolve16) => {
19549
+ const child = spawn("claude", [prompt], {
19550
+ stdio: "inherit",
19551
+ shell: process.platform === "win32"
19552
+ });
19553
+ child.on("error", () => resolve16(EXIT_SERVER3));
19554
+ child.on("close", (status) => resolve16(status ?? EXIT_OK2));
19555
+ });
19556
+ }
19557
+ function readBody(req) {
19558
+ return new Promise((resolve16, reject) => {
19559
+ let raw = "";
19560
+ req.setEncoding("utf8");
19561
+ req.on("data", (chunk) => {
19562
+ raw += chunk;
19563
+ if (raw.length > MAX_BODY_BYTES) {
19564
+ reject(new Error("Request body too large."));
19565
+ req.destroy();
19566
+ }
19567
+ });
19568
+ req.on("end", () => resolve16(raw));
19569
+ req.on("error", reject);
19570
+ });
19571
+ }
19572
+ function writeJson(res, status, payload) {
19573
+ res.writeHead(status, { "content-type": "application/json" });
19574
+ res.end(JSON.stringify(payload));
19575
+ }
19576
+ function startCallbackServer(input2) {
19577
+ const server = createServer((req, res) => {
19578
+ res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*");
19579
+ res.setHeader("Vary", "Origin");
19580
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
19581
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
19582
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
19583
+ res.setHeader("Access-Control-Max-Age", "600");
19584
+ if (req.method === "OPTIONS") {
19585
+ res.writeHead(204);
19586
+ res.end();
19587
+ return;
19131
19588
  }
19132
- const record = event;
19133
- const eventType = String(record.type ?? "");
19134
- const eventKey = messageContentKey(record);
19135
- if (!["user", "assistant"].includes(eventType) || !eventKey) {
19136
- output2.push(rawLines[index] ?? "");
19137
- index += 1;
19138
- continue;
19589
+ if (req.method !== "POST" || req.url !== "/submit") {
19590
+ writeJson(res, 404, { error: "Not found." });
19591
+ return;
19139
19592
  }
19140
- let runCount = 1;
19141
- let cursor = index + 1;
19142
- while (cursor < parsedEvents.length) {
19143
- const next = parsedEvents[cursor];
19144
- if (!next || typeof next !== "object") break;
19145
- const nextRecord = next;
19146
- if (String(nextRecord.type ?? "") !== eventType || messageContentKey(nextRecord) !== eventKey) {
19147
- break;
19593
+ void readBody(req).then((raw) => {
19594
+ let body = null;
19595
+ try {
19596
+ body = JSON.parse(raw);
19597
+ } catch {
19598
+ body = null;
19599
+ }
19600
+ const state = typeof body?.state === "string" ? body.state : "";
19601
+ const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
19602
+ const workflowId = typeof body?.workflow_id === "string" && body.workflow_id.trim() ? body.workflow_id.trim() : null;
19603
+ if (!state || state !== input2.state) {
19604
+ writeJson(res, 403, { error: "Invalid quickstart state token." });
19605
+ return;
19606
+ }
19607
+ if (!prompt) {
19608
+ writeJson(res, 400, { error: "prompt is required." });
19609
+ return;
19610
+ }
19611
+ if (prompt.length > MAX_PROMPT_LENGTH) {
19612
+ writeJson(res, 400, {
19613
+ error: `prompt exceeds ${MAX_PROMPT_LENGTH} characters.`
19614
+ });
19615
+ return;
19616
+ }
19617
+ writeJson(res, 200, { ok: true });
19618
+ setImmediate(() => input2.onSelection({ prompt, workflowId }));
19619
+ }).catch(() => {
19620
+ writeJson(res, 400, { error: "Invalid request body." });
19621
+ });
19622
+ });
19623
+ return new Promise((resolve16, reject) => {
19624
+ server.once("error", reject);
19625
+ server.listen(0, "127.0.0.1", () => {
19626
+ const address = server.address();
19627
+ if (!address || typeof address === "string") {
19628
+ reject(new Error("Failed to bind quickstart callback server."));
19629
+ return;
19148
19630
  }
19149
- runCount += 1;
19150
- cursor += 1;
19631
+ resolve16({ server, port: address.port });
19632
+ });
19633
+ });
19634
+ }
19635
+ async function handleQuickstart(options) {
19636
+ const jsonOutput = shouldEmitJson(options.json === true);
19637
+ const baseUrl = autoDetectBaseUrl().replace(/\/$/, "");
19638
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
19639
+ let apiKey = resolveApiKeyForBaseUrl(baseUrl);
19640
+ if (!apiKey) {
19641
+ if (interactive) {
19642
+ console.error("Not connected yet \u2014 registering this device first.");
19643
+ const registerExit = await handleRegister([]);
19644
+ if (registerExit !== EXIT_OK2) return registerExit;
19645
+ apiKey = resolveApiKeyForBaseUrl(baseUrl);
19151
19646
  }
19152
- if (runCount > 1) {
19153
- record._repeat_count = runCount;
19154
- record._repeat_summary = `${runCount} consecutive identical ${eventType} messages collapsed`;
19155
- output2.push(JSON.stringify(record));
19156
- index = cursor;
19157
- continue;
19647
+ if (!apiKey) {
19648
+ console.error("Not connected. Run: deepline auth register");
19649
+ return EXIT_AUTH2;
19158
19650
  }
19159
- output2.push(rawLines[index] ?? "");
19160
- index += 1;
19161
19651
  }
19162
- return Buffer.from(output2.length > 0 ? `${output2.join("\n")}
19163
- ` : "", "utf8");
19164
- }
19165
- function compactEventValue(value) {
19166
- if (typeof value === "string") {
19167
- if (value.length <= MAX_EVENT_STRING_CHARS) return value;
19168
- return `${value.slice(0, MAX_EVENT_STRING_CHARS - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`;
19652
+ const state = randomBytes(32).toString("hex");
19653
+ let resolveSelection;
19654
+ const selectionPromise = new Promise((resolve16) => {
19655
+ resolveSelection = resolve16;
19656
+ });
19657
+ let callback;
19658
+ try {
19659
+ callback = await startCallbackServer({
19660
+ state,
19661
+ onSelection: (selection) => resolveSelection(selection)
19662
+ });
19663
+ } catch (error) {
19664
+ console.error(
19665
+ `Could not start the local quickstart listener: ${error instanceof Error ? error.message : String(error)}`
19666
+ );
19667
+ return EXIT_SERVER3;
19169
19668
  }
19170
- if (Array.isArray(value)) {
19171
- const compacted = value.slice(0, MAX_EVENT_LIST_ITEMS).map(compactEventValue);
19172
- if (value.length > MAX_EVENT_LIST_ITEMS) {
19173
- compacted.push(
19174
- `${TRUNCATION_MARKER} ${value.length - MAX_EVENT_LIST_ITEMS} more item(s)`
19175
- );
19176
- }
19177
- return compacted;
19669
+ const quickstartUrl = `${baseUrl}/cli/quickstart?cb=${callback.port}&state=${state}`;
19670
+ console.error(" Opening the recipe picker in your browser.");
19671
+ console.error(` If it didn't open, cmd+click: ${quickstartUrl}`);
19672
+ if (options.open !== false) {
19673
+ openInBrowser(quickstartUrl);
19178
19674
  }
19179
- if (value && typeof value === "object") {
19180
- const entries = Object.entries(value);
19181
- const compacted = {};
19182
- for (const [key, item] of entries.slice(0, MAX_EVENT_OBJECT_KEYS)) {
19183
- compacted[key] = compactEventValue(item);
19184
- }
19185
- if (entries.length > MAX_EVENT_OBJECT_KEYS) {
19186
- compacted._truncated_keys = entries.length - MAX_EVENT_OBJECT_KEYS;
19187
- }
19188
- return compacted;
19675
+ const timeoutSeconds = Number.parseInt(options.timeout ?? "300", 10);
19676
+ const timeoutMs = (Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds : 300) * 1e3;
19677
+ const timeoutHandle = setTimeout(
19678
+ () => resolveSelection("timeout"),
19679
+ timeoutMs
19680
+ );
19681
+ const progress = createCliProgress(!jsonOutput);
19682
+ progress.phase("waiting for your pick in the browser (Ctrl+C to skip)");
19683
+ const onSigint = () => resolveSelection("sigint");
19684
+ process.once("SIGINT", onSigint);
19685
+ const outcome = await selectionPromise;
19686
+ clearTimeout(timeoutHandle);
19687
+ process.removeListener("SIGINT", onSigint);
19688
+ callback.server.close();
19689
+ if (outcome === "sigint") {
19690
+ progress.fail();
19691
+ console.error("\nSkipped \u2014 run `deepline quickstart` anytime.");
19692
+ return EXIT_OK2;
19189
19693
  }
19190
- return value;
19694
+ if (outcome === "timeout") {
19695
+ progress.fail();
19696
+ console.error(
19697
+ "Timed out waiting for a selection. Run: deepline quickstart"
19698
+ );
19699
+ return EXIT_AUTH2;
19700
+ }
19701
+ progress.complete();
19702
+ const { prompt, workflowId } = outcome;
19703
+ const claudeCommand = `claude ${shellQuote(prompt)}`;
19704
+ const claudeAvailable = hasClaudeBinary();
19705
+ const willLaunch = options.launch !== false && !jsonOutput && interactive && claudeAvailable;
19706
+ printCommandEnvelope(
19707
+ {
19708
+ status: "submitted",
19709
+ workflowId,
19710
+ prompt,
19711
+ claudeCommand,
19712
+ url: quickstartUrl,
19713
+ launched: willLaunch,
19714
+ render: {
19715
+ sections: [
19716
+ {
19717
+ title: "quickstart",
19718
+ lines: [
19719
+ ...workflowId ? [`Recipe: ${workflowId}`] : [],
19720
+ willLaunch ? "Launching Claude Code with your recipe..." : claudeAvailable ? "Run the command below to start your recipe." : "Claude Code not found. Install it (https://code.claude.com/docs/en/overview), then run the command below."
19721
+ ]
19722
+ }
19723
+ ],
19724
+ actions: [{ label: "Run", command: claudeCommand }]
19725
+ }
19726
+ },
19727
+ { json: jsonOutput }
19728
+ );
19729
+ if (willLaunch) {
19730
+ return launchClaude(prompt);
19731
+ }
19732
+ return EXIT_OK2;
19191
19733
  }
19192
- function selectiveCompactToolResults(raw) {
19193
- const lines = [];
19194
- for (const line of normalizedJsonLines(raw)) {
19195
- const parsed = parseJsonLine(line);
19196
- if (!parsed || typeof parsed !== "object") {
19197
- lines.push(line);
19198
- continue;
19199
- }
19200
- const record = parsed;
19201
- if (record.type === "user") {
19202
- const message = record.message;
19203
- const content = message && typeof message === "object" ? message.content : null;
19204
- if (Array.isArray(content)) {
19205
- message.content = content.map(
19206
- (block) => block && typeof block === "object" && block.type === "tool_result" ? compactEventValue(block) : block
19207
- );
19734
+ function registerQuickstartCommands(program) {
19735
+ program.command("quickstart").description(
19736
+ "Pick a starter recipe in the browser and launch it with Claude Code."
19737
+ ).addHelpText(
19738
+ "after",
19739
+ `
19740
+ Notes:
19741
+ Opens the hosted recipe picker in your browser. The picker sends your
19742
+ selection back to a temporary listener on 127.0.0.1, so the browser must run
19743
+ on the same machine as the CLI. Once a recipe arrives, the CLI prints the
19744
+ matching claude command and, when Claude Code is installed and the shell is
19745
+ interactive, launches it directly. Press Ctrl+C while waiting to skip.
19746
+
19747
+ Examples:
19748
+ deepline quickstart
19749
+ deepline quickstart --no-open
19750
+ deepline quickstart --no-launch --json
19751
+ deepline quickstart --timeout 120
19752
+ `
19753
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").option("--no-open", "Do not open the browser; print the URL only").option("--no-launch", "Print the claude command instead of launching it").option(
19754
+ "--timeout <seconds>",
19755
+ "Maximum seconds to wait for a selection",
19756
+ "300"
19757
+ ).action(async (options) => {
19758
+ process.exitCode = await handleQuickstart(options);
19759
+ });
19760
+ }
19761
+
19762
+ // src/cli/commands/secrets.ts
19763
+ import { stdin as input, stdout as output } from "process";
19764
+ var hiddenInputBuffer = "";
19765
+ function normalizeSecretName(value) {
19766
+ const normalized = value.trim().toUpperCase();
19767
+ if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(normalized)) {
19768
+ throw new Error(
19769
+ "Secret names must be 2-64 characters and use uppercase letters, numbers, and underscores."
19770
+ );
19771
+ }
19772
+ return normalized;
19773
+ }
19774
+ function renderSecret(secret) {
19775
+ const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
19776
+ return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
19777
+ }
19778
+ async function readHiddenLine(prompt) {
19779
+ if (!input.isTTY || !output.isTTY) {
19780
+ throw new Error(
19781
+ "Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
19782
+ );
19783
+ }
19784
+ output.write(prompt);
19785
+ const previousRawMode = input.isRaw;
19786
+ if (typeof input.setRawMode === "function") input.setRawMode(true);
19787
+ let value = "";
19788
+ input.resume();
19789
+ return await new Promise((resolve16, reject) => {
19790
+ let settled = false;
19791
+ const cleanup = () => {
19792
+ input.off("data", onData);
19793
+ input.off("end", onEnd);
19794
+ input.off("error", onError);
19795
+ if (typeof input.setRawMode === "function") {
19796
+ input.setRawMode(previousRawMode);
19797
+ }
19798
+ };
19799
+ const finish = (line) => {
19800
+ if (settled) return;
19801
+ settled = true;
19802
+ output.write("\n");
19803
+ cleanup();
19804
+ resolve16(line);
19805
+ };
19806
+ const fail = (error) => {
19807
+ if (settled) return;
19808
+ settled = true;
19809
+ output.write("\n");
19810
+ cleanup();
19811
+ reject(error);
19812
+ };
19813
+ const processText = (text) => {
19814
+ for (let index = 0; index < text.length; index++) {
19815
+ const char = text[index];
19816
+ const code = char.charCodeAt(0);
19817
+ if (char === "\r" || char === "\n") {
19818
+ hiddenInputBuffer = text.slice(index + 1);
19819
+ finish(value);
19820
+ return;
19821
+ }
19822
+ if (code === 3) {
19823
+ fail(new Error("Secret input cancelled."));
19824
+ return;
19825
+ }
19826
+ if (code === 8 || code === 127) {
19827
+ value = value.slice(0, -1);
19828
+ continue;
19829
+ }
19830
+ if (code >= 32) {
19831
+ value += char;
19832
+ }
19208
19833
  }
19834
+ hiddenInputBuffer = "";
19835
+ };
19836
+ const onData = (chunk) => {
19837
+ processText(chunk.toString());
19838
+ };
19839
+ const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
19840
+ const onError = (error) => fail(error);
19841
+ input.on("data", onData);
19842
+ input.once("end", onEnd);
19843
+ input.once("error", onError);
19844
+ if (hiddenInputBuffer) {
19845
+ const buffered = hiddenInputBuffer;
19846
+ hiddenInputBuffer = "";
19847
+ processText(buffered);
19209
19848
  }
19210
- lines.push(JSON.stringify(record));
19211
- }
19212
- return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
19213
- ` : "", "utf8");
19214
- }
19215
- function prepareSessionBuffer(raw) {
19216
- return selectiveCompactToolResults(
19217
- dedupConsecutiveEvents(stripNoiseEvents(raw))
19218
- );
19849
+ });
19219
19850
  }
19220
- function buildSessionUploadContent(raw) {
19221
- const prepared = prepareSessionBuffer(raw);
19222
- const encoded = gzipSync(prepared).toString("base64");
19223
- if (encoded.length <= MAX_SESSION_UPLOAD_BYTES && prepared.length <= MAX_DIRECT_SESSION_DECODED_BYTES) {
19224
- return { encodedContent: encoded, needsChunking: false };
19851
+ async function readSecretValue() {
19852
+ const first = await readHiddenLine("Secret value: ");
19853
+ if (!first) throw new Error("Secret value is required.");
19854
+ const second = await readHiddenLine("Confirm secret value: ");
19855
+ if (first !== second) {
19856
+ throw new Error("Secret values did not match.");
19225
19857
  }
19226
- return { encodedContent: encoded, needsChunking: true };
19227
- }
19228
- async function uploadPayload(path, payload) {
19229
- const { http } = getAuthedHttpClient();
19230
- return await http.post(path, payload);
19858
+ return first;
19231
19859
  }
19232
- async function uploadChunkedSessions(sessions, options) {
19233
- const uploadId = randomUUID();
19234
- for (const session of sessions) {
19235
- const bytes = Buffer.from(session.encodedContent, "base64");
19236
- const chunks = [];
19237
- for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE_BYTES) {
19238
- chunks.push(bytes.subarray(offset, offset + CHUNK_SIZE_BYTES));
19239
- }
19240
- process.stderr.write(
19241
- `Uploading ${session.label} in ${chunks.length} chunk(s)...
19242
- `
19860
+ function preventShellHistoryLeak(forbidden) {
19861
+ if (forbidden.length > 0) {
19862
+ throw new Error(
19863
+ "Do not pass secret values as command arguments. Run `deepline secrets set NAME` and enter the value at the hidden prompt."
19243
19864
  );
19244
- for (const [index, chunk] of chunks.entries()) {
19245
- await uploadPayload("/api/v2/cli/send-session/chunk", {
19246
- upload_id: uploadId,
19247
- session_id: session.sessionId,
19248
- index,
19249
- total_chunks: chunks.length,
19250
- data: chunk.toString("base64")
19251
- });
19252
- }
19253
19865
  }
19254
- const response = await uploadPayload("/api/v2/cli/send-session/finalize", {
19255
- upload_id: uploadId,
19256
- session_ids: sessions.map((session) => session.sessionId),
19257
- labels: sessions.map((session) => session.label),
19258
- environments: sessions.map(() => detectShellContext())
19259
- });
19866
+ }
19867
+ async function handleList(options) {
19868
+ const client2 = new DeeplineClient();
19869
+ const secrets = await client2.listSecrets();
19260
19870
  printCommandEnvelope(
19261
19871
  {
19262
- ...response,
19263
- ok: true,
19264
- uploaded: sessions.length,
19872
+ secrets,
19873
+ count: secrets.length,
19265
19874
  render: {
19266
19875
  sections: [
19267
19876
  {
19268
- title: "sessions send",
19269
- lines: ["Session uploaded to #internal-reports (chunked)."]
19877
+ title: "secrets",
19878
+ lines: secrets.length ? secrets.map(renderSecret) : ["No play secrets are configured."]
19270
19879
  }
19271
19880
  ]
19272
19881
  }
@@ -19274,247 +19883,266 @@ async function uploadChunkedSessions(sessions, options) {
19274
19883
  { json: options.json }
19275
19884
  );
19276
19885
  }
19277
- async function handleSessionsSend(options) {
19278
- if (options.file) {
19279
- const filePath = resolve12(options.file);
19280
- if (!existsSync8(filePath)) {
19281
- throw new Error(`File not found: ${options.file}`);
19282
- }
19283
- const response2 = await uploadPayload("/api/v2/cli/send-session", {
19284
- file: readFileSync7(filePath).toString("base64"),
19285
- filename: basename4(filePath)
19286
- });
19287
- printCommandEnvelope(
19288
- {
19289
- ...response2,
19290
- ok: true,
19291
- filename: basename4(filePath),
19292
- render: {
19293
- sections: [
19294
- {
19295
- title: "sessions send",
19296
- lines: [
19297
- `File '${basename4(filePath)}' uploaded to #internal-reports.`
19298
- ]
19299
- }
19300
- ]
19301
- }
19302
- },
19303
- { json: options.json }
19304
- );
19305
- return;
19306
- }
19307
- const targets = resolveSessionTargets({
19308
- sessionIds: options.sessionId,
19309
- labels: options.label,
19310
- currentSession: options.currentSession
19311
- });
19312
- const built = targets.map((target) => {
19313
- const upload = buildSessionUploadContent(readFileSync7(target.filePath));
19314
- return { ...target, ...upload };
19315
- });
19316
- if (built.some((session) => session.needsChunking)) {
19317
- await uploadChunkedSessions(built, options);
19318
- return;
19319
- }
19320
- const response = built.length === 1 && !options.label?.length ? await uploadPayload("/api/v2/cli/send-session", {
19321
- session_id: built[0]?.sessionId,
19322
- content: built[0]?.encodedContent,
19323
- environment: detectShellContext()
19324
- }) : await uploadPayload("/api/v2/cli/send-session", {
19325
- sessions: built.map((session) => ({
19326
- session_id: session.sessionId,
19327
- content: session.encodedContent,
19328
- label: session.label,
19329
- environment: detectShellContext()
19330
- })),
19331
- environment: detectShellContext()
19332
- });
19886
+ async function handleCheck(nameInput, options) {
19887
+ const name = normalizeSecretName(nameInput);
19888
+ const client2 = new DeeplineClient();
19889
+ const secret = await client2.checkSecret(name);
19333
19890
  printCommandEnvelope(
19334
19891
  {
19335
- ...response,
19336
- ok: true,
19337
- uploaded: built.length,
19892
+ ok: Boolean(secret),
19893
+ name,
19894
+ secret: secret ?? null,
19338
19895
  render: {
19339
19896
  sections: [
19340
19897
  {
19341
- title: "sessions send",
19342
- lines: ["Session uploaded to #internal-reports."]
19898
+ title: "secret check",
19899
+ lines: [
19900
+ secret ? `${name}: active` : `${name}: missing, disabled, or empty`
19901
+ ]
19343
19902
  }
19344
19903
  ]
19345
19904
  }
19346
19905
  },
19347
19906
  { json: options.json }
19348
19907
  );
19908
+ if (!secret) process.exitCode = 4;
19349
19909
  }
19350
- function fallbackViewerAssets() {
19351
- return {
19352
- css: [
19353
- "body{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin:0;padding:16px;background:#fafafa;color:#111}",
19354
- ".section{background:#fff;border:1px solid #ddd;border-radius:8px;padding:12px;margin-bottom:12px}",
19355
- ".section h2{margin:0 0 8px 0;font-size:14px}",
19356
- "pre{margin:0;white-space:pre-wrap;word-break:break-word;background:#f6f8fa;border:1px solid #e3e5e8;border-radius:6px;padding:10px}"
19357
- ].join(""),
19358
- js: [
19359
- "(() => {",
19360
- 'const root=document.getElementById("main-content");',
19361
- 'const raw=document.getElementById("raw-sessions");',
19362
- "if(!root||!raw)return;",
19363
- 'let sessions=[];try{sessions=JSON.parse(raw.textContent||"[]")}catch{}',
19364
- 'root.innerHTML="";',
19365
- "for(const session of sessions){",
19366
- 'const section=document.createElement("section");section.className="section";',
19367
- 'const title=document.createElement("h2");title.textContent=String(session.label||"session");',
19368
- 'const pre=document.createElement("pre");',
19369
- 'pre.textContent=(Array.isArray(session.events)?session.events:[]).map((event)=>JSON.stringify(event)).join("\\n");',
19370
- "section.append(title,pre);root.appendChild(section);",
19371
- "}",
19372
- "})();"
19373
- ].join("")
19374
- };
19375
- }
19376
- function parsePreparedEvents(buffer) {
19377
- return normalizedJsonLines(buffer).map((line) => {
19378
- const parsed = parseJsonLine(line);
19379
- return parsed ?? line;
19380
- });
19381
- }
19382
- async function handleSessionsRender(options) {
19383
- const targets = resolveSessionTargets({
19384
- sessionIds: options.sessionId,
19385
- labels: options.label,
19386
- currentSession: options.currentSession
19387
- });
19388
- let outputPath = options.output ? resolve12(options.output) : "";
19389
- if (!outputPath) {
19390
- const outputDir = join9(process.cwd(), "deepline", "data");
19391
- mkdirSync4(outputDir, { recursive: true });
19392
- outputPath = join9(
19393
- outputDir,
19394
- targets.length > 1 ? "session-viewer.html" : `session-${targets[0]?.sessionId}.html`
19395
- );
19396
- } else {
19397
- mkdirSync4(dirname9(outputPath), { recursive: true });
19910
+ async function handleSet(nameInput, forbidden, options) {
19911
+ preventShellHistoryLeak(forbidden);
19912
+ const name = normalizeSecretName(nameInput);
19913
+ const scope = options.scope === "play" ? "play" : "org";
19914
+ const playName = options.play?.trim();
19915
+ if (scope === "play" && !playName) {
19916
+ throw new Error("--play <name> is required when --scope play is used.");
19398
19917
  }
19399
- const sessions = targets.map((target) => ({
19400
- label: target.label,
19401
- events: parsePreparedEvents(
19402
- prepareSessionBuffer(readFileSync7(target.filePath))
19403
- )
19404
- }));
19405
- const { css, js } = fallbackViewerAssets();
19406
- const refreshMeta = options.autoRefresh ? `<meta http-equiv="refresh" content="${Number.parseInt(options.autoRefresh, 10)}">` : "";
19407
- const rawJson = JSON.stringify(sessions).replace(/<\//g, "<\\/");
19408
- const html = `<!DOCTYPE html>
19409
- <html lang="en">
19410
- <head>
19411
- <meta charset="UTF-8">
19412
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
19413
- ${refreshMeta}
19414
- <title>Session Viewer</title>
19415
- <style>${css}</style>
19416
- </head>
19417
- <body>
19418
- <div class="layout">
19419
- <div class="main" id="main-content"></div>
19420
- </div>
19421
- <script type="application/json" id="raw-sessions">${rawJson}</script>
19422
- <script>${js}</script>
19423
- </body>
19424
- </html>`;
19425
- writeFileSync8(outputPath, html, "utf8");
19918
+ const value = await readSecretValue();
19919
+ const { http } = getAuthedHttpClient();
19920
+ const response = await http.post(
19921
+ "/api/v2/secrets",
19922
+ {
19923
+ name,
19924
+ value,
19925
+ scope,
19926
+ ...playName ? { playName } : {}
19927
+ }
19928
+ );
19929
+ const secret = response.secret;
19426
19930
  printCommandEnvelope(
19427
19931
  {
19428
19932
  ok: true,
19429
- file: outputPath,
19430
- session_count: targets.length,
19933
+ secret,
19431
19934
  render: {
19432
19935
  sections: [
19433
19936
  {
19434
- title: "sessions render",
19435
- lines: [`Rendered session viewer: ${outputPath}`]
19937
+ title: "secret saved",
19938
+ lines: [`${secret.name}: saved (${secret.scope})`]
19436
19939
  }
19437
19940
  ]
19438
19941
  }
19439
- },
19440
- { json: options.json }
19441
- );
19442
- }
19443
- function collectOption(value, previous) {
19444
- previous.push(value);
19445
- return previous;
19942
+ },
19943
+ { json: options.json }
19944
+ );
19446
19945
  }
19447
- function registerSessionsCommands(program) {
19448
- const sessions = program.command("sessions").description("Upload and render local agent session transcripts.").addHelpText(
19946
+ function registerSecretsCommands(program) {
19947
+ const secrets = program.command("secrets").description("Manage play secrets without revealing values.").addHelpText(
19449
19948
  "after",
19450
19949
  `
19451
19950
  Notes:
19452
- Session commands operate on local Claude session JSONL files under
19453
- ~/.claude/projects. send uploads a compacted transcript or file to Deepline.
19454
- render writes a local HTML viewer.
19951
+ Secret values are never accepted as command arguments, stdin pipes, env vars,
19952
+ or files. Use deepline secrets set NAME and type the value at the hidden TTY
19953
+ prompt. Agents can list/check metadata but should not enter secret values.
19455
19954
 
19456
19955
  Examples:
19457
- deepline sessions send --current-session --json
19458
- deepline sessions send --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca
19459
- deepline sessions render --current-session --output session.html
19956
+ deepline secrets list
19957
+ deepline secrets check HUBSPOT_TOKEN
19958
+ deepline secrets set HUBSPOT_TOKEN
19460
19959
  `
19461
19960
  );
19462
- sessions.command("send").description("Upload session transcript(s) or a local file to Deepline.").addHelpText(
19463
- "after",
19464
- `
19465
- Examples:
19466
- deepline sessions send --current-session --json
19467
- deepline sessions send --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca --label "pilot run"
19468
- deepline sessions send --file ./debug.log --json
19469
- `
19470
- ).option(
19471
- "--session-id <uuid>",
19472
- "Claude session UUID. Repeat for multiple sessions.",
19473
- collectOption,
19474
- []
19475
- ).option(
19476
- "--label <label>",
19477
- "Label for the preceding session id",
19478
- collectOption,
19479
- []
19480
- ).option("--current-session", "Use the newest local Claude session JSONL").option("--file <path>", "Upload a raw local file instead of a session").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleSessionsSend);
19481
- sessions.command("render").description("Render local session transcript(s) to an HTML viewer.").addHelpText(
19961
+ secrets.command("list").description("List secret metadata only.").option("--json", "Emit JSON output").action(async (options) => {
19962
+ await handleList(options);
19963
+ });
19964
+ secrets.command("check").description("Check whether a secret exists and is active.").argument("<name>", "Secret name").option("--json", "Emit JSON output").action(async (name, options) => {
19965
+ await handleCheck(name, options);
19966
+ });
19967
+ secrets.command("set").description("Set or rotate a secret through a hidden interactive prompt.").argument("<name>", "Secret name").argument("[forbidden...]", "Do not pass secret values here").option("--json", "Emit JSON output").option("--scope <scope>", "Secret scope: org or play", "org").option("--play <name>", "Play name for play-scoped secrets").action(
19968
+ async (name, forbidden, options) => {
19969
+ await handleSet(name, forbidden ?? [], options);
19970
+ }
19971
+ );
19972
+ }
19973
+
19974
+ // src/cli/commands/switch.ts
19975
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
19976
+ import { homedir as homedir6 } from "os";
19977
+ import { dirname as dirname10, join as join10 } from "path";
19978
+ function hostSlugFromBaseUrl(baseUrl) {
19979
+ try {
19980
+ const url = new URL(baseUrl);
19981
+ const port = url.port ? Number.parseInt(url.port, 10) : null;
19982
+ let slug = (url.hostname || "unknown").replace(/[^a-zA-Z0-9]/g, "-");
19983
+ if (port && port !== 80 && port !== 443) {
19984
+ slug = `${slug}-${port}`;
19985
+ }
19986
+ return slug.toLowerCase().replace(/^-+|-+$/g, "") || "unknown";
19987
+ } catch {
19988
+ return "unknown";
19989
+ }
19990
+ }
19991
+ function resolveConfigScope() {
19992
+ const explicit = (process.env.DEEPLINE_CONFIG_SCOPE || "").trim();
19993
+ if (explicit) return explicit;
19994
+ return hostSlugFromBaseUrl(autoDetectBaseUrl());
19995
+ }
19996
+ function activeFamilyPath() {
19997
+ const home = process.env.HOME || process.env.USERPROFILE || homedir6();
19998
+ return join10(
19999
+ home,
20000
+ ".local",
20001
+ "deepline",
20002
+ resolveConfigScope(),
20003
+ "cli",
20004
+ ".active-family"
20005
+ );
20006
+ }
20007
+ function readActiveFamily() {
20008
+ const path = activeFamilyPath();
20009
+ try {
20010
+ return readFileSync8(path, "utf-8").trim() || "sdk";
20011
+ } catch {
20012
+ return "sdk";
20013
+ }
20014
+ }
20015
+ function writeActiveFamily(family) {
20016
+ const path = activeFamilyPath();
20017
+ mkdirSync5(dirname10(path), { recursive: true });
20018
+ writeFileSync9(path, `${family}
20019
+ `, "utf-8");
20020
+ return path;
20021
+ }
20022
+ function handleSwitch(action, options) {
20023
+ const normalized = (action || "status").trim().toLowerCase();
20024
+ if (normalized === "status") {
20025
+ const path = activeFamilyPath();
20026
+ const activeFamily = readActiveFamily();
20027
+ printCommandEnvelope(
20028
+ {
20029
+ ok: true,
20030
+ active_family: activeFamily,
20031
+ active_family_path: path,
20032
+ active_family_file_exists: existsSync9(path),
20033
+ render: {
20034
+ sections: [
20035
+ {
20036
+ title: "cli switch",
20037
+ lines: [
20038
+ `Active CLI family: ${activeFamily}`,
20039
+ `Active family file: ${path}`
20040
+ ]
20041
+ }
20042
+ ]
20043
+ }
20044
+ },
20045
+ { json: options.json }
20046
+ );
20047
+ return 0;
20048
+ }
20049
+ if (normalized === "python" || normalized === "rollback") {
20050
+ const path = writeActiveFamily("python");
20051
+ printCommandEnvelope(
20052
+ {
20053
+ ok: true,
20054
+ active_family: "python",
20055
+ active_family_path: path,
20056
+ render: {
20057
+ sections: [
20058
+ {
20059
+ title: "cli switch",
20060
+ lines: [
20061
+ "Switched installer-managed `deepline` to the Python CLI."
20062
+ ]
20063
+ }
20064
+ ]
20065
+ }
20066
+ },
20067
+ { json: options.json }
20068
+ );
20069
+ return 0;
20070
+ }
20071
+ if (normalized === "sdk") {
20072
+ const path = writeActiveFamily("sdk");
20073
+ printCommandEnvelope(
20074
+ {
20075
+ ok: true,
20076
+ active_family: "sdk",
20077
+ active_family_path: path,
20078
+ render: {
20079
+ sections: [
20080
+ {
20081
+ title: "cli switch",
20082
+ lines: ["Switched installer-managed `deepline` to the SDK CLI."]
20083
+ }
20084
+ ]
20085
+ }
20086
+ },
20087
+ { json: options.json }
20088
+ );
20089
+ return 0;
20090
+ }
20091
+ const message = `Unknown switch target: ${action}. Use one of: status, sdk, python, rollback.`;
20092
+ const envelope = {
20093
+ ok: false,
20094
+ error: message,
20095
+ code: "usage_error",
20096
+ render: {
20097
+ sections: [{ title: "cli switch", lines: [message] }]
20098
+ }
20099
+ };
20100
+ const wantsJson = options.json === true;
20101
+ if (wantsJson) {
20102
+ printCommandEnvelope(envelope, { json: true });
20103
+ } else {
20104
+ process.stderr.write(`${message}
20105
+ `);
20106
+ }
20107
+ return 2;
20108
+ }
20109
+ function registerSwitchCommands(program) {
20110
+ program.command("switch [target]").description(
20111
+ "Switch the installer-managed Deepline CLI between SDK and Python families."
20112
+ ).option("--json", "Emit JSON output").addHelpText(
19482
20113
  "after",
19483
20114
  `
20115
+ Notes:
20116
+ This command changes only the local installer-managed wrapper state. It does
20117
+ not re-authenticate, reinstall packages, or contact Deepline servers.
20118
+
19484
20119
  Examples:
19485
- deepline sessions render --current-session
19486
- deepline sessions render --session-id 5a3bfb97-a2d9-49d9-82c6-52ccc03dadca --output session.html
19487
- deepline sessions render --current-session --auto-refresh 5 --json
20120
+ deepline switch status
20121
+ deepline switch python
20122
+ deepline switch rollback
20123
+ deepline switch sdk
19488
20124
  `
19489
- ).option(
19490
- "--session-id <uuid>",
19491
- "Claude session UUID. Repeat for multiple sessions.",
19492
- collectOption,
19493
- []
19494
- ).option(
19495
- "--label <label>",
19496
- "Label for the preceding session id",
19497
- collectOption,
19498
- []
19499
- ).option("--current-session", "Use the newest local Claude session JSONL").option("--auto-refresh <seconds>", "Add a browser auto-refresh interval").option("-o, --output <path>", "Output HTML path").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleSessionsRender);
20125
+ ).action((target, options) => {
20126
+ process.exitCode = handleSwitch(target, options);
20127
+ });
19500
20128
  }
19501
20129
 
19502
20130
  // src/cli/commands/tools.ts
19503
20131
  import { Option } from "commander";
19504
20132
  import {
19505
20133
  chmodSync,
19506
- existsSync as existsSync9,
20134
+ existsSync as existsSync10,
19507
20135
  mkdtempSync,
19508
- readFileSync as readFileSync8,
19509
- writeFileSync as writeFileSync10
20136
+ readFileSync as readFileSync9,
20137
+ writeFileSync as writeFileSync11
19510
20138
  } from "fs";
19511
20139
  import { tmpdir as tmpdir4 } from "os";
19512
- import { join as join11, resolve as resolve13 } from "path";
20140
+ import { join as join12, resolve as resolve13 } from "path";
19513
20141
 
19514
20142
  // src/tool-output.ts
19515
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync9 } from "fs";
19516
- import { homedir as homedir6 } from "os";
19517
- import { join as join10 } from "path";
20143
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "fs";
20144
+ import { homedir as homedir7 } from "os";
20145
+ import { join as join11 } from "path";
19518
20146
  function isPlainObject(value) {
19519
20147
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19520
20148
  }
@@ -19610,19 +20238,19 @@ function tryConvertToList(payload, options) {
19610
20238
  return null;
19611
20239
  }
19612
20240
  function ensureOutputDir() {
19613
- const outputDir = join10(homedir6(), ".local", "share", "deepline", "data");
19614
- mkdirSync5(outputDir, { recursive: true });
20241
+ const outputDir = join11(homedir7(), ".local", "share", "deepline", "data");
20242
+ mkdirSync6(outputDir, { recursive: true });
19615
20243
  return outputDir;
19616
20244
  }
19617
20245
  function writeJsonOutputFile(payload, stem) {
19618
20246
  const outputDir = ensureOutputDir();
19619
- const outputPath = join10(outputDir, `${stem}_${Date.now()}.json`);
19620
- writeFileSync9(outputPath, JSON.stringify(payload, null, 2), "utf-8");
20247
+ const outputPath = join11(outputDir, `${stem}_${Date.now()}.json`);
20248
+ writeFileSync10(outputPath, JSON.stringify(payload, null, 2), "utf-8");
19621
20249
  return outputPath;
19622
20250
  }
19623
20251
  function writeCsvOutputFile(rows, stem) {
19624
20252
  const outputDir = ensureOutputDir();
19625
- const outputPath = join10(outputDir, `${stem}_${Date.now()}.csv`);
20253
+ const outputPath = join11(outputDir, `${stem}_${Date.now()}.csv`);
19626
20254
  const seen = /* @__PURE__ */ new Set();
19627
20255
  const columns = [];
19628
20256
  for (const row of rows) {
@@ -19645,7 +20273,7 @@ function writeCsvOutputFile(rows, stem) {
19645
20273
  for (const row of rows) {
19646
20274
  lines.push(columns.map((column) => escapeCell(row[column])).join(","));
19647
20275
  }
19648
- writeFileSync9(outputPath, `${lines.join("\n")}
20276
+ writeFileSync10(outputPath, `${lines.join("\n")}
19649
20277
  `, "utf-8");
19650
20278
  const previewRows = rows.slice(0, 5);
19651
20279
  const previewColumns = columns.slice(0, 5);
@@ -19684,13 +20312,12 @@ var TOOL_COMMAND_TEMPLATES = {
19684
20312
  };
19685
20313
  function toListedTool(tool) {
19686
20314
  if (isPlayLikeTool(tool)) {
19687
- const playReference = playReferenceForTool(tool);
19688
20315
  return {
19689
20316
  ...tool,
19690
20317
  description: listedToolDescription(tool),
19691
20318
  id: tool.toolId,
19692
20319
  type: "play",
19693
- executeCommand: `deepline plays run ${playReference} --input '{...}' --watch`
20320
+ executeCommand: playRunCommandForTool(tool, tool.toolId)
19694
20321
  };
19695
20322
  }
19696
20323
  return {
@@ -19904,17 +20531,32 @@ function isPlayLikeTool(tool) {
19904
20531
  }
19905
20532
  function playReferenceForTool(tool) {
19906
20533
  const record = tool;
19907
- const toolId = typeof record.toolId === "string" ? record.toolId : "play";
19908
- return `prebuilt/${toolId.replace(/_/g, "-")}`;
20534
+ const declared = stringField(record, "playReference", "play_reference");
20535
+ if (declared.startsWith("prebuilt/")) {
20536
+ return declared;
20537
+ }
20538
+ return null;
20539
+ }
20540
+ function playSearchCommandForToolId(toolId) {
20541
+ return `deepline plays search ${JSON.stringify(toolId.replace(/_/g, " "))} --json`;
19909
20542
  }
19910
- function playLikeToolExecuteErrorMessage(toolId) {
19911
- const playReference = `prebuilt/${toolId.replace(/_/g, "-")}`;
19912
- return `${toolId} is a workflow/play entry, not an atomic provider tool.
19913
- Use: deepline plays run ${playReference} --input '{...}' --watch
19914
- Or search provider tools only with: deepline tools search "<query>" --json`;
20543
+ function playRunCommandForTool(tool, toolId, input2) {
20544
+ const playReference = playReferenceForTool(tool);
20545
+ if (!playReference) return playSearchCommandForToolId(toolId);
20546
+ const inputArg = input2 === void 0 ? "'{...}'" : `'${JSON.stringify(input2)}'`;
20547
+ return `deepline plays run ${playReference} --input ${inputArg} --watch`;
19915
20548
  }
19916
- function printPlayLikeToolExecuteError(toolId) {
19917
- console.error(playLikeToolExecuteErrorMessage(toolId));
20549
+ function playLikeToolExecuteErrorMessage(toolId, tool) {
20550
+ const playReference = tool ? playReferenceForTool(tool) : null;
20551
+ const nextStep = playReference ? `Use: deepline plays run ${playReference} --input '{...}' --watch` : `Find the maintained workflow with: ${playSearchCommandForToolId(toolId)}`;
20552
+ return [
20553
+ `${toolId} is a workflow/play entry, not an atomic provider tool.`,
20554
+ nextStep,
20555
+ 'Or search provider tools only with: deepline tools search "<query>" --json'
20556
+ ].join("\n");
20557
+ }
20558
+ function printPlayLikeToolExecuteError(toolId, tool) {
20559
+ console.error(playLikeToolExecuteErrorMessage(toolId, tool));
19918
20560
  }
19919
20561
  function registerToolsCommands(program) {
19920
20562
  const tools = program.command("tools").description("Search, describe, and execute atomic provider tools.").addHelpText(
@@ -20212,13 +20854,14 @@ function toolContractJsonForDescribe(tool, requestedToolId) {
20212
20854
  "deeplineUsdPerPricingUnit",
20213
20855
  "deepline_usd_per_pricing_unit"
20214
20856
  );
20215
- const starterScript = extractedLists.length > 0 ? starterScriptJson(
20857
+ const starterScript = !isPlayLikeTool(tool) && extractedLists.length > 0 ? starterScriptJson(
20216
20858
  seedToolListScript({
20217
20859
  toolId,
20218
20860
  payload: samplePayloadForInputFields(inputFields),
20219
20861
  rows: []
20220
20862
  })
20221
20863
  ) : null;
20864
+ const executeCommand = isPlayLikeTool(tool) ? playRunCommandForTool(tool, toolId) : `deepline tools execute ${toolId} --input '{...}' --json`;
20222
20865
  return {
20223
20866
  schemaVersion: 1,
20224
20867
  toolId,
@@ -20243,7 +20886,7 @@ function toolContractJsonForDescribe(tool, requestedToolId) {
20243
20886
  extractedLists,
20244
20887
  extractedValues
20245
20888
  },
20246
- executeCommand: `deepline tools execute ${toolId} --input '{...}' --json`,
20889
+ executeCommand,
20247
20890
  ...starterScript ? { starterScript } : {}
20248
20891
  };
20249
20892
  }
@@ -20355,6 +20998,10 @@ function printToolSchemaOnly(tool, requestedToolId) {
20355
20998
  }
20356
20999
  }
20357
21000
  function printToolExamplesOnly(tool, requestedToolId, options = {}) {
21001
+ if (isPlayLikeTool(tool)) {
21002
+ printPlayLikeToolUsage(tool, requestedToolId);
21003
+ return;
21004
+ }
20358
21005
  const contract = toolContractJsonForDescribe(tool, requestedToolId);
20359
21006
  const toolId = String(contract.toolId);
20360
21007
  const inputFields = Array.isArray(contract.inputFields) ? contract.inputFields : [];
@@ -20386,6 +21033,25 @@ function printToolExamplesOnly(tool, requestedToolId, options = {}) {
20386
21033
  printSamples(samples);
20387
21034
  }
20388
21035
  }
21036
+ function printPlayLikeToolUsage(tool, requestedToolId) {
21037
+ const toolId = String(tool.toolId || requestedToolId);
21038
+ const inputFields = toolInputFieldsForDisplay(
21039
+ recordField2(tool, "inputSchema", "input_schema")
21040
+ );
21041
+ const sampleInput = samplePayloadForInputFields(inputFields);
21042
+ const playReference = playReferenceForTool(tool);
21043
+ console.log("Run as a play:");
21044
+ console.log(
21045
+ playRunCommandForTool(
21046
+ tool,
21047
+ toolId,
21048
+ Object.keys(sampleInput).length ? sampleInput : void 0
21049
+ )
21050
+ );
21051
+ if (playReference) {
21052
+ console.log(`deepline plays describe ${playReference} --json`);
21053
+ }
21054
+ }
20389
21055
  function printToolGettersOnly(tool, requestedToolId) {
20390
21056
  const contract = toolContractJsonForDescribe(tool, requestedToolId);
20391
21057
  const getters = isRecord8(contract.getters) ? contract.getters : {};
@@ -20472,13 +21138,14 @@ function toolMetadataJsonForDescribe(tool, requestedToolId) {
20472
21138
  const extractedLists = extractionContractEntries(
20473
21139
  arrayField(toolExecutionResult, "extractedLists", "extracted_lists")
20474
21140
  );
20475
- const starterScript = extractedLists.length > 0 ? starterScriptJson(
21141
+ const starterScript = !isPlayLikeTool(tool) && extractedLists.length > 0 ? starterScriptJson(
20476
21142
  seedToolListScript({
20477
21143
  toolId,
20478
21144
  payload: samplePayloadForInputFields(inputFields),
20479
21145
  rows: []
20480
21146
  })
20481
21147
  ) : null;
21148
+ const observedOutputCommand = isPlayLikeTool(tool) ? playRunCommandForTool(tool, toolId) : `deepline tools execute ${toolId} --input '{...}' --json`;
20482
21149
  const {
20483
21150
  cost: _cost,
20484
21151
  deeplineCreditsPerPricingUnit: _deeplineCreditsPerPricingUnit,
@@ -20504,8 +21171,8 @@ function toolMetadataJsonForDescribe(tool, requestedToolId) {
20504
21171
  getterScope: "extractedValues/extractedLists .get() only works for declared Deepline getters listed in usageGuidance.toolExecutionResult.",
20505
21172
  rawToolResponse: "Use toolExecutionResult.toolResponse.raw for provider/tool-specific fields, fields in outputSchema that are not declared getters, and debugging.",
20506
21173
  invalidGetterHint: "If TypeScript says an extractedValues/extractedLists property does not exist, that field is not a declared Deepline getter.",
20507
- observeActualShape: `deepline tools execute ${toolId} --input '{...}' --json`,
20508
- observedOutput: `deepline tools execute ${toolId} --input '{...}' --json`,
21174
+ observeActualShape: observedOutputCommand,
21175
+ observedOutput: observedOutputCommand,
20509
21176
  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.",
20510
21177
  executeOutputFields: "tools execute JSON may include output_preview for this direct probe only; play debugging uses run output and returned dataset handles."
20511
21178
  },
@@ -20661,10 +21328,10 @@ function normalizeOutputFormat(raw) {
20661
21328
  function resolveAtFilePath(rawPath) {
20662
21329
  const trimmed = rawPath.trim();
20663
21330
  const resolved = resolve13(trimmed);
20664
- if (existsSync9(resolved)) return resolved;
21331
+ if (existsSync10(resolved)) return resolved;
20665
21332
  if (process.platform !== "win32" && trimmed.includes("\\")) {
20666
21333
  const normalized = resolve13(trimmed.replace(/\\/g, "/"));
20667
- if (existsSync9(normalized)) return normalized;
21334
+ if (existsSync10(normalized)) return normalized;
20668
21335
  }
20669
21336
  return resolved;
20670
21337
  }
@@ -20675,7 +21342,7 @@ function readJsonArgument(raw, flagName) {
20675
21342
  throw new Error(`Invalid ${flagName} value: empty @file path.`);
20676
21343
  }
20677
21344
  try {
20678
- return readFileSync8(resolveAtFilePath(filePath), "utf8").replace(
21345
+ return readFileSync9(resolveAtFilePath(filePath), "utf8").replace(
20679
21346
  /^\uFEFF/,
20680
21347
  ""
20681
21348
  );
@@ -20777,9 +21444,9 @@ function starterScriptJson(script) {
20777
21444
  function seedToolListScript(input2) {
20778
21445
  const stem = safeFileStem(input2.toolId);
20779
21446
  const fileName = `${stem}-workflow-seed-${Date.now()}.play.ts`;
20780
- const scriptDir = mkdtempSync(join11(tmpdir4(), "deepline-workflow-seed-"));
21447
+ const scriptDir = mkdtempSync(join12(tmpdir4(), "deepline-workflow-seed-"));
20781
21448
  chmodSync(scriptDir, 448);
20782
- const scriptPath = join11(scriptDir, fileName);
21449
+ const scriptPath = join12(scriptDir, fileName);
20783
21450
  const projectDir = `deepline/projects/${stem}-workflow`;
20784
21451
  const playName = `${stem}-workflow`;
20785
21452
  const sampleRows = input2.rows.length > 0 ? `${JSON.stringify(input2.rows.slice(0, 2)).replace(/\]$/, "")}, ...]` : "[]";
@@ -20815,7 +21482,7 @@ export default definePlay(${JSON.stringify(playName)}, async (ctx) => {
20815
21482
  };
20816
21483
  });
20817
21484
  `;
20818
- writeFileSync10(scriptPath, script, { encoding: "utf-8", mode: 384 });
21485
+ writeFileSync11(scriptPath, script, { encoding: "utf-8", mode: 384 });
20819
21486
  return {
20820
21487
  path: scriptPath,
20821
21488
  sourceCode: script,
@@ -20916,9 +21583,19 @@ async function executeTool(args) {
20916
21583
  }
20917
21584
  if (isPlayLikeTool(metadata)) {
20918
21585
  if (argsWantJson(args)) {
20919
- printJsonError(new Error(playLikeToolExecuteErrorMessage(parsed.toolId)));
21586
+ printJsonError(
21587
+ new Error(
21588
+ playLikeToolExecuteErrorMessage(
21589
+ parsed.toolId,
21590
+ metadata
21591
+ )
21592
+ )
21593
+ );
20920
21594
  } else {
20921
- printPlayLikeToolExecuteError(parsed.toolId);
21595
+ printPlayLikeToolExecuteError(
21596
+ parsed.toolId,
21597
+ metadata
21598
+ );
20922
21599
  }
20923
21600
  return 2;
20924
21601
  }
@@ -21070,7 +21747,7 @@ async function executeTool(args) {
21070
21747
 
21071
21748
  // src/cli/commands/workflow.ts
21072
21749
  import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
21073
- import { dirname as dirname10, join as join12, resolve as resolve14 } from "path";
21750
+ import { dirname as dirname11, join as join13, resolve as resolve14 } from "path";
21074
21751
 
21075
21752
  // src/cli/workflow-to-play.ts
21076
21753
  import { createHash as createHash4 } from "crypto";
@@ -21311,8 +21988,8 @@ async function transformOne(api, workflowId, outDir, publish) {
21311
21988
  revision.config,
21312
21989
  { workflowName: workflow.name, version: revision.version }
21313
21990
  );
21314
- const file = join12(resolve14(outDir), `${compiled.playName}.play.ts`);
21315
- await mkdir5(dirname10(file), { recursive: true });
21991
+ const file = join13(resolve14(outDir), `${compiled.playName}.play.ts`);
21992
+ await mkdir5(dirname11(file), { recursive: true });
21316
21993
  await writeFile5(file, compiled.sourceCode, "utf8");
21317
21994
  let published = false;
21318
21995
  if (publish) {
@@ -21559,8 +22236,8 @@ Notes:
21559
22236
 
21560
22237
  // src/cli/commands/update.ts
21561
22238
  import { spawn as spawn2 } from "child_process";
21562
- import { existsSync as existsSync10 } from "fs";
21563
- import { dirname as dirname11, join as join13, resolve as resolve15 } from "path";
22239
+ import { existsSync as existsSync11 } from "fs";
22240
+ import { dirname as dirname12, join as join14, resolve as resolve15 } from "path";
21564
22241
  function posixShellQuote(value) {
21565
22242
  return `'${value.replace(/'/g, `'\\''`)}'`;
21566
22243
  }
@@ -21581,17 +22258,17 @@ function buildSourceUpdateCommand(sourceRoot) {
21581
22258
  function findRepoBackedSdkRoot(startPath) {
21582
22259
  let current = resolve15(startPath);
21583
22260
  while (true) {
21584
- if (existsSync10(join13(current, "sdk", "package.json")) && existsSync10(join13(current, "sdk", "bin", "deepline-dev.ts"))) {
22261
+ if (existsSync11(join14(current, "sdk", "package.json")) && existsSync11(join14(current, "sdk", "bin", "deepline-dev.ts"))) {
21585
22262
  return current;
21586
22263
  }
21587
- const parent = dirname11(current);
22264
+ const parent = dirname12(current);
21588
22265
  if (parent === current) return null;
21589
22266
  current = parent;
21590
22267
  }
21591
22268
  }
21592
22269
  function resolveUpdatePlan() {
21593
22270
  const entrypoint = process.argv[1] ? resolve15(process.argv[1]) : "";
21594
- const sourceRoot = entrypoint ? findRepoBackedSdkRoot(dirname11(entrypoint)) : null;
22271
+ const sourceRoot = entrypoint ? findRepoBackedSdkRoot(dirname12(entrypoint)) : null;
21595
22272
  if (sourceRoot) {
21596
22273
  return {
21597
22274
  kind: "source",
@@ -21758,6 +22435,23 @@ var install_commands_default = {
21758
22435
 
21759
22436
  // src/cli/install-commands.ts
21760
22437
  var INSTALL_COMMANDS = install_commands_default;
22438
+ var DEFAULT_V1_SKILL_NAMES = [
22439
+ "build-tam",
22440
+ "clay-to-deepline",
22441
+ "deepline-analytics",
22442
+ "deepline-feedback",
22443
+ "deepline-gtm",
22444
+ "deepline-quickstart",
22445
+ "find-qualified-titles",
22446
+ "linkedin-url-lookup",
22447
+ "niche-signal-discovery",
22448
+ "portfolio-prospecting",
22449
+ "workflow-hello-world"
22450
+ ];
22451
+ var DEFAULT_SDK_SKILL_NAMES = [
22452
+ ...DEFAULT_V1_SKILL_NAMES,
22453
+ "deepline-plays"
22454
+ ];
21761
22455
  function normalizeBaseUrl2(baseUrl) {
21762
22456
  return baseUrl.replace(/\/$/, "");
21763
22457
  }
@@ -21788,10 +22482,7 @@ function buildSkillsAddArgs(baseUrl, skillName, options = {}) {
21788
22482
  return INSTALL_COMMANDS.skills.default_agents;
21789
22483
  }
21790
22484
  if (arg === "{skill_name}") {
21791
- return [
21792
- value,
21793
- ...extraSkillNames.flatMap((name) => ["--skill", name])
21794
- ];
22485
+ return [value, ...extraSkillNames.flatMap((name) => ["--skill", name])];
21795
22486
  }
21796
22487
  return value;
21797
22488
  }
@@ -21835,8 +22526,8 @@ function commandCompatibilityHint(currentFamily, commandName, baseUrl) {
21835
22526
  if (currentFamily === "sdk") {
21836
22527
  lines.push(
21837
22528
  "",
21838
- " To stay on the SDK CLI, install the SDK agent skill:",
21839
- ` ${skillsInstallCommand(baseUrl, "deepline-plays")}`,
22529
+ " To stay on the SDK CLI, refresh the Deepline agent skills:",
22530
+ ` ${skillsInstallCommand(baseUrl, DEFAULT_SDK_SKILL_NAMES)}`,
21840
22531
  " To use the legacy Python CLI instead:",
21841
22532
  ` ${legacyPythonInstallCommand(baseUrl)}`,
21842
22533
  " `deepline update` updates this SDK CLI, but it will not switch CLI families."
@@ -21847,9 +22538,9 @@ function commandCompatibilityHint(currentFamily, commandName, baseUrl) {
21847
22538
  } else {
21848
22539
  lines.push(
21849
22540
  "",
21850
- " To use SDK commands, install the SDK CLI and SDK agent skill:",
22541
+ " To use SDK commands, install the SDK CLI and refresh Deepline agent skills:",
21851
22542
  ` ${sdkNpmGlobalInstallCommand()}`,
21852
- ` ${skillsInstallCommand(baseUrl, "deepline-plays")}`,
22543
+ ` ${skillsInstallCommand(baseUrl, DEFAULT_SDK_SKILL_NAMES)}`,
21853
22544
  " `deepline update` updates this Python CLI and its skills, but it will not switch CLI families."
21854
22545
  );
21855
22546
  if (compatibility.python_alternative) {
@@ -21930,23 +22621,11 @@ async function maybeAutoUpdateAndRelaunch(response) {
21930
22621
 
21931
22622
  // src/cli/skills-sync.ts
21932
22623
  import { spawn as spawn4, spawnSync as spawnSync2 } from "child_process";
21933
- import {
21934
- existsSync as existsSync11,
21935
- mkdirSync as mkdirSync6,
21936
- readdirSync as readdirSync3,
21937
- readFileSync as readFileSync9,
21938
- statSync as statSync4,
21939
- writeFileSync as writeFileSync11
21940
- } from "fs";
21941
- import { homedir as homedir7 } from "os";
21942
- import { dirname as dirname12, join as join14 } from "path";
22624
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync12 } from "fs";
22625
+ import { homedir as homedir8 } from "os";
22626
+ import { dirname as dirname13, join as join15 } from "path";
21943
22627
  var CHECK_TIMEOUT_MS2 = 3e3;
21944
- var SDK_SKILL_NAME = "deepline-plays";
21945
- var SDK_SKILL_NAMES = [
21946
- SDK_SKILL_NAME,
21947
- "deepline-plays-feedback",
21948
- "deepline-plays-quickstart"
21949
- ];
22628
+ var SDK_PLAY_SKILL_NAME = "deepline-plays";
21950
22629
  var attemptedSync = false;
21951
22630
  function shouldSkipSkillsSync() {
21952
22631
  const value = process.env.DEEPLINE_SKIP_SKILLS_SYNC?.trim().toLowerCase();
@@ -21958,20 +22637,20 @@ function activePluginSkillsDir() {
21958
22637
  return "";
21959
22638
  }
21960
22639
  const dir = process.env.DEEPLINE_PLUGIN_SKILLS_DIR?.trim() ?? "";
21961
- return dir && existsSync11(dir) ? dir : "";
22640
+ return dir && existsSync12(dir) ? dir : "";
21962
22641
  }
21963
22642
  function readPluginSkillsVersion() {
21964
22643
  const dir = activePluginSkillsDir();
21965
22644
  if (!dir) return "";
21966
22645
  try {
21967
- return readFileSync9(join14(dir, ".version"), "utf-8").trim();
22646
+ return readFileSync10(join15(dir, ".version"), "utf-8").trim();
21968
22647
  } catch {
21969
22648
  return "";
21970
22649
  }
21971
22650
  }
21972
22651
  function sdkSkillsVersionPath(baseUrl) {
21973
- const home = process.env.HOME?.trim() || homedir7();
21974
- return join14(
22652
+ const home = process.env.HOME?.trim() || homedir8();
22653
+ return join15(
21975
22654
  home,
21976
22655
  ".local",
21977
22656
  "deepline",
@@ -21984,55 +22663,46 @@ function readLocalSkillsVersion(baseUrl) {
21984
22663
  const pluginVersion = readPluginSkillsVersion();
21985
22664
  if (pluginVersion) return pluginVersion;
21986
22665
  const path = sdkSkillsVersionPath(baseUrl);
21987
- if (!existsSync11(path)) return "";
22666
+ if (!existsSync12(path)) return "";
21988
22667
  try {
21989
- return readFileSync9(path, "utf-8").trim();
22668
+ return readFileSync10(path, "utf-8").trim();
21990
22669
  } catch {
21991
22670
  return "";
21992
22671
  }
21993
22672
  }
21994
22673
  function writeLocalSkillsVersion(baseUrl, version) {
21995
22674
  const path = sdkSkillsVersionPath(baseUrl);
21996
- mkdirSync6(dirname12(path), { recursive: true });
21997
- writeFileSync11(path, `${version}
22675
+ mkdirSync7(dirname13(path), { recursive: true });
22676
+ writeFileSync12(path, `${version}
21998
22677
  `, "utf-8");
21999
22678
  }
22000
- function installedSdkSkillHasStalePositionalExecuteExamples() {
22001
- const home = process.env.HOME?.trim() || homedir7();
22002
- const pluginSkillsDir = activePluginSkillsDir();
22003
- const roots = pluginSkillsDir ? [join14(pluginSkillsDir, SDK_SKILL_NAME)] : [
22004
- join14(home, ".claude", "skills", SDK_SKILL_NAME),
22005
- join14(home, ".agents", "skills", SDK_SKILL_NAME)
22006
- ];
22007
- const staleMarkers = [
22008
- "ctx.tools.execute(key",
22009
- "ctx.tools.execute('",
22010
- 'ctx.tools.execute("',
22011
- "rowCtx.tools.execute('",
22012
- 'rowCtx.tools.execute("'
22013
- ];
22014
- const scan = (dir) => {
22015
- for (const entry of readdirSync3(dir)) {
22016
- const path = join14(dir, entry);
22017
- const stat4 = statSync4(path);
22018
- if (stat4.isDirectory()) {
22019
- if (scan(path)) return true;
22020
- continue;
22021
- }
22022
- if (!entry.endsWith(".md")) continue;
22023
- const text = readFileSync9(path, "utf-8");
22024
- if (staleMarkers.some((marker) => text.includes(marker))) return true;
22025
- }
22026
- return false;
22027
- };
22028
- for (const root of roots) {
22029
- try {
22030
- if (existsSync11(root) && scan(root)) return true;
22031
- } catch {
22032
- continue;
22033
- }
22679
+ function sortedUniqueSkillNames(names) {
22680
+ return [...new Set(names.map((name) => name.trim()).filter(Boolean))].sort(
22681
+ (a, b) => a.localeCompare(b)
22682
+ );
22683
+ }
22684
+ async function fetchV1SkillNames(baseUrl) {
22685
+ const controller = new AbortController();
22686
+ const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS2);
22687
+ try {
22688
+ const response = await fetch(
22689
+ new URL("/.well-known/skills/index.json", baseUrl),
22690
+ { signal: controller.signal }
22691
+ );
22692
+ if (!response.ok) return [];
22693
+ const data = await response.json().catch(() => null);
22694
+ const names = (data?.skills ?? []).filter((skill) => skill.install_surface === "v1").map((skill) => skill.name).filter(
22695
+ (name) => typeof name === "string" && name.length > 0
22696
+ );
22697
+ return sortedUniqueSkillNames(names);
22698
+ } catch {
22699
+ return [];
22700
+ } finally {
22701
+ clearTimeout(timeout);
22034
22702
  }
22035
- return false;
22703
+ }
22704
+ function buildSdkSkillNames(v1SkillNames) {
22705
+ return sortedUniqueSkillNames([...v1SkillNames, SDK_PLAY_SKILL_NAME]);
22036
22706
  }
22037
22707
  async function fetchSkillsUpdate(baseUrl, localVersion) {
22038
22708
  const controller = new AbortController();
@@ -22062,11 +22732,13 @@ async function fetchSkillsUpdate(baseUrl, localVersion) {
22062
22732
  clearTimeout(timeout);
22063
22733
  }
22064
22734
  }
22065
- function buildSkillsInstallArgs(baseUrl) {
22066
- return buildSkillsAddArgs(baseUrl, SDK_SKILL_NAMES);
22735
+ function buildSkillsInstallArgs(baseUrl, skillNames = DEFAULT_SDK_SKILL_NAMES) {
22736
+ return buildSkillsAddArgs(baseUrl, sortedUniqueSkillNames(skillNames));
22067
22737
  }
22068
- function buildBunxSkillsInstallArgs(baseUrl) {
22069
- return buildSkillsAddArgs(baseUrl, SDK_SKILL_NAMES, { firstArg: "--bun" });
22738
+ function buildBunxSkillsInstallArgs(baseUrl, skillNames) {
22739
+ return buildSkillsAddArgs(baseUrl, sortedUniqueSkillNames(skillNames), {
22740
+ firstArg: "--bun"
22741
+ });
22070
22742
  }
22071
22743
  function hasCommand(command) {
22072
22744
  const result = spawnSync2(command, ["--version"], {
@@ -22078,15 +22750,15 @@ function hasCommand(command) {
22078
22750
  function shellQuote4(arg) {
22079
22751
  return `'${arg.replace(/'/g, `'\\''`)}'`;
22080
22752
  }
22081
- function resolveSkillsInstallCommands(baseUrl) {
22082
- const npxArgs = buildSkillsInstallArgs(baseUrl);
22753
+ function resolveSkillsInstallCommands(baseUrl, skillNames = DEFAULT_SDK_SKILL_NAMES) {
22754
+ const npxArgs = buildSkillsInstallArgs(baseUrl, skillNames);
22083
22755
  const npxInstall = {
22084
22756
  command: "npx",
22085
22757
  args: npxArgs,
22086
22758
  manualCommand: `npx ${npxArgs.map(shellQuote4).join(" ")}`
22087
22759
  };
22088
22760
  if (hasCommand("bunx")) {
22089
- const bunxArgs = buildBunxSkillsInstallArgs(baseUrl);
22761
+ const bunxArgs = buildBunxSkillsInstallArgs(baseUrl, skillNames);
22090
22762
  return [
22091
22763
  {
22092
22764
  command: "bunx",
@@ -22129,9 +22801,9 @@ function runOneSkillsInstall(install) {
22129
22801
  });
22130
22802
  });
22131
22803
  }
22132
- async function runSkillsInstall(baseUrl) {
22804
+ async function runSkillsInstall(baseUrl, skillNames) {
22133
22805
  const failures = [];
22134
- for (const install of resolveSkillsInstallCommands(baseUrl)) {
22806
+ for (const install of resolveSkillsInstallCommands(baseUrl, skillNames)) {
22135
22807
  const result = await runOneSkillsInstall(install);
22136
22808
  if (result.ok) return true;
22137
22809
  failures.push(result);
@@ -22161,26 +22833,289 @@ async function syncSdkSkillsIfNeeded(baseUrl) {
22161
22833
  const usingPluginSkills = Boolean(activePluginSkillsDir());
22162
22834
  const localVersion = readLocalSkillsVersion(baseUrl);
22163
22835
  const update = await fetchSkillsUpdate(baseUrl, localVersion);
22164
- const hasStaleInstalledSkill = installedSdkSkillHasStalePositionalExecuteExamples();
22165
22836
  if (usingPluginSkills) {
22166
22837
  return;
22167
22838
  }
22168
- if (!update?.needsUpdate && !hasStaleInstalledSkill || !update?.remoteVersion) {
22839
+ if (!update?.needsUpdate || !update.remoteVersion) {
22169
22840
  return;
22170
22841
  }
22171
- writeSdkSkillsStatusLine(
22172
- "SDK skills changed; syncing Deepline SDK skills..."
22842
+ const remoteSkillNames = await fetchV1SkillNames(baseUrl);
22843
+ const skillNames = buildSdkSkillNames(
22844
+ remoteSkillNames.length > 0 ? remoteSkillNames : DEFAULT_SDK_SKILL_NAMES
22173
22845
  );
22174
- const installed = await runSkillsInstall(baseUrl);
22846
+ if (skillNames.length === 0) return;
22847
+ writeSdkSkillsStatusLine("Deepline skills changed; syncing agent skills...");
22848
+ const installed = await runSkillsInstall(baseUrl, skillNames);
22175
22849
  if (!installed) return;
22176
- if (installedSdkSkillHasStalePositionalExecuteExamples()) {
22177
- process.stderr.write(
22178
- "SDK skills sync completed, but installed deepline-plays docs still contain stale positional ctx.tools.execute examples.\n"
22179
- );
22180
- return;
22181
- }
22182
22850
  writeLocalSkillsVersion(baseUrl, update.remoteVersion);
22183
- writeSdkSkillsStatusLine("SDK skills are up to date.");
22851
+ writeSdkSkillsStatusLine("Deepline agent skills are up to date.");
22852
+ }
22853
+
22854
+ // src/cli/failure-reporting.ts
22855
+ import { hostname as hostname2, platform as platform2, release } from "os";
22856
+ var FAILURE_REPORT_DISABLE_ENV = "DEEPLINE_DISABLE_FAILURE_REPORTING";
22857
+ var REPORT_FAILURE_TIMEOUT_MS = 1e4;
22858
+ var MAX_FAILURE_TEXT_CHARS = 4e3;
22859
+ var MAX_COMMAND_TOKENS = 3;
22860
+ var REPORTABLE_EXIT_CODES = /* @__PURE__ */ new Set([4, 5]);
22861
+ var EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
22862
+ var UNIX_PATH_RE = /(?:\/Users\/|\/home\/|\/var\/folders\/|\/tmp\/|\/sessions\/)[^"'\n\r`]+/g;
22863
+ var WINDOWS_PATH_RE = /[A-Za-z]:(?:\\{1,2})(?:Users|Temp|tmp)(?:\\{1,2})[^"'\n\r`]+/g;
22864
+ var ASSIGNMENT_SECRET_RE = /\b(access[_-]?token|api[_-]?key|apikey|auth(?:orization)?|bearer|password|secret|session)\b(\s*[:=]\s*)(?!Bearer\s+\[redacted-secret\])([^\s,;]+)/gi;
22865
+ var BEARER_SECRET_RE = /\b(bearer)\s+([A-Za-z0-9._-]+)/gi;
22866
+ var GENERIC_TOKEN_RE = /\b(?:dlp|sk|ghp|xox[baprs])[-_A-Za-z0-9]{8,}\b/g;
22867
+ var SECRET_OPTION_RE = /^--?(?:access[-_]?token|api[-_]?key|apikey|auth(?:orization)?|bearer|password|secret|session|token)$/i;
22868
+ function truthyEnv(name) {
22869
+ return ["1", "true", "yes", "on"].includes(
22870
+ String(process.env[name] ?? "").trim().toLowerCase()
22871
+ );
22872
+ }
22873
+ function isFailureReportingDisabled() {
22874
+ return truthyEnv(FAILURE_REPORT_DISABLE_ENV);
22875
+ }
22876
+ function redactFailureText(value, maxChars = MAX_FAILURE_TEXT_CHARS) {
22877
+ const home = process.env.HOME?.trim();
22878
+ let text = String(value ?? "");
22879
+ if (!text) return "";
22880
+ if (home && home !== "/") {
22881
+ text = text.split(home).join("~");
22882
+ }
22883
+ return text.replace(EMAIL_RE, "[redacted-email]").replace(UNIX_PATH_RE, "[redacted-path]").replace(WINDOWS_PATH_RE, "[redacted-path]").replace(BEARER_SECRET_RE, "$1 [redacted-secret]").replace(ASSIGNMENT_SECRET_RE, "$1$2[redacted-secret]").replace(GENERIC_TOKEN_RE, "[redacted-secret]").slice(0, maxChars);
22884
+ }
22885
+ function sanitizeCommand(argv, prefix = "deepline") {
22886
+ const tokens = [];
22887
+ for (let index = 0; index < argv.length; index += 1) {
22888
+ const arg = argv[index];
22889
+ const value = String(arg ?? "").trim();
22890
+ if (!value) continue;
22891
+ if (value.startsWith("-")) {
22892
+ if (!value.includes("=") && SECRET_OPTION_RE.test(value)) {
22893
+ index += 1;
22894
+ }
22895
+ continue;
22896
+ }
22897
+ tokens.push(redactFailureText(value, 200));
22898
+ if (tokens.length >= MAX_COMMAND_TOKENS) break;
22899
+ }
22900
+ return tokens.length > 0 ? [prefix, ...tokens].join(" ") : prefix;
22901
+ }
22902
+ function errorMessage3(error) {
22903
+ if (error instanceof Error) return `${error.name}: ${error.message}`;
22904
+ return String(error ?? "");
22905
+ }
22906
+ function errorStack(error) {
22907
+ if (error instanceof Error && error.stack) {
22908
+ return redactFailureText(error.stack);
22909
+ }
22910
+ return null;
22911
+ }
22912
+ function classifyNetworkFailure(error) {
22913
+ const seen = /* @__PURE__ */ new Set();
22914
+ let current = error;
22915
+ while (current && !seen.has(current)) {
22916
+ seen.add(current);
22917
+ const record = typeof current === "object" && current !== null ? current : {};
22918
+ const code = String(record.code ?? "").toLowerCase();
22919
+ const name = current instanceof Error ? current.name.toLowerCase() : "";
22920
+ const text = String(
22921
+ current instanceof Error ? current.message : current
22922
+ ).toLowerCase();
22923
+ const combined = `${code} ${name} ${text}`;
22924
+ if (combined.includes("aborterror") || combined.includes("timeout") || combined.includes("timed out") || combined.includes("etimedout")) {
22925
+ return "network_timeout";
22926
+ }
22927
+ if (combined.includes("enotfound") || combined.includes("eai_again") || combined.includes("name or service not known") || combined.includes("temporary failure in name resolution")) {
22928
+ return "network_dns_resolution_failed";
22929
+ }
22930
+ if (combined.includes("econnrefused") || combined.includes("connection refused")) {
22931
+ return "network_connection_refused";
22932
+ }
22933
+ if (combined.includes("econnreset") || combined.includes("connection reset")) {
22934
+ return "network_connection_reset";
22935
+ }
22936
+ if (combined.includes("incompleteread") || combined.includes("incomplete read")) {
22937
+ return "network_incomplete_read";
22938
+ }
22939
+ if (combined.includes("remotedisconnected") || combined.includes("remote end closed connection") || combined.includes("other side closed") || combined.includes("socket hang up")) {
22940
+ return "network_remote_disconnected";
22941
+ }
22942
+ if (combined.includes("ssl") || combined.includes("tls") || combined.includes("unexpected_eof_while_reading")) {
22943
+ return "network_ssl_error";
22944
+ }
22945
+ current = record.cause ?? record.context;
22946
+ }
22947
+ return "network_error";
22948
+ }
22949
+ function isNetworkFailure(error) {
22950
+ if (!(error instanceof Error)) return false;
22951
+ if (error instanceof DeeplineError && error.statusCode) return false;
22952
+ const code = classifyNetworkFailure(error);
22953
+ return code !== "network_error" || /unable to connect|unable to stream/i.test(error.message);
22954
+ }
22955
+ function detectAgentRuntime3() {
22956
+ if (process.env.CODEX_THREAD_ID?.trim()) return "codex";
22957
+ const pluginMode = truthyEnv("DEEPLINE_PLUGIN_MODE");
22958
+ const claudeRemote = truthyEnv("CLAUDE_CODE_REMOTE");
22959
+ const projectDir = Boolean(process.env.CLAUDE_PROJECT_DIR?.trim());
22960
+ const pluginRoot = Boolean(process.env.DEEPLINE_PLUGIN_ROOT?.trim());
22961
+ const sessionHome = process.env.HOME?.trim().startsWith("/sessions/") ?? false;
22962
+ if ((pluginMode || pluginRoot) && (claudeRemote || projectDir || sessionHome)) {
22963
+ return "claude_cowork";
22964
+ }
22965
+ if (process.env.CLAUDECODE?.trim() === "1") return "claude_code";
22966
+ if (process.env.CLINE_ACTIVE?.trim().toLowerCase() === "true") return "cline";
22967
+ if (process.env.CURSOR_TRACE_ID?.trim() || process.env.CURSOR_AGENT?.trim()) {
22968
+ return "cursor";
22969
+ }
22970
+ if (process.env.WINDSURF?.trim() || process.env.CASCADE?.trim()) {
22971
+ return "windsurf";
22972
+ }
22973
+ return "unknown";
22974
+ }
22975
+ function buildEnvironmentContext() {
22976
+ const context = {
22977
+ os: platform2(),
22978
+ os_release: release(),
22979
+ platform: `${platform2()}-${release()}-${process.arch}`,
22980
+ node_version: process.version,
22981
+ runtime: "Node.js",
22982
+ hostname: hostname2(),
22983
+ agent_runtime: detectAgentRuntime3()
22984
+ };
22985
+ for (const key of ["CLAUDE_CODE_REMOTE", "DEEPLINE_PLUGIN_MODE"]) {
22986
+ const normalized = process.env[key]?.trim();
22987
+ if (normalized) context[key.toLowerCase()] = normalized;
22988
+ }
22989
+ if (process.env.CLAUDE_PROJECT_DIR?.trim()) {
22990
+ context.claude_project_dir_present = "true";
22991
+ }
22992
+ if (process.env.DEEPLINE_PLUGIN_ROOT?.trim()) {
22993
+ context.deepline_plugin_root_present = "true";
22994
+ }
22995
+ if (process.env.DEEPLINE_PLUGIN_SKILLS_DIR?.trim()) {
22996
+ context.deepline_plugin_skills_dir_present = "true";
22997
+ }
22998
+ if (process.env.HOME?.trim().startsWith("/sessions/")) {
22999
+ context.home_scope = "sessions";
23000
+ }
23001
+ return context;
23002
+ }
23003
+ function subcommandFromArgv(argv) {
23004
+ return argv.find((arg) => arg && !arg.startsWith("-")) ?? null;
23005
+ }
23006
+ function commandTokens(argv) {
23007
+ return argv.map((arg) => String(arg ?? "").trim()).filter((arg) => arg && !arg.startsWith("-"));
23008
+ }
23009
+ function isServerLoggedPlayRunStartFailure(input2) {
23010
+ const [subcommand, command] = commandTokens(input2.argv);
23011
+ return subcommand === "plays" && command === "run" && input2.error instanceof DeeplineError && typeof input2.error.statusCode === "number";
23012
+ }
23013
+ function shouldReport(input2) {
23014
+ if (input2.error !== void 0) return true;
23015
+ return input2.exitCode !== null && REPORTABLE_EXIT_CODES.has(input2.exitCode);
23016
+ }
23017
+ function failureCode(input2) {
23018
+ if (input2.error !== void 0) {
23019
+ if (isNetworkFailure(input2.error))
23020
+ return classifyNetworkFailure(input2.error);
23021
+ if (input2.error instanceof DeeplineError && input2.error.code) {
23022
+ return input2.error.code;
23023
+ }
23024
+ if (input2.error instanceof Error && input2.error.name)
23025
+ return input2.error.name;
23026
+ return "CLI_FAILURE";
23027
+ }
23028
+ return input2.exitCode === 4 ? "network_error" : "command_exit";
23029
+ }
23030
+ function resolvedExitCode(input2) {
23031
+ if (typeof input2.exitCode === "number" && Number.isFinite(input2.exitCode)) {
23032
+ return Math.trunc(input2.exitCode);
23033
+ }
23034
+ if (input2.error === void 0) return null;
23035
+ return isNetworkFailure(input2.error) ? 4 : 5;
23036
+ }
23037
+ function resolveSdkCliFailureExitCode(error) {
23038
+ return isNetworkFailure(error) ? 4 : 1;
23039
+ }
23040
+ function resolvedFailureKind(input2) {
23041
+ return input2.error === void 0 ? "command_exit" : "uncaught_exception";
23042
+ }
23043
+ function buildFailureReport(input2) {
23044
+ const durationMs = Math.max(0, Date.now() - input2.startedAtMs);
23045
+ const failureKind = resolvedFailureKind({
23046
+ exitCode: input2.exitCode,
23047
+ error: input2.error
23048
+ });
23049
+ const code = failureCode({ exitCode: input2.exitCode, error: input2.error });
23050
+ const errorBody = input2.error === void 0 ? `SDK CLI command exited ${input2.exitCode ?? "unknown"}` : errorMessage3(input2.error);
23051
+ return {
23052
+ command: sanitizeCommand(input2.argv),
23053
+ subcommand: subcommandFromArgv(input2.argv),
23054
+ error_status: input2.exitCode,
23055
+ error_body: redactFailureText(errorBody),
23056
+ exit_code: input2.exitCode,
23057
+ duration_ms: durationMs,
23058
+ error_class: input2.error instanceof Error ? input2.error.name : null,
23059
+ stack_trace: errorStack(input2.error),
23060
+ failure_kind: failureKind,
23061
+ failure_code: code,
23062
+ failure_stage: input2.error === void 0 ? "command_exit" : "cli_main",
23063
+ cli_version: SDK_VERSION,
23064
+ context: {
23065
+ base_url: redactFailureText(input2.baseUrl, 400),
23066
+ command_summary: sanitizeCommand(input2.argv),
23067
+ environment: buildEnvironmentContext(),
23068
+ failure_kind: failureKind,
23069
+ failure_code: code,
23070
+ failure_stage: input2.error === void 0 ? "command_exit" : "cli_main",
23071
+ duration_ms: durationMs,
23072
+ ...input2.exitCode !== null ? { exit_code: input2.exitCode } : {}
23073
+ }
23074
+ };
23075
+ }
23076
+ async function maybeReportSdkCliFailure(input2) {
23077
+ if (isFailureReportingDisabled()) return false;
23078
+ if (isServerLoggedPlayRunStartFailure(input2)) return false;
23079
+ const exitCode = resolvedExitCode(input2);
23080
+ if (!shouldReport({ exitCode, error: input2.error })) return false;
23081
+ const baseUrl = autoDetectBaseUrl().replace(/\/$/, "");
23082
+ const apiKey = resolveApiKeyForBaseUrl(baseUrl);
23083
+ if (!apiKey) return false;
23084
+ const controller = new AbortController();
23085
+ const timeout = setTimeout(
23086
+ () => controller.abort(),
23087
+ REPORT_FAILURE_TIMEOUT_MS
23088
+ );
23089
+ try {
23090
+ await fetch(new URL("/api/v2/cli/report-failure", baseUrl), {
23091
+ method: "POST",
23092
+ headers: {
23093
+ Authorization: `Bearer ${apiKey}`,
23094
+ "Content-Type": "application/json",
23095
+ "User-Agent": `deepline-ts-sdk/${SDK_VERSION}`,
23096
+ "X-Deepline-Client-Family": "sdk",
23097
+ "X-Deepline-CLI-Family": "sdk",
23098
+ "X-Deepline-Agent-Runtime": detectAgentRuntime3(),
23099
+ "X-Deepline-CLI-Version": SDK_VERSION,
23100
+ "X-Deepline-SDK-Version": SDK_VERSION
23101
+ },
23102
+ body: JSON.stringify(
23103
+ buildFailureReport({
23104
+ argv: input2.argv,
23105
+ startedAtMs: input2.startedAtMs,
23106
+ error: input2.error,
23107
+ exitCode,
23108
+ baseUrl
23109
+ })
23110
+ ),
23111
+ signal: controller.signal
23112
+ });
23113
+ return true;
23114
+ } catch {
23115
+ return false;
23116
+ } finally {
23117
+ clearTimeout(timeout);
23118
+ }
22184
23119
  }
22185
23120
 
22186
23121
  // src/cli/index.ts
@@ -22224,8 +23159,8 @@ function topLevelCommandKnown(program, commandName) {
22224
23159
  );
22225
23160
  }
22226
23161
  async function runPlayRunnerHealthCheck() {
22227
- const dir = await mkdtemp2(join15(tmpdir5(), "deepline-health-play-"));
22228
- const file = join15(dir, "health-check.play.ts");
23162
+ const dir = await mkdtemp2(join16(tmpdir5(), "deepline-health-play-"));
23163
+ const file = join16(dir, "health-check.play.ts");
22229
23164
  try {
22230
23165
  await writeFile6(
22231
23166
  file,
@@ -22442,7 +23377,7 @@ Exit codes:
22442
23377
  `
22443
23378
  );
22444
23379
  program.hook("preAction", async (_thisCommand, actionCommand) => {
22445
- if (actionCommand.name() === "version" || actionCommand.name() === "update" || isLegacyNoopInvocation()) {
23380
+ if (actionCommand.name() === "version" || actionCommand.name() === "update" || actionCommand.name() === "switch" || isLegacyNoopInvocation()) {
22446
23381
  return;
22447
23382
  }
22448
23383
  if (printStartupPhase) {
@@ -22490,6 +23425,7 @@ Exit codes:
22490
23425
  registerLegacyNoopCommands(program);
22491
23426
  registerUpdateCommand(program);
22492
23427
  registerQuickstartCommands(program);
23428
+ registerSwitchCommands(program);
22493
23429
  program.command("preflight").description("Run compact health, auth, and Deepline billing checks.").option("--json", "Force JSON output.").addHelpText(
22494
23430
  "after",
22495
23431
  `
@@ -22609,6 +23545,11 @@ Examples:
22609
23545
  ms: Date.now() - mainStartedAt,
22610
23546
  ok: true
22611
23547
  });
23548
+ await maybeReportSdkCliFailure({
23549
+ argv: process.argv.slice(2),
23550
+ startedAtMs: mainStartedAt,
23551
+ exitCode: typeof process.exitCode === "number" ? process.exitCode : null
23552
+ });
22612
23553
  } catch (error) {
22613
23554
  const commanderError = asCommanderError(error);
22614
23555
  recordCliTrace({
@@ -22644,7 +23585,14 @@ Examples:
22644
23585
  } else {
22645
23586
  console.error(`Error: ${String(error)}`);
22646
23587
  }
22647
- process.exitCode = 1;
23588
+ const failureExitCode = resolveSdkCliFailureExitCode(error);
23589
+ process.exitCode = failureExitCode;
23590
+ await maybeReportSdkCliFailure({
23591
+ argv: process.argv.slice(2),
23592
+ startedAtMs: mainStartedAt,
23593
+ exitCode: failureExitCode,
23594
+ error
23595
+ });
22648
23596
  }
22649
23597
  process.exitCode = process.exitCode ?? 0;
22650
23598
  }