aiden-runtime 4.1.5 → 4.5.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 (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -19,6 +19,7 @@
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.CliCallbacks = void 0;
22
+ exports.mapBlockerToCard = mapBlockerToCard;
22
23
  exports.renderApprovalBox = renderApprovalBox;
23
24
  const box_1 = require("./box");
24
25
  async function defaultPrompts() {
@@ -45,6 +46,68 @@ const DECISION_CHOICES = [
45
46
  { name: 'Deny', value: 'deny' },
46
47
  ];
47
48
  const KNOWN_TIERS = new Set(['safe', 'caution', 'dangerous']);
49
+ // ── v4.3 Phase 3 — manual-blocker card mapping ────────────────────────────
50
+ /** Best-effort hostname extraction; falls back to the raw URL. */
51
+ function blockerHostname(url) {
52
+ try {
53
+ return new URL(url).hostname || url;
54
+ }
55
+ catch {
56
+ return url;
57
+ }
58
+ }
59
+ /**
60
+ * Map a BlockerSurface to the existing CapabilityCardData chrome.
61
+ * Pure helper — same shape per `BlockerKind`, parameterised by the
62
+ * blocker's URL + optional subtype for the body text. The renderer
63
+ * (`display.capabilityCard`) handles all rendering chrome; this
64
+ * function only fills the slots semantically.
65
+ *
66
+ * Public for unit tests; chatSession + callbacks consume via the
67
+ * `renderBlockerCardIfPresent` method below.
68
+ */
69
+ function mapBlockerToCard(blocker) {
70
+ const host = blockerHostname(blocker.url);
71
+ const labels = {
72
+ captcha: {
73
+ title: `CAPTCHA challenge at ${host}`,
74
+ canStill: ['Solve the challenge in the browser tab', 'Cancel this task'],
75
+ cannot: ['Continue automatically without your action'],
76
+ fix: `I'll wait — solve the ${blocker.subtype ?? 'CAPTCHA'} challenge and tell me when ready.`,
77
+ },
78
+ login: {
79
+ title: `Sign-in required at ${host}`,
80
+ canStill: ['Sign in via the browser tab', 'Cancel this task'],
81
+ cannot: ['Continue without authentication'],
82
+ fix: `I'll wait — sign in and let me know when done.`,
83
+ },
84
+ '2fa': {
85
+ title: `Two-factor code required at ${host}`,
86
+ canStill: ['Enter the 2FA code in the browser tab', 'Cancel this task'],
87
+ cannot: ['Continue without the verification code'],
88
+ fix: `I'll wait — enter your code and tell me when complete.`,
89
+ },
90
+ verification: {
91
+ title: `Identity verification at ${host}`,
92
+ canStill: ['Complete the verification in the browser', 'Cancel this task'],
93
+ cannot: ['Continue without verification'],
94
+ fix: `I'll wait — finish the verification and tell me when done.`,
95
+ },
96
+ consent: {
97
+ title: `Consent banner at ${host}`,
98
+ canStill: ['Dismiss the banner in the browser', 'Continue if banner is dismissable'],
99
+ cannot: ['Reliably interact with content while the banner blocks it'],
100
+ fix: `Dismiss the cookie or privacy banner and I'll retry.`,
101
+ },
102
+ };
103
+ const t = labels[blocker.kind];
104
+ return {
105
+ title: `🛑 ${t.title}`,
106
+ canStill: t.canStill,
107
+ cannotReliably: t.cannot,
108
+ fix: t.fix,
109
+ };
110
+ }
48
111
  function parseRiskTier(content) {
49
112
  const head = content.trim().toLowerCase().split(/\s+/)[0] ?? '';
50
113
  // Strip punctuation that might bleed in from JSON: "safe.", "caution,"...
@@ -161,6 +224,17 @@ class CliCallbacks {
161
224
  catch { /* defensive */ }
162
225
  return;
163
226
  }
227
+ // v4.3 Phase 3 — render a structured "agent needs human help"
228
+ // card when the browser observer detected a manual blocker
229
+ // (CAPTCHA / login / 2FA / verification / consent). Renders for
230
+ // ALL trail-row outcomes below — the blocker is independent of
231
+ // the tool's own success/fail signal. Inline placement gives
232
+ // the user immediate awareness; the model's next reply will
233
+ // explain in prose. Defensive: missing fields silently skip.
234
+ try {
235
+ this.renderBlockerCardIfPresent(result);
236
+ }
237
+ catch { /* defensive */ }
164
238
  // v4.1.4 reply-quality polish — Part 1.6. Helper used by ALL
165
239
  // outcome branches below so the activity indicator gets re-armed
166
240
  // for the gap that follows this tool (next tool, or final reply).
@@ -353,12 +427,84 @@ Reply with ONE word: safe, caution, or dangerous.`;
353
427
  const label = files.length > 0 ? files.join(', ') : 'none';
354
428
  this.display.dim(`[memory] refreshed system prompt (${label})`);
355
429
  };
430
+ /**
431
+ * v4.1.6 Polish 2 — post-turn skill-proposal handler.
432
+ *
433
+ * chatSession calls this AFTER `agentTurn` has rendered the agent's
434
+ * reply on screen. Internally:
435
+ * 1. Reuses `promptSkillProposal` (the existing inquirer modal)
436
+ * to ask the user "Save this as a reusable skill? Yes/No".
437
+ * 2. If accepted AND a SkillTeacher reference is wired, calls
438
+ * `skillTeacher.handleProposal({...skipPrompt})` to build
439
+ * the markdown + persist via skillManager.
440
+ * 3. Returns the result so chatSession can surface a confirmation
441
+ * line if needed.
442
+ *
443
+ * Decoupled from the agent's `runConversation` so the inquirer no
444
+ * longer fires mid-turn (the v4.1.5 visual-smoke regression).
445
+ *
446
+ * Defensive — exceptions in any step return `{created: false,
447
+ * reason: 'error'}` so a misbehaving prompt or save can't break
448
+ * the chat loop. The inquirer modal itself catches input-stream
449
+ * exceptions via the existing try/catch in promptSkillProposal.
450
+ */
451
+ this.handleSkillProposal = async (proposal) => {
452
+ // Step 1: ask the user (reuses existing modal).
453
+ let accept;
454
+ try {
455
+ accept = await this.promptSkillProposal(proposal);
456
+ }
457
+ catch {
458
+ return { created: false, reason: 'prompt_error' };
459
+ }
460
+ if (!accept) {
461
+ return { created: false, reason: 'declined' };
462
+ }
463
+ // Step 2: persist via SkillTeacher when available. Test harnesses
464
+ // that don't wire a teacher just get the prompt without the save.
465
+ if (!this.skillTeacher) {
466
+ return { created: false, reason: 'no_skill_teacher_wired' };
467
+ }
468
+ try {
469
+ // Pass `promptUser: undefined` to bypass the modal in
470
+ // `handleProposal` (we already showed it above). The teacher
471
+ // sees tier === 'tier_3_propose' WITHOUT a prompt callback,
472
+ // which it interprets as `no_prompt_callback` and skips the
473
+ // save — so we need to call the create branch directly.
474
+ //
475
+ // SkillTeacher's tier-4 (auto) branch creates without
476
+ // prompting; we reuse that path by passing a stub that always
477
+ // returns true (user already accepted above).
478
+ const result = await this.skillTeacher.handleProposal(proposal, {
479
+ promptUser: async () => true,
480
+ });
481
+ return result;
482
+ }
483
+ catch (err) {
484
+ return {
485
+ created: false,
486
+ reason: `create_failed: ${err instanceof Error ? err.message : String(err)}`,
487
+ };
488
+ }
489
+ };
356
490
  this.display = opts.display;
357
491
  this.auxiliaryClient = opts.auxiliaryClient;
358
492
  this.verboseMode = opts.verboseMode ?? 'normal';
359
493
  this.promptsPromise = opts.promptModule
360
494
  ? Promise.resolve(opts.promptModule)
361
495
  : defaultPrompts();
496
+ this.skillTeacher = opts.skillTeacher;
497
+ }
498
+ /**
499
+ * v4.1.6 Polish 2 — late-wire the SkillTeacher reference. aidenCLI
500
+ * constructs CliCallbacks early (the approval engine needs it
501
+ * stitched in), but SkillTeacher is built later in the boot
502
+ * sequence after skillLoader / skillManager are ready. Call this
503
+ * once after both exist so `handleSkillProposal` can persist
504
+ * accepted proposals.
505
+ */
506
+ setSkillTeacher(teacher) {
507
+ this.skillTeacher = teacher;
362
508
  }
363
509
  /** Update verbose mode at runtime (wired to /verbose). */
364
510
  setVerboseMode(mode) {
@@ -415,6 +561,30 @@ Reply with ONE word: safe, caution, or dangerous.`;
415
561
  this.toolTraceBeforeHook = opts.before;
416
562
  this.toolTraceAfterHook = opts.after;
417
563
  }
564
+ /**
565
+ * v4.3 Phase 3 — render a manual-blocker card when the browser
566
+ * observer detected a CAPTCHA / login / 2FA / verification /
567
+ * consent surface on the page. Reuses the existing capabilityCard
568
+ * chrome via a `mapBlockerToCard` semantic mapping — no new layout
569
+ * code, no new dedicated card component.
570
+ *
571
+ * Defensive: silently skips when the field shape doesn't match
572
+ * (no browserState, no blocker, unrecognised kind). Never throws
573
+ * — caller wraps in try/catch defensively.
574
+ *
575
+ * The blocker info is structural data emitted by the observer
576
+ * HOC (`tools/v4/browser/_observer.ts`); the renderer reads it
577
+ * from `result.result.browserState.blocker` after every browser
578
+ * tool call. Inline placement gives users immediate awareness
579
+ * before the model's next reply lands.
580
+ */
581
+ renderBlockerCardIfPresent(result) {
582
+ const inner = (result?.result ?? null);
583
+ const blocker = inner?.browserState?.blocker;
584
+ if (!blocker)
585
+ return;
586
+ this.display.capabilityCard(mapBlockerToCard(blocker));
587
+ }
418
588
  }
419
589
  exports.CliCallbacks = CliCallbacks;
420
590
  // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
@@ -872,6 +872,21 @@ class ChatSession {
872
872
  }
873
873
  }
