@tangle-network/agent-app 0.46.60 → 0.47.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,6 +173,18 @@ Two `createMissionService` seams adopters hit on day one:
173
173
  - **`generateId`** (on `MissionServiceOptions`) defaults to `crypto.randomUUID()` — a 36-char dashed UUID. If your mission table has an existing id shape (e.g. 32-hex to match D1 row defaults), inject your own generator; the service stamps it verbatim on the inserted record.
174
174
  - **`CreateMissionInput.extras`** carries opaque product-column values (a `workflowId` FK, a source-turn pointer) verbatim to `MissionStorePort.insert(record, extras)`, so creation is a single write — no post-insert stamp. The service never reads them.
175
175
 
176
+ ### Sandbox reuse
177
+
178
+ Set `livenessProbe: {}` to verify a reused runtime with `echo alive`.
179
+ The command must exit successfully and return only `alive`, allowing surrounding whitespace.
180
+ A healthy idle runtime does not need a running CLI process.
181
+ Remove `sidecarProcessPattern`, `psTimeoutMs`, and imports of `DEFAULT_SIDECAR_PROCESS_PATTERN` when upgrading.
182
+
183
+ `execTimeoutMs` sets the SDK command timeout, from 100 to 600,000 milliseconds; the default is 5,000.
184
+ SDK readiness and transport deadlines apply separately, so this value is not an end-to-end provisioning deadline.
185
+ Recovery waits for the SDK request to settle before restarting the sandbox.
186
+ Failures retain their SDK cause for the redacted diagnostic serializer and preserve the workspace.
187
+
176
188
  ## Compatibility
177
189
 
178
190
  - **ESM only.** Ships `import` + `types` conditions per subpath.
@@ -19,7 +19,7 @@ import {
19
19
  AsyncView
20
20
  } from "../chunk-3UBAO3N5.js";
21
21
  import "../chunk-SJWIZT7B.js";
22
- import "../chunk-QY4BRKRJ.js";
22
+ import "../chunk-HZVVO6VR.js";
23
23
  import "../chunk-SUO2R2ET.js";
24
24
  import "../chunk-IRYULX3R.js";
25
25
  import "../chunk-ZVEEWGDK.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-MH74DY2I.js";
4
4
  import {
5
5
  createSandboxFileIndexRoute
6
- } from "../chunk-QY4BRKRJ.js";
6
+ } from "../chunk-HZVVO6VR.js";
7
7
  import {
8
8
  ALLOWED_ATTACHMENT_SNIFFED_MIMES,
9
9
  ATTACHMENT_ACCEPT,
@@ -112,7 +112,7 @@ import {
112
112
  flattenHistory,
113
113
  readSandboxBinaryBytes,
114
114
  statSandboxFileSize
115
- } from "../chunk-WSRECTKA.js";
115
+ } from "../chunk-P6YXUGNA.js";
116
116
  import "../chunk-MCYJON3F.js";
117
117
  import "../chunk-LWSJK546.js";
118
118
  import "../chunk-P5PIAQVS.js";
@@ -10,6 +10,7 @@ var DEFAULT_IGNORE_SEGMENTS = [
10
10
  "venv"
11
11
  ];
12
12
  function isIgnored(relPath, ignoreSegments) {
13
+ if (!relPath || relPath.startsWith("/") || relPath.includes("\\")) return true;
13
14
  for (const segment of relPath.split("/")) {
14
15
  if (!segment) continue;
15
16
  if (segment.startsWith(".")) return true;
@@ -59,10 +60,10 @@ function createSandboxFileIndexRoute(options) {
59
60
  if (!isMissingRootError(err, auth.root)) throw err;
60
61
  return Response.json({ status: "warming" });
61
62
  }
62
- const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments));
63
+ const filtered = scan.files.filter((f) => !isIgnored(relativeTo(auth.root, f.path), ignoreSegments));
63
64
  const truncated = scan.stats.truncated || filtered.length > maxEntries;
