aiden-runtime 4.9.1 → 4.9.3

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 (41) hide show
  1. package/README.md +47 -1
  2. package/dist/cli/v4/aidenPrompt.js +12 -0
  3. package/dist/cli/v4/chatSession.js +41 -13
  4. package/dist/cli/v4/commands/channel.js +4 -6
  5. package/dist/cli/v4/commands/cron.js +6 -1
  6. package/dist/cli/v4/commands/daemonDoctor.js +5 -5
  7. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  8. package/dist/cli/v4/commands/greeter.js +86 -0
  9. package/dist/cli/v4/commands/help.js +2 -0
  10. package/dist/cli/v4/commands/index.js +4 -0
  11. package/dist/cli/v4/commands/mcp.js +2 -2
  12. package/dist/cli/v4/commands/plugins.js +4 -6
  13. package/dist/cli/v4/commands/trigger.js +18 -18
  14. package/dist/cli/v4/confirmPrompt.js +67 -0
  15. package/dist/cli/v4/greeter/history.js +134 -0
  16. package/dist/cli/v4/greeter/index.js +147 -0
  17. package/dist/cli/v4/greeter/scan.js +140 -0
  18. package/dist/cli/v4/greeter/selectOffer.js +118 -0
  19. package/dist/cli/v4/greeter/templates.js +51 -0
  20. package/dist/cli/v4/greeter/types.js +23 -0
  21. package/dist/core/v4/daemon/db/migrations.js +398 -398
  22. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +10 -10
  23. package/dist/core/v4/daemon/incarnationStore.js +9 -9
  24. package/dist/core/v4/daemon/runs/attemptStore.js +8 -8
  25. package/dist/core/v4/daemon/runs/reclaim.js +12 -12
  26. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +19 -19
  27. package/dist/core/v4/daemon/spans/spanStore.js +14 -14
  28. package/dist/core/v4/daemon/triggerBus.js +61 -61
  29. package/dist/core/v4/hooks/auditQuery.js +11 -11
  30. package/dist/core/v4/hooks/dispatcher.js +13 -13
  31. package/dist/core/v4/hooks/registry.js +8 -8
  32. package/dist/core/v4/mcp/transport.js +9 -9
  33. package/dist/core/v4/update/executeInstall.js +29 -18
  34. package/dist/core/v4/update/recoveryScript.js +70 -0
  35. package/dist/core/v4/util/spawnCommand.js +151 -0
  36. package/package.json +1 -1
  37. package/themes/default.yaml +52 -52
  38. package/themes/dracula.yaml +32 -32
  39. package/themes/light.yaml +32 -32
  40. package/themes/monochrome.yaml +31 -31
  41. package/themes/tokyo-night.yaml +32 -32
package/README.md CHANGED
@@ -105,6 +105,13 @@ Windows · Linux · WSL · macOS (API Mode)
105
105
 
106
106
 
107
107
 
108
+ https://github.com/user-attachments/assets/1081e5c5-f1ec-4980-b710-1640981ec58b
109
+
110
+
111
+
112
+
113
+
114
+
108
115
  > A semi-autonomous AI agent that runs on your machine. Touches your files, browser, and shell. Remembers what matters. Built solo. Open source. Still rough in spots.
109
116
 
110
117
  <br>
@@ -130,7 +137,13 @@ Drop a file in `~/Documents/inbox/anything.txt` and Aiden acts on it. The agent
130
137
 
131
138
  <br>
132
139
 
133
- ![Aiden in action](docs/screenshots/autonomy.png)
140
+
141
+
142
+
143
+
144
+ https://github.com/user-attachments/assets/7a66bc19-8b17-4b01-be85-3aa5945a1b3b
145
+
146
+
134
147
 
135
148
  <br>
136
149
 
