hovclaw 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 HOVClaw Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ <h1 align="center">HOVClaw</h1>
2
+
3
+ <p align="center">
4
+ <strong>Lean self-hosted AI agent gateway with OpenClaw-compatible control surface</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="#philosophy">Philosophy</a> •
9
+ <a href="#features">Features</a> •
10
+ <a href="#installation">Installation</a> •
11
+ <a href="#configuration">Configuration</a> •
12
+ <a href="#architecture">Architecture</a>
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## Philosophy
18
+
19
+ HOVClaw is built on a simple principle: **run your own AI agent infrastructure, controlled from the channels you already use**.
20
+
21
+ - **Self-hosted first** - Everything runs on your machine, no cloud dependency
22
+ - **Channel-native** - Talk to your agent via Telegram or Discord, not a custom app
23
+ - **OpenClaw-compatible** - Mirror config to `~/.openclaw` for ClawHub discovery and tooling interop
24
+ - **Gateway-first control** - WebSocket protocol v3 for programmatic access, with a built-in web UI for quick ops
25
+
26
+ ## Features
27
+
28
+ ### Multi-Channel Agent Gateway
29
+
30
+ - **Telegram** - Polling/webhook intake, callback queries, topic routing (`chatId#threadId`), media send, reactions
31
+ - **Discord** - Full bot adapter via discord.js
32
+ - **Multi-account Telegram** - Multiple bot accounts with per-account status and logout
33
+ - **Text mode controls** - Per-channel `plain|markdown` rendering mode (default `plain`)
34
+ - **Policy layer** - `dmPolicy`, `groupPolicy`, per-group/per-topic overrides, pairing flow
35
+
36
+ ### Gateway & Control Plane
37
+
38
+ - **WebSocket protocol v3** - Request/response/event frames with 21 methods
39
+ - **Built-in web UI** - Connection, health, channels, sessions, and chat in one page
40
+ - **LaunchAgent integration** - `hovclaw gateway install/start/stop` for macOS background service
41
+ - **Programmatic access** - `hovclaw gateway call <method>` for scripting
42
+
43
+ ### Agent Runtime
44
+
45
+ - **Pi agent core** - `@mariozechner/pi-agent-core` for agent loop and tool orchestration
46
+ - **Multi-provider models** - Anthropic, Google, OpenAI, OpenRouter via `@mariozechner/pi-ai`
47
+ - **Model routing** - Per-target model slots (interactive, discord, cron) with fallback policy
48
+ - **Workspace-first tools** - Relative file tool paths resolve from agent workspace
49
+ - **Session persistence** - SQLite-backed sessions, messages, agent state, and usage tracking
50
+
51
+ ### Scheduling & Automation
52
+
53
+ - **Cron jobs** - `agents/*/cron.json` with configurable schedules and timezone support
54
+ - **Channel notifications** - Scheduled job results delivered to Telegram or Discord
55
+ - **Concurrent execution** - Configurable max concurrent jobs
56
+
57
+ ### OpenClaw Compatibility
58
+
59
+ - **Mirror strategy** - HOVClaw is source of truth; mirror files written to `~/.openclaw`
60
+ - **ClawHub discovery** - `~/.openclaw/openclaw.json` + `~/.openclaw/skills` symlink
61
+ - **Compat CLI** - `hovclaw compat status --sync` to verify mirror state
62
+
63
+ ## Installation
64
+
65
+ ### Prerequisites
66
+
67
+ - Node.js 22+
68
+ - Bun package manager
69
+
70
+ ### Build
71
+
72
+ ```bash
73
+ # Clone the repository
74
+ git clone https://github.com/user/hovclaw.git
75
+ cd hovclaw
76
+
77
+ # Install dependencies
78
+ bun install
79
+
80
+ # Build
81
+ bun run build
82
+
83
+ # Run tests
84
+ bun run test
85
+ ```
86
+
87
+ ### First-Time Setup
88
+
89
+ ```bash
90
+ # Interactive onboarding (configures channels, models, credentials)
91
+ bun run onboard
92
+
93
+ # Or if hovclaw is linked globally
94
+ hovclaw onboard
95
+ ```
96
+
97
+ ## Configuration
98
+
99
+ Run the onboarding wizard to get started:
100
+
101
+ ```bash
102
+ hovclaw onboard
103
+ ```
104
+
105
+ The wizard handles channel tokens, model provider credentials (via OAuth or API key),
106
+ and agent configuration. All settings are saved to `~/.hovclaw/config.json`.
107
+
108
+ ### Workspace Defaults and Bootstrap
109
+
110
+ - Default workspace: `~/.hovclaw/workspace`
111
+ - Blank agent workspace values resolve to the same default workspace
112
+ - On startup and onboarding, HOVClaw auto-creates missing workspace files:
113
+ - `AGENTS.md`
114
+ - `IDENTITY.md`
115
+ - `USER.md`
116
+ - `BOOTSTRAP.md` (only when the workspace is effectively empty)
117
+ - Workspace files are appended to the system prompt in this order:
118
+ - `AGENTS.md` -> `IDENTITY.md` -> `USER.md` -> `BOOTSTRAP.md`
119
+ - capped at 4,000 chars per file and 12,000 chars total
120
+
121
+ ### Config Structure
122
+
123
+ | Key | Purpose |
124
+ |-----|---------|
125
+ | `assistant` | Assistant name and identity |
126
+ | `agents` | Agent definitions and defaults |
127
+ | `bindings` | Inbound message routing rules |
128
+ | `models` | Model slots, fallback policy, aliases |
129
+ | `runtime` | Execution mode, timeouts, allowed paths/commands |
130
+ | `channels` | Telegram and Discord channel config |
131
+ | `gateway` | Gateway host, port, auth, web UI settings |
132
+ | `scheduler` | Cron poll interval, concurrency, timezone |
133
+
134
+ Environment overrides are supported for most fields. See [docs/config-reference.md](docs/config-reference.md).
135
+
136
+ ## Architecture
137
+
138
+ ```
139
+ ┌──────────────────────────────────────────────────────────────┐
140
+ │ Channels │
141
+ │ ┌──────────┐ ┌─────────┐ ┌─────┐ ┌───────────┐ │
142
+ │ │ Telegram │ │ Discord │ │ CLI │ │ Scheduler │ │
143
+ │ └────┬─────┘ └────┬────┘ └──┬──┘ └─────┬─────┘ │
144
+ │ └──────────────┴─────────┴────────────┘ │
145
+ │ │ │
146
+ │ ┌──────▼──────┐ │
147
+ │ │ Router │ Binding-based agent │
148
+ │ │ │ resolution │
149
+ │ └──────┬──────┘ │
150
+ │ │ │
151
+ │ ┌─────────▼─────────┐ │
152
+ │ │ Agent Manager │ Session lifecycle │
153
+ │ │ │ + persistence │
154
+ │ └─────────┬─────────┘ │
155
+ │ │ │
156
+ │ ┌────────────────┼────────────────┐ │
157
+ │ ▼ ▼ ▼ │
158
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
159
+ │ │ Agent │ │ Agent │ │ Agent │ pi-agent │
160
+ │ │ Session │ │ Session │ │ Session │ core loop │
161
+ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
162
+ │ └────────────────┼────────────────┘ │
163
+ │ │ │
164
+ │ ┌──────────▼──────────┐ │
165
+ │ │ Tool Runtime │ │
166
+ │ │ ┌───────────────┐ │ │
167
+ │ │ │ Built-in │ │ │
168
+ │ │ │ Skills │ │ │
169
+ │ │ │ Local / Docker│ │ │
170
+ │ │ └───────────────┘ │ │
171
+ │ └─────────────────────┘ │
172
+ │ │
173
+ │ ┌────────────────────────────────────────────────────────┐ │
174
+ │ │ Gateway (ws + http) │ │
175
+ │ │ WebSocket v3 protocol • Web UI • 21 methods │ │
176
+ │ └────────────────────────────────────────────────────────┘ │
177
+ │ │
178
+ │ ┌────────────────────────────────────────────────────────┐ │
179
+ │ │ SQLite (better-sqlite3) │ │
180
+ │ │ sessions • messages • agent_state • usage_costs │ │
181
+ │ │ scheduled_jobs • task_run_logs • audit_log │ │
182
+ │ └────────────────────────────────────────────────────────┘ │
183
+ └──────────────────────────────────────────────────────────────┘
184
+ ```
185
+
186
+ ### Core Components
187
+
188
+ | Component | Purpose |
189
+ |-----------|---------|
190
+ | **Agent Manager** | Per-session agent lifecycle, state persistence, model resolution |
191
+ | **Router** | Binding-based inbound routing with peer/guild/account/channel cascade |
192
+ | **Scheduler** | Cron job loading, execution, and channel notifications |
193
+ | **Gateway** | WebSocket v3 server with 21 methods, 5 event types, built-in web UI |
194
+ | **Skill Loader** | SKILL.md frontmatter parsing and dependency checking |
195
+ | **Channels** | Telegram (multi-account, policy, pairing) and Discord adapters |
196
+
197
+ ## CLI Overview
198
+
199
+ ```bash
200
+ # Setup
201
+ hovclaw onboard
202
+ hovclaw login [provider]
203
+ hovclaw doctor [--fix] [--deep] [--json]
204
+ hovclaw status [--json]
205
+
206
+ # Messaging
207
+ hovclaw message send --channel telegram --to <chat_id> --message "hello"
208
+
209
+ # Channel management
210
+ hovclaw channels list|status|add|remove|login|logout [--account <id>] [--json]
211
+
212
+ # Model management
213
+ hovclaw models list|status|set [--model <ref>] [--target <slot>] [--json]
214
+
215
+ # Gateway lifecycle
216
+ hovclaw gateway run
217
+ hovclaw gateway install|uninstall|start|stop|restart [--json]
218
+ hovclaw gateway status|health [--json]
219
+ hovclaw gateway call <method> [--params '{...}'] [--json]
220
+ hovclaw gateway open-ui
221
+
222
+ # Daemon
223
+ hovclaw daemon install|uninstall|start|stop|restart|status|logs
224
+
225
+ # Compatibility
226
+ hovclaw compat status [--sync] [--json]
227
+ ```
228
+
229
+ ## Gateway Methods (v3)
230
+
231
+ | Method | Purpose |
232
+ |--------|---------|
233
+ | `health` | Uptime, active sessions, channels |
234
+ | `status` | Gateway config, channel status, session counts |
235
+ | `channels.status` | Per-channel enabled/connected status |
236
+ | `channels.logout` | Log out a channel (optionally scoped) |
237
+ | `config.get` / `config.set` / `config.patch` | Read/write/merge config |
238
+ | `models.list` / `models.set` / `models.status` | Model catalog and routing |
239
+ | `skills.status` | Skill list with dependency checks |
240
+ | `sessions.list` / `sessions.preview` | Session listing and message history |
241
+ | `send` | Send text/media/reaction to a channel |
242
+ | `agent` | Run agent loop, stream events |
243
+ | `chat.history` / `chat.send` / `chat.abort` | Chat session interaction |
244
+ | `cron.list` / `cron.status` | Scheduled job listing and status |
245
+ | `logs.tail` | Recent audit events |
246
+
247
+ Events: `tick`, `health`, `agent`, `chat`, `shutdown`
248
+
249
+ ## Development
250
+
251
+ ```bash
252
+ bun run build # tsc compile to dist/
253
+ bun run typecheck # tsc --noEmit
254
+ bun run dev # start daemon (tsx src/index.ts)
255
+ bun run test # vitest run
256
+ bun run test:watch # vitest watch
257
+ ```
258
+
259
+ ## OpenClaw Heritage
260
+
261
+ HOVClaw is a lean TypeScript implementation inspired by [OpenClaw](https://github.com/openclaw/openclaw). See [FEATURE_PARITY.md](FEATURE_PARITY.md) for the complete tracking matrix.
262
+
263
+ Key differences:
264
+
265
+ - **Lean scope** - Telegram + Discord only, not all 20+ channels
266
+ - **Gateway-first** - WebSocket v3 control plane with built-in web UI
267
+ - **SQLite persistence** - Single-file database, no external services
268
+ - **Pi agent runtime** - `@mariozechner/pi-agent-core` for agent loop orchestration
269
+ - **Multi-account Telegram** - Per-account config, policy, and pairing
270
+
271
+ ## Docs
272
+
273
+ - [docs/concept.md](docs/concept.md)
274
+ - [docs/architecture.md](docs/architecture.md)
275
+ - [docs/config-reference.md](docs/config-reference.md)
276
+ - [docs/gateway-compat.md](docs/gateway-compat.md)
277
+
278
+ ## License
279
+
280
+ MIT
@@ -0,0 +1,211 @@
1
+ import { A as hasCredentialsFile, C as detectLegacyEnvConfig, E as getCredentialsPath, I as saveCredentials, M as loadCredentials, O as getHovclawHome, T as getConfigPath, j as loadConfig, k as hasConfigFile, w as ensureConfigFromLegacyEnv } from "./hovclaw.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { intro, log, outro } from "@clack/prompts";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ //#region src/cli/doctor.ts
9
+ function addFinding(findings, id, status, title, detail) {
10
+ findings.push({
11
+ id,
12
+ status,
13
+ title,
14
+ detail
15
+ });
16
+ }
17
+ function summarize(findings) {
18
+ return findings.reduce((acc, finding) => {
19
+ acc[finding.status] += 1;
20
+ return acc;
21
+ }, {
22
+ pass: 0,
23
+ warn: 0,
24
+ fail: 0,
25
+ repair: 0
26
+ });
27
+ }
28
+ function hasAtLeastOneCredential(credentials) {
29
+ return Object.keys(credentials).length > 0;
30
+ }
31
+ function isValidTimezone(tz) {
32
+ try {
33
+ Intl.DateTimeFormat("en-US", { timeZone: tz }).format(/* @__PURE__ */ new Date());
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ function pathInsideAnyRoot(filePath, roots) {
40
+ const resolved = path.resolve(filePath);
41
+ return roots.some((root) => {
42
+ const resolvedRoot = path.resolve(root);
43
+ return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`);
44
+ });
45
+ }
46
+ function checkDockerAvailability() {
47
+ const result = spawnSync("docker", [
48
+ "version",
49
+ "--format",
50
+ "{{.Server.Version}}"
51
+ ], {
52
+ encoding: "utf8",
53
+ timeout: 1e4
54
+ });
55
+ if (result.status === 0) return {
56
+ ok: true,
57
+ detail: `Docker available (${result.stdout.trim() || "unknown"}).`
58
+ };
59
+ return {
60
+ ok: false,
61
+ detail: result.stderr?.trim() || result.stdout?.trim() || "docker command failed. Install/start Docker Desktop and retry."
62
+ };
63
+ }
64
+ function parseDoctorArgs(args) {
65
+ const options = {
66
+ repair: false,
67
+ deep: false,
68
+ json: false
69
+ };
70
+ for (const arg of args) {
71
+ if (arg === "--repair" || arg === "--fix") {
72
+ options.repair = true;
73
+ continue;
74
+ }
75
+ if (arg === "--deep") {
76
+ options.deep = true;
77
+ continue;
78
+ }
79
+ if (arg === "--json") {
80
+ options.json = true;
81
+ continue;
82
+ }
83
+ throw new Error(`Unknown flag: ${arg}`);
84
+ }
85
+ return options;
86
+ }
87
+ function runDoctorChecks(options, env = process.env) {
88
+ const findings = [];
89
+ const hovclawHome = getHovclawHome(env);
90
+ const configPath = getConfigPath(env);
91
+ const credentialsPath = getCredentialsPath(env);
92
+ if (!fs.existsSync(hovclawHome)) if (options.repair) {
93
+ fs.mkdirSync(hovclawHome, {
94
+ recursive: true,
95
+ mode: 448
96
+ });
97
+ addFinding(findings, "home-dir", "repair", "Created HovClaw home directory", hovclawHome);
98
+ } else addFinding(findings, "home-dir", "warn", "HovClaw home directory missing", `${hovclawHome} (run 'npm run onboard' or re-run doctor with --fix)`);
99
+ else addFinding(findings, "home-dir", "pass", "HovClaw home directory", hovclawHome);
100
+ let configLoaded = false;
101
+ let loadedConfig = null;
102
+ if (!hasConfigFile(env)) if (options.repair && detectLegacyEnvConfig(env)) if (ensureConfigFromLegacyEnv(env)) addFinding(findings, "config-file", "repair", "Imported legacy env config", `Config created at ${configPath}`);
103
+ else addFinding(findings, "config-file", "fail", "Config file missing", `No config found at ${configPath}. Run 'npm run onboard'.`);
104
+ else addFinding(findings, "config-file", "fail", "Config file missing", `No config found at ${configPath}. Run 'npm run onboard'.`);
105
+ try {
106
+ loadedConfig = loadConfig(env);
107
+ configLoaded = true;
108
+ addFinding(findings, "config-parse", "pass", "Config parsed successfully", configPath);
109
+ } catch (error) {
110
+ addFinding(findings, "config-parse", "fail", "Config is invalid", `${configPath}: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+ if (!hasCredentialsFile(env)) if (options.repair) {
113
+ saveCredentials({}, env);
114
+ addFinding(findings, "credentials-file", "repair", "Created credentials file", credentialsPath);
115
+ } else addFinding(findings, "credentials-file", "warn", "Credentials file missing", `${credentialsPath} (run 'npm run onboard' to configure providers).`);
116
+ try {
117
+ const credentials = loadCredentials(env);
118
+ if (hasAtLeastOneCredential(credentials)) addFinding(findings, "credentials-presence", "pass", "Credentials are configured", `Providers: ${Object.keys(credentials).sort().join(", ")}`);
119
+ else addFinding(findings, "credentials-presence", "warn", "No provider credentials configured", "Run 'npm run onboard' or 'npm run login -- <provider>'.");
120
+ } catch (error) {
121
+ addFinding(findings, "credentials-parse", "fail", "Credentials file is invalid", `${credentialsPath}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ if (configLoaded && loadedConfig) {
124
+ if (!loadedConfig.channels.discord.enabled && !loadedConfig.channels.telegram.enabled) addFinding(findings, "channels-enabled", "warn", "No channels enabled", "Enable at least one channel in onboarding or config.");
125
+ else addFinding(findings, "channels-enabled", "pass", "At least one channel enabled", "OK");
126
+ if (loadedConfig.channels.discord.enabled) if (!loadedConfig.channels.discord.botToken.trim()) addFinding(findings, "discord-token", "fail", "Discord enabled without token", "Set channels.discord.botToken via onboarding.");
127
+ else addFinding(findings, "discord-token", "pass", "Discord token configured", "OK");
128
+ if (loadedConfig.channels.telegram.enabled) if (!loadedConfig.channels.telegram.botToken.trim()) addFinding(findings, "telegram-token", "fail", "Telegram enabled without token", "Set channels.telegram.botToken via onboarding.");
129
+ else addFinding(findings, "telegram-token", "pass", "Telegram token configured", "OK");
130
+ if (!loadedConfig.gateway.enabled) addFinding(findings, "gateway-enabled", "warn", "Gateway is disabled", "Enable gateway for OpenClaw/ClawHub compatibility.");
131
+ else addFinding(findings, "gateway-enabled", "pass", "Gateway enabled", "OK");
132
+ if (!loadedConfig.gateway.host.trim() || loadedConfig.gateway.port <= 0) addFinding(findings, "gateway-bind", "fail", "Gateway bind is invalid", "Set gateway.host and gateway.port to valid values.");
133
+ else addFinding(findings, "gateway-bind", "pass", "Gateway bind configured", `${loadedConfig.gateway.host}:${loadedConfig.gateway.port}`);
134
+ if (loadedConfig.gateway.mode === "remote" && !loadedConfig.gateway.remote.url.trim()) addFinding(findings, "gateway-remote-url", "fail", "Gateway remote mode missing URL", "Set gateway.remote.url or switch gateway.mode to local.");
135
+ if (loadedConfig.runtime.allowedReadRoots.length === 0) addFinding(findings, "runtime-read-roots", "fail", "No read roots configured", "Set runtime.allowedReadRoots in onboarding/config.");
136
+ else addFinding(findings, "runtime-read-roots", "pass", "Read roots configured", `${loadedConfig.runtime.allowedReadRoots.length} root(s)`);
137
+ if (loadedConfig.runtime.allowedWriteRoots.length === 0) addFinding(findings, "runtime-write-roots", "warn", "No write roots configured", "Agent will not be able to write files.");
138
+ else addFinding(findings, "runtime-write-roots", "pass", "Write roots configured", `${loadedConfig.runtime.allowedWriteRoots.length} root(s)`);
139
+ const unreadableWriteRoots = loadedConfig.runtime.allowedWriteRoots.filter((writeRoot) => !pathInsideAnyRoot(writeRoot, loadedConfig.runtime.allowedReadRoots));
140
+ if (unreadableWriteRoots.length > 0) addFinding(findings, "runtime-write-without-read", "warn", "Some write roots are not in read roots", unreadableWriteRoots.join(", "));
141
+ for (const writeRoot of loadedConfig.runtime.allowedWriteRoots) if (!fs.existsSync(writeRoot)) if (options.repair) {
142
+ fs.mkdirSync(writeRoot, {
143
+ recursive: true,
144
+ mode: 448
145
+ });
146
+ addFinding(findings, "runtime-write-root-create", "repair", "Created missing write root", writeRoot);
147
+ } else addFinding(findings, "runtime-write-root-missing", "warn", "Write root does not exist", `${writeRoot} (re-run doctor with --fix to create).`);
148
+ if (!isValidTimezone(loadedConfig.scheduler.timezone)) addFinding(findings, "scheduler-timezone", "fail", "Scheduler timezone is invalid", loadedConfig.scheduler.timezone);
149
+ else addFinding(findings, "scheduler-timezone", "pass", "Scheduler timezone valid", loadedConfig.scheduler.timezone);
150
+ const mainAgentPath = path.join(loadedConfig.agentsDir, "main", "agent.json");
151
+ if (!fs.existsSync(mainAgentPath)) addFinding(findings, "main-agent", "fail", "Main agent config missing", `${mainAgentPath} not found.`);
152
+ else {
153
+ addFinding(findings, "main-agent", "pass", "Main agent config present", mainAgentPath);
154
+ try {
155
+ const parsed = JSON.parse(fs.readFileSync(mainAgentPath, "utf8"));
156
+ if (Array.isArray(parsed.skills)) {
157
+ const missingSkills = parsed.skills.filter((entry) => typeof entry === "string").filter((skill) => !fs.existsSync(path.join(loadedConfig.skillsDir, skill, "SKILL.md")));
158
+ if (missingSkills.length > 0) addFinding(findings, "main-agent-skills", "fail", "Main agent references missing skills", missingSkills.join(", "));
159
+ else addFinding(findings, "main-agent-skills", "pass", "Main agent skills are resolvable", "OK");
160
+ } else addFinding(findings, "main-agent-skills", "warn", "Main agent skills list is missing or invalid", "Expected skills: string[] in agents/main/agent.json");
161
+ } catch (error) {
162
+ addFinding(findings, "main-agent-parse", "fail", "Main agent config is invalid JSON", error instanceof Error ? error.message : String(error));
163
+ }
164
+ }
165
+ if (options.deep && loadedConfig.runtime.mode === "container") {
166
+ const docker = checkDockerAvailability();
167
+ if (docker.ok) addFinding(findings, "docker", "pass", "Docker is available", docker.detail);
168
+ else addFinding(findings, "docker", "fail", "Docker unavailable for container runtime", docker.detail);
169
+ }
170
+ }
171
+ const summary = summarize(findings);
172
+ return {
173
+ findings,
174
+ summary,
175
+ ok: summary.fail === 0
176
+ };
177
+ }
178
+ function printReport(report) {
179
+ for (const finding of report.findings) {
180
+ const line = `[${finding.status === "pass" ? "PASS" : finding.status === "warn" ? "WARN" : finding.status === "repair" ? "REPAIR" : "FAIL"}] ${finding.title}: ${finding.detail}`;
181
+ if (finding.status === "pass") log.success(line);
182
+ else if (finding.status === "warn") log.warn(line);
183
+ else if (finding.status === "repair") log.info(line);
184
+ else log.error(line);
185
+ }
186
+ const { pass, warn, fail, repair } = report.summary;
187
+ log.message(`Summary: ${pass} pass, ${warn} warn, ${fail} fail, ${repair} repair`);
188
+ }
189
+ async function main(argv = process.argv.slice(2)) {
190
+ let options;
191
+ try {
192
+ options = parseDoctorArgs(argv);
193
+ } catch (error) {
194
+ log.error(error instanceof Error ? error.message : String(error));
195
+ log.error("Usage: hovclaw doctor [--fix|--repair] [--deep] [--json]");
196
+ process.exit(1);
197
+ return;
198
+ }
199
+ if (!options.json) intro("HovClaw Doctor");
200
+ const report = runDoctorChecks(options);
201
+ if (options.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
202
+ else {
203
+ printReport(report);
204
+ outro(report.ok ? "Doctor complete." : "Doctor found issues.");
205
+ }
206
+ if (!report.ok) process.exit(1);
207
+ }
208
+ if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) main();
209
+
210
+ //#endregion
211
+ export { main };