64
65
  const files = filtered.slice(0, maxEntries).map((f) => {
65
- const path = relativeTo(scan.root, f.path);
66
+ const path = relativeTo(auth.root, f.path);
66
67
  const entry = { path, name: basename(path) };
67
68
  if (typeof f.size === "number") entry.size = f.size;
68
69
  return entry;
@@ -81,4 +82,4 @@ function createSandboxFileIndexRoute(options) {
81
82
  export {
82
83
  createSandboxFileIndexRoute
83
84
  };
84
- //# sourceMappingURL=chunk-QY4BRKRJ.js.map
85
+ //# sourceMappingURL=chunk-HZVVO6VR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n *\n * A box can also be running with its workspace root not yet materialised, which\n * `authorize` cannot see; the route recognises that one signal off `fs.tree`\n * and answers `warming` too, so every consumer gets the retry-and-wait state\n * instead of a 500. Every other `tree()` failure propagates.\n */\n\nimport type { FileMention } from './wire'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\n/** Describe a ready file index response with workspace-relative entries and truncation status */\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. Two situations produce it: `authorize` reporting a box that\n * is not running, and a running box whose workspace root does not exist yet\n * (see `isMissingRootError`). */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\n/** Resolve a response indicating the file index is either ready or warming up */\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\n/** Define authorization details and parameters for indexing a file workspace with optional caching and ignore rules */\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\n/** Define options to authorize and configure sandbox file index route behavior */\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n if (!relPath || relPath.startsWith('/') || relPath.includes('\\\\')) return true\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\n/**\n * A box can answer `running` before it has materialised the workspace root —\n * `authorize` has already committed to `ready` by then, so `fs.tree` is the\n * first thing to notice, and it rejects with the sandbox SDK's\n * `ValidationError` wrapping a box-side `ENOENT … lstat` on the root. That is\n * the SAME \"not usable yet\" state `authorize` collapses onto `warming` for an\n * absent or stopped box, just discovered one step later, so it gets the same\n * answer instead of escaping as a 500.\n *\n * Matched STRUCTURALLY, not with `instanceof`: importing the SDK's error class\n * would make `@tangle-network/sandbox` a hard dependency of a route factory\n * whose entire `fs` seam is structural (`SandboxFileTreeSource`), and would\n * break any host feeding it a non-SDK handle.\n *\n * Deliberately narrow — the error code, `ENOENT`, the ENOENT message text, AND\n * the failing syscall's own operand all have to line up. A permission error, a\n * timeout, an auth failure, or an ENOENT on some other path inside the tree is\n * a real failure and still surfaces.\n *\n * The operand is matched as the quoted `lstat '<root>'` clause rather than by\n * substring, because the root is a PREFIX of everything under it: a plain\n * `includes(root)` would also swallow an ENOENT on `<root>/gone/x.md`, and on\n * a prefix sibling like `/home/agent-old/...`.\n */\nfunction isMissingRootError(err: unknown, root: string): boolean {\n if (!(err instanceof Error)) return false\n if ((err as { code?: unknown }).code !== 'VALIDATION_ERROR') return false\n return (\n /ENOENT/.test(err.message) &&\n /no such file or directory/.test(err.message) &&\n new RegExp(`\\\\blstat '${escapeRegExp(root)}'`).test(err.message)\n )\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Resolve a sandbox file index route with authorization, caching, and configurable depth and entries limits */\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json(cached)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n\n let scan: SandboxTreeResult\n try {\n scan = await auth.fs.tree(auth.root, { maxDepth })\n } catch (err) {\n if (!isMissingRootError(err, auth.root)) throw err\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(auth.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(auth.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}
@@ -1093,7 +1093,6 @@ function buildProductEgressPolicy(publicOrigin, extraDomains = []) {
1093
1093
  includeImplicitDomains: false
1094
1094
  };
1095
1095
  }
1096
- var DEFAULT_SIDECAR_PROCESS_PATTERN = "opencode|claude|codex";
1097
1096
  var DEFAULT_SANDBOX_RESOURCES = {
1098
1097
  image: "universal",
1099
1098
  cpuCores: 2,
@@ -1590,39 +1589,16 @@ function assertEnvWithinLimits(env) {
1590
1589
  );
1591
1590
  }
1592
1591
  }
1593
- function resolveSidecarProcessPattern(probe, harness) {
1594
- const pattern = probe.sidecarProcessPattern?.(harness) ?? DEFAULT_SIDECAR_PROCESS_PATTERN;
1595
- if (pattern.includes("\\|")) {
1596
- throw new Error(
1597
- 'Invalid livenessProbe.sidecarProcessPattern: pgrep -f uses extended regular expressions; use a bare "|" for alternation, not "\\|".'
1598
- );
1599
- }
1600
- return pattern;
1601
- }
1602
- async function isBoxAlive(box, harness, probe) {
1603
- if (!probe) return true;
1604
- const execTimeout = probe.execTimeoutMs ?? 5e3;
1605
- const psTimeout = probe.psTimeoutMs ?? 3e3;
1606
- const pattern = resolveSidecarProcessPattern(probe, harness);
1607
- const race = (p, ms, label) => Promise.race([
1608
- p,
1609
- new Promise((_, reject) => setTimeout(() => reject(new Error(label)), ms))
1610
- ]);
1592
+ async function isBoxAlive(box, probe) {
1593
+ if (!probe) return ok(void 0);
1611
1594
  try {
1612
- const alive = await race(box.exec("echo alive"), execTimeout, "alive check timeout");
1613
- if (!alive.stdout.includes("alive")) return false;
1614
- try {
1615
- const ps = await race(
1616
- box.exec(`pgrep -f ${shellSingleQuote(pattern)} || echo no-sidecar`),
1617
- psTimeout,
1618
- "ps check timeout"
1619
- );
1620
- if (ps.stdout.includes("no-sidecar")) return false;
1621
- } catch {
1595
+ const alive = await box.exec("echo alive", { timeoutMs: probe.execTimeoutMs ?? 5e3 });
1596
+ if (alive.exitCode !== 0 || alive.stdout.trim() !== "alive") {
1597
+ return fail(new Error("alive check did not return a successful alive marker"));
1622
1598
  }
1623
- return true;
1624
- } catch {
1625
- return false;
1599
+ return ok(void 0);
1600
+ } catch (cause) {
1601
+ return fail(new Error("alive check failed", { cause }));
1626
1602
  }
1627
1603
  }
1628
1604
  var DEFAULT_LIVENESS_CACHE_TTL_MS = 5e3;
@@ -1755,15 +1731,15 @@ async function writeDeferredFilesWithRuntimeAuthRefresh(client, box, files, stag
1755
1731
  )
1756
1732
  );
1757
1733
  }
1758
- async function isReusableBox(box, harness, probe) {
1734
+ async function isReusableBox(box, probe) {
1759
1735
  if (sandboxEdgeFailed(box) || !sandboxRuntimeUrl(box)) {
1760
1736
  livenessVerifiedAt.delete(box.id);
1761
- return false;
1737
+ return fail(new Error(sandboxEdgeFailed(box) ? "runtime edge readiness failed" : "runtime URL is missing"));
1762
1738
  }
1763
- if (!probe) return true;
1764
- if (hasRecentLivenessVerification(box, probe)) return true;
1765
- const alive = await isBoxAlive(box, harness, probe);
1766
- if (alive) livenessVerifiedAt.set(box.id, Date.now());
1739
+ if (!probe) return ok(void 0);
1740
+ if (hasRecentLivenessVerification(box, probe)) return ok(void 0);
1741
+ const alive = await isBoxAlive(box, probe);
1742
+ if (alive.succeeded) livenessVerifiedAt.set(box.id, Date.now());
1767
1743
  else livenessVerifiedAt.delete(box.id);
1768
1744
  return alive;
1769
1745
  }
@@ -1794,7 +1770,7 @@ async function resumeStoppedBox(box, timeoutMs, onProgress) {
1794
1770
  return fail(stoppedBoxResumeError(box, cause));
1795
1771
  }
1796
1772
  }
1797
- async function recoverUnresponsiveBox(client, box, harness, probe, stage, name, resumeTimeout, onProgress) {
1773
+ async function recoverUnresponsiveBox(client, box, probe, stage, name, resumeTimeout, onProgress) {
1798
1774
  try {
1799
1775
  await box.stop();
1800
1776
  } catch (err) {
@@ -1817,12 +1793,14 @@ async function recoverUnresponsiveBox(client, box, harness, probe, stage, name,
1817
1793
  );
1818
1794
  }
1819
1795
  const recovered = await refreshRuntimeConnection(client, resumed.value);
1820
- if (!await isReusableBox(recovered, harness, probe)) {
1796
+ const reusable = await isReusableBox(recovered, probe);
1797
+ if (!reusable.succeeded) {
1821
1798
  throw new SandboxRecoveryFailedError(
1822
1799
  stage,
1823
1800
  name,
1824
1801
  "probe",
1825
- "the box is still unresponsive after a state-preserving restart"
1802
+ `the box is still unresponsive after a state-preserving restart: ${reusable.error.message}`,
1803
+ reusable.error
1826
1804
  );
1827
1805
  }
1828
1806
  return recovered;
@@ -1993,6 +1971,9 @@ async function requestSandboxReplacement(recover, failure, label) {
1993
1971
  return { ...recovery.value, replacementBoxKey };
1994
1972
  }
1995
1973
  async function requestMissingSandboxReplacement(shell, failure) {
1974
+ if (failure.error instanceof SandboxRecoveryFailedError && failure.error.phase === "probe") {
1975
+ throw failure.error;
1976
+ }
1996
1977
  const diagnostics = serializeSandboxProvisioningError(failure.error);
1997
1978
  if (!isSandboxApiSandboxMissingFailure(diagnostics)) throw failure.error;
1998
1979
  return requestSandboxReplacement(
@@ -2002,6 +1983,10 @@ async function requestMissingSandboxReplacement(shell, failure) {
2002
1983
  );
2003
1984
  }
2004
1985
  async function provisionWorkspaceSandbox(shell, options) {
1986
+ const execTimeoutMs = shell.livenessProbe?.execTimeoutMs;
1987
+ if (execTimeoutMs !== void 0 && (!Number.isInteger(execTimeoutMs) || execTimeoutMs < 100 || execTimeoutMs > 6e5)) {
1988
+ throw new Error("livenessProbe.execTimeoutMs must be an integer between 100 and 600000");
1989
+ }
2005
1990
  const { workspaceId, userId, harness, forceNew, onProgress, billingOwnerId } = options;
2006
1991
  const resolved = await resolveWorkspaceSandboxClient(shell, workspaceId, userId);
2007
1992
  const { scope, client } = resolved;
@@ -2021,13 +2006,12 @@ async function provisionWorkspaceSandbox(shell, options) {
2021
2006
  } else if (found.metadata?.harness === harness) {
2022
2007
  try {
2023
2008
  const ready = await refreshRuntimeConnection(client, found);
2024
- if (await isReusableBox(ready, harness, shell.livenessProbe)) {
2009
+ if ((await isReusableBox(ready, shell.livenessProbe)).succeeded) {
2025
2010
  return await finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope);
2026
2011
  }
2027
2012
  const recovered = await recoverUnresponsiveBox(
2028
2013
  client,
2029
2014
  ready,
2030
- harness,
2031
2015
  shell.livenessProbe,
2032
2016
  "reused",
2033
2017
  name,
@@ -2074,13 +2058,12 @@ async function provisionWorkspaceSandbox(shell, options) {
2074
2058
  } else {
2075
2059
  try {
2076
2060
  const box2 = await refreshRuntimeConnection(client, resumed.value);
2077
- if (await isReusableBox(box2, harness, shell.livenessProbe)) {
2061
+ if ((await isReusableBox(box2, shell.livenessProbe)).succeeded) {
2078
2062
  return await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope);
2079
2063
  }
2080
2064
  const recovered = await recoverUnresponsiveBox(
2081
2065
  client,
2082
2066
  box2,
2083
- harness,
2084
2067
  shell.livenessProbe,
2085
2068
  "resumed",
2086
2069
  name,
@@ -2785,7 +2768,6 @@ export {
2785
2768
  resolveSandboxClientCredentials,
2786
2769
  PYPI_EGRESS_DOMAINS,
2787
2770
  buildProductEgressPolicy,
2788
- DEFAULT_SIDECAR_PROCESS_PATTERN,
2789
2771
  DEFAULT_SANDBOX_RESOURCES,
2790
2772
  getClient,
2791
2773
  resetClientCache,
@@ -2834,4 +2816,4 @@ export {
2834
2816
  isTerminalPromptEvent,
2835
2817
  detectInteractiveQuestion
2836
2818
  };
2837
- //# sourceMappingURL=chunk-WSRECTKA.js.map
2819
+ //# sourceMappingURL=chunk-P6YXUGNA.js.map