aiden-runtime 4.0.1 → 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 +513 -14
- 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 +269 -52
- 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 +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -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 +300 -14
- 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/setupWizard.js +466 -232
- 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/firstRun/providerDetection.js +287 -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/providers/v4/nullAdapter.js +58 -0
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- package/package.json +11 -2
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|