aiden-runtime 4.1.1 → 4.1.3
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 +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +452 -0
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +185 -69
package/dist/cli/v4/doctor.js
CHANGED
|
@@ -55,6 +55,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
55
55
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
56
56
|
};
|
|
57
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.DOCTOR_GROUP_ORDER = void 0;
|
|
59
|
+
exports.subsystemHealthResults = subsystemHealthResults;
|
|
60
|
+
exports.renderSubsystemHealthSection = renderSubsystemHealthSection;
|
|
61
|
+
exports.skillOutcomeResults = skillOutcomeResults;
|
|
62
|
+
exports.sessionCounterResults = sessionCounterResults;
|
|
63
|
+
exports.renderSkillOutcomesSection = renderSkillOutcomesSection;
|
|
58
64
|
exports.resolveBinaryPath = resolveBinaryPath;
|
|
59
65
|
exports._resetBinaryResolutionCacheForTests = _resetBinaryResolutionCacheForTests;
|
|
60
66
|
exports.buildProbeInvocation = buildProbeInvocation;
|
|
@@ -83,6 +89,273 @@ const license_1 = require("../../core/v4/license");
|
|
|
83
89
|
const checkUpdate_1 = require("../../core/v4/update/checkUpdate");
|
|
84
90
|
const box_1 = require("./box");
|
|
85
91
|
const audioBackend_1 = require("../../core/voice/audioBackend");
|
|
92
|
+
/**
|
|
93
|
+
* Display order for groups. Renderer iterates this list rather than
|
|
94
|
+
* the order checks appear in the results array — keeps the visual
|
|
95
|
+
* hierarchy stable even if `runDoctor` reorders its check sequence.
|
|
96
|
+
*/
|
|
97
|
+
exports.DOCTOR_GROUP_ORDER = [
|
|
98
|
+
'Providers',
|
|
99
|
+
'Inference',
|
|
100
|
+
'System tools',
|
|
101
|
+
'Storage',
|
|
102
|
+
'Voice',
|
|
103
|
+
'Updates',
|
|
104
|
+
'Subsystem health',
|
|
105
|
+
'Skill outcomes',
|
|
106
|
+
// v4.1.3-essentials doctor-polish: session counters land just
|
|
107
|
+
// above provider liveness — they're agent-loop telemetry, distinct
|
|
108
|
+
// from skill outcomes (per-skill stats) and subsystem health
|
|
109
|
+
// (per-subsystem error tracking).
|
|
110
|
+
'Session counters',
|
|
111
|
+
'Provider liveness',
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* v4.1.3-essentials doctor-polish: convert the subsystem-health
|
|
115
|
+
* registry snapshot into CheckResult rows so they render inside the
|
|
116
|
+
* grouped health box alongside the other checks. Returns an empty
|
|
117
|
+
* array when the registry is undefined / empty so the renderer skips
|
|
118
|
+
* the group cleanly (DOCTOR_GROUP_ORDER drops empty groups).
|
|
119
|
+
*
|
|
120
|
+
* One row per subsystem, plus a fixed "(not instrumented yet)" row
|
|
121
|
+
* for HonestyEnforcement — same convention as the legacy section
|
|
122
|
+
* renderer below.
|
|
123
|
+
*/
|
|
124
|
+
function subsystemHealthResults(registry) {
|
|
125
|
+
if (!registry)
|
|
126
|
+
return [];
|
|
127
|
+
const snaps = registry.snapshot();
|
|
128
|
+
if (snaps.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const s of snaps) {
|
|
132
|
+
const passed = s.totalErrors === 0;
|
|
133
|
+
const stats = `${s.totalCalls} call${s.totalCalls === 1 ? '' : 's'}, ${s.totalErrors} error${s.totalErrors === 1 ? '' : 's'}`;
|
|
134
|
+
let message = stats;
|
|
135
|
+
let suggestion;
|
|
136
|
+
if (s.lastError) {
|
|
137
|
+
const ago = humanAge(Date.now() - s.lastError.at.getTime());
|
|
138
|
+
const streak = s.lastError.consecutive > 1
|
|
139
|
+
? ` (${s.lastError.consecutive} consecutive)`
|
|
140
|
+
: '';
|
|
141
|
+
message = `${stats}${streak}`;
|
|
142
|
+
suggestion = `last ${ago} ago: "${s.lastError.message}"`;
|
|
143
|
+
}
|
|
144
|
+
out.push({
|
|
145
|
+
name: s.subsystem,
|
|
146
|
+
group: 'Subsystem health',
|
|
147
|
+
passed,
|
|
148
|
+
message,
|
|
149
|
+
...(suggestion ? { suggestion } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Slice3 audit decision: HonestyEnforcement was deliberately not
|
|
153
|
+
// instrumented (pure-pattern path has no failure surface). Surface
|
|
154
|
+
// that explicitly so users know the gap is known, not forgotten.
|
|
155
|
+
out.push({
|
|
156
|
+
name: 'honesty',
|
|
157
|
+
group: 'Subsystem health',
|
|
158
|
+
passed: true,
|
|
159
|
+
message: '(not instrumented yet)',
|
|
160
|
+
});
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Phase v4.1.2-slice3: render the Subsystem health section.
|
|
165
|
+
*
|
|
166
|
+
* v4.1.3-essentials doctor-polish: kept for back-compat with any
|
|
167
|
+
* direct callers but the in-REPL `/doctor` path now uses
|
|
168
|
+
* `subsystemHealthResults()` and renders inline via the grouped box.
|
|
169
|
+
*
|
|
170
|
+
* Decision tree (per slice3 Phase 3 Q4):
|
|
171
|
+
* - registry undefined → render nothing (no live state to report)
|
|
172
|
+
* - all subsystems healthy → one-line green summary
|
|
173
|
+
* - any degradation → expand block with last-error per failed sub
|
|
174
|
+
*
|
|
175
|
+
* The Honesty layer is intentionally listed as "(not instrumented yet)"
|
|
176
|
+
* when the expanded block fires, because the audit determined the
|
|
177
|
+
* pure-pattern path has no I/O failure surface today.
|
|
178
|
+
*/
|
|
179
|
+
function renderSubsystemHealthSection(registry) {
|
|
180
|
+
if (!registry)
|
|
181
|
+
return '';
|
|
182
|
+
const snaps = registry.snapshot();
|
|
183
|
+
if (snaps.length === 0)
|
|
184
|
+
return '';
|
|
185
|
+
const degraded = snaps.filter((s) => s.totalErrors > 0);
|
|
186
|
+
if (degraded.length === 0) {
|
|
187
|
+
return `\nSubsystem health: all green (${snaps.length} subsystems instrumented)\n`;
|
|
188
|
+
}
|
|
189
|
+
// Expanded form. Per-subsystem rows:
|
|
190
|
+
// ✓ name N calls, 0 errors
|
|
191
|
+
// ✗ name N calls, E errors (last <duration> ago: "message")
|
|
192
|
+
// - honesty (not instrumented yet)
|
|
193
|
+
const lines = ['\nSubsystem health'];
|
|
194
|
+
for (const s of snaps) {
|
|
195
|
+
const mark = s.totalErrors > 0 ? 'x' : 'ok';
|
|
196
|
+
const stats = `${s.totalCalls} call${s.totalCalls === 1 ? '' : 's'}, ${s.totalErrors} error${s.totalErrors === 1 ? '' : 's'}`;
|
|
197
|
+
if (s.lastError) {
|
|
198
|
+
const ago = humanAge(Date.now() - s.lastError.at.getTime());
|
|
199
|
+
const streak = s.lastError.consecutive > 1
|
|
200
|
+
? ` (${s.lastError.consecutive} consecutive)`
|
|
201
|
+
: '';
|
|
202
|
+
lines.push(` [${mark}] ${s.subsystem.padEnd(16)} ${stats}${streak} (last ${ago} ago: "${s.lastError.message}")`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
lines.push(` [${mark}] ${s.subsystem.padEnd(16)} ${stats}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Slice3 audit decision: HonestyEnforcement was deliberately not
|
|
209
|
+
// instrumented (pure-pattern path has no failure surface). Surface
|
|
210
|
+
// that explicitly so users know the gap is known, not forgotten.
|
|
211
|
+
lines.push(` [-] honesty (not instrumented yet)`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
function humanAge(ms) {
|
|
216
|
+
if (ms < 1000)
|
|
217
|
+
return `${ms}ms`;
|
|
218
|
+
if (ms < 60000)
|
|
219
|
+
return `${(ms / 1000).toFixed(0)}s`;
|
|
220
|
+
if (ms < 3600000)
|
|
221
|
+
return `${Math.floor(ms / 60000)}m`;
|
|
222
|
+
if (ms < 86400000)
|
|
223
|
+
return `${Math.floor(ms / 3600000)}h`;
|
|
224
|
+
return `${Math.floor(ms / 86400000)}d`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* v4.1.3-essentials doctor-polish: convert the skill-outcome tracker
|
|
228
|
+
* snapshot into CheckResult rows so they render inside the grouped
|
|
229
|
+
* health box. Empty tracker → empty array → group dropped by renderer.
|
|
230
|
+
*
|
|
231
|
+
* One row per top-N skill (sorted by load count). Skills with at
|
|
232
|
+
* least one failure render as `passed:false` so they get the red
|
|
233
|
+
* `✗` icon and a hint line with the last-error message.
|
|
234
|
+
*/
|
|
235
|
+
function skillOutcomeResults(tracker, topN = 5) {
|
|
236
|
+
if (!tracker)
|
|
237
|
+
return [];
|
|
238
|
+
const snaps = tracker.snapshot();
|
|
239
|
+
if (snaps.length === 0)
|
|
240
|
+
return [];
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const s of snaps.slice(0, topN)) {
|
|
243
|
+
const attributed = s.toolSuccesses + s.toolFailures;
|
|
244
|
+
const rate = attributed === 0
|
|
245
|
+
? '—'
|
|
246
|
+
: `${Math.round((s.toolSuccesses / attributed) * 100)}% success`;
|
|
247
|
+
const last = s.lastUsed
|
|
248
|
+
? `, last ${humanAge(Date.now() - new Date(s.lastUsed).getTime())} ago`
|
|
249
|
+
: '';
|
|
250
|
+
const message = `loaded ${s.loaded}, ${s.toolSuccesses} ok, ${s.toolFailures} err (${rate})${last}`;
|
|
251
|
+
const passed = s.toolFailures === 0;
|
|
252
|
+
out.push({
|
|
253
|
+
name: s.skillName,
|
|
254
|
+
group: 'Skill outcomes',
|
|
255
|
+
passed,
|
|
256
|
+
message,
|
|
257
|
+
...(s.lastError && !passed
|
|
258
|
+
? { suggestion: `last failure: "${s.lastError.message}"` }
|
|
259
|
+
: {}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function sessionCounterResults(agent) {
|
|
265
|
+
if (!agent)
|
|
266
|
+
return [];
|
|
267
|
+
const s = agent.getSkillEnforcementMetrics();
|
|
268
|
+
const u = agent.getUrlProvenanceMetrics();
|
|
269
|
+
const e = agent.getEmptyResponseMetrics();
|
|
270
|
+
// Same all-zero classifier per row — keeps the rule symmetric
|
|
271
|
+
// across the three counter sources even though their internal
|
|
272
|
+
// semantics differ.
|
|
273
|
+
const allZero = (vals) => vals.every((v) => v === 0);
|
|
274
|
+
const rows = [];
|
|
275
|
+
// ── skill enforcement ─────────────────────────────────────────────
|
|
276
|
+
{
|
|
277
|
+
const passed = allZero([s.armed, s.preArmed, s.recovered, s.failed]);
|
|
278
|
+
rows.push({
|
|
279
|
+
name: 'skill enforcement',
|
|
280
|
+
group: 'Session counters',
|
|
281
|
+
passed: true,
|
|
282
|
+
message: `armed=${s.armed}, pre-armed=${s.preArmed}, ` +
|
|
283
|
+
`recovered=${s.recovered}, failed=${s.failed}`,
|
|
284
|
+
// Suggestion attached only when non-zero so outcomeBucket()
|
|
285
|
+
// routes the row to warning (yellow) rather than passing (green).
|
|
286
|
+
// Text is concise — full counters already in `message`.
|
|
287
|
+
...(passed ? {} : { suggestion: 'guard fired this session — review if any failed > 0' }),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// ── URL provenance ────────────────────────────────────────────────
|
|
291
|
+
{
|
|
292
|
+
const passed = allZero([u.blocked, u.recovered, u.failed]);
|
|
293
|
+
rows.push({
|
|
294
|
+
name: 'url provenance',
|
|
295
|
+
group: 'Session counters',
|
|
296
|
+
passed: true,
|
|
297
|
+
message: `blocked=${u.blocked}, recovered=${u.recovered}, failed=${u.failed}`,
|
|
298
|
+
...(passed ? {} : { suggestion: 'open_url provenance gate fired this session' }),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
// ── empty response ────────────────────────────────────────────────
|
|
302
|
+
{
|
|
303
|
+
const passed = allZero([e.detected, e.retried, e.recovered]);
|
|
304
|
+
rows.push({
|
|
305
|
+
name: 'empty response',
|
|
306
|
+
group: 'Session counters',
|
|
307
|
+
passed: true,
|
|
308
|
+
message: `detected=${e.detected}, retried=${e.retried}, recovered=${e.recovered}`,
|
|
309
|
+
...(passed ? {} : { suggestion: 'provider emitted empty turn(s) this session' }),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return rows;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Phase v4.1.2-slice4: render the Skill outcomes section.
|
|
316
|
+
*
|
|
317
|
+
* v4.1.3-essentials doctor-polish: kept for back-compat. The in-REPL
|
|
318
|
+
* `/doctor` path now uses `skillOutcomeResults()` and renders inline
|
|
319
|
+
* via the grouped box.
|
|
320
|
+
*
|
|
321
|
+
* Per Q3 decision: silent on empty state (no tracker, or no skills
|
|
322
|
+
* tracked yet) — doctor output for healthy systems stays short.
|
|
323
|
+
*
|
|
324
|
+
* Output (when not empty): top N skills sorted by load count, with
|
|
325
|
+
* total observations and success percentage. Last-error message
|
|
326
|
+
* shown for the one most-recently failing skill (cap one row of
|
|
327
|
+
* detail so the block stays compact).
|
|
328
|
+
*/
|
|
329
|
+
function renderSkillOutcomesSection(tracker, topN = 5) {
|
|
330
|
+
if (!tracker)
|
|
331
|
+
return '';
|
|
332
|
+
const snaps = tracker.snapshot();
|
|
333
|
+
if (snaps.length === 0)
|
|
334
|
+
return '';
|
|
335
|
+
const lines = ['\nSkill outcomes (top ' + Math.min(topN, snaps.length) + ' by load count)'];
|
|
336
|
+
for (const s of snaps.slice(0, topN)) {
|
|
337
|
+
const attributed = s.toolSuccesses + s.toolFailures;
|
|
338
|
+
const rate = attributed === 0
|
|
339
|
+
? '—'
|
|
340
|
+
: `${Math.round((s.toolSuccesses / attributed) * 100)}% success`;
|
|
341
|
+
const stats = `loaded ${s.loaded}, ${s.toolSuccesses} ok, ${s.toolFailures} err (${rate})`;
|
|
342
|
+
const last = s.lastUsed
|
|
343
|
+
? ` last ${humanAge(Date.now() - new Date(s.lastUsed).getTime())} ago`
|
|
344
|
+
: '';
|
|
345
|
+
lines.push(` ${s.skillName.padEnd(32)} ${stats}${last}`);
|
|
346
|
+
}
|
|
347
|
+
// Spotlight the most-recent failure across all tracked skills so a
|
|
348
|
+
// single broken skill is visible without scanning every row.
|
|
349
|
+
const recentFailures = snaps
|
|
350
|
+
.filter((s) => s.lastError)
|
|
351
|
+
.sort((a, b) => new Date(b.lastError.at).getTime() - new Date(a.lastError.at).getTime());
|
|
352
|
+
if (recentFailures.length > 0) {
|
|
353
|
+
const f = recentFailures[0];
|
|
354
|
+
lines.push(` ↳ last failure: ${f.skillName} — "${f.lastError.message}"`);
|
|
355
|
+
}
|
|
356
|
+
lines.push('');
|
|
357
|
+
return lines.join('\n');
|
|
358
|
+
}
|
|
86
359
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
87
360
|
/** Wrap a promise with a timeout. The timed-out path resolves to the fallback result. */
|
|
88
361
|
async function withTimeout(p, ms, fallback) {
|
|
@@ -228,6 +501,7 @@ async function checkConfigFile(paths) {
|
|
|
228
501
|
await node_fs_1.promises.access(paths.configYaml);
|
|
229
502
|
return {
|
|
230
503
|
name: 'config file',
|
|
504
|
+
group: 'Storage',
|
|
231
505
|
passed: true,
|
|
232
506
|
message: `found at ${paths.configYaml}`,
|
|
233
507
|
durationMs: Date.now() - t0,
|
|
@@ -236,6 +510,7 @@ async function checkConfigFile(paths) {
|
|
|
236
510
|
catch {
|
|
237
511
|
return {
|
|
238
512
|
name: 'config file',
|
|
513
|
+
group: 'Storage',
|
|
239
514
|
passed: false,
|
|
240
515
|
message: `missing at ${paths.configYaml}`,
|
|
241
516
|
suggestion: 'run `aiden setup` to create one',
|
|
@@ -259,6 +534,7 @@ function checkProviderAuth(env) {
|
|
|
259
534
|
if (present.length === 0) {
|
|
260
535
|
return {
|
|
261
536
|
name: 'provider auth',
|
|
537
|
+
group: 'Providers',
|
|
262
538
|
passed: false,
|
|
263
539
|
message: 'no provider API key found in environment',
|
|
264
540
|
suggestion: 'run `aiden setup` and pick a provider, or set ANTHROPIC_API_KEY',
|
|
@@ -267,6 +543,7 @@ function checkProviderAuth(env) {
|
|
|
267
543
|
}
|
|
268
544
|
return {
|
|
269
545
|
name: 'provider auth',
|
|
546
|
+
group: 'Providers',
|
|
270
547
|
passed: true,
|
|
271
548
|
message: `${present.length} provider key(s) present (${present.join(', ')})`,
|
|
272
549
|
durationMs: Date.now() - t0,
|
|
@@ -276,6 +553,7 @@ async function checkOllamaReachable(opts) {
|
|
|
276
553
|
const t0 = Date.now();
|
|
277
554
|
const fallback = {
|
|
278
555
|
name: 'ollama reachable',
|
|
556
|
+
group: 'Inference',
|
|
279
557
|
passed: false,
|
|
280
558
|
message: 'no response from http://localhost:11434',
|
|
281
559
|
suggestion: 'install Ollama from https://ollama.com or skip if you only use cloud providers',
|
|
@@ -293,6 +571,7 @@ async function checkOllamaReachable(opts) {
|
|
|
293
571
|
}
|
|
294
572
|
return {
|
|
295
573
|
name: 'ollama reachable',
|
|
574
|
+
group: 'Inference',
|
|
296
575
|
passed: true,
|
|
297
576
|
message: 'Ollama responding on :11434',
|
|
298
577
|
durationMs: Date.now() - t0,
|
|
@@ -311,6 +590,7 @@ async function checkPythonAvailable(opts) {
|
|
|
311
590
|
const t0 = Date.now();
|
|
312
591
|
const fallback = {
|
|
313
592
|
name: 'python available',
|
|
593
|
+
group: 'System tools',
|
|
314
594
|
passed: false,
|
|
315
595
|
message: 'python not found on PATH',
|
|
316
596
|
suggestion: 'install python 3.10+ — required for graphify and a few skills',
|
|
@@ -323,6 +603,7 @@ async function checkPythonAvailable(opts) {
|
|
|
323
603
|
if (res.ok) {
|
|
324
604
|
return {
|
|
325
605
|
name: 'python available',
|
|
606
|
+
group: 'System tools',
|
|
326
607
|
passed: true,
|
|
327
608
|
message: res.stdout || `${bin} present`,
|
|
328
609
|
durationMs: Date.now() - t0,
|
|
@@ -339,6 +620,7 @@ async function checkDockerAvailable(opts) {
|
|
|
339
620
|
if (res.ok) {
|
|
340
621
|
return {
|
|
341
622
|
name: 'docker available',
|
|
623
|
+
group: 'System tools',
|
|
342
624
|
passed: true,
|
|
343
625
|
message: res.stdout || 'docker present',
|
|
344
626
|
durationMs: Date.now() - t0,
|
|
@@ -346,6 +628,7 @@ async function checkDockerAvailable(opts) {
|
|
|
346
628
|
}
|
|
347
629
|
return {
|
|
348
630
|
name: 'docker available',
|
|
631
|
+
group: 'System tools',
|
|
349
632
|
passed: false,
|
|
350
633
|
message: 'docker not found on PATH',
|
|
351
634
|
suggestion: 'optional — install Docker Desktop if you want sandboxed tool execution',
|
|
@@ -353,6 +636,7 @@ async function checkDockerAvailable(opts) {
|
|
|
353
636
|
};
|
|
354
637
|
})(), opts.timeoutMs, {
|
|
355
638
|
name: 'docker available',
|
|
639
|
+
group: 'System tools',
|
|
356
640
|
passed: false,
|
|
357
641
|
message: 'docker probe timed out',
|
|
358
642
|
durationMs: Date.now() - t0,
|
|
@@ -365,6 +649,7 @@ async function checkNpxAvailable(opts) {
|
|
|
365
649
|
if (res.ok) {
|
|
366
650
|
return {
|
|
367
651
|
name: 'npx available',
|
|
652
|
+
group: 'System tools',
|
|
368
653
|
passed: true,
|
|
369
654
|
message: `npx ${res.stdout}`,
|
|
370
655
|
durationMs: Date.now() - t0,
|
|
@@ -372,6 +657,7 @@ async function checkNpxAvailable(opts) {
|
|
|
372
657
|
}
|
|
373
658
|
return {
|
|
374
659
|
name: 'npx available',
|
|
660
|
+
group: 'System tools',
|
|
375
661
|
passed: false,
|
|
376
662
|
message: 'npx not found on PATH',
|
|
377
663
|
suggestion: 'install Node.js 20+ — required for npm-published MCP servers',
|
|
@@ -379,6 +665,7 @@ async function checkNpxAvailable(opts) {
|
|
|
379
665
|
};
|
|
380
666
|
})(), opts.timeoutMs, {
|
|
381
667
|
name: 'npx available',
|
|
668
|
+
group: 'System tools',
|
|
382
669
|
passed: false,
|
|
383
670
|
message: 'npx probe timed out',
|
|
384
671
|
durationMs: Date.now() - t0,
|
|
@@ -391,6 +678,7 @@ async function checkSkillsDir(paths) {
|
|
|
391
678
|
if (!stat.isDirectory()) {
|
|
392
679
|
return {
|
|
393
680
|
name: 'skills dir',
|
|
681
|
+
group: 'Storage',
|
|
394
682
|
passed: false,
|
|
395
683
|
message: `${paths.skillsDir} is not a directory`,
|
|
396
684
|
durationMs: Date.now() - t0,
|
|
@@ -399,6 +687,7 @@ async function checkSkillsDir(paths) {
|
|
|
399
687
|
const entries = await node_fs_1.promises.readdir(paths.skillsDir);
|
|
400
688
|
return {
|
|
401
689
|
name: 'skills dir',
|
|
690
|
+
group: 'Storage',
|
|
402
691
|
passed: true,
|
|
403
692
|
message: `${paths.skillsDir} (${entries.length} entries)`,
|
|
404
693
|
durationMs: Date.now() - t0,
|
|
@@ -407,6 +696,7 @@ async function checkSkillsDir(paths) {
|
|
|
407
696
|
catch {
|
|
408
697
|
return {
|
|
409
698
|
name: 'skills dir',
|
|
699
|
+
group: 'Storage',
|
|
410
700
|
passed: false,
|
|
411
701
|
message: `missing ${paths.skillsDir}`,
|
|
412
702
|
suggestion: 'run `aiden setup` — it creates the skills directory',
|
|
@@ -420,6 +710,7 @@ async function checkBundledManifest(paths) {
|
|
|
420
710
|
await node_fs_1.promises.access(paths.bundledManifest);
|
|
421
711
|
return {
|
|
422
712
|
name: 'bundled manifest',
|
|
713
|
+
group: 'Storage',
|
|
423
714
|
passed: true,
|
|
424
715
|
message: `present at ${paths.bundledManifest}`,
|
|
425
716
|
durationMs: Date.now() - t0,
|
|
@@ -428,6 +719,7 @@ async function checkBundledManifest(paths) {
|
|
|
428
719
|
catch {
|
|
429
720
|
return {
|
|
430
721
|
name: 'bundled manifest',
|
|
722
|
+
group: 'Storage',
|
|
431
723
|
passed: false,
|
|
432
724
|
message: 'bundled skill manifest missing',
|
|
433
725
|
suggestion: 'reinstall `aiden` — the package was not unpacked correctly',
|
|
@@ -441,6 +733,7 @@ async function checkPlatformPaths(paths) {
|
|
|
441
733
|
await node_fs_1.promises.access(paths.root);
|
|
442
734
|
return {
|
|
443
735
|
name: 'platform paths',
|
|
736
|
+
group: 'Storage',
|
|
444
737
|
passed: true,
|
|
445
738
|
message: `aiden home: ${paths.root}`,
|
|
446
739
|
durationMs: Date.now() - t0,
|
|
@@ -449,6 +742,7 @@ async function checkPlatformPaths(paths) {
|
|
|
449
742
|
catch {
|
|
450
743
|
return {
|
|
451
744
|
name: 'platform paths',
|
|
745
|
+
group: 'Storage',
|
|
452
746
|
passed: false,
|
|
453
747
|
message: `aiden home missing: ${paths.root}`,
|
|
454
748
|
suggestion: 'run `aiden setup` to initialise the home directory',
|
|
@@ -475,6 +769,7 @@ async function checkAudioBackend() {
|
|
|
475
769
|
if (playback && record) {
|
|
476
770
|
return {
|
|
477
771
|
name: 'audio backend',
|
|
772
|
+
group: 'Voice',
|
|
478
773
|
passed: true,
|
|
479
774
|
message: `${process.platform}: playback=${playback.label} · record=${record.label}`,
|
|
480
775
|
durationMs: Date.now() - t0,
|
|
@@ -488,6 +783,7 @@ async function checkAudioBackend() {
|
|
|
488
783
|
missing.push((0, audioBackend_1.missingBackendMessage)('record'));
|
|
489
784
|
return {
|
|
490
785
|
name: 'audio backend',
|
|
786
|
+
group: 'Voice',
|
|
491
787
|
passed: true,
|
|
492
788
|
message: `${process.platform}: voice features will not work — backends missing (known: ${[...new Set([...known.playback, ...known.record])].join(', ') || 'none'})`,
|
|
493
789
|
suggestion: missing.join(' || '),
|
|
@@ -508,6 +804,7 @@ async function checkLicense(opts) {
|
|
|
508
804
|
if (!present) {
|
|
509
805
|
return {
|
|
510
806
|
name: 'license',
|
|
807
|
+
group: 'Updates',
|
|
511
808
|
passed: true,
|
|
512
809
|
message: 'free tier (no license cache)',
|
|
513
810
|
durationMs: Date.now() - t0,
|
|
@@ -521,6 +818,7 @@ async function checkLicense(opts) {
|
|
|
521
818
|
if (status.tier !== 'pro') {
|
|
522
819
|
return {
|
|
523
820
|
name: 'license',
|
|
821
|
+
group: 'Updates',
|
|
524
822
|
passed: true,
|
|
525
823
|
message: 'license cache present but not currently valid (free tier)',
|
|
526
824
|
suggestion: 'run /license refresh to re-verify against server',
|
|
@@ -530,12 +828,14 @@ async function checkLicense(opts) {
|
|
|
530
828
|
const expiry = status.cache.expiresAt || 'lifetime';
|
|
531
829
|
return {
|
|
532
830
|
name: 'license',
|
|
831
|
+
group: 'Updates',
|
|
533
832
|
passed: true,
|
|
534
833
|
message: `Pro (${status.cache.plan}, expires ${expiry})`,
|
|
535
834
|
durationMs: Date.now() - t0,
|
|
536
835
|
};
|
|
537
836
|
})(), opts.timeoutMs, {
|
|
538
837
|
name: 'license',
|
|
838
|
+
group: 'Updates',
|
|
539
839
|
passed: false,
|
|
540
840
|
message: 'license check timed out reading cache',
|
|
541
841
|
durationMs: Date.now() - t0,
|
|
@@ -544,6 +844,7 @@ async function checkLicense(opts) {
|
|
|
544
844
|
catch (err) {
|
|
545
845
|
return {
|
|
546
846
|
name: 'license',
|
|
847
|
+
group: 'Updates',
|
|
547
848
|
passed: false,
|
|
548
849
|
message: `license check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
549
850
|
suggestion: 'run /license refresh; if persistent, re-activate with /license activate <key>',
|
|
@@ -568,6 +869,7 @@ async function checkUpdate(opts) {
|
|
|
568
869
|
const where = status.fromCache ? 'cached' : 'live';
|
|
569
870
|
return {
|
|
570
871
|
name: 'npm update',
|
|
872
|
+
group: 'Updates',
|
|
571
873
|
passed: true,
|
|
572
874
|
message: `installed v${status.installed} is up to date (${where})`,
|
|
573
875
|
durationMs: Date.now() - t0,
|
|
@@ -575,6 +877,7 @@ async function checkUpdate(opts) {
|
|
|
575
877
|
}
|
|
576
878
|
return {
|
|
577
879
|
name: 'npm update',
|
|
880
|
+
group: 'Updates',
|
|
578
881
|
passed: true,
|
|
579
882
|
message: `v${status.latest} available (installed: v${status.installed})`,
|
|
580
883
|
suggestion: 'run `npm install -g aiden-runtime@latest`',
|
|
@@ -584,6 +887,7 @@ async function checkUpdate(opts) {
|
|
|
584
887
|
catch (err) {
|
|
585
888
|
return {
|
|
586
889
|
name: 'npm update',
|
|
890
|
+
group: 'Updates',
|
|
587
891
|
passed: false,
|
|
588
892
|
message: `update check error: ${err instanceof Error ? err.message : String(err)}`,
|
|
589
893
|
durationMs: Date.now() - t0,
|
|
@@ -591,6 +895,7 @@ async function checkUpdate(opts) {
|
|
|
591
895
|
}
|
|
592
896
|
})(), opts.timeoutMs, {
|
|
593
897
|
name: 'npm update',
|
|
898
|
+
group: 'Updates',
|
|
594
899
|
passed: true,
|
|
595
900
|
message: 'update check timed out (network slow — non-fatal)',
|
|
596
901
|
durationMs: Date.now() - t0,
|
|
@@ -605,6 +910,7 @@ async function checkLogsWritable(paths) {
|
|
|
605
910
|
await node_fs_1.promises.unlink(probe);
|
|
606
911
|
return {
|
|
607
912
|
name: 'logs writable',
|
|
913
|
+
group: 'Storage',
|
|
608
914
|
passed: true,
|
|
609
915
|
message: paths.logsDir,
|
|
610
916
|
durationMs: Date.now() - t0,
|
|
@@ -613,6 +919,7 @@ async function checkLogsWritable(paths) {
|
|
|
613
919
|
catch (err) {
|
|
614
920
|
return {
|
|
615
921
|
name: 'logs writable',
|
|
922
|
+
group: 'Storage',
|
|
616
923
|
passed: false,
|
|
617
924
|
message: `cannot write to ${paths.logsDir}: ${err instanceof Error ? err.message : String(err)}`,
|
|
618
925
|
suggestion: `check permissions on ${node_os_1.default.homedir()}`,
|
|
@@ -702,21 +1009,56 @@ function checkIconKind(r) {
|
|
|
702
1009
|
return { icon: '⚠', colour: 'warn' };
|
|
703
1010
|
return { icon: '✓', colour: 'success' };
|
|
704
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Classify outcomes into the three buckets the top summary surfaces:
|
|
1014
|
+
* passing — passed && no suggestion
|
|
1015
|
+
* warning — passed && suggestion present (soft warning — works but
|
|
1016
|
+
* has a remediation hint, e.g. audio backend on Linux)
|
|
1017
|
+
* failing — !passed
|
|
1018
|
+
*
|
|
1019
|
+
* Order matters: failing dominates warning, warning dominates passing.
|
|
1020
|
+
* Same convention as the row-icon picker above.
|
|
1021
|
+
*/
|
|
1022
|
+
function outcomeBucket(r) {
|
|
1023
|
+
if (!r.passed)
|
|
1024
|
+
return 'failing';
|
|
1025
|
+
if (r.suggestion)
|
|
1026
|
+
return 'warning';
|
|
1027
|
+
return 'passing';
|
|
1028
|
+
}
|
|
705
1029
|
function maxNameWidth(report) {
|
|
706
1030
|
return report.results.reduce((m, r) => Math.max(m, r.name.length), 0);
|
|
707
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* v4.1.3-essentials doctor-polish: group results by their `group`
|
|
1034
|
+
* field, preserving the configured display order (DOCTOR_GROUP_ORDER)
|
|
1035
|
+
* and dropping empty groups. Within each group, results stay in their
|
|
1036
|
+
* original insertion order — keeps related checks visually adjacent
|
|
1037
|
+
* even when `runDoctor` reorders for parallelism in a future revision.
|
|
1038
|
+
*/
|
|
1039
|
+
function groupResults(results) {
|
|
1040
|
+
const byGroup = new Map();
|
|
1041
|
+
for (const r of results) {
|
|
1042
|
+
const list = byGroup.get(r.group) ?? [];
|
|
1043
|
+
list.push(r);
|
|
1044
|
+
byGroup.set(r.group, list);
|
|
1045
|
+
}
|
|
1046
|
+
const out = [];
|
|
1047
|
+
for (const g of exports.DOCTOR_GROUP_ORDER) {
|
|
1048
|
+
const rows = byGroup.get(g);
|
|
1049
|
+
if (rows && rows.length > 0)
|
|
1050
|
+
out.push({ group: g, rows });
|
|
1051
|
+
}
|
|
1052
|
+
return out;
|
|
1053
|
+
}
|
|
708
1054
|
/**
|
|
709
1055
|
* Compute the inner-cell width for the health box: widest visible
|
|
710
|
-
* content row across all check rows
|
|
711
|
-
*
|
|
712
|
-
* HEALTH_BOX_MIN/MAX_WIDTH. Title length also
|
|
713
|
-
* `╭── Health Check ─...─╮` row doesn't underflow.
|
|
1056
|
+
* content row across all check rows, section headers, hint
|
|
1057
|
+
* continuations, and the top summary, plus a 1-char trailing gutter.
|
|
1058
|
+
* Floored / capped per HEALTH_BOX_MIN/MAX_WIDTH. Title length also
|
|
1059
|
+
* factored so the `╭── Health Check ─...─╮` row doesn't underflow.
|
|
714
1060
|
*/
|
|
715
1061
|
function computeHealthBoxWidth(report, nameWidth) {
|
|
716
|
-
// The minimum width the title needs (`╭── <title> ──╮` shape):
|
|
717
|
-
// 2 corners + 2 leading dashes + 1 space + title + 1 space + at
|
|
718
|
-
// least 2 trailing dashes, minus the 2 corners since the cell is
|
|
719
|
-
// measured between them.
|
|
720
1062
|
const titleMin = 2 + 1 + HEALTH_BOX_TITLE.length + 1 + 2;
|
|
721
1063
|
let widest = titleMin;
|
|
722
1064
|
const measureRow = (row) => {
|
|
@@ -724,25 +1066,41 @@ function computeHealthBoxWidth(report, nameWidth) {
|
|
|
724
1066
|
if (v + 1 > widest)
|
|
725
1067
|
widest = v + 1; // +1 for trailing gutter
|
|
726
1068
|
};
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1069
|
+
// Top summary line.
|
|
1070
|
+
const buckets = { passing: 0, warning: 0, failing: 0 };
|
|
1071
|
+
for (const r of report.results)
|
|
1072
|
+
buckets[outcomeBucket(r)] += 1;
|
|
1073
|
+
measureRow(` Overall: ${buckets.passing} passing · ${buckets.warning} warning · ${buckets.failing} failing (${report.results.length} checks, ${report.totalMs} ms)`);
|
|
1074
|
+
// Section header rows + group's checks + any hint continuations.
|
|
1075
|
+
const groups = groupResults(report.results);
|
|
1076
|
+
for (const g of groups) {
|
|
1077
|
+
const passed = g.rows.filter((r) => r.passed).length;
|
|
1078
|
+
measureRow(` ${g.group} (${passed}/${g.rows.length})`);
|
|
1079
|
+
for (const r of g.rows) {
|
|
1080
|
+
measureRow(` ✓ ${r.name.padEnd(nameWidth)} ${r.message}`);
|
|
1081
|
+
if (r.suggestion && !r.passed) {
|
|
1082
|
+
measureRow(` hint: ${r.suggestion}`);
|
|
1083
|
+
}
|
|
731
1084
|
}
|
|
732
1085
|
}
|
|
733
|
-
const passedCount = report.results.filter((x) => x.passed).length;
|
|
734
|
-
measureRow(` ${passedCount} of ${report.results.length} checks passed in ${report.totalMs} ms`);
|
|
735
1086
|
return Math.max(HEALTH_BOX_MIN_WIDTH, Math.min(HEALTH_BOX_MAX_WIDTH, widest));
|
|
736
1087
|
}
|
|
737
1088
|
/**
|
|
738
|
-
* Render the report as an orange-bordered rounded box
|
|
739
|
-
*
|
|
740
|
-
*
|
|
1089
|
+
* Render the report as an orange-bordered rounded box with grouped
|
|
1090
|
+
* sections + top summary.
|
|
1091
|
+
*
|
|
1092
|
+
* v4.1.3-essentials doctor-polish: previously rendered as a flat list
|
|
1093
|
+
* of 13 rows with the summary at the bottom — hard to scan, easy to
|
|
1094
|
+
* miss issues. Now:
|
|
1095
|
+
* - Top: `Overall: X passing · Y warning · Z failing` with per-
|
|
1096
|
+
* bucket colors (green / yellow / red).
|
|
1097
|
+
* - Section headers per group (`Providers (1/1)`) in brand+bold.
|
|
1098
|
+
* - Rows packed tight within group, blank line between groups.
|
|
1099
|
+
* - Hints stay on a continuation line under failed rows only.
|
|
741
1100
|
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
* truncated content mid-word for any Windows path > 65-ish chars.
|
|
1101
|
+
* Pure — returns the multi-line string; caller writes it. `display`
|
|
1102
|
+
* is needed for skin-aware colouring of the border, icons, headers,
|
|
1103
|
+
* and summary buckets.
|
|
746
1104
|
*/
|
|
747
1105
|
function renderHealthBox(report, display) {
|
|
748
1106
|
const nameWidth = maxNameWidth(report);
|
|
@@ -758,23 +1116,50 @@ function renderHealthBox(report, display) {
|
|
|
758
1116
|
return `${display.brand(left)}${inner}${display.brand(right)}`;
|
|
759
1117
|
};
|
|
760
1118
|
const lines = [top, side('')];
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1119
|
+
// ── Top summary — three colored buckets ───────────────────────────
|
|
1120
|
+
const buckets = { passing: 0, warning: 0, failing: 0 };
|
|
1121
|
+
for (const r of report.results)
|
|
1122
|
+
buckets[outcomeBucket(r)] += 1;
|
|
1123
|
+
// Each bucket colored only when non-zero; zero counters stay muted
|
|
1124
|
+
// so the eye lands on the actual state.
|
|
1125
|
+
const paintBucket = (n, kind, label) => {
|
|
1126
|
+
const text = `${n} ${label}`;
|
|
1127
|
+
return n > 0 ? display.paint(text, kind) : display.muted(text);
|
|
1128
|
+
};
|
|
1129
|
+
const summary = ` Overall: ${paintBucket(buckets.passing, 'success', 'passing')} · ` +
|
|
1130
|
+
`${paintBucket(buckets.warning, 'warn', 'warning')} · ` +
|
|
1131
|
+
`${paintBucket(buckets.failing, 'error', 'failing')} ` +
|
|
1132
|
+
`${display.muted(`(${report.results.length} checks, ${report.totalMs} ms)`)}`;
|
|
1133
|
+
lines.push(side(summary));
|
|
1134
|
+
lines.push(side(''));
|
|
1135
|
+
// ── Grouped section rendering ─────────────────────────────────────
|
|
1136
|
+
const groups = groupResults(report.results);
|
|
1137
|
+
for (let gi = 0; gi < groups.length; gi += 1) {
|
|
1138
|
+
const g = groups[gi];
|
|
1139
|
+
const passed = g.rows.filter((r) => r.passed).length;
|
|
1140
|
+
// Group-level count tinted by aggregate state: all pass → success,
|
|
1141
|
+
// any fail → error, only warnings → warn.
|
|
1142
|
+
const anyFail = g.rows.some((r) => !r.passed);
|
|
1143
|
+
const anyWarn = g.rows.some((r) => r.passed && r.suggestion);
|
|
1144
|
+
const countColour = anyFail ? 'error' : anyWarn ? 'warn' : 'success';
|
|
1145
|
+
const header = ` ${display.brand(g.group)} ` +
|
|
1146
|
+
display.paint(`(${passed}/${g.rows.length})`, countColour);
|
|
1147
|
+
lines.push(side(header));
|
|
1148
|
+
for (const r of g.rows) {
|
|
1149
|
+
const { icon, colour } = checkIconKind(r);
|
|
1150
|
+
const colouredIcon = display.paint(icon, colour);
|
|
1151
|
+
const namePart = ` ${colouredIcon} ${r.name.padEnd(nameWidth)} ${r.message}`;
|
|
1152
|
+
lines.push(side(namePart));
|
|
1153
|
+
if (r.suggestion && !r.passed) {
|
|
1154
|
+
const hint = ` ${display.muted('hint:')} ${r.suggestion}`;
|
|
1155
|
+
lines.push(side(hint));
|
|
1156
|
+
}
|
|
771
1157
|
}
|
|
1158
|
+
// Blank line between groups (not after the last one).
|
|
1159
|
+
if (gi < groups.length - 1)
|
|
1160
|
+
lines.push(side(''));
|
|
772
1161
|
}
|
|
773
1162
|
lines.push(side(''));
|
|
774
|
-
const passedCount = report.results.filter((r) => r.passed).length;
|
|
775
|
-
const summary = `${passedCount} of ${report.results.length} checks passed in ${report.totalMs} ms`;
|
|
776
|
-
const summaryColour = report.passed ? 'success' : 'warn';
|
|
777
|
-
lines.push(side(' ' + display.paint(summary, summaryColour)));
|
|
778
1163
|
lines.push(bot);
|
|
779
1164
|
return lines.join('\n');
|
|
780
1165
|
}
|
|
@@ -785,39 +1170,68 @@ function renderHealthBox(report, display) {
|
|
|
785
1170
|
*/
|
|
786
1171
|
async function runDoctorCli(opts) {
|
|
787
1172
|
const report = await runDoctor(opts);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
|
|
1173
|
+
// v4.1.3-essentials doctor-polish: Path-A unification — the CLI
|
|
1174
|
+
// path now uses the SAME `renderHealthBox` renderer as `/doctor`
|
|
1175
|
+
// (in-REPL). Previously this emitted a plain `[ok]` / `[fail]`
|
|
1176
|
+
// list with no box, no grouping, and the optional sections
|
|
1177
|
+
// (subsystem health, skill outcomes, liveness) dangled below the
|
|
1178
|
+
// summary. Now everything renders inside one cohesive box.
|
|
1179
|
+
//
|
|
1180
|
+
// NO_COLOR / forceMono handling: the Display instance honors both,
|
|
1181
|
+
// so `aiden doctor | tee report.txt` produces clean plain text
|
|
1182
|
+
// when piped (no ANSI in the captured file).
|
|
1183
|
+
//
|
|
1184
|
+
// Lazy-import Display + SkinEngine here because runDoctorCli is
|
|
1185
|
+
// also called from non-REPL contexts (tests) where instantiating
|
|
1186
|
+
// a Display might not be appropriate. CLI invocations get a fresh
|
|
1187
|
+
// skin engine; tests can mock or inspect the report directly.
|
|
1188
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1189
|
+
const { Display: DisplayCtor } = require('./display');
|
|
1190
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1191
|
+
const { SkinEngine: SkinEngineCtor } = require('./skinEngine');
|
|
1192
|
+
const display = new DisplayCtor({ skin: new SkinEngineCtor() });
|
|
1193
|
+
// Pull in-process surfaces if the caller supplied them. CLI
|
|
1194
|
+
// invocations from a fresh process don't have a live agent so the
|
|
1195
|
+
// helpers return empty arrays → groups dropped by renderer.
|
|
1196
|
+
report.results.push(...subsystemHealthResults(opts?.subsystemHealthRegistry));
|
|
1197
|
+
report.results.push(...skillOutcomeResults(opts?.skillOutcomeTracker));
|
|
1198
|
+
// Provider-liveness path: runs an opt-in network probe per
|
|
1199
|
+
// configured provider when `--providers` is set. Results coerce
|
|
1200
|
+
// to CheckResult shape and fold into the same grouped box.
|
|
807
1201
|
let livenessFailed = false;
|
|
808
1202
|
if (opts?.liveness) {
|
|
809
|
-
|
|
810
|
-
const { runProviderLiveness, renderProviderLivenessSection } = await Promise.resolve().then(() => __importStar(require('./doctorLiveness')));
|
|
1203
|
+
const { runProviderLiveness } = await Promise.resolve().then(() => __importStar(require('./doctorLiveness')));
|
|
811
1204
|
const paths = opts.paths ?? (0, paths_1.resolveAidenPaths)();
|
|
812
|
-
const { results, summary } = await runProviderLiveness({
|
|
1205
|
+
const { results: liveResults, summary } = await runProviderLiveness({
|
|
813
1206
|
paths,
|
|
814
1207
|
env: opts.env,
|
|
815
1208
|
fetchImpl: opts.fetchImpl,
|
|
816
1209
|
timeoutMs: opts.livenessTimeoutMs,
|
|
817
1210
|
});
|
|
818
|
-
|
|
1211
|
+
// Coerce per-provider liveness results into CheckResult rows.
|
|
1212
|
+
// `liveResults` shape varies by provider but always has a
|
|
1213
|
+
// `passed`/`ok` flag and a message. Treat unknown shapes as
|
|
1214
|
+
// pass-through so future probe-result additions don't break.
|
|
1215
|
+
for (const lr of liveResults) {
|
|
1216
|
+
const passed = lr.passed === true || lr.ok === true;
|
|
1217
|
+
const latency = typeof lr.latencyMs === 'number' ? ` (${lr.latencyMs}ms)` : '';
|
|
1218
|
+
const msg = (lr.message ?? lr.status ?? (passed ? 'live' : (lr.error ?? 'failed'))) + latency;
|
|
1219
|
+
report.results.push({
|
|
1220
|
+
name: String(lr.name ?? lr.provider ?? 'provider'),
|
|
1221
|
+
group: 'Provider liveness',
|
|
1222
|
+
passed,
|
|
1223
|
+
message: msg,
|
|
1224
|
+
...(passed ? {} : { suggestion: lr.error ?? 'check API key / network' }),
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
819
1227
|
livenessFailed = summary.red > 0;
|
|
820
1228
|
}
|
|
1229
|
+
process.stdout.write(renderHealthBox(report, display) + '\n');
|
|
1230
|
+
// Phase v4.1.1-oauth-fix Phase 5: discoverability hint for the deep
|
|
1231
|
+
// mode. Outside the box so it reads as meta-guidance, not a check.
|
|
1232
|
+
if (!opts?.liveness) {
|
|
1233
|
+
process.stdout.write('\n hint: Run `aiden doctor --providers` for live provider checks\n');
|
|
1234
|
+
}
|
|
821
1235
|
// Liveness reds count toward the overall exit code so CI / scripts
|
|
822
1236
|
// can `aiden doctor --providers && deploy`.
|
|
823
1237
|
process.exitCode = (report.passed && !livenessFailed) ? 0 : 1;
|