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
|
@@ -5,15 +5,31 @@
|
|
|
5
5
|
// ============================================================
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.channelManager = exports.ChannelManager = void 0;
|
|
8
|
-
|
|
8
|
+
const logger_1 = require("../v4/logger");
|
|
9
9
|
class ChannelManager {
|
|
10
|
-
constructor() {
|
|
10
|
+
constructor(opts = {}) {
|
|
11
11
|
this.adapters = new Map();
|
|
12
12
|
this.lastActivity = new Map();
|
|
13
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
13
14
|
}
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Phase v4.1-1.3a — late-binding logger setter. Lets the singleton
|
|
17
|
+
* pick up a real logger after construction (api/server.ts boot path
|
|
18
|
+
* imports the singleton; we can't change its constructor without
|
|
19
|
+
* breaking every existing import site).
|
|
20
|
+
*/
|
|
21
|
+
attachLogger(logger) {
|
|
22
|
+
this.log = logger;
|
|
23
|
+
}
|
|
24
|
+
/** Register an adapter — must be called before startAll(). */
|
|
15
25
|
register(adapter) {
|
|
16
26
|
this.adapters.set(adapter.name, adapter);
|
|
27
|
+
// Phase v4.1-1.3a — hand the adapter a scoped child logger so its
|
|
28
|
+
// own diagnostics route through the same sink chain as the manager.
|
|
29
|
+
// Adapters that haven't been migrated yet skip silently (no method).
|
|
30
|
+
if (typeof adapter.attachLogger === 'function') {
|
|
31
|
+
adapter.attachLogger(this.log.child(adapter.name));
|
|
32
|
+
}
|
|
17
33
|
}
|
|
18
34
|
/**
|
|
19
35
|
* Start all registered adapters.
|
|
@@ -29,11 +45,13 @@ class ChannelManager {
|
|
|
29
45
|
results.push({ name, status });
|
|
30
46
|
}
|
|
31
47
|
catch (error) {
|
|
32
|
-
|
|
48
|
+
this.log.error(`${name} failed to start: ${error.message}`);
|
|
33
49
|
results.push({ name, status: 'failed', error: String(error.message) });
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
|
-
// Summary line
|
|
52
|
+
// Summary line — single info record so log files capture the
|
|
53
|
+
// boot snapshot. CLI mode: routes to file only (REPL stays clean).
|
|
54
|
+
// Serve mode: routes to NDJSON stdout for log aggregators.
|
|
37
55
|
const summary = results
|
|
38
56
|
.map(r => {
|
|
39
57
|
if (r.status === 'started')
|
|
@@ -44,7 +62,7 @@ class ChannelManager {
|
|
|
44
62
|
})
|
|
45
63
|
.join(' | ');
|
|
46
64
|
if (results.length > 0)
|
|
47
|
-
|
|
65
|
+
this.log.info(`startup: ${summary}`);
|
|
48
66
|
return results;
|
|
49
67
|
}
|
|
50
68
|
/** Gracefully stop all adapters — called on SIGTERM / shutdown */
|
|
@@ -54,7 +72,7 @@ class ChannelManager {
|
|
|
54
72
|
await adapter.stop();
|
|
55
73
|
}
|
|
56
74
|
catch (e) {
|
|
57
|
-
|
|
75
|
+
this.log.error(`Error stopping ${adapter.name}: ${e.message}`);
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.HARD_CHAR_CAP = exports.MAX_PDF_BYTES = void 0;
|
|
8
|
+
exports.extractPdfForChannel = extractPdfForChannel;
|
|
9
|
+
// core/channels/pdf-extract.ts — Phase v4.1-4.
|
|
10
|
+
//
|
|
11
|
+
// Channel-side adapter for inbound PDFs. Wraps the existing
|
|
12
|
+
// `core/fileIngestion.ts` `extractPDF` function with the policies
|
|
13
|
+
// the Telegram adapter cares about:
|
|
14
|
+
//
|
|
15
|
+
// 1. 20 MB pre-download size cap matching Telegram's documented
|
|
16
|
+
// Bot API getFile attachment limit. Refusing here keeps the
|
|
17
|
+
// polling loop from spending bandwidth on a payload Telegram
|
|
18
|
+
// would refuse to deliver anyway.
|
|
19
|
+
// 2. Token-budget truncation. The agent loop has to fit the PDF
|
|
20
|
+
// content INSIDE the user turn alongside system prompts, prior
|
|
21
|
+
// history, and a reserve for the response. We cap injected text
|
|
22
|
+
// at the lesser of:
|
|
23
|
+
// - 50,000 characters (hard ceiling — keeps pathological
|
|
24
|
+
// 200-page PDFs from blowing the budget on small models)
|
|
25
|
+
// - (modelContextWindow - 8K reserved-for-response) * 4
|
|
26
|
+
// chars/token (rough OpenAI heuristic, errs on the safe side)
|
|
27
|
+
// and report `{ truncated: true, originalChars }` so the channel
|
|
28
|
+
// adapter can append a "PDF truncated to fit context, original
|
|
29
|
+
// was N chars" note inside the agent annotation.
|
|
30
|
+
// 3. Result-shape contract that maps onto the bracketed user-turn
|
|
31
|
+
// annotation the adapter emits. Failures return `success:false`
|
|
32
|
+
// with a human-readable `error` so the agent gets a directive
|
|
33
|
+
// ("[transcription failed: …. Apologize and ask them to send a
|
|
34
|
+
// shorter file.]") instead of an empty message.
|
|
35
|
+
//
|
|
36
|
+
// Logger comes from the v4.1-1.3a contract; defaults to a noop sink.
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const fileIngestion_1 = require("../fileIngestion");
|
|
39
|
+
const logger_1 = require("../v4/logger");
|
|
40
|
+
// 20 MiB. Telegram's documented Bot API attachment download limit.
|
|
41
|
+
// Anything bigger would have been refused by getFile upstream, so
|
|
42
|
+
// we reject here without spending bandwidth on the round-trip.
|
|
43
|
+
exports.MAX_PDF_BYTES = 20 * 1024 * 1024;
|
|
44
|
+
/** Hard ceiling on injected PDF text — protects small-context models. */
|
|
45
|
+
exports.HARD_CHAR_CAP = 50000;
|
|
46
|
+
/** Reserved tokens for the agent's response when computing context budget. */
|
|
47
|
+
const RESPONSE_RESERVED_TOKENS = 8000;
|
|
48
|
+
/** Conservative chars-per-token estimate for budget math. */
|
|
49
|
+
const CHARS_PER_TOKEN = 4;
|
|
50
|
+
/**
|
|
51
|
+
* Extract a PDF and return text bounded by the channel-layer's
|
|
52
|
+
* truncation policy. Never throws — failures land on
|
|
53
|
+
* `success: false` with a human-readable `error`.
|
|
54
|
+
*/
|
|
55
|
+
async function extractPdfForChannel(opts) {
|
|
56
|
+
const log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
57
|
+
const cap = opts.maxBytesOverride ?? exports.MAX_PDF_BYTES;
|
|
58
|
+
// ── 1. Size precheck ────────────────────────────────────────────
|
|
59
|
+
let sizeBytes;
|
|
60
|
+
try {
|
|
61
|
+
const st = await node_fs_1.promises.stat(opts.filePath);
|
|
62
|
+
sizeBytes = st.size;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
log.warn('pdf file not readable', { path: opts.filePath, error: e?.message });
|
|
66
|
+
return { success: false, truncated: false, error: `pdf file not readable: ${e?.message ?? 'unknown error'}` };
|
|
67
|
+
}
|
|
68
|
+
if (sizeBytes > cap) {
|
|
69
|
+
log.warn('pdf file too large', { sizeBytes, cap });
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
truncated: false,
|
|
73
|
+
error: `PDF too large: ${(sizeBytes / (1024 * 1024)).toFixed(1)} MB ` +
|
|
74
|
+
`(cap is ${(cap / (1024 * 1024)).toFixed(0)} MB).`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// ── 2. Hand off to the local extractor ──────────────────────────
|
|
78
|
+
const extractor = opts.extractFn ?? fileIngestion_1.extractPDF;
|
|
79
|
+
let extracted;
|
|
80
|
+
try {
|
|
81
|
+
extracted = await extractor(opts.filePath);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
log.error('pdf extraction threw', { error: e?.message ?? String(e) });
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
truncated: false,
|
|
88
|
+
error: `pdf extraction failed: ${e?.message ?? 'unknown error'}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const fullText = (extracted.text ?? '').trim();
|
|
92
|
+
if (!fullText) {
|
|
93
|
+
log.warn('pdf extracted empty text', { pageCount: extracted.pageCount });
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
truncated: false,
|
|
97
|
+
pageCount: extracted.pageCount,
|
|
98
|
+
error: 'pdf extraction returned empty text (scanned image PDF?)',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ── 3. Truncation budget ────────────────────────────────────────
|
|
102
|
+
const charBudget = computeCharBudget(opts.modelContextWindow);
|
|
103
|
+
const originalChars = fullText.length;
|
|
104
|
+
if (originalChars <= charBudget) {
|
|
105
|
+
log.info('pdf extracted', {
|
|
106
|
+
pageCount: extracted.pageCount,
|
|
107
|
+
chars: originalChars,
|
|
108
|
+
truncated: false,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
text: fullText,
|
|
113
|
+
truncated: false,
|
|
114
|
+
originalChars,
|
|
115
|
+
pageCount: extracted.pageCount,
|
|
116
|
+
wordCount: extracted.wordCount,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Truncate to the budget — slice on a sentence/paragraph boundary
|
|
120
|
+
// when one's available within the last 1 KB of the budget so we
|
|
121
|
+
// don't mid-word the agent's view of the text.
|
|
122
|
+
const truncatedText = truncateOnBoundary(fullText, charBudget);
|
|
123
|
+
log.info('pdf extracted (truncated)', {
|
|
124
|
+
pageCount: extracted.pageCount,
|
|
125
|
+
originalChars,
|
|
126
|
+
finalChars: truncatedText.length,
|
|
127
|
+
budget: charBudget,
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
text: truncatedText,
|
|
132
|
+
truncated: true,
|
|
133
|
+
originalChars,
|
|
134
|
+
pageCount: extracted.pageCount,
|
|
135
|
+
wordCount: countWords(truncatedText),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Compute the truncation budget. When `modelContextWindow` is given,
|
|
140
|
+
* subtract 8K reserved-for-response tokens, multiply by 4 chars/token,
|
|
141
|
+
* and cap at the hard 50K ceiling. When not given, fall back to the
|
|
142
|
+
* hard ceiling. Always returns at least 4 KB so a tiny-context model
|
|
143
|
+
* doesn't end up with nothing.
|
|
144
|
+
*/
|
|
145
|
+
function computeCharBudget(modelContextWindow) {
|
|
146
|
+
if (typeof modelContextWindow !== 'number' || !Number.isFinite(modelContextWindow) || modelContextWindow <= 0) {
|
|
147
|
+
return exports.HARD_CHAR_CAP;
|
|
148
|
+
}
|
|
149
|
+
const usableTokens = Math.max(0, modelContextWindow - RESPONSE_RESERVED_TOKENS);
|
|
150
|
+
const usableChars = usableTokens * CHARS_PER_TOKEN;
|
|
151
|
+
const budget = Math.min(exports.HARD_CHAR_CAP, usableChars);
|
|
152
|
+
return Math.max(budget, 4000);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Slice `text` at `cap` characters but try to land on the last newline
|
|
156
|
+
* in the trailing 1 KB of the cap, falling back to the last sentence
|
|
157
|
+
* terminator, finally a hard cut.
|
|
158
|
+
*/
|
|
159
|
+
function truncateOnBoundary(text, cap) {
|
|
160
|
+
if (text.length <= cap)
|
|
161
|
+
return text;
|
|
162
|
+
const window = text.slice(0, cap);
|
|
163
|
+
const tailStart = Math.max(0, cap - 1024);
|
|
164
|
+
const lastNewline = window.lastIndexOf('\n', cap);
|
|
165
|
+
if (lastNewline >= tailStart)
|
|
166
|
+
return window.slice(0, lastNewline);
|
|
167
|
+
// Match common sentence terminators; English-centric but Devanagari
|
|
168
|
+
// and Latin punctuation both work since `.` is in the regex.
|
|
169
|
+
const terminator = window.slice(tailStart).search(/[.!?।]\s/);
|
|
170
|
+
if (terminator >= 0) {
|
|
171
|
+
const cutAt = tailStart + terminator + 1;
|
|
172
|
+
return window.slice(0, cutAt);
|
|
173
|
+
}
|
|
174
|
+
return window;
|
|
175
|
+
}
|
|
176
|
+
function countWords(text) {
|
|
177
|
+
if (!text)
|
|
178
|
+
return 0;
|
|
179
|
+
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
180
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MAX_PHOTO_BYTES = void 0;
|
|
8
|
+
exports.analyzePhotoForChannel = analyzePhotoForChannel;
|
|
9
|
+
// core/channels/photo-vision.ts — Phase v4.1-4.
|
|
10
|
+
//
|
|
11
|
+
// Channel-side adapter for inbound photos. Wraps the existing
|
|
12
|
+
// `core/visionAnalyze.ts` chain with Telegram-specific concerns:
|
|
13
|
+
//
|
|
14
|
+
// 1. 25 MB pre-download size cap. Mirrors the voice path's policy
|
|
15
|
+
// (Telegram Bot API getFile limit is the binding constraint).
|
|
16
|
+
// Refusing here saves the round-trip when we already know the
|
|
17
|
+
// payload is too big for the providers.
|
|
18
|
+
// 2. Mode decision — `native` vs `text` — based on whether the
|
|
19
|
+
// currently active model carries a `supportsVision: true` flag in
|
|
20
|
+
// `providers/v4/modelCatalog.ts`. When vision is supported, the
|
|
21
|
+
// channel adapter attaches the file path on the user turn so the
|
|
22
|
+
// provider sees pixels directly. Otherwise we pre-analyze with
|
|
23
|
+
// the auxiliary `analyzeImage` chain (Anthropic / OpenAI / Ollama
|
|
24
|
+
// llava) and prepend a description annotation — same "smuggle
|
|
25
|
+
// into agent turn" pattern as voice transcripts.
|
|
26
|
+
// 3. Result-shape contract that matches the channel adapter's
|
|
27
|
+
// expectations so the Telegram adapter's `handlePhotoMessage`
|
|
28
|
+
// can branch on `mode` and assemble the right outbound payload.
|
|
29
|
+
//
|
|
30
|
+
// Logger comes from the v4.1-1.3a contract; defaults to a noop sink
|
|
31
|
+
// so anything that calls into this module without a wired logger
|
|
32
|
+
// stays REPL-clean.
|
|
33
|
+
const node_fs_1 = require("node:fs");
|
|
34
|
+
const modelCatalog_1 = require("../../providers/v4/modelCatalog");
|
|
35
|
+
const visionAnalyze_1 = require("../visionAnalyze");
|
|
36
|
+
const logger_1 = require("../v4/logger");
|
|
37
|
+
// 25 MiB. Matches the voice cap and the OpenAI / Anthropic vision
|
|
38
|
+
// request-size envelopes; Telegram's getFile cap is 20 MB so a 25 MB
|
|
39
|
+
// payload would already have been rejected upstream — but keeping
|
|
40
|
+
// these caps consistent simplifies the operator mental model.
|
|
41
|
+
exports.MAX_PHOTO_BYTES = 25 * 1024 * 1024;
|
|
42
|
+
/**
|
|
43
|
+
* Default text-mode prompt. Single source so smokes and the adapter
|
|
44
|
+
* agree on what gets sent to the auxiliary vision chain. Phrased
|
|
45
|
+
* for the agent's perspective — the description ends up bracketed
|
|
46
|
+
* inside an "[The user sent an image. Description: ...]" annotation.
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_DESCRIBE_PROMPT = 'Describe everything visible in this image in detail. Include any ' +
|
|
49
|
+
'text, code, layout, objects, people, colors, and any other notable ' +
|
|
50
|
+
'visual information.';
|
|
51
|
+
/**
|
|
52
|
+
* Decide how an inbound photo should be presented to the model and
|
|
53
|
+
* (in text mode) generate the description the channel adapter will
|
|
54
|
+
* smuggle into the agent's user turn.
|
|
55
|
+
*
|
|
56
|
+
* Never throws — failures land on `success: false` with a
|
|
57
|
+
* human-readable `error`. Callers downstream decide whether to:
|
|
58
|
+
* - hand a `[The user sent an image but description failed: ...]`
|
|
59
|
+
* directive to the agent, or
|
|
60
|
+
* - render a friendly user-facing reject reply.
|
|
61
|
+
*/
|
|
62
|
+
async function analyzePhotoForChannel(opts) {
|
|
63
|
+
const log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
64
|
+
const cap = opts.maxBytesOverride ?? exports.MAX_PHOTO_BYTES;
|
|
65
|
+
// ── 1. Size precheck ────────────────────────────────────────────
|
|
66
|
+
let sizeBytes;
|
|
67
|
+
try {
|
|
68
|
+
const st = await node_fs_1.promises.stat(opts.filePath);
|
|
69
|
+
sizeBytes = st.size;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
log.warn('photo file not readable', { path: opts.filePath, error: e?.message });
|
|
73
|
+
return { success: false, mode: 'text', error: `photo file not readable: ${e?.message ?? 'unknown error'}` };
|
|
74
|
+
}
|
|
75
|
+
if (sizeBytes > cap) {
|
|
76
|
+
log.warn('photo file too large', { sizeBytes, cap });
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
mode: 'text',
|
|
80
|
+
error: `Photo too large: ${(sizeBytes / (1024 * 1024)).toFixed(1)} MB ` +
|
|
81
|
+
`(cap is ${(cap / (1024 * 1024)).toFixed(0)} MB).`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ── 2. Mode decision ────────────────────────────────────────────
|
|
85
|
+
const mode = decideMode(opts.providerId, opts.modelId, log);
|
|
86
|
+
if (mode === 'native') {
|
|
87
|
+
log.info('photo routed native', {
|
|
88
|
+
providerId: opts.providerId,
|
|
89
|
+
modelId: opts.modelId,
|
|
90
|
+
sizeBytes,
|
|
91
|
+
});
|
|
92
|
+
return { success: true, mode: 'native', nativePath: opts.filePath };
|
|
93
|
+
}
|
|
94
|
+
// ── 3. Text mode: pre-analyze via the auxiliary vision chain ────
|
|
95
|
+
const analyze = opts.analyzeFn ?? visionAnalyze_1.analyzeImage;
|
|
96
|
+
try {
|
|
97
|
+
const visionResult = await analyze(opts.filePath, opts.prompt ?? DEFAULT_DESCRIBE_PROMPT, log);
|
|
98
|
+
const description = (visionResult.description ?? '').trim();
|
|
99
|
+
if (!description) {
|
|
100
|
+
log.warn('vision chain returned empty description', { provider: visionResult.provider });
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
mode: 'text',
|
|
104
|
+
error: 'vision chain returned an empty description',
|
|
105
|
+
provider: visionResult.provider,
|
|
106
|
+
modelUsed: visionResult.modelUsed,
|
|
107
|
+
durationMs: visionResult.durationMs,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
mode: 'text',
|
|
113
|
+
description,
|
|
114
|
+
provider: visionResult.provider,
|
|
115
|
+
modelUsed: visionResult.modelUsed,
|
|
116
|
+
durationMs: visionResult.durationMs,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
log.error('vision chain threw', { error: e?.message ?? String(e) });
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
mode: 'text',
|
|
124
|
+
error: `vision chain failed: ${e?.message ?? 'unknown error'}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve native-vs-text routing for the active model. Returns
|
|
130
|
+
* `'text'` whenever:
|
|
131
|
+
* - either id is missing (caller didn't tell us)
|
|
132
|
+
* - the model isn't in `MODEL_CATALOG` (registry drift)
|
|
133
|
+
* - the model's `supportsVision` is false
|
|
134
|
+
* - the lookup throws for any reason
|
|
135
|
+
*
|
|
136
|
+
* `'text'` is the safe fallback — it always works because the
|
|
137
|
+
* auxiliary `analyzeImage` chain runs against its own provider keys
|
|
138
|
+
* independent of whatever the user has selected for the agent loop.
|
|
139
|
+
*/
|
|
140
|
+
function decideMode(providerId, modelId, log) {
|
|
141
|
+
if (!providerId || !modelId) {
|
|
142
|
+
log.debug('photo mode: missing provider/model id, defaulting to text');
|
|
143
|
+
return 'text';
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const entry = (0, modelCatalog_1.findModel)(providerId, modelId);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
log.debug('photo mode: model not in catalog, defaulting to text', { providerId, modelId });
|
|
149
|
+
return 'text';
|
|
150
|
+
}
|
|
151
|
+
return entry.supportsVision ? 'native' : 'text';
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
log.debug('photo mode: lookup threw, defaulting to text', { error: e?.message });
|
|
155
|
+
return 'text';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -25,9 +25,12 @@ exports.SignalAdapter = void 0;
|
|
|
25
25
|
// SIGNAL_ALLOWED_NUMBERS — optional comma-separated allowlist
|
|
26
26
|
const axios_1 = __importDefault(require("axios"));
|
|
27
27
|
const gateway_1 = require("../gateway");
|
|
28
|
+
const logger_1 = require("../v4/logger");
|
|
28
29
|
class SignalAdapter {
|
|
29
30
|
constructor() {
|
|
30
31
|
this.name = 'signal';
|
|
32
|
+
// Phase v4.1-1.3a — diagnostics route through scope logger.
|
|
33
|
+
this.log = (0, logger_1.noopLogger)();
|
|
31
34
|
this.healthy = false;
|
|
32
35
|
this.pollTimer = null;
|
|
33
36
|
this.lastReceived = 0;
|
|
@@ -36,21 +39,22 @@ class SignalAdapter {
|
|
|
36
39
|
const raw = process.env.SIGNAL_ALLOWED_NUMBERS ?? '';
|
|
37
40
|
this.allowedNumbers = raw ? new Set(raw.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
|
|
38
41
|
}
|
|
42
|
+
attachLogger(logger) { this.log = logger; }
|
|
39
43
|
// ── Lifecycle ──────────────────────────────────────────────
|
|
40
44
|
async start() {
|
|
41
45
|
if (!this.myNumber) {
|
|
42
|
-
|
|
46
|
+
this.log.info('Disabled — set SIGNAL_PHONE_NUMBER to enable');
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
49
|
// Verify signal-cli is reachable
|
|
46
50
|
const reachable = await this.checkHealth();
|
|
47
51
|
if (!reachable) {
|
|
48
|
-
|
|
52
|
+
this.log.info('Disabled — signal-cli-rest-api not reachable at ${this.baseUrl}');
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
this.healthy = true;
|
|
52
56
|
this.lastReceived = Date.now();
|
|
53
|
-
|
|
57
|
+
this.log.info('Connected — polling ${this.baseUrl}');
|
|
54
58
|
// Register outbound delivery
|
|
55
59
|
gateway_1.gateway.registerChannel('signal', async (msg) => {
|
|
56
60
|
await this.send(msg.channelId, msg.text);
|
|
@@ -66,7 +70,7 @@ class SignalAdapter {
|
|
|
66
70
|
this.pollTimer = null;
|
|
67
71
|
}
|
|
68
72
|
gateway_1.gateway.unregisterChannel('signal');
|
|
69
|
-
|
|
73
|
+
this.log.info('Disconnected');
|
|
70
74
|
}
|
|
71
75
|
async send(target, message) {
|
|
72
76
|
if (!this.healthy)
|
|
@@ -75,7 +79,7 @@ class SignalAdapter {
|
|
|
75
79
|
await axios_1.default.post(`${this.baseUrl}/v2/send`, { message, number: this.myNumber, recipients: [target] }, { timeout: 10000 });
|
|
76
80
|
}
|
|
77
81
|
catch (e) {
|
|
78
|
-
|
|
82
|
+
this.log.error(`send error:${e.message}`);
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
isHealthy() { return this.healthy; }
|
|
@@ -112,7 +116,7 @@ class SignalAdapter {
|
|
|
112
116
|
catch (e) {
|
|
113
117
|
// Don't spam logs on transient poll errors
|
|
114
118
|
if (this.healthy) {
|
|
115
|
-
|
|
119
|
+
this.log.error(`poll error:${e.message}`);
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
@@ -132,7 +136,7 @@ class SignalAdapter {
|
|
|
132
136
|
});
|
|
133
137
|
}
|
|
134
138
|
catch (e) {
|
|
135
|
-
|
|
139
|
+
this.log.error(`routeMessage error:${e.message}`);
|
|
136
140
|
return '❌ Something went wrong. Try again.';
|
|
137
141
|
}
|
|
138
142
|
}
|
|
@@ -27,11 +27,13 @@ exports.SlackAdapter = void 0;
|
|
|
27
27
|
// - Graceful degradation: missing creds → disabled, no crash
|
|
28
28
|
const bolt_1 = require("@slack/bolt");
|
|
29
29
|
const gateway_1 = require("../gateway");
|
|
30
|
+
const logger_1 = require("../v4/logger");
|
|
30
31
|
class SlackAdapter {
|
|
31
32
|
constructor() {
|
|
32
33
|
this.name = 'slack';
|
|
33
34
|
this.app = null;
|
|
34
35
|
this.healthy = false;
|
|
36
|
+
this.log = (0, logger_1.noopLogger)(); // Phase v4.1-1.3a — wired by ChannelManager.register
|
|
35
37
|
this.botToken = process.env.SLACK_BOT_TOKEN ?? '';
|
|
36
38
|
this.signingSecret = process.env.SLACK_SIGNING_SECRET ?? '';
|
|
37
39
|
this.appToken = process.env.SLACK_APP_TOKEN ?? '';
|
|
@@ -40,10 +42,11 @@ class SlackAdapter {
|
|
|
40
42
|
? new Set(rawChannels.split(',').map(s => s.trim()).filter(Boolean))
|
|
41
43
|
: new Set();
|
|
42
44
|
}
|
|
45
|
+
attachLogger(logger) { this.log = logger; }
|
|
43
46
|
// ── Lifecycle ──────────────────────────────────────────────
|
|
44
47
|
async start() {
|
|
45
48
|
if (!this.botToken || !this.signingSecret) {
|
|
46
|
-
|
|
49
|
+
this.log.info('Disabled — set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET to enable');
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
const useSocketMode = !!this.appToken;
|
|
@@ -63,7 +66,7 @@ class SlackAdapter {
|
|
|
63
66
|
if (this.allowedChannels.size > 0 && !this.allowedChannels.has(msg.channel))
|
|
64
67
|
return;
|
|
65
68
|
const response = await this.processMessage(msg.channel, msg.user ?? 'unknown', msg.text ?? '');
|
|
66
|
-
await say({ text: response, thread_ts: msg.ts }).catch((e) =>
|
|
69
|
+
await say({ text: response, thread_ts: msg.ts }).catch((e) => this.log.error(`say error: ${e.message}`));
|
|
67
70
|
});
|
|
68
71
|
// ── App mentions (@Aiden) ────────────────────────────────
|
|
69
72
|
this.app.event('app_mention', async ({ event, say }) => {
|
|
@@ -72,7 +75,7 @@ class SlackAdapter {
|
|
|
72
75
|
// Strip the @mention prefix from the message
|
|
73
76
|
const text = (event.text ?? '').replace(/<@[^>]+>/g, '').trim();
|
|
74
77
|
const response = await this.processMessage(event.channel, event.user, text);
|
|
75
|
-
await say({ text: response, thread_ts: event.ts }).catch((e) =>
|
|
78
|
+
await say({ text: response, thread_ts: event.ts }).catch((e) => this.log.error(`mention reply error: ${e.message}`));
|
|
76
79
|
});
|
|
77
80
|
// ── Slash command /aiden ─────────────────────────────────
|
|
78
81
|
this.app.command('/aiden', async ({ command, ack, say }) => {
|
|
@@ -82,7 +85,7 @@ class SlackAdapter {
|
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
87
|
const response = await this.processMessage(command.channel_id, command.user_id, command.text);
|
|
85
|
-
await say(response).catch((e) =>
|
|
88
|
+
await say(response).catch((e) => this.log.error(`slash reply error: ${e.message}`));
|
|
86
89
|
});
|
|
87
90
|
// Register outbound delivery so gateway.deliver() and broadcast() work
|
|
88
91
|
gateway_1.gateway.registerChannel('slack', async (msg) => {
|
|
@@ -91,7 +94,7 @@ class SlackAdapter {
|
|
|
91
94
|
return true;
|
|
92
95
|
}
|
|
93
96
|
catch (e) {
|
|
94
|
-
|
|
97
|
+
this.log.error(`Delivery error: ${e.message}`);
|
|
95
98
|
return false;
|
|
96
99
|
}
|
|
97
100
|
});
|
|
@@ -99,10 +102,10 @@ class SlackAdapter {
|
|
|
99
102
|
try {
|
|
100
103
|
await this.app.start(port);
|
|
101
104
|
this.healthy = true;
|
|
102
|
-
|
|
105
|
+
this.log.info(`Connected (${useSocketMode ? 'socket mode' : `HTTP mode port ${port}`})`);
|
|
103
106
|
}
|
|
104
107
|
catch (e) {
|
|
105
|
-
|
|
108
|
+
this.log.error(`Start failed: ${e.message}`);
|
|
106
109
|
this.healthy = false;
|
|
107
110
|
}
|
|
108
111
|
}
|
|
@@ -113,10 +116,10 @@ class SlackAdapter {
|
|
|
113
116
|
await this.app.stop().catch(() => { });
|
|
114
117
|
this.app = null;
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
this.log.info('Disconnected');
|
|
117
120
|
}
|
|
118
121
|
async send(channelId, message) {
|
|
119
|
-
await this.app?.client.chat.postMessage({ channel: channelId, text: message }).catch((e) =>
|
|
122
|
+
await this.app?.client.chat.postMessage({ channel: channelId, text: message }).catch((e) => this.log.error(`send error: ${e.message}`));
|
|
120
123
|
}
|
|
121
124
|
isHealthy() { return this.healthy; }
|
|
122
125
|
// ── Helpers ────────────────────────────────────────────────
|
|
@@ -131,7 +134,7 @@ class SlackAdapter {
|
|
|
131
134
|
});
|
|
132
135
|
}
|
|
133
136
|
catch (e) {
|
|
134
|
-
|
|
137
|
+
this.log.error(`routeMessage error: ${e.message}`);
|
|
135
138
|
return '❌ Something went wrong. Try again.';
|
|
136
139
|
}
|
|
137
140
|
}
|