hovclaw 0.1.0 → 0.1.2
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/README.md +32 -2
- package/dist/{doctor-I8YVuapp.js → doctor-D52M80De.js} +42 -4
- package/dist/gateway/ui/app.js +3 -3
- package/dist/gateway/ui/credentials.d.ts +1 -2
- package/dist/gateway/ui/credentials.js +5 -7
- package/dist/gateway/ui/index.html +177 -204
- package/dist/gateway/ui/styles.css +495 -101
- package/dist/hovclaw.js +1049 -236
- package/dist/index.js +2100 -504
- package/dist/{login-Ca1_XRup.js → login-BwvBMKdz.js} +2 -2
- package/dist/{onboard-Cgbgh2Jn.js → onboard-DL6VDf50.js} +43 -13
- package/dist/reset-BJUhrojJ.js +165 -0
- package/dist/{src-D_mIwpeq.js → src-Y6AqidKn.js} +1087 -259
- package/package.json +4 -1
- /package/dist/{oauth-6sxOTr3f.js → oauth-CQsXP0kP.js} +0 -0
package/README.md
CHANGED
|
@@ -32,11 +32,15 @@ HOVClaw is built on a simple principle: **run your own AI agent infrastructure,
|
|
|
32
32
|
- **Multi-account Telegram** - Multiple bot accounts with per-account status and logout
|
|
33
33
|
- **Text mode controls** - Per-channel `plain|markdown` rendering mode (default `plain`)
|
|
34
34
|
- **Policy layer** - `dmPolicy`, `groupPolicy`, per-group/per-topic overrides, pairing flow
|
|
35
|
+
- **Native Telegram commands** - Auto-registered slash command menu (including skill aliases)
|
|
36
|
+
- **Thinking controls** - `/think <level> <task>` plus persisted default via `commands.defaultThinkingLevel`
|
|
35
37
|
|
|
36
38
|
### Gateway & Control Plane
|
|
37
39
|
|
|
38
40
|
- **WebSocket protocol v3** - Request/response/event frames with 21 methods
|
|
39
41
|
- **Built-in web UI** - Connection, health, channels, sessions, and chat in one page
|
|
42
|
+
- **Fail-closed auth defaults** - Gateway auth is required unless explicitly opting into insecure mode
|
|
43
|
+
- **Origin-aware WS policy** - Browser `Origin` headers must be same-origin or allowlisted
|
|
40
44
|
- **LaunchAgent integration** - `hovclaw gateway install/start/stop` for macOS background service
|
|
41
45
|
- **Programmatic access** - `hovclaw gateway call <method>` for scripting
|
|
42
46
|
|
|
@@ -46,11 +50,12 @@ HOVClaw is built on a simple principle: **run your own AI agent infrastructure,
|
|
|
46
50
|
- **Multi-provider models** - Anthropic, Google, OpenAI, OpenRouter via `@mariozechner/pi-ai`
|
|
47
51
|
- **Model routing** - Per-target model slots (interactive, discord, cron) with fallback policy
|
|
48
52
|
- **Workspace-first tools** - Relative file tool paths resolve from agent workspace
|
|
53
|
+
- **Least-privilege tools** - Bash tool disabled by default (`runtime.tools.bashEnabled=false`)
|
|
49
54
|
- **Session persistence** - SQLite-backed sessions, messages, agent state, and usage tracking
|
|
50
55
|
|
|
51
56
|
### Scheduling & Automation
|
|
52
57
|
|
|
53
|
-
- **Cron jobs** -
|
|
58
|
+
- **Cron jobs** - `~/.hovclaw/agents/*/cron.json` with configurable schedules and timezone support
|
|
54
59
|
- **Channel notifications** - Scheduled job results delivered to Telegram or Discord
|
|
55
60
|
- **Concurrent execution** - Configurable max concurrent jobs
|
|
56
61
|
|
|
@@ -105,17 +110,33 @@ hovclaw onboard
|
|
|
105
110
|
The wizard handles channel tokens, model provider credentials (via OAuth or API key),
|
|
106
111
|
and agent configuration. All settings are saved to `~/.hovclaw/config.json`.
|
|
107
112
|
|
|
113
|
+
Security defaults in this release are intentionally strict:
|
|
114
|
+
- gateway start fails if `gateway.enabled=true` and neither `gateway.auth.token` nor `gateway.auth.password` is set (unless `gateway.auth.allowUnauthenticated=true`)
|
|
115
|
+
- Telegram webhook mode requires a non-empty webhook secret
|
|
116
|
+
- bash runtime tool is opt-in only via `runtime.tools.bashEnabled=true`
|
|
117
|
+
|
|
118
|
+
Agent and skill definitions are loaded from:
|
|
119
|
+
- `~/.hovclaw/agents/<name>/agent.json` (`CLAUDE.md`, `cron.json`)
|
|
120
|
+
- missing `~/.hovclaw/agents/main/agent.json` is auto-bootstrapped with a minimal scaffold (`name`, `skills`)
|
|
121
|
+
- `~/.agents/skills/<name>/SKILL.md`
|
|
122
|
+
- legacy `~/.hovclaw/skills` content is copied once when shared skills are empty
|
|
123
|
+
|
|
124
|
+
Runtime state is written under:
|
|
125
|
+
- `~/.hovclaw/store` (SQLite `hovclaw.db`, pairing store)
|
|
126
|
+
- `~/.hovclaw/data`
|
|
127
|
+
|
|
108
128
|
### Workspace Defaults and Bootstrap
|
|
109
129
|
|
|
110
130
|
- Default workspace: `~/.hovclaw/workspace`
|
|
111
131
|
- Blank agent workspace values resolve to the same default workspace
|
|
112
132
|
- On startup and onboarding, HOVClaw auto-creates missing workspace files:
|
|
113
133
|
- `AGENTS.md`
|
|
134
|
+
- `SOUL.md`
|
|
114
135
|
- `IDENTITY.md`
|
|
115
136
|
- `USER.md`
|
|
116
137
|
- `BOOTSTRAP.md` (only when the workspace is effectively empty)
|
|
117
138
|
- Workspace files are appended to the system prompt in this order:
|
|
118
|
-
- `AGENTS.md` -> `IDENTITY.md` -> `USER.md` -> `BOOTSTRAP.md`
|
|
139
|
+
- `AGENTS.md` -> `SOUL.md` -> `IDENTITY.md` -> `USER.md` -> `BOOTSTRAP.md`
|
|
119
140
|
- capped at 4,000 chars per file and 12,000 chars total
|
|
120
141
|
|
|
121
142
|
### Config Structure
|
|
@@ -126,6 +147,7 @@ and agent configuration. All settings are saved to `~/.hovclaw/config.json`.
|
|
|
126
147
|
| `agents` | Agent definitions and defaults |
|
|
127
148
|
| `bindings` | Inbound message routing rules |
|
|
128
149
|
| `models` | Model slots, fallback policy, aliases |
|
|
150
|
+
| `commands` | Native command behavior, slash registration, authorization |
|
|
129
151
|
| `runtime` | Execution mode, timeouts, allowed paths/commands |
|
|
130
152
|
| `channels` | Telegram and Discord channel config |
|
|
131
153
|
| `gateway` | Gateway host, port, auth, web UI settings |
|
|
@@ -200,6 +222,7 @@ Environment overrides are supported for most fields. See [docs/config-reference.
|
|
|
200
222
|
# Setup
|
|
201
223
|
hovclaw onboard
|
|
202
224
|
hovclaw login [provider]
|
|
225
|
+
hovclaw reset [--scope config|config+creds+sessions|full] [--yes] [--non-interactive] [--dry-run] [--json]
|
|
203
226
|
hovclaw doctor [--fix] [--deep] [--json]
|
|
204
227
|
hovclaw status [--json]
|
|
205
228
|
|
|
@@ -209,9 +232,16 @@ hovclaw message send --channel telegram --to <chat_id> --message "hello"
|
|
|
209
232
|
# Channel management
|
|
210
233
|
hovclaw channels list|status|add|remove|login|logout [--account <id>] [--json]
|
|
211
234
|
|
|
235
|
+
# Pairing management
|
|
236
|
+
hovclaw pairing approve telegram <code>
|
|
237
|
+
hovclaw pairing approve --channel telegram [--account <id>] <code> [--json]
|
|
238
|
+
|
|
212
239
|
# Model management
|
|
213
240
|
hovclaw models list|status|set [--model <ref>] [--target <slot>] [--json]
|
|
214
241
|
|
|
242
|
+
# Skill management
|
|
243
|
+
hovclaw skills list|info|check|init [--json]
|
|
244
|
+
|
|
215
245
|
# Gateway lifecycle
|
|
216
246
|
hovclaw gateway run
|
|
217
247
|
hovclaw gateway install|uninstall|start|stop|restart [--json]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as ensureConfigFromLegacyEnv, F as hasConfigFile, H as saveCredentials, I as hasCredentialsFile, L as loadConfig, M as getCredentialsPath, P as getHovclawHome, R as loadCredentials, V as saveConfigFile, j as getConfigPath, k as detectLegacyEnvConfig, z as loadFileConfig } from "./hovclaw.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { intro, log, outro } from "@clack/prompts";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
|
|
8
9
|
//#region src/cli/doctor.ts
|
|
@@ -61,6 +62,11 @@ function checkDockerAvailability() {
|
|
|
61
62
|
detail: result.stderr?.trim() || result.stdout?.trim() || "docker command failed. Install/start Docker Desktop and retry."
|
|
62
63
|
};
|
|
63
64
|
}
|
|
65
|
+
function mutateFileConfig(env, mutate) {
|
|
66
|
+
const fileConfig = loadFileConfig(env);
|
|
67
|
+
mutate(fileConfig);
|
|
68
|
+
saveConfigFile(fileConfig, env);
|
|
69
|
+
}
|
|
64
70
|
function parseDoctorArgs(args) {
|
|
65
71
|
const options = {
|
|
66
72
|
repair: false,
|
|
@@ -125,10 +131,35 @@ function runDoctorChecks(options, env = process.env) {
|
|
|
125
131
|
else addFinding(findings, "channels-enabled", "pass", "At least one channel enabled", "OK");
|
|
126
132
|
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
133
|
else addFinding(findings, "discord-token", "pass", "Discord token configured", "OK");
|
|
128
|
-
if (loadedConfig.channels.telegram.enabled)
|
|
129
|
-
|
|
134
|
+
if (loadedConfig.channels.telegram.enabled) {
|
|
135
|
+
if (!loadedConfig.channels.telegram.botToken.trim()) addFinding(findings, "telegram-token", "fail", "Telegram enabled without token", "Set channels.telegram.botToken via onboarding.");
|
|
136
|
+
else addFinding(findings, "telegram-token", "pass", "Telegram token configured", "OK");
|
|
137
|
+
const webhookAccountsMissingSecret = Object.entries(loadedConfig.channels.telegram.accounts).filter(([, account]) => account.webhook.enabled && !account.webhook.secret.trim()).map(([accountId]) => accountId);
|
|
138
|
+
if (webhookAccountsMissingSecret.length > 0) if (options.repair) {
|
|
139
|
+
mutateFileConfig(env, (fileConfig) => {
|
|
140
|
+
for (const accountId of webhookAccountsMissingSecret) {
|
|
141
|
+
const account = fileConfig.channels.telegram.accounts[accountId];
|
|
142
|
+
if (!account || !account.webhook.enabled) continue;
|
|
143
|
+
account.webhook.secret = randomBytes(24).toString("base64url");
|
|
144
|
+
if (accountId === fileConfig.channels.telegram.defaultAccountId) fileConfig.channels.telegram.webhook.secret = account.webhook.secret;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
addFinding(findings, "telegram-webhook-secret", "repair", "Generated missing Telegram webhook secret(s)", webhookAccountsMissingSecret.join(", "));
|
|
148
|
+
} else addFinding(findings, "telegram-webhook-secret", "fail", "Telegram webhook secret missing", `Accounts: ${webhookAccountsMissingSecret.join(", ")}`);
|
|
149
|
+
else addFinding(findings, "telegram-webhook-secret", "pass", "Telegram webhook secret policy", "OK");
|
|
150
|
+
}
|
|
130
151
|
if (!loadedConfig.gateway.enabled) addFinding(findings, "gateway-enabled", "warn", "Gateway is disabled", "Enable gateway for OpenClaw/ClawHub compatibility.");
|
|
131
152
|
else addFinding(findings, "gateway-enabled", "pass", "Gateway enabled", "OK");
|
|
153
|
+
if (loadedConfig.gateway.auth.allowUnauthenticated) addFinding(findings, "gateway-auth-mode", "warn", "Gateway unauthenticated mode is enabled", "Set gateway.auth.allowUnauthenticated=false for secure deployments.");
|
|
154
|
+
else if (!loadedConfig.gateway.auth.token.trim() && !loadedConfig.gateway.auth.password.trim()) if (options.repair) {
|
|
155
|
+
const generatedToken = randomBytes(24).toString("base64url");
|
|
156
|
+
mutateFileConfig(env, (fileConfig) => {
|
|
157
|
+
fileConfig.gateway.auth.allowUnauthenticated = false;
|
|
158
|
+
fileConfig.gateway.auth.token = generatedToken;
|
|
159
|
+
});
|
|
160
|
+
addFinding(findings, "gateway-auth-required", "repair", "Generated gateway auth token", "Set in gateway.auth.token to satisfy strict auth mode.");
|
|
161
|
+
} else addFinding(findings, "gateway-auth-required", "fail", "Gateway auth secrets are missing", "Set gateway.auth.token or gateway.auth.password, or explicitly enable allowUnauthenticated.");
|
|
162
|
+
else addFinding(findings, "gateway-auth-required", "pass", "Gateway auth configured", "OK");
|
|
132
163
|
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
164
|
else addFinding(findings, "gateway-bind", "pass", "Gateway bind configured", `${loadedConfig.gateway.host}:${loadedConfig.gateway.port}`);
|
|
134
165
|
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.");
|
|
@@ -136,6 +167,13 @@ function runDoctorChecks(options, env = process.env) {
|
|
|
136
167
|
else addFinding(findings, "runtime-read-roots", "pass", "Read roots configured", `${loadedConfig.runtime.allowedReadRoots.length} root(s)`);
|
|
137
168
|
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
169
|
else addFinding(findings, "runtime-write-roots", "pass", "Write roots configured", `${loadedConfig.runtime.allowedWriteRoots.length} root(s)`);
|
|
170
|
+
if (loadedConfig.runtime.tools.bashEnabled) if (options.repair) {
|
|
171
|
+
mutateFileConfig(env, (fileConfig) => {
|
|
172
|
+
fileConfig.runtime.tools.bashEnabled = false;
|
|
173
|
+
});
|
|
174
|
+
addFinding(findings, "runtime-bash-enabled", "repair", "Disabled bash tool by default", "Set runtime.tools.bashEnabled=true only for trusted environments.");
|
|
175
|
+
} else addFinding(findings, "runtime-bash-enabled", "warn", "Bash tool is enabled", "High-risk surface. Prefer runtime.tools.bashEnabled=false.");
|
|
176
|
+
else addFinding(findings, "runtime-bash-enabled", "pass", "Bash tool disabled", "OK");
|
|
139
177
|
const unreadableWriteRoots = loadedConfig.runtime.allowedWriteRoots.filter((writeRoot) => !pathInsideAnyRoot(writeRoot, loadedConfig.runtime.allowedReadRoots));
|
|
140
178
|
if (unreadableWriteRoots.length > 0) addFinding(findings, "runtime-write-without-read", "warn", "Some write roots are not in read roots", unreadableWriteRoots.join(", "));
|
|
141
179
|
for (const writeRoot of loadedConfig.runtime.allowedWriteRoots) if (!fs.existsSync(writeRoot)) if (options.repair) {
|
|
@@ -157,7 +195,7 @@ function runDoctorChecks(options, env = process.env) {
|
|
|
157
195
|
const missingSkills = parsed.skills.filter((entry) => typeof entry === "string").filter((skill) => !fs.existsSync(path.join(loadedConfig.skillsDir, skill, "SKILL.md")));
|
|
158
196
|
if (missingSkills.length > 0) addFinding(findings, "main-agent-skills", "fail", "Main agent references missing skills", missingSkills.join(", "));
|
|
159
197
|
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",
|
|
198
|
+
} else addFinding(findings, "main-agent-skills", "warn", "Main agent skills list is missing or invalid", `Expected skills: string[] in ${mainAgentPath}`);
|
|
161
199
|
} catch (error) {
|
|
162
200
|
addFinding(findings, "main-agent-parse", "fail", "Main agent config is invalid JSON", error instanceof Error ? error.message : String(error));
|
|
163
201
|
}
|
package/dist/gateway/ui/app.js
CHANGED
|
@@ -194,7 +194,7 @@ function appendChatMessage(role, text) {
|
|
|
194
194
|
function showChatEmpty(text) {
|
|
195
195
|
chatOutput.innerHTML = "";
|
|
196
196
|
const p = document.createElement("p");
|
|
197
|
-
p.className = "
|
|
197
|
+
p.className = "chat-empty";
|
|
198
198
|
p.textContent = text;
|
|
199
199
|
chatOutput.appendChild(p);
|
|
200
200
|
}
|
|
@@ -372,7 +372,7 @@ async function connectGateway(event) {
|
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
connected = true;
|
|
375
|
-
saveStoredConnection({ gatewayUrl
|
|
375
|
+
saveStoredConnection({ gatewayUrl });
|
|
376
376
|
const connId = hello?.server?.connId || "ok";
|
|
377
377
|
setConnectionState(true, `Connected (${connId})`);
|
|
378
378
|
logLine(`Connected to ${gatewayUrl}`);
|
|
@@ -501,7 +501,7 @@ function wireEvents() {
|
|
|
501
501
|
function bootstrap() {
|
|
502
502
|
const stored = loadStoredConnection();
|
|
503
503
|
gatewayUrlInput.value = stored.gatewayUrl || defaultGatewayUrl();
|
|
504
|
-
tokenInput.value =
|
|
504
|
+
tokenInput.value = "";
|
|
505
505
|
passwordInput.value = "";
|
|
506
506
|
setConnectionState(false, "Disconnected");
|
|
507
507
|
setupPanels();
|
|
@@ -2,7 +2,6 @@ export const STORAGE_KEY: string;
|
|
|
2
2
|
|
|
3
3
|
export interface StoredConnection {
|
|
4
4
|
gatewayUrl: string;
|
|
5
|
-
token: string;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
export interface StorageLike {
|
|
@@ -13,7 +12,7 @@ export interface StorageLike {
|
|
|
13
12
|
|
|
14
13
|
export function loadStoredConnection(storage?: StorageLike | null): StoredConnection;
|
|
15
14
|
export function saveStoredConnection(
|
|
16
|
-
connection: { gatewayUrl?: string;
|
|
15
|
+
connection: { gatewayUrl?: string; [key: string]: unknown },
|
|
17
16
|
storage?: StorageLike | null,
|
|
18
17
|
): void;
|
|
19
18
|
export function clearStoredConnection(storage?: StorageLike | null): void;
|
|
@@ -17,26 +17,25 @@ function resolveStorage(storage) {
|
|
|
17
17
|
export function loadStoredConnection(storage) {
|
|
18
18
|
const targetStorage = resolveStorage(storage);
|
|
19
19
|
if (!targetStorage) {
|
|
20
|
-
return { gatewayUrl: ""
|
|
20
|
+
return { gatewayUrl: "" };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const raw = targetStorage.getItem(STORAGE_KEY);
|
|
24
24
|
if (!raw) {
|
|
25
|
-
return { gatewayUrl: ""
|
|
25
|
+
return { gatewayUrl: "" };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
try {
|
|
29
29
|
const parsed = JSON.parse(raw);
|
|
30
30
|
if (!isRecord(parsed)) {
|
|
31
|
-
return { gatewayUrl: ""
|
|
31
|
+
return { gatewayUrl: "" };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const gatewayUrl =
|
|
35
35
|
typeof parsed.gatewayUrl === "string" ? parsed.gatewayUrl : "";
|
|
36
|
-
|
|
37
|
-
return { gatewayUrl, token };
|
|
36
|
+
return { gatewayUrl };
|
|
38
37
|
} catch {
|
|
39
|
-
return { gatewayUrl: ""
|
|
38
|
+
return { gatewayUrl: "" };
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -49,7 +48,6 @@ export function saveStoredConnection(connection, storage) {
|
|
|
49
48
|
const payload = {
|
|
50
49
|
gatewayUrl:
|
|
51
50
|
typeof connection?.gatewayUrl === "string" ? connection.gatewayUrl : "",
|
|
52
|
-
token: typeof connection?.token === "string" ? connection.token : "",
|
|
53
51
|
};
|
|
54
52
|
|
|
55
53
|
targetStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|