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 +21 -0
- package/README.md +280 -0
- package/dist/doctor-I8YVuapp.js +211 -0
- package/dist/gateway/ui/app.js +512 -0
- package/dist/gateway/ui/credentials.d.ts +19 -0
- package/dist/gateway/ui/credentials.js +66 -0
- package/dist/gateway/ui/index.html +209 -0
- package/dist/gateway/ui/styles.css +208 -0
- package/dist/hovclaw.js +5250 -0
- package/dist/index.js +5742 -0
- package/dist/login-Ca1_XRup.js +47 -0
- package/dist/oauth-6sxOTr3f.js +62 -0
- package/dist/onboard-Cgbgh2Jn.js +1314 -0
- package/dist/src-D_mIwpeq.js +2032 -0
- package/hovclaw.mjs +37 -0
- package/package.json +64 -0
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 };
|