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.
@@ -1,5 +1,5 @@
1
- import { I as saveCredentials, M as loadCredentials } from "./hovclaw.js";
2
- import { n as runOAuthLogin, t as SUPPORTED_OAUTH_PROVIDERS } from "./oauth-6sxOTr3f.js";
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 { D as getDefaultFileConfig, E as getCredentialsPath, F as saveConfigFile, I as saveCredentials, M as loadCredentials, O as getHovclawHome, S as config, T as getConfigPath, j as loadConfig, k as hasConfigFile, l as ensureWorkspaceBootstrapForConfig, p as listAvailableSkills } from "./hovclaw.js";
2
- import { n as runOAuthLogin } from "./oauth-6sxOTr3f.js";
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
- const raw = await fs.readFile(agentFilePath, "utf8");
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 parsed = await readAgentJson(agentPath);
33
- const previousSkills = Array.isArray(parsed.skills) ? parsed.skills.filter((value) => typeof value === "string") : [];
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
- ...parsed,
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("No installed skills found in skills/*/SKILL.md; keeping existing main agent skills.");
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("Select skills for agents/main/agent.json", availableSkills.map((skill) => ({
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 askOptional("Telegram webhook secret (optional but recommended)", runtimeState.config.channels.telegram.webhook.secret);
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 };