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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as runOAuthLogin, t as SUPPORTED_OAUTH_PROVIDERS } from "./oauth-
|
|
1
|
+
import { H as saveCredentials, R as loadCredentials } from "./hovclaw.js";
|
|
2
|
+
import { n as runOAuthLogin, t as SUPPORTED_OAUTH_PROVIDERS } from "./oauth-CQsXP0kP.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { cancel, intro, isCancel, note, outro, select } from "@clack/prompts";
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as runOAuthLogin } from "./oauth-
|
|
1
|
+
import { F as hasConfigFile, H as saveCredentials, L as loadConfig, M as getCredentialsPath, N as getDefaultFileConfig, O as config, P as getHovclawHome, R as loadCredentials, V as saveConfigFile, j as getConfigPath, m as ensureWorkspaceBootstrapForConfig, y as listAvailableSkills } from "./hovclaw.js";
|
|
2
|
+
import { n as runOAuthLogin } from "./oauth-CQsXP0kP.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, password, select, text } from "@clack/prompts";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import fs from "node:fs/promises";
|
|
7
8
|
|
|
8
9
|
//#region src/cli/onboard/agent-skills.ts
|
|
9
10
|
const DEFAULT_ONBOARD_MAIN_SKILLS = ["web-search"];
|
|
10
|
-
async function readAgentJson(agentFilePath) {
|
|
11
|
-
|
|
11
|
+
async function readAgentJson(agentFilePath, options = {}) {
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = await fs.readFile(agentFilePath, "utf8");
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const code = error.code;
|
|
17
|
+
if (options.allowMissing && code === "ENOENT") return null;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
12
20
|
const parsed = JSON.parse(raw);
|
|
13
21
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("main agent config must be a JSON object.");
|
|
14
22
|
return parsed;
|
|
15
23
|
}
|
|
16
24
|
async function loadMainAgentSkills(agentsDir) {
|
|
17
|
-
const parsed = await readAgentJson(path.join(agentsDir, "main", "agent.json"));
|
|
25
|
+
const parsed = await readAgentJson(path.join(agentsDir, "main", "agent.json"), { allowMissing: true });
|
|
26
|
+
if (!parsed) return [];
|
|
18
27
|
return Array.isArray(parsed.skills) ? parsed.skills.filter((value) => typeof value === "string") : [];
|
|
19
28
|
}
|
|
20
29
|
function resolveInitialMainAgentSkills(params) {
|
|
@@ -29,8 +38,8 @@ async function updateMainAgentSkills(agentsDir, selectedSkills, availableSkills)
|
|
|
29
38
|
const unknown = selectedSkills.filter((skill) => !validSkillSet.has(skill));
|
|
30
39
|
if (unknown.length > 0) throw new Error(`Unknown skills selected: ${unknown.join(", ")}`);
|
|
31
40
|
const agentPath = path.join(agentsDir, "main", "agent.json");
|
|
32
|
-
const
|
|
33
|
-
const previousSkills = Array.isArray(
|
|
41
|
+
const existingAgent = await readAgentJson(agentPath, { allowMissing: true }) ?? {};
|
|
42
|
+
const previousSkills = Array.isArray(existingAgent.skills) ? existingAgent.skills.filter((value) => typeof value === "string") : [];
|
|
34
43
|
const nextSkills = Array.from(new Set(selectedSkills));
|
|
35
44
|
if (!(previousSkills.length !== nextSkills.length || previousSkills.some((skill, index) => skill !== nextSkills[index]))) return {
|
|
36
45
|
changed: false,
|
|
@@ -39,9 +48,10 @@ async function updateMainAgentSkills(agentsDir, selectedSkills, availableSkills)
|
|
|
39
48
|
path: agentPath
|
|
40
49
|
};
|
|
41
50
|
const updated = {
|
|
42
|
-
...
|
|
51
|
+
...existingAgent,
|
|
43
52
|
skills: nextSkills
|
|
44
53
|
};
|
|
54
|
+
await fs.mkdir(path.dirname(agentPath), { recursive: true });
|
|
45
55
|
await fs.writeFile(agentPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
46
56
|
return {
|
|
47
57
|
changed: true,
|
|
@@ -154,6 +164,7 @@ function applySectionEdits(base, draft, selectedSections, resetSections, default
|
|
|
154
164
|
agents: choose("assistant", base.agents, draft.agents, defaults.agents),
|
|
155
165
|
bindings: choose("channels", base.bindings, draft.bindings, defaults.bindings),
|
|
156
166
|
models: choose("models", base.models, draft.models, defaults.models),
|
|
167
|
+
commands: choose("runtime", base.commands, draft.commands, defaults.commands),
|
|
157
168
|
runtime: choose("runtime", base.runtime, draft.runtime, defaults.runtime),
|
|
158
169
|
channels: choose("channels", base.channels, draft.channels, defaults.channels),
|
|
159
170
|
gateway: choose("runtime", base.gateway, draft.gateway, defaults.gateway),
|
|
@@ -694,6 +705,18 @@ function asFileConfig(appConfig) {
|
|
|
694
705
|
aliases: appConfig.models.aliases,
|
|
695
706
|
allowlist: appConfig.models.allowlist
|
|
696
707
|
},
|
|
708
|
+
commands: {
|
|
709
|
+
native: appConfig.commands.native,
|
|
710
|
+
nativeSkills: appConfig.commands.nativeSkills,
|
|
711
|
+
defaultThinkingLevel: appConfig.commands.defaultThinkingLevel,
|
|
712
|
+
text: appConfig.commands.text,
|
|
713
|
+
config: appConfig.commands.config,
|
|
714
|
+
debug: appConfig.commands.debug,
|
|
715
|
+
bash: appConfig.commands.bash,
|
|
716
|
+
restart: appConfig.commands.restart,
|
|
717
|
+
useAccessGroups: appConfig.commands.useAccessGroups,
|
|
718
|
+
allowFrom: appConfig.commands.allowFrom
|
|
719
|
+
},
|
|
697
720
|
runtime: {
|
|
698
721
|
mode: appConfig.runtime.mode,
|
|
699
722
|
containerImage: appConfig.runtime.containerImage,
|
|
@@ -703,7 +726,8 @@ function asFileConfig(appConfig) {
|
|
|
703
726
|
maxOutputBytes: appConfig.runtime.maxOutputBytes,
|
|
704
727
|
allowedReadRoots: [...appConfig.runtime.allowedReadRoots],
|
|
705
728
|
allowedWriteRoots: [...appConfig.runtime.allowedWriteRoots],
|
|
706
|
-
allowedCommandPrefixes: [...appConfig.runtime.allowedCommandPrefixes]
|
|
729
|
+
allowedCommandPrefixes: [...appConfig.runtime.allowedCommandPrefixes],
|
|
730
|
+
tools: { bashEnabled: appConfig.runtime.tools.bashEnabled }
|
|
707
731
|
},
|
|
708
732
|
channels: {
|
|
709
733
|
discord: {
|
|
@@ -737,7 +761,9 @@ function asFileConfig(appConfig) {
|
|
|
737
761
|
},
|
|
738
762
|
auth: {
|
|
739
763
|
token: appConfig.gateway.auth.token,
|
|
740
|
-
password: appConfig.gateway.auth.password
|
|
764
|
+
password: appConfig.gateway.auth.password,
|
|
765
|
+
allowUnauthenticated: appConfig.gateway.auth.allowUnauthenticated,
|
|
766
|
+
allowedOrigins: [...appConfig.gateway.auth.allowedOrigins]
|
|
741
767
|
},
|
|
742
768
|
remote: {
|
|
743
769
|
url: appConfig.gateway.remote.url,
|
|
@@ -1162,9 +1188,9 @@ async function main() {
|
|
|
1162
1188
|
}
|
|
1163
1189
|
if (stepId === "main-skills") {
|
|
1164
1190
|
stats.sectionsVisited.add("skills");
|
|
1165
|
-
if (availableSkills.length === 0) log.warn(
|
|
1191
|
+
if (availableSkills.length === 0) log.warn(`No installed skills found in ${path.join(config.skillsDir, "*/SKILL.md")}; keeping existing main agent skills.`);
|
|
1166
1192
|
else {
|
|
1167
|
-
selectedSkills = await askMultiSelect(
|
|
1193
|
+
selectedSkills = await askMultiSelect(`Select skills for ${path.join(config.agentsDir, "main", "agent.json")}`, availableSkills.map((skill) => ({
|
|
1168
1194
|
value: skill,
|
|
1169
1195
|
label: skill
|
|
1170
1196
|
})), initialSelectedSkills, false);
|
|
@@ -1192,7 +1218,7 @@ async function main() {
|
|
|
1192
1218
|
const webhookPortRaw = await askText("Telegram webhook local port", String(runtimeState.config.channels.telegram.webhook.port), "8788");
|
|
1193
1219
|
const parsedWebhookPort = Number.parseInt(webhookPortRaw, 10);
|
|
1194
1220
|
runtimeState.config.channels.telegram.webhook.port = Number.isFinite(parsedWebhookPort) && parsedWebhookPort > 0 ? parsedWebhookPort : runtimeState.config.channels.telegram.webhook.port;
|
|
1195
|
-
runtimeState.config.channels.telegram.webhook.secret = await
|
|
1221
|
+
runtimeState.config.channels.telegram.webhook.secret = await askText("Telegram webhook secret", runtimeState.config.channels.telegram.webhook.secret, "required when webhook mode is enabled");
|
|
1196
1222
|
}
|
|
1197
1223
|
continue;
|
|
1198
1224
|
}
|
|
@@ -1235,6 +1261,10 @@ async function main() {
|
|
|
1235
1261
|
await configureCredentials(runtimeState, stats, false);
|
|
1236
1262
|
}
|
|
1237
1263
|
const finalConfig = hasExisting ? applySectionEdits(baseConfig, runtimeState.config, selectedSections, resetSections, defaults) : runtimeState.config;
|
|
1264
|
+
if (finalConfig.gateway.enabled && !finalConfig.gateway.auth.allowUnauthenticated && !finalConfig.gateway.auth.token.trim() && !finalConfig.gateway.auth.password.trim()) {
|
|
1265
|
+
finalConfig.gateway.auth.token = randomBytes(24).toString("base64url");
|
|
1266
|
+
log.info("Generated a gateway auth token because strict gateway auth is enabled.");
|
|
1267
|
+
}
|
|
1238
1268
|
const beforeProviders = providerNamesFromCredentials(baseCredentials);
|
|
1239
1269
|
const afterProviders = providerNamesFromCredentials(runtimeState.credentials);
|
|
1240
1270
|
note(summaryLines(baseConfig, finalConfig, beforeProviders, afterProviders, baseSkills, runtimeState.selectedSkills).join("\n"), "Review before write");
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { L as loadConfig, M as getCredentialsPath, P as getHovclawHome, j as getConfigPath, n as stopDaemon } from "./hovclaw.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { cancel, confirm, isCancel, log, select } from "@clack/prompts";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/reset.ts
|
|
8
|
+
const defaultResetDeps = { stopService: async (env) => stopDaemon(env) };
|
|
9
|
+
function parseResetScope(raw) {
|
|
10
|
+
if (!raw) return null;
|
|
11
|
+
if (raw === "config" || raw === "config+creds+sessions" || raw === "full") return raw;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function collectWorkspaceDirs(loadedConfig) {
|
|
15
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
16
|
+
const fallback = loadedConfig.agents.defaults.workspace;
|
|
17
|
+
dirs.add(fallback);
|
|
18
|
+
for (const agent of loadedConfig.agents.list) dirs.add(agent.workspace?.trim() || fallback);
|
|
19
|
+
return Array.from(dirs);
|
|
20
|
+
}
|
|
21
|
+
function addDbArtifacts(paths, baseDir) {
|
|
22
|
+
paths.add(path.join(baseDir, "hovclaw.db"));
|
|
23
|
+
paths.add(path.join(baseDir, "hovclaw.db-shm"));
|
|
24
|
+
paths.add(path.join(baseDir, "hovclaw.db-wal"));
|
|
25
|
+
}
|
|
26
|
+
function removeIfNested(paths, target) {
|
|
27
|
+
const resolvedTarget = path.resolve(target);
|
|
28
|
+
for (const existing of paths) {
|
|
29
|
+
const resolvedExisting = path.resolve(existing);
|
|
30
|
+
if (resolvedTarget === resolvedExisting || resolvedTarget.startsWith(`${resolvedExisting}${path.sep}`)) return;
|
|
31
|
+
}
|
|
32
|
+
paths.add(resolvedTarget);
|
|
33
|
+
}
|
|
34
|
+
function assertSafeRemovalPath(targetPath) {
|
|
35
|
+
const resolved = path.resolve(targetPath);
|
|
36
|
+
if (resolved === path.parse(resolved).root) throw new Error(`Refusing to reset root path: ${resolved}`);
|
|
37
|
+
if (resolved === path.resolve(os.homedir())) throw new Error(`Refusing to reset user home path: ${resolved}`);
|
|
38
|
+
}
|
|
39
|
+
function collectResetPaths(scope, env = process.env) {
|
|
40
|
+
const paths = /* @__PURE__ */ new Set();
|
|
41
|
+
const hovclawHome = getHovclawHome(env);
|
|
42
|
+
const configPath = getConfigPath(env);
|
|
43
|
+
const credentialsPath = getCredentialsPath(env);
|
|
44
|
+
let loadedConfig = null;
|
|
45
|
+
try {
|
|
46
|
+
loadedConfig = loadConfig(env);
|
|
47
|
+
} catch {
|
|
48
|
+
loadedConfig = null;
|
|
49
|
+
}
|
|
50
|
+
if (scope === "config") paths.add(path.resolve(configPath));
|
|
51
|
+
if (scope === "config+creds+sessions") {
|
|
52
|
+
paths.add(path.resolve(configPath));
|
|
53
|
+
paths.add(path.resolve(credentialsPath));
|
|
54
|
+
addDbArtifacts(paths, hovclawHome);
|
|
55
|
+
const storeDir = path.resolve(loadedConfig?.storeDir ?? path.join(hovclawHome, "store"));
|
|
56
|
+
addDbArtifacts(paths, storeDir);
|
|
57
|
+
paths.add(path.resolve(path.join(storeDir, "telegram-pairing.json")));
|
|
58
|
+
}
|
|
59
|
+
if (scope === "full") {
|
|
60
|
+
removeIfNested(paths, hovclawHome);
|
|
61
|
+
if (loadedConfig) {
|
|
62
|
+
removeIfNested(paths, loadedConfig.storeDir);
|
|
63
|
+
removeIfNested(paths, loadedConfig.dataDir);
|
|
64
|
+
for (const workspaceDir of collectWorkspaceDirs(loadedConfig)) removeIfNested(paths, workspaceDir);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Array.from(paths);
|
|
68
|
+
}
|
|
69
|
+
async function chooseScopeInteractively() {
|
|
70
|
+
const selected = await select({
|
|
71
|
+
message: "Reset scope",
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
value: "config",
|
|
75
|
+
label: "Config only",
|
|
76
|
+
hint: "~/.hovclaw/config.json"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: "config+creds+sessions",
|
|
80
|
+
label: "Config + credentials + sessions",
|
|
81
|
+
hint: "Keeps agents/shared skills/workspace definitions"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: "full",
|
|
85
|
+
label: "Full reset",
|
|
86
|
+
hint: "Config + credentials + sessions + workspace"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
initialValue: "config+creds+sessions"
|
|
90
|
+
});
|
|
91
|
+
if (isCancel(selected)) {
|
|
92
|
+
cancel("Reset cancelled.");
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return selected;
|
|
96
|
+
}
|
|
97
|
+
async function confirmProceed(scope) {
|
|
98
|
+
const accepted = await confirm({
|
|
99
|
+
message: `Proceed with ${scope} reset?`,
|
|
100
|
+
initialValue: false
|
|
101
|
+
});
|
|
102
|
+
if (isCancel(accepted)) {
|
|
103
|
+
cancel("Reset cancelled.");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return accepted;
|
|
107
|
+
}
|
|
108
|
+
function removePath(targetPath) {
|
|
109
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
110
|
+
fs.rmSync(targetPath, {
|
|
111
|
+
recursive: true,
|
|
112
|
+
force: true
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
async function runResetCommand(options, env = process.env, deps = defaultResetDeps) {
|
|
117
|
+
const nonInteractive = Boolean(options.nonInteractive);
|
|
118
|
+
const yes = Boolean(options.yes);
|
|
119
|
+
const dryRun = Boolean(options.dryRun);
|
|
120
|
+
if (nonInteractive && !yes) throw new Error("Non-interactive mode requires --yes.");
|
|
121
|
+
let scope = parseResetScope(options.scope);
|
|
122
|
+
if (!scope) {
|
|
123
|
+
if (options.scope) throw new Error("Invalid --scope. Expected \"config\", \"config+creds+sessions\", or \"full\".");
|
|
124
|
+
if (nonInteractive) throw new Error("Non-interactive mode requires --scope.");
|
|
125
|
+
scope = await chooseScopeInteractively();
|
|
126
|
+
if (!scope) throw new Error("reset_cancelled");
|
|
127
|
+
}
|
|
128
|
+
if (!nonInteractive && !yes) {
|
|
129
|
+
if (!await confirmProceed(scope)) throw new Error("reset_cancelled");
|
|
130
|
+
}
|
|
131
|
+
if (scope !== "config") if (dryRun) log.info("[dry-run] stop daemon service");
|
|
132
|
+
else await deps.stopService(env);
|
|
133
|
+
const paths = collectResetPaths(scope, env);
|
|
134
|
+
const removed = [];
|
|
135
|
+
const missing = [];
|
|
136
|
+
for (const targetPath of paths) {
|
|
137
|
+
assertSafeRemovalPath(targetPath);
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
if (fs.existsSync(targetPath)) removed.push(targetPath);
|
|
140
|
+
else missing.push(targetPath);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (removePath(targetPath)) removed.push(targetPath);
|
|
144
|
+
else missing.push(targetPath);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
scope,
|
|
148
|
+
dryRun,
|
|
149
|
+
removed,
|
|
150
|
+
missing
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function renderResetResult(result) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push(`Scope: ${result.scope}`);
|
|
156
|
+
lines.push(`Mode: ${result.dryRun ? "dry-run" : "apply"}`);
|
|
157
|
+
lines.push(`Removed: ${result.removed.length}`);
|
|
158
|
+
if (result.removed.length > 0) lines.push(...result.removed.map((entry) => ` - ${entry}`));
|
|
159
|
+
lines.push(`Missing/skipped: ${result.missing.length}`);
|
|
160
|
+
if (result.missing.length > 0) lines.push(...result.missing.map((entry) => ` - ${entry}`));
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { renderResetResult, runResetCommand };
|