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/dist/hovclaw.js CHANGED
@@ -7,7 +7,7 @@ import { Command } from "commander";
7
7
  import os from "node:os";
8
8
  import dotenv from "dotenv";
9
9
  import { z } from "zod";
10
- import crypto, { randomUUID } from "node:crypto";
10
+ import crypto, { randomUUID, timingSafeEqual } from "node:crypto";
11
11
  import WebSocket from "ws";
12
12
  import fs$1 from "node:fs/promises";
13
13
  import { Agent } from "@mariozechner/pi-agent-core";
@@ -23,6 +23,23 @@ import { JSDOM } from "jsdom";
23
23
  import { Readability } from "@mozilla/readability";
24
24
  import Parser from "rss-parser";
25
25
 
26
+ //#region \0rolldown/runtime.js
27
+ var __defProp = Object.defineProperty;
28
+ var __exportAll = (all, no_symbols) => {
29
+ let target = {};
30
+ for (var name in all) {
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true
34
+ });
35
+ }
36
+ if (!no_symbols) {
37
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
38
+ }
39
+ return target;
40
+ };
41
+
42
+ //#endregion
26
43
  //#region src/compat/openclaw-mirror.ts
27
44
  function resolveOpenClawHome(env = process.env) {
28
45
  const override = env.OPENCLAW_STATE_DIR?.trim();
@@ -153,6 +170,12 @@ function getOpenClawMirrorStatus(config) {
153
170
  dotenv.config();
154
171
  const PROJECT_ROOT = process.cwd();
155
172
  const DEFAULT_WORKSPACE_PATH = "~/.hovclaw/workspace";
173
+ const DEFAULT_SHARED_SKILLS_PATH = "~/.agents/skills";
174
+ const USER_CONTENT_DIRS = ["agents"];
175
+ const USER_STATE_DIRS = ["store", "data"];
176
+ const ensuredHomeContentDirs = /* @__PURE__ */ new Set();
177
+ const ensuredHomeStateDirs = /* @__PURE__ */ new Set();
178
+ const ensuredSharedSkillsDirs = /* @__PURE__ */ new Set();
156
179
  const DEFAULT_FILE_CONFIG = {
157
180
  assistant: { name: "Andy" },
158
181
  agents: {
@@ -183,6 +206,18 @@ const DEFAULT_FILE_CONFIG = {
183
206
  aliases: {},
184
207
  allowlist: []
185
208
  },
209
+ commands: {
210
+ native: "auto",
211
+ nativeSkills: "auto",
212
+ defaultThinkingLevel: "medium",
213
+ text: true,
214
+ config: false,
215
+ debug: false,
216
+ bash: false,
217
+ restart: false,
218
+ useAccessGroups: true,
219
+ allowFrom: {}
220
+ },
186
221
  runtime: {
187
222
  mode: "local",
188
223
  containerImage: "ghcr.io/mariozechner/pi-agent:latest",
@@ -206,7 +241,8 @@ const DEFAULT_FILE_CONFIG = {
206
241
  "head",
207
242
  "tail",
208
243
  "wc"
209
- ]
244
+ ],
245
+ tools: { bashEnabled: false }
210
246
  },
211
247
  channels: {
212
248
  discord: {
@@ -260,7 +296,9 @@ const DEFAULT_FILE_CONFIG = {
260
296
  },
261
297
  auth: {
262
298
  token: "",
263
- password: ""
299
+ password: "",
300
+ allowUnauthenticated: false,
301
+ allowedOrigins: []
264
302
  },
265
303
  remote: {
266
304
  url: "",
@@ -279,11 +317,36 @@ const telegramWebhookSchema = z.object({
279
317
  path: z.string().min(1),
280
318
  port: z.number().int().positive(),
281
319
  secret: z.string()
320
+ }).superRefine((value, ctx) => {
321
+ if (!value.enabled) return;
322
+ if (value.secret.trim().length > 0) return;
323
+ ctx.addIssue({
324
+ code: z.ZodIssueCode.custom,
325
+ message: "Webhook secret is required when webhook mode is enabled.",
326
+ path: ["secret"]
327
+ });
282
328
  });
283
329
  const telegramCustomCommandSchema = z.object({
284
330
  command: z.string().min(1),
285
331
  description: z.string().min(1)
286
332
  });
333
+ const commandAllowFromSchema = z.record(z.string(), z.array(z.union([z.string(), z.number()])));
334
+ const commandsConfigSchema = z.object({
335
+ native: z.union([z.boolean(), z.literal("auto")]),
336
+ nativeSkills: z.union([z.boolean(), z.literal("auto")]),
337
+ defaultThinkingLevel: z.enum([
338
+ "low",
339
+ "medium",
340
+ "high"
341
+ ]),
342
+ text: z.boolean(),
343
+ config: z.boolean(),
344
+ debug: z.boolean(),
345
+ bash: z.boolean(),
346
+ restart: z.boolean(),
347
+ useAccessGroups: z.boolean(),
348
+ allowFrom: commandAllowFromSchema
349
+ });
287
350
  const telegramTopicConfigSchema = z.object({
288
351
  enabled: z.boolean().optional(),
289
352
  requireMention: z.boolean().optional(),
@@ -343,6 +406,46 @@ const telegramAccountConfigSchema = z.object({
343
406
  ]).optional(),
344
407
  textMode: z.enum(["plain", "markdown"]).optional()
345
408
  });
409
+ const partialTelegramWebhookSchema = z.object({
410
+ enabled: z.boolean().optional(),
411
+ path: z.string().min(1).optional(),
412
+ port: z.number().int().positive().optional(),
413
+ secret: z.string().optional()
414
+ });
415
+ const partialTelegramAccountConfigSchema = z.object({
416
+ enabled: z.boolean().optional(),
417
+ name: z.string().optional(),
418
+ botToken: z.string().optional(),
419
+ webhook: partialTelegramWebhookSchema.optional(),
420
+ dmPolicy: z.enum([
421
+ "pairing",
422
+ "allowlist",
423
+ "open",
424
+ "disabled"
425
+ ]).optional(),
426
+ groupPolicy: z.enum([
427
+ "open",
428
+ "allowlist",
429
+ "disabled"
430
+ ]).optional(),
431
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
432
+ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
433
+ groups: z.record(z.string(), telegramGroupConfigSchema).optional(),
434
+ commands: z.union([z.boolean(), z.literal("auto")]).optional(),
435
+ customCommands: z.array(telegramCustomCommandSchema).optional(),
436
+ reactionNotifications: z.enum([
437
+ "off",
438
+ "own",
439
+ "all"
440
+ ]).optional(),
441
+ reactionLevel: z.enum([
442
+ "off",
443
+ "ack",
444
+ "minimal",
445
+ "extensive"
446
+ ]).optional(),
447
+ textMode: z.enum(["plain", "markdown"]).optional()
448
+ });
346
449
  const fileConfigSchema = z.object({
347
450
  assistant: z.object({ name: z.string().min(1) }),
348
451
  agents: z.object({
@@ -395,6 +498,7 @@ const fileConfigSchema = z.object({
395
498
  aliases: z.record(z.string(), z.string()),
396
499
  allowlist: z.array(z.string())
397
500
  }),
501
+ commands: commandsConfigSchema,
398
502
  runtime: z.object({
399
503
  mode: z.enum(["local", "container"]),
400
504
  containerImage: z.string().min(1),
@@ -404,7 +508,8 @@ const fileConfigSchema = z.object({
404
508
  maxOutputBytes: z.number().int().positive(),
405
509
  allowedReadRoots: z.array(z.string().min(1)),
406
510
  allowedWriteRoots: z.array(z.string().min(1)),
407
- allowedCommandPrefixes: z.array(z.string().min(1)).min(1)
511
+ allowedCommandPrefixes: z.array(z.string().min(1)).min(1),
512
+ tools: z.object({ bashEnabled: z.boolean() })
408
513
  }),
409
514
  channels: z.object({
410
515
  discord: z.object({
@@ -433,7 +538,9 @@ const fileConfigSchema = z.object({
433
538
  }),
434
539
  auth: z.object({
435
540
  token: z.string(),
436
- password: z.string()
541
+ password: z.string(),
542
+ allowUnauthenticated: z.boolean(),
543
+ allowedOrigins: z.array(z.string().min(1))
437
544
  }),
438
545
  remote: z.object({
439
546
  url: z.string(),
@@ -451,21 +558,37 @@ const partialTelegramConfigSchema = z.object({
451
558
  enabled: z.boolean().optional(),
452
559
  botToken: z.string().optional(),
453
560
  defaultAccountId: z.string().optional(),
454
- webhook: telegramWebhookSchema.partial().optional(),
455
- accounts: z.record(z.string(), telegramAccountConfigSchema.partial()).optional()
561
+ webhook: partialTelegramWebhookSchema.optional(),
562
+ accounts: z.record(z.string(), partialTelegramAccountConfigSchema).optional()
456
563
  }).optional();
457
564
  const partialChannelsSchema = z.object({
458
565
  discord: fileConfigSchema.shape.channels.shape.discord.partial().optional(),
459
566
  telegram: partialTelegramConfigSchema
460
567
  }).optional();
568
+ const partialGatewayConfigSchema = z.object({
569
+ enabled: z.boolean().optional(),
570
+ host: z.string().min(1).optional(),
571
+ port: z.number().int().positive().optional(),
572
+ mode: z.enum(["local", "remote"]).optional(),
573
+ tickIntervalMs: z.number().int().positive().optional(),
574
+ webUi: fileConfigSchema.shape.gateway.shape.webUi.partial().optional(),
575
+ auth: z.object({
576
+ token: z.string().optional(),
577
+ password: z.string().optional(),
578
+ allowUnauthenticated: z.boolean().optional(),
579
+ allowedOrigins: z.array(z.string().min(1)).optional()
580
+ }).optional(),
581
+ remote: fileConfigSchema.shape.gateway.shape.remote.partial().optional()
582
+ }).optional();
461
583
  const partialFileConfigSchema = z.object({
462
584
  assistant: fileConfigSchema.shape.assistant.partial().optional(),
463
585
  agents: fileConfigSchema.shape.agents.partial().optional(),
464
586
  bindings: fileConfigSchema.shape.bindings.optional(),
465
587
  models: fileConfigSchema.shape.models.partial().optional(),
588
+ commands: commandsConfigSchema.partial().optional(),
466
589
  runtime: fileConfigSchema.shape.runtime.partial().optional(),
467
590
  channels: partialChannelsSchema,
468
- gateway: fileConfigSchema.shape.gateway.partial().optional(),
591
+ gateway: partialGatewayConfigSchema,
469
592
  scheduler: fileConfigSchema.shape.scheduler.partial().optional()
470
593
  });
471
594
  const apiKeyCredentialSchema = z.object({
@@ -526,12 +649,97 @@ function defaultHovclawHome() {
526
649
  function getHovclawHome(env = process.env) {
527
650
  return path.resolve(expandPath(env.HOVCLAW_HOME || defaultHovclawHome()));
528
651
  }
652
+ function defaultSharedSkillsDir(env = process.env) {
653
+ if ((env.NODE_ENV || "production") === "test") return path.join(getHovclawHome(env), ".agents", "skills");
654
+ return DEFAULT_SHARED_SKILLS_PATH;
655
+ }
656
+ function getSharedSkillsDir(env = process.env) {
657
+ const configured = env.HOVCLAW_SKILLS_DIR?.trim();
658
+ return path.resolve(expandPath(configured || defaultSharedSkillsDir(env)));
659
+ }
529
660
  function getConfigPath(env = process.env) {
530
661
  return path.join(getHovclawHome(env), "config.json");
531
662
  }
532
663
  function getCredentialsPath(env = process.env) {
533
664
  return path.join(getHovclawHome(env), "credentials.json");
534
665
  }
666
+ function ensureHomeContentDirs(projectRoot, hovclawHome) {
667
+ for (const dirName of USER_CONTENT_DIRS) {
668
+ const destinationDir = path.join(hovclawHome, dirName);
669
+ if (fs.existsSync(destinationDir)) continue;
670
+ fs.mkdirSync(destinationDir, {
671
+ recursive: true,
672
+ mode: 448
673
+ });
674
+ }
675
+ }
676
+ function ensureMainAgentConfig(agentsDir) {
677
+ const mainAgentPath = path.join(agentsDir, "main", "agent.json");
678
+ if (fs.existsSync(mainAgentPath)) return;
679
+ writeJsonFile(mainAgentPath, {
680
+ name: "Main",
681
+ skills: []
682
+ });
683
+ }
684
+ function ensureHomeStateDirs(projectRoot, hovclawHome) {
685
+ for (const dirName of USER_STATE_DIRS) {
686
+ const destinationDir = path.join(hovclawHome, dirName);
687
+ if (fs.existsSync(destinationDir)) continue;
688
+ const sourceDir = path.join(projectRoot, dirName);
689
+ if (fs.existsSync(sourceDir)) {
690
+ fs.cpSync(sourceDir, destinationDir, { recursive: true });
691
+ continue;
692
+ }
693
+ fs.mkdirSync(destinationDir, {
694
+ recursive: true,
695
+ mode: 448
696
+ });
697
+ }
698
+ }
699
+ function ensureHomeContentDirsOnce(projectRoot, hovclawHome) {
700
+ if (ensuredHomeContentDirs.has(hovclawHome)) return;
701
+ ensureHomeContentDirs(projectRoot, hovclawHome);
702
+ ensuredHomeContentDirs.add(hovclawHome);
703
+ }
704
+ function ensureHomeStateDirsOnce(projectRoot, hovclawHome) {
705
+ if (ensuredHomeStateDirs.has(hovclawHome)) return;
706
+ ensureHomeStateDirs(projectRoot, hovclawHome);
707
+ ensuredHomeStateDirs.add(hovclawHome);
708
+ }
709
+ function directoryHasEntries(dirPath) {
710
+ if (!fs.existsSync(dirPath)) return false;
711
+ try {
712
+ return fs.readdirSync(dirPath).length > 0;
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+ function copyDirectoryEntries(sourceDir, destinationDir) {
718
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
719
+ const sourcePath = path.join(sourceDir, entry.name);
720
+ const destinationPath = path.join(destinationDir, entry.name);
721
+ fs.cpSync(sourcePath, destinationPath, {
722
+ recursive: true,
723
+ force: true
724
+ });
725
+ }
726
+ }
727
+ function ensureSharedSkillsDir(hovclawHome, env = process.env) {
728
+ const sharedSkillsDir = getSharedSkillsDir(env);
729
+ ensureSecureDir(sharedSkillsDir);
730
+ if (directoryHasEntries(sharedSkillsDir)) return sharedSkillsDir;
731
+ const legacySkillsDir = path.join(hovclawHome, "skills");
732
+ if (!directoryHasEntries(legacySkillsDir)) return sharedSkillsDir;
733
+ copyDirectoryEntries(legacySkillsDir, sharedSkillsDir);
734
+ return sharedSkillsDir;
735
+ }
736
+ function ensureSharedSkillsDirOnce(hovclawHome, env = process.env) {
737
+ const sharedSkillsDir = getSharedSkillsDir(env);
738
+ if (ensuredSharedSkillsDirs.has(sharedSkillsDir)) return sharedSkillsDir;
739
+ ensureSharedSkillsDir(hovclawHome, env);
740
+ ensuredSharedSkillsDirs.add(sharedSkillsDir);
741
+ return sharedSkillsDir;
742
+ }
535
743
  function hasConfigFile(env = process.env) {
536
744
  return fs.existsSync(getConfigPath(env));
537
745
  }
@@ -627,6 +835,18 @@ function mergeWithDefaults(partial) {
627
835
  aliases: partial.models?.aliases ?? DEFAULT_FILE_CONFIG.models.aliases,
628
836
  allowlist: partial.models?.allowlist ?? DEFAULT_FILE_CONFIG.models.allowlist
629
837
  },
838
+ commands: {
839
+ native: partial.commands?.native ?? DEFAULT_FILE_CONFIG.commands.native,
840
+ nativeSkills: partial.commands?.nativeSkills ?? DEFAULT_FILE_CONFIG.commands.nativeSkills,
841
+ defaultThinkingLevel: partial.commands?.defaultThinkingLevel ?? DEFAULT_FILE_CONFIG.commands.defaultThinkingLevel,
842
+ text: partial.commands?.text ?? DEFAULT_FILE_CONFIG.commands.text,
843
+ config: partial.commands?.config ?? DEFAULT_FILE_CONFIG.commands.config,
844
+ debug: partial.commands?.debug ?? DEFAULT_FILE_CONFIG.commands.debug,
845
+ bash: partial.commands?.bash ?? DEFAULT_FILE_CONFIG.commands.bash,
846
+ restart: partial.commands?.restart ?? DEFAULT_FILE_CONFIG.commands.restart,
847
+ useAccessGroups: partial.commands?.useAccessGroups ?? DEFAULT_FILE_CONFIG.commands.useAccessGroups,
848
+ allowFrom: partial.commands?.allowFrom ?? DEFAULT_FILE_CONFIG.commands.allowFrom
849
+ },
630
850
  runtime: {
631
851
  mode: partial.runtime?.mode ?? DEFAULT_FILE_CONFIG.runtime.mode,
632
852
  containerImage: partial.runtime?.containerImage ?? DEFAULT_FILE_CONFIG.runtime.containerImage,
@@ -636,7 +856,8 @@ function mergeWithDefaults(partial) {
636
856
  maxOutputBytes: partial.runtime?.maxOutputBytes ?? DEFAULT_FILE_CONFIG.runtime.maxOutputBytes,
637
857
  allowedReadRoots: partial.runtime?.allowedReadRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedReadRoots,
638
858
  allowedWriteRoots: partial.runtime?.allowedWriteRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedWriteRoots,
639
- allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes
859
+ allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes,
860
+ tools: { bashEnabled: partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.bashEnabled }
640
861
  },
641
862
  channels: {
642
863
  discord: {
@@ -659,7 +880,9 @@ function mergeWithDefaults(partial) {
659
880
  },
660
881
  auth: {
661
882
  token: partial.gateway?.auth?.token ?? DEFAULT_FILE_CONFIG.gateway.auth.token,
662
- password: partial.gateway?.auth?.password ?? DEFAULT_FILE_CONFIG.gateway.auth.password
883
+ password: partial.gateway?.auth?.password ?? DEFAULT_FILE_CONFIG.gateway.auth.password,
884
+ allowUnauthenticated: partial.gateway?.auth?.allowUnauthenticated ?? DEFAULT_FILE_CONFIG.gateway.auth.allowUnauthenticated,
885
+ allowedOrigins: partial.gateway?.auth?.allowedOrigins ?? DEFAULT_FILE_CONFIG.gateway.auth.allowedOrigins
663
886
  },
664
887
  remote: {
665
888
  url: partial.gateway?.remote?.url ?? DEFAULT_FILE_CONFIG.gateway.remote.url,
@@ -724,6 +947,18 @@ function applyEnvOverrides(base, env) {
724
947
  aliases: base.models.aliases,
725
948
  allowlist: base.models.allowlist
726
949
  },
950
+ commands: {
951
+ native: base.commands.native,
952
+ nativeSkills: base.commands.nativeSkills,
953
+ defaultThinkingLevel: env.COMMANDS_DEFAULT_THINKING_LEVEL === "low" || env.COMMANDS_DEFAULT_THINKING_LEVEL === "medium" || env.COMMANDS_DEFAULT_THINKING_LEVEL === "high" ? env.COMMANDS_DEFAULT_THINKING_LEVEL : base.commands.defaultThinkingLevel,
954
+ text: toBool(env.COMMANDS_TEXT, base.commands.text),
955
+ config: toBool(env.COMMANDS_CONFIG, base.commands.config),
956
+ debug: toBool(env.COMMANDS_DEBUG, base.commands.debug),
957
+ bash: toBool(env.COMMANDS_BASH, base.commands.bash),
958
+ restart: toBool(env.COMMANDS_RESTART, base.commands.restart),
959
+ useAccessGroups: toBool(env.COMMANDS_USE_ACCESS_GROUPS, base.commands.useAccessGroups),
960
+ allowFrom: base.commands.allowFrom
961
+ },
727
962
  runtime: {
728
963
  mode: env.RUNTIME_MODE === "container" || env.RUNTIME_MODE === "local" ? env.RUNTIME_MODE : base.runtime.mode,
729
964
  containerImage: env.RUNTIME_CONTAINER_IMAGE || base.runtime.containerImage,
@@ -733,7 +968,8 @@ function applyEnvOverrides(base, env) {
733
968
  maxOutputBytes: toPositiveInt(env.TOOL_MAX_OUTPUT_BYTES, base.runtime.maxOutputBytes),
734
969
  allowedReadRoots: splitCsv(env.ALLOWED_READ_ROOTS, base.runtime.allowedReadRoots),
735
970
  allowedWriteRoots: splitCsv(env.ALLOWED_WRITE_ROOTS, base.runtime.allowedWriteRoots),
736
- allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes)
971
+ allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes),
972
+ tools: { bashEnabled: toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.bashEnabled) }
737
973
  },
738
974
  channels: {
739
975
  discord: {
@@ -762,7 +998,9 @@ function applyEnvOverrides(base, env) {
762
998
  },
763
999
  auth: {
764
1000
  token: env.GATEWAY_AUTH_TOKEN || base.gateway.auth.token,
765
- password: env.GATEWAY_AUTH_PASSWORD || base.gateway.auth.password
1001
+ password: env.GATEWAY_AUTH_PASSWORD || base.gateway.auth.password,
1002
+ allowUnauthenticated: toBool(env.GATEWAY_ALLOW_UNAUTHENTICATED, base.gateway.auth.allowUnauthenticated),
1003
+ allowedOrigins: splitCsv(env.GATEWAY_ALLOWED_ORIGINS, base.gateway.auth.allowedOrigins)
766
1004
  },
767
1005
  remote: {
768
1006
  url: env.GATEWAY_REMOTE_URL || base.gateway.remote.url,
@@ -783,21 +1021,33 @@ function escapeRegex(value) {
783
1021
  function normalizeRoots(paths) {
784
1022
  return paths.map((entry) => path.resolve(expandPath(entry)));
785
1023
  }
1024
+ function isLegacyProjectRootWorkspace(workspacePath, projectRoot) {
1025
+ if (!workspacePath) return false;
1026
+ const trimmed = workspacePath.trim();
1027
+ if (!trimmed) return false;
1028
+ return path.resolve(expandPath(trimmed)) === path.resolve(projectRoot);
1029
+ }
786
1030
  function loadConfig(env = process.env) {
787
1031
  const merged = applyEnvOverrides(loadConfigFile(env), env);
788
1032
  const hovclawHome = getHovclawHome(env);
1033
+ const agentsDir = path.join(hovclawHome, "agents");
1034
+ ensureHomeContentDirsOnce(PROJECT_ROOT, hovclawHome);
1035
+ ensureHomeStateDirsOnce(PROJECT_ROOT, hovclawHome);
1036
+ ensureMainAgentConfig(agentsDir);
1037
+ const sharedSkillsDir = ensureSharedSkillsDirOnce(hovclawHome, env);
789
1038
  const defaultWorkspace = path.join(hovclawHome, "workspace");
790
- const effectiveWorkspace = merged.agents.defaults.workspace.trim() || defaultWorkspace;
1039
+ const configuredWorkspace = merged.agents.defaults.workspace.trim();
1040
+ const effectiveWorkspace = !configuredWorkspace || isLegacyProjectRootWorkspace(configuredWorkspace, PROJECT_ROOT) ? defaultWorkspace : configuredWorkspace;
791
1041
  const assistantName = merged.assistant.name;
792
1042
  return {
793
1043
  projectRoot: PROJECT_ROOT,
794
1044
  hovclawHome,
795
1045
  configPath: getConfigPath(env),
796
1046
  credentialsPath: getCredentialsPath(env),
797
- agentsDir: path.join(PROJECT_ROOT, "agents"),
798
- skillsDir: path.join(PROJECT_ROOT, "skills"),
799
- storeDir: path.join(PROJECT_ROOT, "store"),
800
- dataDir: path.join(PROJECT_ROOT, "data"),
1047
+ agentsDir,
1048
+ skillsDir: sharedSkillsDir,
1049
+ storeDir: path.join(hovclawHome, "store"),
1050
+ dataDir: path.join(hovclawHome, "data"),
801
1051
  environment: env.NODE_ENV || "development",
802
1052
  logLevel: env.LOG_LEVEL || "info",
803
1053
  assistantName,
@@ -807,12 +1057,13 @@ function loadConfig(env = process.env) {
807
1057
  list: merged.agents.list.map((entry) => ({
808
1058
  id: entry.id,
809
1059
  name: entry.name,
810
- workspace: entry.workspace ? path.resolve(expandPath(entry.workspace)) : void 0,
1060
+ workspace: entry.workspace ? isLegacyProjectRootWorkspace(entry.workspace, PROJECT_ROOT) ? path.resolve(expandPath(defaultWorkspace)) : path.resolve(expandPath(entry.workspace)) : void 0,
811
1061
  default: entry.default
812
1062
  }))
813
1063
  },
814
1064
  bindings: merged.bindings,
815
1065
  models: { ...merged.models },
1066
+ commands: { ...merged.commands },
816
1067
  runtime: {
817
1068
  mode: merged.runtime.mode,
818
1069
  containerImage: merged.runtime.containerImage,
@@ -822,7 +1073,8 @@ function loadConfig(env = process.env) {
822
1073
  maxOutputBytes: merged.runtime.maxOutputBytes,
823
1074
  allowedReadRoots: normalizeRoots(merged.runtime.allowedReadRoots),
824
1075
  allowedWriteRoots: normalizeRoots(merged.runtime.allowedWriteRoots),
825
- allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes
1076
+ allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes,
1077
+ tools: { bashEnabled: merged.runtime.tools.bashEnabled }
826
1078
  },
827
1079
  channels: {
828
1080
  discord: {
@@ -859,7 +1111,9 @@ function loadConfig(env = process.env) {
859
1111
  },
860
1112
  auth: {
861
1113
  token: merged.gateway.auth.token,
862
- password: merged.gateway.auth.password
1114
+ password: merged.gateway.auth.password,
1115
+ allowUnauthenticated: merged.gateway.auth.allowUnauthenticated,
1116
+ allowedOrigins: merged.gateway.auth.allowedOrigins
863
1117
  },
864
1118
  remote: {
865
1119
  url: merged.gateway.remote.url,
@@ -917,6 +1171,7 @@ function legacyEnvKeys() {
917
1171
  "ALLOWED_READ_ROOTS",
918
1172
  "ALLOWED_WRITE_ROOTS",
919
1173
  "ALLOWED_COMMAND_PREFIXES",
1174
+ "RUNTIME_BASH_ENABLED",
920
1175
  "TOOL_TIMEOUT_MS",
921
1176
  "TOOL_MAX_OUTPUT_BYTES",
922
1177
  "ENABLE_DISCORD",
@@ -936,6 +1191,8 @@ function legacyEnvKeys() {
936
1191
  "GATEWAY_TICK_INTERVAL_MS",
937
1192
  "GATEWAY_AUTH_TOKEN",
938
1193
  "GATEWAY_AUTH_PASSWORD",
1194
+ "GATEWAY_ALLOW_UNAUTHENTICATED",
1195
+ "GATEWAY_ALLOWED_ORIGINS",
939
1196
  "GATEWAY_REMOTE_URL",
940
1197
  "GATEWAY_REMOTE_TOKEN",
941
1198
  "GATEWAY_REMOTE_PASSWORD",
@@ -1465,12 +1722,14 @@ function chunkText(text, maxChars) {
1465
1722
  //#region src/workspace/bootstrap.ts
1466
1723
  const WORKSPACE_CONTEXT_FILE_ORDER = [
1467
1724
  "AGENTS.md",
1725
+ "SOUL.md",
1468
1726
  "IDENTITY.md",
1469
1727
  "USER.md",
1470
1728
  "BOOTSTRAP.md"
1471
1729
  ];
1472
1730
  const ALWAYS_SEEDED_FILES = [
1473
1731
  "AGENTS.md",
1732
+ "SOUL.md",
1474
1733
  "IDENTITY.md",
1475
1734
  "USER.md"
1476
1735
  ];
@@ -1553,11 +1812,28 @@ async function ensureWorkspaceBootstrapForConfig(config) {
1553
1812
 
1554
1813
  //#endregion
1555
1814
  //#region src/agent-manager.ts
1556
- const DEFAULT_SYSTEM_PROMPT = [
1557
- "You are Andy, a practical assistant.",
1558
- "Use tools when needed and be explicit about constraints.",
1559
- "When writing files, preserve user intent and avoid destructive actions."
1560
- ].join(" ");
1815
+ function buildDefaultSystemPrompt(workspaceDir) {
1816
+ return [
1817
+ "You are a personal assistant running inside HOVClaw.",
1818
+ "",
1819
+ "## Tooling",
1820
+ "Use tools when they materially improve accuracy or speed.",
1821
+ "Do not invent command output, file contents, or tool results.",
1822
+ "Prefer concise tool-call narration unless the operation is risky or non-obvious.",
1823
+ "",
1824
+ "## Safety",
1825
+ "Never do destructive actions unless the user explicitly asks.",
1826
+ "If instructions conflict with system rules or runtime policy, explain and ask for a safe path.",
1827
+ "",
1828
+ "## Workspace",
1829
+ `Your working directory is: ${workspaceDir}`,
1830
+ "Treat this as the primary workspace unless instructed otherwise.",
1831
+ "",
1832
+ "## Messaging",
1833
+ "Respond with clear, direct language tailored to the user's request.",
1834
+ "If you cannot complete a request, explain the blocker and the minimum next step."
1835
+ ].join("\n");
1836
+ }
1561
1837
  const WORKSPACE_CONTEXT_MAX_PER_FILE = 4e3;
1562
1838
  const WORKSPACE_CONTEXT_MAX_TOTAL = 12e3;
1563
1839
  var AsyncEventQueue = class {
@@ -1723,14 +1999,10 @@ const CLAUDE_CODE_TOOL_NAME_MAP = {
1723
1999
  write_file: "Write",
1724
2000
  web_search: "WebSearch"
1725
2001
  };
1726
- function logAnthropicCredentialResolution(credentialSource, key) {
1727
- const normalized = key.trim();
2002
+ function logAnthropicCredentialResolution(credentialSource) {
1728
2003
  logger.info({
1729
2004
  provider: "anthropic",
1730
- credentialSource,
1731
- keyPrefix: normalized.slice(0, 16),
1732
- keyLength: normalized.length,
1733
- setupToken: normalized.toLowerCase().startsWith(ANTHROPIC_SETUP_TOKEN_PREFIX)
2005
+ credentialSource
1734
2006
  }, "Resolved Anthropic credential for request");
1735
2007
  }
1736
2008
  /**
@@ -1872,6 +2144,7 @@ function truncateTextToMaxChars(value, maxChars) {
1872
2144
  }
1873
2145
  async function buildWorkspacePromptContext(workspaceDir) {
1874
2146
  const sections = [];
2147
+ let hasSoulFile = false;
1875
2148
  for (const fileName of WORKSPACE_CONTEXT_FILE_ORDER) {
1876
2149
  const filePath = path.join(workspaceDir, fileName);
1877
2150
  let raw;
@@ -1883,6 +2156,7 @@ async function buildWorkspacePromptContext(workspaceDir) {
1883
2156
  const trimmed = raw.trim();
1884
2157
  if (!trimmed) continue;
1885
2158
  const clipped = truncateTextToMaxChars(trimmed, WORKSPACE_CONTEXT_MAX_PER_FILE);
2159
+ if (fileName.toLowerCase() === "soul.md") hasSoulFile = true;
1886
2160
  sections.push([
1887
2161
  `## ${fileName}`,
1888
2162
  clipped.text,
@@ -1890,7 +2164,14 @@ async function buildWorkspacePromptContext(workspaceDir) {
1890
2164
  ].filter(Boolean).join("\n\n"));
1891
2165
  }
1892
2166
  if (sections.length === 0) return "";
1893
- const rendered = ["# Workspace Context", ...sections].join("\n\n");
2167
+ const rendered = [
2168
+ "# Project Context",
2169
+ "",
2170
+ "The following project context files have been loaded:",
2171
+ hasSoulFile ? "If SOUL.md is present, embody its persona and tone unless higher-priority rules override it." : "",
2172
+ "",
2173
+ ...sections
2174
+ ].filter(Boolean).join("\n\n");
1894
2175
  if (rendered.length <= WORKSPACE_CONTEXT_MAX_TOTAL) return rendered;
1895
2176
  const footer = `\n\n[Workspace context truncated at ${WORKSPACE_CONTEXT_MAX_TOTAL} total characters.]`;
1896
2177
  if (footer.length >= WORKSPACE_CONTEXT_MAX_TOTAL) return rendered.slice(0, WORKSPACE_CONTEXT_MAX_TOTAL);
@@ -1899,7 +2180,7 @@ async function buildWorkspacePromptContext(workspaceDir) {
1899
2180
  }
1900
2181
  async function loadAgentPrompt(agentName, workspaceDir) {
1901
2182
  const promptPath = path.join(config.agentsDir, agentName, "CLAUDE.md");
1902
- let basePrompt = DEFAULT_SYSTEM_PROMPT;
2183
+ let basePrompt = buildDefaultSystemPrompt(workspaceDir);
1903
2184
  try {
1904
2185
  const prompt = await fs$1.readFile(promptPath, "utf8");
1905
2186
  if (prompt.trim().length > 0) basePrompt = prompt;
@@ -1926,14 +2207,14 @@ async function resolveProviderApiKey(provider, env = process.env) {
1926
2207
  const isAnthropicProvider = provider.toLowerCase() === "anthropic";
1927
2208
  const fromEnv = getProviderApiKeyFromEnv(provider, env);
1928
2209
  if (fromEnv) {
1929
- if (isAnthropicProvider) logAnthropicCredentialResolution("env", fromEnv);
2210
+ if (isAnthropicProvider) logAnthropicCredentialResolution("env");
1930
2211
  return fromEnv;
1931
2212
  }
1932
2213
  const credentials = loadCredentials(env);
1933
2214
  const entry = credentials[provider];
1934
2215
  if (!entry) return void 0;
1935
2216
  if (entry.type === "api-key") {
1936
- if (isAnthropicProvider) logAnthropicCredentialResolution("credentials-api-key", entry.key);
2217
+ if (isAnthropicProvider) logAnthropicCredentialResolution("credentials-api-key");
1937
2218
  return entry.key;
1938
2219
  }
1939
2220
  if (isAnthropicProvider && isAnthropicOAuthCredentialUnsupported(credentials.anthropic)) {
@@ -1941,7 +2222,7 @@ async function resolveProviderApiKey(provider, env = process.env) {
1941
2222
  return;
1942
2223
  }
1943
2224
  if (isAnthropicProvider && credentials.anthropic?.type === "oauth" && isAnthropicSetupToken(credentials.anthropic)) {
1944
- logAnthropicCredentialResolution("credentials-oauth-setup-token", credentials.anthropic.access);
2225
+ logAnthropicCredentialResolution("credentials-oauth-setup-token");
1945
2226
  return credentials.anthropic.access;
1946
2227
  }
1947
2228
  const result = await getOAuthApiKey(provider, toOAuthCredentialMap(credentials));
@@ -1957,7 +2238,7 @@ async function resolveProviderApiKey(provider, env = process.env) {
1957
2238
  ...result.newCredentials
1958
2239
  };
1959
2240
  saveCredentials(credentials, env);
1960
- if (isAnthropicProvider) logAnthropicCredentialResolution("oauth-refresh", result.apiKey);
2241
+ if (isAnthropicProvider) logAnthropicCredentialResolution("oauth-refresh");
1961
2242
  return result.apiKey;
1962
2243
  }
1963
2244
  var PiAgentManager = class {
@@ -2127,6 +2408,10 @@ var PiAgentManager = class {
2127
2408
  if (!handle) return;
2128
2409
  this.db.saveAgentState(sessionKey, JSON.stringify({ messages: handle.agent.state.messages }));
2129
2410
  }
2411
+ async resetSession(sessionKey) {
2412
+ this.sessions.delete(sessionKey);
2413
+ this.db.clearSession(sessionKey);
2414
+ }
2130
2415
  async persistAllSessions() {
2131
2416
  await Promise.all(Array.from(this.sessions.keys()).map((sessionKey) => this.persistSession(sessionKey)));
2132
2417
  }
@@ -2205,6 +2490,11 @@ var DiscordChannel = class {
2205
2490
 
2206
2491
  //#endregion
2207
2492
  //#region src/channels/telegram.ts
2493
+ var telegram_exports = /* @__PURE__ */ __exportAll({
2494
+ TelegramChannel: () => TelegramChannel,
2495
+ parseTargetChatId: () => parseTargetChatId,
2496
+ toInboundMessage: () => toInboundMessage
2497
+ });
2208
2498
  const TELEGRAM_CHUNK_LIMIT = 3900;
2209
2499
  const TELEGRAM_ALLOWED_UPDATES = [
2210
2500
  "message",
@@ -2220,6 +2510,7 @@ const DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
2220
2510
  const DEFAULT_POLL_SUCCESS_DELAY_MS = 250;
2221
2511
  const DEFAULT_POLL_FAILURE_DELAY_MS = 1e3;
2222
2512
  const DEFAULT_MAX_POLL_FAILURE_DELAY_MS = 15e3;
2513
+ const TELEGRAM_MAX_WEBHOOK_BODY_BYTES = 1048576;
2223
2514
  function sleep$1(ms) {
2224
2515
  return new Promise((resolve) => {
2225
2516
  setTimeout(resolve, ms);
@@ -2387,14 +2678,38 @@ function toInboundMessage(update, accountId = "default") {
2387
2678
  raw: update
2388
2679
  };
2389
2680
  }
2390
- async function readBody(req) {
2681
+ function compareWebhookSecrets(expected, provided) {
2682
+ const expectedBuffer = Buffer.from(expected, "utf8");
2683
+ const providedBuffer = Buffer.from(provided, "utf8");
2684
+ if (expectedBuffer.length !== providedBuffer.length) return false;
2685
+ return timingSafeEqual(expectedBuffer, providedBuffer);
2686
+ }
2687
+ async function readBody(req, maxBytes) {
2391
2688
  return new Promise((resolve, reject) => {
2392
2689
  let body = "";
2690
+ let totalBytes = 0;
2691
+ let done = false;
2393
2692
  req.on("data", (chunk) => {
2693
+ if (done) return;
2694
+ totalBytes += chunk.length;
2695
+ if (totalBytes > maxBytes) {
2696
+ done = true;
2697
+ req.destroy();
2698
+ reject(/* @__PURE__ */ new Error("payload_too_large"));
2699
+ return;
2700
+ }
2394
2701
  body += chunk.toString("utf8");
2395
2702
  });
2396
- req.on("end", () => resolve(body));
2397
- req.on("error", (error) => reject(error));
2703
+ req.on("end", () => {
2704
+ if (done) return;
2705
+ done = true;
2706
+ resolve(body);
2707
+ });
2708
+ req.on("error", (error) => {
2709
+ if (done) return;
2710
+ done = true;
2711
+ reject(error);
2712
+ });
2398
2713
  });
2399
2714
  }
2400
2715
  var TelegramChannel = class {
@@ -2590,14 +2905,25 @@ var TelegramChannel = class {
2590
2905
  }
2591
2906
  const expectedSecret = account.webhook.secret.trim();
2592
2907
  if (expectedSecret) {
2593
- if (String(req.headers["x-telegram-bot-api-secret-token"] || "") !== expectedSecret) {
2908
+ if (!compareWebhookSecrets(expectedSecret, String(req.headers["x-telegram-bot-api-secret-token"] || ""))) {
2594
2909
  res.statusCode = 403;
2595
2910
  this.lastWebhookStatus = 403;
2596
2911
  res.end("forbidden");
2597
2912
  return;
2598
2913
  }
2599
2914
  }
2600
- const body = await readBody(req);
2915
+ let body = "";
2916
+ try {
2917
+ body = await readBody(req, TELEGRAM_MAX_WEBHOOK_BODY_BYTES);
2918
+ } catch (error) {
2919
+ if (error instanceof Error && error.message === "payload_too_large") {
2920
+ res.statusCode = 413;
2921
+ this.lastWebhookStatus = 413;
2922
+ res.end("payload too large");
2923
+ return;
2924
+ }
2925
+ throw error;
2926
+ }
2601
2927
  if (!body) {
2602
2928
  res.statusCode = 204;
2603
2929
  this.lastWebhookStatus = 204;
@@ -2652,6 +2978,9 @@ var TelegramChannel = class {
2652
2978
  this.schedulePoll(10);
2653
2979
  logger.info({ accountId: this.accountId }, "Telegram polling channel started");
2654
2980
  }
2981
+ getAccountId() {
2982
+ return this.accountId;
2983
+ }
2655
2984
  onMessage(handler) {
2656
2985
  this.handler = handler;
2657
2986
  }
@@ -2689,6 +3018,21 @@ var TelegramChannel = class {
2689
3018
  text: text?.trim() || void 0
2690
3019
  });
2691
3020
  }
3021
+ async setMyCommands(commands) {
3022
+ const normalized = commands.map((entry) => ({
3023
+ command: entry.command,
3024
+ description: entry.description
3025
+ }));
3026
+ for (const scope of [
3027
+ { type: "default" },
3028
+ { type: "all_private_chats" },
3029
+ { type: "all_group_chats" },
3030
+ { type: "all_chat_administrators" }
3031
+ ]) await this.callApi("setMyCommands", {
3032
+ commands: normalized,
3033
+ scope
3034
+ });
3035
+ }
2692
3036
  async sendMedia(target, media) {
2693
3037
  const parsedTarget = parseTargetChatId(target.chatId);
2694
3038
  const { method, mediaField } = mediaMethod(media);
@@ -2770,9 +3114,46 @@ var TelegramChannel = class {
2770
3114
  }
2771
3115
  };
2772
3116
 
3117
+ //#endregion
3118
+ //#region src/redaction.ts
3119
+ const REDACTED_VALUE = "[REDACTED]";
3120
+ const SENSITIVE_KEY_PATTERNS = [
3121
+ /token/i,
3122
+ /password/i,
3123
+ /secret/i,
3124
+ /api[_-]?key/i,
3125
+ /^key$/i,
3126
+ /authorization/i,
3127
+ /cookie/i,
3128
+ /refresh/i,
3129
+ /access/i
3130
+ ];
3131
+ function isSensitiveKey(key) {
3132
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
3133
+ }
3134
+ function redactValue(value, seen) {
3135
+ if (Array.isArray(value)) return value.map((entry) => redactValue(entry, seen));
3136
+ if (!value || typeof value !== "object") return value;
3137
+ if (seen.has(value)) return "[Circular]";
3138
+ seen.add(value);
3139
+ const record = value;
3140
+ const result = {};
3141
+ for (const [key, entry] of Object.entries(record)) {
3142
+ if (isSensitiveKey(key)) {
3143
+ result[key] = REDACTED_VALUE;
3144
+ continue;
3145
+ }
3146
+ result[key] = redactValue(entry, seen);
3147
+ }
3148
+ return result;
3149
+ }
3150
+ function redactSensitiveData(value) {
3151
+ return redactValue(value, /* @__PURE__ */ new WeakSet());
3152
+ }
3153
+
2773
3154
  //#endregion
2774
3155
  //#region src/db.ts
2775
- function nowIso() {
3156
+ function nowIso$1() {
2776
3157
  return (/* @__PURE__ */ new Date()).toISOString();
2777
3158
  }
2778
3159
  var HovClawDb = class {
@@ -2900,7 +3281,7 @@ var HovClawDb = class {
2900
3281
  return Boolean(row?.name);
2901
3282
  }
2902
3283
  upsertSession(parts, sessionKey, model, options) {
2903
- const createdAt = nowIso();
3284
+ const createdAt = nowIso$1();
2904
3285
  this.db.prepare(`
2905
3286
  INSERT INTO sessions (
2906
3287
  session_key,
@@ -2995,7 +3376,7 @@ var HovClawDb = class {
2995
3376
  ON CONFLICT(account_id) DO UPDATE SET
2996
3377
  last_update_id = excluded.last_update_id,
2997
3378
  updated_at = excluded.updated_at
2998
- `).run(accountId, updateId, nowIso());
3379
+ `).run(accountId, updateId, nowIso$1());
2999
3380
  }
3000
3381
  hasTelegramDedupe(accountId, updateId) {
3001
3382
  return typeof this.db.prepare(`
@@ -3008,7 +3389,7 @@ var HovClawDb = class {
3008
3389
  this.db.prepare(`
3009
3390
  INSERT OR IGNORE INTO telegram_dedupe (account_id, update_id, updated_at)
3010
3391
  VALUES (?, ?, ?)
3011
- `).run(accountId, updateId, nowIso());
3392
+ `).run(accountId, updateId, nowIso$1());
3012
3393
  }
3013
3394
  pruneTelegramDedupe(olderThanIso) {
3014
3395
  return this.db.prepare(`
@@ -3017,7 +3398,7 @@ var HovClawDb = class {
3017
3398
  `).run(olderThanIso).changes;
3018
3399
  }
3019
3400
  appendMessage(sessionKey, role, content) {
3020
- this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso());
3401
+ this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso$1());
3021
3402
  }
3022
3403
  getMessages(sessionKey) {
3023
3404
  return this.db.prepare(`
@@ -3038,11 +3419,16 @@ var HovClawDb = class {
3038
3419
  ON CONFLICT(session_key) DO UPDATE SET
3039
3420
  state_json = excluded.state_json,
3040
3421
  updated_at = excluded.updated_at
3041
- `).run(sessionKey, stateJson, nowIso());
3422
+ `).run(sessionKey, stateJson, nowIso$1());
3042
3423
  }
3043
3424
  getAgentState(sessionKey) {
3044
3425
  return this.db.prepare(`SELECT state_json FROM agent_state WHERE session_key = ?`).get(sessionKey)?.state_json ?? null;
3045
3426
  }
3427
+ clearSession(sessionKey) {
3428
+ this.db.prepare(`DELETE FROM agent_state WHERE session_key = ?`).run(sessionKey);
3429
+ this.db.prepare(`DELETE FROM messages WHERE session_key = ?`).run(sessionKey);
3430
+ this.db.prepare(`DELETE FROM sessions WHERE session_key = ?`).run(sessionKey);
3431
+ }
3046
3432
  recordUsageCost(record) {
3047
3433
  this.db.prepare(`
3048
3434
  INSERT INTO usage_costs (
@@ -3054,7 +3440,7 @@ var HovClawDb = class {
3054
3440
  cost_usd,
3055
3441
  created_at
3056
3442
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
3057
- `).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso());
3443
+ `).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso$1());
3058
3444
  }
3059
3445
  upsertScheduledJob(job) {
3060
3446
  this.db.prepare(`
@@ -3184,10 +3570,11 @@ var HovClawDb = class {
3184
3570
  }));
3185
3571
  }
3186
3572
  appendAuditEvent(record) {
3573
+ const sanitizedPayload = redactSensitiveData(record.payload);
3187
3574
  this.db.prepare(`
3188
3575
  INSERT INTO audit_log (ts, session_key, actor, event_type, payload_json)
3189
3576
  VALUES (?, ?, ?, ?, ?)
3190
- `).run(record.ts ?? nowIso(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(record.payload));
3577
+ `).run(record.ts ?? nowIso$1(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
3191
3578
  }
3192
3579
  getAuditEvents(eventType) {
3193
3580
  const query = eventType ? `SELECT ts, session_key, actor, event_type, payload_json FROM audit_log WHERE event_type = ? ORDER BY id DESC` : `SELECT ts, session_key, actor, event_type, payload_json FROM audit_log ORDER BY id DESC`;
@@ -3238,19 +3625,67 @@ function extractCommandPrefix$1(command) {
3238
3625
  if (!trimmed) return "";
3239
3626
  return trimmed.split(/\s+/)[0] ?? "";
3240
3627
  }
3241
- function splitCommandSegments$1(command) {
3242
- return command.split(/(?:&&|\|\||[;|\n])/).map((segment) => segment.trim()).filter(Boolean);
3628
+ function nonOptionArgs$1(args) {
3629
+ const out = [];
3630
+ let skipNext = false;
3631
+ for (const arg of args) {
3632
+ if (skipNext) {
3633
+ skipNext = false;
3634
+ continue;
3635
+ }
3636
+ if (arg === "--") break;
3637
+ if (arg === "-n" || arg === "-c" || arg === "--max-depth" || arg === "--glob" || arg === "--iglob" || arg === "--type" || arg === "--type-not" || arg === "-e" || arg === "-f") {
3638
+ skipNext = true;
3639
+ continue;
3640
+ }
3641
+ if (arg.startsWith("-")) continue;
3642
+ out.push(arg);
3643
+ }
3644
+ return out;
3645
+ }
3646
+ function isLikelyPathOperand$1(value) {
3647
+ const trimmed = value.trim();
3648
+ if (!trimmed) return false;
3649
+ if (/^\d+$/.test(trimmed)) return false;
3650
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return false;
3651
+ if (trimmed === "." || trimmed === ".." || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || trimmed.includes("/")) return true;
3652
+ return /^[A-Za-z0-9._-]+$/.test(trimmed);
3243
3653
  }
3244
- function validateCommand$1(command, allowedCommandPrefixes) {
3654
+ function collectReadPathOperands$1(prefix, args) {
3655
+ if (prefix === "rg") {
3656
+ const plain = nonOptionArgs$1(args);
3657
+ if (plain.length <= 1) return [];
3658
+ return plain.slice(1);
3659
+ }
3660
+ if (prefix === "find") {
3661
+ const plain = nonOptionArgs$1(args);
3662
+ return plain.length > 0 ? [plain[0]] : [];
3663
+ }
3664
+ return nonOptionArgs$1(args);
3665
+ }
3666
+ function validateCommand$1(command, allowedCommandPrefixes, allowedReadRoots, workspaceDir) {
3245
3667
  const trimmed = command.trim();
3246
3668
  if (!trimmed) throw new Error("Command prefix not allowed: <empty>");
3247
- if (/[`]|[$][(]|[<][(]|[>][(]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
3248
- if (/[<>]/.test(trimmed)) throw new Error("Command redirection is not allowed");
3249
- const segments = splitCommandSegments$1(trimmed);
3250
- if (segments.length === 0) throw new Error("Command prefix not allowed: <empty>");
3251
- for (const segment of segments) {
3252
- const prefix = extractCommandPrefix$1(segment);
3253
- if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
3669
+ if (/[;&|`$()<>]/.test(trimmed) || /[\r\n]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
3670
+ if (/["'\\]/.test(trimmed)) throw new Error("Quoted/escaped shell syntax is not allowed");
3671
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
3672
+ if (tokens.length === 0) throw new Error("Command prefix not allowed: <empty>");
3673
+ const prefix = extractCommandPrefix$1(trimmed);
3674
+ if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
3675
+ if (![
3676
+ "cat",
3677
+ "head",
3678
+ "tail",
3679
+ "wc",
3680
+ "rg",
3681
+ "ls",
3682
+ "find"
3683
+ ].includes(prefix)) return;
3684
+ const readRoots = buildEffectiveRoots$1(allowedReadRoots, workspaceDir);
3685
+ const operands = collectReadPathOperands$1(prefix, tokens.slice(1)).filter(isLikelyPathOperand$1);
3686
+ for (const operand of operands) {
3687
+ const resolved = resolveToolPath$1(operand, workspaceDir);
3688
+ if (!startsWithAnyRoot$1(resolved, readRoots)) throw new Error(`Command path is outside allowed read roots: ${resolved}. Allowed roots: ${readRoots.join(", ") || "<none>"}`);
3254
3689
  }
3255
3690
  }
3256
3691
  function maybeTruncate$1(value, maxOutputBytes) {
@@ -3268,7 +3703,7 @@ function toContainerName(agentName, sessionKey) {
3268
3703
  const hash = crypto.createHash("sha256").update(sessionKey).digest("hex").slice(0, 12);
3269
3704
  return `hovclaw-${agentName.toLowerCase().replace(/[^a-z0-9-]/g, "-")}-${hash}`;
3270
3705
  }
3271
- function shellEscape(input) {
3706
+ function shellEscape$1(input) {
3272
3707
  return `'${input.replace(/'/g, `'"'"'`)}'`;
3273
3708
  }
3274
3709
  var ContainerRuntime = class {
@@ -3471,7 +3906,8 @@ var ContainerRuntime = class {
3471
3906
  }
3472
3907
  async exec(command, limits) {
3473
3908
  const effective = this.mergeLimits(limits);
3474
- validateCommand$1(command, effective.allowedCommandPrefixes);
3909
+ const workspaceDir = getRuntimeSessionContext()?.workspaceDir;
3910
+ validateCommand$1(command, effective.allowedCommandPrefixes, effective.allowedReadRoots, workspaceDir);
3475
3911
  return this.withContainer(effective, async (container) => {
3476
3912
  return this.runDocker([
3477
3913
  "exec",
@@ -3512,8 +3948,8 @@ var ContainerRuntime = class {
3512
3948
  const effective = this.mergeLimits(limits);
3513
3949
  const workspaceDir = getRuntimeSessionContext()?.workspaceDir;
3514
3950
  const resolved = enforceAllowedPath$1(filePath, effective.allowedWriteRoots, workspaceDir, "write");
3515
- const escapedPath = shellEscape(resolved);
3516
- const escapedDir = shellEscape(path.dirname(resolved));
3951
+ const escapedPath = shellEscape$1(resolved);
3952
+ const escapedDir = shellEscape$1(path.dirname(resolved));
3517
3953
  return this.withContainer(effective, async (container) => {
3518
3954
  const result = await this.runDocker([
3519
3955
  "exec",
@@ -3540,7 +3976,7 @@ var ContainerRuntime = class {
3540
3976
  container.name,
3541
3977
  "sh",
3542
3978
  "-lc",
3543
- `curl -L --max-time ${Math.max(5, Math.floor(effective.timeoutMs / 1e3))} -sS ${shellEscape(url)}`
3979
+ `curl -L --max-time ${Math.max(5, Math.floor(effective.timeoutMs / 1e3))} -sS ${shellEscape$1(url)}`
3544
3980
  ], {
3545
3981
  timeoutMs: effective.timeoutMs,
3546
3982
  maxOutputBytes: effective.maxOutputBytes,
@@ -3611,19 +4047,67 @@ function extractCommandPrefix(command) {
3611
4047
  if (!trimmed) return "";
3612
4048
  return trimmed.split(/\s+/)[0] ?? "";
3613
4049
  }
3614
- function splitCommandSegments(command) {
3615
- return command.split(/(?:&&|\|\||[;|\n])/).map((segment) => segment.trim()).filter(Boolean);
4050
+ function nonOptionArgs(args) {
4051
+ const out = [];
4052
+ let skipNext = false;
4053
+ for (const arg of args) {
4054
+ if (skipNext) {
4055
+ skipNext = false;
4056
+ continue;
4057
+ }
4058
+ if (arg === "--") break;
4059
+ if (arg === "-n" || arg === "-c" || arg === "--max-depth" || arg === "--glob" || arg === "--iglob" || arg === "--type" || arg === "--type-not" || arg === "-e" || arg === "-f") {
4060
+ skipNext = true;
4061
+ continue;
4062
+ }
4063
+ if (arg.startsWith("-")) continue;
4064
+ out.push(arg);
4065
+ }
4066
+ return out;
4067
+ }
4068
+ function isLikelyPathOperand(value) {
4069
+ const trimmed = value.trim();
4070
+ if (!trimmed) return false;
4071
+ if (/^\d+$/.test(trimmed)) return false;
4072
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return false;
4073
+ if (trimmed === "." || trimmed === ".." || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || trimmed.includes("/")) return true;
4074
+ return /^[A-Za-z0-9._-]+$/.test(trimmed);
3616
4075
  }
3617
- function validateCommand(command, allowedCommandPrefixes) {
4076
+ function collectReadPathOperands(prefix, args) {
4077
+ if (prefix === "rg") {
4078
+ const plain = nonOptionArgs(args);
4079
+ if (plain.length <= 1) return [];
4080
+ return plain.slice(1);
4081
+ }
4082
+ if (prefix === "find") {
4083
+ const plain = nonOptionArgs(args);
4084
+ return plain.length > 0 ? [plain[0]] : [];
4085
+ }
4086
+ return nonOptionArgs(args);
4087
+ }
4088
+ function validateCommand(command, allowedCommandPrefixes, allowedReadRoots, workspaceDir) {
3618
4089
  const trimmed = command.trim();
3619
4090
  if (!trimmed) throw new Error("Command prefix not allowed: <empty>");
3620
- if (/[`]|[$][(]|[<][(]|[>][(]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
3621
- if (/[<>]/.test(trimmed)) throw new Error("Command redirection is not allowed");
3622
- const segments = splitCommandSegments(trimmed);
3623
- if (segments.length === 0) throw new Error("Command prefix not allowed: <empty>");
3624
- for (const segment of segments) {
3625
- const prefix = extractCommandPrefix(segment);
3626
- if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
4091
+ if (/[;&|`$()<>]/.test(trimmed) || /[\r\n]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
4092
+ if (/["'\\]/.test(trimmed)) throw new Error("Quoted/escaped shell syntax is not allowed");
4093
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
4094
+ if (tokens.length === 0) throw new Error("Command prefix not allowed: <empty>");
4095
+ const prefix = extractCommandPrefix(trimmed);
4096
+ if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
4097
+ if (![
4098
+ "cat",
4099
+ "head",
4100
+ "tail",
4101
+ "wc",
4102
+ "rg",
4103
+ "ls",
4104
+ "find"
4105
+ ].includes(prefix)) return;
4106
+ const readRoots = buildEffectiveRoots(allowedReadRoots, workspaceDir);
4107
+ const operands = collectReadPathOperands(prefix, tokens.slice(1)).filter(isLikelyPathOperand);
4108
+ for (const operand of operands) {
4109
+ const resolved = resolveToolPath(operand, workspaceDir);
4110
+ if (!startsWithAnyRoot(resolved, readRoots)) throw new Error(`Command path is outside allowed read roots: ${resolved}. Allowed roots: ${readRoots.join(", ") || "<none>"}`);
3627
4111
  }
3628
4112
  }
3629
4113
  function maybeTruncate(value, maxOutputBytes) {
@@ -3649,13 +4133,18 @@ var LocalHostRuntime = class {
3649
4133
  }
3650
4134
  async exec(command, limits) {
3651
4135
  const effective = this.mergeLimits(limits);
3652
- validateCommand(command, effective.allowedCommandPrefixes);
4136
+ const workspaceDir = getRuntimeSessionContext()?.workspaceDir;
4137
+ validateCommand(command, effective.allowedCommandPrefixes, effective.allowedReadRoots, workspaceDir);
4138
+ const cwd = workspaceDir ? path.resolve(expandUserPath(workspaceDir)) : process.cwd();
3653
4139
  return await new Promise((resolve, reject) => {
3654
- const child = spawn("bash", ["-lc", command], { stdio: [
3655
- "ignore",
3656
- "pipe",
3657
- "pipe"
3658
- ] });
4140
+ const child = spawn("bash", ["-lc", command], {
4141
+ stdio: [
4142
+ "ignore",
4143
+ "pipe",
4144
+ "pipe"
4145
+ ],
4146
+ cwd
4147
+ });
3659
4148
  let stdout = "";
3660
4149
  let stderr = "";
3661
4150
  let timedOut = false;
@@ -3748,149 +4237,151 @@ function normalizeErrorMessage(error) {
3748
4237
  if (error instanceof Error) return error.message;
3749
4238
  return String(error);
3750
4239
  }
3751
- function createTools({ runtime, audit }) {
4240
+ function createTools({ runtime, audit, bashEnabled }) {
3752
4241
  const parser = new Parser();
3753
- return [
3754
- {
3755
- name: "bash",
3756
- label: "Bash",
3757
- description: "Run a shell command on allowed command prefixes only.",
3758
- parameters: Type.Object({
3759
- command: Type.String(),
3760
- timeoutMs: Type.Optional(Type.Number({
3761
- minimum: 1e3,
3762
- maximum: 12e4
3763
- }))
3764
- }),
3765
- execute: async (_toolCallId, params) => {
3766
- audit({
3767
- actor: "tool",
3768
- eventType: "tool.exec",
3769
- payload: { command: params.command }
3770
- });
3771
- const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
3772
- return textResult([
3773
- `exitCode: ${result.exitCode}`,
3774
- result.timedOut ? "timedOut: true" : "timedOut: false",
3775
- result.truncated ? "truncated: true" : "truncated: false",
3776
- "",
3777
- result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
3778
- "",
3779
- result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
3780
- ].join("\n"), result);
3781
- }
3782
- },
3783
- {
3784
- name: "read_file",
3785
- label: "Read File",
3786
- description: "Read a text file from an allowlisted path.",
3787
- parameters: Type.Object({
3788
- path: Type.String(),
3789
- maxBytes: Type.Optional(Type.Number({
3790
- minimum: 128,
3791
- maximum: 1e6
3792
- }))
3793
- }),
3794
- execute: async (_toolCallId, params) => {
3795
- audit({
3796
- actor: "tool",
3797
- eventType: "tool.read_file",
3798
- payload: { path: params.path }
3799
- });
3800
- const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
3801
- return textResult(result.content, result);
3802
- }
3803
- },
3804
- {
3805
- name: "write_file",
3806
- label: "Write File",
3807
- description: "Write UTF-8 text content to an allowlisted path.",
3808
- parameters: Type.Object({
3809
- path: Type.String(),
3810
- content: Type.String()
4242
+ const bashTool = {
4243
+ name: "bash",
4244
+ label: "Bash",
4245
+ description: "Run a shell command on allowed command prefixes only.",
4246
+ parameters: Type.Object({
4247
+ command: Type.String(),
4248
+ timeoutMs: Type.Optional(Type.Number({
4249
+ minimum: 1e3,
4250
+ maximum: 12e4
4251
+ }))
4252
+ }),
4253
+ execute: async (_toolCallId, params) => {
4254
+ audit({
4255
+ actor: "tool",
4256
+ eventType: "tool.exec",
4257
+ payload: { command: params.command }
4258
+ });
4259
+ const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
4260
+ return textResult([
4261
+ `exitCode: ${result.exitCode}`,
4262
+ result.timedOut ? "timedOut: true" : "timedOut: false",
4263
+ result.truncated ? "truncated: true" : "truncated: false",
4264
+ "",
4265
+ result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
4266
+ "",
4267
+ result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
4268
+ ].join("\n"), result);
4269
+ }
4270
+ };
4271
+ const readFileTool = {
4272
+ name: "read_file",
4273
+ label: "Read File",
4274
+ description: "Read a text file from an allowlisted path.",
4275
+ parameters: Type.Object({
4276
+ path: Type.String(),
4277
+ maxBytes: Type.Optional(Type.Number({
4278
+ minimum: 128,
4279
+ maximum: 1e6
4280
+ }))
4281
+ }),
4282
+ execute: async (_toolCallId, params) => {
4283
+ audit({
4284
+ actor: "tool",
4285
+ eventType: "tool.read_file",
4286
+ payload: { path: params.path }
4287
+ });
4288
+ const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
4289
+ return textResult(result.content, result);
4290
+ }
4291
+ };
4292
+ const writeFileTool = {
4293
+ name: "write_file",
4294
+ label: "Write File",
4295
+ description: "Write UTF-8 text content to an allowlisted path.",
4296
+ parameters: Type.Object({
4297
+ path: Type.String(),
4298
+ content: Type.String()
4299
+ }),
4300
+ execute: async (_toolCallId, params) => {
4301
+ audit({
4302
+ actor: "tool",
4303
+ eventType: "tool.write_file",
4304
+ payload: {
4305
+ path: params.path,
4306
+ bytes: Buffer.byteLength(params.content, "utf8")
4307
+ }
4308
+ });
4309
+ const result = await runtime.writeFile(params.path, params.content);
4310
+ return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
4311
+ }
4312
+ };
4313
+ const webSearchTool = {
4314
+ name: "web_search",
4315
+ label: "Web Search",
4316
+ description: "Fetch a URL and return readable article text.",
4317
+ parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
4318
+ execute: async (_toolCallId, params) => {
4319
+ audit({
4320
+ actor: "tool",
4321
+ eventType: "tool.fetch_web",
4322
+ payload: { url: params.url }
4323
+ });
4324
+ const result = await runtime.fetchWeb(params.url);
4325
+ return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
4326
+ }
4327
+ };
4328
+ const fetchPodcastFeedTool = {
4329
+ name: "fetch_podcast_feed",
4330
+ label: "Fetch Podcast Feed",
4331
+ description: "Fetch one or more RSS podcast feeds and return recent episodes.",
4332
+ parameters: Type.Object({
4333
+ urls: Type.Array(Type.String({ format: "uri" }), {
4334
+ minItems: 1,
4335
+ maxItems: 20
3811
4336
  }),
3812
- execute: async (_toolCallId, params) => {
3813
- audit({
3814
- actor: "tool",
3815
- eventType: "tool.write_file",
3816
- payload: {
3817
- path: params.path,
3818
- bytes: Buffer.byteLength(params.content, "utf8")
3819
- }
4337
+ limitPerFeed: Type.Optional(Type.Number({
4338
+ minimum: 1,
4339
+ maximum: 50
4340
+ }))
4341
+ }),
4342
+ execute: async (_toolCallId, params) => {
4343
+ const limit = params.limitPerFeed ?? 5;
4344
+ const output = [];
4345
+ for (const url of params.urls) try {
4346
+ const feed = await parser.parseURL(url);
4347
+ const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
4348
+ title: item.title ?? "Untitled episode",
4349
+ publishedAt: item.pubDate || item.isoDate || null,
4350
+ link: item.link ?? "",
4351
+ summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
4352
+ }));
4353
+ output.push({
4354
+ url,
4355
+ title: feed.title ?? "Untitled feed",
4356
+ episodes
3820
4357
  });
3821
- const result = await runtime.writeFile(params.path, params.content);
3822
- return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
3823
- }
3824
- },
3825
- {
3826
- name: "web_search",
3827
- label: "Web Search",
3828
- description: "Fetch a URL and return readable article text.",
3829
- parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
3830
- execute: async (_toolCallId, params) => {
3831
- audit({
3832
- actor: "tool",
3833
- eventType: "tool.fetch_web",
3834
- payload: { url: params.url }
4358
+ } catch (error) {
4359
+ output.push({
4360
+ url,
4361
+ title: "Unknown feed",
4362
+ episodes: [],
4363
+ error: normalizeErrorMessage(error)
3835
4364
  });
3836
- const result = await runtime.fetchWeb(params.url);
3837
- return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
3838
4365
  }
3839
- },
3840
- {
3841
- name: "fetch_podcast_feed",
3842
- label: "Fetch Podcast Feed",
3843
- description: "Fetch one or more RSS podcast feeds and return recent episodes.",
3844
- parameters: Type.Object({
3845
- urls: Type.Array(Type.String({ format: "uri" }), {
3846
- minItems: 1,
3847
- maxItems: 20
3848
- }),
3849
- limitPerFeed: Type.Optional(Type.Number({
3850
- minimum: 1,
3851
- maximum: 50
3852
- }))
3853
- }),
3854
- execute: async (_toolCallId, params) => {
3855
- const limit = params.limitPerFeed ?? 5;
3856
- const output = [];
3857
- for (const url of params.urls) try {
3858
- const feed = await parser.parseURL(url);
3859
- const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
3860
- title: item.title ?? "Untitled episode",
3861
- publishedAt: item.pubDate || item.isoDate || null,
3862
- link: item.link ?? "",
3863
- summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
3864
- }));
3865
- output.push({
3866
- url,
3867
- title: feed.title ?? "Untitled feed",
3868
- episodes
3869
- });
3870
- } catch (error) {
3871
- output.push({
3872
- url,
3873
- title: "Unknown feed",
3874
- episodes: [],
3875
- error: normalizeErrorMessage(error)
3876
- });
4366
+ audit({
4367
+ actor: "tool",
4368
+ eventType: "tool.fetch_podcast_feed",
4369
+ payload: {
4370
+ urls: params.urls,
4371
+ limitPerFeed: limit
3877
4372
  }
3878
- audit({
3879
- actor: "tool",
3880
- eventType: "tool.fetch_podcast_feed",
3881
- payload: {
3882
- urls: params.urls,
3883
- limitPerFeed: limit
3884
- }
3885
- });
3886
- return textResult(output.map((feed) => {
3887
- if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
3888
- const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
3889
- return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
3890
- }).join("\n\n---\n\n"), output);
3891
- }
4373
+ });
4374
+ return textResult(output.map((feed) => {
4375
+ if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
4376
+ const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
4377
+ return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
4378
+ }).join("\n\n---\n\n"), output);
3892
4379
  }
3893
- ];
4380
+ };
4381
+ const tools = [];
4382
+ if (bashEnabled) tools.push(bashTool);
4383
+ tools.push(readFileTool, writeFileTool, webSearchTool, fetchPodcastFeedTool);
4384
+ return tools;
3894
4385
  }
3895
4386
 
3896
4387
  //#endregion
@@ -4140,7 +4631,7 @@ function renderSnapshots(snapshots) {
4140
4631
  }
4141
4632
  return lines.join("\n");
4142
4633
  }
4143
- function printJson$1(value) {
4634
+ function printJson$2(value) {
4144
4635
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4145
4636
  }
4146
4637
  function parseCompatChannel(raw) {
@@ -4186,7 +4677,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4186
4677
  const snapshots = buildChannelCompatSnapshots(deps.readFileConfig());
4187
4678
  const payload = { channels: snapshots };
4188
4679
  if (options.json) {
4189
- printJson$1(payload);
4680
+ printJson$2(payload);
4190
4681
  return;
4191
4682
  }
4192
4683
  process.stdout.write(`${renderSnapshots(snapshots)}\n`);
@@ -4194,7 +4685,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4194
4685
  addChannelConnectionOptions(channels.command("status").description("Show channel status via gateway with config fallback").action(async (options) => {
4195
4686
  const report = await resolveChannelsStatusWithFallback(deps.readFileConfig(), toRpcOptions$2(options), deps.rpcCall);
4196
4687
  if (options.json) {
4197
- printJson$1(report);
4688
+ printJson$2(report);
4198
4689
  return;
4199
4690
  }
4200
4691
  process.stdout.write(`Source: ${report.source}\n`);
@@ -4218,7 +4709,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4218
4709
  tokenSource: channel === "telegram" ? normalizeTokenSource(next.channels.telegram.accounts[accountId]?.botToken ?? "") : normalizeTokenSource(next.channels.discord.botToken)
4219
4710
  };
4220
4711
  if (options.json) {
4221
- printJson$1(payload);
4712
+ printJson$2(payload);
4222
4713
  return;
4223
4714
  }
4224
4715
  process.stdout.write(`Updated ${channel}/${accountId}: enabled=true, token=${payload.tokenSource}.\n`);
@@ -4241,7 +4732,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4241
4732
  deleted: Boolean(options.delete)
4242
4733
  };
4243
4734
  if (options.json) {
4244
- printJson$1(payload);
4735
+ printJson$2(payload);
4245
4736
  return;
4246
4737
  }
4247
4738
  process.stdout.write(`Updated ${channel}/${accountId}: enabled=false, token=${payload.tokenSource}.\n`);
@@ -4264,7 +4755,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4264
4755
  note: "HOVClaw uses token-based setup for Telegram/Discord. No interactive OAuth/pairing flow is required."
4265
4756
  };
4266
4757
  if (options.json) {
4267
- printJson$1(payload);
4758
+ printJson$2(payload);
4268
4759
  return;
4269
4760
  }
4270
4761
  process.stdout.write(`${payload.note}\n`);
@@ -4286,7 +4777,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4286
4777
  payload
4287
4778
  };
4288
4779
  if (options.json) {
4289
- printJson$1(result);
4780
+ printJson$2(result);
4290
4781
  return;
4291
4782
  }
4292
4783
  process.stdout.write("Channel logout request sent via gateway.\n");
@@ -4300,7 +4791,7 @@ function registerChannelsCommands(program, deps = defaultChannelsCommandDeps) {
4300
4791
  gatewayError
4301
4792
  };
4302
4793
  if (options.json) {
4303
- printJson$1(result);
4794
+ printJson$2(result);
4304
4795
  return;
4305
4796
  }
4306
4797
  process.stdout.write(`Gateway logout failed (${gatewayError}). Used local fallback: ${fallback.ok ? "ok" : "failed"}.\n`);
@@ -4325,7 +4816,7 @@ function toRpcOptions$1(options) {
4325
4816
  timeoutMs: parseTimeoutMs$1(options.timeout)
4326
4817
  };
4327
4818
  }
4328
- function printJson(value) {
4819
+ function printJson$1(value) {
4329
4820
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4330
4821
  }
4331
4822
  function registerModelsCommands(program) {
@@ -4333,29 +4824,187 @@ function registerModelsCommands(program) {
4333
4824
  models.command("list").description("List configured/known models").option("--url <url>", "Gateway URL").option("--token <token>", "Gateway auth token").option("--password <password>", "Gateway auth password").option("--timeout <ms>", "Gateway timeout in ms").option("--json", "Print JSON output").action(async (options) => {
4334
4825
  const payload = await callGatewayRpc("models.list", void 0, toRpcOptions$1(options));
4335
4826
  if (options.json) {
4336
- printJson(payload);
4827
+ printJson$1(payload);
4337
4828
  return;
4338
4829
  }
4339
- printJson(payload);
4830
+ printJson$1(payload);
4340
4831
  });
4341
4832
  models.command("status").description("Show effective model policy and defaults").option("--url <url>", "Gateway URL").option("--token <token>", "Gateway auth token").option("--password <password>", "Gateway auth password").option("--timeout <ms>", "Gateway timeout in ms").option("--json", "Print JSON output").action(async (options) => {
4342
4833
  const payload = await callGatewayRpc("models.status", void 0, toRpcOptions$1(options));
4343
4834
  if (options.json) {
4344
- printJson(payload);
4835
+ printJson$1(payload);
4345
4836
  return;
4346
4837
  }
4347
- printJson(payload);
4838
+ printJson$1(payload);
4348
4839
  });
4349
4840
  models.command("set").description("Set model for a target route").requiredOption("--model <model>", "Model reference provider:model").option("--target <target>", "interactive|interactiveFallback|discord|cron").option("--url <url>", "Gateway URL").option("--token <token>", "Gateway auth token").option("--password <password>", "Gateway auth password").option("--timeout <ms>", "Gateway timeout in ms").option("--json", "Print JSON output").action(async (options) => {
4350
4841
  const payload = await callGatewayRpc("models.set", {
4351
4842
  model: options.model,
4352
4843
  target: options.target
4353
4844
  }, toRpcOptions$1(options));
4845
+ if (options.json) {
4846
+ printJson$1(payload);
4847
+ return;
4848
+ }
4849
+ printJson$1(payload);
4850
+ });
4851
+ }
4852
+
4853
+ //#endregion
4854
+ //#region src/channels/telegram-pairing-store.ts
4855
+ function emptyState() {
4856
+ return {
4857
+ approved: {},
4858
+ pending: {}
4859
+ };
4860
+ }
4861
+ function nowIso() {
4862
+ return (/* @__PURE__ */ new Date()).toISOString();
4863
+ }
4864
+ function randomCode() {
4865
+ return Math.random().toString(36).slice(2, 8).toUpperCase();
4866
+ }
4867
+ var TelegramPairingStore = class {
4868
+ filePath;
4869
+ constructor(storeDir) {
4870
+ this.filePath = path.join(storeDir, "telegram-pairing.json");
4871
+ }
4872
+ readState() {
4873
+ if (!fs.existsSync(this.filePath)) return emptyState();
4874
+ try {
4875
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
4876
+ if (!parsed || typeof parsed !== "object") return emptyState();
4877
+ return {
4878
+ approved: parsed.approved ?? {},
4879
+ pending: parsed.pending ?? {}
4880
+ };
4881
+ } catch {
4882
+ return emptyState();
4883
+ }
4884
+ }
4885
+ writeState(state) {
4886
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
4887
+ fs.writeFileSync(this.filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
4888
+ }
4889
+ isApproved(accountId, userId) {
4890
+ return (this.readState().approved[accountId] ?? []).includes(userId);
4891
+ }
4892
+ ensurePendingCode(accountId, userId) {
4893
+ const state = this.readState();
4894
+ const pendingByAccount = state.pending[accountId] ?? {};
4895
+ const existing = Object.entries(pendingByAccount).find(([, entry]) => entry.userId === userId);
4896
+ if (existing) return existing[0];
4897
+ const code = randomCode();
4898
+ state.pending[accountId] = {
4899
+ ...pendingByAccount,
4900
+ [code]: {
4901
+ userId,
4902
+ createdAt: nowIso()
4903
+ }
4904
+ };
4905
+ this.writeState(state);
4906
+ return code;
4907
+ }
4908
+ approveByCode(accountId, code) {
4909
+ const state = this.readState();
4910
+ const pendingByAccount = state.pending[accountId] ?? {};
4911
+ const entry = pendingByAccount[code];
4912
+ if (!entry) return { ok: false };
4913
+ const approved = new Set(state.approved[accountId] ?? []);
4914
+ approved.add(entry.userId);
4915
+ state.approved[accountId] = Array.from(approved).sort((a, b) => a.localeCompare(b));
4916
+ delete pendingByAccount[code];
4917
+ state.pending[accountId] = pendingByAccount;
4918
+ this.writeState(state);
4919
+ return {
4920
+ ok: true,
4921
+ userId: entry.userId
4922
+ };
4923
+ }
4924
+ rejectByCode(accountId, code) {
4925
+ const state = this.readState();
4926
+ const pendingByAccount = state.pending[accountId] ?? {};
4927
+ const entry = pendingByAccount[code];
4928
+ if (!entry) return { ok: false };
4929
+ delete pendingByAccount[code];
4930
+ state.pending[accountId] = pendingByAccount;
4931
+ this.writeState(state);
4932
+ return {
4933
+ ok: true,
4934
+ userId: entry.userId
4935
+ };
4936
+ }
4937
+ };
4938
+
4939
+ //#endregion
4940
+ //#region src/cli/pairing.ts
4941
+ const APPROVE_USAGE = "Usage: hovclaw pairing approve <channel> <code> (or: hovclaw pairing approve --channel <channel> <code>)";
4942
+ const APPROVE_FLAG_USAGE = "Too many arguments. Use: hovclaw pairing approve --channel <channel> <code>";
4943
+ const defaultPairingCommandDeps = {
4944
+ loadAppConfig: loadConfig,
4945
+ createTelegramPairingStore: (storeDir) => new TelegramPairingStore(storeDir),
4946
+ notifyTelegramPairingApproved: async (params) => {
4947
+ const { TelegramChannel } = await Promise.resolve().then(() => telegram_exports);
4948
+ await new TelegramChannel({ accountId: params.accountId }).sendMessage({
4949
+ channel: "telegram",
4950
+ chatId: params.userId,
4951
+ accountId: params.accountId
4952
+ }, "Your pairing request was approved. You can now send messages to the assistant.");
4953
+ }
4954
+ };
4955
+ function printJson(value) {
4956
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4957
+ }
4958
+ function resolvePairingApproveInput(params) {
4959
+ const channelOption = params.channelOption?.trim();
4960
+ if (channelOption) {
4961
+ if (params.code !== void 0) throw new Error(APPROVE_FLAG_USAGE);
4962
+ const code = params.codeOrChannel.trim().toUpperCase();
4963
+ if (!code) throw new Error(APPROVE_USAGE);
4964
+ return {
4965
+ channel: channelOption.toLowerCase(),
4966
+ code
4967
+ };
4968
+ }
4969
+ if (params.code === void 0) throw new Error(APPROVE_USAGE);
4970
+ const channel = params.codeOrChannel.trim().toLowerCase();
4971
+ const code = params.code.trim().toUpperCase();
4972
+ if (!channel || !code) throw new Error(APPROVE_USAGE);
4973
+ return {
4974
+ channel,
4975
+ code
4976
+ };
4977
+ }
4978
+ function registerPairingCommands(program, deps = defaultPairingCommandDeps) {
4979
+ program.command("pairing").description("Pairing helpers").command("approve").description("Approve a pairing code and allow that sender").option("--channel <channel>", "Channel (telegram)").option("--account <id>", "Telegram account id").option("--json", "Print JSON output").argument("<codeOrChannel>", "Pairing code (or channel when using 2 args)").argument("[code]", "Pairing code (when channel is passed as the 1st arg)").action(async (codeOrChannel, code, options) => {
4980
+ const parsed = resolvePairingApproveInput({
4981
+ codeOrChannel,
4982
+ code,
4983
+ channelOption: options.channel
4984
+ });
4985
+ if (parsed.channel !== "telegram") throw new Error(`Channel ${parsed.channel} does not support pairing`);
4986
+ const loadedConfig = deps.loadAppConfig();
4987
+ const defaultAccountId = loadedConfig.channels.telegram.defaultAccountId.trim() || "default";
4988
+ const accountId = options.account?.trim() || defaultAccountId || "default";
4989
+ const result = deps.createTelegramPairingStore(loadedConfig.storeDir).approveByCode(accountId, parsed.code);
4990
+ if (!result.ok) throw new Error(`No pending pairing request found for code: ${parsed.code}`);
4991
+ if (result.userId) await deps.notifyTelegramPairingApproved({
4992
+ accountId,
4993
+ userId: result.userId,
4994
+ code: parsed.code
4995
+ }).catch(() => void 0);
4996
+ const payload = {
4997
+ ok: true,
4998
+ channel: "telegram",
4999
+ accountId,
5000
+ code: parsed.code,
5001
+ ...result.userId ? { userId: result.userId } : {}
5002
+ };
4354
5003
  if (options.json) {
4355
5004
  printJson(payload);
4356
5005
  return;
4357
5006
  }
4358
- printJson(payload);
5007
+ process.stdout.write(`Approved pairing: channel=telegram account=${accountId}${result.userId ? ` user=${result.userId}` : ""}\n`);
4359
5008
  });
4360
5009
  }
4361
5010
 
@@ -4590,6 +5239,33 @@ function sleep(ms) {
4590
5239
  setTimeout(resolve, ms);
4591
5240
  });
4592
5241
  }
5242
+ function shellEscape(value) {
5243
+ return `'${value.replace(/'/g, "'\\''")}'`;
5244
+ }
5245
+ function spawnDetachedDaemonWithDelay(launch, logPath, env, delayMs) {
5246
+ const outFd = fs.openSync(logPath, "a");
5247
+ const errFd = fs.openSync(logPath, "a");
5248
+ try {
5249
+ const delaySeconds = Math.max(0, delayMs) / 1e3;
5250
+ const command = [launch.command, ...launch.args].map((entry) => shellEscape(entry)).join(" ");
5251
+ const child = spawn("/bin/sh", ["-lc", `sleep ${delaySeconds.toFixed(3)}; exec ${command}`], {
5252
+ cwd: launch.cwd,
5253
+ env,
5254
+ detached: true,
5255
+ stdio: [
5256
+ "ignore",
5257
+ outFd,
5258
+ errFd
5259
+ ]
5260
+ });
5261
+ child.unref();
5262
+ if (child.pid === void 0) throw new Error("Failed to spawn replacement daemon process.");
5263
+ return child.pid;
5264
+ } finally {
5265
+ fs.closeSync(outFd);
5266
+ fs.closeSync(errFd);
5267
+ }
5268
+ }
4593
5269
  function parseLogLinesOption(raw) {
4594
5270
  const parsed = Number(raw);
4595
5271
  if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid line count: ${raw}`);
@@ -4778,6 +5454,42 @@ async function restartDaemon(env = process.env) {
4778
5454
  await stopDaemon(env);
4779
5455
  return startDaemon(env);
4780
5456
  }
5457
+ async function requestDaemonRestartFromCurrentProcess(env = process.env) {
5458
+ const launchd = getLaunchdStatus(env);
5459
+ if (launchd.supported && launchd.installed) {
5460
+ if (!launchd.loaded) runLaunchctl([
5461
+ "bootstrap",
5462
+ launchd.domain,
5463
+ launchd.plistPath
5464
+ ], false);
5465
+ runLaunchctl(["enable", `${launchd.domain}/${launchd.label}`], true);
5466
+ runLaunchctl([
5467
+ "kickstart",
5468
+ "-k",
5469
+ `${launchd.domain}/${launchd.label}`
5470
+ ], false);
5471
+ clearState(env);
5472
+ return {
5473
+ mode: "launchd-kickstart",
5474
+ pid: launchd.pid
5475
+ };
5476
+ }
5477
+ const launch = resolveLaunchSpec();
5478
+ const logPath = getLogPath(env);
5479
+ const pid = spawnDetachedDaemonWithDelay(launch, logPath, env, 350);
5480
+ saveState({
5481
+ pid,
5482
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5483
+ command: launch.command,
5484
+ args: launch.args,
5485
+ cwd: launch.cwd,
5486
+ logPath
5487
+ }, env);
5488
+ return {
5489
+ mode: "spawn-reexec",
5490
+ pid
5491
+ };
5492
+ }
4781
5493
  function readDaemonLogTail(lineCount = 100, env = process.env) {
4782
5494
  const logPath = getLogPath(env);
4783
5495
  if (!fs.existsSync(logPath)) return "";
@@ -4919,7 +5631,7 @@ function registerGatewayCommands(program, deps = defaultGatewayCommandDeps) {
4919
5631
  gateway.command("run").description("Run HOVClaw daemon with gateway in foreground").action(async () => {
4920
5632
  if (!config.gateway.enabled) throw new Error("Gateway is disabled in config. Enable gateway.enabled first.");
4921
5633
  process.stdout.write(`Starting HOVClaw gateway on ${config.gateway.host}:${config.gateway.port}...\n`);
4922
- await import("./src-D_mIwpeq.js");
5634
+ await import("./src-Y6AqidKn.js");
4923
5635
  });
4924
5636
  gateway.command("open-ui").description("Open the minimal gateway web UI in your default browser").option("--no-open", "Print URL but do not open browser").option("--json", "Print JSON output").action(async (options) => {
4925
5637
  const url = resolveGatewayWebUiUrl();
@@ -4984,6 +5696,86 @@ function registerGatewayCommands(program, deps = defaultGatewayCommandDeps) {
4984
5696
  }));
4985
5697
  }
4986
5698
 
5699
+ //#endregion
5700
+ //#region src/cli/skills.ts
5701
+ const SKILL_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
5702
+ async function createSkillScaffold(name, description) {
5703
+ const normalizedName = name.trim();
5704
+ if (!normalizedName) throw new Error("Skill name is required.");
5705
+ if (!SKILL_NAME_RE.test(normalizedName)) throw new Error("Invalid skill name. Use 1-64 chars: letters, numbers, dot, underscore, or hyphen.");
5706
+ const skillDir = path.join(config.skillsDir, normalizedName);
5707
+ const skillPath = path.join(skillDir, "SKILL.md");
5708
+ await fs$1.mkdir(skillDir, { recursive: false });
5709
+ const content = [
5710
+ "---",
5711
+ `name: ${normalizedName}`,
5712
+ `description: ${description?.trim() || `Skill ${normalizedName}`}`,
5713
+ "---",
5714
+ "",
5715
+ `# ${normalizedName}`,
5716
+ "",
5717
+ "Describe what this skill does and how it should be used."
5718
+ ].join("\n");
5719
+ await fs$1.writeFile(skillPath, `${content}\n`, "utf8");
5720
+ return skillPath;
5721
+ }
5722
+ function registerSkillsCommands(program) {
5723
+ const skills = program.command("skills").description("Inspect and create local skills");
5724
+ skills.command("list").description("List installed skills").option("--json", "Print JSON output").action((options) => {
5725
+ const names = listAvailableSkills();
5726
+ if (options.json) {
5727
+ const payload = names.map((name) => {
5728
+ return {
5729
+ name,
5730
+ description: loadSkill(name)?.frontmatter.description ?? null
5731
+ };
5732
+ });
5733
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
5734
+ return;
5735
+ }
5736
+ if (names.length === 0) {
5737
+ process.stdout.write("No skills found.\n");
5738
+ return;
5739
+ }
5740
+ for (const name of names) {
5741
+ const description = loadSkill(name)?.frontmatter.description?.trim();
5742
+ process.stdout.write(`- ${name}${description ? `: ${description}` : ""}\n`);
5743
+ }
5744
+ });
5745
+ skills.command("info <name>").description("Show details for one skill").option("--json", "Print JSON output").action((name, options) => {
5746
+ const skill = loadSkill(name);
5747
+ if (!skill) throw new Error(`Skill not found: ${name}`);
5748
+ if (options.json) {
5749
+ process.stdout.write(`${JSON.stringify(skill, null, 2)}\n`);
5750
+ return;
5751
+ }
5752
+ process.stdout.write(`name: ${skill.name}\n`);
5753
+ if (skill.frontmatter.description) process.stdout.write(`description: ${skill.frontmatter.description}\n`);
5754
+ process.stdout.write(`instructions_chars: ${skill.instructions.length}\n`);
5755
+ });
5756
+ skills.command("check").description("Validate installed skill metadata").option("--json", "Print JSON output").action((options) => {
5757
+ const report = listAvailableSkills().map((name) => {
5758
+ const skill = loadSkill(name);
5759
+ const hasName = Boolean(skill?.frontmatter.name && skill.frontmatter.name !== "unknown");
5760
+ const hasDescription = Boolean(skill?.frontmatter.description?.trim());
5761
+ return {
5762
+ name,
5763
+ ok: Boolean(skill && hasName),
5764
+ hasDescription
5765
+ };
5766
+ });
5767
+ if (options.json) {
5768
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
5769
+ return;
5770
+ }
5771
+ for (const entry of report) process.stdout.write(`${entry.ok ? "ok" : "invalid"} ${entry.name}${entry.hasDescription ? "" : " (missing description)"}\n`);
5772
+ });
5773
+ skills.command("init <name>").description("Create a new local skill scaffold").option("-d, --description <text>", "Skill description").action(async (name, options) => {
5774
+ const skillPath = await createSkillScaffold(name, options.description);
5775
+ process.stdout.write(`Created ${skillPath}\n`);
5776
+ });
5777
+ }
5778
+
4987
5779
  //#endregion
4988
5780
  //#region src/cli/status.ts
4989
5781
  function statusForDiscord(loadedConfig) {
@@ -5137,19 +5929,38 @@ function loadCliVersion() {
5137
5929
  }
5138
5930
  function registerCoreCommands(program) {
5139
5931
  program.command("onboard").description("Run interactive onboarding wizard").action(async () => {
5140
- await (await import("./onboard-Cgbgh2Jn.js")).main();
5932
+ await (await import("./onboard-DL6VDf50.js")).main();
5141
5933
  });
5142
5934
  program.command("login [provider]").description("Run OAuth login for a provider").action(async (provider) => {
5143
- await (await import("./login-Ca1_XRup.js")).main(provider ? [provider] : []);
5935
+ await (await import("./login-BwvBMKdz.js")).main(provider ? [provider] : []);
5144
5936
  });
5145
5937
  program.command("doctor").description("Run installation and config health checks").option("--fix", "Attempt auto-repair").option("--repair", "Attempt auto-repair").option("--deep", "Run deep checks").option("--json", "Print JSON output").action(async (options) => {
5146
- const module = await import("./doctor-I8YVuapp.js");
5938
+ const module = await import("./doctor-D52M80De.js");
5147
5939
  const args = [];
5148
5940
  if (options.fix || options.repair) args.push("--fix");
5149
5941
  if (options.deep) args.push("--deep");
5150
5942
  if (options.json) args.push("--json");
5151
5943
  await module.main(args);
5152
5944
  });
5945
+ program.command("reset").description("Reset local config/state while keeping HovClaw installed").option("--scope <scope>", "config|config+creds+sessions|full").option("--yes", "Skip confirmation prompts").option("--non-interactive", "Disable prompts (requires --scope + --yes)").option("--dry-run", "Print reset plan without deleting files").option("--json", "Print JSON output").action(async (options) => {
5946
+ const module = await import("./reset-BJUhrojJ.js");
5947
+ try {
5948
+ const result = await module.runResetCommand({
5949
+ scope: options.scope,
5950
+ yes: options.yes,
5951
+ nonInteractive: options.nonInteractive,
5952
+ dryRun: options.dryRun
5953
+ });
5954
+ if (options.json) {
5955
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
5956
+ return;
5957
+ }
5958
+ process.stdout.write(`${module.renderResetResult(result)}\n`);
5959
+ } catch (error) {
5960
+ if ((error instanceof Error ? error.message : String(error)) === "reset_cancelled") return;
5961
+ throw error;
5962
+ }
5963
+ });
5153
5964
  }
5154
5965
  function registerMessageCommands(program) {
5155
5966
  program.command("message").description("Send direct channel messages").command("send").description("Send a message to a channel target").requiredOption("-c, --channel <channel>", "telegram|discord").requiredOption("-t, --to <target>", "Target chat id").requiredOption("-m, --message <text>", "Message text").option("--json", "Print JSON output").action(async (options) => {
@@ -5237,7 +6048,9 @@ async function main() {
5237
6048
  registerDaemonCommands(program);
5238
6049
  registerChannelsCommands(program);
5239
6050
  registerModelsCommands(program);
6051
+ registerPairingCommands(program);
5240
6052
  registerGatewayCommands(program);
6053
+ registerSkillsCommands(program);
5241
6054
  registerCompatCommands(program);
5242
6055
  await program.parseAsync(process.argv);
5243
6056
  }
@@ -5247,4 +6060,4 @@ main().catch((error) => {
5247
6060
  });
5248
6061
 
5249
6062
  //#endregion
5250
- export { hasCredentialsFile as A, detectLegacyEnvConfig as C, getDefaultFileConfig as D, getCredentialsPath as E, saveConfigFile as F, saveCredentials as I, writeOpenClawMirror as L, loadCredentials as M, loadFileConfig as N, getHovclawHome as O, resolveTelegramAccountConfig as P, config as S, getConfigPath as T, resolveModelAlias as _, TelegramChannel as a, parseConnectParams as b, composeSessionKey as c, extractAssistantText as d, toUserFacingAssistantError as f, parseModelRef as g, listConfiguredModelRefs as h, HovClawDb as i, loadConfig as j, hasConfigFile as k, ensureWorkspaceBootstrapForConfig as l, loadSkill as m, LocalHostRuntime as n, DiscordChannel as o, listAvailableSkills as p, ContainerRuntime as r, PiAgentManager as s, createTools as t, extractAssistantError as u, logger as v, ensureConfigFromLegacyEnv as w, parseGatewayFrame as x, PROTOCOL_VERSION as y };
6063
+ export { ensureConfigFromLegacyEnv as A, resolveTelegramAccountConfig as B, resolveModelAlias as C, parseGatewayFrame as D, parseConnectParams as E, hasConfigFile as F, saveCredentials as H, hasCredentialsFile as I, loadConfig as L, getCredentialsPath as M, getDefaultFileConfig as N, config as O, getHovclawHome as P, loadCredentials as R, parseModelRef as S, PROTOCOL_VERSION as T, writeOpenClawMirror as U, saveConfigFile as V, extractAssistantText as _, LocalHostRuntime as a, loadSkill as b, redactSensitiveData as c, PiAgentManager as d, composeSessionKey as f, extractAssistantError as g, resolveAgentWorkspaceDir as h, createTools as i, getConfigPath as j, detectLegacyEnvConfig as k, TelegramChannel as l, ensureWorkspaceBootstrapForConfig as m, stopDaemon as n, ContainerRuntime as o, WORKSPACE_CONTEXT_FILE_ORDER as p, TelegramPairingStore as r, HovClawDb as s, requestDaemonRestartFromCurrentProcess as t, DiscordChannel as u, toUserFacingAssistantError as v, logger as w, listConfiguredModelRefs as x, listAvailableSkills as y, loadFileConfig as z };