aiden-runtime 4.0.2 → 4.1.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 (108) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +421 -5
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +41 -0
  28. package/dist/cli/v4/envSources.js +105 -0
  29. package/dist/cli/v4/ghostMatch.js +74 -0
  30. package/dist/cli/v4/historyStore.js +163 -0
  31. package/dist/cli/v4/pasteCompression.js +124 -0
  32. package/dist/cli/v4/pasteIntercept.js +203 -0
  33. package/dist/cli/v4/replyRenderer.js +209 -0
  34. package/dist/cli/v4/resizeGuard.js +92 -0
  35. package/dist/cli/v4/shellInterpolation.js +139 -0
  36. package/dist/cli/v4/skinEngine.js +21 -1
  37. package/dist/cli/v4/streamingPrefix.js +121 -0
  38. package/dist/cli/v4/syntaxHighlight.js +345 -0
  39. package/dist/cli/v4/table.js +216 -0
  40. package/dist/cli/v4/themeDetect.js +81 -0
  41. package/dist/cli/v4/uiBuild.js +74 -0
  42. package/dist/cli/v4/voiceCli.js +113 -0
  43. package/dist/cli/v4/voicePromptApi.js +196 -0
  44. package/dist/core/channels/discord.js +16 -10
  45. package/dist/core/channels/email.js +13 -9
  46. package/dist/core/channels/imessage.js +13 -9
  47. package/dist/core/channels/manager.js +25 -7
  48. package/dist/core/channels/pdf-extract.js +180 -0
  49. package/dist/core/channels/photo-vision.js +157 -0
  50. package/dist/core/channels/signal.js +11 -7
  51. package/dist/core/channels/slack.js +13 -10
  52. package/dist/core/channels/telegram-commands.js +154 -0
  53. package/dist/core/channels/telegram-groups.js +198 -0
  54. package/dist/core/channels/telegram-rate-limit.js +124 -0
  55. package/dist/core/channels/telegram.js +1980 -0
  56. package/dist/core/channels/twilio.js +11 -7
  57. package/dist/core/channels/webhook.js +9 -5
  58. package/dist/core/channels/whatsapp.js +15 -11
  59. package/dist/core/channels/whisper-transcribe.js +163 -0
  60. package/dist/core/cronManager.js +33 -294
  61. package/dist/core/gateway.js +29 -8
  62. package/dist/core/playwrightBridge.js +90 -0
  63. package/dist/core/v4/aidenAgent.js +35 -0
  64. package/dist/core/v4/auxiliaryClient.js +2 -2
  65. package/dist/core/v4/cron/atomicWrite.js +18 -4
  66. package/dist/core/v4/cron/cronExecute.js +300 -0
  67. package/dist/core/v4/cron/cronManager.js +502 -0
  68. package/dist/core/v4/cron/cronState.js +314 -0
  69. package/dist/core/v4/cron/cronTick.js +90 -0
  70. package/dist/core/v4/cron/diagnostics.js +104 -0
  71. package/dist/core/v4/cron/graceWindow.js +79 -0
  72. package/dist/core/v4/logger/factory.js +110 -0
  73. package/dist/core/v4/logger/index.js +22 -0
  74. package/dist/core/v4/logger/logger.js +101 -0
  75. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  76. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  77. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  78. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  79. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  80. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  81. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  82. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  83. package/dist/core/v4/platformPaths.js +105 -0
  84. package/dist/core/v4/providerFallback.js +25 -0
  85. package/dist/core/v4/skillLoader.js +21 -5
  86. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  87. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  88. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  89. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  90. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  91. package/dist/core/v4/subagent/budget.js +76 -0
  92. package/dist/core/v4/subagent/diagnostics.js +22 -0
  93. package/dist/core/v4/subagent/fanout.js +216 -0
  94. package/dist/core/v4/subagent/merger.js +148 -0
  95. package/dist/core/v4/subagent/providerRotation.js +54 -0
  96. package/dist/core/v4/voice/audioStream.js +373 -0
  97. package/dist/core/v4/voice/cliVoice.js +393 -0
  98. package/dist/core/v4/voice/diagnostics.js +66 -0
  99. package/dist/core/v4/voice/ttsStream.js +193 -0
  100. package/dist/core/version.js +1 -1
  101. package/dist/core/visionAnalyze.js +291 -90
  102. package/dist/core/voice/audio.js +61 -5
  103. package/dist/core/voice/audioBackend.js +134 -0
  104. package/dist/core/voice/stt.js +61 -6
  105. package/dist/core/voice/tts.js +19 -3
  106. package/dist/tools/v4/index.js +32 -1
  107. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  108. package/package.json +11 -2
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AIDEN_PRESHIP_BUILD = exports.AIDEN_CROSS_PLATFORM_BUILD = exports.AIDEN_REPLY_FORMAT_BUILD = exports.AIDEN_SKILL_MINING_BUILD = exports.AIDEN_UI_BUILD = void 0;
4
+ exports.citationsEnabled = citationsEnabled;
5
+ exports.isMcpServeMode = isMcpServeMode;
6
+ exports.isNoUiMode = isNoUiMode;
7
+ exports.uiIconsEnabled = uiIconsEnabled;
8
+ /**
9
+ * Copyright (c) 2026 Shiva Deore (Taracod). Licensed under AGPL-3.0.
10
+ *
11
+ * cli/v4/uiBuild.ts — Aiden v4.1 Tier-3 UI build fingerprint.
12
+ *
13
+ * A single source-of-truth string the smokes can `require()` from
14
+ * the built artifact. Bumped by hand at the start of each tier-3
15
+ * sub-phase so smoke harnesses can pin against the expected build.
16
+ */
17
+ exports.AIDEN_UI_BUILD = 'v4.1-tier3-essentials';
18
+ /**
19
+ * Phase v4.1-skill-mining: build fingerprint for the auto-extract
20
+ * subsystem. Bumped per skill-mining sub-phase so smokes can pin
21
+ * against the expected build.
22
+ */
23
+ exports.AIDEN_SKILL_MINING_BUILD = 'v4.1-skill-mining';
24
+ /**
25
+ * Phase v4.1-reply-formatting: build fingerprint for the structured
26
+ * markdown rendering / citation footer / streaming stable-prefix
27
+ * subsystem. Render-layer only — no agent prompts or behavior change.
28
+ */
29
+ exports.AIDEN_REPLY_FORMAT_BUILD = 'v4.1-reply-formatting';
30
+ /**
31
+ * Phase v4.1-cross-platform: build fingerprint for the Linux / macOS
32
+ * compatibility pass — path helpers, audio backend detection, skill
33
+ * loader case-insensitive lookup, doctor checks per OS, CI matrix.
34
+ */
35
+ exports.AIDEN_CROSS_PLATFORM_BUILD = 'v4.1-cross-platform';
36
+ /**
37
+ * Phase v4.1-preship-cleanup: build fingerprint for the day-one
38
+ * polish batch — vitest baseline goes from 37 fails to 0, telegram
39
+ * 409 path gains a local-machine polling lock to prevent same-box
40
+ * rivals from racing.
41
+ */
42
+ exports.AIDEN_PRESHIP_BUILD = 'v4.1-preship-cleanup';
43
+ /** Predicate: is the citation footer enabled? Default off. */
44
+ function citationsEnabled() {
45
+ return process.env.AIDEN_CITATIONS === '1';
46
+ }
47
+ /**
48
+ * Predicate: are we running in MCP serve mode? When true, the
49
+ * stdout channel belongs to JSON-RPC and any UI write would corrupt
50
+ * the wire. Tier-3 UI helpers consult this before printing.
51
+ *
52
+ * The MCP server CLI (cli/v4/commands/mcp.ts) sets
53
+ * `process.env.AIDEN_MCP_SERVE = '1'` early in its boot path; that
54
+ * env-var check is intentionally cheap and safe to read often.
55
+ */
56
+ function isMcpServeMode() {
57
+ return process.env.AIDEN_MCP_SERVE === '1';
58
+ }
59
+ /**
60
+ * Predicate: is the legacy/no-UI flag in effect? Disables tier-3
61
+ * polish (autosuggest ghost text, inline status line, etc.) and
62
+ * falls back to pre-tier3.1 rendering. Set by `aiden --no-ui`.
63
+ */
64
+ function isNoUiMode() {
65
+ return process.env.AIDEN_NO_UI === '1';
66
+ }
67
+ /**
68
+ * Predicate: should slash-command icons render? Default OFF; opt-in
69
+ * via `AIDEN_UI_ICONS=1`. Lets users with emoji-friendly terminals
70
+ * recover the previous icon column.
71
+ */
72
+ function uiIconsEnabled() {
73
+ return process.env.AIDEN_UI_ICONS === '1';
74
+ }
@@ -0,0 +1,113 @@
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/voiceCli.ts — Phase v4.1-voice-cli
10
+ *
11
+ * `aiden voice <action>` top-level CLI subcommand. Three actions:
12
+ *
13
+ * doctor — print diagnostics: build, TTY, mic backend,
14
+ * TTS providers, current config. No mic open.
15
+ * tts <text> — synthesise + play one short clip. Real
16
+ * provider call.
17
+ * transcribe <f> — STT one audio file. Reuses the v4.1-3
18
+ * `whisper-transcribe` channel pipeline.
19
+ *
20
+ * Distinct from the `/voice` slash command (which mutates session
21
+ * state from inside the REPL). This subcommand exists so users can
22
+ * verify mic + speaker setup BEFORE entering the REPL — useful for
23
+ * first-run mic-permission grants on Windows where the OS prompts
24
+ * the first time the device is opened.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.AIDEN_VOICE_CLI_BUILD = void 0;
28
+ exports.runVoiceSubcommand = runVoiceSubcommand;
29
+ /* eslint-disable @typescript-eslint/no-explicit-any */
30
+ const node_fs_1 = require("node:fs");
31
+ const diagnostics_1 = require("../../core/v4/voice/diagnostics");
32
+ Object.defineProperty(exports, "AIDEN_VOICE_CLI_BUILD", { enumerable: true, get: function () { return diagnostics_1.AIDEN_VOICE_CLI_BUILD; } });
33
+ const tts_1 = require("../../core/voice/tts");
34
+ const whisper_transcribe_1 = require("../../core/channels/whisper-transcribe");
35
+ async function runVoiceSubcommand(action, args, opts = {}) {
36
+ const writeOut = opts.writeOut ?? ((t) => process.stdout.write(t));
37
+ const writeErr = opts.writeErr ?? ((t) => process.stderr.write(t));
38
+ switch (action) {
39
+ case 'doctor': {
40
+ const diag = await (0, diagnostics_1.collectVoiceDiagnostics)();
41
+ writeOut(`Aiden voice — ${diagnostics_1.AIDEN_VOICE_CLI_BUILD}\n`);
42
+ writeOut(` tty: ${diag.isTty ? 'yes' : 'no'}\n`);
43
+ writeOut(` enabled: ${diag.enabled ? 'yes' : 'no (refused — non-TTY stdin)'}\n`);
44
+ writeOut(` mic backend: ${diag.audio.backend}\n`);
45
+ writeOut(` mic active: ${diag.audio.active ? 'yes' : 'no'}\n`);
46
+ writeOut(` sox on PATH: ${diag.audio.soxOnPath ? 'yes' : 'no'}\n`);
47
+ writeOut(` mode: ${diag.config.mode}\n`);
48
+ writeOut(` tts voice: ${diag.config.ttsVoice}\n`);
49
+ writeOut(` beeps: ${diag.config.beepsEnabled ? 'on' : 'off'}\n`);
50
+ writeOut(` tts providers:\n`);
51
+ for (const p of diag.ttsProviders) {
52
+ const tag = p.available ? '✓' : '✗';
53
+ writeOut(` ${tag} ${p.name.padEnd(12)} ${p.note ?? ''}\n`);
54
+ }
55
+ // Mic-backend hint when nothing is installed.
56
+ if (diag.audio.backend === 'unavailable') {
57
+ writeOut(`\n Hint: install \`decibri\` (npm i decibri) for prebuilt mic capture,\n`);
58
+ writeOut(` OR install sox (https://sox.sourceforge.io/) + node-record-lpcm16.\n`);
59
+ }
60
+ return 0;
61
+ }
62
+ case 'tts': {
63
+ const text = args.join(' ').trim();
64
+ if (!text) {
65
+ writeErr(`Usage: aiden voice tts "<text>"\n`);
66
+ return 1;
67
+ }
68
+ const cleaned = (0, tts_1.cleanForTTS)(text);
69
+ if (!cleaned) {
70
+ writeErr(`Empty after cleanForTTS — nothing to speak.\n`);
71
+ return 1;
72
+ }
73
+ writeOut(`Synthesising via TTS chain (${cleaned.length} chars)...\n`);
74
+ const r = await (0, tts_1.synthesize)({ text: cleaned });
75
+ if (r.error) {
76
+ writeErr(`TTS failed: ${r.error}\n`);
77
+ return 1;
78
+ }
79
+ writeOut(`TTS ok — provider: ${r.provider}, ${r.durationMs}ms\n`);
80
+ return 0;
81
+ }
82
+ case 'transcribe': {
83
+ const filePath = args[0];
84
+ if (!filePath) {
85
+ writeErr(`Usage: aiden voice transcribe <audio-file>\n`);
86
+ return 1;
87
+ }
88
+ try {
89
+ await node_fs_1.promises.access(filePath);
90
+ }
91
+ catch {
92
+ writeErr(`File not found: ${filePath}\n`);
93
+ return 1;
94
+ }
95
+ writeOut(`Transcribing ${filePath}...\n`);
96
+ const r = await (0, whisper_transcribe_1.transcribeForChannel)({ filePath });
97
+ if (!r.success) {
98
+ writeErr(`Transcribe failed: ${r.error ?? 'unknown'}\n`);
99
+ return 1;
100
+ }
101
+ const conf = typeof r.avgLogprob === 'number'
102
+ ? ` (avgLogprob=${r.avgLogprob.toFixed(2)})`
103
+ : '';
104
+ writeOut(`Transcript${conf}:\n${r.text ?? ''}\n`);
105
+ return 0;
106
+ }
107
+ default: {
108
+ writeErr(`Unknown 'aiden voice' action: ${action}\n`);
109
+ writeErr(`Actions: doctor | tts <text> | transcribe <file>\n`);
110
+ return 1;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,196 @@
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/voicePromptApi.ts — Phase v4.1-voice-cli
10
+ *
11
+ * Wraps a default `ChatPromptApi` implementation with a raw-mode
12
+ * spacebar toggle for push-to-talk recording. When the user is at
13
+ * the prompt and presses Space:
14
+ *
15
+ * 1. Switch to raw mode + start `cliVoice.startRecording()`
16
+ * 2. Update the spinner: "🎤 recording (Space to stop, Esc to cancel)"
17
+ * 3. On second Space: stop and transcribe → return transcript
18
+ * 4. On Esc: cancel and return empty (caller falls back to text)
19
+ * 5. On any other character before the first Space: hand control
20
+ * back to the wrapped `inquirer` prompt so the user types
21
+ * normally
22
+ *
23
+ * Hard-refuses activation when `process.stdin.isTTY` is false. This
24
+ * is the MCP-stdio invariant — `aiden mcp serve` uses stdin as the
25
+ * JSON-RPC transport, and toggling raw mode there would corrupt
26
+ * every protocol frame. The refusal is silent in MCP context (the
27
+ * default `readLine` runs unchanged); explicit in REPL context (a
28
+ * stderr warning + fall-through).
29
+ *
30
+ * `selectSlashCommand` is delegated unchanged — slash commands
31
+ * still go through the inquirer dropdown.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.createVoicePromptApi = createVoicePromptApi;
35
+ exports.voiceModeAllowed = voiceModeAllowed;
36
+ const factory_1 = require("../../core/v4/logger/factory");
37
+ const KEY_SPACE = 0x20;
38
+ const KEY_ESC = 0x1b;
39
+ /** Build a prompt API that intercepts Space for push-to-talk and
40
+ * falls through to `inner` for normal text input. */
41
+ function createVoicePromptApi(opts) {
42
+ const logger = (opts.logger ?? (0, factory_1.noopLogger)()).child('voice-prompt');
43
+ const stdin = opts.stdin ?? process.stdin;
44
+ const stdout = opts.stdout ?? process.stdout;
45
+ return {
46
+ async readLine(prompt) {
47
+ // Hard refuse when stdin isn't a TTY. Voice mode requires raw
48
+ // mode; raw mode requires a TTY. MCP stdio mode hits this path
49
+ // when Claude Desktop spawns aiden — silently fall through.
50
+ if (!stdin.isTTY) {
51
+ return opts.inner.readLine(prompt);
52
+ }
53
+ const transcript = await waitForSpaceOrTypedInput({
54
+ prompt,
55
+ stdin,
56
+ stdout,
57
+ voice: opts.voice,
58
+ onStatus: opts.onStatus,
59
+ logger,
60
+ });
61
+ if (transcript === null) {
62
+ // User typed text — hand off to the regular prompt API. The
63
+ // first character is already in the typeahead via the buffer
64
+ // — `inner.readLine` reads from there.
65
+ return opts.inner.readLine(prompt);
66
+ }
67
+ if (transcript === '') {
68
+ // Cancelled — fall back to text prompt.
69
+ return opts.inner.readLine(prompt);
70
+ }
71
+ return transcript;
72
+ },
73
+ async selectSlashCommand(source) {
74
+ // Slash commands don't get voice intercept — they're a
75
+ // discrete dropdown.
76
+ return opts.inner.selectSlashCommand(source);
77
+ },
78
+ };
79
+ }
80
+ /** Wait for either Space (start recording) or any other char (fall
81
+ * through to text prompt). Returns:
82
+ * - the transcribed string when recording completes
83
+ * - '' when user cancels (Esc)
84
+ * - null when user types non-space (fall through to inner) */
85
+ async function waitForSpaceOrTypedInput(args) {
86
+ // Show a brief hint so users know voice mode is hot.
87
+ args.stdout.write(`${args.prompt}\x1b[2m(Space to talk)\x1b[0m `);
88
+ const stdin = args.stdin;
89
+ // Snapshot current raw mode state to restore on exit.
90
+ const wasRaw = !!stdin.isRaw;
91
+ if (!wasRaw)
92
+ stdin.setRawMode(true);
93
+ stdin.resume();
94
+ let result = undefined;
95
+ let recording = false;
96
+ let transcript = null;
97
+ let resolveOuter = null;
98
+ const cleanup = () => {
99
+ stdin.removeListener('data', onData);
100
+ stdin.removeListener('error', onError);
101
+ if (!wasRaw) {
102
+ try {
103
+ stdin.setRawMode(false);
104
+ }
105
+ catch { /* ignore */ }
106
+ }
107
+ stdin.pause();
108
+ };
109
+ const onData = (chunk) => {
110
+ if (chunk.length === 0)
111
+ return;
112
+ const code = chunk[0];
113
+ if (!recording) {
114
+ if (code === KEY_SPACE) {
115
+ // Start recording.
116
+ recording = true;
117
+ args.voice.startRecording().catch((err) => {
118
+ args.logger.warn('startRecording threw', { error: err.message });
119
+ });
120
+ }
121
+ else if (code === KEY_ESC) {
122
+ result = '';
123
+ cleanup();
124
+ resolveOuter?.('');
125
+ }
126
+ else if (code === 0x03) {
127
+ // Ctrl+C — propagate to inner via empty cancel.
128
+ result = '';
129
+ cleanup();
130
+ resolveOuter?.('');
131
+ }
132
+ else {
133
+ // Any other key — fall through to inner prompt. Push the
134
+ // byte back so inner reads it (best-effort; on Windows
135
+ // the unread() trick isn't reliable, so we just signal
136
+ // null and inner re-prompts).
137
+ result = null;
138
+ cleanup();
139
+ resolveOuter?.(null);
140
+ }
141
+ }
142
+ else {
143
+ // Already recording. Space stops; Esc cancels.
144
+ if (code === KEY_SPACE) {
145
+ args.voice.stopRecording().catch((err) => {
146
+ args.logger.warn('stopRecording threw', { error: err.message });
147
+ });
148
+ }
149
+ else if (code === KEY_ESC || code === 0x03) {
150
+ args.voice.cancel();
151
+ result = '';
152
+ cleanup();
153
+ resolveOuter?.('');
154
+ }
155
+ }
156
+ };
157
+ const onError = (err) => {
158
+ args.logger.warn('stdin error during voice prompt', { error: err.message });
159
+ cleanup();
160
+ resolveOuter?.(null);
161
+ };
162
+ // Voice handle's onTranscript wins the race when recording succeeds.
163
+ // We register a one-shot subscription via the existing callback by
164
+ // taking advantage of the fact that handle.startRecording resolves
165
+ // when transcribe completes — at that point transcript will be set
166
+ // through the host's status callback. To keep this module narrowly
167
+ // scoped, we POLL voice.getStatus() between awaits via a watcher.
168
+ const watcher = setInterval(() => {
169
+ const s = args.voice.getStatus();
170
+ args.onStatus?.(s);
171
+ // Recording finished naturally OR errored.
172
+ if (recording && s === 'idle') {
173
+ // Drain stdin and resolve. The transcript was forwarded via
174
+ // the host's onTranscript callback (set up in the cliVoice
175
+ // constructor); the host stitches it into the conversation.
176
+ // For the prompt-API contract we resolve with empty so the
177
+ // outer loop spins to the next iteration.
178
+ cleanup();
179
+ clearInterval(watcher);
180
+ resolveOuter?.(transcript ?? '');
181
+ }
182
+ }, 50);
183
+ stdin.on('data', onData);
184
+ stdin.on('error', onError);
185
+ return new Promise((resolve) => {
186
+ resolveOuter = (v) => {
187
+ clearInterval(watcher);
188
+ resolve(v);
189
+ };
190
+ });
191
+ }
192
+ /** Test-only helper: enforce the TTY guard. Returns true when voice
193
+ * mode is allowed to activate in this process. */
194
+ function voiceModeAllowed(stdin = process.stdin) {
195
+ return !!stdin.isTTY;
196
+ }
@@ -20,21 +20,27 @@ exports.DiscordAdapter = void 0;
20
20
  // - Graceful degradation: missing token → disabled, no crash
21
21
  const discord_js_1 = require("discord.js");
22
22
  const gateway_1 = require("../gateway");
23
+ const logger_1 = require("../v4/logger");
23
24
  class DiscordAdapter {
24
25
  constructor() {
25
26
  this.name = 'discord';
26
27
  this.client = null;
27
28
  this.healthy = false;
29
+ // Phase v4.1-1.3a — diagnostics route through the channel scope
30
+ // logger; ChannelManager.register injects it. Default noop keeps
31
+ // pre-attach calls silent.
32
+ this.log = (0, logger_1.noopLogger)();
28
33
  this.token = process.env.DISCORD_BOT_TOKEN ?? '';
29
34
  const rawGuilds = process.env.DISCORD_ALLOWED_GUILDS ?? '';
30
35
  const rawChannels = process.env.DISCORD_ALLOWED_CHANNELS ?? '';
31
36
  this.allowedGuilds = rawGuilds ? new Set(rawGuilds.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
32
37
  this.allowedChannels = rawChannels ? new Set(rawChannels.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
33
38
  }
39
+ attachLogger(logger) { this.log = logger; }
34
40
  // ── Lifecycle ──────────────────────────────────────────────
35
41
  async start() {
36
42
  if (!this.token) {
37
- console.log('[Discord] Disabled — set DISCORD_BOT_TOKEN to enable');
43
+ this.log.info('Disabled — set DISCORD_BOT_TOKEN to enable');
38
44
  return;
39
45
  }
40
46
  this.client = new discord_js_1.Client({
@@ -46,14 +52,14 @@ class DiscordAdapter {
46
52
  ],
47
53
  });
48
54
  this.client.once(discord_js_1.Events.ClientReady, async (c) => {
49
- console.log(`[Discord] Connected as ${c.user.tag}`);
55
+ this.log.info(`Connected as ${c.user.tag}`);
50
56
  this.healthy = true;
51
57
  // Register outbound delivery so gateway.deliver() and broadcast() work
52
58
  gateway_1.gateway.registerChannel('discord', async (msg) => {
53
59
  return this.deliverToChannel(msg.channelId, msg.text);
54
60
  });
55
61
  // Register slash commands globally (takes ~1h to propagate on first run)
56
- await this.registerSlashCommands(c.user.id).catch((e) => console.warn('[Discord] Slash command registration failed:', e.message));
62
+ await this.registerSlashCommands(c.user.id).catch((e) => this.log.warn(`Slash command registration failed: ${e.message}`));
57
63
  });
58
64
  this.client.on(discord_js_1.Events.MessageCreate, async (message) => {
59
65
  if (!this.shouldHandle(message.author.id, message.guildId, message.channelId, message.author.bot))
@@ -63,7 +69,7 @@ class DiscordAdapter {
63
69
  }
64
70
  catch { }
65
71
  const response = await this.processMessage(message.channelId, message.author.id, message.content);
66
- await message.reply(response.substring(0, 2000)).catch((e) => console.error('[Discord] Reply error:', e.message));
72
+ await message.reply(response.substring(0, 2000)).catch((e) => this.log.error(`Reply error: ${e.message}`));
67
73
  });
68
74
  this.client.on(discord_js_1.Events.InteractionCreate, async (interaction) => {
69
75
  if (!interaction.isChatInputCommand())
@@ -84,7 +90,7 @@ class DiscordAdapter {
84
90
  const prompt = interaction.options.getString('prompt', true);
85
91
  await interaction.deferReply();
86
92
  const response = await this.processMessage(channelId, userId, prompt);
87
- await interaction.editReply(response.substring(0, 2000)).catch((e) => console.error('[Discord] editReply error:', e.message));
93
+ await interaction.editReply(response.substring(0, 2000)).catch((e) => this.log.error(`editReply error: ${e.message}`));
88
94
  }
89
95
  else if (interaction.commandName === 'aiden-help') {
90
96
  await interaction.reply({
@@ -97,7 +103,7 @@ class DiscordAdapter {
97
103
  await this.client.login(this.token);
98
104
  }
99
105
  catch (e) {
100
- console.error('[Discord] Login failed:', e.message);
106
+ this.log.error(`Login failed: ${e.message}`);
101
107
  this.healthy = false;
102
108
  }
103
109
  }
@@ -108,7 +114,7 @@ class DiscordAdapter {
108
114
  await this.client.destroy();
109
115
  this.client = null;
110
116
  }
111
- console.log('[Discord] Disconnected');
117
+ this.log.info('Disconnected');
112
118
  }
113
119
  async send(channelId, message) {
114
120
  await this.deliverToChannel(channelId, message);
@@ -135,7 +141,7 @@ class DiscordAdapter {
135
141
  });
136
142
  }
137
143
  catch (e) {
138
- console.error('[Discord] routeMessage error:', e.message);
144
+ this.log.error(`routeMessage error: ${e.message}`);
139
145
  return '❌ Something went wrong. Try again.';
140
146
  }
141
147
  }
@@ -149,7 +155,7 @@ class DiscordAdapter {
149
155
  return false;
150
156
  }
151
157
  catch (e) {
152
- console.error('[Discord] Delivery error:', e.message);
158
+ this.log.error(`Delivery error: ${e.message}`);
153
159
  return false;
154
160
  }
155
161
  }
@@ -167,7 +173,7 @@ class DiscordAdapter {
167
173
  .toJSON(),
168
174
  ];
169
175
  await rest.put(discord_js_1.Routes.applicationCommands(appId), { body: commands });
170
- console.log('[Discord] Slash commands registered globally');
176
+ this.log.info('Slash commands registered globally');
171
177
  }
172
178
  }
173
179
  exports.DiscordAdapter = DiscordAdapter;
@@ -33,9 +33,12 @@ exports.EmailAdapter = void 0;
33
33
  // EMAIL_POLL_INTERVAL — polling interval in seconds (default 60)
34
34
  const nodemailer_1 = __importDefault(require("nodemailer"));
35
35
  const gateway_1 = require("../gateway");
36
+ const logger_1 = require("../v4/logger");
36
37
  class EmailAdapter {
37
38
  constructor() {
38
39
  this.name = 'email';
40
+ // Phase v4.1-1.3a — diagnostics route through scope logger.
41
+ this.log = (0, logger_1.noopLogger)();
39
42
  this.healthy = false;
40
43
  this.pollTimer = null;
41
44
  this.processedIds = new Set();
@@ -52,14 +55,15 @@ class EmailAdapter {
52
55
  this.allowedSenders = raw ? new Set(raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)) : new Set();
53
56
  this.pollIntervalMs = parseInt(process.env.EMAIL_POLL_INTERVAL ?? '60', 10) * 1000;
54
57
  }
58
+ attachLogger(logger) { this.log = logger; }
55
59
  // ── Lifecycle ──────────────────────────────────────────────
56
60
  async start() {
57
61
  if (!this.imapHost || !this.imapUser || !this.imapPassword) {
58
- console.log('[Email] Disabled — set EMAIL_IMAP_HOST, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD to enable');
62
+ this.log.info('Disabled — set EMAIL_IMAP_HOST, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD to enable');
59
63
  return;
60
64
  }
61
65
  if (!this.smtpHost || !this.smtpUser || !this.smtpPassword) {
62
- console.log('[Email] Disabled — set EMAIL_SMTP_HOST, EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD to enable');
66
+ this.log.info('Disabled — set EMAIL_SMTP_HOST, EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD to enable');
63
67
  return;
64
68
  }
65
69
  // Set up SMTP transporter
@@ -75,11 +79,11 @@ class EmailAdapter {
75
79
  // Verify SMTP connection
76
80
  const smtpOk = await this.transporter.verify().then(() => true).catch(() => false);
77
81
  if (!smtpOk) {
78
- console.log('[Email] Disabled — SMTP connection failed. Check EMAIL_SMTP_* settings.');
82
+ this.log.info('Disabled — SMTP connection failed. Check EMAIL_SMTP_* settings.');
79
83
  return;
80
84
  }
81
85
  this.healthy = true;
82
- console.log(`[Email] Ready — polling ${this.imapUser} every ${this.pollIntervalMs / 1000}s`);
86
+ this.log.info('Ready — polling ${this.imapUser} every ${this.pollIntervalMs / 1000}s');
83
87
  // Register outbound delivery
84
88
  gateway_1.gateway.registerChannel('email', async (msg) => {
85
89
  await this.send(msg.channelId, msg.text);
@@ -97,7 +101,7 @@ class EmailAdapter {
97
101
  }
98
102
  this.transporter = null;
99
103
  gateway_1.gateway.unregisterChannel('email');
100
- console.log('[Email] Disconnected');
104
+ this.log.info('Disconnected');
101
105
  }
102
106
  async send(target, message) {
103
107
  if (!this.transporter)
@@ -112,7 +116,7 @@ class EmailAdapter {
112
116
  });
113
117
  }
114
118
  catch (e) {
115
- console.error('[Email] send error:', e.message);
119
+ this.log.error(`send error:${e.message}`);
116
120
  }
117
121
  }
118
122
  isHealthy() { return this.healthy; }
@@ -191,7 +195,7 @@ class EmailAdapter {
191
195
  }
192
196
  catch (e) {
193
197
  if (this.healthy) {
194
- console.error('[Email] poll error:', e.message);
198
+ this.log.error(`poll error:${e.message}`);
195
199
  }
196
200
  }
197
201
  finally {
@@ -217,7 +221,7 @@ class EmailAdapter {
217
221
  });
218
222
  }
219
223
  catch (e) {
220
- console.error('[Email] reply error:', e.message);
224
+ this.log.error(`reply error:${e.message}`);
221
225
  }
222
226
  }
223
227
  extractText(raw) {
@@ -245,7 +249,7 @@ class EmailAdapter {
245
249
  });
246
250
  }
247
251
  catch (e) {
248
- console.error('[Email] routeMessage error:', e.message);
252
+ this.log.error(`routeMessage error:${e.message}`);
249
253
  return 'Something went wrong processing your email. Please try again.';
250
254
  }
251
255
  }
@@ -26,9 +26,12 @@ exports.IMessageAdapter = void 0;
26
26
  const axios_1 = __importDefault(require("axios"));
27
27
  const ws_1 = require("ws");
28
28
  const gateway_1 = require("../gateway");
29
+ const logger_1 = require("../v4/logger");
29
30
  class IMessageAdapter {
30
31
  constructor() {
31
32
  this.name = 'imessage';
33
+ // Phase v4.1-1.3a — diagnostics route through scope logger.
34
+ this.log = (0, logger_1.noopLogger)();
32
35
  this.healthy = false;
33
36
  this.ws = null;
34
37
  this.reconnectTimer = null;
@@ -37,20 +40,21 @@ class IMessageAdapter {
37
40
  const raw = process.env.BLUEBUBBLES_ALLOWED_NUMBERS ?? '';
38
41
  this.allowedNumbers = raw ? new Set(raw.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
39
42
  }
43
+ attachLogger(logger) { this.log = logger; }
40
44
  // ── Lifecycle ──────────────────────────────────────────────
41
45
  async start() {
42
46
  if (!this.baseUrl || !this.password) {
43
- console.log('[iMessage] Disabled — set BLUEBUBBLES_URL and BLUEBUBBLES_PASSWORD to enable');
47
+ this.log.info('Disabled — set BLUEBUBBLES_URL and BLUEBUBBLES_PASSWORD to enable');
44
48
  return;
45
49
  }
46
50
  // Verify BlueBubbles is reachable
47
51
  const reachable = await this.checkHealth();
48
52
  if (!reachable) {
49
- console.log(`[iMessage] Disabled — BlueBubbles server not reachable at ${this.baseUrl}`);
53
+ this.log.info('Disabled — BlueBubbles server not reachable at ${this.baseUrl}');
50
54
  return;
51
55
  }
52
56
  this.healthy = true;
53
- console.log(`[iMessage] Connected to BlueBubbles at ${this.baseUrl}`);
57
+ this.log.info('Connected to BlueBubbles at ${this.baseUrl}');
54
58
  // Register outbound delivery
55
59
  gateway_1.gateway.registerChannel('imessage', async (msg) => {
56
60
  await this.send(msg.channelId, msg.text);
@@ -70,7 +74,7 @@ class IMessageAdapter {
70
74
  this.ws = null;
71
75
  }
72
76
  gateway_1.gateway.unregisterChannel('imessage');
73
- console.log('[iMessage] Disconnected');
77
+ this.log.info('Disconnected');
74
78
  }
75
79
  async send(target, message) {
76
80
  if (!this.healthy)
@@ -82,7 +86,7 @@ class IMessageAdapter {
82
86
  });
83
87
  }
84
88
  catch (e) {
85
- console.error('[iMessage] send error:', e.message);
89
+ this.log.error(`send error:${e.message}`);
86
90
  }
87
91
  }
88
92
  isHealthy() { return this.healthy; }
@@ -105,7 +109,7 @@ class IMessageAdapter {
105
109
  const wsUrl = this.baseUrl.replace(/^http/, 'ws');
106
110
  this.ws = new ws_1.WebSocket(`${wsUrl}?password=${encodeURIComponent(this.password)}`);
107
111
  this.ws.on('open', () => {
108
- console.log('[iMessage] WebSocket connected');
112
+ this.log.info('WebSocket connected');
109
113
  });
110
114
  this.ws.on('message', async (raw) => {
111
115
  try {
@@ -127,11 +131,11 @@ class IMessageAdapter {
127
131
  await this.send(chatId || sender, response);
128
132
  }
129
133
  catch (e) {
130
- console.error('[iMessage] message parse error:', e.message);
134
+ this.log.error(`message parse error:${e.message}`);
131
135
  }
132
136
  });
133
137
  this.ws.on('error', (e) => {
134
- console.error('[iMessage] WebSocket error:', e.message);
138
+ this.log.error(`WebSocket error:${e.message}`);
135
139
  });
136
140
  this.ws.on('close', () => {
137
141
  if (this.healthy) {
@@ -156,7 +160,7 @@ class IMessageAdapter {
156
160
  });
157
161
  }
158
162
  catch (e) {
159
- console.error('[iMessage] routeMessage error:', e.message);
163
+ this.log.error(`routeMessage error:${e.message}`);
160
164
  return '❌ Something went wrong. Try again.';
161
165
  }
162
166
  }