aiden-runtime 4.1.5 → 4.6.0

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 (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -67,6 +67,32 @@ function renderCapabilityCard(data, colorize) {
67
67
  // Compose the inner rows that boxSharp will wrap. Each row is the
68
68
  // CONTENT (no border) — boxSharp adds the side borders + padding.
69
69
  const rows = [];
70
+ // v4.2 Phase 3 — optional "what happened" one-liner above the
71
+ // canStill section. Rendered as a muted-tone line so it reads as
72
+ // context, not action. Skipped cleanly when absent → v4.1.3
73
+ // capability-card behaviour preserved for non-Phase-3 callers.
74
+ if (data.whatHappened) {
75
+ rows.push('');
76
+ rows.push(colorize(truncToContent(data.whatHappened), 'muted'));
77
+ }
78
+ // v4.2 Phase 3 — optional failure-category pill row. Each entry
79
+ // renders as `<category>(<count>)` separated by " · " bullets.
80
+ // Pre-sorted by the generator (desc count then category priority);
81
+ // renderer just formats. Skipped cleanly when absent.
82
+ if (data.failuresByCategory && data.failuresByCategory.length > 0) {
83
+ const pills = data.failuresByCategory
84
+ .map((p) => `${p.category}(${p.count})`)
85
+ .join(' · ');
86
+ const label = colorize('Failures:', 'error');
87
+ rows.push(`${label} ${truncToContent(pills)}`);
88
+ }
89
+ // v4.3 Phase 5 — optional one-line browser context summary
90
+ // (active tab, blocker kind, other-tab count, stale-ref retries).
91
+ // Already pre-formatted by recoveryReport's `buildBrowserContextLine`.
92
+ // Rendered as a muted-tone line below the Failures: row when present.
93
+ if (data.browserContext) {
94
+ rows.push(colorize(truncToContent(data.browserContext), 'muted'));
95
+ }
70
96
  if (data.canStill.length > 0) {
71
97
  rows.push('');
72
98
  rows.push(heading('Can still:'));
@@ -523,14 +523,21 @@ class Display {
523
523
  return (0, box_1.truncateVisible)(text, INTERIOR);
524
524
  return text + ' '.repeat(INTERIOR - v);
525
525
  };
526
+ // v4.5 TUI polish — add a leading + trailing blank line so the
527
+ // box has visual breathing room from the lines above/below, and
528
+ // a trailing interior blank so contact info doesn't crowd the
529
+ // bottom border.
526
530
  return [
531
+ '',
527
532
  lidIndent + lid,
528
533
  wallIndent + pipe + ' '.repeat(INTERIOR) + pipe,
529
534
  wallIndent + pipe + padInner(` ${heart} ${val('Built solo')}`) + pipe,
530
535
  wallIndent + pipe + padInner(` ${lab('GitHub:')} ${val('github.com/taracodlabs/aiden')}`) + pipe,
531
536
  wallIndent + pipe + padInner(` ${lab('Web:')} ${val('aiden.taracod.com')}`) + pipe,
532
537
  wallIndent + pipe + padInner(` ${lab('Contact:')} ${val('contact@taracod.com')}`) + pipe,
538
+ wallIndent + pipe + ' '.repeat(INTERIOR) + pipe,
533
539
  wallIndent + pipe + lid + pipe,
540
+ '',
534
541
  ].join('\n');
535
542
  }
536
543
  /**
@@ -929,22 +936,25 @@ class Display {
929
936
  }
930
937
  };
931
938
  const eraseLine = () => {
932
- // Walk up to the indicator's row(s) + erase. Cursor lands at
933
- // col 0 of the (now empty) verb row. NO trailing newline here:
934
- // the caller is about to write content on this row, and
935
- // whatever they write will include their own `\n` to flush
936
- // the buffer. If we emitted `\n` here, we'd leave a phantom
937
- // blank row before the caller's content.
939
+ // Walk up to the indicator's row(s) + erase, then drop ONE
940
+ // newline so the cursor lands on a blank line BELOW the
941
+ // indicator's old footprint. v4.1.6 polish: previous behavior
942
+ // left the cursor at col 0 of the just-erased row so caller
943
+ // writes (agentHeader, tool row, etc.) sat tight against where
944
+ // the indicator had been. v4.1.5 visual smoke flagged the
945
+ // wave-bar→`┃ Aiden` proximity as feeling cramped. The
946
+ // trailing `\n` gains one visible blank row of breathing space
947
+ // AND adds another Windows ConPTY flush trigger (Issue M).
938
948
  //
939
949
  // v4.1.5 Issue K — with wave bar enabled, walk up 2 rows (two
940
950
  // up-1+erase sequences). Without the bar, walk up 1 row.
941
951
  if (!isTty || !printed)
942
952
  return;
943
953
  if (waveBarEnabled) {
944
- out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}`);
954
+ out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}\n`);
945
955
  }
946
956
  else {
947
- out.write(ANSI_UP_ERASE);
957
+ out.write(`${ANSI_UP_ERASE}\n`);
948
958
  }
949
959
  };
950
960
  // Initial paint — only on TTY. Indicator + `\n` so the buffer
@@ -43,32 +43,38 @@ function paint(kind) {
43
43
  return (text) => (0, skinEngine_1.getSkinEngine)().applyColors(text, kind);
44
44
  }
45
45
  /**
46
- * v4.1.3-essentials: bold (`**foo**`) markdown emphasis renders as
47
- * ANSI bold + underline. Previously painted 'brand' (orange) which
48
- * collided with the heading hierarchy. Briefly tried bold + bright-
49
- * white; landed on bold + underline because underline carries
50
- * emphasis without consuming a color slotthe palette stays
51
- * available for state semantics (yellow=degraded, red=error, etc.).
46
+ * v4.1.3-essentials → v4.5 TUI polish: bold (`**foo**`) markdown
47
+ * emphasis renders as plain ANSI bold. Earlier iterations tried
48
+ * 'brand' (orange, collided with heading hierarchy), bright-white
49
+ * (low contrast on dark themes), and bold+underline (made bulleted
50
+ * list items look like clickable hyperlinksuser feedback after
51
+ * v4.5 Phase 8 stabilisation).
52
52
  *
53
- * ANSI sequence: `\x1b[1m\x1b[4m{text}\x1b[24m\x1b[22m` bold ON +
54
- * underline ON, then underline OFF + bold OFF. Reset order matters
55
- * (underline first, bold second) so the closing codes don't reorder
56
- * styles surprisingly on terminals that batch SGR updates.
53
+ * Landed on bold-only: weight carries emphasis, no color slot
54
+ * consumed, and no underline confusion with terminal URL/path
55
+ * auto-highlight features.
57
56
  *
58
- * Bypasses the skin system intentionally bold-as-underline is an
59
- * opinionated default for this slice. Same caveat as the prior bold-
60
- * as-color iteration: nested markdown loses the outer style after
61
- * close (pre-existing limitation of the painter-stack architecture).
57
+ * ANSI sequence: `\x1b[1m{text}\x1b[22m` bold ON, bold OFF.
58
+ *
59
+ * Bypasses the skin system intentionally emphasis is an
60
+ * opinionated default for this slice. Same caveat as the prior
61
+ * bold-as-color iteration: nested markdown loses the outer style
62
+ * after close (pre-existing limitation of the painter-stack
63
+ * architecture).
62
64
  *
63
65
  * Honors `NO_COLOR=1` per the standard (skips the wrap entirely).
64
- * Strictly speaking `NO_COLOR` is about color and underline isn't
65
- * a color, but the wrap still emits ANSI escapes; honoring the env
66
- * var keeps output paste-safe in scripted contexts.
66
+ * Strictly speaking `NO_COLOR` is about color, but the wrap still
67
+ * emits ANSI escapes; honoring the env var keeps output paste-safe
68
+ * in scripted contexts.
69
+ *
70
+ * Function name: `paintEmphasis` rather than `paintBold` because
71
+ * the latter is already taken by a different (parameterised) helper
72
+ * below.
67
73
  */
68
- function paintBoldUnderline(text) {
74
+ function paintEmphasis(text) {
69
75
  if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
70
76
  return text;
71
- return `\x1b[1m\x1b[4m${text}\x1b[24m\x1b[22m`;
77
+ return `\x1b[1m${text}\x1b[22m`;
72
78
  }
73
79
  /**
74
80
  * v4.1.3-essentials reply-polish: bold-on + skin paint + bold-off.
@@ -367,10 +373,12 @@ function getReplyRenderer() {
367
373
  hr: () => paint('muted')('─'.repeat((0, frame_1.getBodyWidth)())) + '\n',
368
374
  listitem: renderListItem,
369
375
  paragraph: (text) => `${text}\n\n`,
370
- // v4.1.3-essentials: bold renders as ANSI bold + underline
371
- // (was 'brand' / orange, then bright-white; landed on underline
372
- // so the color palette stays available for state semantics).
373
- strong: paintBoldUnderline,
376
+ // v4.1.3-essentials → v4.5 TUI polish: bold renders as plain ANSI
377
+ // bold. Earlier iterations tried orange (collision with headings),
378
+ // bright-white (low contrast), and bold+underline (made bulleted
379
+ // **labels** look like clickable links — user feedback after v4.5
380
+ // Phase 8 stabilisation). Weight alone carries emphasis.
381
+ strong: paintEmphasis,
374
382
  em: paint('muted'),
375
383
  // v4.1.3-essentials reply-polish: inline `` `code` `` — strip
376
384
  // the literal backticks (used to leak into the visible output)
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/updateBootPrompt.ts — v4.5 update system.
10
+ *
11
+ * Boxed three-option prompt rendered after the boot card / status
12
+ * pills, before the bottomPromptHint (Q-U5(b) position). When an
13
+ * update is available AND not skipped, the user sees:
14
+ *
15
+ * ┌─────────────────────────────────────────────────────────┐
16
+ * │ ◆ Aiden 4.5.1 available (you're on 4.5.0) │
17
+ * │ │
18
+ * │ What's new: bug fix for IMAP reconnect on Windows │
19
+ * │ │
20
+ * │ Update now? (y/n/later) │
21
+ * │ y — update now, restart after │
22
+ * │ n — skip this version (don't ask again) │
23
+ * │ later — remind me next session │
24
+ * └─────────────────────────────────────────────────────────┘
25
+ *
26
+ * Behavior (Q-U2(a)):
27
+ * - 5-second timeout defaults to 'later' (no state change)
28
+ * - 'y' triggers `executeInstall` via the method-aware dispatch
29
+ * - 'n' persists `skippedVersion = status.latest` to the cache
30
+ * - 'later' is a no-op (re-prompt next session)
31
+ *
32
+ * Display sink: writes via the supplied `display`. Keypress capture
33
+ * uses raw stdin mode so the user can press a single key — no Enter
34
+ * needed for y/n. 'later' is the timeout default; an explicit 'l'
35
+ * keypress also maps to it.
36
+ *
37
+ * Designed to be skippable: if stdin isn't a TTY (CI / piped /
38
+ * non-interactive), short-circuits to 'later' immediately so boot
39
+ * doesn't hang.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.renderBootUpdateBox = renderBootUpdateBox;
43
+ exports.showBootUpdatePrompt = showBootUpdatePrompt;
44
+ const DEFAULT_TIMEOUT_MS = 5000;
45
+ /**
46
+ * Pure box renderer — public so tests can assert the rendered shape
47
+ * without driving stdin.
48
+ */
49
+ function renderBootUpdateBox(status, method) {
50
+ const innerWidth = 60;
51
+ const pad = (s) => {
52
+ const visible = s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
53
+ const len = visible.length;
54
+ if (len >= innerWidth)
55
+ return s.slice(0, innerWidth);
56
+ return s + ' '.repeat(innerWidth - len);
57
+ };
58
+ const top = '┌' + '─'.repeat(innerWidth) + '┐';
59
+ const bottom = '└' + '─'.repeat(innerWidth) + '┘';
60
+ const blank = '│' + ' '.repeat(innerWidth) + '│';
61
+ const lines = [];
62
+ lines.push(top);
63
+ lines.push(blank);
64
+ lines.push('│' + pad(` ◆ Aiden ${status.latest} available (you're on ${status.installed})`) + '│');
65
+ if (status.releaseNotes && status.releaseNotes.length > 0) {
66
+ lines.push(blank);
67
+ lines.push('│' + pad(` What's new: ${status.releaseNotes}`) + '│');
68
+ }
69
+ lines.push(blank);
70
+ lines.push('│' + pad(` Update now? (y/n/later)`) + '│');
71
+ lines.push('│' + pad(` y — update via ${method.method}`) + '│');
72
+ lines.push('│' + pad(` n — skip ${status.latest} (don't ask again)`) + '│');
73
+ lines.push('│' + pad(` later — remind me next session (default in 5s)`) + '│');
74
+ lines.push(blank);
75
+ lines.push(bottom);
76
+ return lines;
77
+ }
78
+ /**
79
+ * Show the prompt and resolve with the user's choice. Never throws
80
+ * — returns 'later' on any error / timeout / non-TTY.
81
+ */
82
+ async function showBootUpdatePrompt(input) {
83
+ // Test seam — short-circuit before any I/O.
84
+ if (input._testChoice)
85
+ return input._testChoice;
86
+ const isTTY = input.isTTY ?? Boolean(input.stdin?.isTTY ?? process.stdin.isTTY);
87
+ // Non-interactive stdin → silently default to 'later'. Boot must
88
+ // not hang in CI / piped contexts.
89
+ if (!isTTY)
90
+ return 'later';
91
+ if (!input.status.updateAvailable || !input.status.latest)
92
+ return 'later';
93
+ if (input.status.skipped)
94
+ return 'later';
95
+ // Render the box.
96
+ for (const line of renderBootUpdateBox(input.status, input.method)) {
97
+ input.display.write(line + '\n');
98
+ }
99
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
100
+ const stdin = input.stdin ?? process.stdin;
101
+ return await captureSingleKey(stdin, timeoutMs);
102
+ }
103
+ /**
104
+ * Read ONE keypress from stdin in raw mode. Maps:
105
+ * - 'y' / 'Y' → 'install'
106
+ * - 'n' / 'N' → 'skip'
107
+ * - 'l' / 'L' / Enter / any other key → 'later'
108
+ * - Timeout → 'later'
109
+ *
110
+ * Restores stdin's prior pause/resume + rawMode state so the
111
+ * subsequent REPL prompt isn't broken.
112
+ */
113
+ function captureSingleKey(stdin, timeoutMs) {
114
+ return new Promise((resolve) => {
115
+ let done = false;
116
+ const wasRaw = stdin.isRaw === true;
117
+ const wasPaused = stdin.isPaused();
118
+ const cleanup = () => {
119
+ if (done)
120
+ return;
121
+ done = true;
122
+ try {
123
+ stdin.removeListener('data', onData);
124
+ }
125
+ catch { /* noop */ }
126
+ try {
127
+ if (!wasRaw && stdin.setRawMode)
128
+ stdin.setRawMode(false);
129
+ }
130
+ catch { /* noop */ }
131
+ if (wasPaused)
132
+ try {
133
+ stdin.pause();
134
+ }
135
+ catch { /* noop */ }
136
+ clearTimeout(timer);
137
+ };
138
+ const onData = (chunk) => {
139
+ const raw = chunk.toString();
140
+ const ch = raw.length > 0 ? raw[0].toLowerCase() : '';
141
+ let choice;
142
+ if (ch === 'y')
143
+ choice = 'install';
144
+ else if (ch === 'n')
145
+ choice = 'skip';
146
+ else
147
+ choice = 'later';
148
+ cleanup();
149
+ resolve(choice);
150
+ };
151
+ try {
152
+ if (stdin.setRawMode && !wasRaw)
153
+ stdin.setRawMode(true);
154
+ if (wasPaused)
155
+ stdin.resume();
156
+ stdin.on('data', onData);
157
+ }
158
+ catch {
159
+ cleanup();
160
+ resolve('later');
161
+ return;
162
+ }
163
+ const timer = setTimeout(() => {
164
+ cleanup();
165
+ resolve('later');
166
+ }, timeoutMs);
167
+ if (typeof timer.unref === 'function')
168
+ timer.unref();
169
+ });
170
+ }
@@ -58,11 +58,14 @@ exports.pwClickFirstResult = pwClickFirstResult;
58
58
  exports.pwType = pwType;
59
59
  exports.pwScroll = pwScroll;
60
60
  exports.pwSnapshot = pwSnapshot;
61
+ exports.pwSnapshotHash = pwSnapshotHash;
62
+ exports.pwSnapshotTabs = pwSnapshotTabs;
61
63
  exports.pwGetUrl = pwGetUrl;
62
64
  exports.pwClose = pwClose;
63
65
  exports.getActiveBrowserPage = getActiveBrowserPage;
64
66
  const path_1 = __importDefault(require("path"));
65
67
  const fs_1 = __importDefault(require("fs"));
68
+ const crypto_1 = __importDefault(require("crypto"));
66
69
  const paths_1 = require("./paths");
67
70
  // ── Lazy-import Playwright so the server boots even if playwright
68
71
  // is not installed (tools will return a clear error message).
@@ -410,6 +413,132 @@ async function pwSnapshot() {
410
413
  return { ok: false, error: e.message };
411
414
  }
412
415
  }
416
+ /**
417
+ * v4.3 Phase 1 — structured page-state snapshot used by the BrowserState
418
+ * observer. Captures URL + title + body-text hash + recursive iframe-tree
419
+ * hash in a single in-page evaluate. Truncates body innerText to 5 000
420
+ * chars before hashing so cost stays bounded for large pages.
421
+ *
422
+ * Cross-origin iframe srcs are surfaced (URL is visible); attempting to
423
+ * read `iframe.contentDocument` on a cross-origin frame throws — the
424
+ * recursive walker catches and skips, recording only the iframe's src.
425
+ *
426
+ * Returns `ok: false` when the browser is closed or evaluate fails.
427
+ * Caller (BrowserState.captureState) treats `ok: false` as "snapshot
428
+ * unavailable, embed no sidecar this call".
429
+ */
430
+ async function pwSnapshotHash() {
431
+ try {
432
+ const page = await ensurePage();
433
+ const url = page.url();
434
+ const title = await page.title();
435
+ // eslint-disable-next-line no-undef
436
+ const data = await page.evaluate(() => {
437
+ const doc = globalThis.document;
438
+ const text = (doc?.body?.innerText ?? '');
439
+ // Recursive iframe URL walk. Cross-origin iframes throw on
440
+ // contentDocument access — catch and record just the src.
441
+ const urls = [];
442
+ function walk(d) {
443
+ try {
444
+ const iframes = Array.from(d.querySelectorAll('iframe'));
445
+ for (const f of iframes) {
446
+ urls.push(String(f.src ?? ''));
447
+ try {
448
+ if (f.contentDocument)
449
+ walk(f.contentDocument);
450
+ }
451
+ catch { /* cross-origin */ }
452
+ }
453
+ }
454
+ catch { /* defensive */ }
455
+ }
456
+ walk(doc);
457
+ return { text, frame_urls: urls.join('|') };
458
+ });
459
+ const dom_text_hash = crypto_1.default.createHash('sha256').update(data.text.slice(0, 5000)).digest('hex');
460
+ const frame_tree_hash = crypto_1.default.createHash('sha256').update(data.frame_urls).digest('hex');
461
+ return { ok: true, url, title, dom_text_hash, frame_tree_hash };
462
+ }
463
+ catch (e) {
464
+ return { ok: false, error: e.message };
465
+ }
466
+ }
467
+ // ── v4.3 Phase 4 — multi-tab snapshot ─────────────────────────────────────
468
+ //
469
+ // Persistent context can hold many pages (target=_blank, window.open,
470
+ // CDP popups). Aiden's tools still target `_activePage` only — Phase 4
471
+ // is DATA-ONLY: it records what tabs exist, who opened whom, and which
472
+ // is the active one, so v4.4+ cross-tab orchestration has a foundation.
473
+ //
474
+ // `_tabIdMap` is a WeakMap that assigns each observed Page a stable
475
+ // `tab_id` (`tab-1`, `tab-2`, ...). When a Page closes, Playwright drops
476
+ // its reference and the WeakMap entry GCs naturally — no manual cleanup.
477
+ const _tabIdMap = new WeakMap();
478
+ let _nextTabIdCounter = 0;
479
+ function getOrAssignTabId(page) {
480
+ let id = _tabIdMap.get(page);
481
+ if (!id) {
482
+ _nextTabIdCounter += 1;
483
+ id = `tab-${_nextTabIdCounter}`;
484
+ _tabIdMap.set(page, id);
485
+ }
486
+ return id;
487
+ }
488
+ /**
489
+ * v4.3 Phase 4 — enumerate all pages in the persistent context and
490
+ * return their wire-data form. Stable `tab_id` per Page via the
491
+ * `_tabIdMap` WeakMap. The opener Page is looked up the same way, so
492
+ * `opener_id` is stable across reconciliations as long as the parent
493
+ * still exists.
494
+ *
495
+ * Cheap — `context.pages()` is in-process; the per-page work is one
496
+ * `url()` getter and one async `title()` call. Total cost scales
497
+ * linearly with the number of tabs; for typical sessions (1-5 tabs)
498
+ * it's well under 50ms.
499
+ *
500
+ * Returns `ok: false` when the browser is closed. Caller (BrowserState
501
+ * .reconcileTabs) treats `ok: false` as "no reconciliation this cycle".
502
+ */
503
+ async function pwSnapshotTabs() {
504
+ try {
505
+ const ctx = await ensureContext();
506
+ const pages = ctx.pages();
507
+ const tabs = [];
508
+ for (const p of pages) {
509
+ // Skip pages already closed mid-walk (rare race).
510
+ if (typeof p.isClosed === 'function' && p.isClosed())
511
+ continue;
512
+ const tab_id = getOrAssignTabId(p);
513
+ const url = (typeof p.url === 'function') ? p.url() : '';
514
+ // title() can throw if the page navigated mid-walk; default to empty.
515
+ let title = '';
516
+ try {
517
+ title = await p.title();
518
+ }
519
+ catch { /* defensive */ }
520
+ // opener() returns the parent Page (or null). Same WeakMap lookup.
521
+ let opener_id = null;
522
+ try {
523
+ const opener = typeof p.opener === 'function' ? await p.opener() : null;
524
+ if (opener)
525
+ opener_id = _tabIdMap.get(opener) ?? null;
526
+ }
527
+ catch { /* defensive */ }
528
+ tabs.push({
529
+ tab_id,
530
+ url,
531
+ title,
532
+ is_active: p === _activePage,
533
+ opener_id,
534
+ });
535
+ }
536
+ return { ok: true, tabs };
537
+ }
538
+ catch (e) {
539
+ return { ok: false, error: e.message };
540
+ }
541
+ }
413
542
  /** Return the URL currently loaded in the active browser page. */
414
543
  async function pwGetUrl() {
415
544
  try {