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,287 @@
|
|
|
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
|
+
* core/v4/firstRun/providerDetection.ts — Aiden v4.0.2 (Phase 30.2)
|
|
10
|
+
*
|
|
11
|
+
* Fast (< 100 ms) check at boot: does the user have ANY working
|
|
12
|
+
* provider configured? Drives the "auto-launch setup wizard if not"
|
|
13
|
+
* behaviour in cli/v4/aidenCLI.ts and the "model not configured"
|
|
14
|
+
* fallback in the boot card.
|
|
15
|
+
*
|
|
16
|
+
* Three signals are inspected, all local — no real API calls:
|
|
17
|
+
*
|
|
18
|
+
* 1. Env vars — process.env keys matching any of the wizard's
|
|
19
|
+
* `PROVIDERS[].envVar` entries. (Wizard-managed
|
|
20
|
+
* `.env` is loaded into process.env upstream by
|
|
21
|
+
* `loadAidenEnvFile`, so this catches both shell
|
|
22
|
+
* env and Aiden's persisted .env.)
|
|
23
|
+
*
|
|
24
|
+
* 2. OAuth tokens — `<paths.root>/auth/<provider>.json`. We treat
|
|
25
|
+
* the file's presence as "credentials available"
|
|
26
|
+
* — actual decrypt + expiry happens later in
|
|
27
|
+
* `runtimeResolver` and is reported via plugin
|
|
28
|
+
* boot-card status. Avoids paying scrypt + AES
|
|
29
|
+
* cost on every boot just to gate the wizard.
|
|
30
|
+
*
|
|
31
|
+
* 3. Ollama — TCP probe of `http://localhost:11434/api/tags`
|
|
32
|
+
* with a HARD 80 ms abort. Non-fatal on timeout
|
|
33
|
+
* so a slow loopback doesn't slow boot.
|
|
34
|
+
*
|
|
35
|
+
* Returns a `ProviderDetection` snapshot the caller can consult to
|
|
36
|
+
* decide whether to launch the wizard. The shape is intentionally
|
|
37
|
+
* descriptive (lists, not just a boolean) so smoke tests and
|
|
38
|
+
* `aiden doctor` can render the why.
|
|
39
|
+
*/
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.detectAvailableProviders = detectAvailableProviders;
|
|
45
|
+
exports.summarizeDetection = summarizeDetection;
|
|
46
|
+
const node_fs_1 = require("node:fs");
|
|
47
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
48
|
+
const setupWizard_1 = require("../../../cli/v4/setupWizard");
|
|
49
|
+
/**
|
|
50
|
+
* Walk `PROVIDERS` and return env-var names whose value is set + non-empty.
|
|
51
|
+
* Includes the multi-slot Groq fallback vars so a user with `GROQ_API_KEY_2`
|
|
52
|
+
* but no primary still counts as configured.
|
|
53
|
+
*/
|
|
54
|
+
function detectEnvVars(env) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const out = [];
|
|
57
|
+
const consider = (name) => {
|
|
58
|
+
if (!name)
|
|
59
|
+
return;
|
|
60
|
+
if (seen.has(name))
|
|
61
|
+
return;
|
|
62
|
+
const val = env[name];
|
|
63
|
+
if (typeof val === 'string' && val.trim().length > 0) {
|
|
64
|
+
seen.add(name);
|
|
65
|
+
out.push(name);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
for (const p of setupWizard_1.PROVIDERS)
|
|
69
|
+
consider(p.envVar);
|
|
70
|
+
// Multi-slot Groq fallbacks live in core/v4/providerFallback.ts. Keep
|
|
71
|
+
// this list local so detection has zero deep imports off the boot
|
|
72
|
+
// path; the fallback module is heavy.
|
|
73
|
+
for (const extra of [
|
|
74
|
+
'GROQ_API_KEY_2',
|
|
75
|
+
'GROQ_API_KEY_3',
|
|
76
|
+
'GROQ_API_KEY_4',
|
|
77
|
+
'TOGETHER_API_KEY',
|
|
78
|
+
]) {
|
|
79
|
+
consider(extra);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read `<paths.root>/auth/` and return provider ids whose `.json`
|
|
85
|
+
* file exists. ENOENT on the directory is treated as "no tokens".
|
|
86
|
+
*/
|
|
87
|
+
async function detectOAuthTokens(paths) {
|
|
88
|
+
const dir = node_path_1.default.join(paths.root, 'auth');
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = await node_fs_1.promises.readdir(dir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const e of entries) {
|
|
98
|
+
if (!e.endsWith('.json'))
|
|
99
|
+
continue;
|
|
100
|
+
const id = e.replace(/\.json$/, '');
|
|
101
|
+
// tokenStore writes one JSON per provider id; the file itself is
|
|
102
|
+
// always non-empty when it exists. Skip the size check — the
|
|
103
|
+
// resolver will surface a corrupt file with a clear error.
|
|
104
|
+
out.push(id);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Quick local probe of an Ollama daemon. Hard-aborts at `timeoutMs`
|
|
110
|
+
* so a slow loopback (e.g. WSL2 mirror mode warming up) never delays
|
|
111
|
+
* boot past the budget.
|
|
112
|
+
*/
|
|
113
|
+
async function probeOllamaQuick(opts) {
|
|
114
|
+
const ctrl = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
116
|
+
try {
|
|
117
|
+
const res = await opts.fetchImpl('http://localhost:11434/api/tags', { signal: ctrl.signal });
|
|
118
|
+
return res.ok;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Cheap regex parse of `model.provider:` / `model.modelId:` AND the
|
|
129
|
+
* `providers:` section from config.yaml. Avoids pulling in js-yaml on
|
|
130
|
+
* the boot hot-path. Tolerates quoted values and inline comments.
|
|
131
|
+
* Returns nulls / empty list when the file is missing.
|
|
132
|
+
*
|
|
133
|
+
* The `providers:` walker mirrors `cli/v4/setupWizard.isFreshInstall`
|
|
134
|
+
* so a config.yaml that only carries inline `providers.foo.apiKey`
|
|
135
|
+
* (no env var) still counts as "the user has configured something" —
|
|
136
|
+
* the moat-boot test suite relies on this fixture shape.
|
|
137
|
+
*/
|
|
138
|
+
async function readConfigProviders(configYaml) {
|
|
139
|
+
let text;
|
|
140
|
+
try {
|
|
141
|
+
text = await node_fs_1.promises.readFile(configYaml, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return { provider: null, model: null, configuredProviders: [] };
|
|
145
|
+
}
|
|
146
|
+
const lines = text.split(/\r?\n/);
|
|
147
|
+
let inModel = false;
|
|
148
|
+
let inProviders = false;
|
|
149
|
+
let provider = null;
|
|
150
|
+
let model = null;
|
|
151
|
+
// Two-line lookahead would let us match `apiKey: ...` under each
|
|
152
|
+
// provider id; instead we keep the most recently seen provider id
|
|
153
|
+
// and stamp it on `configuredProviders` when its child field is
|
|
154
|
+
// populated. Idempotent within a single file.
|
|
155
|
+
let currentProviderId = null;
|
|
156
|
+
const seenProviders = [];
|
|
157
|
+
for (const raw of lines) {
|
|
158
|
+
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
159
|
+
if (/^model\s*:\s*$/.test(line)) {
|
|
160
|
+
inModel = true;
|
|
161
|
+
inProviders = false;
|
|
162
|
+
currentProviderId = null;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (/^providers\s*:\s*$/.test(line)) {
|
|
166
|
+
inProviders = true;
|
|
167
|
+
inModel = false;
|
|
168
|
+
currentProviderId = null;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Top-level non-indented key ends both blocks.
|
|
172
|
+
if (/^\S/.test(line) && line.length > 0) {
|
|
173
|
+
inModel = false;
|
|
174
|
+
inProviders = false;
|
|
175
|
+
currentProviderId = null;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (inModel) {
|
|
179
|
+
const provM = line.match(/^\s+provider\s*:\s*['"]?([^'"\s]+)['"]?\s*$/);
|
|
180
|
+
if (provM)
|
|
181
|
+
provider = provM[1];
|
|
182
|
+
const modM = line.match(/^\s+modelId\s*:\s*['"]?([^'"\s]+)['"]?\s*$/);
|
|
183
|
+
if (modM)
|
|
184
|
+
model = modM[1];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (inProviders) {
|
|
188
|
+
// 2-space indented `<id>:` opens a provider entry.
|
|
189
|
+
const idM = line.match(/^ ([A-Za-z0-9_.-]+)\s*:\s*$/);
|
|
190
|
+
if (idM) {
|
|
191
|
+
currentProviderId = idM[1];
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// 4-space indented `apiKey:` / `baseUrl:` flags it as configured.
|
|
195
|
+
if (currentProviderId &&
|
|
196
|
+
/^ (apiKey|baseUrl|auth)\s*:\s*\S/.test(line)) {
|
|
197
|
+
if (!seenProviders.includes(currentProviderId)) {
|
|
198
|
+
seenProviders.push(currentProviderId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { provider, model, configuredProviders: seenProviders };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Map a config provider id to the env-var name(s) and/or OAuth provider
|
|
207
|
+
* id that would represent valid credentials for it. Drives the
|
|
208
|
+
* `configuredProviderHasCredentials` flag.
|
|
209
|
+
*/
|
|
210
|
+
function configProviderCredentialKeys(providerId) {
|
|
211
|
+
const entry = setupWizard_1.PROVIDERS.find((p) => p.id === providerId);
|
|
212
|
+
const envVars = [];
|
|
213
|
+
const oauthIds = [];
|
|
214
|
+
if (entry?.envVar)
|
|
215
|
+
envVars.push(entry.envVar);
|
|
216
|
+
// Pro/oauth providers store tokens under their provider id.
|
|
217
|
+
if (entry?.kind === 'pro' || entry?.kind === 'oauth') {
|
|
218
|
+
oauthIds.push(providerId);
|
|
219
|
+
}
|
|
220
|
+
// Ollama needs no credentials; mirror the env-var-less local-key path.
|
|
221
|
+
if (entry?.kind === 'local') {
|
|
222
|
+
envVars.push('__OLLAMA_REACHABLE__'); // sentinel handled by caller
|
|
223
|
+
}
|
|
224
|
+
return { envVars, oauthIds };
|
|
225
|
+
}
|
|
226
|
+
async function detectAvailableProviders(opts) {
|
|
227
|
+
const env = opts.env ?? process.env;
|
|
228
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
229
|
+
const timeoutMs = opts.ollamaTimeoutMs ?? 80;
|
|
230
|
+
// Run the four independent probes in parallel — Ollama is the only
|
|
231
|
+
// one that can take real wall time, but capping it at 80 ms keeps
|
|
232
|
+
// total detection cost under the 100 ms budget on every realistic host.
|
|
233
|
+
const [envVars, oauthTokens, ollamaReachable, cfg] = await Promise.all([
|
|
234
|
+
Promise.resolve(detectEnvVars(env)),
|
|
235
|
+
detectOAuthTokens(opts.paths),
|
|
236
|
+
opts.skipOllamaProbe
|
|
237
|
+
? Promise.resolve(false)
|
|
238
|
+
: probeOllamaQuick({ fetchImpl, timeoutMs }),
|
|
239
|
+
readConfigProviders(opts.paths.configYaml),
|
|
240
|
+
]);
|
|
241
|
+
const hasAnyProvider = envVars.length > 0 ||
|
|
242
|
+
oauthTokens.length > 0 ||
|
|
243
|
+
ollamaReachable ||
|
|
244
|
+
cfg.configuredProviders.length > 0;
|
|
245
|
+
let configuredProviderHasCredentials = false;
|
|
246
|
+
if (cfg.provider) {
|
|
247
|
+
const want = configProviderCredentialKeys(cfg.provider);
|
|
248
|
+
const envHit = want.envVars.some((v) => v === '__OLLAMA_REACHABLE__' ? ollamaReachable : envVars.includes(v));
|
|
249
|
+
const oauthHit = want.oauthIds.some((id) => oauthTokens.includes(id));
|
|
250
|
+
// Inline `providers.<id>.apiKey` in config.yaml is also a valid
|
|
251
|
+
// credential source — it's what the moat-boot fixtures rely on
|
|
252
|
+
// and what users get when they hand-edit config.yaml.
|
|
253
|
+
const inlineHit = cfg.configuredProviders.includes(cfg.provider);
|
|
254
|
+
configuredProviderHasCredentials = envHit || oauthHit || inlineHit;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
hasAnyProvider,
|
|
258
|
+
envVars,
|
|
259
|
+
oauthTokens,
|
|
260
|
+
ollamaReachable,
|
|
261
|
+
configProvider: cfg.provider,
|
|
262
|
+
configModel: cfg.model,
|
|
263
|
+
configuredProviders: cfg.configuredProviders,
|
|
264
|
+
configuredProviderHasCredentials,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Format a single-line summary suitable for the boot UX preamble.
|
|
269
|
+
* Public so the wizard auto-trigger path can mirror it and so smoke
|
|
270
|
+
* tests can assert on stable text.
|
|
271
|
+
*/
|
|
272
|
+
function summarizeDetection(d) {
|
|
273
|
+
if (d.hasAnyProvider) {
|
|
274
|
+
const parts = [];
|
|
275
|
+
if (d.envVars.length > 0)
|
|
276
|
+
parts.push(`env: ${d.envVars.length}`);
|
|
277
|
+
if (d.oauthTokens.length > 0)
|
|
278
|
+
parts.push(`oauth: ${d.oauthTokens.length}`);
|
|
279
|
+
if (d.ollamaReachable)
|
|
280
|
+
parts.push('ollama');
|
|
281
|
+
if (d.configuredProviders.length > 0) {
|
|
282
|
+
parts.push(`config: ${d.configuredProviders.length}`);
|
|
283
|
+
}
|
|
284
|
+
return `Providers detected — ${parts.join(', ')}.`;
|
|
285
|
+
}
|
|
286
|
+
return 'No AI provider configured yet.';
|
|
287
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
* core/v4/logger/factory.ts — Phase v4.1-1.3a
|
|
10
|
+
*
|
|
11
|
+
* Build a root `Logger` for the running process based on which mode
|
|
12
|
+
* Aiden is in. Each mode has different invariants:
|
|
13
|
+
*
|
|
14
|
+
* - cli-interactive — REPL is sacred. Zero stdout sinks. Errors go
|
|
15
|
+
* to stderr (visible to the user without
|
|
16
|
+
* touching the chat prompt). Everything to file.
|
|
17
|
+
* - cli-headless — `aiden setup`, `aiden doctor`, scripts. No
|
|
18
|
+
* REPL to protect. Warnings/errors go to stderr.
|
|
19
|
+
* Everything to file. Stdout stays free for the
|
|
20
|
+
* command's own output (so users can pipe).
|
|
21
|
+
* - serve — daemon. Logs go to stdout as NDJSON for systemd
|
|
22
|
+
* / docker / log aggregators. File mirror keeps
|
|
23
|
+
* a local trace.
|
|
24
|
+
* - test — vitest etc. NullSink only. Pass `withMemory:
|
|
25
|
+
* true` to swap in a MemorySink for assertions.
|
|
26
|
+
*
|
|
27
|
+
* Modules NEVER pick their own sinks — they receive a Logger and call
|
|
28
|
+
* `.info()` etc. The factory is the only place mode-routing decisions
|
|
29
|
+
* live.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.createBootLogger = createBootLogger;
|
|
33
|
+
exports.noopLogger = noopLogger;
|
|
34
|
+
exports.markReplActive = markReplActive;
|
|
35
|
+
exports.markReplInactive = markReplInactive;
|
|
36
|
+
exports.isReplActive = isReplActive;
|
|
37
|
+
const logger_1 = require("./logger");
|
|
38
|
+
const fileSink_1 = require("./sinks/fileSink");
|
|
39
|
+
const stdSink_1 = require("./sinks/stdSink");
|
|
40
|
+
const nullSink_1 = require("./sinks/nullSink");
|
|
41
|
+
function createBootLogger(opts) {
|
|
42
|
+
switch (opts.mode) {
|
|
43
|
+
case 'cli-interactive': {
|
|
44
|
+
// REPL invariant: zero stdout writes. Stderr is allowed for
|
|
45
|
+
// warn/error so a real failure isn't completely silent.
|
|
46
|
+
const sinks = [];
|
|
47
|
+
if (opts.logsDir)
|
|
48
|
+
sinks.push(new fileSink_1.FileSink({ dir: opts.logsDir, name: 'aiden' }));
|
|
49
|
+
sinks.push(new stdSink_1.StderrSink({ minLevel: 'warn' }));
|
|
50
|
+
return { logger: new logger_1.CoreLogger({ sinks }) };
|
|
51
|
+
}
|
|
52
|
+
case 'cli-headless': {
|
|
53
|
+
const sinks = [];
|
|
54
|
+
if (opts.logsDir)
|
|
55
|
+
sinks.push(new fileSink_1.FileSink({ dir: opts.logsDir, name: 'aiden' }));
|
|
56
|
+
sinks.push(new stdSink_1.StderrSink({ minLevel: 'warn' }));
|
|
57
|
+
return { logger: new logger_1.CoreLogger({ sinks }) };
|
|
58
|
+
}
|
|
59
|
+
case 'serve': {
|
|
60
|
+
// Daemon — stdout NDJSON for log aggregators, mirror to file for
|
|
61
|
+
// local-on-disk debugging.
|
|
62
|
+
const sinks = [new stdSink_1.StdoutJsonSink()];
|
|
63
|
+
if (opts.logsDir)
|
|
64
|
+
sinks.push(new fileSink_1.FileSink({ dir: opts.logsDir, name: 'aiden' }));
|
|
65
|
+
return { logger: new logger_1.CoreLogger({ sinks }) };
|
|
66
|
+
}
|
|
67
|
+
case 'test': {
|
|
68
|
+
if (opts.withMemory) {
|
|
69
|
+
const memory = new nullSink_1.MemorySink();
|
|
70
|
+
return { logger: new logger_1.CoreLogger({ sinks: [memory] }), memory };
|
|
71
|
+
}
|
|
72
|
+
return { logger: new logger_1.CoreLogger({ sinks: [new nullSink_1.NullSink()] }) };
|
|
73
|
+
}
|
|
74
|
+
case 'mcp-stdio': {
|
|
75
|
+
// Phase v4.1-mcp invariant: stdout carries the JSON-RPC protocol
|
|
76
|
+
// frames — any byte written to stdout outside the MCP transport
|
|
77
|
+
// corrupts the wire. So this mode wires ZERO stdout sinks. Errors
|
|
78
|
+
// and warnings go to stderr (visible to the spawning client's log
|
|
79
|
+
// stream); everything else lands in the file sink for postmortems.
|
|
80
|
+
const sinks = [];
|
|
81
|
+
if (opts.logsDir)
|
|
82
|
+
sinks.push(new fileSink_1.FileSink({ dir: opts.logsDir, name: 'aiden-mcp' }));
|
|
83
|
+
sinks.push(new stdSink_1.StderrSink({ minLevel: 'warn' }));
|
|
84
|
+
return { logger: new logger_1.CoreLogger({ sinks }) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* No-op singleton — what `attachLogger()` setters fall back to when no
|
|
90
|
+
* caller has wired in a real one yet. Avoids null-checks at every emit
|
|
91
|
+
* site. Lazy-built so tests that import this module don't allocate
|
|
92
|
+
* sinks they'll never touch.
|
|
93
|
+
*/
|
|
94
|
+
let _noop = null;
|
|
95
|
+
function noopLogger() {
|
|
96
|
+
if (!_noop)
|
|
97
|
+
_noop = new logger_1.CoreLogger({ sinks: [new nullSink_1.NullSink()] });
|
|
98
|
+
return _noop;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Phase v4.1-1.3a — process-wide flag tripped once the chat prompt is
|
|
102
|
+
* up. The repl-sacred invariant in `cli-interactive` mode comes from
|
|
103
|
+
* the factory not wiring any stdout sink, but a defense-in-depth layer:
|
|
104
|
+
* if any future code path manages to grab stdout directly, this flag
|
|
105
|
+
* lets us assert in tests + audit.
|
|
106
|
+
*/
|
|
107
|
+
let _replActive = false;
|
|
108
|
+
function markReplActive() { _replActive = true; }
|
|
109
|
+
function markReplInactive() { _replActive = false; }
|
|
110
|
+
function isReplActive() { return _replActive; }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/v4/logger/index.ts — Phase v4.1-1.3a barrel export.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MultiSink = exports.MemorySink = exports.NullSink = exports.StdoutJsonSink = exports.StderrSink = exports.FileSink = exports.isReplActive = exports.markReplInactive = exports.markReplActive = exports.noopLogger = exports.createBootLogger = exports.CoreLogger = void 0;
|
|
5
|
+
var logger_1 = require("./logger");
|
|
6
|
+
Object.defineProperty(exports, "CoreLogger", { enumerable: true, get: function () { return logger_1.CoreLogger; } });
|
|
7
|
+
var factory_1 = require("./factory");
|
|
8
|
+
Object.defineProperty(exports, "createBootLogger", { enumerable: true, get: function () { return factory_1.createBootLogger; } });
|
|
9
|
+
Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return factory_1.noopLogger; } });
|
|
10
|
+
Object.defineProperty(exports, "markReplActive", { enumerable: true, get: function () { return factory_1.markReplActive; } });
|
|
11
|
+
Object.defineProperty(exports, "markReplInactive", { enumerable: true, get: function () { return factory_1.markReplInactive; } });
|
|
12
|
+
Object.defineProperty(exports, "isReplActive", { enumerable: true, get: function () { return factory_1.isReplActive; } });
|
|
13
|
+
var fileSink_1 = require("./sinks/fileSink");
|
|
14
|
+
Object.defineProperty(exports, "FileSink", { enumerable: true, get: function () { return fileSink_1.FileSink; } });
|
|
15
|
+
var stdSink_1 = require("./sinks/stdSink");
|
|
16
|
+
Object.defineProperty(exports, "StderrSink", { enumerable: true, get: function () { return stdSink_1.StderrSink; } });
|
|
17
|
+
Object.defineProperty(exports, "StdoutJsonSink", { enumerable: true, get: function () { return stdSink_1.StdoutJsonSink; } });
|
|
18
|
+
var nullSink_1 = require("./sinks/nullSink");
|
|
19
|
+
Object.defineProperty(exports, "NullSink", { enumerable: true, get: function () { return nullSink_1.NullSink; } });
|
|
20
|
+
Object.defineProperty(exports, "MemorySink", { enumerable: true, get: function () { return nullSink_1.MemorySink; } });
|
|
21
|
+
var multiSink_1 = require("./sinks/multiSink");
|
|
22
|
+
Object.defineProperty(exports, "MultiSink", { enumerable: true, get: function () { return multiSink_1.MultiSink; } });
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
* core/v4/logger/logger.ts — Phase v4.1-1.3a
|
|
10
|
+
*
|
|
11
|
+
* The Logger contract. Every module that emits diagnostics goes through
|
|
12
|
+
* this — never `console.*` directly. The CLI's REPL is sacred: in
|
|
13
|
+
* `cli-interactive` mode the factory wires zero stdout sinks, so a
|
|
14
|
+
* misbehaving module CANNOT corrupt the chat prompt.
|
|
15
|
+
*
|
|
16
|
+
* Three pieces:
|
|
17
|
+
* - `Logger` — what consumers call (debug / info / warn / error
|
|
18
|
+
* + child(scope) for nested namespaces).
|
|
19
|
+
* - `LoggerSink` — where lines actually go (file, stderr, null, …).
|
|
20
|
+
* - `Logger` impl — fans every line out to all attached sinks.
|
|
21
|
+
*
|
|
22
|
+
* Sinks are the routing surface; the factory in `./factory.ts` picks
|
|
23
|
+
* the right combination per AidenMode. Adding a new module never
|
|
24
|
+
* touches sink logic — modules just call `logger.info('...')` and
|
|
25
|
+
* the factory decides where it goes.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.CoreLogger = exports.LOG_LEVEL_ORDER = void 0;
|
|
29
|
+
/** Stable numeric ordering for level filtering. */
|
|
30
|
+
exports.LOG_LEVEL_ORDER = {
|
|
31
|
+
debug: 10,
|
|
32
|
+
info: 20,
|
|
33
|
+
warn: 30,
|
|
34
|
+
error: 40,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Default `Logger` implementation. Holds a list of sinks and the
|
|
38
|
+
* current scope; child loggers share the same sink list (so updating
|
|
39
|
+
* the level / detaching at the root affects everything).
|
|
40
|
+
*/
|
|
41
|
+
class CoreLogger {
|
|
42
|
+
/**
|
|
43
|
+
* Construct a root logger. Use `child(segment)` for sub-loggers.
|
|
44
|
+
* `sinks` may be empty — useful for tests; writes silently drop.
|
|
45
|
+
*/
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.scope = opts.scope ?? '';
|
|
48
|
+
this.sinks = opts.sinks;
|
|
49
|
+
this.level = opts.level ?? 'debug';
|
|
50
|
+
this.sinksOwner = { sinks: this.sinks, level: this.level };
|
|
51
|
+
}
|
|
52
|
+
/** Internal — used by `child()` to share state with the root. */
|
|
53
|
+
static childOf(parent, segment) {
|
|
54
|
+
const c = Object.create(CoreLogger.prototype);
|
|
55
|
+
const nextScope = parent.scope ? `${parent.scope}.${segment}` : segment;
|
|
56
|
+
Object.assign(c, {
|
|
57
|
+
scope: nextScope,
|
|
58
|
+
sinks: parent.sinksOwner.sinks,
|
|
59
|
+
level: parent.sinksOwner.level,
|
|
60
|
+
sinksOwner: parent.sinksOwner,
|
|
61
|
+
});
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
child(segment) {
|
|
65
|
+
return CoreLogger.childOf(this, segment);
|
|
66
|
+
}
|
|
67
|
+
setLevel(level) {
|
|
68
|
+
this.sinksOwner.level = level;
|
|
69
|
+
this.level = level;
|
|
70
|
+
}
|
|
71
|
+
getLevel() {
|
|
72
|
+
return this.sinksOwner.level;
|
|
73
|
+
}
|
|
74
|
+
detachAll() {
|
|
75
|
+
this.sinksOwner.sinks.length = 0;
|
|
76
|
+
}
|
|
77
|
+
debug(msg, ctx) { this.write('debug', msg, ctx); }
|
|
78
|
+
info(msg, ctx) { this.write('info', msg, ctx); }
|
|
79
|
+
warn(msg, ctx) { this.write('warn', msg, ctx); }
|
|
80
|
+
error(msg, ctx) { this.write('error', msg, ctx); }
|
|
81
|
+
write(level, msg, ctx) {
|
|
82
|
+
if (exports.LOG_LEVEL_ORDER[level] < exports.LOG_LEVEL_ORDER[this.sinksOwner.level])
|
|
83
|
+
return;
|
|
84
|
+
const record = {
|
|
85
|
+
ts: new Date(),
|
|
86
|
+
level,
|
|
87
|
+
scope: this.scope,
|
|
88
|
+
msg,
|
|
89
|
+
ctx,
|
|
90
|
+
};
|
|
91
|
+
// Sinks must not throw — the helpers in ./sinks/* all wrap their
|
|
92
|
+
// I/O in try/catch. Be defensive anyway.
|
|
93
|
+
for (const s of this.sinksOwner.sinks) {
|
|
94
|
+
try {
|
|
95
|
+
s.write(record);
|
|
96
|
+
}
|
|
97
|
+
catch { /* logging must not break callers */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.CoreLogger = CoreLogger;
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
* core/v4/logger/sinks/fileSink.ts — Phase v4.1-1.3a
|
|
10
|
+
*
|
|
11
|
+
* Append log records to a file under `<aidenRoot>/logs/<name>.log`.
|
|
12
|
+
*
|
|
13
|
+
* One file per stream. Synchronous append (`appendFileSync`) so a line
|
|
14
|
+
* is durable even if the process exits mid-emission — same trade-off
|
|
15
|
+
* `core/v4/aidenLogger.ts` makes; small writes (< 100 / s during boot,
|
|
16
|
+
* occasional after) keep the cost negligible.
|
|
17
|
+
*
|
|
18
|
+
* Coarse rotation: when the file passes `MAX_BYTES`, rename to
|
|
19
|
+
* `<name>.log.1` (overwriting any previous rotation). One rotation is
|
|
20
|
+
* enough for diagnostics — older history isn't useful for debugging
|
|
21
|
+
* the current session and we'd rather not stash megabytes.
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.FileSink = void 0;
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
+
/** Rotate at 5 MB — comfortable for a long debugging session, never huge. */
|
|
31
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
32
|
+
class FileSink {
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.dirReady = false;
|
|
35
|
+
this.dir = opts.dir;
|
|
36
|
+
this.filePath = node_path_1.default.join(opts.dir, `${opts.name}.log`);
|
|
37
|
+
}
|
|
38
|
+
write(record) {
|
|
39
|
+
if (!this.ensureDir())
|
|
40
|
+
return;
|
|
41
|
+
this.maybeRotate();
|
|
42
|
+
const line = this.format(record);
|
|
43
|
+
try {
|
|
44
|
+
(0, node_fs_1.appendFileSync)(this.filePath, line, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch { /* disk full / permission denied — drop */ }
|
|
47
|
+
}
|
|
48
|
+
/** Make `<dir>` once. Repeated calls are cheap (cache hit). */
|
|
49
|
+
ensureDir() {
|
|
50
|
+
if (this.dirReady)
|
|
51
|
+
return true;
|
|
52
|
+
try {
|
|
53
|
+
(0, node_fs_1.mkdirSync)(this.dir, { recursive: true });
|
|
54
|
+
this.dirReady = true;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* If the file is over MAX_BYTES, rename to `<name>.log.1` (overwriting
|
|
63
|
+
* the prior rotation if any). Best-effort — rotation failure isn't
|
|
64
|
+
* worth blocking the next write for.
|
|
65
|
+
*/
|
|
66
|
+
maybeRotate() {
|
|
67
|
+
let size = 0;
|
|
68
|
+
try {
|
|
69
|
+
size = (0, node_fs_1.statSync)(this.filePath).size;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (size <= MAX_BYTES)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
(0, node_fs_1.renameSync)(this.filePath, `${this.filePath}.1`);
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Pretty single-line format — easy to grep, easy to tail. Structured
|
|
83
|
+
* fields are appended JSON-style after the message. Sinks that want
|
|
84
|
+
* NDJSON live elsewhere (e.g. `serve` mode would use a future
|
|
85
|
+
* JsonStdoutSink).
|
|
86
|
+
*
|
|
87
|
+
* 2026-05-08T01:32:44.681Z [info] [channels.telegram] Connected as @bot
|
|
88
|
+
* 2026-05-08T01:32:50.001Z [warn] [channels.telegram] Polling 409 {"streak":1}
|
|
89
|
+
*/
|
|
90
|
+
format(r) {
|
|
91
|
+
const scope = r.scope ? ` [${r.scope}]` : '';
|
|
92
|
+
const ctx = r.ctx && Object.keys(r.ctx).length > 0
|
|
93
|
+
? ' ' + safeJson(r.ctx)
|
|
94
|
+
: '';
|
|
95
|
+
return `${r.ts.toISOString()} [${r.level}]${scope} ${r.msg}${ctx}\n`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.FileSink = FileSink;
|
|
99
|
+
/**
|
|
100
|
+
* Defensive JSON.stringify — never throws; circular refs collapse to a
|
|
101
|
+
* placeholder so a misbehaving caller can't kill the log line.
|
|
102
|
+
*/
|
|
103
|
+
function safeJson(obj) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(obj);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return '"[unserializable ctx]"';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
* core/v4/logger/sinks/multiSink.ts — Phase v4.1-1.3a
|
|
10
|
+
*
|
|
11
|
+
* `Logger` already fans out to every attached sink, so MultiSink is
|
|
12
|
+
* thin sugar for the cases where a sink itself wants to delegate to
|
|
13
|
+
* several others (e.g. wrap a stderr-warn-only filter and a file
|
|
14
|
+
* everything filter behind a single object the caller treats as one
|
|
15
|
+
* sink). In 3a it's used by tests; production logger compositions go
|
|
16
|
+
* through `factory.createBootLogger` which picks sinks per mode.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.MultiSink = void 0;
|
|
20
|
+
class MultiSink {
|
|
21
|
+
constructor(children) {
|
|
22
|
+
this.children = children;
|
|
23
|
+
}
|
|
24
|
+
write(r) {
|
|
25
|
+
for (const c of this.children) {
|
|
26
|
+
try {
|
|
27
|
+
c.write(r);
|
|
28
|
+
}
|
|
29
|
+
catch { /* one sink's failure must not poison the others */ }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async close() {
|
|
33
|
+
for (const c of this.children) {
|
|
34
|
+
if (typeof c.close === 'function') {
|
|
35
|
+
try {
|
|
36
|
+
await c.close();
|
|
37
|
+
}
|
|
38
|
+
catch { /* ignore */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MultiSink = MultiSink;
|