aiden-runtime 4.0.2 → 4.1.1

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 (113) hide show
  1. package/README.md +19 -11
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +424 -7
  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 +256 -55
  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 +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +102 -1
  28. package/dist/cli/v4/doctorLiveness.js +329 -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/shellInterpolation.js +139 -0
  37. package/dist/cli/v4/skinEngine.js +21 -1
  38. package/dist/cli/v4/streamingPrefix.js +121 -0
  39. package/dist/cli/v4/syntaxHighlight.js +345 -0
  40. package/dist/cli/v4/table.js +216 -0
  41. package/dist/cli/v4/themeDetect.js +81 -0
  42. package/dist/cli/v4/uiBuild.js +74 -0
  43. package/dist/cli/v4/voiceCli.js +113 -0
  44. package/dist/cli/v4/voicePromptApi.js +196 -0
  45. package/dist/core/channels/discord.js +16 -10
  46. package/dist/core/channels/email.js +13 -9
  47. package/dist/core/channels/imessage.js +13 -9
  48. package/dist/core/channels/manager.js +25 -7
  49. package/dist/core/channels/pdf-extract.js +180 -0
  50. package/dist/core/channels/photo-vision.js +157 -0
  51. package/dist/core/channels/signal.js +11 -7
  52. package/dist/core/channels/slack.js +13 -10
  53. package/dist/core/channels/telegram-commands.js +154 -0
  54. package/dist/core/channels/telegram-groups.js +198 -0
  55. package/dist/core/channels/telegram-rate-limit.js +124 -0
  56. package/dist/core/channels/telegram.js +1980 -0
  57. package/dist/core/channels/twilio.js +11 -7
  58. package/dist/core/channels/webhook.js +9 -5
  59. package/dist/core/channels/whatsapp.js +15 -11
  60. package/dist/core/channels/whisper-transcribe.js +163 -0
  61. package/dist/core/cronManager.js +33 -294
  62. package/dist/core/gateway.js +29 -8
  63. package/dist/core/playwrightBridge.js +90 -0
  64. package/dist/core/v4/aidenAgent.js +35 -0
  65. package/dist/core/v4/auxiliaryClient.js +2 -2
  66. package/dist/core/v4/cron/atomicWrite.js +18 -4
  67. package/dist/core/v4/cron/cronExecute.js +300 -0
  68. package/dist/core/v4/cron/cronManager.js +502 -0
  69. package/dist/core/v4/cron/cronState.js +314 -0
  70. package/dist/core/v4/cron/cronTick.js +90 -0
  71. package/dist/core/v4/cron/diagnostics.js +104 -0
  72. package/dist/core/v4/cron/graceWindow.js +79 -0
  73. package/dist/core/v4/logger/factory.js +110 -0
  74. package/dist/core/v4/logger/index.js +22 -0
  75. package/dist/core/v4/logger/logger.js +101 -0
  76. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  77. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  78. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  79. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  80. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  81. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  82. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  83. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  84. package/dist/core/v4/platformPaths.js +105 -0
  85. package/dist/core/v4/providerFallback.js +25 -0
  86. package/dist/core/v4/skillLoader.js +21 -5
  87. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  88. package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
  89. package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
  90. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  91. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  92. package/dist/core/v4/subagent/budget.js +76 -0
  93. package/dist/core/v4/subagent/diagnostics.js +22 -0
  94. package/dist/core/v4/subagent/fanout.js +216 -0
  95. package/dist/core/v4/subagent/merger.js +148 -0
  96. package/dist/core/v4/subagent/providerRotation.js +54 -0
  97. package/dist/core/v4/voice/audioStream.js +373 -0
  98. package/dist/core/v4/voice/cliVoice.js +393 -0
  99. package/dist/core/v4/voice/diagnostics.js +66 -0
  100. package/dist/core/v4/voice/ttsStream.js +193 -0
  101. package/dist/core/version.js +1 -1
  102. package/dist/core/visionAnalyze.js +291 -90
  103. package/dist/core/voice/audio.js +61 -5
  104. package/dist/core/voice/audioBackend.js +134 -0
  105. package/dist/core/voice/stt.js +61 -6
  106. package/dist/core/voice/tts.js +19 -3
  107. package/dist/moat/dangerousPatterns.js +1 -1
  108. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  109. package/dist/providers/v4/errors.js +51 -1
  110. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  111. package/dist/tools/v4/index.js +32 -1
  112. package/dist/tools/v4/subagent/subagentFanout.js +190 -0
  113. package/package.json +11 -2
