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.
- package/README.md +19 -11
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +424 -7
- 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 +256 -55
- 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 +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -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 +297 -13
- package/dist/cli/v4/doctor.js +102 -1
- package/dist/cli/v4/doctorLiveness.js +329 -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/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/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 +118 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +140 -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/moat/dangerousPatterns.js +1 -1
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +51 -1
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +190 -0
- 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
|
+
}
|