aiden-runtime 4.1.2 → 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/dist/cli/v4/aidenCLI.js +10 -0
- package/dist/cli/v4/callbacks.js +15 -0
- package/dist/cli/v4/chatSession.js +120 -2
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/model.js +30 -1
- 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 +464 -132
- package/dist/cli/v4/doctor.js +377 -75
- package/dist/cli/v4/promotionPrompt.js +135 -5
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +14 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/sessionDistiller.js +48 -1
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/providers/v4/errors.js +92 -0
- package/dist/tools/v4/index.js +24 -1
- package/dist/tools/v4/sessions/recallSession.js +14 -0
- package/dist/tools/v4/system/_psHelpers.js +70 -2
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +136 -10
- package/dist/tools/v4/system/mediaKey.js +35 -4
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/package.json +1 -1
- package/skills/system_control.md +56 -6
package/dist/cli/v4/doctor.js
CHANGED
|
@@ -55,7 +55,11 @@ 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;
|
|
58
60
|
exports.renderSubsystemHealthSection = renderSubsystemHealthSection;
|
|
61
|
+
exports.skillOutcomeResults = skillOutcomeResults;
|
|
62
|
+
exports.sessionCounterResults = sessionCounterResults;
|
|
59
63
|
exports.renderSkillOutcomesSection = renderSkillOutcomesSection;
|
|
60
64
|
exports.resolveBinaryPath = resolveBinaryPath;
|
|
61
65
|
exports._resetBinaryResolutionCacheForTests = _resetBinaryResolutionCacheForTests;
|
|
@@ -86,8 +90,84 @@ const checkUpdate_1 = require("../../core/v4/update/checkUpdate");
|
|
|
86
90
|
const box_1 = require("./box");
|
|
87
91
|
const audioBackend_1 = require("../../core/voice/audioBackend");
|
|
88
92
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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):
|
|
91
171
|
* - registry undefined → render nothing (no live state to report)
|
|
92
172
|
* - all subsystems healthy → one-line green summary
|
|
93
173
|
* - any degradation → expand block with last-error per failed sub
|
|
@@ -144,9 +224,102 @@ function humanAge(ms) {
|
|
|
144
224
|
return `${Math.floor(ms / 86400000)}d`;
|
|
145
225
|
}
|
|
146
226
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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.
|
|
150
323
|
*
|
|
151
324
|
* Output (when not empty): top N skills sorted by load count, with
|
|
152
325
|
* total observations and success percentage. Last-error message
|
|
@@ -328,6 +501,7 @@ async function checkConfigFile(paths) {
|
|
|
328
501
|
await node_fs_1.promises.access(paths.configYaml);
|
|
329
502
|
return {
|
|
330
503
|
name: 'config file',
|
|
504
|
+
group: 'Storage',
|
|
331
505
|
passed: true,
|
|
332
506
|
message: `found at ${paths.configYaml}`,
|
|
333
507
|
durationMs: Date.now() - t0,
|
|
@@ -336,6 +510,7 @@ async function checkConfigFile(paths) {
|
|
|
336
510
|
catch {
|
|
337
511
|
return {
|
|
338
512
|
name: 'config file',
|
|
513
|
+
group: 'Storage',
|
|
339
514
|
passed: false,
|
|
340
515
|
message: `missing at ${paths.configYaml}`,
|
|
341
516
|
suggestion: 'run `aiden setup` to create one',
|
|
@@ -359,6 +534,7 @@ function checkProviderAuth(env) {
|
|
|
359
534
|
if (present.length === 0) {
|
|
360
535
|
return {
|
|
361
536
|
name: 'provider auth',
|
|
537
|
+
group: 'Providers',
|
|
362
538
|
passed: false,
|
|
363
539
|
message: 'no provider API key found in environment',
|
|
364
540
|
suggestion: 'run `aiden setup` and pick a provider, or set ANTHROPIC_API_KEY',
|
|
@@ -367,6 +543,7 @@ function checkProviderAuth(env) {
|
|
|
367
543
|
}
|
|
368
544
|
return {
|
|
369
545
|
name: 'provider auth',
|
|
546
|
+
group: 'Providers',
|
|
370
547
|
passed: true,
|
|
371
548
|
message: `${present.length} provider key(s) present (${present.join(', ')})`,
|
|
372
549
|
durationMs: Date.now() - t0,
|
|
@@ -376,6 +553,7 @@ async function checkOllamaReachable(opts) {
|
|
|
376
553
|
const t0 = Date.now();
|
|
377
554
|
const fallback = {
|
|
378
555
|
name: 'ollama reachable',
|
|
556
|
+
group: 'Inference',
|
|
379
557
|
passed: false,
|
|
380
558
|
message: 'no response from http://localhost:11434',
|
|
381
559
|
suggestion: 'install Ollama from https://ollama.com or skip if you only use cloud providers',
|
|
@@ -393,6 +571,7 @@ async function checkOllamaReachable(opts) {
|
|
|
393
571
|
}
|
|
394
572
|
return {
|
|
395
573
|
name: 'ollama reachable',
|
|
574
|
+
group: 'Inference',
|
|
396
575
|
passed: true,
|
|
397
576
|
message: 'Ollama responding on :11434',
|
|
398
577
|
durationMs: Date.now() - t0,
|
|
@@ -411,6 +590,7 @@ async function checkPythonAvailable(opts) {
|
|
|
411
590
|
const t0 = Date.now();
|
|
412
591
|
const fallback = {
|
|
413
592
|
name: 'python available',
|
|
593
|
+
group: 'System tools',
|
|
414
594
|
passed: false,
|
|
415
595
|
message: 'python not found on PATH',
|
|
416
596
|
suggestion: 'install python 3.10+ — required for graphify and a few skills',
|
|
@@ -423,6 +603,7 @@ async function checkPythonAvailable(opts) {
|
|
|
423
603
|
if (res.ok) {
|
|
424
604
|
return {
|
|
425
605
|
name: 'python available',
|
|
606
|
+
group: 'System tools',
|
|
426
607
|
passed: true,
|
|
427
608
|
message: res.stdout || `${bin} present`,
|
|
428
609
|
durationMs: Date.now() - t0,
|
|
@@ -439,6 +620,7 @@ async function checkDockerAvailable(opts) {
|
|
|
439
620
|
if (res.ok) {
|
|
440
621
|
return {
|
|
441
622
|
name: 'docker available',
|
|
623
|
+
group: 'System tools',
|
|
442
624
|
passed: true,
|
|
443
625
|
message: res.stdout || 'docker present',
|
|
444
626
|
durationMs: Date.now() - t0,
|
|
@@ -446,6 +628,7 @@ async function checkDockerAvailable(opts) {
|
|
|
446
628
|
}
|
|
447
629
|
return {
|
|
448
630
|
name: 'docker available',
|
|
631
|
+
group: 'System tools',
|
|
449
632
|
passed: false,
|
|
450
633
|
message: 'docker not found on PATH',
|
|
451
634
|
suggestion: 'optional — install Docker Desktop if you want sandboxed tool execution',
|
|
@@ -453,6 +636,7 @@ async function checkDockerAvailable(opts) {
|
|
|
453
636
|
};
|
|
454
637
|
})(), opts.timeoutMs, {
|
|
455
638
|
name: 'docker available',
|
|
639
|
+
group: 'System tools',
|
|
456
640
|
passed: false,
|
|
457
641
|
message: 'docker probe timed out',
|
|
458
642
|
durationMs: Date.now() - t0,
|
|
@@ -465,6 +649,7 @@ async function checkNpxAvailable(opts) {
|
|
|
465
649
|
if (res.ok) {
|
|
466
650
|
return {
|
|
467
651
|
name: 'npx available',
|
|
652
|
+
group: 'System tools',
|
|
468
653
|
passed: true,
|
|
469
654
|
message: `npx ${res.stdout}`,
|
|
470
655
|
durationMs: Date.now() - t0,
|
|
@@ -472,6 +657,7 @@ async function checkNpxAvailable(opts) {
|
|
|
472
657
|
}
|
|
473
658
|
return {
|
|
474
659
|
name: 'npx available',
|
|
660
|
+
group: 'System tools',
|
|
475
661
|
passed: false,
|
|
476
662
|
message: 'npx not found on PATH',
|
|
477
663
|
suggestion: 'install Node.js 20+ — required for npm-published MCP servers',
|
|
@@ -479,6 +665,7 @@ async function checkNpxAvailable(opts) {
|
|
|
479
665
|
};
|
|
480
666
|
})(), opts.timeoutMs, {
|
|
481
667
|
name: 'npx available',
|
|
668
|
+
group: 'System tools',
|
|
482
669
|
passed: false,
|
|
483
670
|
message: 'npx probe timed out',
|
|
484
671
|
durationMs: Date.now() - t0,
|
|
@@ -491,6 +678,7 @@ async function checkSkillsDir(paths) {
|
|
|
491
678
|
if (!stat.isDirectory()) {
|
|
492
679
|
return {
|
|
493
680
|
name: 'skills dir',
|
|
681
|
+
group: 'Storage',
|
|
494
682
|
passed: false,
|
|
495
683
|
message: `${paths.skillsDir} is not a directory`,
|
|
496
684
|
durationMs: Date.now() - t0,
|
|
@@ -499,6 +687,7 @@ async function checkSkillsDir(paths) {
|
|
|
499
687
|
const entries = await node_fs_1.promises.readdir(paths.skillsDir);
|
|
500
688
|
return {
|
|
501
689
|
name: 'skills dir',
|
|
690
|
+
group: 'Storage',
|
|
502
691
|
passed: true,
|
|
503
692
|
message: `${paths.skillsDir} (${entries.length} entries)`,
|
|
504
693
|
durationMs: Date.now() - t0,
|
|
@@ -507,6 +696,7 @@ async function checkSkillsDir(paths) {
|
|
|
507
696
|
catch {
|
|
508
697
|
return {
|
|
509
698
|
name: 'skills dir',
|
|
699
|
+
group: 'Storage',
|
|
510
700
|
passed: false,
|
|
511
701
|
message: `missing ${paths.skillsDir}`,
|
|
512
702
|
suggestion: 'run `aiden setup` — it creates the skills directory',
|
|
@@ -520,6 +710,7 @@ async function checkBundledManifest(paths) {
|
|
|
520
710
|
await node_fs_1.promises.access(paths.bundledManifest);
|
|
521
711
|
return {
|
|
522
712
|
name: 'bundled manifest',
|
|
713
|
+
group: 'Storage',
|
|
523
714
|
passed: true,
|
|
524
715
|
message: `present at ${paths.bundledManifest}`,
|
|
525
716
|
durationMs: Date.now() - t0,
|
|
@@ -528,6 +719,7 @@ async function checkBundledManifest(paths) {
|
|
|
528
719
|
catch {
|
|
529
720
|
return {
|
|
530
721
|
name: 'bundled manifest',
|
|
722
|
+
group: 'Storage',
|
|
531
723
|
passed: false,
|
|
532
724
|
message: 'bundled skill manifest missing',
|
|
533
725
|
suggestion: 'reinstall `aiden` — the package was not unpacked correctly',
|
|
@@ -541,6 +733,7 @@ async function checkPlatformPaths(paths) {
|
|
|
541
733
|
await node_fs_1.promises.access(paths.root);
|
|
542
734
|
return {
|
|
543
735
|
name: 'platform paths',
|
|
736
|
+
group: 'Storage',
|
|
544
737
|
passed: true,
|
|
545
738
|
message: `aiden home: ${paths.root}`,
|
|
546
739
|
durationMs: Date.now() - t0,
|
|
@@ -549,6 +742,7 @@ async function checkPlatformPaths(paths) {
|
|
|
549
742
|
catch {
|
|
550
743
|
return {
|
|
551
744
|
name: 'platform paths',
|
|
745
|
+
group: 'Storage',
|
|
552
746
|
passed: false,
|
|
553
747
|
message: `aiden home missing: ${paths.root}`,
|
|
554
748
|
suggestion: 'run `aiden setup` to initialise the home directory',
|
|
@@ -575,6 +769,7 @@ async function checkAudioBackend() {
|
|
|
575
769
|
if (playback && record) {
|
|
576
770
|
return {
|
|
577
771
|
name: 'audio backend',
|
|
772
|
+
group: 'Voice',
|
|
578
773
|
passed: true,
|
|
579
774
|
message: `${process.platform}: playback=${playback.label} · record=${record.label}`,
|
|
580
775
|
durationMs: Date.now() - t0,
|
|
@@ -588,6 +783,7 @@ async function checkAudioBackend() {
|
|
|
588
783
|
missing.push((0, audioBackend_1.missingBackendMessage)('record'));
|
|
589
784
|
return {
|
|
590
785
|
name: 'audio backend',
|
|
786
|
+
group: 'Voice',
|
|
591
787
|
passed: true,
|
|
592
788
|
message: `${process.platform}: voice features will not work — backends missing (known: ${[...new Set([...known.playback, ...known.record])].join(', ') || 'none'})`,
|
|
593
789
|
suggestion: missing.join(' || '),
|
|
@@ -608,6 +804,7 @@ async function checkLicense(opts) {
|
|
|
608
804
|
if (!present) {
|
|
609
805
|
return {
|
|
610
806
|
name: 'license',
|
|
807
|
+
group: 'Updates',
|
|
611
808
|
passed: true,
|
|
612
809
|
message: 'free tier (no license cache)',
|
|
613
810
|
durationMs: Date.now() - t0,
|
|
@@ -621,6 +818,7 @@ async function checkLicense(opts) {
|
|
|
621
818
|
if (status.tier !== 'pro') {
|
|
622
819
|
return {
|
|
623
820
|
name: 'license',
|
|
821
|
+
group: 'Updates',
|
|
624
822
|
passed: true,
|
|
625
823
|
message: 'license cache present but not currently valid (free tier)',
|
|
626
824
|
suggestion: 'run /license refresh to re-verify against server',
|
|
@@ -630,12 +828,14 @@ async function checkLicense(opts) {
|
|
|
630
828
|
const expiry = status.cache.expiresAt || 'lifetime';
|
|
631
829
|
return {
|
|
632
830
|
name: 'license',
|
|
831
|
+
group: 'Updates',
|
|
633
832
|
passed: true,
|
|
634
833
|
message: `Pro (${status.cache.plan}, expires ${expiry})`,
|
|
635
834
|
durationMs: Date.now() - t0,
|
|
636
835
|
};
|
|
637
836
|
})(), opts.timeoutMs, {
|
|
638
837
|
name: 'license',
|
|
838
|
+
group: 'Updates',
|
|
639
839
|
passed: false,
|
|
640
840
|
message: 'license check timed out reading cache',
|
|
641
841
|
durationMs: Date.now() - t0,
|
|
@@ -644,6 +844,7 @@ async function checkLicense(opts) {
|
|
|
644
844
|
catch (err) {
|
|
645
845
|
return {
|
|
646
846
|
name: 'license',
|
|
847
|
+
group: 'Updates',
|
|
647
848
|
passed: false,
|
|
648
849
|
message: `license check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
649
850
|
suggestion: 'run /license refresh; if persistent, re-activate with /license activate <key>',
|
|
@@ -668,6 +869,7 @@ async function checkUpdate(opts) {
|
|
|
668
869
|
const where = status.fromCache ? 'cached' : 'live';
|
|
669
870
|
return {
|
|
670
871
|
name: 'npm update',
|
|
872
|
+
group: 'Updates',
|
|
671
873
|
passed: true,
|
|
672
874
|
message: `installed v${status.installed} is up to date (${where})`,
|
|
673
875
|
durationMs: Date.now() - t0,
|
|
@@ -675,6 +877,7 @@ async function checkUpdate(opts) {
|
|
|
675
877
|
}
|
|
676
878
|
return {
|
|
677
879
|
name: 'npm update',
|
|
880
|
+
group: 'Updates',
|
|
678
881
|
passed: true,
|
|
679
882
|
message: `v${status.latest} available (installed: v${status.installed})`,
|
|
680
883
|
suggestion: 'run `npm install -g aiden-runtime@latest`',
|
|
@@ -684,6 +887,7 @@ async function checkUpdate(opts) {
|
|
|
684
887
|
catch (err) {
|
|
685
888
|
return {
|
|
686
889
|
name: 'npm update',
|
|
890
|
+
group: 'Updates',
|
|
687
891
|
passed: false,
|
|
688
892
|
message: `update check error: ${err instanceof Error ? err.message : String(err)}`,
|
|
689
893
|
durationMs: Date.now() - t0,
|
|
@@ -691,6 +895,7 @@ async function checkUpdate(opts) {
|
|
|
691
895
|
}
|
|
692
896
|
})(), opts.timeoutMs, {
|
|
693
897
|
name: 'npm update',
|
|
898
|
+
group: 'Updates',
|
|
694
899
|
passed: true,
|
|
695
900
|
message: 'update check timed out (network slow — non-fatal)',
|
|
696
901
|
durationMs: Date.now() - t0,
|
|
@@ -705,6 +910,7 @@ async function checkLogsWritable(paths) {
|
|
|
705
910
|
await node_fs_1.promises.unlink(probe);
|
|
706
911
|
return {
|
|
707
912
|
name: 'logs writable',
|
|
913
|
+
group: 'Storage',
|
|
708
914
|
passed: true,
|
|
709
915
|
message: paths.logsDir,
|
|
710
916
|
durationMs: Date.now() - t0,
|
|
@@ -713,6 +919,7 @@ async function checkLogsWritable(paths) {
|
|
|
713
919
|
catch (err) {
|
|
714
920
|
return {
|
|
715
921
|
name: 'logs writable',
|
|
922
|
+
group: 'Storage',
|
|
716
923
|
passed: false,
|
|
717
924
|
message: `cannot write to ${paths.logsDir}: ${err instanceof Error ? err.message : String(err)}`,
|
|
718
925
|
suggestion: `check permissions on ${node_os_1.default.homedir()}`,
|
|
@@ -802,21 +1009,56 @@ function checkIconKind(r) {
|
|
|
802
1009
|
return { icon: '⚠', colour: 'warn' };
|
|
803
1010
|
return { icon: '✓', colour: 'success' };
|
|
804
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
|
+
}
|
|
805
1029
|
function maxNameWidth(report) {
|
|
806
1030
|
return report.results.reduce((m, r) => Math.max(m, r.name.length), 0);
|
|
807
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
|
+
}
|
|
808
1054
|
/**
|
|
809
1055
|
* Compute the inner-cell width for the health box: widest visible
|
|
810
|
-
* content row across all check rows
|
|
811
|
-
*
|
|
812
|
-
* HEALTH_BOX_MIN/MAX_WIDTH. Title length also
|
|
813
|
-
* `╭── 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.
|
|
814
1060
|
*/
|
|
815
1061
|
function computeHealthBoxWidth(report, nameWidth) {
|
|
816
|
-
// The minimum width the title needs (`╭── <title> ──╮` shape):
|
|
817
|
-
// 2 corners + 2 leading dashes + 1 space + title + 1 space + at
|
|
818
|
-
// least 2 trailing dashes, minus the 2 corners since the cell is
|
|
819
|
-
// measured between them.
|
|
820
1062
|
const titleMin = 2 + 1 + HEALTH_BOX_TITLE.length + 1 + 2;
|
|
821
1063
|
let widest = titleMin;
|
|
822
1064
|
const measureRow = (row) => {
|
|
@@ -824,25 +1066,41 @@ function computeHealthBoxWidth(report, nameWidth) {
|
|
|
824
1066
|
if (v + 1 > widest)
|
|
825
1067
|
widest = v + 1; // +1 for trailing gutter
|
|
826
1068
|
};
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
}
|
|
831
1084
|
}
|
|
832
1085
|
}
|
|
833
|
-
const passedCount = report.results.filter((x) => x.passed).length;
|
|
834
|
-
measureRow(` ${passedCount} of ${report.results.length} checks passed in ${report.totalMs} ms`);
|
|
835
1086
|
return Math.max(HEALTH_BOX_MIN_WIDTH, Math.min(HEALTH_BOX_MAX_WIDTH, widest));
|
|
836
1087
|
}
|
|
837
1088
|
/**
|
|
838
|
-
* Render the report as an orange-bordered rounded box
|
|
839
|
-
*
|
|
840
|
-
*
|
|
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.
|
|
841
1100
|
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* 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.
|
|
846
1104
|
*/
|
|
847
1105
|
function renderHealthBox(report, display) {
|
|
848
1106
|
const nameWidth = maxNameWidth(report);
|
|
@@ -858,23 +1116,50 @@ function renderHealthBox(report, display) {
|
|
|
858
1116
|
return `${display.brand(left)}${inner}${display.brand(right)}`;
|
|
859
1117
|
};
|
|
860
1118
|
const lines = [top, side('')];
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
}
|
|
871
1157
|
}
|
|
1158
|
+
// Blank line between groups (not after the last one).
|
|
1159
|
+
if (gi < groups.length - 1)
|
|
1160
|
+
lines.push(side(''));
|
|
872
1161
|
}
|
|
873
1162
|
lines.push(side(''));
|
|
874
|
-
const passedCount = report.results.filter((r) => r.passed).length;
|
|
875
|
-
const summary = `${passedCount} of ${report.results.length} checks passed in ${report.totalMs} ms`;
|
|
876
|
-
const summaryColour = report.passed ? 'success' : 'warn';
|
|
877
|
-
lines.push(side(' ' + display.paint(summary, summaryColour)));
|
|
878
1163
|
lines.push(bot);
|
|
879
1164
|
return lines.join('\n');
|
|
880
1165
|
}
|
|
@@ -885,51 +1170,68 @@ function renderHealthBox(report, display) {
|
|
|
885
1170
|
*/
|
|
886
1171
|
async function runDoctorCli(opts) {
|
|
887
1172
|
const report = await runDoctor(opts);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
//
|
|
906
|
-
|
|
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.
|
|
907
1201
|
let livenessFailed = false;
|
|
908
1202
|
if (opts?.liveness) {
|
|
909
|
-
|
|
910
|
-
const { runProviderLiveness, renderProviderLivenessSection } = await Promise.resolve().then(() => __importStar(require('./doctorLiveness')));
|
|
1203
|
+
const { runProviderLiveness } = await Promise.resolve().then(() => __importStar(require('./doctorLiveness')));
|
|
911
1204
|
const paths = opts.paths ?? (0, paths_1.resolveAidenPaths)();
|
|
912
|
-
const { results, summary } = await runProviderLiveness({
|
|
1205
|
+
const { results: liveResults, summary } = await runProviderLiveness({
|
|
913
1206
|
paths,
|
|
914
1207
|
env: opts.env,
|
|
915
1208
|
fetchImpl: opts.fetchImpl,
|
|
916
1209
|
timeoutMs: opts.livenessTimeoutMs,
|
|
917
1210
|
});
|
|
918
|
-
|
|
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
|
+
}
|
|
919
1227
|
livenessFailed = summary.red > 0;
|
|
920
1228
|
}
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
// Phase v4.1.2-slice4: skill-outcome surface. Same gating — only
|
|
928
|
-
// renders when a tracker was passed and has at least one observed
|
|
929
|
-
// skill. Standalone CLI invocations skip it.
|
|
930
|
-
const outcomesBlock = renderSkillOutcomesSection(opts?.skillOutcomeTracker);
|
|
931
|
-
if (outcomesBlock)
|
|
932
|
-
process.stdout.write(outcomesBlock);
|
|
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
|
+
}
|
|
933
1235
|
// Liveness reds count toward the overall exit code so CI / scripts
|
|
934
1236
|
// can `aiden doctor --providers && deploy`.
|
|
935
1237
|
process.exitCode = (report.passed && !livenessFailed) ? 0 : 1;
|