@ouro.bot/cli 0.1.0-alpha.301 → 0.1.0-alpha.303
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/changelog.json +8 -2
- package/dist/heart/daemon/cli-exec.js +43 -1
- package/dist/heart/daemon/cli-help.js +306 -0
- package/dist/heart/daemon/cli-parse.js +15 -1
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +322 -0
- package/dist/nerves/coverage/file-completeness.js +2 -1
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
4
|
{
|
|
5
|
-
"version": "0.1.0-alpha.
|
|
5
|
+
"version": "0.1.0-alpha.303",
|
|
6
6
|
"changes": [
|
|
7
|
-
"
|
|
7
|
+
"feat(cli): overhaul CLI help and error handling. `ouro --help` now shows commands grouped into 9 categories (Lifecycle, Agents, Chat, Tasks, Habits, Friends, Auth, Internal, System) instead of a flat wall of text. `ouro help <command>` and `ouro <command> --help` show per-command help with description, usage syntax, subcommands, and an example. Unknown commands get Levenshtein-based typo suggestions when distance <= 2 (e.g., `ouro stauts` -> `Did you mean 'status'?`). `ouro up` now prints progress messages: 'Checking for updates...' -> 'Installing <version>...' or 'Up to date.' -> 'Starting daemon...'. New command metadata registry in `src/heart/daemon/cli-help.ts` covers 32 commands. 50+ new tests at 100% coverage on all new code."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.302",
|
|
12
|
+
"changes": [
|
|
13
|
+
"feat(cli): add `ouro doctor` system health check command. New command runs 6 diagnostic categories — daemon (socket existence + responsiveness), agents (bundle discovery, agent.json validation for version/humanFacing/agentFacing/enabled), senses (BlueBubbles and Teams config presence and well-formedness), habits (launchd plist discovery, degraded state), security (secrets.json permissions, credential leak detection in agent.json), and disk (log size thresholds at 100MB warn / 500MB critical, bundle root existence). Output is a colored checklist with per-category grouping and a summary line. Works without daemon running — daemon checks fail gracefully while all other categories still execute, making it useful for cold diagnostics. 3 new files (doctor.ts, doctor-types.ts, cli-render-doctor.ts), 3 modified (cli-types.ts, cli-parse.ts, cli-exec.ts), 4 test files with 61 tests at 100% coverage on new code."
|
|
8
14
|
]
|
|
9
15
|
},
|
|
10
16
|
{
|
|
@@ -66,8 +66,11 @@ const auth_flow_1 = require("../auth/auth-flow");
|
|
|
66
66
|
const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
|
|
67
67
|
const cli_parse_1 = require("./cli-parse");
|
|
68
68
|
const cli_parse_2 = require("./cli-parse");
|
|
69
|
+
const cli_help_1 = require("./cli-help");
|
|
69
70
|
const cli_render_1 = require("./cli-render");
|
|
70
71
|
const cli_defaults_1 = require("./cli-defaults");
|
|
72
|
+
const doctor_1 = require("./doctor");
|
|
73
|
+
const cli_render_doctor_1 = require("./cli-render-doctor");
|
|
71
74
|
// ── ensureDaemonRunning ──
|
|
72
75
|
async function ensureDaemonRunning(deps) {
|
|
73
76
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
@@ -645,7 +648,7 @@ function resolveClonePath(options, checkExists, deps) {
|
|
|
645
648
|
// ── Main CLI execution ──
|
|
646
649
|
async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDeps)()) {
|
|
647
650
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
648
|
-
const text = (0,
|
|
651
|
+
const text = (0, cli_help_1.getGroupedHelp)();
|
|
649
652
|
deps.writeStdout(text);
|
|
650
653
|
return text;
|
|
651
654
|
}
|
|
@@ -723,6 +726,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
723
726
|
message: "ouro CLI command invoked",
|
|
724
727
|
meta: { kind: command.kind },
|
|
725
728
|
});
|
|
729
|
+
if (command.kind === "help") {
|
|
730
|
+
const text = command.command
|
|
731
|
+
? ((0, cli_help_1.getCommandHelp)(command.command) ?? `Unknown command: ${command.command}\n\n${(0, cli_help_1.getGroupedHelp)()}`)
|
|
732
|
+
: (0, cli_help_1.getGroupedHelp)();
|
|
733
|
+
deps.writeStdout(text);
|
|
734
|
+
return text;
|
|
735
|
+
}
|
|
726
736
|
if (command.kind === "bluebubbles.replay") {
|
|
727
737
|
const { replayBlueBubblesMessage, formatBlueBubblesReplayText } = await Promise.resolve().then(() => __importStar(require("../../senses/bluebubbles/replay")));
|
|
728
738
|
const replay = await replayBlueBubblesMessage({
|
|
@@ -771,10 +781,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
771
781
|
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
772
782
|
// ── versioned CLI update check ──
|
|
773
783
|
if (deps.checkForCliUpdate) {
|
|
784
|
+
deps.writeStdout("checking for updates...");
|
|
774
785
|
let pendingReExec = false;
|
|
775
786
|
try {
|
|
776
787
|
const updateResult = await deps.checkForCliUpdate();
|
|
777
788
|
if (updateResult.available && updateResult.latestVersion) {
|
|
789
|
+
deps.writeStdout(`installing ${updateResult.latestVersion}...`);
|
|
778
790
|
/* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
|
|
779
791
|
const currentVersion = linkedVersionBeforeUp ?? "unknown";
|
|
780
792
|
await deps.installCliVersion(updateResult.latestVersion);
|
|
@@ -802,6 +814,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
802
814
|
if (pendingReExec) {
|
|
803
815
|
deps.reExecFromNewVersion(args);
|
|
804
816
|
}
|
|
817
|
+
else {
|
|
818
|
+
deps.writeStdout("up to date.");
|
|
819
|
+
}
|
|
805
820
|
}
|
|
806
821
|
await performSystemSetup(deps);
|
|
807
822
|
// Track whether we've already printed the "ouro updated to" message
|
|
@@ -871,6 +886,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
871
886
|
const count = agents.length;
|
|
872
887
|
deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
|
|
873
888
|
}
|
|
889
|
+
deps.writeStdout("starting daemon...");
|
|
874
890
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
875
891
|
deps.writeStdout(daemonResult.message);
|
|
876
892
|
// Persist boot startup AFTER daemon is running — bootstrap is safe now
|
|
@@ -1912,6 +1928,32 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
1912
1928
|
deps.writeStdout(message);
|
|
1913
1929
|
return message;
|
|
1914
1930
|
}
|
|
1931
|
+
// ── doctor (local, no daemon socket needed) ──
|
|
1932
|
+
if (command.kind === "doctor") {
|
|
1933
|
+
const doctorDeps = {
|
|
1934
|
+
/* v8 ignore start -- thin fs wrappers tested via doctor.test.ts with injected deps @preserve */
|
|
1935
|
+
existsSync: (p) => fs.existsSync(p),
|
|
1936
|
+
readFileSync: (p) => fs.readFileSync(p, "utf-8"),
|
|
1937
|
+
readdirSync: (p) => fs.readdirSync(p),
|
|
1938
|
+
statSync: (p) => fs.statSync(p),
|
|
1939
|
+
/* v8 ignore stop */
|
|
1940
|
+
checkSocketAlive: deps.checkSocketAlive,
|
|
1941
|
+
socketPath: deps.socketPath,
|
|
1942
|
+
bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
|
|
1943
|
+
secretsRoot: deps.secretsRoot ?? path.join(os.homedir(), ".agentsecrets"),
|
|
1944
|
+
homedir: os.homedir(),
|
|
1945
|
+
};
|
|
1946
|
+
const doctorResult = await (0, doctor_1.runDoctorChecks)(doctorDeps);
|
|
1947
|
+
const output = (0, cli_render_doctor_1.formatDoctorOutput)(doctorResult);
|
|
1948
|
+
deps.writeStdout(output);
|
|
1949
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1950
|
+
component: "daemon",
|
|
1951
|
+
event: "daemon.doctor_run",
|
|
1952
|
+
message: "ouro doctor completed",
|
|
1953
|
+
meta: { passed: doctorResult.summary.passed, warnings: doctorResult.summary.warnings, failed: doctorResult.summary.failed },
|
|
1954
|
+
});
|
|
1955
|
+
return output;
|
|
1956
|
+
}
|
|
1915
1957
|
const daemonCommand = toDaemonCommand(command);
|
|
1916
1958
|
let response;
|
|
1917
1959
|
try {
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI help system — command registry, grouped help output, and per-command help.
|
|
4
|
+
*
|
|
5
|
+
* Pure data + formatting: no side effects, no daemon communication.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.COMMAND_REGISTRY = void 0;
|
|
9
|
+
exports.levenshteinDistance = levenshteinDistance;
|
|
10
|
+
exports.suggestCommand = suggestCommand;
|
|
11
|
+
exports.getGroupedHelp = getGroupedHelp;
|
|
12
|
+
exports.getCommandHelp = getCommandHelp;
|
|
13
|
+
// ── Registry ──
|
|
14
|
+
exports.COMMAND_REGISTRY = {
|
|
15
|
+
up: {
|
|
16
|
+
category: "Lifecycle",
|
|
17
|
+
description: "Start the ouro daemon (default command)",
|
|
18
|
+
usage: "ouro [up]",
|
|
19
|
+
example: "ouro up",
|
|
20
|
+
},
|
|
21
|
+
stop: {
|
|
22
|
+
category: "Lifecycle",
|
|
23
|
+
description: "Stop the running daemon",
|
|
24
|
+
usage: "ouro stop",
|
|
25
|
+
example: "ouro stop",
|
|
26
|
+
},
|
|
27
|
+
down: {
|
|
28
|
+
category: "Lifecycle",
|
|
29
|
+
description: "Stop the running daemon (alias for stop)",
|
|
30
|
+
usage: "ouro down",
|
|
31
|
+
example: "ouro down",
|
|
32
|
+
},
|
|
33
|
+
status: {
|
|
34
|
+
category: "Lifecycle",
|
|
35
|
+
description: "Show daemon status",
|
|
36
|
+
usage: "ouro status",
|
|
37
|
+
example: "ouro status",
|
|
38
|
+
},
|
|
39
|
+
logs: {
|
|
40
|
+
category: "Lifecycle",
|
|
41
|
+
description: "View or prune daemon logs",
|
|
42
|
+
usage: "ouro logs [prune]",
|
|
43
|
+
example: "ouro logs",
|
|
44
|
+
subcommands: ["prune"],
|
|
45
|
+
},
|
|
46
|
+
dev: {
|
|
47
|
+
category: "Lifecycle",
|
|
48
|
+
description: "Start daemon in development mode from a local repo",
|
|
49
|
+
usage: "ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
|
|
50
|
+
example: "ouro dev --repo-path ~/code/ouroboros",
|
|
51
|
+
},
|
|
52
|
+
hatch: {
|
|
53
|
+
category: "Lifecycle",
|
|
54
|
+
description: "Create a new agent",
|
|
55
|
+
usage: "ouro hatch [--agent <name>] [--human <name>] [--provider <provider>]",
|
|
56
|
+
example: "ouro hatch --agent Sprout --human Ari --provider anthropic",
|
|
57
|
+
},
|
|
58
|
+
rollback: {
|
|
59
|
+
category: "Lifecycle",
|
|
60
|
+
description: "Roll back to a previous CLI version",
|
|
61
|
+
usage: "ouro rollback [<version>]",
|
|
62
|
+
example: "ouro rollback 0.1.0-alpha.250",
|
|
63
|
+
},
|
|
64
|
+
versions: {
|
|
65
|
+
category: "Lifecycle",
|
|
66
|
+
description: "List installed CLI versions",
|
|
67
|
+
usage: "ouro versions",
|
|
68
|
+
example: "ouro versions",
|
|
69
|
+
},
|
|
70
|
+
doctor: {
|
|
71
|
+
category: "Lifecycle",
|
|
72
|
+
description: "Run diagnostic checks on the ouro installation",
|
|
73
|
+
usage: "ouro doctor",
|
|
74
|
+
example: "ouro doctor",
|
|
75
|
+
},
|
|
76
|
+
outlook: {
|
|
77
|
+
category: "Agents",
|
|
78
|
+
description: "Show the agent's current outlook",
|
|
79
|
+
usage: "ouro outlook [--json]",
|
|
80
|
+
example: "ouro outlook --json",
|
|
81
|
+
},
|
|
82
|
+
whoami: {
|
|
83
|
+
category: "Agents",
|
|
84
|
+
description: "Show current agent identity info",
|
|
85
|
+
usage: "ouro whoami [--agent <name>]",
|
|
86
|
+
example: "ouro whoami",
|
|
87
|
+
},
|
|
88
|
+
config: {
|
|
89
|
+
category: "Agents",
|
|
90
|
+
description: "View or change agent configuration",
|
|
91
|
+
usage: "ouro config <subcommand> --agent <name>",
|
|
92
|
+
example: "ouro config models --agent ouroboros",
|
|
93
|
+
subcommands: ["model", "models"],
|
|
94
|
+
},
|
|
95
|
+
changelog: {
|
|
96
|
+
category: "Agents",
|
|
97
|
+
description: "View the agent changelog",
|
|
98
|
+
usage: "ouro changelog [--from <version>] [--agent <name>]",
|
|
99
|
+
example: "ouro changelog --from 0.1.0-alpha.250",
|
|
100
|
+
},
|
|
101
|
+
chat: {
|
|
102
|
+
category: "Chat",
|
|
103
|
+
description: "Open an interactive chat session with an agent",
|
|
104
|
+
usage: "ouro chat <agent>",
|
|
105
|
+
example: "ouro chat ouroboros",
|
|
106
|
+
},
|
|
107
|
+
msg: {
|
|
108
|
+
category: "Chat",
|
|
109
|
+
description: "Send a single message to an agent",
|
|
110
|
+
usage: "ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
|
|
111
|
+
example: "ouro msg --to ouroboros hello there",
|
|
112
|
+
},
|
|
113
|
+
task: {
|
|
114
|
+
category: "Tasks",
|
|
115
|
+
description: "Manage agent tasks",
|
|
116
|
+
usage: "ouro task <subcommand> [--agent <name>]",
|
|
117
|
+
example: "ouro task board",
|
|
118
|
+
subcommands: ["board", "create", "update", "show", "actionable", "deps", "sessions", "fix"],
|
|
119
|
+
},
|
|
120
|
+
poke: {
|
|
121
|
+
category: "Tasks",
|
|
122
|
+
description: "Poke an agent about a task or habit",
|
|
123
|
+
usage: "ouro poke <agent> --task <task-id> | --habit <name>",
|
|
124
|
+
example: "ouro poke ouroboros --task abc123",
|
|
125
|
+
},
|
|
126
|
+
reminder: {
|
|
127
|
+
category: "Tasks",
|
|
128
|
+
description: "Create reminders for an agent",
|
|
129
|
+
usage: "ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>]",
|
|
130
|
+
example: "ouro reminder create standup --body 'daily standup' --cadence '1d'",
|
|
131
|
+
subcommands: ["create"],
|
|
132
|
+
},
|
|
133
|
+
habit: {
|
|
134
|
+
category: "Habits",
|
|
135
|
+
description: "Manage agent habits",
|
|
136
|
+
usage: "ouro habit <subcommand> [--agent <name>]",
|
|
137
|
+
example: "ouro habit list",
|
|
138
|
+
subcommands: ["list", "create", "poke"],
|
|
139
|
+
},
|
|
140
|
+
friend: {
|
|
141
|
+
category: "Friends",
|
|
142
|
+
description: "Manage agent friends and identity links",
|
|
143
|
+
usage: "ouro friend <subcommand> [--agent <name>]",
|
|
144
|
+
example: "ouro friend list",
|
|
145
|
+
subcommands: ["list", "show", "create", "update", "link", "unlink"],
|
|
146
|
+
},
|
|
147
|
+
link: {
|
|
148
|
+
category: "Friends",
|
|
149
|
+
description: "Link an external identity to an agent friend",
|
|
150
|
+
usage: "ouro link <agent> --friend <id> --provider <provider> --external-id <eid>",
|
|
151
|
+
example: "ouro link ouroboros --friend f1 --provider aad --external-id user@example.com",
|
|
152
|
+
},
|
|
153
|
+
auth: {
|
|
154
|
+
category: "Auth",
|
|
155
|
+
description: "Set up, verify, or switch agent credentials",
|
|
156
|
+
usage: "ouro auth [verify|switch] --agent <name> [--provider <provider>]",
|
|
157
|
+
example: "ouro auth --agent ouroboros",
|
|
158
|
+
subcommands: ["verify", "switch"],
|
|
159
|
+
},
|
|
160
|
+
thoughts: {
|
|
161
|
+
category: "Internal",
|
|
162
|
+
description: "View agent inner dialog thoughts",
|
|
163
|
+
usage: "ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
|
|
164
|
+
example: "ouro thoughts --last 5 --follow",
|
|
165
|
+
},
|
|
166
|
+
inner: {
|
|
167
|
+
category: "Internal",
|
|
168
|
+
description: "Show inner dialog status",
|
|
169
|
+
usage: "ouro inner [--agent <name>]",
|
|
170
|
+
example: "ouro inner",
|
|
171
|
+
},
|
|
172
|
+
attention: {
|
|
173
|
+
category: "Internal",
|
|
174
|
+
description: "View agent attention items",
|
|
175
|
+
usage: "ouro attention [show <id>|history] [--agent <name>]",
|
|
176
|
+
example: "ouro attention history",
|
|
177
|
+
subcommands: ["show", "history"],
|
|
178
|
+
},
|
|
179
|
+
session: {
|
|
180
|
+
category: "Internal",
|
|
181
|
+
description: "Manage agent sessions",
|
|
182
|
+
usage: "ouro session list [--agent <name>]",
|
|
183
|
+
example: "ouro session list",
|
|
184
|
+
subcommands: ["list"],
|
|
185
|
+
},
|
|
186
|
+
mcp: {
|
|
187
|
+
category: "System",
|
|
188
|
+
description: "Interact with MCP servers",
|
|
189
|
+
usage: "ouro mcp <subcommand>",
|
|
190
|
+
example: "ouro mcp list",
|
|
191
|
+
subcommands: ["list", "call"],
|
|
192
|
+
},
|
|
193
|
+
"mcp-serve": {
|
|
194
|
+
category: "System",
|
|
195
|
+
description: "Start an MCP server for dev tool integration",
|
|
196
|
+
usage: "ouro mcp-serve --agent <name> [--friend <id>]",
|
|
197
|
+
example: "ouro mcp-serve --agent ouroboros",
|
|
198
|
+
},
|
|
199
|
+
setup: {
|
|
200
|
+
category: "System",
|
|
201
|
+
description: "Register MCP server and hooks for a dev tool",
|
|
202
|
+
usage: "ouro setup --tool <claude-code|codex> --agent <name>",
|
|
203
|
+
example: "ouro setup --tool claude-code --agent ouroboros",
|
|
204
|
+
},
|
|
205
|
+
hook: {
|
|
206
|
+
category: "System",
|
|
207
|
+
description: "Fire a dev tool lifecycle hook",
|
|
208
|
+
usage: "ouro hook <event> --agent <name>",
|
|
209
|
+
example: "ouro hook session-start --agent ouroboros",
|
|
210
|
+
},
|
|
211
|
+
bluebubbles: {
|
|
212
|
+
category: "System",
|
|
213
|
+
description: "BlueBubbles integration commands",
|
|
214
|
+
usage: "ouro bluebubbles replay --agent <name> --message-guid <guid> [--event-type <type>] [--json]",
|
|
215
|
+
example: "ouro bluebubbles replay --agent ouroboros --message-guid abc123",
|
|
216
|
+
subcommands: ["replay"],
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
// ── Levenshtein distance ──
|
|
220
|
+
function levenshteinDistance(a, b) {
|
|
221
|
+
if (a.length === 0)
|
|
222
|
+
return b.length;
|
|
223
|
+
if (b.length === 0)
|
|
224
|
+
return a.length;
|
|
225
|
+
const matrix = [];
|
|
226
|
+
for (let i = 0; i <= a.length; i++) {
|
|
227
|
+
matrix[i] = [i];
|
|
228
|
+
}
|
|
229
|
+
for (let j = 0; j <= b.length; j++) {
|
|
230
|
+
matrix[0][j] = j;
|
|
231
|
+
}
|
|
232
|
+
for (let i = 1; i <= a.length; i++) {
|
|
233
|
+
for (let j = 1; j <= b.length; j++) {
|
|
234
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
235
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
236
|
+
matrix[i][j - 1] + 1, // insertion
|
|
237
|
+
matrix[i - 1][j - 1] + cost);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return matrix[a.length][b.length];
|
|
241
|
+
}
|
|
242
|
+
// ── Command suggestion ──
|
|
243
|
+
function suggestCommand(input) {
|
|
244
|
+
if (!input)
|
|
245
|
+
return null;
|
|
246
|
+
let bestMatch = null;
|
|
247
|
+
let bestDistance = Infinity;
|
|
248
|
+
for (const name of Object.keys(exports.COMMAND_REGISTRY)) {
|
|
249
|
+
const distance = levenshteinDistance(input, name);
|
|
250
|
+
if (distance < bestDistance) {
|
|
251
|
+
bestDistance = distance;
|
|
252
|
+
bestMatch = name;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return bestDistance <= 2 ? bestMatch : null;
|
|
256
|
+
}
|
|
257
|
+
// ── Category display order ──
|
|
258
|
+
const CATEGORY_ORDER = [
|
|
259
|
+
"Lifecycle",
|
|
260
|
+
"Agents",
|
|
261
|
+
"Chat",
|
|
262
|
+
"Tasks",
|
|
263
|
+
"Habits",
|
|
264
|
+
"Friends",
|
|
265
|
+
"Auth",
|
|
266
|
+
"Internal",
|
|
267
|
+
"System",
|
|
268
|
+
];
|
|
269
|
+
// ── Grouped help output ──
|
|
270
|
+
function getGroupedHelp() {
|
|
271
|
+
const lines = ["Usage: ouro <command> [options]", ""];
|
|
272
|
+
for (const category of CATEGORY_ORDER) {
|
|
273
|
+
lines.push(` ${category}:`);
|
|
274
|
+
for (const [name, entry] of Object.entries(exports.COMMAND_REGISTRY)) {
|
|
275
|
+
if (entry.category === category) {
|
|
276
|
+
lines.push(` ${name.padEnd(16)} ${entry.description}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
lines.push("");
|
|
280
|
+
}
|
|
281
|
+
lines.push("Run 'ouro help <command>' for details on a specific command.");
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
// ── Per-command help ──
|
|
285
|
+
function getCommandHelp(name) {
|
|
286
|
+
const entry = exports.COMMAND_REGISTRY[name];
|
|
287
|
+
if (!entry)
|
|
288
|
+
return null;
|
|
289
|
+
const lines = [
|
|
290
|
+
`${name} - ${entry.description}`,
|
|
291
|
+
"",
|
|
292
|
+
`Usage: ${entry.usage}`,
|
|
293
|
+
];
|
|
294
|
+
if (entry.subcommands && entry.subcommands.length > 0) {
|
|
295
|
+
lines.push("");
|
|
296
|
+
lines.push("Subcommands:");
|
|
297
|
+
for (const sub of entry.subcommands) {
|
|
298
|
+
lines.push(` ${sub}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (entry.example) {
|
|
302
|
+
lines.push("");
|
|
303
|
+
lines.push(`Example: ${entry.example}`);
|
|
304
|
+
}
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
@@ -13,6 +13,7 @@ exports.usage = usage;
|
|
|
13
13
|
exports.parseMcpServeCommand = parseMcpServeCommand;
|
|
14
14
|
exports.parseOuroCommand = parseOuroCommand;
|
|
15
15
|
const types_1 = require("../../mind/friends/types");
|
|
16
|
+
const cli_help_1 = require("./cli-help");
|
|
16
17
|
// ── Shared helpers ──
|
|
17
18
|
function extractAgentFlag(args) {
|
|
18
19
|
const idx = args.indexOf("--agent");
|
|
@@ -78,6 +79,7 @@ function usage() {
|
|
|
78
79
|
" ouro mcp call <server> <tool> [--args '{...}']",
|
|
79
80
|
" ouro rollback [<version>]",
|
|
80
81
|
" ouro versions",
|
|
82
|
+
" ouro doctor",
|
|
81
83
|
].join("\n");
|
|
82
84
|
}
|
|
83
85
|
// ── Per-group parsers ──
|
|
@@ -685,6 +687,14 @@ function parseOuroCommand(args) {
|
|
|
685
687
|
const [head, second] = args;
|
|
686
688
|
if (!head)
|
|
687
689
|
return { kind: "daemon.up" };
|
|
690
|
+
// ── help command ──
|
|
691
|
+
if (head === "help") {
|
|
692
|
+
return second ? { kind: "help", command: second } : { kind: "help" };
|
|
693
|
+
}
|
|
694
|
+
// ── per-command --help ──
|
|
695
|
+
if (args.includes("--help")) {
|
|
696
|
+
return { kind: "help", command: head };
|
|
697
|
+
}
|
|
688
698
|
if (head === "--agent" && second) {
|
|
689
699
|
return parseOuroCommand(args.slice(2));
|
|
690
700
|
}
|
|
@@ -802,7 +812,11 @@ function parseOuroCommand(args) {
|
|
|
802
812
|
return parseMcpServeCommand(args.slice(1));
|
|
803
813
|
if (head === "setup")
|
|
804
814
|
return parseSetupCommand(args.slice(1));
|
|
815
|
+
if (head === "doctor")
|
|
816
|
+
return { kind: "doctor" };
|
|
805
817
|
if (head === "bluebubbles")
|
|
806
818
|
return parseBlueBubblesCommand(args.slice(1));
|
|
807
|
-
|
|
819
|
+
const suggestion = (0, cli_help_1.suggestCommand)(head);
|
|
820
|
+
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
821
|
+
throw new Error(`Unknown command '${args.join(" ")}'.${hint}\n${usage()}`);
|
|
808
822
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rendering for `ouro doctor` output.
|
|
4
|
+
*
|
|
5
|
+
* Pure function: takes a DoctorResult and returns a formatted string
|
|
6
|
+
* with ANSI colors, category grouping, and a summary line.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.formatDoctorOutput = formatDoctorOutput;
|
|
10
|
+
// ── ANSI color helpers ──
|
|
11
|
+
const RESET = "\x1b[0m";
|
|
12
|
+
const BOLD = "\x1b[1m";
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const GREEN = "\x1b[38;2;46;204;64m";
|
|
15
|
+
const RED = "\x1b[38;2;231;76;60m";
|
|
16
|
+
const YELLOW = "\x1b[38;2;230;190;50m";
|
|
17
|
+
const TEAL = "\x1b[38;2;78;201;176m";
|
|
18
|
+
function green(text) { return `${GREEN}${text}${RESET}`; }
|
|
19
|
+
function red(text) { return `${RED}${text}${RESET}`; }
|
|
20
|
+
function yellow(text) { return `${YELLOW}${text}${RESET}`; }
|
|
21
|
+
function bold(text) { return `${BOLD}${text}${RESET}`; }
|
|
22
|
+
function dim(text) { return `${DIM}${text}${RESET}`; }
|
|
23
|
+
function teal(text) { return `${TEAL}${text}${RESET}`; }
|
|
24
|
+
// ── Check status symbols ──
|
|
25
|
+
function statusSymbol(status) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "pass": return green("\u2714"); // checkmark
|
|
28
|
+
case "warn": return yellow("\u26A0"); // warning
|
|
29
|
+
case "fail": return red("\u2718"); // X
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── Main formatter ──
|
|
33
|
+
function formatDoctorOutput(result) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push(` ${bold("ouro doctor")}`);
|
|
37
|
+
lines.push("");
|
|
38
|
+
for (const category of result.categories) {
|
|
39
|
+
lines.push(` ${teal("--")} ${bold(category.name)} ${teal("-".repeat(Math.max(1, 40 - category.name.length)))}`);
|
|
40
|
+
for (const check of category.checks) {
|
|
41
|
+
const symbol = statusSymbol(check.status);
|
|
42
|
+
const detail = check.detail ? ` ${dim(check.detail)}` : "";
|
|
43
|
+
lines.push(` ${symbol} ${check.label}${detail}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
// Summary line
|
|
48
|
+
const { passed, warnings, failed } = result.summary;
|
|
49
|
+
const parts = [
|
|
50
|
+
green(`${passed} passed`),
|
|
51
|
+
yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`),
|
|
52
|
+
red(`${failed} failed`),
|
|
53
|
+
];
|
|
54
|
+
lines.push(` ${parts.join(dim(" | "))}`);
|
|
55
|
+
lines.push("");
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Type definitions for the `ouro doctor` system health check.
|
|
4
|
+
*
|
|
5
|
+
* Describes the structure of health check results: individual checks,
|
|
6
|
+
* grouped categories, and the aggregated result.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* System health check ("ouro doctor") — runs all diagnostic categories
|
|
4
|
+
* and aggregates results into a structured DoctorResult.
|
|
5
|
+
*
|
|
6
|
+
* Each category checker is isolated: if one throws, it produces a single
|
|
7
|
+
* "fail" check and the remaining categories still run.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.checkDaemon = checkDaemon;
|
|
11
|
+
exports.checkAgents = checkAgents;
|
|
12
|
+
exports.checkSenses = checkSenses;
|
|
13
|
+
exports.checkHabits = checkHabits;
|
|
14
|
+
exports.checkSecurity = checkSecurity;
|
|
15
|
+
exports.checkDisk = checkDisk;
|
|
16
|
+
exports.runDoctorChecks = runDoctorChecks;
|
|
17
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
18
|
+
// ── Category checkers ──
|
|
19
|
+
async function checkDaemon(deps) {
|
|
20
|
+
const checks = [];
|
|
21
|
+
const socketExists = deps.existsSync(deps.socketPath);
|
|
22
|
+
checks.push({
|
|
23
|
+
label: "daemon socket exists",
|
|
24
|
+
status: socketExists ? "pass" : "fail",
|
|
25
|
+
detail: socketExists ? deps.socketPath : `not found at ${deps.socketPath}`,
|
|
26
|
+
});
|
|
27
|
+
if (socketExists) {
|
|
28
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
29
|
+
checks.push({
|
|
30
|
+
label: "daemon is responsive",
|
|
31
|
+
status: alive ? "pass" : "fail",
|
|
32
|
+
detail: alive ? "socket responded" : "socket exists but daemon unresponsive",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
checks.push({
|
|
37
|
+
label: "daemon is responsive",
|
|
38
|
+
status: "fail",
|
|
39
|
+
detail: "skipped — socket missing",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return { name: "Daemon", checks };
|
|
43
|
+
}
|
|
44
|
+
/** Discover all *.ouro directories under bundlesRoot. */
|
|
45
|
+
function discoverAgents(deps) {
|
|
46
|
+
if (!deps.existsSync(deps.bundlesRoot))
|
|
47
|
+
return [];
|
|
48
|
+
return deps.readdirSync(deps.bundlesRoot).filter((name) => name.endsWith(".ouro"));
|
|
49
|
+
}
|
|
50
|
+
function checkAgents(deps) {
|
|
51
|
+
const checks = [];
|
|
52
|
+
if (!deps.existsSync(deps.bundlesRoot)) {
|
|
53
|
+
checks.push({ label: "bundles directory", status: "fail", detail: `${deps.bundlesRoot} not found` });
|
|
54
|
+
return { name: "Agents", checks };
|
|
55
|
+
}
|
|
56
|
+
const agents = discoverAgents(deps);
|
|
57
|
+
if (agents.length === 0) {
|
|
58
|
+
checks.push({ label: "agent bundles", status: "warn", detail: "no *.ouro bundles found" });
|
|
59
|
+
return { name: "Agents", checks };
|
|
60
|
+
}
|
|
61
|
+
for (const agentDir of agents) {
|
|
62
|
+
const agentPath = `${deps.bundlesRoot}/${agentDir}`;
|
|
63
|
+
const configPath = `${agentPath}/agent.json`;
|
|
64
|
+
if (!deps.existsSync(configPath)) {
|
|
65
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "fail", detail: "missing" });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let config;
|
|
69
|
+
try {
|
|
70
|
+
config = JSON.parse(deps.readFileSync(configPath));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "fail", detail: "unparseable JSON" });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const missing = [];
|
|
77
|
+
if (!config.version)
|
|
78
|
+
missing.push("version");
|
|
79
|
+
if (!config.humanFacing || typeof config.humanFacing !== "object") {
|
|
80
|
+
missing.push("humanFacing");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const hf = config.humanFacing;
|
|
84
|
+
if (!hf.provider)
|
|
85
|
+
missing.push("humanFacing.provider");
|
|
86
|
+
if (!hf.model)
|
|
87
|
+
missing.push("humanFacing.model");
|
|
88
|
+
}
|
|
89
|
+
if (!config.agentFacing || typeof config.agentFacing !== "object") {
|
|
90
|
+
missing.push("agentFacing");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const af = config.agentFacing;
|
|
94
|
+
if (!af.provider)
|
|
95
|
+
missing.push("agentFacing.provider");
|
|
96
|
+
if (!af.model)
|
|
97
|
+
missing.push("agentFacing.model");
|
|
98
|
+
}
|
|
99
|
+
if (missing.length > 0) {
|
|
100
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "warn", detail: `missing fields: ${missing.join(", ")}` });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
checks.push({ label: `${agentDir}/agent.json`, status: "pass", detail: "valid" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { name: "Agents", checks };
|
|
107
|
+
}
|
|
108
|
+
function checkSenses(deps) {
|
|
109
|
+
const checks = [];
|
|
110
|
+
const agents = discoverAgents(deps);
|
|
111
|
+
for (const agentDir of agents) {
|
|
112
|
+
const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
|
|
113
|
+
if (!deps.existsSync(configPath))
|
|
114
|
+
continue;
|
|
115
|
+
let config;
|
|
116
|
+
try {
|
|
117
|
+
config = JSON.parse(deps.readFileSync(configPath));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
checks.push({ label: `${agentDir} senses`, status: "fail", detail: "agent.json unparseable" });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!config.senses || typeof config.senses !== "object") {
|
|
124
|
+
checks.push({ label: `${agentDir} senses`, status: "warn", detail: "no senses config block" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const senses = config.senses;
|
|
128
|
+
const senseNames = ["cli", "teams", "bluebubbles"];
|
|
129
|
+
for (const sense of senseNames) {
|
|
130
|
+
if (!(sense in senses))
|
|
131
|
+
continue;
|
|
132
|
+
const entry = senses[sense];
|
|
133
|
+
if (!entry || typeof entry !== "object") {
|
|
134
|
+
checks.push({ label: `${agentDir} ${sense}`, status: "fail", detail: "malformed sense entry" });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const senseObj = entry;
|
|
138
|
+
if (typeof senseObj.enabled !== "boolean") {
|
|
139
|
+
checks.push({ label: `${agentDir} ${sense}`, status: "warn", detail: "missing enabled boolean" });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
checks.push({
|
|
143
|
+
label: `${agentDir} ${sense}`,
|
|
144
|
+
status: "pass",
|
|
145
|
+
detail: senseObj.enabled ? "enabled" : "disabled",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (checks.length === 0) {
|
|
151
|
+
checks.push({ label: "senses", status: "warn", detail: "no agents with senses config found" });
|
|
152
|
+
}
|
|
153
|
+
return { name: "Senses", checks };
|
|
154
|
+
}
|
|
155
|
+
function checkHabits(deps) {
|
|
156
|
+
const checks = [];
|
|
157
|
+
const agents = discoverAgents(deps);
|
|
158
|
+
for (const agentDir of agents) {
|
|
159
|
+
const agentName = agentDir.replace(/\.ouro$/, "");
|
|
160
|
+
const habitsDir = `${deps.bundlesRoot}/${agentDir}/habits`;
|
|
161
|
+
if (!deps.existsSync(habitsDir)) {
|
|
162
|
+
checks.push({ label: `${agentDir} habits dir`, status: "warn", detail: "no habits directory" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
|
|
166
|
+
// Check for launchd plists on macOS
|
|
167
|
+
const launchAgentsDir = `${deps.homedir}/Library/LaunchAgents`;
|
|
168
|
+
if (deps.existsSync(launchAgentsDir)) {
|
|
169
|
+
const plists = deps.readdirSync(launchAgentsDir).filter((f) => f.startsWith(`bot.ouro.${agentName}.`) && f.endsWith(".plist"));
|
|
170
|
+
if (plists.length > 0) {
|
|
171
|
+
checks.push({ label: `${agentDir} launchd plists`, status: "pass", detail: `${plists.length} plist(s)` });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
checks.push({ label: `${agentDir} launchd plists`, status: "fail", detail: "no matching plists in LaunchAgents" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (checks.length === 0) {
|
|
179
|
+
checks.push({ label: "habits", status: "warn", detail: "no agents found" });
|
|
180
|
+
}
|
|
181
|
+
return { name: "Habits", checks };
|
|
182
|
+
}
|
|
183
|
+
function checkSecurity(deps) {
|
|
184
|
+
const checks = [];
|
|
185
|
+
const agents = discoverAgents(deps);
|
|
186
|
+
for (const agentDir of agents) {
|
|
187
|
+
const agentName = agentDir.replace(/\.ouro$/, "");
|
|
188
|
+
const secretsPath = `${deps.secretsRoot}/${agentName}/secrets.json`;
|
|
189
|
+
if (!deps.existsSync(secretsPath)) {
|
|
190
|
+
checks.push({ label: `${agentDir} secrets.json`, status: "fail", detail: "missing" });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Check file permissions
|
|
194
|
+
const stat = deps.statSync(secretsPath);
|
|
195
|
+
const worldReadable = (stat.mode & 0o004) !== 0;
|
|
196
|
+
if (worldReadable) {
|
|
197
|
+
checks.push({ label: `${agentDir} secrets.json perms`, status: "warn", detail: "world-readable — consider chmod 600" });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
checks.push({ label: `${agentDir} secrets.json perms`, status: "pass", detail: "not world-readable" });
|
|
201
|
+
}
|
|
202
|
+
// Check agent.json for leaked credential keys
|
|
203
|
+
const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
|
|
204
|
+
if (deps.existsSync(configPath)) {
|
|
205
|
+
try {
|
|
206
|
+
const raw = deps.readFileSync(configPath);
|
|
207
|
+
const sensitiveKeys = ["apiKey", "token", "secret", "password"];
|
|
208
|
+
const found = sensitiveKeys.filter((key) => raw.includes(`"${key}"`));
|
|
209
|
+
if (found.length > 0) {
|
|
210
|
+
checks.push({ label: `${agentDir} credential leak`, status: "warn", detail: `agent.json contains keys: ${found.join(", ")}` });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
checks.push({ label: `${agentDir} credential leak`, status: "pass", detail: "no credential keys in agent.json" });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
checks.push({ label: `${agentDir} credential leak`, status: "fail", detail: "could not read agent.json" });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (checks.length === 0) {
|
|
222
|
+
checks.push({ label: "security", status: "warn", detail: "no agents found" });
|
|
223
|
+
}
|
|
224
|
+
return { name: "Security", checks };
|
|
225
|
+
}
|
|
226
|
+
function checkDisk(deps) {
|
|
227
|
+
const checks = [];
|
|
228
|
+
// Check daemon logs directory
|
|
229
|
+
const logsDir = `${deps.homedir}/.ouro-cli/logs`;
|
|
230
|
+
if (!deps.existsSync(logsDir)) {
|
|
231
|
+
checks.push({ label: "daemon logs dir", status: "warn", detail: `${logsDir} not found` });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
let totalSize = 0;
|
|
235
|
+
try {
|
|
236
|
+
const files = deps.readdirSync(logsDir);
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
try {
|
|
239
|
+
const stat = deps.statSync(`${logsDir}/${file}`);
|
|
240
|
+
totalSize += stat.size;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// skip unreadable files
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// readdirSync failure handled below
|
|
249
|
+
}
|
|
250
|
+
const sizeMB = totalSize / (1024 * 1024);
|
|
251
|
+
if (sizeMB > 500) {
|
|
252
|
+
checks.push({ label: "daemon log size", status: "fail", detail: `${sizeMB.toFixed(1)}MB — exceeds 500MB limit` });
|
|
253
|
+
}
|
|
254
|
+
else if (sizeMB > 100) {
|
|
255
|
+
checks.push({ label: "daemon log size", status: "warn", detail: `${sizeMB.toFixed(1)}MB — consider pruning with \`ouro logs prune\`` });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
checks.push({ label: "daemon log size", status: "pass", detail: `${sizeMB.toFixed(1)}MB` });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Check AgentBundles root
|
|
262
|
+
if (deps.existsSync(deps.bundlesRoot)) {
|
|
263
|
+
checks.push({ label: "bundles root", status: "pass", detail: deps.bundlesRoot });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
checks.push({ label: "bundles root", status: "warn", detail: `${deps.bundlesRoot} not found` });
|
|
267
|
+
}
|
|
268
|
+
return { name: "Disk", checks };
|
|
269
|
+
}
|
|
270
|
+
// ── Orchestrator ──
|
|
271
|
+
function computeSummary(categories) {
|
|
272
|
+
let passed = 0;
|
|
273
|
+
let warnings = 0;
|
|
274
|
+
let failed = 0;
|
|
275
|
+
for (const cat of categories) {
|
|
276
|
+
for (const check of cat.checks) {
|
|
277
|
+
/* v8 ignore next 3 -- all three branches tested; v8 misreports compound if/else-if chain @preserve */
|
|
278
|
+
if (check.status === "pass")
|
|
279
|
+
passed++;
|
|
280
|
+
else if (check.status === "warn")
|
|
281
|
+
warnings++;
|
|
282
|
+
else
|
|
283
|
+
failed++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { passed, warnings, failed };
|
|
287
|
+
}
|
|
288
|
+
const CATEGORY_CHECKERS = [
|
|
289
|
+
{ name: "Daemon", fn: checkDaemon },
|
|
290
|
+
{ name: "Agents", fn: checkAgents },
|
|
291
|
+
{ name: "Senses", fn: checkSenses },
|
|
292
|
+
{ name: "Habits", fn: checkHabits },
|
|
293
|
+
{ name: "Security", fn: checkSecurity },
|
|
294
|
+
{ name: "Disk", fn: checkDisk },
|
|
295
|
+
];
|
|
296
|
+
async function runDoctorChecks(deps) {
|
|
297
|
+
const categories = [];
|
|
298
|
+
for (const checker of CATEGORY_CHECKERS) {
|
|
299
|
+
try {
|
|
300
|
+
const category = await Promise.resolve(checker.fn(deps));
|
|
301
|
+
categories.push(category);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
(0, runtime_1.emitNervesEvent)({
|
|
305
|
+
level: "warn",
|
|
306
|
+
component: "daemon",
|
|
307
|
+
event: "daemon.doctor_check_error",
|
|
308
|
+
message: `doctor check ${checker.name} failed`,
|
|
309
|
+
meta: { category: checker.name, error: error instanceof Error ? error.message : String(error) },
|
|
310
|
+
});
|
|
311
|
+
categories.push({
|
|
312
|
+
name: checker.name,
|
|
313
|
+
checks: [{
|
|
314
|
+
label: checker.name.toLowerCase(),
|
|
315
|
+
status: "fail",
|
|
316
|
+
detail: `check crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
317
|
+
}],
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { categories, summary: computeSummary(categories) };
|
|
322
|
+
}
|
|
@@ -59,9 +59,10 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
59
59
|
"repertoire/tools-config",
|
|
60
60
|
"repertoire/tools-base",
|
|
61
61
|
// CLI sub-modules: cli-exec.ts is the router with emitNervesEvent calls;
|
|
62
|
-
// cli-parse and cli-
|
|
62
|
+
// cli-parse, cli-render, and cli-help are pure functions/data with no side effects.
|
|
63
63
|
"daemon/cli-parse",
|
|
64
64
|
"daemon/cli-render",
|
|
65
|
+
"daemon/cli-help",
|
|
65
66
|
// Shared utility modules: pure helpers consumed by modules that own observability.
|
|
66
67
|
"arc/json-store",
|
|
67
68
|
"repertoire/api-client",
|