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.
Files changed (112) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +513 -14
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +269 -52
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. 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;