@@ -0,0 +1,329 @@
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
+ * cli/v4/doctorLiveness.ts — Phase v4.1.1-oauth-fix Phase 4.
10
+ *
11
+ * `aiden doctor --providers` deep-mode helper. Pings every configured /
12
+ * authed provider with a minimal request, reports green / red / skipped
13
+ * + per-provider latency + verbatim upstream error message.
14
+ *
15
+ * Why a separate file:
16
+ * - Default doctor (`aiden doctor` with no flag) stays unchanged and
17
+ * fast — config-shape checks only.
18
+ * - `--providers` is opt-in. When the user types it we extend the
19
+ * report with one liveness row per probe, then render a summary
20
+ * line at the bottom.
21
+ * - Tool-catalog validation is deliberately OUT of scope. Liveness
22
+ * probes ship `tools: []` (see comment in checkProviderLiveness)
23
+ * so one bad tool schema doesn't false-red every provider that
24
+ * validates strictly. The eval-harness / registration-time schema
25
+ * validator (v4.1.1 main) is the right home for that concern.
26
+ *
27
+ * Trust artifact:
28
+ * - On failure we surface `err.message` VERBATIM (truncated to 200
29
+ * chars). Phase 3 (`providers/v4/errors.ts`) already composes the
30
+ * upstream response body into ProviderError.message — so a 400
31
+ * prints the actual OpenAI reason, not a generic "provider failed."
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.enumerateConfiguredProviders = enumerateConfiguredProviders;
35
+ exports.checkProviderLiveness = checkProviderLiveness;
36
+ exports.runProviderLiveness = runProviderLiveness;
37
+ exports.renderProviderLivenessSection = renderProviderLivenessSection;
38
+ const registry_1 = require("../../providers/v4/registry");
39
+ const runtimeResolver_1 = require("../../providers/v4/runtimeResolver");
40
+ const credentialResolver_1 = require("../../providers/v4/credentialResolver");
41
+ const tokenStore_1 = require("../../core/v4/auth/tokenStore");
42
+ const DEFAULT_LIVENESS_TIMEOUT_MS = 8000;
43
+ const PROBE_MAX_TOKENS = 4;
44
+ const ERROR_TRUNCATE_CHARS = 200;
45
+ const OLLAMA_PROBE_TIMEOUT_MS = 1500;
46
+ const OLLAMA_HEALTH_URL = 'http://127.0.0.1:11434/api/tags';
47
+ // ── Helpers ─────────────────────────────────────────────────────────────
48
+ /**
49
+ * Truncate a long error string for display. The full error remains on
50
+ * the in-memory `LivenessResult.error` for programmatic consumers /
51
+ * test assertions.
52
+ */
53
+ function truncate(s, max = ERROR_TRUNCATE_CHARS) {
54
+ if (s.length <= max)
55
+ return s;
56
+ return `${s.slice(0, max - 1)}…`;
57
+ }
58
+ /**
59
+ * Wrap a promise with a hard timeout. Resolves to the inner result on
60
+ * success, throws a clearly-labelled `Error` on timeout. Cleans up the
61
+ * timer either way.
62
+ */
63
+ async function withTimeout(p, ms, label) {
64
+ let timer;
65
+ try {
66
+ return await Promise.race([
67
+ p,
68
+ new Promise((_, reject) => {
69
+ timer = setTimeout(() => reject(new Error(`${label}: timeout after ${ms}ms`)), ms);
70
+ }),
71
+ ]);
72
+ }
73
+ finally {
74
+ if (timer)
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /**
79
+ * Decide whether a provider counts as "configured" for liveness purposes.
80
+ * Three paths:
81
+ * 1. API key in env (`apiKeyEnvVar` set and the env var is non-empty).
82
+ * 2. OAuth (entry.oauth present and a non-expired token sits in the
83
+ * tokenStore at <paths.root>/auth/<providerId>.json).
84
+ * 3. Local providers (ollama) — probed via a quick HTTP health check.
85
+ *
86
+ * Anything else is `configured: false` and gets a `skip_reason`.
87
+ */
88
+ async function enumerateConfiguredProviders(opts) {
89
+ const env = opts.env ?? process.env;
90
+ const fetchImpl = opts.fetchImpl ?? fetch;
91
+ const out = [];
92
+ for (const entry of Object.values(registry_1.PROVIDER_REGISTRY)) {
93
+ // Every provider needs at least one model to probe against.
94
+ const model = entry.modelIds[0];
95
+ if (!model) {
96
+ out.push({
97
+ entry,
98
+ model: '',
99
+ configured: false,
100
+ reason: 'no models declared in registry',
101
+ });
102
+ continue;
103
+ }
104
+ // 1. API-key providers.
105
+ if (entry.apiKeyEnvVar) {
106
+ const value = env[entry.apiKeyEnvVar];
107
+ if (value && value.length > 0) {
108
+ out.push({ entry, model, configured: true });
109
+ }
110
+ else {
111
+ out.push({
112
+ entry,
113
+ model,
114
+ configured: false,
115
+ reason: `env ${entry.apiKeyEnvVar} not set`,
116
+ });
117
+ }
118
+ continue;
119
+ }
120
+ // 2. OAuth providers — check tokenStore.
121
+ if (entry.oauth) {
122
+ try {
123
+ const tokens = await (0, tokenStore_1.loadTokens)(opts.paths, entry.oauth.providerId);
124
+ if (tokens && tokens.accessToken) {
125
+ if ((0, tokenStore_1.isExpired)(tokens, tokenStore_1.PREFLIGHT_REFRESH_WINDOW_MS)) {
126
+ out.push({
127
+ entry,
128
+ model,
129
+ configured: false,
130
+ reason: 'OAuth token expired — run `/auth refresh` or `/auth login`',
131
+ });
132
+ }
133
+ else {
134
+ out.push({ entry, model, configured: true });
135
+ }
136
+ }
137
+ else {
138
+ out.push({
139
+ entry,
140
+ model,
141
+ configured: false,
142
+ reason: 'no OAuth token — run `/auth login`',
143
+ });
144
+ }
145
+ }
146
+ catch (err) {
147
+ out.push({
148
+ entry,
149
+ model,
150
+ configured: false,
151
+ reason: `tokenStore read failed: ${err.message}`,
152
+ });
153
+ }
154
+ continue;
155
+ }
156
+ // 3. Local / no-credential providers (ollama). Configured = the
157
+ // local daemon answers a health probe.
158
+ const controller = new AbortController();
159
+ const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS);
160
+ try {
161
+ const res = await fetchImpl(OLLAMA_HEALTH_URL, { signal: controller.signal });
162
+ if (res.ok) {
163
+ out.push({ entry, model, configured: true });
164
+ }
165
+ else {
166
+ out.push({
167
+ entry,
168
+ model,
169
+ configured: false,
170
+ reason: `local daemon HTTP ${res.status}`,
171
+ });
172
+ }
173
+ }
174
+ catch (err) {
175
+ out.push({
176
+ entry,
177
+ model,
178
+ configured: false,
179
+ reason: `not running on ${OLLAMA_HEALTH_URL}`,
180
+ });
181
+ }
182
+ finally {
183
+ clearTimeout(timer);
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ /**
189
+ * Probe one provider/model pair via a minimal `adapter.call()`. Returns
190
+ * a structured result; never throws.
191
+ *
192
+ * On failure, `result.error` is `err.message` verbatim truncated to
193
+ * 200 chars. Phase v4.1.1-oauth-fix Phase 3 made `ProviderError.message`
194
+ * carry the upstream response body, so a 400 surfaces the actual
195
+ * reason (e.g. "Invalid schema for function 'subagent_fanout': …").
196
+ */
197
+ async function checkProviderLiveness(provider, model, adapter, opts) {
198
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS;
199
+ const start = Date.now();
200
+ // Liveness probes "is this provider reachable + authenticated?".
201
+ // Tool-catalog validation is a separate concern (eval harness,
202
+ // v4.1.1 main). Sending tools: [] ensures one bad tool schema
203
+ // doesn't false-red every provider that validates strictly.
204
+ const input = {
205
+ messages: [{ role: 'user', content: 'ping' }],
206
+ tools: [],
207
+ maxTokens: PROBE_MAX_TOKENS,
208
+ };
209
+ try {
210
+ await withTimeout(adapter.call(input), timeoutMs, `liveness ${provider}`);
211
+ return {
212
+ provider,
213
+ model,
214
+ status: 'green',
215
+ latency_ms: Date.now() - start,
216
+ };
217
+ }
218
+ catch (err) {
219
+ const raw = err instanceof Error ? err.message : String(err);
220
+ return {
221
+ provider,
222
+ model,
223
+ status: 'red',
224
+ latency_ms: Date.now() - start,
225
+ error: truncate(raw),
226
+ };
227
+ }
228
+ }
229
+ /**
230
+ * Run liveness probes against every provider returned by
231
+ * `enumerateConfiguredProviders`. Unconfigured providers come back as
232
+ * `status: 'skipped'` without any network traffic.
233
+ *
234
+ * Returns `{ results, summary }`. Summary tallies + a wall-clock total
235
+ * so the UI can print "X green · Y red · Z skipped · NNNms total".
236
+ */
237
+ async function runProviderLiveness(opts) {
238
+ const start = Date.now();
239
+ const parallel = opts.parallel ?? true;
240
+ const resolver = opts.resolverImpl ?? new runtimeResolver_1.RuntimeResolver(new credentialResolver_1.CredentialResolver(opts.paths.authJson));
241
+ const configured = await enumerateConfiguredProviders({
242
+ paths: opts.paths,
243
+ env: opts.env,
244
+ fetchImpl: opts.fetchImpl,
245
+ });
246
+ const probe = async (c) => {
247
+ if (!c.configured) {
248
+ return {
249
+ provider: c.entry.id,
250
+ status: 'skipped',
251
+ latency_ms: 0,
252
+ skip_reason: c.reason ?? 'not configured',
253
+ };
254
+ }
255
+ try {
256
+ const adapter = await resolver.resolve({
257
+ providerId: c.entry.id,
258
+ modelId: c.model,
259
+ paths: opts.paths,
260
+ });
261
+ return await checkProviderLiveness(c.entry.id, c.model, adapter, { timeoutMs: opts.timeoutMs });
262
+ }
263
+ catch (err) {
264
+ // Resolve failure (missing credential, unknown model, etc.).
265
+ // Same surface treatment as a probe failure.
266
+ const raw = err instanceof Error ? err.message : String(err);
267
+ return {
268
+ provider: c.entry.id,
269
+ model: c.model,
270
+ status: 'red',
271
+ latency_ms: 0,
272
+ error: truncate(raw),
273
+ };
274
+ }
275
+ };
276
+ const results = parallel
277
+ ? await Promise.all(configured.map(probe))
278
+ : await runSequential(configured.map((c) => () => probe(c)));
279
+ const summary = {
280
+ green: results.filter((r) => r.status === 'green').length,
281
+ red: results.filter((r) => r.status === 'red').length,
282
+ skipped: results.filter((r) => r.status === 'skipped').length,
283
+ total_ms: Date.now() - start,
284
+ };
285
+ return { results, summary };
286
+ }
287
+ async function runSequential(thunks) {
288
+ const out = [];
289
+ for (const t of thunks)
290
+ out.push(await t());
291
+ return out;
292
+ }
293
+ /**
294
+ * Render the liveness section as plain-text rows. The doctor command
295
+ * prints this BELOW the standard health box so the default `aiden doctor`
296
+ * output stays byte-identical.
297
+ *
298
+ * Visual style matches the existing doctor rows (✓ / ✗ / -) but lays
299
+ * out as a tabular block rather than a box — the rows can be long
300
+ * (upstream error bodies) and forcing them into the 100-col box would
301
+ * truncate the diagnostic that's the whole point of the feature.
302
+ */
303
+ function renderProviderLivenessSection(results, summary) {
304
+ const nameWidth = Math.max(8, ...results.map((r) => r.provider.length));
305
+ const lines = [];
306
+ lines.push('');
307
+ lines.push(' Provider liveness (deep check)');
308
+ lines.push(` ${'─'.repeat(60)}`);
309
+ for (const r of results) {
310
+ const icon = r.status === 'green' ? '✓'
311
+ : r.status === 'red' ? '✗'
312
+ : '-';
313
+ const name = r.provider.padEnd(nameWidth);
314
+ const status = r.status === 'green' ? 'green'.padEnd(8)
315
+ : r.status === 'red' ? 'red '.padEnd(8)
316
+ : 'skip '.padEnd(8);
317
+ const latency = r.latency_ms > 0
318
+ ? `${r.latency_ms}ms`.padEnd(8)
319
+ : ''.padEnd(8);
320
+ const tail = r.status === 'green' ? (r.model ?? '')
321
+ : r.status === 'red' ? (r.error ?? 'unknown error')
322
+ : (r.skip_reason ?? 'not configured');
323
+ lines.push(` ${icon} ${name} ${status}${latency}${tail}`);
324
+ }
325
+ lines.push(` ${'─'.repeat(60)}`);
326
+ lines.push(` ${summary.green} green · ${summary.red} red · ${summary.skipped} skipped · ${summary.total_ms}ms total`);
327
+ lines.push('');
328
+ return lines.join('\n');
329
+ }
@@ -33,9 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.KNOWN_PROVIDER_KEYS = void 0;
36
37
  exports.loadAidenEnvFile = loadAidenEnvFile;
37
38
  exports.getEnvSource = getEnvSource;
38
39
  exports.__resetEnvSources = __resetEnvSources;
40
+ exports.resolveAidenInstallDir = resolveAidenInstallDir;
41
+ exports.loadMcpEnvSources = loadMcpEnvSources;
42
+ exports.describeProviderKeys = describeProviderKeys;
39
43
  /**
40
44
  * Copyright (c) 2026 Shiva Deore (Taracod).
41
45
  * Licensed under AGPL-3.0. See LICENSE for details.
@@ -59,6 +63,7 @@ exports.__resetEnvSources = __resetEnvSources;
59
63
  * - 'unset' — not in process.env
60
64
  */
61
65
  const fs = __importStar(require("node:fs"));
66
+ const path = __importStar(require("node:path"));
62
67
  const ENV_SOURCE_TAG = Symbol.for('aiden.envSource');
63
68
  function getMap() {
64
69
  let m = globalThis[ENV_SOURCE_TAG];
@@ -121,3 +126,103 @@ function getEnvSource(key) {
121
126
  function __resetEnvSources() {
122
127
  getMap().clear();
123
128
  }
129
+ // ── Phase v4.1-mcp.2 — Multi-source env loader for `aiden mcp serve` ──
130
+ //
131
+ // When Claude Desktop / Cursor / Claude Code spawn `aiden mcp serve`
132
+ // over stdio, they pass an EMPTY env block by default. Without an
133
+ // explicit `env: {...}` per-server entry in the client config, the
134
+ // spawned aiden has no GROQ_API_KEY, GEMINI_API_KEY, etc. Provider-
135
+ // using tools (subagent_fanout, web_search, fetch_url, …) then fail
136
+ // with "no providers configured".
137
+ //
138
+ // Fix: at MCP serve startup, eagerly load .env files from a small
139
+ // list of well-known locations into process.env. Same fill-only
140
+ // semantics as `loadAidenEnvFile` (preset > file). Caller passes
141
+ // `paths.envFile` (covers `~/.aiden/.env` and Windows-equivalent).
142
+ // We additionally probe the install directory so a project-local
143
+ // `.env` in the Aiden checkout works out of the box.
144
+ //
145
+ // NEVER log the values — only the source path and the set of keys
146
+ // (names) detected.
147
+ /** Walk up from `from` looking for an Aiden install root — a directory
148
+ * containing a `package.json` whose `name` is `aiden-runtime`. Returns
149
+ * null when no candidate is found within `maxDepth` levels. */
150
+ function resolveAidenInstallDir(from = __dirname, maxDepth = 8) {
151
+ let dir = path.resolve(from);
152
+ for (let i = 0; i < maxDepth; i += 1) {
153
+ const pkg = path.join(dir, 'package.json');
154
+ try {
155
+ const text = fs.readFileSync(pkg, 'utf8');
156
+ const parsed = JSON.parse(text);
157
+ if (parsed.name === 'aiden-runtime')
158
+ return dir;
159
+ }
160
+ catch { /* not present or unparseable, walk up */ }
161
+ const parent = path.dirname(dir);
162
+ if (parent === dir)
163
+ break;
164
+ dir = parent;
165
+ }
166
+ return null;
167
+ }
168
+ /** Load `.env` files for `aiden mcp serve`. Order:
169
+ *
170
+ * 1. `<aiden_install_dir>/.env` — project-local, dev convenience
171
+ * 2. `aidenHomeEnv` — per-user, `paths.envFile`
172
+ *
173
+ * 3. process.env — already loaded; takes precedence over both
174
+ * via fill-only semantics.
175
+ *
176
+ * Caller logs the report via the mcp-stdio logger (stderr-safe).
177
+ * Values are NEVER returned — only counts + key names. */
178
+ function loadMcpEnvSources(opts) {
179
+ const attempts = [];
180
+ const installDir = opts.installDir ?? resolveAidenInstallDir();
181
+ const candidates = [];
182
+ if (installDir)
183
+ candidates.push(path.join(installDir, '.env'));
184
+ candidates.push(opts.aidenHomeEnv);
185
+ let appliedTotal = 0;
186
+ for (const file of candidates) {
187
+ const before = new Set(Object.keys(process.env));
188
+ let exists = false;
189
+ try {
190
+ fs.accessSync(file, fs.constants.R_OK);
191
+ exists = true;
192
+ }
193
+ catch { /* missing — record and skip */ }
194
+ if (exists)
195
+ loadAidenEnvFile(file);
196
+ const appliedKeys = [];
197
+ for (const k of Object.keys(process.env)) {
198
+ if (!before.has(k))
199
+ appliedKeys.push(k);
200
+ }
201
+ appliedTotal += appliedKeys.length;
202
+ attempts.push({ path: file, exists, appliedKeys });
203
+ }
204
+ return { attempts, appliedTotal };
205
+ }
206
+ /** The provider-key surface aiden cares about for `mcp status` output.
207
+ * Listed explicitly so we never accidentally enumerate / log a
208
+ * newly-added secret. */
209
+ exports.KNOWN_PROVIDER_KEYS = [
210
+ 'GROQ_API_KEY',
211
+ 'GEMINI_API_KEY',
212
+ 'TOGETHER_API_KEY',
213
+ 'OPENROUTER_API_KEY',
214
+ 'ANTHROPIC_API_KEY',
215
+ 'OPENAI_API_KEY',
216
+ 'CEREBRAS_API_KEY',
217
+ 'NVIDIA_API_KEY',
218
+ 'COHERE_API_KEY',
219
+ ];
220
+ /** Snapshot of provider-key presence + source. Values NEVER returned;
221
+ * only the source tag (`preset`/`aiden-env`/`unset`). */
222
+ function describeProviderKeys(keys = exports.KNOWN_PROVIDER_KEYS) {
223
+ return keys.map((key) => ({
224
+ key,
225
+ present: typeof process.env[key] === 'string' && process.env[key].length > 0,
226
+ source: getEnvSource(key),
227
+ }));
228
+ }
@@ -0,0 +1,74 @@
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
+ * cli/v4/ghostMatch.ts — Tier-3.1.1 (v4.1-tier3.1.1)
10
+ *
11
+ * Compute the best ghost-text match for what the user has typed so far.
12
+ * The aidenPrompt component calls `findGhost(typed, ctx)` on every
13
+ * keystroke and renders the returned suggestion (in dim) past the
14
+ * cursor.
15
+ *
16
+ * Two modes:
17
+ *
18
+ * 1. Slash mode (typed starts with `/`): match against registered
19
+ * slash command names + aliases. Longest start-with match wins
20
+ * (so `/p` favours `/plugins` over `/personality`/`/providers`
21
+ * only if no shorter unique match exists; ties broken by
22
+ * alphabetical order so the result is deterministic).
23
+ *
24
+ * 2. Free-text mode: match against recent user prompts (most recent
25
+ * first). Returns the first prompt that starts with `typed` and
26
+ * is strictly longer.
27
+ *
28
+ * Returns the SUFFIX to append after the typed text, or `null` if no
29
+ * match exists. Empty/whitespace-only typed text → null. Typed text
30
+ * containing a paste-compression label (`[paste #N: …]`) → null
31
+ * (don't suggest over a compressed paste).
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.findGhost = findGhost;
35
+ const PASTE_LABEL_RE = /\[paste #\d+:[^\]]*\]/;
36
+ /**
37
+ * Return the suffix to append (everything past `typed`) or null.
38
+ *
39
+ * Examples:
40
+ * findGhost('/cr', { slashNames: ['cron','clear'], … }) → 'on'
41
+ * findGhost('/x', { slashNames: ['cron'], … }) → null
42
+ * findGhost('how ', { history: ['how do I quit'], … }) → 'do I quit'
43
+ */
44
+ function findGhost(typed, ctx) {
45
+ if (!typed || typed.trim().length === 0)
46
+ return null;
47
+ if (PASTE_LABEL_RE.test(typed))
48
+ return null;
49
+ if (typed.startsWith('/')) {
50
+ const stem = typed.slice(1);
51
+ if (stem.length === 0)
52
+ return null;
53
+ const all = [...ctx.slashNames, ...ctx.slashAliases];
54
+ // Longest start-with match wins. Ties broken alphabetically.
55
+ const candidates = all
56
+ .filter((n) => n.startsWith(stem) && n.length > stem.length)
57
+ .sort();
58
+ if (candidates.length === 0)
59
+ return null;
60
+ // Prefer the SHORTEST candidate that uniquely starts with stem
61
+ // (more likely the user-intended completion). Fall back to the
62
+ // alphabetically-first if all share the same length.
63
+ const shortest = candidates.reduce((best, c) => c.length < best.length ? c : best);
64
+ return shortest.slice(stem.length);
65
+ }
66
+ // Free-text — history fallback. Prefer the most-recent strict
67
+ // start-with match.
68
+ for (const past of ctx.history) {
69
+ if (past.startsWith(typed) && past.length > typed.length) {
70
+ return past.slice(typed.length);
71
+ }
72
+ }
73
+ return null;
74
+ }
@@ -0,0 +1,163 @@
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
+ * cli/v4/historyStore.ts — Tier-3.1.1 (v4.1-tier3.1.1)
10
+ *
11
+ * Persistent input history for the chat REPL. Each user prompt is
12
+ * appended to `<aidenHome>/.aiden_history`, one entry per line,
13
+ * multiline-encoded so a prompt with embedded newlines round-trips
14
+ * faithfully:
15
+ * - `\n` inside a prompt is encoded as `\\n` on disk
16
+ * - `\\` inside a prompt is encoded as `\\\\` on disk
17
+ *
18
+ * The store filters out:
19
+ * - blank entries
20
+ * - duplicates of the most recent entry
21
+ * - very-short entries (<3 chars after trim) — too noisy to suggest
22
+ * - paste-labelled entries (`[paste #N: …]`) — privacy. The user's
23
+ * real prompt was the label-substituted text; storing the label
24
+ * would leak nothing but storing the expanded text would leak the
25
+ * entire pasted block into a plain-text history file.
26
+ *
27
+ * On startup, `loadRecent()` returns the last `limit` entries (newest
28
+ * first) for the autosuggest history fallback.
29
+ *
30
+ * Atomic write: each `append` writes a temp sibling then renames over
31
+ * the live file (Windows rename is atomic on the same volume), so a
32
+ * crash mid-write can never corrupt the history.
33
+ */
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.HISTORY_MAX_ENTRIES = void 0;
39
+ exports.appendHistory = appendHistory;
40
+ exports.loadRecent = loadRecent;
41
+ exports._resetForTests = _resetForTests;
42
+ const node_fs_1 = require("node:fs");
43
+ const node_path_1 = __importDefault(require("node:path"));
44
+ const paths_1 = require("../../core/v4/paths");
45
+ const HISTORY_FILENAME = '.aiden_history';
46
+ const PASTE_LABEL_RE = /\[paste #\d+:[^\]]*\]/;
47
+ let writeLatch = Promise.resolve();
48
+ function historyPath() {
49
+ return node_path_1.default.join((0, paths_1.resolveAidenPaths)().root, HISTORY_FILENAME);
50
+ }
51
+ /** Encode a prompt for one-line storage on disk. */
52
+ function encode(s) {
53
+ return s.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
54
+ }
55
+ /** Reverse of `encode`. */
56
+ function decode(s) {
57
+ // Walk the string so `\\\\n` decodes to `\\n` (literal backslash + n)
58
+ // rather than the placeholder for newline.
59
+ let out = '';
60
+ for (let i = 0; i < s.length; i += 1) {
61
+ const c = s[i];
62
+ if (c === '\\' && i + 1 < s.length) {
63
+ const next = s[i + 1];
64
+ if (next === 'n') {
65
+ out += '\n';
66
+ i += 1;
67
+ }
68
+ else if (next === '\\') {
69
+ out += '\\';
70
+ i += 1;
71
+ }
72
+ else {
73
+ out += c;
74
+ }
75
+ }
76
+ else {
77
+ out += c;
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * Append `entry` to the history file. Filters per the rules above.
84
+ * Best-effort — disk failures are swallowed so a crashed history
85
+ * write never breaks the agent loop.
86
+ */
87
+ /**
88
+ * Tier-3-essentials: cap the live file at this many entries. When an
89
+ * append would push the count above, we rotate the oldest out before
90
+ * writing the new line. 5000 = ~250 KB at typical prompt sizes,
91
+ * trivial to keep on disk and load on every prompt.
92
+ */
93
+ exports.HISTORY_MAX_ENTRIES = 5000;
94
+ async function appendHistory(entry) {
95
+ const trimmed = entry.trim();
96
+ if (trimmed.length < 3)
97
+ return;
98
+ if (PASTE_LABEL_RE.test(trimmed))
99
+ return;
100
+ await (writeLatch = writeLatch.then(async () => {
101
+ try {
102
+ const p = historyPath();
103
+ const dir = node_path_1.default.dirname(p);
104
+ if (!(0, node_fs_1.existsSync)(dir))
105
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
106
+ // Read current contents once — we use them for both dup-suppress
107
+ // and rotation.
108
+ let priorLines = [];
109
+ try {
110
+ const cur = await node_fs_1.promises.readFile(p, 'utf8');
111
+ priorLines = cur.split('\n').filter((l) => l.length > 0);
112
+ }
113
+ catch { /* file may not exist yet */ }
114
+ // Skip if equal to the last entry on disk (cheap dup-suppress).
115
+ const lastDecoded = priorLines.length > 0 ? decode(priorLines[priorLines.length - 1]) : '';
116
+ if (lastDecoded === entry)
117
+ return;
118
+ // Rotation: cap at HISTORY_MAX_ENTRIES. The new line will push
119
+ // total to len+1; if that exceeds the cap, drop the oldest
120
+ // (len+1 - cap) entries from the front so we land EXACTLY at
121
+ // the cap.
122
+ const wantTotal = priorLines.length + 1;
123
+ if (wantTotal > exports.HISTORY_MAX_ENTRIES) {
124
+ const dropFront = wantTotal - exports.HISTORY_MAX_ENTRIES;
125
+ priorLines = priorLines.slice(dropFront);
126
+ }
127
+ const tmp = `${p}.tmp`;
128
+ const nextContent = priorLines.join('\n')
129
+ + (priorLines.length > 0 ? '\n' : '')
130
+ + `${encode(entry)}\n`;
131
+ await node_fs_1.promises.writeFile(tmp, nextContent, 'utf8');
132
+ await node_fs_1.promises.rename(tmp, p);
133
+ }
134
+ catch {
135
+ // History write failure must not bubble up.
136
+ }
137
+ }));
138
+ }
139
+ /**
140
+ * Return the last `limit` entries (newest first). Decoded — caller
141
+ * sees the original prompt verbatim including any embedded newlines.
142
+ *
143
+ * Tier-3-essentials: default raised 100 → 500 so the autosuggest
144
+ * history-mode reaches further back. The on-disk cap is independently
145
+ * controlled by `HISTORY_MAX_ENTRIES`.
146
+ */
147
+ async function loadRecent(limit = 500) {
148
+ try {
149
+ const p = historyPath();
150
+ const raw = await node_fs_1.promises.readFile(p, 'utf8');
151
+ const lines = raw.split('\n').filter((l) => l.length > 0);
152
+ const decoded = lines.map(decode);
153
+ const sliced = decoded.slice(Math.max(0, decoded.length - limit));
154
+ return sliced.reverse();
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ /** Test/reset hook: drop in-process state. Disk untouched. */
161
+ function _resetForTests() {
162
+ writeLatch = Promise.resolve();
163
+ }