@@ -219,6 +232,13 @@ Full v4.5 internals: [`docs/v4.5/`](docs/v4.5/) (overview, triggers, architectur
219
232
  | **MCP** | Model Context Protocol bridge — stdio + HTTP transports, schema discovery, tool dispatch. |
220
233
  | **Security moat** | Tiered approval engine (`safe` / `caution` / `dangerous`), dangerous-command pattern classifier, honesty enforcement (post-loop scan rewrites false claims), memory guard, planner-guard tool narrowing, SSRF-safe URL fetcher, secret/PII pre-write scanner, skill-teacher (auto-create skills from successful flows). |
221
234
 
235
+
236
+
237
+
238
+ https://github.com/user-attachments/assets/a76bf4a5-28ca-43b5-8975-5ef0a66ee90d
239
+
240
+
241
+
222
242
  <br>
223
243
 
224
244
  ## Architecture
@@ -229,6 +249,13 @@ Detailed diagrams + module map in [`docs/v4.5/architecture.md`](docs/v4.5/archit
229
249
 
230
250
  <br>
231
251
 
252
+
253
+
254
+
255
+ https://github.com/user-attachments/assets/323c9aa7-959a-425a-a5b3-4bae2b1a14bc
256
+
257
+
258
+
232
259
  ## Install + first run
233
260
 
234
261
  ### Linux / WSL / macOS (one-line)
@@ -285,6 +312,10 @@ Remove-Item -Recurse -Force $env:LOCALAPPDATA\aiden
285
312
  <img width="938" height="1049" alt="preview (3)" src="https://github.com/user-attachments/assets/4e32ae38-74ad-433d-b986-0a15bc2dffec" />
286
313
 
287
314
 
315
+
316
+ https://github.com/user-attachments/assets/398e1d48-cc5a-4fb5-a195-05dbef824198
317
+
318
+
288
319
  ## Recommended terminal setup
289
320
 
290
321
  For best visual rendering, Aiden looks crispest with:
@@ -488,6 +519,21 @@ Common issues live in [`docs/v4.5/troubleshooting.md`](docs/v4.5/troubleshooting
488
519
  - **`/help` doesn't list a command** — that command likely needs an active session field; run from a real REPL
489
520
  - **`npm install` permission errors on Windows** — install into a real folder (not a drive root like `S:\`)
490
521
 
522
+
523
+
524
+
525
+ https://github.com/user-attachments/assets/3acc997f-1d71-45d1-9955-11b67abd0c50
526
+
527
+
528
+
529
+ https://github.com/user-attachments/assets/9e734168-cf76-4cc0-975a-379e5402ee90
530
+
531
+
532
+
533
+ https://github.com/user-attachments/assets/5e7011a5-630d-43bd-8ed3-67084c7645db
534
+
535
+
536
+
491
537
  <br>
492
538
 
493
539
  ## The book
@@ -291,6 +291,18 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
291
291
  line = `${message} ${theme.style.answer(value)}`;
292
292
  }
293
293
  else {
294
+ // v4.9.2 Slice 2 (commit 0d0668f1) attempted to fix cursor
295
+ // misalignment by post-pending cursorBackward(ghost.length).
296
+ // Live-REPL diagnostic proved the fix is structurally inert:
297
+ // @inquirer/core's screen-manager.js:56 appends an ABSOLUTE
298
+ // cursorTo(this.cursorPos.cols) AFTER our content, overriding
299
+ // any cursor-positioning escape we emit inline. The real fix
300
+ // requires either rendering the ghost via a side-channel post-
301
+ // render write or moving it out of the inline line entirely —
302
+ // both need the proper save/restore refactor scheduled for v4.10
303
+ // once the prompt has a real screen-manager-aware test harness.
304
+ // Reverted here so the shipped v4.9.2 doesn't carry a "fix" that
305
+ // doesn't fix anything. Bug D status: known, deferred.
294
306
  const ghostStr = ghost ? dim(ghost) : '';
295
307
  line = `${prefix} ${message}${value}${ghostStr}`;
296
308
  }
@@ -82,6 +82,8 @@ const progressBar_1 = require("./display/progressBar");
82
82
  const uiBuild_1 = require("./uiBuild");
83
83
  const sessionSummaryGate_1 = require("./sessionSummaryGate");
84
84
  const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
85
+ const confirmPrompt_1 = require("./confirmPrompt");
86
+ const greeter_1 = require("./greeter");
85
87
  const historyStore_1 = require("./historyStore");
86
88
  const modelMetadata_1 = require("../../core/v4/modelMetadata");
87
89
  // v4.1.3-prebump: classify provider errors so the catch path can show
@@ -496,17 +498,21 @@ class ChatSession {
496
498
  agent: this.opts.agent,
497
499
  pluginLoader: this.opts.pluginLoader,
498
500
  channelManager: this.opts.channelManager,
499
- confirm: async (msg) => {
500
- // Phase 17.1: bug was reading `this.opts.promptApi?` which is
501
- // undefined when no override is passed; the chain silently
502
- // resolved to undefined returned false → "Grant cancelled"
503
- // before the user could type anything. Use the resolved local
504
- // promptApi (which falls back to readline-default) instead.
505
- const r = await promptApi.readLine(msg);
506
- if (typeof r !== 'string')
507
- return false;
508
- return /^(y|yes)$/i.test(r.trim());
509
- },
501
+ // v4.9.2 Slice 3 — UX-rebuilt confirmation primitive.
502
+ // The stdin/keypress mechanics worked correctly all along;
503
+ // users simply couldn't see the prompt was open. The
504
+ // extracted `runConfirm` helper now owns the canonical
505
+ // y/N hint, the warn-tinted '?' glyph, the
506
+ // suggestionsDisabled flag (so confirmations skip ghost-
507
+ // match against outer chat history), and the per-input
508
+ // honest cancellation message.
509
+ //
510
+ // Phase 17.1 anchor: the previous primitive read
511
+ // `this.opts.promptApi?` (undefined → silently returned
512
+ // false → "Grant cancelled" before user could type) —
513
+ // fixed by routing through the resolved local `promptApi`.
514
+ // That fix stands; Slice 3 adds the UX layer on top.
515
+ confirm: (msg) => (0, confirmPrompt_1.runConfirm)(msg, promptApi, this.opts.display),
510
516
  // Phase 18: raw text prompt for /auth login OAuth code paste.
511
517
  prompt: (msg) => promptApi.readLine(msg),
512
518
  });
@@ -1676,6 +1682,22 @@ class ChatSession {
1676
1682
  }
1677
1683
  }
1678
1684
  catch { /* never let a missing marker crash boot */ }
1685
+ // v4.9.3 Slice 1b — boot greeter. Silent on first-ever launch (lets
1686
+ // renderFirstRunHint above own boot #1), silent when /greeter off,
1687
+ // silent when no offer wins. Lazy-required so test-harness sessions
1688
+ // without `paths` wired skip the fs cost. Internal errors are
1689
+ // already swallowed inside renderGreeter; outer try/catch is the
1690
+ // belt-and-braces guarantee against a boot-crash regression.
1691
+ try {
1692
+ if (this.opts.paths) {
1693
+ await (0, greeter_1.renderGreeter)({
1694
+ paths: this.opts.paths,
1695
+ version: version_1.VERSION,
1696
+ display: this.opts.display,
1697
+ });
1698
+ }
1699
+ }
1700
+ catch { /* never let the greeter crash boot */ }
1679
1701
  // v4.9.0 pre-ship UI: hint moved BEFORE the closing rule so the
1680
1702
  // rule sits adjacent to the active prompt (it becomes the visual
1681
1703
  // top of the prompt zone). New order: blank · hint · blank · rule.
@@ -2085,9 +2107,15 @@ function createDefaultPromptApi(opts = {}) {
2085
2107
  // aidenPrompt component (ghost text + slash dropdown + history nav).
2086
2108
  const useLegacyPrompt = (0, uiBuild_1.isNoUiMode)() || !opts.commands;
2087
2109
  return {
2088
- async readLine(prompt) {
2110
+ async readLine(prompt, readOpts) {
2089
2111
  try {
2090
- if (useLegacyPrompt) {
2112
+ // v4.9.2 Slice 3 — confirmation prompts (suggestionsDisabled)
2113
+ // always route through the legacy inquirer input path. No
2114
+ // ghost-text (would autocomplete from outer chat history —
2115
+ // wrong context for a y/n question), no slash dropdown
2116
+ // (irrelevant for confirmations). Inquirer's plain input is
2117
+ // the well-tested baseline for single-shot questions.
2118
+ if (readOpts?.suggestionsDisabled || useLegacyPrompt) {
2091
2119
  return (await inq.input({ message: prompt, theme: promptTheme })) ?? '';
2092
2120
  }
2093
2121
  // Fetch history just-in-time so each read sees the latest
@@ -255,10 +255,9 @@ async function telegramRemove(ctx) {
255
255
  return;
256
256
  }
257
257
  const proceed = await confirm('Remove the Telegram bot token? This stops polling.');
258
- if (!proceed) {
259
- display.dim(' Cancelled.');
258
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
259
+ if (!proceed)
260
260
  return;
261
- }
262
261
  // Stop the live adapter first so polling actually halts even if the
263
262
  // .env write fails for some reason.
264
263
  const manager = ctx.channelManager;
@@ -297,10 +296,9 @@ async function telegramTakeover(ctx) {
297
296
  const proceed = confirm
298
297
  ? await confirm('Take over Telegram polling? This will boot any other Aiden instance off the bot.')
299
298
  : true;
300
- if (!proceed) {
301
- display.dim(' Cancelled.');
299
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
300
+ if (!proceed)
302
301
  return;
303
- }
304
302
  const spinner = display.startSpinner('Reclaiming Telegram polling…');
305
303
  let result;
306
304
  try {
@@ -327,8 +327,13 @@ async function cmdRemove(ctx, args) {
327
327
  else if (ctx.confirm) {
328
328
  ok = await ctx.confirm(question);
329
329
  }
330
+ // v4.9.2 Slice 3 — when ctx.confirm was the source, the primitive
331
+ // already printed a per-input rejection message. The ctx.prompt
332
+ // branch above does its own y/N parsing, so it still owns its own
333
+ // "Cancelled." line.
330
334
  if (!ok) {
331
- ctx.display.dim('Cancelled.');
335
+ if (ctx.prompt)
336
+ ctx.display.dim('Cancelled.');
332
337
  return;
333
338
  }
334
339
  if ((0, cronManager_1.deleteJob)(job.id)) {
@@ -85,7 +85,7 @@ function collectDoctorChecks(rootDir) {
85
85
  detail: `current=${ver} latest=${migrations_1.LATEST_SCHEMA_VERSION}`,
86
86
  fixable: false });
87
87
  // 3. Recent incarnation
88
- const inc = db.prepare(`SELECT incarnation_id, started_at, ended_at, exit_reason FROM daemon_incarnations
88
+ const inc = db.prepare(`SELECT incarnation_id, started_at, ended_at, exit_reason FROM daemon_incarnations
89
89
  ORDER BY started_at DESC LIMIT 1`).get();
90
90
  if (!inc) {
91
91
  checks.push({ name: 'recent incarnation', status: 'warn',
@@ -100,7 +100,7 @@ function collectDoctorChecks(rootDir) {
100
100
  }
101
101
  // 4. Recent crashes (24h)
102
102
  const sinceIso = new Date(Date.now() - TWENTY_FOUR_HOURS_MS).toISOString();
103
- const crashes = db.prepare(`SELECT COUNT(*) AS c FROM daemon_incarnations
103
+ const crashes = db.prepare(`SELECT COUNT(*) AS c FROM daemon_incarnations
104
104
  WHERE exit_reason = 'crash' AND started_at > ?`).get(sinceIso);
105
105
  checks.push({ name: 'recent crashes (24h)',
106
106
  status: crashes.c === 0 ? 'ok' : crashes.c > 3 ? 'warn' : 'ok',
@@ -111,7 +111,7 @@ function collectDoctorChecks(rootDir) {
111
111
  if (tableExists(db, 'run_attempts')) {
112
112
  const currentInc = inc?.incarnation_id ?? '';
113
113
  const cutoffIso = new Date(Date.now() - 30 * 60 * 1000).toISOString();
114
- stuckAttempts = db.prepare(`SELECT COUNT(*) AS c FROM run_attempts
114
+ stuckAttempts = db.prepare(`SELECT COUNT(*) AS c FROM run_attempts
115
115
  WHERE status='running' AND incarnation_id != ? AND started_at < ?`).get(currentInc, cutoffIso).c;
116
116
  }
117
117
  checks.push({ name: 'stuck attempts',
@@ -125,7 +125,7 @@ function collectDoctorChecks(rootDir) {
125
125
  let orphanSpans = 0;
126
126
  if (tableExists(db, 'spans')) {
127
127
  const currentInc = inc?.incarnation_id ?? '';
128
- orphanSpans = db.prepare(`SELECT COUNT(*) AS c FROM spans
128
+ orphanSpans = db.prepare(`SELECT COUNT(*) AS c FROM spans
129
129
  WHERE status IS NULL AND ended_at IS NULL AND incarnation_id != ?`).get(currentInc).c;
130
130
  }
131
131
  checks.push({ name: 'orphan spans',
@@ -147,7 +147,7 @@ function collectDoctorChecks(rootDir) {
147
147
  // 8. Stale-claimed trigger events
148
148
  let staleClaimed = 0;
149
149
  if (tableExists(db, 'trigger_events')) {
150
- staleClaimed = db.prepare(`SELECT COUNT(*) AS c FROM trigger_events
150
+ staleClaimed = db.prepare(`SELECT COUNT(*) AS c FROM trigger_events
151
151
  WHERE status='claimed' AND claim_expires_at IS NOT NULL AND claim_expires_at < ?`).get(Date.now()).c;
152
152
  }
153
153
  checks.push({ name: 'stale-claimed trigger events',
@@ -150,7 +150,7 @@ function readSnapshot() {
150
150
  // Recent runs (last 3).
151
151
  const recentRuns = (() => {
152
152
  try {
153
- const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at FROM runs
153
+ const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at FROM runs
154
154
  ORDER BY id DESC LIMIT 3`).all();
155
155
  return rows.map((r) => ({
156
156
  id: r.id,
@@ -0,0 +1,86 @@
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/greeter.ts — v4.9.3 SLICE 1a.
10
+ *
11
+ * `/greeter on | off | status` — REPL controls for the boot greeter.
12
+ * - on → set disabled: false; init the file if missing
13
+ * - off → confirm-gated (uses v4.9.2 Slice 3 ctx.confirm), set
14
+ * disabled: true
15
+ * - status → print current state + last greeting + offer summary
16
+ *
17
+ * Slice 1a: the slash command operates on the history file even
18
+ * though the greeter doesn't fire on boot yet (Slice 1b wires that).
19
+ * `/greeter off` BEFORE Slice 1b still durably disables, so when
20
+ * Slice 1b lands the user's choice is already honored.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.greeter = void 0;
24
+ const history_1 = require("../greeter/history");
25
+ exports.greeter = {
26
+ name: 'greeter',
27
+ description: 'Manage the boot greeter. Actions: on, off, status.',
28
+ category: 'system',
29
+ icon: '👋',
30
+ handler: async (ctx) => {
31
+ if (!ctx.paths) {
32
+ ctx.display.printError('Cannot read greeter state — paths not wired in this session.');
33
+ return;
34
+ }
35
+ const sub = (ctx.args[0] ?? 'status').toLowerCase();
36
+ if (sub === 'on') {
37
+ const h = (await (0, history_1.readHistory)(ctx.paths)) ?? initial();
38
+ h.disabled = false;
39
+ await (0, history_1.writeHistory)(ctx.paths, h);
40
+ ctx.display.success('Greeter on. Next boot will check for noticeable changes.');
41
+ return;
42
+ }
43
+ if (sub === 'off') {
44
+ if (!ctx.confirm) {
45
+ ctx.display.printError('Cannot confirm in this context.');
46
+ return;
47
+ }
48
+ const proceed = await ctx.confirm('Turn the boot greeter off? You can re-enable with /greeter on.');
49
+ if (!proceed)
50
+ return; // confirm() already printed the rejection reason
51
+ const h = (await (0, history_1.readHistory)(ctx.paths)) ?? initial();
52
+ h.disabled = true;
53
+ await (0, history_1.writeHistory)(ctx.paths, h);
54
+ ctx.display.success('Greeter off. No greeting on boot until /greeter on.');
55
+ return;
56
+ }
57
+ if (sub === 'status') {
58
+ const h = await (0, history_1.readHistory)(ctx.paths);
59
+ if (!h) {
60
+ ctx.display.dim('Greeter has not been initialized yet (no boots since v4.9.3).');
61
+ return;
62
+ }
63
+ const state = h.disabled ? 'off' : 'on';
64
+ const accepted = h.offers.filter((o) => o.response === 'accepted').length;
65
+ const ignored = h.offers.filter((o) => o.response === 'ignored').length;
66
+ const pending = h.offers.filter((o) => !o.response).length;
67
+ ctx.display.write('\n Greeter status:\n');
68
+ ctx.display.write(` state: ${state}\n`);
69
+ ctx.display.write(` first launch: ${h.firstLaunchAt}\n`);
70
+ ctx.display.write(` last greeting: ${h.lastGreetingAt}\n`);
71
+ ctx.display.write(` offers: ${h.offers.length} (${accepted} accepted · ${ignored} ignored · ${pending} pending)\n\n`);
72
+ return;
73
+ }
74
+ ctx.display.printError(`Unknown greeter action '${sub}'.`, 'Try: /greeter on | off | status');
75
+ },
76
+ };
77
+ function initial() {
78
+ const now = new Date().toISOString();
79
+ return {
80
+ v: 1,
81
+ firstLaunchAt: now,
82
+ lastGreetingAt: now,
83
+ offers: [],
84
+ disabled: false,
85
+ };
86
+ }
@@ -79,6 +79,8 @@ exports.SUBSECTION_MAP = {
79
79
  // v4.9.1 amendment — REPL surfaces for memory + hooks (daemon already mapped).
80
80
  memory: 'System',
81
81
  hooks: 'System',
82
+ // v4.9.3 Slice 1b — boot greeter management.
83
+ greeter: 'System',
82
84
  // ── Authentication ──
83
85
  auth: 'Authentication',
84
86
  // ── Help ──
@@ -111,6 +111,8 @@ const memorySlash_1 = require("./memorySlash");
111
111
  Object.defineProperty(exports, "memory", { enumerable: true, get: function () { return memorySlash_1.memory; } });
112
112
  const hooksSlash_1 = require("./hooksSlash");
113
113
  Object.defineProperty(exports, "hooks", { enumerable: true, get: function () { return hooksSlash_1.hooks; } });
114
+ // v4.9.3 Slice 1b — boot greeter management.
115
+ const greeter_1 = require("./greeter");
114
116
  /** All built-in system commands, in canonical order. */
115
117
  exports.allCommands = [
116
118
  help_1.help,
@@ -166,6 +168,8 @@ exports.allCommands = [
166
168
  // v4.9.1 amendment — REPL slash surfaces mirroring CLI subcommands.
167
169
  memorySlash_1.memory,
168
170
  hooksSlash_1.hooks,
171
+ // v4.9.3 Slice 1b — boot greeter management.
172
+ greeter_1.greeter,
169
173
  clear_1.clear,
170
174
  quit_1.quit,
171
175
  ];
@@ -266,8 +266,8 @@ async function wireSubagentFanout(opts) {
266
266
  // WAL-coexistence model as REPL — connection.ts caches per-path.
267
267
  const mcpInstanceId = `mcp-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
268
268
  const mcpDb = (0, daemon_1.openDaemonDb)((0, daemon_1.daemonDbPath)(opts.paths.root));
269
- mcpDb.prepare(`INSERT OR IGNORE INTO daemon_instances
270
- (instance_id, pid, hostname, started_at, last_heartbeat, version)
269
+ mcpDb.prepare(`INSERT OR IGNORE INTO daemon_instances
270
+ (instance_id, pid, hostname, started_at, last_heartbeat, version)
271
271
  VALUES (?, ?, ?, ?, ?, ?)`).run(mcpInstanceId, process.pid, node_os_1.default.hostname(), Date.now(), Date.now(), version_1.VERSION);
272
272
  const mcpRunStore = (0, daemon_1.createRunStore)({ db: mcpDb });
273
273
  // v4.6 Phase 3b — self-improvement loop singleton against the
@@ -161,10 +161,9 @@ exports.plugins = {
161
161
  ctx.display.write('\n');
162
162
  const confirmFn = ctx.confirm ?? (async () => false);
163
163
  const allow = await confirmFn(`Install ${manifest.name} with the listed permissions? [y/N] `);
164
- if (!allow) {
165
- ctx.display.dim('Install cancelled.');
164
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
165
+ if (!allow)
166
166
  return {};
167
- }
168
167
  // Copy into the user plugins dir.
169
168
  const dst = node_path_1.default.join(ctx.paths.pluginsDir, manifest.name);
170
169
  try {
@@ -258,10 +257,9 @@ exports.plugins = {
258
257
  const allow = await confirmFn(isUpgrade
259
258
  ? `Grant the listed permissions (including ${newPerms.length} new)? [y/N] `
260
259
  : `Grant the listed permissions? [y/N] `);
261
- if (!allow) {
262
- ctx.display.dim('Grant cancelled.');
260
+ // v4.9.2 Slice 3 — confirm() now owns the rejection message.
261
+ if (!allow)
263
262
  return {};
264
- }
265
263
  await (0, plugins_1.saveGrantedPermissions)(dir, entry.manifest.permissions);
266
264
  // Reload so the new state takes effect.
267
265
  await ctx.pluginLoader.teardown();
@@ -79,9 +79,9 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
79
79
  spec.paths = spec.paths.map((p) => node_path_1.default.resolve(p));
80
80
  const id = (0, node_crypto_1.randomUUID)();
81
81
  const now = Date.now();
82
- db.prepare(`INSERT INTO triggers
83
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
84
- created_at, updated_at)
82
+ db.prepare(`INSERT INTO triggers
83
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
84
+ created_at, updated_at)
85
85
  VALUES (?, 'file', ?, ?, ?, ?, 0, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, now, now);
86
86
  out(`trigger added: ${id} (${a.name})\n`);
87
87
  out('Restart the daemon to activate the watcher.\n');
@@ -182,11 +182,11 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
182
182
  return 1;
183
183
  }
184
184
  const prefix = `trigger:${trig.source}:${id}:`;
185
- const rows = db.prepare(`SELECT re.ts, re.kind, re.payload, r.id AS run_id
186
- FROM run_events re
187
- JOIN runs r ON re.run_id = r.id
188
- WHERE r.session_id LIKE ?
189
- ORDER BY re.ts DESC
185
+ const rows = db.prepare(`SELECT re.ts, re.kind, re.payload, r.id AS run_id
186
+ FROM run_events re
187
+ JOIN runs r ON re.run_id = r.id
188
+ WHERE r.session_id LIKE ?
189
+ ORDER BY re.ts DESC
190
190
  LIMIT 50`).all(`${prefix}%`);
191
191
  if (rows.length === 0) {
192
192
  out(`No run events recorded for trigger ${id} (${trig.name}).\n`);
@@ -213,10 +213,10 @@ async function runTriggerSubcommand(action, args, argv, opts = {}) {
213
213
  return 1;
214
214
  }
215
215
  const prefix = `trigger:${trig.source}:${id}:`;
216
- const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at
217
- FROM runs
218
- WHERE session_id LIKE ?
219
- ORDER BY started_at DESC
216
+ const rows = db.prepare(`SELECT id, status, finish_reason, started_at, completed_at
217
+ FROM runs
218
+ WHERE session_id LIKE ?
219
+ ORDER BY started_at DESC
220
220
  LIMIT 50`).all(`${prefix}%`);
221
221
  if (rows.length === 0) {
222
222
  out(`No runs recorded for trigger ${id} (${trig.name}).\n`);
@@ -261,9 +261,9 @@ function runAddWebhook(db, argv, out, err) {
261
261
  });
262
262
  const id = (0, node_crypto_1.randomUUID)();
263
263
  const now = Date.now();
264
- db.prepare(`INSERT INTO triggers
265
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
266
- created_at, updated_at)
264
+ db.prepare(`INSERT INTO triggers
265
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
266
+ created_at, updated_at)
267
267
  VALUES (?, 'webhook', ?, ?, ?, ?, ?, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, spec.deliverOnly ? 1 : 0, now, now);
268
268
  const cfg = (0, daemon_1.getDaemonConfig)();
269
269
  const host = process.env.AIDEN_DAEMON_BIND ?? '127.0.0.1';
@@ -359,9 +359,9 @@ async function runAddEmail(db, argv, out, err) {
359
359
  }
360
360
  const id = (0, node_crypto_1.randomUUID)();
361
361
  const now = Date.now();
362
- db.prepare(`INSERT INTO triggers
363
- (id, source, name, spec_json, enabled, prompt_template, deliver_only,
364
- created_at, updated_at)
362
+ db.prepare(`INSERT INTO triggers
363
+ (id, source, name, spec_json, enabled, prompt_template, deliver_only,
364
+ created_at, updated_at)
365
365
  VALUES (?, 'email', ?, ?, ?, ?, ?, ?, ?)`).run(id, a.name, JSON.stringify(spec), a.disabled ? 0 : 1, spec.promptTemplate ?? null, spec.deliverOnly ? 1 : 0, now, now);
366
366
  out(`trigger added: ${id} (${a.name})\n`);
367
367
  out(`imap host: ${spec.imap.host}:${spec.imap.port}${spec.imap.tls ? ' (TLS)' : ''}\n`);
@@ -0,0 +1,67 @@
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/confirmPrompt.ts — v4.9.2 SLICE 3.
10
+ *
11
+ * The slash-command `ctx.confirm()` primitive, extracted from the
12
+ * chatSession closure so it has one source of truth + is unit-testable
13
+ * without spinning up a full REPL.
14
+ *
15
+ * Behaviour:
16
+ * - Canonicalises the y/n hint: strips any caller-appended ` (y/N) `
17
+ * / ` [y/N] ` / ` (Y/n) ` so the primitive can append exactly one
18
+ * ` (y/N) ` in canonical lowercase-y / capital-N form.
19
+ * - Prefixes with a warn-tinted `?` glyph so the confirmation chrome
20
+ * is visually distinct from the main ▲ chat prompt (Slice 3 root
21
+ * cause: users couldn't tell a prompt was open).
22
+ * - Routes through `promptApi.readLine` with `suggestionsDisabled:true`
23
+ * so the inquirer-input path runs (no ghost-text from outer chat
24
+ * history, no slash dropdown — irrelevant for y/n).
25
+ * - Emits a per-input cancellation reason:
26
+ * empty / Enter alone → "Cancelled (press 'y' to confirm; …)"
27
+ * 'n' / 'no' → "Cancelled." (deliberate decline)
28
+ * other non-y → `Cancelled ("<x>" not recognized — …)`
29
+ * null / non-string → "Cancelled (no input)."
30
+ * Callers no longer print their own "Cancelled." line — the
31
+ * primitive owns the rejection message.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.runConfirm = runConfirm;
35
+ /** Strip any caller-appended `(y/N)` / `[y/N]` / `(Y/n)` so we can
36
+ * re-append the canonical hint without duplication. */
37
+ const TRAILING_YN_HINT_RE = /\s*[\[(](y\/[nN]|Y\/n)[\])]\s*$/i;
38
+ /**
39
+ * Run a single confirmation prompt. Resolves to `true` on `y` / `yes`
40
+ * (case insensitive, trimmed); `false` on anything else, with a
41
+ * specific cancellation line written to `display.dim()`.
42
+ *
43
+ * Never throws — readLine errors and non-string returns degrade to
44
+ * `false` with an honest "no input" reason.
45
+ */
46
+ async function runConfirm(msg, promptApi, display) {
47
+ const stripped = msg.replace(TRAILING_YN_HINT_RE, '').trimEnd();
48
+ const decorated = `${display.paint('?', 'warn')} ${stripped} (y/N) `;
49
+ const r = await promptApi.readLine(decorated, { suggestionsDisabled: true });
50
+ if (typeof r !== 'string') {
51
+ display.dim('Cancelled (no input).');
52
+ return false;
53
+ }
54
+ const trimmed = r.trim();
55
+ if (/^(y|yes)$/i.test(trimmed))
56
+ return true;
57
+ if (trimmed === '') {
58
+ display.dim(`Cancelled (press 'y' to confirm; Enter alone = no).`);
59
+ }
60
+ else if (/^(n|no)$/i.test(trimmed)) {
61
+ display.dim('Cancelled.');
62
+ }
63
+ else {
64
+ display.dim(`Cancelled ("${trimmed}" not recognized — expected y/yes/n/no).`);
65
+ }
66
+ return false;
67
+ }