@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 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.301",
5
+ "version": "0.1.0-alpha.303",
6
6
  "changes": [
7
- "fix(daemon): add an injectable `statusWriter` seam to `DaemonProcessManager` so human-visible config-failure lines still default to stderr in production, but tests can route or silence them intentionally. Status-writer failures are now swallowed with a `daemon.status_writer_error` nerves event instead of breaking agent startup bookkeeping. `process-manager.test.ts` now passes a no-op writer in the snapshot-focused config-check tests that were leaking expected fixture noise into the full coverage log, and adds a regression test proving a throwing status writer does not prevent the crashed snapshot/fix hint from being recorded."
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, cli_parse_2.usage)();
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
- throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
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-render are pure functions with no side effects.
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.301",
3
+ "version": "0.1.0-alpha.303",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",