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
@@ -0,0 +1,41 @@
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
+ * core/v4/suggestionCatalog.ts — v4.5 Phase 8b.
10
+ *
11
+ * Pre-rendered user-facing copy for each contextual suggestion slot.
12
+ * Centralised here so future tone tuning / i18n can change one file
13
+ * without touching the engine.
14
+ *
15
+ * Style guide (approved tone Q-P8b-5(b)):
16
+ * - One line per tip.
17
+ * - ≤ 90 visible chars (room for indentation + emoji).
18
+ * - "💡 tip:" prefix, lowercase command + brief why.
19
+ * - No trailing punctuation gymnastics.
20
+ * - No second-person preaching ("you should" → drop).
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.SUGGESTION_COPY = void 0;
24
+ exports.suggestionMessageFor = suggestionMessageFor;
25
+ /**
26
+ * Slot → message map. The engine resolves the slot and the
27
+ * catalog renders the matching copy.
28
+ */
29
+ exports.SUGGESTION_COPY = Object.freeze({
30
+ sandbox: '💡 tip: enable /sandbox on for a safer guardrail against destructive ops.',
31
+ browser_depth: '💡 tip: enable /browser-depth on to capture page state + auto-retry stale refs.',
32
+ daemon_scheduling: '💡 tip: this looks like a recurring task — `aiden cron add` or `aiden trigger add file` can run it on a schedule.',
33
+ tce_recovery: '💡 tip: enable /tce on so Aiden classifies tool failures + auto-recovers.',
34
+ });
35
+ /**
36
+ * Look up the rendered tip string for a slot. Pure — caller decides
37
+ * when to display (`display.dim()` is the conventional sink).
38
+ */
39
+ function suggestionMessageFor(slot) {
40
+ return exports.SUGGESTION_COPY[slot];
41
+ }
@@ -0,0 +1,210 @@
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
+ * core/v4/suggestionEngine.ts — v4.5 Phase 8b.
10
+ *
11
+ * Contextual one-time suggestions for the v4.4/v4.5 subsystems the
12
+ * user hasn't enabled yet. Fires when their CURRENT task would
13
+ * genuinely benefit:
14
+ *
15
+ * - sandbox → destructive shell pattern + sandbox is OFF
16
+ * - browser_depth → browser_* tool call + browser_depth is OFF
17
+ * - daemon_scheduling → user said "every day", "watch this folder",
18
+ * "when an email arrives", etc.
19
+ * - tce_recovery → recovery situation reached + TCE is OFF
20
+ * (lower priority; most users keep TCE on)
21
+ *
22
+ * Budget Q-P8b-1(a): 2 suggestions per session global. Each slot
23
+ * fires AT MOST ONCE per session. Resets on REPL restart.
24
+ *
25
+ * Dismissal Q-P8b-4(c) + Q-P8b-6(a):
26
+ * - Per-session: `dismissAll()` silences for the rest of the session.
27
+ * - Permanent: `runtime_toggles.suggestions = false` in config.yaml
28
+ * (slash command persists). Engine reads via runtimeToggles
29
+ * singleton — when the toggle reports OFF the engine treats every
30
+ * classification as "no tip".
31
+ *
32
+ * Pure module — no I/O. Display happens at the call site
33
+ * (`display.dim()` per audit Q-P8b-3). Engine only classifies +
34
+ * tracks state.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.buildSuggestionEngine = buildSuggestionEngine;
38
+ exports.getSuggestionEngine = getSuggestionEngine;
39
+ exports._resetSuggestionEngineForTests = _resetSuggestionEngineForTests;
40
+ const dangerousPatterns_1 = require("../../moat/dangerousPatterns");
41
+ const runtimeToggles_1 = require("./runtimeToggles");
42
+ const suggestionCatalog_1 = require("./suggestionCatalog");
43
+ // ── Daemon-scheduling regex (Q-P8b-2a — simple keyword) ────────────────────
44
+ const SCHEDULING_PATTERNS = [
45
+ /\b(every|each)\s+(day|hour|minute|week|morning|evening|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
46
+ /\b(daily|hourly|weekly|nightly)\b/i,
47
+ /\bat\s+\d{1,2}(:\d{2})?\s*(am|pm)?\s+(every|each)\b/i,
48
+ /\bwatch\s+(this|the)?\s*(folder|directory|path)\b/i,
49
+ /\bmonitor\s+(changes|files?|directory|folder)\b/i,
50
+ /\bwhen\s+(an?\s+)?(email|webhook|file|message)\s+(arrives|comes|is\s+received|drops)\b/i,
51
+ /\b(remind|alert|notify)\s+me\s+(when|to|every)\b/i,
52
+ /\bset\s+up\s+(a\s+)?(cron|schedule|trigger|watcher|webhook)\b/i,
53
+ ];
54
+ // ── System-path heuristic for file_* tool sandbox tips ─────────────────────
55
+ const SYSTEM_PATH_PATTERNS = [
56
+ /^\/etc\b/i,
57
+ /^\/System\b/i,
58
+ /^\/Library\/System\b/i,
59
+ /^\/usr\/(s)?bin\b/i,
60
+ /^[A-Z]:\\Windows\b/i,
61
+ /^[A-Z]:\\Program\s+Files\b/i,
62
+ /\\System32\\/i,
63
+ ];
64
+ function isSystemPath(p) {
65
+ if (typeof p !== 'string' || p.length === 0)
66
+ return false;
67
+ return SYSTEM_PATH_PATTERNS.some((r) => r.test(p));
68
+ }
69
+ // ── Classifiers (pure) ─────────────────────────────────────────────────────
70
+ /** sandbox slot — destructive shell or system-path write. */
71
+ function classifySandbox(call) {
72
+ const args = (call.arguments ?? {});
73
+ if (call.name === 'shell_exec') {
74
+ const cmd = typeof args.command === 'string' ? args.command : '';
75
+ if (cmd.length === 0)
76
+ return false;
77
+ const cls = (0, dangerousPatterns_1.classifyCommand)(cmd);
78
+ return cls.tier === 'caution' || cls.tier === 'dangerous';
79
+ }
80
+ if (call.name === 'file_write' || call.name === 'file_delete'
81
+ || call.name === 'file_move' || call.name === 'file_patch') {
82
+ const target = typeof args.path === 'string' ? args.path
83
+ : typeof args.target === 'string' ? args.target
84
+ : '';
85
+ return isSystemPath(target);
86
+ }
87
+ return false;
88
+ }
89
+ /** browser_depth slot — any browser_* tool fires the suggestion when off. */
90
+ function classifyBrowserDepth(call) {
91
+ return call.name.startsWith('browser_');
92
+ }
93
+ /** daemon_scheduling slot — keyword regex on initial message. */
94
+ function classifySchedulingIntent(message) {
95
+ if (typeof message !== 'string' || message.length < 6)
96
+ return false;
97
+ return SCHEDULING_PATTERNS.some((r) => r.test(message));
98
+ }
99
+ // ── Singleton ──────────────────────────────────────────────────────────────
100
+ const DEFAULT_BUDGET_PER_SESSION = 2;
101
+ function buildSuggestionEngine(opts = {}) {
102
+ const budget = opts.budgetPerSession ?? DEFAULT_BUDGET_PER_SESSION;
103
+ const getRT = opts.runtimeTogglesGetter ?? (() => (0, runtimeToggles_1.getRuntimeToggles)());
104
+ const firedSlots = new Set();
105
+ let dismissedSession = false;
106
+ function permanentlyOff() {
107
+ try {
108
+ // 'suggestions' is wired as a runtime toggle key via Phase 8b
109
+ // schema extension. When the user has typed `/suggestions off`
110
+ // (persisted to config.yaml runtime_toggles.suggestions=false),
111
+ // the toggle reports off and we skip every classification.
112
+ return !getRT().isEnabled('suggestions');
113
+ }
114
+ catch {
115
+ // Defensive — if the toggle key isn't registered yet we treat
116
+ // suggestions as on (the default).
117
+ return false;
118
+ }
119
+ }
120
+ function canFire(slot) {
121
+ if (dismissedSession)
122
+ return false;
123
+ if (permanentlyOff())
124
+ return false;
125
+ if (firedSlots.has(slot))
126
+ return false;
127
+ if (firedSlots.size >= budget)
128
+ return false;
129
+ // Only suggest when the relevant subsystem is OFF — otherwise the
130
+ // tip is noise. Mapping: slot → underlying runtimeToggles key.
131
+ try {
132
+ switch (slot) {
133
+ case 'sandbox':
134
+ if (getRT().isEnabled('sandbox'))
135
+ return false;
136
+ break;
137
+ case 'browser_depth':
138
+ if (getRT().isEnabled('browser_depth'))
139
+ return false;
140
+ break;
141
+ case 'tce_recovery':
142
+ if (getRT().isEnabled('tce'))
143
+ return false;
144
+ break;
145
+ case 'daemon_scheduling':
146
+ // No matching toggle — daemon mode is a process-level
147
+ // boolean (AIDEN_DAEMON). Suggest only when daemon is off,
148
+ // which we detect by reading the env var (the daemon
149
+ // toggle isn't part of runtime_toggles by design).
150
+ if (process.env.AIDEN_DAEMON === '1')
151
+ return false;
152
+ break;
153
+ }
154
+ }
155
+ catch { /* defensive */ }
156
+ return true;
157
+ }
158
+ function build(slot) {
159
+ return { slot, message: (0, suggestionCatalog_1.suggestionMessageFor)(slot) };
160
+ }
161
+ return {
162
+ checkToolCall(call) {
163
+ if (!call || typeof call.name !== 'string')
164
+ return null;
165
+ if (classifyBrowserDepth(call) && canFire('browser_depth')) {
166
+ return build('browser_depth');
167
+ }
168
+ if (classifySandbox(call) && canFire('sandbox')) {
169
+ return build('sandbox');
170
+ }
171
+ return null;
172
+ },
173
+ checkInitialMessage(message) {
174
+ if (classifySchedulingIntent(message) && canFire('daemon_scheduling')) {
175
+ return build('daemon_scheduling');
176
+ }
177
+ return null;
178
+ },
179
+ recordFired(slot) {
180
+ firedSlots.add(slot);
181
+ },
182
+ dismissAll() {
183
+ dismissedSession = true;
184
+ },
185
+ snapshot() {
186
+ return {
187
+ firedSlots: [...firedSlots],
188
+ dismissedSession,
189
+ permanentlyOff: permanentlyOff(),
190
+ budgetRemaining: Math.max(0, budget - firedSlots.size),
191
+ };
192
+ },
193
+ _resetForTests() {
194
+ firedSlots.clear();
195
+ dismissedSession = false;
196
+ },
197
+ };
198
+ }
199
+ // ── Process-wide singleton ─────────────────────────────────────────────────
200
+ let _singleton = null;
201
+ function getSuggestionEngine() {
202
+ if (!_singleton)
203
+ _singleton = buildSuggestionEngine();
204
+ return _singleton;
205
+ }
206
+ function _resetSuggestionEngineForTests() {
207
+ if (_singleton)
208
+ _singleton._resetForTests();
209
+ _singleton = null;
210
+ }
@@ -49,10 +49,19 @@ class ToolRegistry {
49
49
  return [...this.handlers.keys()];
50
50
  }
51
51
  /**
52
- * Schemas to advertise to the LLM. When `filterToolsets` is provided,
53
- * only handlers whose `toolset` matches one of the entries are returned.
52
+ * Schemas to advertise to the LLM. Two optional filters, AND-combined:
53
+ *
54
+ * - `filterToolsets`: include only handlers whose `toolset` matches
55
+ * one of the entries. Applied first (preserves pre-v4.6 behaviour
56
+ * when called with one argument).
57
+ * - `context` (v4.6 Phase 1): include only handlers whose
58
+ * `contexts` array contains this value, OR whose `contexts` is
59
+ * undefined (default = visible everywhere). Applied second.
60
+ *
61
+ * Both filters default to "no filter" when omitted. Callers that
62
+ * predate v4.6 pass one arg or none and continue working unchanged.
54
63
  */
55
- getSchemas(filterToolsets) {
64
+ getSchemas(filterToolsets, context) {
56
65
  const out = [];
57
66
  for (const handler of this.handlers.values()) {
58
67
  if (filterToolsets && filterToolsets.length > 0) {
@@ -60,6 +69,13 @@ class ToolRegistry {
60
69
  continue;
61
70
  }
62
71
  }
72
+ if (context !== undefined) {
73
+ // contexts undefined → tool is visible in both REPL and daemon
74
+ // (backward-compat default for every pre-v4.6 tool).
75
+ if (handler.contexts !== undefined && !handler.contexts.includes(context)) {
76
+ continue;
77
+ }
78
+ }
63
79
  out.push(handler.schema);
64
80
  }
65
81
  return out;
@@ -136,12 +152,30 @@ class ToolRegistry {
136
152
  riskTier = c.tier;
137
153
  reason = c.reason;
138
154
  }
155
+ // v4.4 Phase 4 — dangerous-tier auto-preview. Surface
156
+ // "what would happen if you say yes" to the approval prompt.
157
+ // Effective tier is the handler annotation (Phase 1 floor)
158
+ // OR the classifier escalation above (whichever is higher).
159
+ let preview;
160
+ const effectiveTier = (riskTier === 'dangerous' || handler.riskTier === 'dangerous')
161
+ ? 'dangerous' : (riskTier ?? handler.riskTier);
162
+ if (effectiveTier === 'dangerous' && typeof handler.buildPreview === 'function') {
163
+ try {
164
+ preview = await handler.buildPreview(args, context);
165
+ }
166
+ catch {
167
+ // Preview is best-effort. A bad preview never blocks
168
+ // the underlying approval decision.
169
+ preview = undefined;
170
+ }
171
+ }
139
172
  const allowed = await context.approvalEngine.checkApproval({
140
173
  toolName: call.name,
141
174
  category: handler.category,
142
175
  args,
143
176
  riskTier,
144
177
  reason,
178
+ preview,
145
179
  });
146
180
  if (!allowed) {
147
181
  return {