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
|
@@ -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
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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.TelegramCommandRouter = void 0;
|
|
8
|
+
const logger_1 = require("../v4/logger");
|
|
9
|
+
class TelegramCommandRouter {
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.store = opts.store;
|
|
12
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
13
|
+
this.botUsername = opts.botUsername ?? (() => null);
|
|
14
|
+
this.fetchGroupAdmins = opts.fetchGroupAdmins;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Inspect a Telegram message and decide whether to consume it as a
|
|
18
|
+
* command or pass it through to the agent. Pure dispatch — never
|
|
19
|
+
* sends messages itself; the caller renders the reply text and
|
|
20
|
+
* applies state changes.
|
|
21
|
+
*/
|
|
22
|
+
async route(msg) {
|
|
23
|
+
const text = (msg.text ?? '').trim();
|
|
24
|
+
if (!text.startsWith('/'))
|
|
25
|
+
return { kind: 'agent' };
|
|
26
|
+
const { cmd, args } = this.parseCommand(text);
|
|
27
|
+
if (!cmd)
|
|
28
|
+
return { kind: 'agent' };
|
|
29
|
+
const chatId = String(msg.chat.id);
|
|
30
|
+
const chatType = msg.chat.type;
|
|
31
|
+
const senderId = msg.from?.id ? String(msg.from.id) : '';
|
|
32
|
+
switch (cmd) {
|
|
33
|
+
case '/help':
|
|
34
|
+
case '/start':
|
|
35
|
+
return { kind: 'reply', text: this.helpText(chatType) };
|
|
36
|
+
case '/status':
|
|
37
|
+
return { kind: 'reply', text: '✓ Online' };
|
|
38
|
+
case '/clear': {
|
|
39
|
+
// DMs: anyone may /clear their own chat. Groups: admin only.
|
|
40
|
+
if (chatType !== 'private' && !(await this.isAdmin(senderId, chatId))) {
|
|
41
|
+
this.log.info(`/clear ignored — non-admin in group`, { chatId, senderId });
|
|
42
|
+
return { kind: 'handled' };
|
|
43
|
+
}
|
|
44
|
+
if (chatType !== 'private')
|
|
45
|
+
this.store.recordAdminAction(chatId, 'clear', senderId);
|
|
46
|
+
return { kind: 'cleared' };
|
|
47
|
+
}
|
|
48
|
+
case '/pause': {
|
|
49
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
50
|
+
this.log.info(`/pause ignored`, { chatId, senderId, chatType });
|
|
51
|
+
return { kind: 'handled' };
|
|
52
|
+
}
|
|
53
|
+
this.store.setPaused(chatId, true, senderId);
|
|
54
|
+
return { kind: 'paused', groupId: chatId };
|
|
55
|
+
}
|
|
56
|
+
case '/resume': {
|
|
57
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
58
|
+
this.log.info(`/resume ignored`, { chatId, senderId, chatType });
|
|
59
|
+
return { kind: 'handled' };
|
|
60
|
+
}
|
|
61
|
+
this.store.setPaused(chatId, false, senderId);
|
|
62
|
+
return { kind: 'resumed', groupId: chatId };
|
|
63
|
+
}
|
|
64
|
+
case '/allowusers': {
|
|
65
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
66
|
+
this.log.info(`/allowusers ignored`, { chatId, senderId, chatType });
|
|
67
|
+
return { kind: 'handled' };
|
|
68
|
+
}
|
|
69
|
+
// Comma- or space-separated. `/allowusers reset` clears the list.
|
|
70
|
+
const raw = args.join(' ').trim();
|
|
71
|
+
if (raw === '' || raw === 'reset' || raw === 'clear') {
|
|
72
|
+
this.store.setAllowedUsers(chatId, [], senderId);
|
|
73
|
+
return { kind: 'reply', text: '✓ Cleared user allowlist for this group.' };
|
|
74
|
+
}
|
|
75
|
+
const ids = raw.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
this.store.setAllowedUsers(chatId, ids, senderId);
|
|
77
|
+
return {
|
|
78
|
+
kind: 'reply',
|
|
79
|
+
text: `✓ User allowlist updated: ${ids.length} id(s) — only these users may chat with the bot here.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
// Unknown slash command — fall through to the agent. The
|
|
84
|
+
// model can decide whether to interpret it as natural input.
|
|
85
|
+
return { kind: 'agent' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Admin checks ────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* True when `senderId` is allowed to issue admin-only commands here.
|
|
91
|
+
* Owner takes priority; TELEGRAM_ADMIN_USERS escalates additional
|
|
92
|
+
* ids; TELEGRAM_TRUST_GROUP_ADMINS=true (off by default) accepts
|
|
93
|
+
* Telegram-side group admins.
|
|
94
|
+
*/
|
|
95
|
+
async isAdmin(senderId, chatId) {
|
|
96
|
+
if (!senderId)
|
|
97
|
+
return false;
|
|
98
|
+
const ownerId = (process.env.TELEGRAM_OWNER_ID ?? '').trim();
|
|
99
|
+
if (ownerId && senderId === ownerId)
|
|
100
|
+
return true;
|
|
101
|
+
const adminCsv = (process.env.TELEGRAM_ADMIN_USERS ?? '').trim();
|
|
102
|
+
if (adminCsv) {
|
|
103
|
+
const admins = adminCsv.split(',').map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
if (admins.includes(senderId))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const trustGroupAdmins = (process.env.TELEGRAM_TRUST_GROUP_ADMINS ?? '').toLowerCase() === 'true';
|
|
108
|
+
if (trustGroupAdmins && this.fetchGroupAdmins) {
|
|
109
|
+
try {
|
|
110
|
+
const admins = await this.fetchGroupAdmins(chatId);
|
|
111
|
+
if (admins.includes(senderId))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.log.warn(`getChatAdministrators failed: ${err?.message ?? err}`, { chatId });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// ── Internals ───────────────────────────────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* Extract `/cmd` and the args list from raw text.
|
|
123
|
+
* Strips `@bot_username` suffixes Telegram appends in groups so
|
|
124
|
+
* `/clear@aiden_test_bot` resolves to `/clear`.
|
|
125
|
+
*/
|
|
126
|
+
parseCommand(raw) {
|
|
127
|
+
const parts = raw.split(/\s+/);
|
|
128
|
+
if (!parts[0] || !parts[0].startsWith('/'))
|
|
129
|
+
return { cmd: null, args: [] };
|
|
130
|
+
const username = (this.botUsername() ?? '').toLowerCase();
|
|
131
|
+
let head = parts[0].toLowerCase();
|
|
132
|
+
if (username && head.endsWith(`@${username}`)) {
|
|
133
|
+
head = head.slice(0, head.length - username.length - 1);
|
|
134
|
+
}
|
|
135
|
+
return { cmd: head, args: parts.slice(1) };
|
|
136
|
+
}
|
|
137
|
+
requireGroup(chatType) {
|
|
138
|
+
return chatType === 'group' || chatType === 'supergroup';
|
|
139
|
+
}
|
|
140
|
+
helpText(chatType) {
|
|
141
|
+
const groupExtras = chatType !== 'private'
|
|
142
|
+
? '`/pause` admin: stop bot in this group\n' +
|
|
143
|
+
'`/resume` admin: resume bot\n' +
|
|
144
|
+
'`/allowusers` admin: restrict who may chat\n'
|
|
145
|
+
: '';
|
|
146
|
+
return ('*Aiden* — your local AI assistant.\n\n' +
|
|
147
|
+
'Send any message (or @mention me in a group) to start. Built-in commands:\n' +
|
|
148
|
+
'`/help` show this message\n' +
|
|
149
|
+
'`/status` bot health check\n' +
|
|
150
|
+
'`/clear` wipe this chat\'s memory\n' +
|
|
151
|
+
groupExtras);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.TelegramCommandRouter = TelegramCommandRouter;
|