874
874
  async runAgentTurn(userInput) {
875
+ // v4.5 Phase 8b — daemon-scheduling intent check on the user's
876
+ // initial message. Classifies regex hits like "every day at",
877
+ // "watch this folder", "when an email arrives" — and queues a
878
+ // tip to render at the END of the agent's response (so it
879
+ // doesn't crowd the agent's actual reply). Engine handles
880
+ // budget + dismissal.
881
+ let _deferredTip = null;
882
+ try {
883
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
884
+ const { getSuggestionEngine } = require('../../core/v4/suggestionEngine');
885
+ const t = getSuggestionEngine().checkInitialMessage(userInput);
886
+ if (t)
887
+ _deferredTip = t;
888
+ }
889
+ catch { /* defensive — never block a turn on a suggestion */ }
875
890
  // Phase 30.2.1 — explore mode: short-circuit BEFORE building the
876
891
  // turn-status spinner / agent call. The wizard skipped, so there's
877
892
  // no real provider to talk to. Print a friendly redirect to /setup
@@ -1178,9 +1193,25 @@ class ChatSession {
1178
1193
  if (result.toolCallTrace && result.toolCallTrace.length > 0) {
1179
1194
  this.sessionToolTrace.push(...result.toolCallTrace);
1180
1195
  }
1181
- // When streaming was active and emitted the final content already,
1182
- // skip the markdown re-render we'd otherwise duplicate text.
1183
- if (result.finalContent && !streamingActive) {
1196
+ // v4.1.6 spike (TCE) tool-loop terminal surface. When the
1197
+ // agent ended the turn via the recovery controller's surface
1198
+ // stage, render a structured-failure card instead of the
1199
+ // (empty) reply. Same chrome as auth / platform capability
1200
+ // cards — fits the established Aiden UX language for
1201
+ // "the action you wanted didn't happen, here's why and what
1202
+ // you can do." Surface BEFORE the tool→reply separator path
1203
+ // below because there's no agent reply to introduce.
1204
+ if (result.finishReason === 'tool_loop' && result.toolLoopCard) {
1205
+ // Emit the muted rule so the card visually separates from
1206
+ // the tool trail above it.
1207
+ emitToolReplySeparator();
1208
+ this.opts.display.capabilityCard(result.toolLoopCard);
1209
+ }
1210
+ else if (result.finalContent && !streamingActive) {
1211
+ // When streaming was active and emitted the final content
1212
+ // already, skip the markdown re-render — we'd otherwise
1213
+ // duplicate text.
1214
+ //
1184
1215
  // v4.1.5 Issue O — non-streaming reply path. Emit the muted
1185
1216
  // rule between the tool trail and the agent header before
1186
1217
  // the one-shot reply lands. Idempotent + tool-gated by
@@ -1188,6 +1219,30 @@ class ChatSession {
1188
1219
  emitToolReplySeparator();
1189
1220
  this.opts.display.write(this.opts.display.agentTurn(result.finalContent));
1190
1221
  }
1222
+ // v4.1.6 Polish 2 — post-render skill-proposal handler.
1223
+ // The agent loop now SKIPS the inquirer prompt when a
1224
+ // prompt callback is wired, surfacing the SkillProposal
1225
+ // here instead. We fire the prompt AFTER the agent reply
1226
+ // has rendered so the user sees the answer before being
1227
+ // asked "save this as a reusable skill?" — fixing the
1228
+ // v4.1.5 visual-smoke regression where the prompt fired
1229
+ // mid-turn and clobbered the reply.
1230
+ //
1231
+ // Wrapped in try/catch so a buggy proposal flow never
1232
+ // breaks the chat loop. A successful save surfaces a
1233
+ // dim confirmation line that fits the established
1234
+ // memory-confirmation chrome.
1235
+ if (result.skillProposal && this.opts.callbacks?.handleSkillProposal) {
1236
+ try {
1237
+ const saveResult = await this.opts.callbacks.handleSkillProposal(result.skillProposal);
1238
+ if (saveResult?.created && saveResult.skillName) {
1239
+ this.opts.display.dim(` ✓ Saved as skill: ${saveResult.skillName}`);
1240
+ }
1241
+ }
1242
+ catch {
1243
+ /* defensive — never let proposal flow break the chat loop */
1244
+ }
1245
+ }
1191
1246
  if (this.sessionId) {
1192
1247
  // Only persist the new tail of messages — what got added this turn.
1193
1248
  const newSlice = this.history.slice(turnStart);
@@ -1195,6 +1250,20 @@ class ChatSession {
1195
1250
  }
1196
1251
  this.setStatusState({ kind: 'ready' });
1197
1252
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
1253
+ // v4.5 Phase 8b — surface a deferred daemon-scheduling tip
1254
+ // queued at turn start. Renders AFTER the agent's response per
1255
+ // Q-P8b-3(b) — the user reads the answer first, then sees the
1256
+ // ambient capability hint.
1257
+ if (_deferredTip) {
1258
+ try {
1259
+ this.opts.display.dim(_deferredTip.message);
1260
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1261
+ const { getSuggestionEngine } = require('../../core/v4/suggestionEngine');
1262
+ getSuggestionEngine().recordFired(_deferredTip.slot);
1263
+ }
1264
+ catch { /* defensive */ }
1265
+ _deferredTip = null;
1266
+ }
1198
1267
  // Tier-3.1a: dim full-width rule between the agent reply and the
1199
1268
  // post-turn status footer.
1200
1269
  this.opts.display.write(` ${this.opts.display.rule()}\n`);
@@ -1349,6 +1418,9 @@ class ChatSession {
1349
1418
  providerOk: !this.opts.unconfigured,
1350
1419
  version: version_1.VERSION,
1351
1420
  }) + '\n');
1421
+ // v4.5 TUI polish — blank line so the status pills row doesn't
1422
+ // crowd the muted source annotation right beneath it.
1423
+ display.write('\n');
1352
1424
  // v4.1.3-prebump: dim source annotation under the pills row so the
1353
1425
  // user can see WHY this provider/model was chosen — closes the
1354
1426
  // information gap that made Case 3 (persisted-config) look like a
@@ -1408,10 +1480,73 @@ class ChatSession {
1408
1480
  }
1409
1481
  // Scroll footer (parchment at ≥80 cols, single-line credits below).
1410
1482
  display.write(display.scrollFooter() + '\n');
1483
+ // v4.5 update system — boxed three-option prompt rendered AFTER
1484
+ // the boot card / status pills (Q-U5b less-intrusive position),
1485
+ // BEFORE the bottomPromptHint. Fires only when:
1486
+ // - update check came back with `updateAvailable && !skipped`
1487
+ // - stdin is a TTY (non-interactive boots short-circuit to 'later')
1488
+ // 5-second timeout defaults to 'later' so a user away from
1489
+ // keyboard isn't held up. Skip-on-'n' writes the version to the
1490
+ // .update_check.json cache so subsequent boots stay quiet until
1491
+ // a newer release ships.
1492
+ try {
1493
+ await this.maybeShowBootUpdatePrompt();
1494
+ }
1495
+ catch { /* never let the update prompt crash boot */ }
1411
1496
  // Bottom prompt hint — final line of the boot card.
1412
1497
  display.write('\n');
1413
1498
  display.write(display.bottomPromptHint() + '\n');
1414
1499
  }
1500
+ /**
1501
+ * v4.5 update system — orchestrates the boot prompt. Lazy-imports
1502
+ * the update modules so non-boot code paths (e.g. test harness
1503
+ * sessions constructed without paths wired) don't pay the cost.
1504
+ */
1505
+ async maybeShowBootUpdatePrompt() {
1506
+ if (!this.opts.paths)
1507
+ return;
1508
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1509
+ const cu = require('../../core/v4/update/checkUpdate');
1510
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1511
+ const md = require('../../core/v4/update/installMethodDetect');
1512
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1513
+ const ss = require('../../core/v4/update/skipState');
1514
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1515
+ const bp = require('./updateBootPrompt');
1516
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1517
+ const ei = require('../../core/v4/update/executeInstall');
1518
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1519
+ const ver = require('../../core/version');
1520
+ const status = await cu.checkForUpdate({ paths: this.opts.paths, installedVersion: ver.VERSION });
1521
+ if (!status.updateAvailable || !status.latest || status.skipped)
1522
+ return;
1523
+ const method = md.detectInstallMethod();
1524
+ const choice = await bp.showBootUpdatePrompt({
1525
+ status, method,
1526
+ display: { write: (s) => this.opts.display.write(s), dim: (s) => this.opts.display.dim(s) },
1527
+ });
1528
+ if (choice === 'install') {
1529
+ if (method.inProcessInstallSupported) {
1530
+ this.opts.display.write(`Installing aiden-runtime ${status.latest}…\n`);
1531
+ const result = await ei.executeInstall({ packageSpec: `aiden-runtime@${status.latest}` });
1532
+ if (result.success) {
1533
+ this.opts.display.write(` ✓ aiden-runtime ${result.installedVersion ?? status.latest} installed.\n`);
1534
+ this.opts.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
1535
+ }
1536
+ else {
1537
+ this.opts.display.warn(result.error ?? 'Install failed (no error message).');
1538
+ }
1539
+ }
1540
+ else {
1541
+ this.opts.display.write(`To update, run:\n ${method.updateCommand(status.latest)}\n`);
1542
+ }
1543
+ }
1544
+ else if (choice === 'skip') {
1545
+ await cu.updateCacheFile(this.opts.paths, (current) => ss.applySkip(current, status.latest));
1546
+ this.opts.display.dim(` skipped ${status.latest}. Boot prompt resumes when a newer version ships.`);
1547
+ }
1548
+ // 'later' = no-op; prompt fires again next session.
1549
+ }
1415
1550
  /** Phase 22 Task 4: state transitions for the right-most segment. */
1416
1551
  setStatusState(state) {
1417
1552
  this.statusState = state;
@@ -0,0 +1,92 @@
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/commands/_runtimeToggleHelpers.ts — v4.5 Phase 8a.
10
+ *
11
+ * Shared `flip` + `printStatus` helpers used by /sandbox, /tce, and
12
+ * /browser-depth. Each subsystem command is a tiny wrapper over
13
+ * these — same on/off/status surface, same status format.
14
+ *
15
+ * The persist path goes through `ConfigManager.set()` + `save()` so
16
+ * the runtime_toggles section is written verbatim to config.yaml.
17
+ * The runtimeToggles singleton's `set()` then fires onChange
18
+ * callbacks for cached consumers (sandboxConfig's singleton).
19
+ *
20
+ * Test seam: `flip` and `printStatus` accept a `ctx` shaped like
21
+ * SlashCommandContext (just `display`, optionally `config`). Tests
22
+ * pass a minimal stub.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.flip = flip;
26
+ exports.printStatus = printStatus;
27
+ exports.parseSubcommand = parseSubcommand;
28
+ const runtimeToggles_1 = require("../../../core/v4/runtimeToggles");
29
+ const LABEL = {
30
+ sandbox: 'Sandbox',
31
+ tce: 'TCE',
32
+ browser_depth: 'Browser depth',
33
+ suggestions: 'Suggestions',
34
+ };
35
+ const CONFIG_DOTTED = {
36
+ sandbox: 'runtime_toggles.sandbox',
37
+ tce: 'runtime_toggles.tce',
38
+ browser_depth: 'runtime_toggles.browser_depth',
39
+ suggestions: 'runtime_toggles.suggestions',
40
+ };
41
+ /**
42
+ * Apply a toggle change. When `ctx.config` is wired, persists to
43
+ * config.yaml. Otherwise the flip is in-process only (current
44
+ * session sees the new value; next process boot doesn't).
45
+ *
46
+ * Returns nothing — prints status via ctx.display.
47
+ */
48
+ async function flip(key, value, ctx) {
49
+ const rt = (0, runtimeToggles_1.getRuntimeToggles)();
50
+ // Persist when a ConfigManager is wired.
51
+ if (ctx.config) {
52
+ try {
53
+ ctx.config.set(CONFIG_DOTTED[key], value);
54
+ await ctx.config.save();
55
+ }
56
+ catch (e) {
57
+ ctx.display.warn(`[${key}] config.yaml save failed (${e instanceof Error ? e.message : String(e)}); ` +
58
+ `flip applies to this session only.`);
59
+ }
60
+ }
61
+ await rt.set(key, value, { persist: false });
62
+ printStatus(key, ctx);
63
+ }
64
+ /**
65
+ * Print the current state of one toggle. One-line output per
66
+ * Q-P8a-2(a):
67
+ *
68
+ * `Sandbox: ON (source: config)`
69
+ *
70
+ * `source` reveals which precedence layer provided the value —
71
+ * critical for debugging "why is it ON when my .env says 0".
72
+ */
73
+ function printStatus(key, ctx) {
74
+ const snap = (0, runtimeToggles_1.getRuntimeToggles)().snapshot()[key];
75
+ const label = LABEL[key];
76
+ const state = snap.value ? 'ON' : 'OFF';
77
+ ctx.display.write(`${label}: ${state} (source: ${snap.source})\n`);
78
+ }
79
+ /**
80
+ * Parse the on/off/status subcommand. Returns null when the input
81
+ * is unrecognised (caller prints usage).
82
+ */
83
+ function parseSubcommand(raw) {
84
+ const s = (raw ?? 'status').toLowerCase();
85
+ if (s === 'on' || s === 'enable' || s === '1' || s === 'true')
86
+ return 'on';
87
+ if (s === 'off' || s === 'disable' || s === '0' || s === 'false')
88
+ return 'off';
89
+ if (s === 'status' || s === '' || s === undefined)
90
+ return 'status';
91
+ return null;
92
+ }
@@ -0,0 +1,45 @@
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/commands/browserDepth.ts — v4.5 Phase 8a.
10
+ *
11
+ * `/browser-depth on|off|status` — flip the v4.3 state-aware
12
+ * browser observer (URL/DOM/iframe-tree capture, stale-ref retry,
13
+ * manual-blocker detection) without restart. Persists to
14
+ * config.yaml. Env var AIDEN_BROWSER_DEPTH always wins.
15
+ *
16
+ * Q-P8a-5(a): named `/browser-depth` to mirror the env var
17
+ * exactly. Reserves `/browser` for future browser-navigation
18
+ * commands so the namespace stays unambiguous.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.browserDepth = void 0;
22
+ const _runtimeToggleHelpers_1 = require("./_runtimeToggleHelpers");
23
+ exports.browserDepth = {
24
+ name: 'browser-depth',
25
+ description: 'Toggle the v4.3 state-aware browser observer.',
26
+ category: 'system',
27
+ icon: '🌐',
28
+ handler: async (ctx) => {
29
+ const sub = (0, _runtimeToggleHelpers_1.parseSubcommand)(ctx.args[0]);
30
+ if (sub === 'on') {
31
+ await (0, _runtimeToggleHelpers_1.flip)('browser_depth', true, ctx);
32
+ return {};
33
+ }
34
+ if (sub === 'off') {
35
+ await (0, _runtimeToggleHelpers_1.flip)('browser_depth', false, ctx);
36
+ return {};
37
+ }
38
+ if (sub === 'status') {
39
+ (0, _runtimeToggleHelpers_1.printStatus)('browser_depth', ctx);
40
+ return {};
41
+ }
42
+ ctx.display.printError('Usage: /browser-depth on|off|status');
43
+ return {};
44
+ },
45
+ };