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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +421 -5
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- 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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
this.log.info('Disabled — SMTP connection failed. Check EMAIL_SMTP_* settings.');
|
|
79
83
|
return;
|
|
80
84
|
}
|
|
81
85
|
this.healthy = true;
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
this.log.info('Disabled — BlueBubbles server not reachable at ${this.baseUrl}');
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
this.healthy = true;
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
this.log.error(`message parse error:${e.message}`);
|
|
131
135
|
}
|
|
132
136
|
});
|
|
133
137
|
this.ws.on('error', (e) => {
|
|
134
|
-
|
|
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
|
-
|
|
163
|
+
this.log.error(`routeMessage error:${e.message}`);
|
|
160
164
|
return '❌ Something went wrong. Try again.';
|
|
161
165
|
}
|
|
162
166
|
}
|