hovclaw 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/{doctor-I8YVuapp.js → doctor-D52M80De.js} +42 -4
- package/dist/gateway/ui/app.js +3 -3
- package/dist/gateway/ui/credentials.d.ts +1 -2
- package/dist/gateway/ui/credentials.js +5 -7
- package/dist/gateway/ui/index.html +177 -204
- package/dist/gateway/ui/styles.css +495 -101
- package/dist/hovclaw.js +1049 -236
- package/dist/index.js +2100 -504
- package/dist/{login-Ca1_XRup.js → login-BwvBMKdz.js} +2 -2
- package/dist/{onboard-Cgbgh2Jn.js → onboard-DL6VDf50.js} +43 -13
- package/dist/reset-BJUhrojJ.js +165 -0
- package/dist/{src-D_mIwpeq.js → src-Y6AqidKn.js} +1087 -259
- package/package.json +4 -1
- /package/dist/{oauth-6sxOTr3f.js → oauth-CQsXP0kP.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,11 +11,11 @@ import pino from "pino";
|
|
|
11
11
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
12
|
import { parse } from "yaml";
|
|
13
13
|
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
14
|
+
import crypto, { randomUUID, timingSafeEqual } from "node:crypto";
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import Database from "better-sqlite3";
|
|
17
18
|
import WebSocket, { WebSocketServer } from "ws";
|
|
18
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
19
19
|
import { JSDOM } from "jsdom";
|
|
20
20
|
import { Readability } from "@mozilla/readability";
|
|
21
21
|
import { Cron } from "croner";
|
|
@@ -25,6 +25,12 @@ import Parser from "rss-parser";
|
|
|
25
25
|
dotenv.config();
|
|
26
26
|
const PROJECT_ROOT = process.cwd();
|
|
27
27
|
const DEFAULT_WORKSPACE_PATH = "~/.hovclaw/workspace";
|
|
28
|
+
const DEFAULT_SHARED_SKILLS_PATH = "~/.agents/skills";
|
|
29
|
+
const USER_CONTENT_DIRS = ["agents"];
|
|
30
|
+
const USER_STATE_DIRS = ["store", "data"];
|
|
31
|
+
const ensuredHomeContentDirs = /* @__PURE__ */ new Set();
|
|
32
|
+
const ensuredHomeStateDirs = /* @__PURE__ */ new Set();
|
|
33
|
+
const ensuredSharedSkillsDirs = /* @__PURE__ */ new Set();
|
|
28
34
|
const DEFAULT_FILE_CONFIG = {
|
|
29
35
|
assistant: { name: "Andy" },
|
|
30
36
|
agents: {
|
|
@@ -55,6 +61,18 @@ const DEFAULT_FILE_CONFIG = {
|
|
|
55
61
|
aliases: {},
|
|
56
62
|
allowlist: []
|
|
57
63
|
},
|
|
64
|
+
commands: {
|
|
65
|
+
native: "auto",
|
|
66
|
+
nativeSkills: "auto",
|
|
67
|
+
defaultThinkingLevel: "medium",
|
|
68
|
+
text: true,
|
|
69
|
+
config: false,
|
|
70
|
+
debug: false,
|
|
71
|
+
bash: false,
|
|
72
|
+
restart: false,
|
|
73
|
+
useAccessGroups: true,
|
|
74
|
+
allowFrom: {}
|
|
75
|
+
},
|
|
58
76
|
runtime: {
|
|
59
77
|
mode: "local",
|
|
60
78
|
containerImage: "ghcr.io/mariozechner/pi-agent:latest",
|
|
@@ -78,7 +96,8 @@ const DEFAULT_FILE_CONFIG = {
|
|
|
78
96
|
"head",
|
|
79
97
|
"tail",
|
|
80
98
|
"wc"
|
|
81
|
-
]
|
|
99
|
+
],
|
|
100
|
+
tools: { bashEnabled: false }
|
|
82
101
|
},
|
|
83
102
|
channels: {
|
|
84
103
|
discord: {
|
|
@@ -132,7 +151,9 @@ const DEFAULT_FILE_CONFIG = {
|
|
|
132
151
|
},
|
|
133
152
|
auth: {
|
|
134
153
|
token: "",
|
|
135
|
-
password: ""
|
|
154
|
+
password: "",
|
|
155
|
+
allowUnauthenticated: false,
|
|
156
|
+
allowedOrigins: []
|
|
136
157
|
},
|
|
137
158
|
remote: {
|
|
138
159
|
url: "",
|
|
@@ -151,11 +172,36 @@ const telegramWebhookSchema = z.object({
|
|
|
151
172
|
path: z.string().min(1),
|
|
152
173
|
port: z.number().int().positive(),
|
|
153
174
|
secret: z.string()
|
|
175
|
+
}).superRefine((value, ctx) => {
|
|
176
|
+
if (!value.enabled) return;
|
|
177
|
+
if (value.secret.trim().length > 0) return;
|
|
178
|
+
ctx.addIssue({
|
|
179
|
+
code: z.ZodIssueCode.custom,
|
|
180
|
+
message: "Webhook secret is required when webhook mode is enabled.",
|
|
181
|
+
path: ["secret"]
|
|
182
|
+
});
|
|
154
183
|
});
|
|
155
184
|
const telegramCustomCommandSchema = z.object({
|
|
156
185
|
command: z.string().min(1),
|
|
157
186
|
description: z.string().min(1)
|
|
158
187
|
});
|
|
188
|
+
const commandAllowFromSchema = z.record(z.string(), z.array(z.union([z.string(), z.number()])));
|
|
189
|
+
const commandsConfigSchema = z.object({
|
|
190
|
+
native: z.union([z.boolean(), z.literal("auto")]),
|
|
191
|
+
nativeSkills: z.union([z.boolean(), z.literal("auto")]),
|
|
192
|
+
defaultThinkingLevel: z.enum([
|
|
193
|
+
"low",
|
|
194
|
+
"medium",
|
|
195
|
+
"high"
|
|
196
|
+
]),
|
|
197
|
+
text: z.boolean(),
|
|
198
|
+
config: z.boolean(),
|
|
199
|
+
debug: z.boolean(),
|
|
200
|
+
bash: z.boolean(),
|
|
201
|
+
restart: z.boolean(),
|
|
202
|
+
useAccessGroups: z.boolean(),
|
|
203
|
+
allowFrom: commandAllowFromSchema
|
|
204
|
+
});
|
|
159
205
|
const telegramTopicConfigSchema = z.object({
|
|
160
206
|
enabled: z.boolean().optional(),
|
|
161
207
|
requireMention: z.boolean().optional(),
|
|
@@ -215,6 +261,46 @@ const telegramAccountConfigSchema = z.object({
|
|
|
215
261
|
]).optional(),
|
|
216
262
|
textMode: z.enum(["plain", "markdown"]).optional()
|
|
217
263
|
});
|
|
264
|
+
const partialTelegramWebhookSchema = z.object({
|
|
265
|
+
enabled: z.boolean().optional(),
|
|
266
|
+
path: z.string().min(1).optional(),
|
|
267
|
+
port: z.number().int().positive().optional(),
|
|
268
|
+
secret: z.string().optional()
|
|
269
|
+
});
|
|
270
|
+
const partialTelegramAccountConfigSchema = z.object({
|
|
271
|
+
enabled: z.boolean().optional(),
|
|
272
|
+
name: z.string().optional(),
|
|
273
|
+
botToken: z.string().optional(),
|
|
274
|
+
webhook: partialTelegramWebhookSchema.optional(),
|
|
275
|
+
dmPolicy: z.enum([
|
|
276
|
+
"pairing",
|
|
277
|
+
"allowlist",
|
|
278
|
+
"open",
|
|
279
|
+
"disabled"
|
|
280
|
+
]).optional(),
|
|
281
|
+
groupPolicy: z.enum([
|
|
282
|
+
"open",
|
|
283
|
+
"allowlist",
|
|
284
|
+
"disabled"
|
|
285
|
+
]).optional(),
|
|
286
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
287
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
288
|
+
groups: z.record(z.string(), telegramGroupConfigSchema).optional(),
|
|
289
|
+
commands: z.union([z.boolean(), z.literal("auto")]).optional(),
|
|
290
|
+
customCommands: z.array(telegramCustomCommandSchema).optional(),
|
|
291
|
+
reactionNotifications: z.enum([
|
|
292
|
+
"off",
|
|
293
|
+
"own",
|
|
294
|
+
"all"
|
|
295
|
+
]).optional(),
|
|
296
|
+
reactionLevel: z.enum([
|
|
297
|
+
"off",
|
|
298
|
+
"ack",
|
|
299
|
+
"minimal",
|
|
300
|
+
"extensive"
|
|
301
|
+
]).optional(),
|
|
302
|
+
textMode: z.enum(["plain", "markdown"]).optional()
|
|
303
|
+
});
|
|
218
304
|
const fileConfigSchema = z.object({
|
|
219
305
|
assistant: z.object({ name: z.string().min(1) }),
|
|
220
306
|
agents: z.object({
|
|
@@ -267,6 +353,7 @@ const fileConfigSchema = z.object({
|
|
|
267
353
|
aliases: z.record(z.string(), z.string()),
|
|
268
354
|
allowlist: z.array(z.string())
|
|
269
355
|
}),
|
|
356
|
+
commands: commandsConfigSchema,
|
|
270
357
|
runtime: z.object({
|
|
271
358
|
mode: z.enum(["local", "container"]),
|
|
272
359
|
containerImage: z.string().min(1),
|
|
@@ -276,7 +363,8 @@ const fileConfigSchema = z.object({
|
|
|
276
363
|
maxOutputBytes: z.number().int().positive(),
|
|
277
364
|
allowedReadRoots: z.array(z.string().min(1)),
|
|
278
365
|
allowedWriteRoots: z.array(z.string().min(1)),
|
|
279
|
-
allowedCommandPrefixes: z.array(z.string().min(1)).min(1)
|
|
366
|
+
allowedCommandPrefixes: z.array(z.string().min(1)).min(1),
|
|
367
|
+
tools: z.object({ bashEnabled: z.boolean() })
|
|
280
368
|
}),
|
|
281
369
|
channels: z.object({
|
|
282
370
|
discord: z.object({
|
|
@@ -305,7 +393,9 @@ const fileConfigSchema = z.object({
|
|
|
305
393
|
}),
|
|
306
394
|
auth: z.object({
|
|
307
395
|
token: z.string(),
|
|
308
|
-
password: z.string()
|
|
396
|
+
password: z.string(),
|
|
397
|
+
allowUnauthenticated: z.boolean(),
|
|
398
|
+
allowedOrigins: z.array(z.string().min(1))
|
|
309
399
|
}),
|
|
310
400
|
remote: z.object({
|
|
311
401
|
url: z.string(),
|
|
@@ -323,21 +413,37 @@ const partialTelegramConfigSchema = z.object({
|
|
|
323
413
|
enabled: z.boolean().optional(),
|
|
324
414
|
botToken: z.string().optional(),
|
|
325
415
|
defaultAccountId: z.string().optional(),
|
|
326
|
-
webhook:
|
|
327
|
-
accounts: z.record(z.string(),
|
|
416
|
+
webhook: partialTelegramWebhookSchema.optional(),
|
|
417
|
+
accounts: z.record(z.string(), partialTelegramAccountConfigSchema).optional()
|
|
328
418
|
}).optional();
|
|
329
419
|
const partialChannelsSchema = z.object({
|
|
330
420
|
discord: fileConfigSchema.shape.channels.shape.discord.partial().optional(),
|
|
331
421
|
telegram: partialTelegramConfigSchema
|
|
332
422
|
}).optional();
|
|
423
|
+
const partialGatewayConfigSchema = z.object({
|
|
424
|
+
enabled: z.boolean().optional(),
|
|
425
|
+
host: z.string().min(1).optional(),
|
|
426
|
+
port: z.number().int().positive().optional(),
|
|
427
|
+
mode: z.enum(["local", "remote"]).optional(),
|
|
428
|
+
tickIntervalMs: z.number().int().positive().optional(),
|
|
429
|
+
webUi: fileConfigSchema.shape.gateway.shape.webUi.partial().optional(),
|
|
430
|
+
auth: z.object({
|
|
431
|
+
token: z.string().optional(),
|
|
432
|
+
password: z.string().optional(),
|
|
433
|
+
allowUnauthenticated: z.boolean().optional(),
|
|
434
|
+
allowedOrigins: z.array(z.string().min(1)).optional()
|
|
435
|
+
}).optional(),
|
|
436
|
+
remote: fileConfigSchema.shape.gateway.shape.remote.partial().optional()
|
|
437
|
+
}).optional();
|
|
333
438
|
const partialFileConfigSchema = z.object({
|
|
334
439
|
assistant: fileConfigSchema.shape.assistant.partial().optional(),
|
|
335
440
|
agents: fileConfigSchema.shape.agents.partial().optional(),
|
|
336
441
|
bindings: fileConfigSchema.shape.bindings.optional(),
|
|
337
442
|
models: fileConfigSchema.shape.models.partial().optional(),
|
|
443
|
+
commands: commandsConfigSchema.partial().optional(),
|
|
338
444
|
runtime: fileConfigSchema.shape.runtime.partial().optional(),
|
|
339
445
|
channels: partialChannelsSchema,
|
|
340
|
-
gateway:
|
|
446
|
+
gateway: partialGatewayConfigSchema,
|
|
341
447
|
scheduler: fileConfigSchema.shape.scheduler.partial().optional()
|
|
342
448
|
});
|
|
343
449
|
const apiKeyCredentialSchema = z.object({
|
|
@@ -398,12 +504,97 @@ function defaultHovclawHome() {
|
|
|
398
504
|
function getHovclawHome(env = process.env) {
|
|
399
505
|
return path.resolve(expandPath(env.HOVCLAW_HOME || defaultHovclawHome()));
|
|
400
506
|
}
|
|
507
|
+
function defaultSharedSkillsDir(env = process.env) {
|
|
508
|
+
if ((env.NODE_ENV || "production") === "test") return path.join(getHovclawHome(env), ".agents", "skills");
|
|
509
|
+
return DEFAULT_SHARED_SKILLS_PATH;
|
|
510
|
+
}
|
|
511
|
+
function getSharedSkillsDir(env = process.env) {
|
|
512
|
+
const configured = env.HOVCLAW_SKILLS_DIR?.trim();
|
|
513
|
+
return path.resolve(expandPath(configured || defaultSharedSkillsDir(env)));
|
|
514
|
+
}
|
|
401
515
|
function getConfigPath(env = process.env) {
|
|
402
516
|
return path.join(getHovclawHome(env), "config.json");
|
|
403
517
|
}
|
|
404
518
|
function getCredentialsPath(env = process.env) {
|
|
405
519
|
return path.join(getHovclawHome(env), "credentials.json");
|
|
406
520
|
}
|
|
521
|
+
function ensureHomeContentDirs(projectRoot, hovclawHome) {
|
|
522
|
+
for (const dirName of USER_CONTENT_DIRS) {
|
|
523
|
+
const destinationDir = path.join(hovclawHome, dirName);
|
|
524
|
+
if (fs.existsSync(destinationDir)) continue;
|
|
525
|
+
fs.mkdirSync(destinationDir, {
|
|
526
|
+
recursive: true,
|
|
527
|
+
mode: 448
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function ensureMainAgentConfig(agentsDir) {
|
|
532
|
+
const mainAgentPath = path.join(agentsDir, "main", "agent.json");
|
|
533
|
+
if (fs.existsSync(mainAgentPath)) return;
|
|
534
|
+
writeJsonFile(mainAgentPath, {
|
|
535
|
+
name: "Main",
|
|
536
|
+
skills: []
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
function ensureHomeStateDirs(projectRoot, hovclawHome) {
|
|
540
|
+
for (const dirName of USER_STATE_DIRS) {
|
|
541
|
+
const destinationDir = path.join(hovclawHome, dirName);
|
|
542
|
+
if (fs.existsSync(destinationDir)) continue;
|
|
543
|
+
const sourceDir = path.join(projectRoot, dirName);
|
|
544
|
+
if (fs.existsSync(sourceDir)) {
|
|
545
|
+
fs.cpSync(sourceDir, destinationDir, { recursive: true });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
fs.mkdirSync(destinationDir, {
|
|
549
|
+
recursive: true,
|
|
550
|
+
mode: 448
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function ensureHomeContentDirsOnce(projectRoot, hovclawHome) {
|
|
555
|
+
if (ensuredHomeContentDirs.has(hovclawHome)) return;
|
|
556
|
+
ensureHomeContentDirs(projectRoot, hovclawHome);
|
|
557
|
+
ensuredHomeContentDirs.add(hovclawHome);
|
|
558
|
+
}
|
|
559
|
+
function ensureHomeStateDirsOnce(projectRoot, hovclawHome) {
|
|
560
|
+
if (ensuredHomeStateDirs.has(hovclawHome)) return;
|
|
561
|
+
ensureHomeStateDirs(projectRoot, hovclawHome);
|
|
562
|
+
ensuredHomeStateDirs.add(hovclawHome);
|
|
563
|
+
}
|
|
564
|
+
function directoryHasEntries(dirPath) {
|
|
565
|
+
if (!fs.existsSync(dirPath)) return false;
|
|
566
|
+
try {
|
|
567
|
+
return fs.readdirSync(dirPath).length > 0;
|
|
568
|
+
} catch {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function copyDirectoryEntries(sourceDir, destinationDir) {
|
|
573
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
574
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
575
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
576
|
+
fs.cpSync(sourcePath, destinationPath, {
|
|
577
|
+
recursive: true,
|
|
578
|
+
force: true
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function ensureSharedSkillsDir(hovclawHome, env = process.env) {
|
|
583
|
+
const sharedSkillsDir = getSharedSkillsDir(env);
|
|
584
|
+
ensureSecureDir(sharedSkillsDir);
|
|
585
|
+
if (directoryHasEntries(sharedSkillsDir)) return sharedSkillsDir;
|
|
586
|
+
const legacySkillsDir = path.join(hovclawHome, "skills");
|
|
587
|
+
if (!directoryHasEntries(legacySkillsDir)) return sharedSkillsDir;
|
|
588
|
+
copyDirectoryEntries(legacySkillsDir, sharedSkillsDir);
|
|
589
|
+
return sharedSkillsDir;
|
|
590
|
+
}
|
|
591
|
+
function ensureSharedSkillsDirOnce(hovclawHome, env = process.env) {
|
|
592
|
+
const sharedSkillsDir = getSharedSkillsDir(env);
|
|
593
|
+
if (ensuredSharedSkillsDirs.has(sharedSkillsDir)) return sharedSkillsDir;
|
|
594
|
+
ensureSharedSkillsDir(hovclawHome, env);
|
|
595
|
+
ensuredSharedSkillsDirs.add(sharedSkillsDir);
|
|
596
|
+
return sharedSkillsDir;
|
|
597
|
+
}
|
|
407
598
|
function hasConfigFile(env = process.env) {
|
|
408
599
|
return fs.existsSync(getConfigPath(env));
|
|
409
600
|
}
|
|
@@ -499,6 +690,18 @@ function mergeWithDefaults(partial) {
|
|
|
499
690
|
aliases: partial.models?.aliases ?? DEFAULT_FILE_CONFIG.models.aliases,
|
|
500
691
|
allowlist: partial.models?.allowlist ?? DEFAULT_FILE_CONFIG.models.allowlist
|
|
501
692
|
},
|
|
693
|
+
commands: {
|
|
694
|
+
native: partial.commands?.native ?? DEFAULT_FILE_CONFIG.commands.native,
|
|
695
|
+
nativeSkills: partial.commands?.nativeSkills ?? DEFAULT_FILE_CONFIG.commands.nativeSkills,
|
|
696
|
+
defaultThinkingLevel: partial.commands?.defaultThinkingLevel ?? DEFAULT_FILE_CONFIG.commands.defaultThinkingLevel,
|
|
697
|
+
text: partial.commands?.text ?? DEFAULT_FILE_CONFIG.commands.text,
|
|
698
|
+
config: partial.commands?.config ?? DEFAULT_FILE_CONFIG.commands.config,
|
|
699
|
+
debug: partial.commands?.debug ?? DEFAULT_FILE_CONFIG.commands.debug,
|
|
700
|
+
bash: partial.commands?.bash ?? DEFAULT_FILE_CONFIG.commands.bash,
|
|
701
|
+
restart: partial.commands?.restart ?? DEFAULT_FILE_CONFIG.commands.restart,
|
|
702
|
+
useAccessGroups: partial.commands?.useAccessGroups ?? DEFAULT_FILE_CONFIG.commands.useAccessGroups,
|
|
703
|
+
allowFrom: partial.commands?.allowFrom ?? DEFAULT_FILE_CONFIG.commands.allowFrom
|
|
704
|
+
},
|
|
502
705
|
runtime: {
|
|
503
706
|
mode: partial.runtime?.mode ?? DEFAULT_FILE_CONFIG.runtime.mode,
|
|
504
707
|
containerImage: partial.runtime?.containerImage ?? DEFAULT_FILE_CONFIG.runtime.containerImage,
|
|
@@ -508,7 +711,8 @@ function mergeWithDefaults(partial) {
|
|
|
508
711
|
maxOutputBytes: partial.runtime?.maxOutputBytes ?? DEFAULT_FILE_CONFIG.runtime.maxOutputBytes,
|
|
509
712
|
allowedReadRoots: partial.runtime?.allowedReadRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedReadRoots,
|
|
510
713
|
allowedWriteRoots: partial.runtime?.allowedWriteRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedWriteRoots,
|
|
511
|
-
allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes
|
|
714
|
+
allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes,
|
|
715
|
+
tools: { bashEnabled: partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.bashEnabled }
|
|
512
716
|
},
|
|
513
717
|
channels: {
|
|
514
718
|
discord: {
|
|
@@ -531,7 +735,9 @@ function mergeWithDefaults(partial) {
|
|
|
531
735
|
},
|
|
532
736
|
auth: {
|
|
533
737
|
token: partial.gateway?.auth?.token ?? DEFAULT_FILE_CONFIG.gateway.auth.token,
|
|
534
|
-
password: partial.gateway?.auth?.password ?? DEFAULT_FILE_CONFIG.gateway.auth.password
|
|
738
|
+
password: partial.gateway?.auth?.password ?? DEFAULT_FILE_CONFIG.gateway.auth.password,
|
|
739
|
+
allowUnauthenticated: partial.gateway?.auth?.allowUnauthenticated ?? DEFAULT_FILE_CONFIG.gateway.auth.allowUnauthenticated,
|
|
740
|
+
allowedOrigins: partial.gateway?.auth?.allowedOrigins ?? DEFAULT_FILE_CONFIG.gateway.auth.allowedOrigins
|
|
535
741
|
},
|
|
536
742
|
remote: {
|
|
537
743
|
url: partial.gateway?.remote?.url ?? DEFAULT_FILE_CONFIG.gateway.remote.url,
|
|
@@ -596,6 +802,18 @@ function applyEnvOverrides(base, env) {
|
|
|
596
802
|
aliases: base.models.aliases,
|
|
597
803
|
allowlist: base.models.allowlist
|
|
598
804
|
},
|
|
805
|
+
commands: {
|
|
806
|
+
native: base.commands.native,
|
|
807
|
+
nativeSkills: base.commands.nativeSkills,
|
|
808
|
+
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,
|
|
809
|
+
text: toBool(env.COMMANDS_TEXT, base.commands.text),
|
|
810
|
+
config: toBool(env.COMMANDS_CONFIG, base.commands.config),
|
|
811
|
+
debug: toBool(env.COMMANDS_DEBUG, base.commands.debug),
|
|
812
|
+
bash: toBool(env.COMMANDS_BASH, base.commands.bash),
|
|
813
|
+
restart: toBool(env.COMMANDS_RESTART, base.commands.restart),
|
|
814
|
+
useAccessGroups: toBool(env.COMMANDS_USE_ACCESS_GROUPS, base.commands.useAccessGroups),
|
|
815
|
+
allowFrom: base.commands.allowFrom
|
|
816
|
+
},
|
|
599
817
|
runtime: {
|
|
600
818
|
mode: env.RUNTIME_MODE === "container" || env.RUNTIME_MODE === "local" ? env.RUNTIME_MODE : base.runtime.mode,
|
|
601
819
|
containerImage: env.RUNTIME_CONTAINER_IMAGE || base.runtime.containerImage,
|
|
@@ -605,7 +823,8 @@ function applyEnvOverrides(base, env) {
|
|
|
605
823
|
maxOutputBytes: toPositiveInt(env.TOOL_MAX_OUTPUT_BYTES, base.runtime.maxOutputBytes),
|
|
606
824
|
allowedReadRoots: splitCsv(env.ALLOWED_READ_ROOTS, base.runtime.allowedReadRoots),
|
|
607
825
|
allowedWriteRoots: splitCsv(env.ALLOWED_WRITE_ROOTS, base.runtime.allowedWriteRoots),
|
|
608
|
-
allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes)
|
|
826
|
+
allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes),
|
|
827
|
+
tools: { bashEnabled: toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.bashEnabled) }
|
|
609
828
|
},
|
|
610
829
|
channels: {
|
|
611
830
|
discord: {
|
|
@@ -634,7 +853,9 @@ function applyEnvOverrides(base, env) {
|
|
|
634
853
|
},
|
|
635
854
|
auth: {
|
|
636
855
|
token: env.GATEWAY_AUTH_TOKEN || base.gateway.auth.token,
|
|
637
|
-
password: env.GATEWAY_AUTH_PASSWORD || base.gateway.auth.password
|
|
856
|
+
password: env.GATEWAY_AUTH_PASSWORD || base.gateway.auth.password,
|
|
857
|
+
allowUnauthenticated: toBool(env.GATEWAY_ALLOW_UNAUTHENTICATED, base.gateway.auth.allowUnauthenticated),
|
|
858
|
+
allowedOrigins: splitCsv(env.GATEWAY_ALLOWED_ORIGINS, base.gateway.auth.allowedOrigins)
|
|
638
859
|
},
|
|
639
860
|
remote: {
|
|
640
861
|
url: env.GATEWAY_REMOTE_URL || base.gateway.remote.url,
|
|
@@ -655,21 +876,33 @@ function escapeRegex(value) {
|
|
|
655
876
|
function normalizeRoots(paths) {
|
|
656
877
|
return paths.map((entry) => path.resolve(expandPath(entry)));
|
|
657
878
|
}
|
|
879
|
+
function isLegacyProjectRootWorkspace(workspacePath, projectRoot) {
|
|
880
|
+
if (!workspacePath) return false;
|
|
881
|
+
const trimmed = workspacePath.trim();
|
|
882
|
+
if (!trimmed) return false;
|
|
883
|
+
return path.resolve(expandPath(trimmed)) === path.resolve(projectRoot);
|
|
884
|
+
}
|
|
658
885
|
function loadConfig(env = process.env) {
|
|
659
886
|
const merged = applyEnvOverrides(loadConfigFile(env), env);
|
|
660
887
|
const hovclawHome = getHovclawHome(env);
|
|
888
|
+
const agentsDir = path.join(hovclawHome, "agents");
|
|
889
|
+
ensureHomeContentDirsOnce(PROJECT_ROOT, hovclawHome);
|
|
890
|
+
ensureHomeStateDirsOnce(PROJECT_ROOT, hovclawHome);
|
|
891
|
+
ensureMainAgentConfig(agentsDir);
|
|
892
|
+
const sharedSkillsDir = ensureSharedSkillsDirOnce(hovclawHome, env);
|
|
661
893
|
const defaultWorkspace = path.join(hovclawHome, "workspace");
|
|
662
|
-
const
|
|
894
|
+
const configuredWorkspace = merged.agents.defaults.workspace.trim();
|
|
895
|
+
const effectiveWorkspace = !configuredWorkspace || isLegacyProjectRootWorkspace(configuredWorkspace, PROJECT_ROOT) ? defaultWorkspace : configuredWorkspace;
|
|
663
896
|
const assistantName = merged.assistant.name;
|
|
664
897
|
return {
|
|
665
898
|
projectRoot: PROJECT_ROOT,
|
|
666
899
|
hovclawHome,
|
|
667
900
|
configPath: getConfigPath(env),
|
|
668
901
|
credentialsPath: getCredentialsPath(env),
|
|
669
|
-
agentsDir
|
|
670
|
-
skillsDir:
|
|
671
|
-
storeDir: path.join(
|
|
672
|
-
dataDir: path.join(
|
|
902
|
+
agentsDir,
|
|
903
|
+
skillsDir: sharedSkillsDir,
|
|
904
|
+
storeDir: path.join(hovclawHome, "store"),
|
|
905
|
+
dataDir: path.join(hovclawHome, "data"),
|
|
673
906
|
environment: env.NODE_ENV || "development",
|
|
674
907
|
logLevel: env.LOG_LEVEL || "info",
|
|
675
908
|
assistantName,
|
|
@@ -679,12 +912,13 @@ function loadConfig(env = process.env) {
|
|
|
679
912
|
list: merged.agents.list.map((entry) => ({
|
|
680
913
|
id: entry.id,
|
|
681
914
|
name: entry.name,
|
|
682
|
-
workspace: entry.workspace ? path.resolve(expandPath(entry.workspace)) : void 0,
|
|
915
|
+
workspace: entry.workspace ? isLegacyProjectRootWorkspace(entry.workspace, PROJECT_ROOT) ? path.resolve(expandPath(defaultWorkspace)) : path.resolve(expandPath(entry.workspace)) : void 0,
|
|
683
916
|
default: entry.default
|
|
684
917
|
}))
|
|
685
918
|
},
|
|
686
919
|
bindings: merged.bindings,
|
|
687
920
|
models: { ...merged.models },
|
|
921
|
+
commands: { ...merged.commands },
|
|
688
922
|
runtime: {
|
|
689
923
|
mode: merged.runtime.mode,
|
|
690
924
|
containerImage: merged.runtime.containerImage,
|
|
@@ -694,7 +928,8 @@ function loadConfig(env = process.env) {
|
|
|
694
928
|
maxOutputBytes: merged.runtime.maxOutputBytes,
|
|
695
929
|
allowedReadRoots: normalizeRoots(merged.runtime.allowedReadRoots),
|
|
696
930
|
allowedWriteRoots: normalizeRoots(merged.runtime.allowedWriteRoots),
|
|
697
|
-
allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes
|
|
931
|
+
allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes,
|
|
932
|
+
tools: { bashEnabled: merged.runtime.tools.bashEnabled }
|
|
698
933
|
},
|
|
699
934
|
channels: {
|
|
700
935
|
discord: {
|
|
@@ -731,7 +966,9 @@ function loadConfig(env = process.env) {
|
|
|
731
966
|
},
|
|
732
967
|
auth: {
|
|
733
968
|
token: merged.gateway.auth.token,
|
|
734
|
-
password: merged.gateway.auth.password
|
|
969
|
+
password: merged.gateway.auth.password,
|
|
970
|
+
allowUnauthenticated: merged.gateway.auth.allowUnauthenticated,
|
|
971
|
+
allowedOrigins: merged.gateway.auth.allowedOrigins
|
|
735
972
|
},
|
|
736
973
|
remote: {
|
|
737
974
|
url: merged.gateway.remote.url,
|
|
@@ -789,6 +1026,7 @@ function legacyEnvKeys() {
|
|
|
789
1026
|
"ALLOWED_READ_ROOTS",
|
|
790
1027
|
"ALLOWED_WRITE_ROOTS",
|
|
791
1028
|
"ALLOWED_COMMAND_PREFIXES",
|
|
1029
|
+
"RUNTIME_BASH_ENABLED",
|
|
792
1030
|
"TOOL_TIMEOUT_MS",
|
|
793
1031
|
"TOOL_MAX_OUTPUT_BYTES",
|
|
794
1032
|
"ENABLE_DISCORD",
|
|
@@ -808,6 +1046,8 @@ function legacyEnvKeys() {
|
|
|
808
1046
|
"GATEWAY_TICK_INTERVAL_MS",
|
|
809
1047
|
"GATEWAY_AUTH_TOKEN",
|
|
810
1048
|
"GATEWAY_AUTH_PASSWORD",
|
|
1049
|
+
"GATEWAY_ALLOW_UNAUTHENTICATED",
|
|
1050
|
+
"GATEWAY_ALLOWED_ORIGINS",
|
|
811
1051
|
"GATEWAY_REMOTE_URL",
|
|
812
1052
|
"GATEWAY_REMOTE_TOKEN",
|
|
813
1053
|
"GATEWAY_REMOTE_PASSWORD",
|
|
@@ -1115,12 +1355,14 @@ function chunkText(text, maxChars) {
|
|
|
1115
1355
|
//#region src/workspace/bootstrap.ts
|
|
1116
1356
|
const WORKSPACE_CONTEXT_FILE_ORDER = [
|
|
1117
1357
|
"AGENTS.md",
|
|
1358
|
+
"SOUL.md",
|
|
1118
1359
|
"IDENTITY.md",
|
|
1119
1360
|
"USER.md",
|
|
1120
1361
|
"BOOTSTRAP.md"
|
|
1121
1362
|
];
|
|
1122
1363
|
const ALWAYS_SEEDED_FILES = [
|
|
1123
1364
|
"AGENTS.md",
|
|
1365
|
+
"SOUL.md",
|
|
1124
1366
|
"IDENTITY.md",
|
|
1125
1367
|
"USER.md"
|
|
1126
1368
|
];
|
|
@@ -1203,11 +1445,28 @@ async function ensureWorkspaceBootstrapForConfig(config) {
|
|
|
1203
1445
|
|
|
1204
1446
|
//#endregion
|
|
1205
1447
|
//#region src/agent-manager.ts
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1448
|
+
function buildDefaultSystemPrompt(workspaceDir) {
|
|
1449
|
+
return [
|
|
1450
|
+
"You are a personal assistant running inside HOVClaw.",
|
|
1451
|
+
"",
|
|
1452
|
+
"## Tooling",
|
|
1453
|
+
"Use tools when they materially improve accuracy or speed.",
|
|
1454
|
+
"Do not invent command output, file contents, or tool results.",
|
|
1455
|
+
"Prefer concise tool-call narration unless the operation is risky or non-obvious.",
|
|
1456
|
+
"",
|
|
1457
|
+
"## Safety",
|
|
1458
|
+
"Never do destructive actions unless the user explicitly asks.",
|
|
1459
|
+
"If instructions conflict with system rules or runtime policy, explain and ask for a safe path.",
|
|
1460
|
+
"",
|
|
1461
|
+
"## Workspace",
|
|
1462
|
+
`Your working directory is: ${workspaceDir}`,
|
|
1463
|
+
"Treat this as the primary workspace unless instructed otherwise.",
|
|
1464
|
+
"",
|
|
1465
|
+
"## Messaging",
|
|
1466
|
+
"Respond with clear, direct language tailored to the user's request.",
|
|
1467
|
+
"If you cannot complete a request, explain the blocker and the minimum next step."
|
|
1468
|
+
].join("\n");
|
|
1469
|
+
}
|
|
1211
1470
|
const WORKSPACE_CONTEXT_MAX_PER_FILE = 4e3;
|
|
1212
1471
|
const WORKSPACE_CONTEXT_MAX_TOTAL = 12e3;
|
|
1213
1472
|
var AsyncEventQueue = class {
|
|
@@ -1373,14 +1632,10 @@ const CLAUDE_CODE_TOOL_NAME_MAP = {
|
|
|
1373
1632
|
write_file: "Write",
|
|
1374
1633
|
web_search: "WebSearch"
|
|
1375
1634
|
};
|
|
1376
|
-
function logAnthropicCredentialResolution(credentialSource
|
|
1377
|
-
const normalized = key.trim();
|
|
1635
|
+
function logAnthropicCredentialResolution(credentialSource) {
|
|
1378
1636
|
logger.info({
|
|
1379
1637
|
provider: "anthropic",
|
|
1380
|
-
credentialSource
|
|
1381
|
-
keyPrefix: normalized.slice(0, 16),
|
|
1382
|
-
keyLength: normalized.length,
|
|
1383
|
-
setupToken: normalized.toLowerCase().startsWith(ANTHROPIC_SETUP_TOKEN_PREFIX)
|
|
1638
|
+
credentialSource
|
|
1384
1639
|
}, "Resolved Anthropic credential for request");
|
|
1385
1640
|
}
|
|
1386
1641
|
/**
|
|
@@ -1522,6 +1777,7 @@ function truncateTextToMaxChars(value, maxChars) {
|
|
|
1522
1777
|
}
|
|
1523
1778
|
async function buildWorkspacePromptContext(workspaceDir) {
|
|
1524
1779
|
const sections = [];
|
|
1780
|
+
let hasSoulFile = false;
|
|
1525
1781
|
for (const fileName of WORKSPACE_CONTEXT_FILE_ORDER) {
|
|
1526
1782
|
const filePath = path.join(workspaceDir, fileName);
|
|
1527
1783
|
let raw;
|
|
@@ -1533,6 +1789,7 @@ async function buildWorkspacePromptContext(workspaceDir) {
|
|
|
1533
1789
|
const trimmed = raw.trim();
|
|
1534
1790
|
if (!trimmed) continue;
|
|
1535
1791
|
const clipped = truncateTextToMaxChars(trimmed, WORKSPACE_CONTEXT_MAX_PER_FILE);
|
|
1792
|
+
if (fileName.toLowerCase() === "soul.md") hasSoulFile = true;
|
|
1536
1793
|
sections.push([
|
|
1537
1794
|
`## ${fileName}`,
|
|
1538
1795
|
clipped.text,
|
|
@@ -1540,7 +1797,14 @@ async function buildWorkspacePromptContext(workspaceDir) {
|
|
|
1540
1797
|
].filter(Boolean).join("\n\n"));
|
|
1541
1798
|
}
|
|
1542
1799
|
if (sections.length === 0) return "";
|
|
1543
|
-
const rendered = [
|
|
1800
|
+
const rendered = [
|
|
1801
|
+
"# Project Context",
|
|
1802
|
+
"",
|
|
1803
|
+
"The following project context files have been loaded:",
|
|
1804
|
+
hasSoulFile ? "If SOUL.md is present, embody its persona and tone unless higher-priority rules override it." : "",
|
|
1805
|
+
"",
|
|
1806
|
+
...sections
|
|
1807
|
+
].filter(Boolean).join("\n\n");
|
|
1544
1808
|
if (rendered.length <= WORKSPACE_CONTEXT_MAX_TOTAL) return rendered;
|
|
1545
1809
|
const footer = `\n\n[Workspace context truncated at ${WORKSPACE_CONTEXT_MAX_TOTAL} total characters.]`;
|
|
1546
1810
|
if (footer.length >= WORKSPACE_CONTEXT_MAX_TOTAL) return rendered.slice(0, WORKSPACE_CONTEXT_MAX_TOTAL);
|
|
@@ -1549,7 +1813,7 @@ async function buildWorkspacePromptContext(workspaceDir) {
|
|
|
1549
1813
|
}
|
|
1550
1814
|
async function loadAgentPrompt(agentName, workspaceDir) {
|
|
1551
1815
|
const promptPath = path.join(config.agentsDir, agentName, "CLAUDE.md");
|
|
1552
|
-
let basePrompt =
|
|
1816
|
+
let basePrompt = buildDefaultSystemPrompt(workspaceDir);
|
|
1553
1817
|
try {
|
|
1554
1818
|
const prompt = await fs$1.readFile(promptPath, "utf8");
|
|
1555
1819
|
if (prompt.trim().length > 0) basePrompt = prompt;
|
|
@@ -1576,14 +1840,14 @@ async function resolveProviderApiKey(provider, env = process.env) {
|
|
|
1576
1840
|
const isAnthropicProvider = provider.toLowerCase() === "anthropic";
|
|
1577
1841
|
const fromEnv = getProviderApiKeyFromEnv(provider, env);
|
|
1578
1842
|
if (fromEnv) {
|
|
1579
|
-
if (isAnthropicProvider) logAnthropicCredentialResolution("env"
|
|
1843
|
+
if (isAnthropicProvider) logAnthropicCredentialResolution("env");
|
|
1580
1844
|
return fromEnv;
|
|
1581
1845
|
}
|
|
1582
1846
|
const credentials = loadCredentials(env);
|
|
1583
1847
|
const entry = credentials[provider];
|
|
1584
1848
|
if (!entry) return void 0;
|
|
1585
1849
|
if (entry.type === "api-key") {
|
|
1586
|
-
if (isAnthropicProvider) logAnthropicCredentialResolution("credentials-api-key"
|
|
1850
|
+
if (isAnthropicProvider) logAnthropicCredentialResolution("credentials-api-key");
|
|
1587
1851
|
return entry.key;
|
|
1588
1852
|
}
|
|
1589
1853
|
if (isAnthropicProvider && isAnthropicOAuthCredentialUnsupported(credentials.anthropic)) {
|
|
@@ -1591,7 +1855,7 @@ async function resolveProviderApiKey(provider, env = process.env) {
|
|
|
1591
1855
|
return;
|
|
1592
1856
|
}
|
|
1593
1857
|
if (isAnthropicProvider && credentials.anthropic?.type === "oauth" && isAnthropicSetupToken(credentials.anthropic)) {
|
|
1594
|
-
logAnthropicCredentialResolution("credentials-oauth-setup-token"
|
|
1858
|
+
logAnthropicCredentialResolution("credentials-oauth-setup-token");
|
|
1595
1859
|
return credentials.anthropic.access;
|
|
1596
1860
|
}
|
|
1597
1861
|
const result = await getOAuthApiKey(provider, toOAuthCredentialMap(credentials));
|
|
@@ -1607,7 +1871,7 @@ async function resolveProviderApiKey(provider, env = process.env) {
|
|
|
1607
1871
|
...result.newCredentials
|
|
1608
1872
|
};
|
|
1609
1873
|
saveCredentials(credentials, env);
|
|
1610
|
-
if (isAnthropicProvider) logAnthropicCredentialResolution("oauth-refresh"
|
|
1874
|
+
if (isAnthropicProvider) logAnthropicCredentialResolution("oauth-refresh");
|
|
1611
1875
|
return result.apiKey;
|
|
1612
1876
|
}
|
|
1613
1877
|
var PiAgentManager = class {
|
|
@@ -1777,6 +2041,10 @@ var PiAgentManager = class {
|
|
|
1777
2041
|
if (!handle) return;
|
|
1778
2042
|
this.db.saveAgentState(sessionKey, JSON.stringify({ messages: handle.agent.state.messages }));
|
|
1779
2043
|
}
|
|
2044
|
+
async resetSession(sessionKey) {
|
|
2045
|
+
this.sessions.delete(sessionKey);
|
|
2046
|
+
this.db.clearSession(sessionKey);
|
|
2047
|
+
}
|
|
1780
2048
|
async persistAllSessions() {
|
|
1781
2049
|
await Promise.all(Array.from(this.sessions.keys()).map((sessionKey) => this.persistSession(sessionKey)));
|
|
1782
2050
|
}
|
|
@@ -1891,6 +2159,7 @@ const DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
|
|
|
1891
2159
|
const DEFAULT_POLL_SUCCESS_DELAY_MS = 250;
|
|
1892
2160
|
const DEFAULT_POLL_FAILURE_DELAY_MS = 1e3;
|
|
1893
2161
|
const DEFAULT_MAX_POLL_FAILURE_DELAY_MS = 15e3;
|
|
2162
|
+
const TELEGRAM_MAX_WEBHOOK_BODY_BYTES = 1048576;
|
|
1894
2163
|
function sleep(ms) {
|
|
1895
2164
|
return new Promise((resolve) => {
|
|
1896
2165
|
setTimeout(resolve, ms);
|
|
@@ -2058,14 +2327,38 @@ function toInboundMessage(update, accountId = "default") {
|
|
|
2058
2327
|
raw: update
|
|
2059
2328
|
};
|
|
2060
2329
|
}
|
|
2061
|
-
|
|
2330
|
+
function compareWebhookSecrets(expected, provided) {
|
|
2331
|
+
const expectedBuffer = Buffer.from(expected, "utf8");
|
|
2332
|
+
const providedBuffer = Buffer.from(provided, "utf8");
|
|
2333
|
+
if (expectedBuffer.length !== providedBuffer.length) return false;
|
|
2334
|
+
return timingSafeEqual(expectedBuffer, providedBuffer);
|
|
2335
|
+
}
|
|
2336
|
+
async function readBody(req, maxBytes) {
|
|
2062
2337
|
return new Promise((resolve, reject) => {
|
|
2063
2338
|
let body = "";
|
|
2339
|
+
let totalBytes = 0;
|
|
2340
|
+
let done = false;
|
|
2064
2341
|
req.on("data", (chunk) => {
|
|
2342
|
+
if (done) return;
|
|
2343
|
+
totalBytes += chunk.length;
|
|
2344
|
+
if (totalBytes > maxBytes) {
|
|
2345
|
+
done = true;
|
|
2346
|
+
req.destroy();
|
|
2347
|
+
reject(/* @__PURE__ */ new Error("payload_too_large"));
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2065
2350
|
body += chunk.toString("utf8");
|
|
2066
2351
|
});
|
|
2067
|
-
req.on("end", () =>
|
|
2068
|
-
|
|
2352
|
+
req.on("end", () => {
|
|
2353
|
+
if (done) return;
|
|
2354
|
+
done = true;
|
|
2355
|
+
resolve(body);
|
|
2356
|
+
});
|
|
2357
|
+
req.on("error", (error) => {
|
|
2358
|
+
if (done) return;
|
|
2359
|
+
done = true;
|
|
2360
|
+
reject(error);
|
|
2361
|
+
});
|
|
2069
2362
|
});
|
|
2070
2363
|
}
|
|
2071
2364
|
var TelegramChannel = class {
|
|
@@ -2261,14 +2554,25 @@ var TelegramChannel = class {
|
|
|
2261
2554
|
}
|
|
2262
2555
|
const expectedSecret = account.webhook.secret.trim();
|
|
2263
2556
|
if (expectedSecret) {
|
|
2264
|
-
if (String(req.headers["x-telegram-bot-api-secret-token"] || "")
|
|
2557
|
+
if (!compareWebhookSecrets(expectedSecret, String(req.headers["x-telegram-bot-api-secret-token"] || ""))) {
|
|
2265
2558
|
res.statusCode = 403;
|
|
2266
2559
|
this.lastWebhookStatus = 403;
|
|
2267
2560
|
res.end("forbidden");
|
|
2268
2561
|
return;
|
|
2269
2562
|
}
|
|
2270
2563
|
}
|
|
2271
|
-
|
|
2564
|
+
let body = "";
|
|
2565
|
+
try {
|
|
2566
|
+
body = await readBody(req, TELEGRAM_MAX_WEBHOOK_BODY_BYTES);
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
if (error instanceof Error && error.message === "payload_too_large") {
|
|
2569
|
+
res.statusCode = 413;
|
|
2570
|
+
this.lastWebhookStatus = 413;
|
|
2571
|
+
res.end("payload too large");
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
throw error;
|
|
2575
|
+
}
|
|
2272
2576
|
if (!body) {
|
|
2273
2577
|
res.statusCode = 204;
|
|
2274
2578
|
this.lastWebhookStatus = 204;
|
|
@@ -2323,6 +2627,9 @@ var TelegramChannel = class {
|
|
|
2323
2627
|
this.schedulePoll(10);
|
|
2324
2628
|
logger.info({ accountId: this.accountId }, "Telegram polling channel started");
|
|
2325
2629
|
}
|
|
2630
|
+
getAccountId() {
|
|
2631
|
+
return this.accountId;
|
|
2632
|
+
}
|
|
2326
2633
|
onMessage(handler) {
|
|
2327
2634
|
this.handler = handler;
|
|
2328
2635
|
}
|
|
@@ -2360,6 +2667,21 @@ var TelegramChannel = class {
|
|
|
2360
2667
|
text: text?.trim() || void 0
|
|
2361
2668
|
});
|
|
2362
2669
|
}
|
|
2670
|
+
async setMyCommands(commands) {
|
|
2671
|
+
const normalized = commands.map((entry) => ({
|
|
2672
|
+
command: entry.command,
|
|
2673
|
+
description: entry.description
|
|
2674
|
+
}));
|
|
2675
|
+
for (const scope of [
|
|
2676
|
+
{ type: "default" },
|
|
2677
|
+
{ type: "all_private_chats" },
|
|
2678
|
+
{ type: "all_group_chats" },
|
|
2679
|
+
{ type: "all_chat_administrators" }
|
|
2680
|
+
]) await this.callApi("setMyCommands", {
|
|
2681
|
+
commands: normalized,
|
|
2682
|
+
scope
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2363
2685
|
async sendMedia(target, media) {
|
|
2364
2686
|
const parsedTarget = parseTargetChatId(target.chatId);
|
|
2365
2687
|
const { method, mediaField } = mediaMethod(media);
|
|
@@ -2548,6 +2870,313 @@ var ChannelPluginManager = class {
|
|
|
2548
2870
|
}
|
|
2549
2871
|
};
|
|
2550
2872
|
|
|
2873
|
+
//#endregion
|
|
2874
|
+
//#region src/cli/daemon.ts
|
|
2875
|
+
const DAEMON_STATE_FILE = "daemon-state.json";
|
|
2876
|
+
const DAEMON_LOG_FILE = "daemon.log";
|
|
2877
|
+
const LAUNCHD_LABEL = "ai.hovclaw.daemon";
|
|
2878
|
+
function ensureHomeDir(env = process.env) {
|
|
2879
|
+
const home = getHovclawHome(env);
|
|
2880
|
+
fs.mkdirSync(home, {
|
|
2881
|
+
recursive: true,
|
|
2882
|
+
mode: 448
|
|
2883
|
+
});
|
|
2884
|
+
return home;
|
|
2885
|
+
}
|
|
2886
|
+
function getStatePath(env = process.env) {
|
|
2887
|
+
return path.join(ensureHomeDir(env), DAEMON_STATE_FILE);
|
|
2888
|
+
}
|
|
2889
|
+
function getLogPath(env = process.env) {
|
|
2890
|
+
return path.join(ensureHomeDir(env), DAEMON_LOG_FILE);
|
|
2891
|
+
}
|
|
2892
|
+
function getUserHomeDir(env = process.env) {
|
|
2893
|
+
return env.HOME || os.homedir();
|
|
2894
|
+
}
|
|
2895
|
+
function getLaunchAgentsDir(env = process.env) {
|
|
2896
|
+
return path.join(getUserHomeDir(env), "Library", "LaunchAgents");
|
|
2897
|
+
}
|
|
2898
|
+
function getLaunchdPlistPath(env = process.env) {
|
|
2899
|
+
return path.join(getLaunchAgentsDir(env), `${LAUNCHD_LABEL}.plist`);
|
|
2900
|
+
}
|
|
2901
|
+
function getLaunchdDomain() {
|
|
2902
|
+
return `gui/${typeof process.getuid === "function" ? process.getuid() : 0}`;
|
|
2903
|
+
}
|
|
2904
|
+
function runLaunchctl(args, allowFailure = false) {
|
|
2905
|
+
const result = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
2906
|
+
if (result.error) {
|
|
2907
|
+
if (!allowFailure) throw new Error(`launchctl ${args.join(" ")} failed: ${result.error.message}`);
|
|
2908
|
+
return {
|
|
2909
|
+
status: 1,
|
|
2910
|
+
stdout: result.stdout || "",
|
|
2911
|
+
stderr: result.stderr || result.error.message
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
const status = result.status ?? 1;
|
|
2915
|
+
if (status !== 0 && !allowFailure) {
|
|
2916
|
+
const detail = (result.stderr || result.stdout || "unknown launchctl error").trim();
|
|
2917
|
+
throw new Error(`launchctl ${args.join(" ")} failed: ${detail}`);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
status,
|
|
2921
|
+
stdout: result.stdout || "",
|
|
2922
|
+
stderr: result.stderr || ""
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
function parseLaunchdRuntimeStatus(raw) {
|
|
2926
|
+
const stateMatch = raw.match(/^\s*state = (.+)$/m);
|
|
2927
|
+
const pidMatch = raw.match(/^\s*pid = (\d+)$/m);
|
|
2928
|
+
const lastExitCodeMatch = raw.match(/^\s*last exit code = (-?\d+)$/m);
|
|
2929
|
+
const stateRaw = stateMatch?.[1];
|
|
2930
|
+
const pidRaw = pidMatch?.[1];
|
|
2931
|
+
const lastExitCodeRaw = lastExitCodeMatch?.[1];
|
|
2932
|
+
const state = typeof stateRaw === "string" ? stateRaw.trim() : null;
|
|
2933
|
+
const pid = typeof pidRaw === "string" ? Number.parseInt(pidRaw, 10) : null;
|
|
2934
|
+
const lastExitCode = typeof lastExitCodeRaw === "string" ? Number.parseInt(lastExitCodeRaw, 10) : null;
|
|
2935
|
+
return {
|
|
2936
|
+
running: typeof pid === "number" && Number.isFinite(pid) && pid > 0 || state === "running",
|
|
2937
|
+
pid: typeof pid === "number" && Number.isFinite(pid) && pid > 0 ? pid : null,
|
|
2938
|
+
state,
|
|
2939
|
+
lastExitCode: typeof lastExitCode === "number" && Number.isFinite(lastExitCode) ? lastExitCode : null
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function getLaunchdRuntimeStatus(domain) {
|
|
2943
|
+
const output = runLaunchctl(["print", `${domain}/${LAUNCHD_LABEL}`], true);
|
|
2944
|
+
if (output.status !== 0) return {
|
|
2945
|
+
loaded: false,
|
|
2946
|
+
running: false,
|
|
2947
|
+
pid: null,
|
|
2948
|
+
state: null,
|
|
2949
|
+
lastExitCode: null
|
|
2950
|
+
};
|
|
2951
|
+
return {
|
|
2952
|
+
loaded: true,
|
|
2953
|
+
...parseLaunchdRuntimeStatus(output.stdout)
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
function getLaunchdStatus(env = process.env) {
|
|
2957
|
+
const domain = getLaunchdDomain();
|
|
2958
|
+
const plistPath = getLaunchdPlistPath(env);
|
|
2959
|
+
if (process.platform !== "darwin") return {
|
|
2960
|
+
supported: false,
|
|
2961
|
+
installed: false,
|
|
2962
|
+
loaded: false,
|
|
2963
|
+
running: false,
|
|
2964
|
+
pid: null,
|
|
2965
|
+
state: null,
|
|
2966
|
+
lastExitCode: null,
|
|
2967
|
+
plistPath,
|
|
2968
|
+
label: LAUNCHD_LABEL,
|
|
2969
|
+
domain
|
|
2970
|
+
};
|
|
2971
|
+
const installed = fs.existsSync(plistPath);
|
|
2972
|
+
const runtime = installed ? getLaunchdRuntimeStatus(domain) : {
|
|
2973
|
+
loaded: false,
|
|
2974
|
+
running: false,
|
|
2975
|
+
pid: null,
|
|
2976
|
+
state: null,
|
|
2977
|
+
lastExitCode: null
|
|
2978
|
+
};
|
|
2979
|
+
return {
|
|
2980
|
+
supported: true,
|
|
2981
|
+
installed,
|
|
2982
|
+
loaded: runtime.loaded,
|
|
2983
|
+
running: runtime.running,
|
|
2984
|
+
pid: runtime.pid,
|
|
2985
|
+
state: runtime.state,
|
|
2986
|
+
lastExitCode: runtime.lastExitCode,
|
|
2987
|
+
plistPath,
|
|
2988
|
+
label: LAUNCHD_LABEL,
|
|
2989
|
+
domain
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function saveState(state, env = process.env) {
|
|
2993
|
+
const statePath = getStatePath(env);
|
|
2994
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
2995
|
+
encoding: "utf8",
|
|
2996
|
+
mode: 384
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
function clearState(env = process.env) {
|
|
3000
|
+
const statePath = getStatePath(env);
|
|
3001
|
+
if (fs.existsSync(statePath)) fs.rmSync(statePath, { force: true });
|
|
3002
|
+
}
|
|
3003
|
+
function resolveLaunchSpec() {
|
|
3004
|
+
const cliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
3005
|
+
const jsEntryPath = path.resolve(cliDir, "../index.js");
|
|
3006
|
+
if (fs.existsSync(jsEntryPath)) return {
|
|
3007
|
+
command: process.execPath,
|
|
3008
|
+
args: [jsEntryPath],
|
|
3009
|
+
cwd: process.cwd()
|
|
3010
|
+
};
|
|
3011
|
+
const tsEntryPath = path.resolve(cliDir, "../index.ts");
|
|
3012
|
+
if (fs.existsSync(tsEntryPath)) return {
|
|
3013
|
+
command: process.execPath,
|
|
3014
|
+
args: [
|
|
3015
|
+
"--import",
|
|
3016
|
+
"tsx",
|
|
3017
|
+
tsEntryPath
|
|
3018
|
+
],
|
|
3019
|
+
cwd: process.cwd()
|
|
3020
|
+
};
|
|
3021
|
+
throw new Error("Could not resolve daemon entrypoint (expected src/index.ts or dist/src/index.js).");
|
|
3022
|
+
}
|
|
3023
|
+
function shellEscape$1(value) {
|
|
3024
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3025
|
+
}
|
|
3026
|
+
function spawnDetachedDaemonWithDelay(launch, logPath, env, delayMs) {
|
|
3027
|
+
const outFd = fs.openSync(logPath, "a");
|
|
3028
|
+
const errFd = fs.openSync(logPath, "a");
|
|
3029
|
+
try {
|
|
3030
|
+
const delaySeconds = Math.max(0, delayMs) / 1e3;
|
|
3031
|
+
const command = [launch.command, ...launch.args].map((entry) => shellEscape$1(entry)).join(" ");
|
|
3032
|
+
const child = spawn("/bin/sh", ["-lc", `sleep ${delaySeconds.toFixed(3)}; exec ${command}`], {
|
|
3033
|
+
cwd: launch.cwd,
|
|
3034
|
+
env,
|
|
3035
|
+
detached: true,
|
|
3036
|
+
stdio: [
|
|
3037
|
+
"ignore",
|
|
3038
|
+
outFd,
|
|
3039
|
+
errFd
|
|
3040
|
+
]
|
|
3041
|
+
});
|
|
3042
|
+
child.unref();
|
|
3043
|
+
if (child.pid === void 0) throw new Error("Failed to spawn replacement daemon process.");
|
|
3044
|
+
return child.pid;
|
|
3045
|
+
} finally {
|
|
3046
|
+
fs.closeSync(outFd);
|
|
3047
|
+
fs.closeSync(errFd);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
async function requestDaemonRestartFromCurrentProcess(env = process.env) {
|
|
3051
|
+
const launchd = getLaunchdStatus(env);
|
|
3052
|
+
if (launchd.supported && launchd.installed) {
|
|
3053
|
+
if (!launchd.loaded) runLaunchctl([
|
|
3054
|
+
"bootstrap",
|
|
3055
|
+
launchd.domain,
|
|
3056
|
+
launchd.plistPath
|
|
3057
|
+
], false);
|
|
3058
|
+
runLaunchctl(["enable", `${launchd.domain}/${launchd.label}`], true);
|
|
3059
|
+
runLaunchctl([
|
|
3060
|
+
"kickstart",
|
|
3061
|
+
"-k",
|
|
3062
|
+
`${launchd.domain}/${launchd.label}`
|
|
3063
|
+
], false);
|
|
3064
|
+
clearState(env);
|
|
3065
|
+
return {
|
|
3066
|
+
mode: "launchd-kickstart",
|
|
3067
|
+
pid: launchd.pid
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
const launch = resolveLaunchSpec();
|
|
3071
|
+
const logPath = getLogPath(env);
|
|
3072
|
+
const pid = spawnDetachedDaemonWithDelay(launch, logPath, env, 350);
|
|
3073
|
+
saveState({
|
|
3074
|
+
pid,
|
|
3075
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3076
|
+
command: launch.command,
|
|
3077
|
+
args: launch.args,
|
|
3078
|
+
cwd: launch.cwd,
|
|
3079
|
+
logPath
|
|
3080
|
+
}, env);
|
|
3081
|
+
return {
|
|
3082
|
+
mode: "spawn-reexec",
|
|
3083
|
+
pid
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
//#endregion
|
|
3088
|
+
//#region src/compat/openclaw-mirror.ts
|
|
3089
|
+
function resolveOpenClawHome(env = process.env) {
|
|
3090
|
+
const override = env.OPENCLAW_STATE_DIR?.trim();
|
|
3091
|
+
if (override) return path.resolve(override.startsWith("~") ? path.join(os.homedir(), override.slice(1)) : override);
|
|
3092
|
+
return path.join(os.homedir(), ".openclaw");
|
|
3093
|
+
}
|
|
3094
|
+
function resolveOpenClawConfigPath(openclawHome) {
|
|
3095
|
+
return path.join(openclawHome, "openclaw.json");
|
|
3096
|
+
}
|
|
3097
|
+
function resolveOpenClawSharedSkillsPath(openclawHome) {
|
|
3098
|
+
return path.join(openclawHome, "skills");
|
|
3099
|
+
}
|
|
3100
|
+
function buildMirrorConfig(config) {
|
|
3101
|
+
const fallbackWorkspace = config.agents.defaults.workspace || path.join(config.hovclawHome, "workspace");
|
|
3102
|
+
const agentList = config.agents.list.length > 0 ? config.agents.list : [{
|
|
3103
|
+
id: "main",
|
|
3104
|
+
name: "Main",
|
|
3105
|
+
workspace: fallbackWorkspace,
|
|
3106
|
+
default: true
|
|
3107
|
+
}];
|
|
3108
|
+
const extraDirs = /* @__PURE__ */ new Set();
|
|
3109
|
+
extraDirs.add(config.skillsDir);
|
|
3110
|
+
for (const agent of agentList) {
|
|
3111
|
+
const workspace = (agent.workspace || fallbackWorkspace).trim();
|
|
3112
|
+
if (!workspace) continue;
|
|
3113
|
+
extraDirs.add(path.join(workspace, "skills"));
|
|
3114
|
+
}
|
|
3115
|
+
return {
|
|
3116
|
+
agent: { workspace: fallbackWorkspace },
|
|
3117
|
+
agents: {
|
|
3118
|
+
defaults: { workspace: fallbackWorkspace },
|
|
3119
|
+
list: agentList
|
|
3120
|
+
},
|
|
3121
|
+
skills: { load: { extraDirs: Array.from(extraDirs) } }
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
function ensureDir(dirPath) {
|
|
3125
|
+
fs.mkdirSync(dirPath, {
|
|
3126
|
+
recursive: true,
|
|
3127
|
+
mode: 448
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
function syncSkillsDir(sharedSkillsPath, sourceSkillsPath) {
|
|
3131
|
+
if (!fs.existsSync(sourceSkillsPath)) {
|
|
3132
|
+
ensureDir(sharedSkillsPath);
|
|
3133
|
+
return false;
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
if (fs.lstatSync(sharedSkillsPath).isSymbolicLink()) {
|
|
3137
|
+
const linkTarget = fs.readlinkSync(sharedSkillsPath);
|
|
3138
|
+
if (path.resolve(path.dirname(sharedSkillsPath), linkTarget) === sourceSkillsPath) return true;
|
|
3139
|
+
fs.unlinkSync(sharedSkillsPath);
|
|
3140
|
+
}
|
|
3141
|
+
} catch {}
|
|
3142
|
+
if (!fs.existsSync(sharedSkillsPath)) try {
|
|
3143
|
+
fs.symlinkSync(sourceSkillsPath, sharedSkillsPath, "dir");
|
|
3144
|
+
return true;
|
|
3145
|
+
} catch {}
|
|
3146
|
+
ensureDir(sharedSkillsPath);
|
|
3147
|
+
for (const entry of fs.readdirSync(sourceSkillsPath, { withFileTypes: true })) {
|
|
3148
|
+
const src = path.join(sourceSkillsPath, entry.name);
|
|
3149
|
+
const dst = path.join(sharedSkillsPath, entry.name);
|
|
3150
|
+
if (entry.isDirectory()) {
|
|
3151
|
+
fs.cpSync(src, dst, {
|
|
3152
|
+
recursive: true,
|
|
3153
|
+
force: true
|
|
3154
|
+
});
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
if (entry.isFile()) fs.copyFileSync(src, dst);
|
|
3158
|
+
}
|
|
3159
|
+
return false;
|
|
3160
|
+
}
|
|
3161
|
+
function writeOpenClawMirror(config) {
|
|
3162
|
+
const openclawHome = resolveOpenClawHome();
|
|
3163
|
+
const configPath = resolveOpenClawConfigPath(openclawHome);
|
|
3164
|
+
const sharedSkillsPath = resolveOpenClawSharedSkillsPath(openclawHome);
|
|
3165
|
+
ensureDir(openclawHome);
|
|
3166
|
+
const mirrorConfig = buildMirrorConfig(config);
|
|
3167
|
+
fs.writeFileSync(configPath, `${JSON.stringify(mirrorConfig, null, 2)}\n`, {
|
|
3168
|
+
encoding: "utf8",
|
|
3169
|
+
mode: 384
|
|
3170
|
+
});
|
|
3171
|
+
fs.chmodSync(configPath, 384);
|
|
3172
|
+
return {
|
|
3173
|
+
openclawHome,
|
|
3174
|
+
configPath,
|
|
3175
|
+
sharedSkillsPath,
|
|
3176
|
+
linkedSharedSkills: syncSkillsDir(sharedSkillsPath, config.skillsDir)
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2551
3180
|
//#endregion
|
|
2552
3181
|
//#region src/models/catalog.ts
|
|
2553
3182
|
function buildModelCatalog(aliases) {
|
|
@@ -2569,98 +3198,240 @@ function buildModelCatalog(aliases) {
|
|
|
2569
3198
|
}
|
|
2570
3199
|
|
|
2571
3200
|
//#endregion
|
|
2572
|
-
//#region src/channels/
|
|
2573
|
-
function
|
|
2574
|
-
return
|
|
2575
|
-
}
|
|
2576
|
-
function
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3201
|
+
//#region src/channels/command-auth.ts
|
|
3202
|
+
function normalizeAllowFromEntry(value) {
|
|
3203
|
+
return String(value).trim().toLowerCase();
|
|
3204
|
+
}
|
|
3205
|
+
function senderCandidates(msg) {
|
|
3206
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
3207
|
+
const userId = msg.userId.trim();
|
|
3208
|
+
if (userId) candidates.add(userId.toLowerCase());
|
|
3209
|
+
const displayName = msg.displayName.trim();
|
|
3210
|
+
if (displayName.startsWith("@")) candidates.add(displayName.toLowerCase());
|
|
3211
|
+
return Array.from(candidates);
|
|
3212
|
+
}
|
|
3213
|
+
function resolveCommandsAllowFrom(config, channel) {
|
|
3214
|
+
const entries = config.commands.allowFrom;
|
|
3215
|
+
if (Object.keys(entries).length === 0) return null;
|
|
3216
|
+
const providerSpecific = entries[channel];
|
|
3217
|
+
if (Array.isArray(providerSpecific)) return providerSpecific;
|
|
3218
|
+
const global = entries["*"];
|
|
3219
|
+
if (Array.isArray(global)) return global;
|
|
3220
|
+
return [];
|
|
3221
|
+
}
|
|
3222
|
+
function isCommandAuthorized(config, msg) {
|
|
3223
|
+
const allowFrom = resolveCommandsAllowFrom(config, msg.channel);
|
|
3224
|
+
if (allowFrom === null) return true;
|
|
3225
|
+
const normalizedAllow = new Set(allowFrom.map((entry) => normalizeAllowFromEntry(entry)));
|
|
3226
|
+
if (normalizedAllow.has("*")) return true;
|
|
3227
|
+
for (const candidate of senderCandidates(msg)) if (normalizedAllow.has(candidate)) return true;
|
|
3228
|
+
return false;
|
|
2585
3229
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
3230
|
+
|
|
3231
|
+
//#endregion
|
|
3232
|
+
//#region src/channels/commands-registry.ts
|
|
3233
|
+
const TELEGRAM_COMMAND_NAME_RE = /^[a-z0-9_]{1,32}$/;
|
|
3234
|
+
const TELEGRAM_COMMAND_LIMIT = 100;
|
|
3235
|
+
const BASE_COMMANDS = [
|
|
3236
|
+
{
|
|
3237
|
+
key: "help",
|
|
3238
|
+
nativeName: "help",
|
|
3239
|
+
description: "Show available commands"
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
key: "commands",
|
|
3243
|
+
nativeName: "commands",
|
|
3244
|
+
description: "List all slash commands"
|
|
3245
|
+
},
|
|
3246
|
+
{
|
|
3247
|
+
key: "status",
|
|
3248
|
+
nativeName: "status",
|
|
3249
|
+
description: "Show runtime status"
|
|
3250
|
+
},
|
|
3251
|
+
{
|
|
3252
|
+
key: "whoami",
|
|
3253
|
+
nativeName: "whoami",
|
|
3254
|
+
description: "Show your sender id"
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
key: "context",
|
|
3258
|
+
nativeName: "context",
|
|
3259
|
+
description: "Show prompt/workspace context info"
|
|
3260
|
+
},
|
|
3261
|
+
{
|
|
3262
|
+
key: "model",
|
|
3263
|
+
nativeName: "model",
|
|
3264
|
+
description: "Show or set interactive model"
|
|
3265
|
+
},
|
|
3266
|
+
{
|
|
3267
|
+
key: "models",
|
|
3268
|
+
nativeName: "models",
|
|
3269
|
+
description: "List available models"
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
key: "skill",
|
|
3273
|
+
nativeName: "skill",
|
|
3274
|
+
description: "Run a skill by name"
|
|
3275
|
+
},
|
|
3276
|
+
{
|
|
3277
|
+
key: "skills",
|
|
3278
|
+
nativeName: "skills",
|
|
3279
|
+
description: "List installed skills"
|
|
3280
|
+
},
|
|
3281
|
+
{
|
|
3282
|
+
key: "identity",
|
|
3283
|
+
nativeName: "identity",
|
|
3284
|
+
description: "View or update IDENTITY.md"
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
key: "soul",
|
|
3288
|
+
nativeName: "soul",
|
|
3289
|
+
description: "View or update SOUL.md"
|
|
3290
|
+
},
|
|
3291
|
+
{
|
|
3292
|
+
key: "pair",
|
|
3293
|
+
nativeName: "pair",
|
|
3294
|
+
description: "Show pairing approval hint"
|
|
3295
|
+
},
|
|
3296
|
+
{
|
|
3297
|
+
key: "new",
|
|
3298
|
+
nativeName: "new",
|
|
3299
|
+
description: "Start a fresh conversation session"
|
|
3300
|
+
},
|
|
3301
|
+
{
|
|
3302
|
+
key: "reset",
|
|
3303
|
+
nativeName: "reset",
|
|
3304
|
+
description: "Reset current conversation session"
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
key: "think",
|
|
3308
|
+
nativeName: "think",
|
|
3309
|
+
description: "Set per-task or default thinking level"
|
|
3310
|
+
},
|
|
3311
|
+
{
|
|
3312
|
+
key: "verbose",
|
|
3313
|
+
nativeName: "verbose",
|
|
3314
|
+
description: "Toggle verbose mode (hint)"
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
key: "reasoning",
|
|
3318
|
+
nativeName: "reasoning",
|
|
3319
|
+
description: "Toggle reasoning visibility (hint)"
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
key: "stop",
|
|
3323
|
+
nativeName: "stop",
|
|
3324
|
+
description: "Stop current run (not yet supported)"
|
|
3325
|
+
},
|
|
3326
|
+
{
|
|
3327
|
+
key: "config",
|
|
3328
|
+
nativeName: "config",
|
|
3329
|
+
description: "View or edit runtime config (if enabled)"
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
key: "debug",
|
|
3333
|
+
nativeName: "debug",
|
|
3334
|
+
description: "Inspect or change debug state (if enabled)"
|
|
3335
|
+
},
|
|
3336
|
+
{
|
|
3337
|
+
key: "bash",
|
|
3338
|
+
nativeName: "bash",
|
|
3339
|
+
description: "Run a shell command through the assistant (if enabled)"
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
key: "restart",
|
|
3343
|
+
nativeName: "restart",
|
|
3344
|
+
description: "Restart daemon (if enabled)"
|
|
3345
|
+
}
|
|
3346
|
+
];
|
|
3347
|
+
function sanitizeCommandName(raw) {
|
|
3348
|
+
return raw.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32);
|
|
3349
|
+
}
|
|
3350
|
+
function coerceDescription(input, fallback) {
|
|
3351
|
+
const trimmed = input.trim();
|
|
3352
|
+
if (!trimmed) return fallback;
|
|
3353
|
+
if (trimmed.length <= 256) return trimmed;
|
|
3354
|
+
return `${trimmed.slice(0, 255)}…`;
|
|
3355
|
+
}
|
|
3356
|
+
function resolveSkillCommandName(skillName, used) {
|
|
3357
|
+
const base = sanitizeCommandName(skillName) || "skill";
|
|
3358
|
+
if (!used.has(base)) return base;
|
|
3359
|
+
for (let i = 2; i < 500; i += 1) {
|
|
3360
|
+
const suffix = `_${i}`;
|
|
3361
|
+
const maxBaseLen = Math.max(1, 32 - suffix.length);
|
|
3362
|
+
const candidate = `${base.slice(0, maxBaseLen)}${suffix}`;
|
|
3363
|
+
if (!used.has(candidate)) return candidate;
|
|
3364
|
+
}
|
|
3365
|
+
return `skill_${used.size}`;
|
|
3366
|
+
}
|
|
3367
|
+
function resolveNativeEnabled(config, accountId) {
|
|
3368
|
+
const accountMode = config.channels.telegram.accounts[accountId]?.commands;
|
|
3369
|
+
if (typeof accountMode === "boolean") return accountMode;
|
|
3370
|
+
if (typeof config.commands.native === "boolean") return config.commands.native;
|
|
3371
|
+
return true;
|
|
2590
3372
|
}
|
|
2591
|
-
function
|
|
2592
|
-
if (
|
|
2593
|
-
|
|
2594
|
-
if (userId && allowSet.has(userId)) return true;
|
|
2595
|
-
const displayName = msg.displayName.trim().replace(/^@/, "").toLowerCase();
|
|
2596
|
-
if (displayName && allowSet.has(displayName)) return true;
|
|
2597
|
-
return false;
|
|
3373
|
+
function resolveNativeSkillsEnabled(config) {
|
|
3374
|
+
if (typeof config.commands.nativeSkills === "boolean") return config.commands.nativeSkills;
|
|
3375
|
+
return true;
|
|
2598
3376
|
}
|
|
2599
|
-
function
|
|
2600
|
-
const
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
};
|
|
2615
|
-
if (dmPolicy === "open") return { allowed: true };
|
|
2616
|
-
if (dmPolicy === "allowlist") {
|
|
2617
|
-
if (isAllowListed(msg, allowFromSet) || pairingStore.isApproved(accountId, msg.userId)) return { allowed: true };
|
|
2618
|
-
return {
|
|
2619
|
-
allowed: false,
|
|
2620
|
-
reason: "dm-not-allowlisted"
|
|
2621
|
-
};
|
|
2622
|
-
}
|
|
2623
|
-
if (isAllowListed(msg, allowFromSet) || pairingStore.isApproved(accountId, msg.userId)) return { allowed: true };
|
|
2624
|
-
return {
|
|
2625
|
-
allowed: false,
|
|
2626
|
-
reason: "pairing-required",
|
|
2627
|
-
pairingCode: pairingStore.ensurePendingCode(accountId, msg.userId)
|
|
2628
|
-
};
|
|
3377
|
+
function listSkillCommandSpecs(config) {
|
|
3378
|
+
const installed = listAvailableSkills();
|
|
3379
|
+
const used = /* @__PURE__ */ new Set();
|
|
3380
|
+
for (const command of BASE_COMMANDS) used.add(command.nativeName);
|
|
3381
|
+
const specs = [];
|
|
3382
|
+
for (const skillName of installed) {
|
|
3383
|
+
const loaded = loadSkill(skillName);
|
|
3384
|
+
if (!loaded) continue;
|
|
3385
|
+
const commandName = resolveSkillCommandName(skillName, used);
|
|
3386
|
+
if (!TELEGRAM_COMMAND_NAME_RE.test(commandName)) continue;
|
|
3387
|
+
used.add(commandName);
|
|
3388
|
+
specs.push({
|
|
3389
|
+
name: commandName,
|
|
3390
|
+
skillName,
|
|
3391
|
+
description: coerceDescription(loaded.frontmatter.description ?? `Run ${skillName} skill`, `Run ${skillName} skill`)
|
|
3392
|
+
});
|
|
2629
3393
|
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
reason: "group-disabled"
|
|
2645
|
-
};
|
|
2646
|
-
if (groupPolicy === "allowlist") {
|
|
2647
|
-
if (!isAllowListed(msg, toIdSet(topicConfig?.allowFrom ?? groupConfig?.allowFrom ?? account.groupAllowFrom))) return {
|
|
2648
|
-
allowed: false,
|
|
2649
|
-
reason: "group-not-allowlisted"
|
|
2650
|
-
};
|
|
3394
|
+
return specs;
|
|
3395
|
+
}
|
|
3396
|
+
function listCustomCommandSpecs(config, accountId) {
|
|
3397
|
+
const account = config.channels.telegram.accounts[accountId];
|
|
3398
|
+
if (!account) return [];
|
|
3399
|
+
const commands = Array.isArray(account.customCommands) ? account.customCommands : [];
|
|
3400
|
+
const out = [];
|
|
3401
|
+
for (const entry of commands) {
|
|
3402
|
+
const normalized = sanitizeCommandName(entry.command);
|
|
3403
|
+
if (!TELEGRAM_COMMAND_NAME_RE.test(normalized)) continue;
|
|
3404
|
+
out.push({
|
|
3405
|
+
command: normalized,
|
|
3406
|
+
description: coerceDescription(entry.description, `Run /${normalized}`)
|
|
3407
|
+
});
|
|
2651
3408
|
}
|
|
2652
|
-
|
|
2653
|
-
allowed: false,
|
|
2654
|
-
reason: "mention-required"
|
|
2655
|
-
};
|
|
2656
|
-
return { allowed: true };
|
|
3409
|
+
return out;
|
|
2657
3410
|
}
|
|
2658
|
-
function
|
|
2659
|
-
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3411
|
+
function listTelegramNativeCommandSpecs(config, accountId) {
|
|
3412
|
+
if (!resolveNativeEnabled(config, accountId)) return [];
|
|
3413
|
+
const out = BASE_COMMANDS.map((command) => ({
|
|
3414
|
+
command: command.nativeName,
|
|
3415
|
+
description: command.description
|
|
3416
|
+
}));
|
|
3417
|
+
if (resolveNativeSkillsEnabled(config)) out.push(...listSkillCommandSpecs(config).map((skill) => ({
|
|
3418
|
+
command: skill.name,
|
|
3419
|
+
description: skill.description
|
|
3420
|
+
})));
|
|
3421
|
+
out.push(...listCustomCommandSpecs(config, accountId));
|
|
3422
|
+
const deduped = [];
|
|
3423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3424
|
+
for (const entry of out) {
|
|
3425
|
+
if (!TELEGRAM_COMMAND_NAME_RE.test(entry.command)) continue;
|
|
3426
|
+
if (seen.has(entry.command)) continue;
|
|
3427
|
+
seen.add(entry.command);
|
|
3428
|
+
deduped.push(entry);
|
|
3429
|
+
if (deduped.length >= TELEGRAM_COMMAND_LIMIT) break;
|
|
3430
|
+
}
|
|
3431
|
+
return deduped;
|
|
3432
|
+
}
|
|
3433
|
+
function listBaseCommands() {
|
|
3434
|
+
return [...BASE_COMMANDS];
|
|
2664
3435
|
}
|
|
2665
3436
|
|
|
2666
3437
|
//#endregion
|
|
@@ -2751,11 +3522,28 @@ function buildModelsKeyboard(params) {
|
|
|
2751
3522
|
|
|
2752
3523
|
//#endregion
|
|
2753
3524
|
//#region src/channels/telegram-commands.ts
|
|
3525
|
+
const SKILL_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
3526
|
+
const DEBUG_LOG_LEVELS = new Set([
|
|
3527
|
+
"trace",
|
|
3528
|
+
"debug",
|
|
3529
|
+
"info",
|
|
3530
|
+
"warn",
|
|
3531
|
+
"error",
|
|
3532
|
+
"fatal",
|
|
3533
|
+
"silent"
|
|
3534
|
+
]);
|
|
3535
|
+
const BLOCKED_CONFIG_PATH_SEGMENTS = new Set([
|
|
3536
|
+
"__proto__",
|
|
3537
|
+
"prototype",
|
|
3538
|
+
"constructor"
|
|
3539
|
+
]);
|
|
2754
3540
|
function extractCommand(text) {
|
|
2755
3541
|
const trimmed = text.trim();
|
|
2756
3542
|
if (!trimmed.startsWith("/")) return null;
|
|
2757
3543
|
const segments = trimmed.split(/\s+/);
|
|
2758
|
-
const
|
|
3544
|
+
const raw = segments[0]?.slice(1).toLowerCase();
|
|
3545
|
+
if (!raw) return null;
|
|
3546
|
+
const command = raw.split("@")[0]?.trim();
|
|
2759
3547
|
if (!command) return null;
|
|
2760
3548
|
return {
|
|
2761
3549
|
command,
|
|
@@ -2770,8 +3558,125 @@ function callbackDataFromMessage(msg) {
|
|
|
2770
3558
|
function setInteractiveModel(modelRef) {
|
|
2771
3559
|
const next = loadFileConfig();
|
|
2772
3560
|
next.models.interactive = modelRef;
|
|
3561
|
+
persistFileConfig(next);
|
|
3562
|
+
}
|
|
3563
|
+
function isRecord$1(value) {
|
|
3564
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3565
|
+
}
|
|
3566
|
+
function deepMerge$1(base, patch) {
|
|
3567
|
+
const result = { ...base };
|
|
3568
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
3569
|
+
const existing = result[key];
|
|
3570
|
+
if (isRecord$1(existing) && isRecord$1(value)) {
|
|
3571
|
+
result[key] = deepMerge$1(existing, value);
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
result[key] = value;
|
|
3575
|
+
}
|
|
3576
|
+
return result;
|
|
3577
|
+
}
|
|
3578
|
+
function reloadRuntimeConfig() {
|
|
3579
|
+
const reloaded = loadConfig();
|
|
3580
|
+
Object.assign(config, reloaded);
|
|
3581
|
+
logger.level = reloaded.logLevel;
|
|
3582
|
+
}
|
|
3583
|
+
function persistFileConfig(next) {
|
|
2773
3584
|
saveConfigFile(next);
|
|
2774
|
-
|
|
3585
|
+
reloadRuntimeConfig();
|
|
3586
|
+
writeOpenClawMirror(config);
|
|
3587
|
+
}
|
|
3588
|
+
function parseConfigPath(raw) {
|
|
3589
|
+
const pathValue = raw?.trim();
|
|
3590
|
+
if (!pathValue) return null;
|
|
3591
|
+
const segments = pathValue.split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
3592
|
+
if (segments.length === 0) return null;
|
|
3593
|
+
if (segments.some((segment) => BLOCKED_CONFIG_PATH_SEGMENTS.has(segment))) return null;
|
|
3594
|
+
return segments;
|
|
3595
|
+
}
|
|
3596
|
+
function readPathValue(root, segments) {
|
|
3597
|
+
let current = root;
|
|
3598
|
+
for (const segment of segments) {
|
|
3599
|
+
if (Array.isArray(current)) {
|
|
3600
|
+
const index = Number.parseInt(segment, 10);
|
|
3601
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) return { found: false };
|
|
3602
|
+
current = current[index];
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (!isRecord$1(current) || !(segment in current)) return { found: false };
|
|
3606
|
+
current = current[segment];
|
|
3607
|
+
}
|
|
3608
|
+
return {
|
|
3609
|
+
found: true,
|
|
3610
|
+
value: current
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
function writePathValue(root, segments, value) {
|
|
3614
|
+
if (segments.length === 0) return false;
|
|
3615
|
+
let current = root;
|
|
3616
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
3617
|
+
const segment = segments[i];
|
|
3618
|
+
const nextSegment = segments[i + 1];
|
|
3619
|
+
if (!segment || !nextSegment) return false;
|
|
3620
|
+
if (Array.isArray(current)) {
|
|
3621
|
+
const index = Number.parseInt(segment, 10);
|
|
3622
|
+
if (!Number.isInteger(index) || index < 0) return false;
|
|
3623
|
+
if (current[index] === void 0) current[index] = /^[0-9]+$/.test(nextSegment) ? [] : {};
|
|
3624
|
+
current = current[index];
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
if (!isRecord$1(current)) return false;
|
|
3628
|
+
const existing = current[segment];
|
|
3629
|
+
if (existing === void 0 || !isRecord$1(existing) && !Array.isArray(existing)) current[segment] = /^[0-9]+$/.test(nextSegment) ? [] : {};
|
|
3630
|
+
current = current[segment];
|
|
3631
|
+
}
|
|
3632
|
+
const lastSegment = segments[segments.length - 1];
|
|
3633
|
+
if (!lastSegment) return false;
|
|
3634
|
+
if (Array.isArray(current)) {
|
|
3635
|
+
const index = Number.parseInt(lastSegment, 10);
|
|
3636
|
+
if (!Number.isInteger(index) || index < 0) return false;
|
|
3637
|
+
current[index] = value;
|
|
3638
|
+
return true;
|
|
3639
|
+
}
|
|
3640
|
+
if (!isRecord$1(current)) return false;
|
|
3641
|
+
current[lastSegment] = value;
|
|
3642
|
+
return true;
|
|
3643
|
+
}
|
|
3644
|
+
function parseConfigValue(raw) {
|
|
3645
|
+
const trimmed = raw.trim();
|
|
3646
|
+
if (!trimmed) return "";
|
|
3647
|
+
try {
|
|
3648
|
+
return JSON.parse(trimmed);
|
|
3649
|
+
} catch {
|
|
3650
|
+
return raw;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
function formatValue(value) {
|
|
3654
|
+
if (typeof value === "string") return value;
|
|
3655
|
+
return JSON.stringify(value, null, 2);
|
|
3656
|
+
}
|
|
3657
|
+
function configSummaryText(current) {
|
|
3658
|
+
return [
|
|
3659
|
+
"Config summary",
|
|
3660
|
+
`assistant.name=${current.assistant.name}`,
|
|
3661
|
+
`models.interactive=${current.models.interactive}`,
|
|
3662
|
+
`models.discord=${current.models.discord}`,
|
|
3663
|
+
`models.cron=${current.models.cron}`,
|
|
3664
|
+
`runtime.mode=${current.runtime.mode}`,
|
|
3665
|
+
`commands.text=${current.commands.text}`,
|
|
3666
|
+
`commands.defaultThinkingLevel=${current.commands.defaultThinkingLevel}`,
|
|
3667
|
+
`commands.config=${current.commands.config}`,
|
|
3668
|
+
`commands.debug=${current.commands.debug}`,
|
|
3669
|
+
`commands.bash=${current.commands.bash}`,
|
|
3670
|
+
`commands.restart=${current.commands.restart}`,
|
|
3671
|
+
`channels.telegram.enabled=${current.channels.telegram.enabled}`,
|
|
3672
|
+
`channels.discord.enabled=${current.channels.discord.enabled}`,
|
|
3673
|
+
"",
|
|
3674
|
+
"Usage:",
|
|
3675
|
+
"/config get <path>",
|
|
3676
|
+
"/config set <path> <json-or-text>",
|
|
3677
|
+
"/config patch <json-object>",
|
|
3678
|
+
"/config reload"
|
|
3679
|
+
].join("\n");
|
|
2775
3680
|
}
|
|
2776
3681
|
function groupedModels() {
|
|
2777
3682
|
return buildModelCatalog(config.models.aliases).reduce((acc, entry) => {
|
|
@@ -2781,9 +3686,57 @@ function groupedModels() {
|
|
|
2781
3686
|
return acc;
|
|
2782
3687
|
}, {});
|
|
2783
3688
|
}
|
|
3689
|
+
function workspaceFilePath(agentId, fileName) {
|
|
3690
|
+
return path.join(resolveAgentWorkspaceDir(config, agentId), fileName);
|
|
3691
|
+
}
|
|
3692
|
+
async function readWorkspaceFileOrEmpty(agentId, fileName) {
|
|
3693
|
+
const filePath = workspaceFilePath(agentId, fileName);
|
|
3694
|
+
try {
|
|
3695
|
+
return await fs$1.readFile(filePath, "utf8");
|
|
3696
|
+
} catch {
|
|
3697
|
+
return "";
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
async function writeWorkspaceFile(agentId, fileName, content) {
|
|
3701
|
+
const filePath = workspaceFilePath(agentId, fileName);
|
|
3702
|
+
await fs$1.mkdir(path.dirname(filePath), { recursive: true });
|
|
3703
|
+
await fs$1.writeFile(filePath, `${content.trimEnd()}\n`, "utf8");
|
|
3704
|
+
}
|
|
3705
|
+
function trimForTelegram(text, limit = 3600) {
|
|
3706
|
+
if (text.length <= limit) return text;
|
|
3707
|
+
return `${text.slice(0, limit)}\n\n[...truncated]`;
|
|
3708
|
+
}
|
|
3709
|
+
async function addSkillToAgent(agentId, skillName) {
|
|
3710
|
+
const agentDir = path.join(config.agentsDir, agentId);
|
|
3711
|
+
const agentPath = path.join(agentDir, "agent.json");
|
|
3712
|
+
let parsed = {};
|
|
3713
|
+
try {
|
|
3714
|
+
const raw = await fs$1.readFile(agentPath, "utf8");
|
|
3715
|
+
parsed = JSON.parse(raw);
|
|
3716
|
+
} catch {
|
|
3717
|
+
parsed = {};
|
|
3718
|
+
}
|
|
3719
|
+
const existingSkills = Array.isArray(parsed.skills) ? parsed.skills.filter((value) => typeof value === "string") : [];
|
|
3720
|
+
if (existingSkills.includes(skillName)) return {
|
|
3721
|
+
added: false,
|
|
3722
|
+
path: agentPath
|
|
3723
|
+
};
|
|
3724
|
+
const nextSkills = [...existingSkills, skillName];
|
|
3725
|
+
const next = {
|
|
3726
|
+
...parsed,
|
|
3727
|
+
skills: nextSkills
|
|
3728
|
+
};
|
|
3729
|
+
await fs$1.mkdir(agentDir, { recursive: true });
|
|
3730
|
+
await fs$1.writeFile(agentPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
3731
|
+
return {
|
|
3732
|
+
added: true,
|
|
3733
|
+
path: agentPath
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
2784
3736
|
async function handleModelCallback(context, callbackData) {
|
|
2785
3737
|
const parsed = parseModelCallbackData(callbackData);
|
|
2786
3738
|
if (!parsed) return false;
|
|
3739
|
+
const authorized = isCommandAuthorized(config, context.msg);
|
|
2787
3740
|
const grouped = groupedModels();
|
|
2788
3741
|
const target = {
|
|
2789
3742
|
channel: "telegram",
|
|
@@ -2815,6 +3768,12 @@ async function handleModelCallback(context, callbackData) {
|
|
|
2815
3768
|
if (callbackId) await context.channel.answerCallbackQuery(callbackId);
|
|
2816
3769
|
return true;
|
|
2817
3770
|
}
|
|
3771
|
+
if (!authorized) {
|
|
3772
|
+
await context.channel.sendMessage(target, "You are not authorized to use this command.");
|
|
3773
|
+
const callbackId = context.msg.raw?.callback_query?.id;
|
|
3774
|
+
if (callbackId) await context.channel.answerCallbackQuery(callbackId, "Not authorized");
|
|
3775
|
+
return true;
|
|
3776
|
+
}
|
|
2818
3777
|
const selectedRef = `${parsed.provider}:${parsed.model}`;
|
|
2819
3778
|
setInteractiveModel(selectedRef);
|
|
2820
3779
|
await context.channel.sendMessage(target, `Interactive model set to ${selectedRef}.`);
|
|
@@ -2822,111 +3781,496 @@ async function handleModelCallback(context, callbackData) {
|
|
|
2822
3781
|
if (callbackId) await context.channel.answerCallbackQuery(callbackId, "Model updated");
|
|
2823
3782
|
return true;
|
|
2824
3783
|
}
|
|
3784
|
+
function buildCommandsHelpText(accountId) {
|
|
3785
|
+
const nativeCommands = listTelegramNativeCommandSpecs(config, accountId);
|
|
3786
|
+
const fallbackCommands = [...listBaseCommands().map((command) => ({
|
|
3787
|
+
command: command.nativeName,
|
|
3788
|
+
description: command.description
|
|
3789
|
+
})), ...listSkillCommandSpecs(config).map((command) => ({
|
|
3790
|
+
command: command.name,
|
|
3791
|
+
description: command.description
|
|
3792
|
+
}))];
|
|
3793
|
+
return ["Commands:", ...(nativeCommands.length > 0 ? nativeCommands : fallbackCommands).map((command) => `/${command.command} - ${command.description}`)].join("\n");
|
|
3794
|
+
}
|
|
3795
|
+
function resolveSkillPrompt(skillName, args) {
|
|
3796
|
+
const input = args.join(" ").trim();
|
|
3797
|
+
if (!input) return `Use the ${skillName} skill to help with the user's request.`;
|
|
3798
|
+
return `Use the ${skillName} skill for this request: ${input}`;
|
|
3799
|
+
}
|
|
3800
|
+
function normalizeSkillName(raw) {
|
|
3801
|
+
const name = raw?.trim();
|
|
3802
|
+
if (!name) return null;
|
|
3803
|
+
if (!SKILL_NAME_RE.test(name)) return null;
|
|
3804
|
+
return name;
|
|
3805
|
+
}
|
|
3806
|
+
function parseThinkingLevel(raw) {
|
|
3807
|
+
const value = raw?.trim().toLowerCase();
|
|
3808
|
+
if (value === "low" || value === "medium" || value === "high") return value;
|
|
3809
|
+
return null;
|
|
3810
|
+
}
|
|
2825
3811
|
async function handleTelegramCommand(context) {
|
|
2826
3812
|
const callbackData = callbackDataFromMessage(context.msg);
|
|
2827
|
-
if (callbackData) return await handleModelCallback(context, callbackData);
|
|
3813
|
+
if (callbackData) return { handled: await handleModelCallback(context, callbackData) };
|
|
2828
3814
|
const parsed = extractCommand(context.msg.text);
|
|
2829
|
-
if (!parsed) return false;
|
|
3815
|
+
if (!parsed) return { handled: false };
|
|
3816
|
+
if (!config.commands.text) return { handled: false };
|
|
2830
3817
|
const target = {
|
|
2831
3818
|
channel: "telegram",
|
|
2832
3819
|
chatId: context.msg.chatId,
|
|
2833
3820
|
accountId: context.msg.accountId
|
|
2834
3821
|
};
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
return true;
|
|
3822
|
+
const authorized = isCommandAuthorized(config, context.msg);
|
|
3823
|
+
const allowWithoutAuth = parsed.command === "help" || parsed.command === "commands";
|
|
3824
|
+
if (!authorized && !allowWithoutAuth) {
|
|
3825
|
+
await context.channel.sendMessage(target, "You are not authorized to use this command.");
|
|
3826
|
+
return { handled: true };
|
|
3827
|
+
}
|
|
3828
|
+
if (parsed.command === "help" || parsed.command === "commands") {
|
|
3829
|
+
const accountId = (context.msg.accountId ?? config.channels.telegram.defaultAccountId).trim() || "default";
|
|
3830
|
+
await context.channel.sendMessage(target, buildCommandsHelpText(accountId));
|
|
3831
|
+
return { handled: true };
|
|
2846
3832
|
}
|
|
2847
3833
|
if (parsed.command === "status") {
|
|
2848
3834
|
const diagnostics = context.channel.getDiagnostics?.() ?? {};
|
|
2849
3835
|
await context.channel.sendMessage(target, `Status\nactiveSessions=${context.db.listSessions().length}\n${JSON.stringify(diagnostics)}`);
|
|
2850
|
-
return true;
|
|
3836
|
+
return { handled: true };
|
|
3837
|
+
}
|
|
3838
|
+
if (parsed.command === "whoami") {
|
|
3839
|
+
const lines = [
|
|
3840
|
+
"Identity",
|
|
3841
|
+
`channel=${context.msg.channel}`,
|
|
3842
|
+
`chatId=${context.msg.chatId}`,
|
|
3843
|
+
`userId=${context.msg.userId}`,
|
|
3844
|
+
`displayName=${context.msg.displayName}`,
|
|
3845
|
+
`agent=${context.agentId}`
|
|
3846
|
+
];
|
|
3847
|
+
await context.channel.sendMessage(target, lines.join("\n"));
|
|
3848
|
+
return { handled: true };
|
|
3849
|
+
}
|
|
3850
|
+
if (parsed.command === "context") {
|
|
3851
|
+
const workspaceDir = resolveAgentWorkspaceDir(config, context.agentId);
|
|
3852
|
+
const lines = [
|
|
3853
|
+
`Agent: ${context.agentId}`,
|
|
3854
|
+
`Workspace: ${workspaceDir}`,
|
|
3855
|
+
"Context files:",
|
|
3856
|
+
...WORKSPACE_CONTEXT_FILE_ORDER.map((file) => `- ${file}`)
|
|
3857
|
+
];
|
|
3858
|
+
await context.channel.sendMessage(target, lines.join("\n"));
|
|
3859
|
+
return { handled: true };
|
|
2851
3860
|
}
|
|
2852
3861
|
if (parsed.command === "models") {
|
|
2853
|
-
const lines = buildModelCatalog(config.models.aliases).slice(0,
|
|
3862
|
+
const lines = buildModelCatalog(config.models.aliases).slice(0, 100).map((entry) => `- ${entry.ref}${entry.alias ? ` (alias: ${entry.alias})` : ""}`);
|
|
2854
3863
|
await context.channel.sendMessage(target, lines.length > 0 ? lines.join("\n") : "No models found.");
|
|
2855
|
-
return true;
|
|
3864
|
+
return { handled: true };
|
|
2856
3865
|
}
|
|
2857
3866
|
if (parsed.command === "model") {
|
|
2858
|
-
const modelArg = parsed.args
|
|
3867
|
+
const modelArg = parsed.args.join(" ").trim();
|
|
2859
3868
|
if (modelArg) {
|
|
2860
3869
|
setInteractiveModel(modelArg);
|
|
2861
3870
|
await context.channel.sendMessage(target, `Interactive model set to ${modelArg}.`);
|
|
2862
|
-
return true;
|
|
3871
|
+
return { handled: true };
|
|
2863
3872
|
}
|
|
2864
3873
|
const providerEntries = Object.entries(groupedModels()).map(([id, models]) => ({
|
|
2865
3874
|
id,
|
|
2866
3875
|
count: models.length
|
|
2867
3876
|
}));
|
|
2868
3877
|
await context.channel.sendRichMessage(target, `Current interactive model: ${config.models.interactive}`, { inlineKeyboard: providerEntries.length > 0 ? buildProviderKeyboard(providerEntries) : buildBrowseProvidersButton() });
|
|
2869
|
-
return true;
|
|
3878
|
+
return { handled: true };
|
|
2870
3879
|
}
|
|
2871
3880
|
if (parsed.command === "pair") {
|
|
2872
|
-
if (!canApproveTelegramPairing({
|
|
2873
|
-
config,
|
|
2874
|
-
msg: context.msg
|
|
2875
|
-
})) {
|
|
2876
|
-
await context.channel.sendMessage(target, "Not authorized to approve pairing requests.");
|
|
2877
|
-
return true;
|
|
2878
|
-
}
|
|
2879
|
-
const action = parsed.args[0]?.toLowerCase();
|
|
2880
|
-
const code = parsed.args[1]?.trim().toUpperCase();
|
|
2881
3881
|
const accountId = (context.msg.accountId ?? config.channels.telegram.defaultAccountId).trim() || "default";
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3882
|
+
const code = (parsed.args[1] ?? parsed.args[0])?.trim().toUpperCase() || "<code>";
|
|
3883
|
+
await context.channel.sendMessage(target, `Pair approvals are CLI-only. Run: hovclaw pairing approve --channel telegram --account ${accountId} ${code}`);
|
|
3884
|
+
return { handled: true };
|
|
3885
|
+
}
|
|
3886
|
+
if (parsed.command === "skills") {
|
|
3887
|
+
const skills = listAvailableSkills();
|
|
3888
|
+
if (skills.length === 0) {
|
|
3889
|
+
await context.channel.sendMessage(target, "No installed skills found.");
|
|
3890
|
+
return { handled: true };
|
|
2885
3891
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
});
|
|
2900
|
-
|
|
3892
|
+
const lines = skills.slice(0, 40).map((name) => {
|
|
3893
|
+
return `- ${name}: ${loadSkill(name)?.frontmatter.description?.trim() || "No description"}`;
|
|
3894
|
+
});
|
|
3895
|
+
await context.channel.sendMessage(target, trimForTelegram(lines.join("\n")));
|
|
3896
|
+
return { handled: true };
|
|
3897
|
+
}
|
|
3898
|
+
if (parsed.command === "identity" || parsed.command === "soul") {
|
|
3899
|
+
const fileName = parsed.command === "identity" ? "IDENTITY.md" : "SOUL.md";
|
|
3900
|
+
const subcommand = parsed.args[0]?.toLowerCase();
|
|
3901
|
+
const body = parsed.args.slice(1).join(" ").trim();
|
|
3902
|
+
if (!subcommand || subcommand === "view") {
|
|
3903
|
+
const current = await readWorkspaceFileOrEmpty(context.agentId, fileName);
|
|
3904
|
+
if (!current.trim()) {
|
|
3905
|
+
await context.channel.sendMessage(target, `${fileName} is empty.`);
|
|
3906
|
+
return { handled: true };
|
|
2901
3907
|
}
|
|
2902
|
-
|
|
3908
|
+
await context.channel.sendMessage(target, trimForTelegram(current));
|
|
3909
|
+
return { handled: true };
|
|
2903
3910
|
}
|
|
2904
|
-
if (
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
context.db.appendAuditEvent({
|
|
2909
|
-
actor: "channel",
|
|
2910
|
-
eventType: "telegram.pair.reject",
|
|
2911
|
-
payload: {
|
|
2912
|
-
accountId,
|
|
2913
|
-
code,
|
|
2914
|
-
userId: result.userId,
|
|
2915
|
-
approver: context.msg.userId
|
|
2916
|
-
}
|
|
2917
|
-
});
|
|
2918
|
-
await context.channel.sendMessage(target, `Rejected pairing for user ${result.userId}.`);
|
|
3911
|
+
if (subcommand === "set") {
|
|
3912
|
+
if (!body) {
|
|
3913
|
+
await context.channel.sendMessage(target, `Usage: /${parsed.command} set <content>`);
|
|
3914
|
+
return { handled: true };
|
|
2919
3915
|
}
|
|
2920
|
-
|
|
3916
|
+
await writeWorkspaceFile(context.agentId, fileName, body);
|
|
3917
|
+
await context.channel.sendMessage(target, `${fileName} updated.`);
|
|
3918
|
+
return { handled: true };
|
|
2921
3919
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
3920
|
+
if (subcommand === "append") {
|
|
3921
|
+
if (!body) {
|
|
3922
|
+
await context.channel.sendMessage(target, `Usage: /${parsed.command} append <content>`);
|
|
3923
|
+
return { handled: true };
|
|
3924
|
+
}
|
|
3925
|
+
const current = await readWorkspaceFileOrEmpty(context.agentId, fileName);
|
|
3926
|
+
const next = current.trim().length > 0 ? `${current.trimEnd()}\n${body}` : body;
|
|
3927
|
+
await writeWorkspaceFile(context.agentId, fileName, next);
|
|
3928
|
+
await context.channel.sendMessage(target, `${fileName} appended.`);
|
|
3929
|
+
return { handled: true };
|
|
3930
|
+
}
|
|
3931
|
+
await context.channel.sendMessage(target, `Usage: /${parsed.command} [view]\n/${parsed.command} set <content>\n/${parsed.command} append <content>`);
|
|
3932
|
+
return { handled: true };
|
|
3933
|
+
}
|
|
3934
|
+
if (parsed.command === "reset" || parsed.command === "new") {
|
|
3935
|
+
const remainder = parsed.args.join(" ").trim();
|
|
3936
|
+
if (!remainder) return {
|
|
3937
|
+
handled: true,
|
|
3938
|
+
resetSession: true
|
|
3939
|
+
};
|
|
3940
|
+
return {
|
|
3941
|
+
handled: false,
|
|
3942
|
+
resetSession: true,
|
|
3943
|
+
promptOverride: remainder
|
|
3944
|
+
};
|
|
2924
3945
|
}
|
|
2925
|
-
|
|
3946
|
+
if (parsed.command === "skill") {
|
|
3947
|
+
const sub = parsed.args[0]?.toLowerCase();
|
|
3948
|
+
if (!sub) {
|
|
3949
|
+
await context.channel.sendMessage(target, "Usage: /skill <name> [input] | /skill create <name> [description] | /skill add <name>");
|
|
3950
|
+
return { handled: true };
|
|
3951
|
+
}
|
|
3952
|
+
if (sub === "create") {
|
|
3953
|
+
const name = normalizeSkillName(parsed.args[1]);
|
|
3954
|
+
if (!name) {
|
|
3955
|
+
await context.channel.sendMessage(target, "Usage: /skill create <name> [description]");
|
|
3956
|
+
return { handled: true };
|
|
3957
|
+
}
|
|
3958
|
+
const description = parsed.args.slice(2).join(" ").trim() || `Skill ${name}`;
|
|
3959
|
+
const skillDir = path.join(config.skillsDir, name);
|
|
3960
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
3961
|
+
const scaffold = [
|
|
3962
|
+
"---",
|
|
3963
|
+
`name: ${name}`,
|
|
3964
|
+
`description: ${description}`,
|
|
3965
|
+
"---",
|
|
3966
|
+
"",
|
|
3967
|
+
`# ${name}`,
|
|
3968
|
+
"",
|
|
3969
|
+
"Describe how this skill should solve the task."
|
|
3970
|
+
].join("\n");
|
|
3971
|
+
try {
|
|
3972
|
+
await fs$1.mkdir(skillDir, { recursive: false });
|
|
3973
|
+
} catch {
|
|
3974
|
+
await context.channel.sendMessage(target, `Skill already exists: ${name}`);
|
|
3975
|
+
return { handled: true };
|
|
3976
|
+
}
|
|
3977
|
+
await fs$1.writeFile(skillPath, `${scaffold}\n`, "utf8");
|
|
3978
|
+
await context.channel.sendMessage(target, `Created ${skillPath}`);
|
|
3979
|
+
return { handled: true };
|
|
3980
|
+
}
|
|
3981
|
+
if (sub === "add") {
|
|
3982
|
+
const skillName = normalizeSkillName(parsed.args[1]);
|
|
3983
|
+
if (!skillName) {
|
|
3984
|
+
await context.channel.sendMessage(target, "Usage: /skill add <name>");
|
|
3985
|
+
return { handled: true };
|
|
3986
|
+
}
|
|
3987
|
+
const { added, path: agentPath } = await addSkillToAgent(context.agentId, skillName);
|
|
3988
|
+
await context.channel.sendMessage(target, added ? `Added ${skillName} to ${agentPath}` : `${skillName} is already enabled for ${context.agentId}.`);
|
|
3989
|
+
return { handled: true };
|
|
3990
|
+
}
|
|
3991
|
+
const targetSkill = normalizeSkillName(sub);
|
|
3992
|
+
if (!targetSkill) {
|
|
3993
|
+
await context.channel.sendMessage(target, "Invalid skill name.");
|
|
3994
|
+
return { handled: true };
|
|
3995
|
+
}
|
|
3996
|
+
if (!new Set(listAvailableSkills()).has(targetSkill)) {
|
|
3997
|
+
await context.channel.sendMessage(target, `Skill not found: ${targetSkill}. Run /skills to list installed skills or /skill create ${targetSkill}.`);
|
|
3998
|
+
return { handled: true };
|
|
3999
|
+
}
|
|
4000
|
+
return {
|
|
4001
|
+
handled: false,
|
|
4002
|
+
promptOverride: resolveSkillPrompt(targetSkill, parsed.args.slice(1))
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
const skillCommand = listSkillCommandSpecs(config).find((entry) => entry.name === parsed.command);
|
|
4006
|
+
if (skillCommand) return {
|
|
4007
|
+
handled: false,
|
|
4008
|
+
promptOverride: resolveSkillPrompt(skillCommand.skillName, parsed.args)
|
|
4009
|
+
};
|
|
4010
|
+
if (parsed.command === "think") {
|
|
4011
|
+
if (parsed.args.length === 0) {
|
|
4012
|
+
await context.channel.sendMessage(target, [
|
|
4013
|
+
`Default thinking level: ${config.commands.defaultThinkingLevel}`,
|
|
4014
|
+
"Usage:",
|
|
4015
|
+
"/think <low|medium|high> <task>",
|
|
4016
|
+
"/think default <low|medium|high>"
|
|
4017
|
+
].join("\n"));
|
|
4018
|
+
return { handled: true };
|
|
4019
|
+
}
|
|
4020
|
+
if (parsed.args[0]?.trim().toLowerCase() === "default") {
|
|
4021
|
+
const requested = parseThinkingLevel(parsed.args[1]);
|
|
4022
|
+
if (!requested) {
|
|
4023
|
+
await context.channel.sendMessage(target, `Current default thinking level: ${config.commands.defaultThinkingLevel}\nUsage: /think default <low|medium|high>`);
|
|
4024
|
+
return { handled: true };
|
|
4025
|
+
}
|
|
4026
|
+
const next = loadFileConfig();
|
|
4027
|
+
next.commands.defaultThinkingLevel = requested;
|
|
4028
|
+
try {
|
|
4029
|
+
persistFileConfig(next);
|
|
4030
|
+
} catch (error) {
|
|
4031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4032
|
+
await context.channel.sendMessage(target, `Failed to update default thinking level: ${message}`);
|
|
4033
|
+
return { handled: true };
|
|
4034
|
+
}
|
|
4035
|
+
await context.channel.sendMessage(target, `Default thinking level set to ${requested}.`);
|
|
4036
|
+
return { handled: true };
|
|
4037
|
+
}
|
|
4038
|
+
const level = parseThinkingLevel(parsed.args[0]);
|
|
4039
|
+
if (!level || parsed.args.length < 2) {
|
|
4040
|
+
await context.channel.sendMessage(target, "Usage: /think <low|medium|high> <task>\nOr set default: /think default <low|medium|high>");
|
|
4041
|
+
return { handled: true };
|
|
4042
|
+
}
|
|
4043
|
+
return {
|
|
4044
|
+
handled: false,
|
|
4045
|
+
promptOverride: parsed.args.slice(1).join(" ").trim(),
|
|
4046
|
+
thinkingLevelOverride: level
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
if (parsed.command === "reasoning") {
|
|
4050
|
+
if (parsed.args.length < 2) {
|
|
4051
|
+
await context.channel.sendMessage(target, "Usage: /reasoning <on|off> <task>\nThis command is per-message and not persisted.");
|
|
4052
|
+
return { handled: true };
|
|
4053
|
+
}
|
|
4054
|
+
const mode = (parsed.args[0] ?? "on").trim().toLowerCase() === "off" ? "off" : "on";
|
|
4055
|
+
const task = parsed.args.slice(1).join(" ").trim();
|
|
4056
|
+
return {
|
|
4057
|
+
handled: false,
|
|
4058
|
+
promptOverride: mode === "on" ? `Solve this task and include concise reasoning in your response: ${task}` : `Solve this task and return only final output without exposing internal reasoning: ${task}`
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
if (parsed.command === "verbose") {
|
|
4062
|
+
if (parsed.args.length < 2) {
|
|
4063
|
+
await context.channel.sendMessage(target, "Usage: /verbose <on|off> <task>\nThis command is per-message and not persisted.");
|
|
4064
|
+
return { handled: true };
|
|
4065
|
+
}
|
|
4066
|
+
const mode = (parsed.args[0] ?? "on").trim().toLowerCase() === "off" ? "off" : "on";
|
|
4067
|
+
const task = parsed.args.slice(1).join(" ").trim();
|
|
4068
|
+
return {
|
|
4069
|
+
handled: false,
|
|
4070
|
+
promptOverride: mode === "on" ? `Answer in a detailed, explicit style for this request: ${task}` : `Answer briefly and directly for this request: ${task}`
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
if (parsed.command === "stop") {
|
|
4074
|
+
await context.channel.sendMessage(target, "Stopping an in-flight run is not supported yet. Use /new to start a fresh session.");
|
|
4075
|
+
return { handled: true };
|
|
4076
|
+
}
|
|
4077
|
+
if (parsed.command === "restart") {
|
|
4078
|
+
if (!config.commands.restart) {
|
|
4079
|
+
await context.channel.sendMessage(target, "Restart command is disabled. Set commands.restart=true to enable.");
|
|
4080
|
+
return { handled: true };
|
|
4081
|
+
}
|
|
4082
|
+
await context.channel.sendMessage(target, "Restart requested. Applying restart now.");
|
|
4083
|
+
try {
|
|
4084
|
+
if ((await requestDaemonRestartFromCurrentProcess()).mode === "spawn-reexec") setTimeout(() => {
|
|
4085
|
+
try {
|
|
4086
|
+
process.kill(process.pid, "SIGTERM");
|
|
4087
|
+
} catch {}
|
|
4088
|
+
}, 250);
|
|
4089
|
+
} catch (error) {
|
|
4090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4091
|
+
await context.channel.sendMessage(target, `Restart failed: ${message}`);
|
|
4092
|
+
}
|
|
4093
|
+
return { handled: true };
|
|
4094
|
+
}
|
|
4095
|
+
if (parsed.command === "config") {
|
|
4096
|
+
if (!config.commands.config) {
|
|
4097
|
+
await context.channel.sendMessage(target, "/config is disabled. Set commands.config=true to enable.");
|
|
4098
|
+
return { handled: true };
|
|
4099
|
+
}
|
|
4100
|
+
const sub = parsed.args[0]?.toLowerCase() ?? "show";
|
|
4101
|
+
if (sub === "show" || sub === "status") {
|
|
4102
|
+
const current = loadFileConfig();
|
|
4103
|
+
await context.channel.sendMessage(target, trimForTelegram(configSummaryText(current)));
|
|
4104
|
+
return { handled: true };
|
|
4105
|
+
}
|
|
4106
|
+
if (sub === "reload") {
|
|
4107
|
+
try {
|
|
4108
|
+
reloadRuntimeConfig();
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4111
|
+
await context.channel.sendMessage(target, `Failed to reload config: ${message}`);
|
|
4112
|
+
return { handled: true };
|
|
4113
|
+
}
|
|
4114
|
+
await context.channel.sendMessage(target, "Reloaded runtime config from disk.");
|
|
4115
|
+
return { handled: true };
|
|
4116
|
+
}
|
|
4117
|
+
if (sub === "get") {
|
|
4118
|
+
const pathSegments = parseConfigPath(parsed.args[1]);
|
|
4119
|
+
if (!pathSegments) {
|
|
4120
|
+
await context.channel.sendMessage(target, "Usage: /config get <path>");
|
|
4121
|
+
return { handled: true };
|
|
4122
|
+
}
|
|
4123
|
+
const found = readPathValue(loadFileConfig(), pathSegments);
|
|
4124
|
+
if (!found.found) {
|
|
4125
|
+
await context.channel.sendMessage(target, `Path not found: ${parsed.args[1]}`);
|
|
4126
|
+
return { handled: true };
|
|
4127
|
+
}
|
|
4128
|
+
const pretty = formatValue(found.value);
|
|
4129
|
+
await context.channel.sendMessage(target, trimForTelegram(`${parsed.args[1]} = ${pretty}`));
|
|
4130
|
+
return { handled: true };
|
|
4131
|
+
}
|
|
4132
|
+
if (sub === "set") {
|
|
4133
|
+
const pathSegments = parseConfigPath(parsed.args[1]);
|
|
4134
|
+
const valueRaw = parsed.args.slice(2).join(" ");
|
|
4135
|
+
if (!pathSegments || !valueRaw.trim()) {
|
|
4136
|
+
await context.channel.sendMessage(target, "Usage: /config set <path> <json-or-text>");
|
|
4137
|
+
return { handled: true };
|
|
4138
|
+
}
|
|
4139
|
+
const next = loadFileConfig();
|
|
4140
|
+
if (!writePathValue(next, pathSegments, parseConfigValue(valueRaw))) {
|
|
4141
|
+
await context.channel.sendMessage(target, `Could not update path: ${parsed.args[1]}`);
|
|
4142
|
+
return { handled: true };
|
|
4143
|
+
}
|
|
4144
|
+
try {
|
|
4145
|
+
persistFileConfig(next);
|
|
4146
|
+
} catch (error) {
|
|
4147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4148
|
+
await context.channel.sendMessage(target, `Config update failed: ${message}`);
|
|
4149
|
+
return { handled: true };
|
|
4150
|
+
}
|
|
4151
|
+
await context.channel.sendMessage(target, `Updated ${parsed.args[1]}.`);
|
|
4152
|
+
return { handled: true };
|
|
4153
|
+
}
|
|
4154
|
+
if (sub === "patch") {
|
|
4155
|
+
const patchRaw = parsed.args.slice(1).join(" ").trim();
|
|
4156
|
+
if (!patchRaw) {
|
|
4157
|
+
await context.channel.sendMessage(target, "Usage: /config patch <json-object>");
|
|
4158
|
+
return { handled: true };
|
|
4159
|
+
}
|
|
4160
|
+
let patch;
|
|
4161
|
+
try {
|
|
4162
|
+
patch = JSON.parse(patchRaw);
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4165
|
+
await context.channel.sendMessage(target, `Invalid JSON patch: ${message}`);
|
|
4166
|
+
return { handled: true };
|
|
4167
|
+
}
|
|
4168
|
+
if (!isRecord$1(patch)) {
|
|
4169
|
+
await context.channel.sendMessage(target, "Patch must be a JSON object.");
|
|
4170
|
+
return { handled: true };
|
|
4171
|
+
}
|
|
4172
|
+
const merged = deepMerge$1(loadFileConfig(), patch);
|
|
4173
|
+
try {
|
|
4174
|
+
persistFileConfig(merged);
|
|
4175
|
+
} catch (error) {
|
|
4176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4177
|
+
await context.channel.sendMessage(target, `Config patch failed: ${message}`);
|
|
4178
|
+
return { handled: true };
|
|
4179
|
+
}
|
|
4180
|
+
await context.channel.sendMessage(target, "Config patch applied.");
|
|
4181
|
+
return { handled: true };
|
|
4182
|
+
}
|
|
4183
|
+
await context.channel.sendMessage(target, "Usage: /config show | get <path> | set <path> <value> | patch <json-object> | reload");
|
|
4184
|
+
return { handled: true };
|
|
4185
|
+
}
|
|
4186
|
+
if (parsed.command === "debug") {
|
|
4187
|
+
const sub = parsed.args[0]?.toLowerCase() ?? "status";
|
|
4188
|
+
const canSelfEnable = sub === "on";
|
|
4189
|
+
if (!config.commands.debug && !canSelfEnable) {
|
|
4190
|
+
await context.channel.sendMessage(target, "/debug is disabled. Set commands.debug=true to enable.");
|
|
4191
|
+
return { handled: true };
|
|
4192
|
+
}
|
|
4193
|
+
if (sub === "on" || sub === "off") {
|
|
4194
|
+
const next = loadFileConfig();
|
|
4195
|
+
next.commands.debug = sub === "on";
|
|
4196
|
+
try {
|
|
4197
|
+
persistFileConfig(next);
|
|
4198
|
+
} catch (error) {
|
|
4199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4200
|
+
await context.channel.sendMessage(target, `Failed to update debug mode: ${message}`);
|
|
4201
|
+
return { handled: true };
|
|
4202
|
+
}
|
|
4203
|
+
await context.channel.sendMessage(target, `commands.debug=${next.commands.debug}`);
|
|
4204
|
+
return { handled: true };
|
|
4205
|
+
}
|
|
4206
|
+
if (sub === "level") {
|
|
4207
|
+
const requested = parsed.args[1]?.trim().toLowerCase();
|
|
4208
|
+
if (!requested || !DEBUG_LOG_LEVELS.has(requested)) {
|
|
4209
|
+
await context.channel.sendMessage(target, "Usage: /debug level <trace|debug|info|warn|error|fatal|silent>");
|
|
4210
|
+
return { handled: true };
|
|
4211
|
+
}
|
|
4212
|
+
logger.level = requested;
|
|
4213
|
+
process.env.LOG_LEVEL = requested;
|
|
4214
|
+
await context.channel.sendMessage(target, `Logger level set to ${requested} (runtime only).`);
|
|
4215
|
+
return { handled: true };
|
|
4216
|
+
}
|
|
4217
|
+
if (sub === "dump") {
|
|
4218
|
+
const payload = {
|
|
4219
|
+
pid: process.pid,
|
|
4220
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
4221
|
+
loggerLevel: logger.level,
|
|
4222
|
+
commands: config.commands,
|
|
4223
|
+
activeSessions: context.db.listSessions().length,
|
|
4224
|
+
channelDiagnostics: context.channel.getDiagnostics?.() ?? {}
|
|
4225
|
+
};
|
|
4226
|
+
await context.channel.sendMessage(target, trimForTelegram(JSON.stringify(payload, null, 2)));
|
|
4227
|
+
return { handled: true };
|
|
4228
|
+
}
|
|
4229
|
+
if (sub === "status") {
|
|
4230
|
+
const lines = [
|
|
4231
|
+
"Debug status",
|
|
4232
|
+
`pid=${process.pid}`,
|
|
4233
|
+
`uptimeSec=${Math.floor(process.uptime())}`,
|
|
4234
|
+
`logger.level=${logger.level}`,
|
|
4235
|
+
`commands.debug=${config.commands.debug}`,
|
|
4236
|
+
`commands.config=${config.commands.config}`,
|
|
4237
|
+
`commands.bash=${config.commands.bash}`,
|
|
4238
|
+
`commands.restart=${config.commands.restart}`,
|
|
4239
|
+
`activeSessions=${context.db.listSessions().length}`,
|
|
4240
|
+
"",
|
|
4241
|
+
"Usage:",
|
|
4242
|
+
"/debug status",
|
|
4243
|
+
"/debug level <trace|debug|info|warn|error|fatal|silent>",
|
|
4244
|
+
"/debug dump",
|
|
4245
|
+
"/debug on|off"
|
|
4246
|
+
];
|
|
4247
|
+
await context.channel.sendMessage(target, lines.join("\n"));
|
|
4248
|
+
return { handled: true };
|
|
4249
|
+
}
|
|
4250
|
+
await context.channel.sendMessage(target, "Usage: /debug status | level <...> | dump | on | off");
|
|
4251
|
+
return { handled: true };
|
|
4252
|
+
}
|
|
4253
|
+
if (parsed.command === "bash") {
|
|
4254
|
+
if (!config.commands.bash) {
|
|
4255
|
+
await context.channel.sendMessage(target, "/bash is disabled. Set commands.bash=true to enable.");
|
|
4256
|
+
return { handled: true };
|
|
4257
|
+
}
|
|
4258
|
+
return {
|
|
4259
|
+
handled: false,
|
|
4260
|
+
promptOverride: `Run this shell command safely: ${parsed.args.join(" ").trim()}`
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
return { handled: false };
|
|
2926
4264
|
}
|
|
2927
4265
|
|
|
2928
4266
|
//#endregion
|
|
2929
4267
|
//#region src/channels/telegram-pairing-store.ts
|
|
4268
|
+
function emptyState() {
|
|
4269
|
+
return {
|
|
4270
|
+
approved: {},
|
|
4271
|
+
pending: {}
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
2930
4274
|
function nowIso$1() {
|
|
2931
4275
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2932
4276
|
}
|
|
@@ -2935,44 +4279,31 @@ function randomCode() {
|
|
|
2935
4279
|
}
|
|
2936
4280
|
var TelegramPairingStore = class {
|
|
2937
4281
|
filePath;
|
|
2938
|
-
state = null;
|
|
2939
4282
|
constructor(storeDir) {
|
|
2940
4283
|
this.filePath = path.join(storeDir, "telegram-pairing.json");
|
|
2941
4284
|
}
|
|
2942
|
-
|
|
2943
|
-
if (this.
|
|
2944
|
-
if (!fs.existsSync(this.filePath)) {
|
|
2945
|
-
this.state = {
|
|
2946
|
-
approved: {},
|
|
2947
|
-
pending: {}
|
|
2948
|
-
};
|
|
2949
|
-
return this.state;
|
|
2950
|
-
}
|
|
4285
|
+
readState() {
|
|
4286
|
+
if (!fs.existsSync(this.filePath)) return emptyState();
|
|
2951
4287
|
try {
|
|
2952
4288
|
const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
|
|
2953
|
-
|
|
4289
|
+
if (!parsed || typeof parsed !== "object") return emptyState();
|
|
4290
|
+
return {
|
|
2954
4291
|
approved: parsed.approved ?? {},
|
|
2955
4292
|
pending: parsed.pending ?? {}
|
|
2956
4293
|
};
|
|
2957
|
-
return this.state;
|
|
2958
4294
|
} catch {
|
|
2959
|
-
|
|
2960
|
-
approved: {},
|
|
2961
|
-
pending: {}
|
|
2962
|
-
};
|
|
2963
|
-
return this.state;
|
|
4295
|
+
return emptyState();
|
|
2964
4296
|
}
|
|
2965
4297
|
}
|
|
2966
|
-
|
|
2967
|
-
if (!this.state) return;
|
|
4298
|
+
writeState(state) {
|
|
2968
4299
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
2969
|
-
fs.writeFileSync(this.filePath, `${JSON.stringify(
|
|
4300
|
+
fs.writeFileSync(this.filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
2970
4301
|
}
|
|
2971
4302
|
isApproved(accountId, userId) {
|
|
2972
|
-
return (this.
|
|
4303
|
+
return (this.readState().approved[accountId] ?? []).includes(userId);
|
|
2973
4304
|
}
|
|
2974
4305
|
ensurePendingCode(accountId, userId) {
|
|
2975
|
-
const state = this.
|
|
4306
|
+
const state = this.readState();
|
|
2976
4307
|
const pendingByAccount = state.pending[accountId] ?? {};
|
|
2977
4308
|
const existing = Object.entries(pendingByAccount).find(([, entry]) => entry.userId === userId);
|
|
2978
4309
|
if (existing) return existing[0];
|
|
@@ -2984,11 +4315,11 @@ var TelegramPairingStore = class {
|
|
|
2984
4315
|
createdAt: nowIso$1()
|
|
2985
4316
|
}
|
|
2986
4317
|
};
|
|
2987
|
-
this.
|
|
4318
|
+
this.writeState(state);
|
|
2988
4319
|
return code;
|
|
2989
4320
|
}
|
|
2990
4321
|
approveByCode(accountId, code) {
|
|
2991
|
-
const state = this.
|
|
4322
|
+
const state = this.readState();
|
|
2992
4323
|
const pendingByAccount = state.pending[accountId] ?? {};
|
|
2993
4324
|
const entry = pendingByAccount[code];
|
|
2994
4325
|
if (!entry) return { ok: false };
|
|
@@ -2997,118 +4328,150 @@ var TelegramPairingStore = class {
|
|
|
2997
4328
|
state.approved[accountId] = Array.from(approved).sort((a, b) => a.localeCompare(b));
|
|
2998
4329
|
delete pendingByAccount[code];
|
|
2999
4330
|
state.pending[accountId] = pendingByAccount;
|
|
3000
|
-
this.
|
|
4331
|
+
this.writeState(state);
|
|
4332
|
+
return {
|
|
4333
|
+
ok: true,
|
|
4334
|
+
userId: entry.userId
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
rejectByCode(accountId, code) {
|
|
4338
|
+
const state = this.readState();
|
|
4339
|
+
const pendingByAccount = state.pending[accountId] ?? {};
|
|
4340
|
+
const entry = pendingByAccount[code];
|
|
4341
|
+
if (!entry) return { ok: false };
|
|
4342
|
+
delete pendingByAccount[code];
|
|
4343
|
+
state.pending[accountId] = pendingByAccount;
|
|
4344
|
+
this.writeState(state);
|
|
3001
4345
|
return {
|
|
3002
4346
|
ok: true,
|
|
3003
4347
|
userId: entry.userId
|
|
3004
4348
|
};
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
|
|
4352
|
+
//#endregion
|
|
4353
|
+
//#region src/channels/telegram-policy.ts
|
|
4354
|
+
function toIdSet(values) {
|
|
4355
|
+
return new Set((values ?? []).map((value) => String(value).trim().toLowerCase()).filter(Boolean));
|
|
4356
|
+
}
|
|
4357
|
+
function splitThread(chatId) {
|
|
4358
|
+
const [baseChatIdRaw, threadIdRaw] = chatId.split("#");
|
|
4359
|
+
return {
|
|
4360
|
+
baseChatId: baseChatIdRaw ?? chatId,
|
|
4361
|
+
...threadIdRaw ? { threadId: threadIdRaw } : {}
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
function isDirectMessage(msg) {
|
|
4365
|
+
return msg.peer?.kind === "direct" || !msg.chatId.startsWith("-") && !msg.chatId.includes("#");
|
|
4366
|
+
}
|
|
4367
|
+
function containsMention(text, assistantName) {
|
|
4368
|
+
const normalized = assistantName.trim().toLowerCase();
|
|
4369
|
+
if (!normalized) return false;
|
|
4370
|
+
return text.toLowerCase().includes(`@${normalized}`);
|
|
4371
|
+
}
|
|
4372
|
+
function isAllowListed(msg, allowSet) {
|
|
4373
|
+
if (allowSet.has("*")) return true;
|
|
4374
|
+
const userId = msg.userId.trim().toLowerCase();
|
|
4375
|
+
if (userId && allowSet.has(userId)) return true;
|
|
4376
|
+
const displayName = msg.displayName.trim().replace(/^@/, "").toLowerCase();
|
|
4377
|
+
if (displayName && allowSet.has(displayName)) return true;
|
|
4378
|
+
return false;
|
|
4379
|
+
}
|
|
4380
|
+
function evaluateTelegramPolicy(params) {
|
|
4381
|
+
const { config, msg, pairingStore } = params;
|
|
4382
|
+
const accountId = (msg.accountId ?? config.channels.telegram.defaultAccountId).trim() || "default";
|
|
4383
|
+
const account = config.channels.telegram.accounts[accountId] ?? config.channels.telegram.accounts[config.channels.telegram.defaultAccountId] ?? config.channels.telegram.accounts.default;
|
|
4384
|
+
if (!account || account.enabled === false) return {
|
|
4385
|
+
allowed: false,
|
|
4386
|
+
reason: "account-disabled"
|
|
4387
|
+
};
|
|
4388
|
+
const direct = isDirectMessage(msg);
|
|
4389
|
+
const allowFromSet = toIdSet(account.allowFrom);
|
|
4390
|
+
if (direct) {
|
|
4391
|
+
const dmPolicy = account.dmPolicy ?? "pairing";
|
|
4392
|
+
if (dmPolicy === "disabled") return {
|
|
4393
|
+
allowed: false,
|
|
4394
|
+
reason: "dm-disabled"
|
|
4395
|
+
};
|
|
4396
|
+
if (dmPolicy === "open") return { allowed: true };
|
|
4397
|
+
if (dmPolicy === "allowlist") {
|
|
4398
|
+
if (isAllowListed(msg, allowFromSet) || pairingStore.isApproved(accountId, msg.userId)) return { allowed: true };
|
|
4399
|
+
return {
|
|
4400
|
+
allowed: false,
|
|
4401
|
+
reason: "dm-not-allowlisted"
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
if (isAllowListed(msg, allowFromSet) || pairingStore.isApproved(accountId, msg.userId)) return { allowed: true };
|
|
3014
4405
|
return {
|
|
3015
|
-
|
|
3016
|
-
|
|
4406
|
+
allowed: false,
|
|
4407
|
+
reason: "pairing-required",
|
|
4408
|
+
pairingCode: pairingStore.ensurePendingCode(accountId, msg.userId)
|
|
3017
4409
|
};
|
|
3018
4410
|
}
|
|
3019
|
-
};
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
default: true
|
|
3041
|
-
}];
|
|
3042
|
-
const extraDirs = /* @__PURE__ */ new Set();
|
|
3043
|
-
extraDirs.add(config.skillsDir);
|
|
3044
|
-
for (const agent of agentList) {
|
|
3045
|
-
const workspace = (agent.workspace || fallbackWorkspace).trim();
|
|
3046
|
-
if (!workspace) continue;
|
|
3047
|
-
extraDirs.add(path.join(workspace, "skills"));
|
|
4411
|
+
const { baseChatId, threadId } = splitThread(msg.chatId);
|
|
4412
|
+
const groupConfig = account.groups?.[baseChatId];
|
|
4413
|
+
const topicConfig = threadId ? groupConfig?.topics?.[threadId] : void 0;
|
|
4414
|
+
if (groupConfig?.enabled === false) return {
|
|
4415
|
+
allowed: false,
|
|
4416
|
+
reason: "group-disabled"
|
|
4417
|
+
};
|
|
4418
|
+
if (topicConfig?.enabled === false) return {
|
|
4419
|
+
allowed: false,
|
|
4420
|
+
reason: "topic-disabled"
|
|
4421
|
+
};
|
|
4422
|
+
const groupPolicy = topicConfig?.groupPolicy ?? groupConfig?.groupPolicy ?? account.groupPolicy ?? "open";
|
|
4423
|
+
if (groupPolicy === "disabled") return {
|
|
4424
|
+
allowed: false,
|
|
4425
|
+
reason: "group-disabled"
|
|
4426
|
+
};
|
|
4427
|
+
if (groupPolicy === "allowlist") {
|
|
4428
|
+
if (!isAllowListed(msg, toIdSet(topicConfig?.allowFrom ?? groupConfig?.allowFrom ?? account.groupAllowFrom))) return {
|
|
4429
|
+
allowed: false,
|
|
4430
|
+
reason: "group-not-allowlisted"
|
|
4431
|
+
};
|
|
3048
4432
|
}
|
|
3049
|
-
return {
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
defaults: { workspace: fallbackWorkspace },
|
|
3053
|
-
list: agentList
|
|
3054
|
-
},
|
|
3055
|
-
skills: { load: { extraDirs: Array.from(extraDirs) } }
|
|
4433
|
+
if ((topicConfig?.requireMention ?? groupConfig?.requireMention ?? false) && !containsMention(msg.text, config.assistantName)) return {
|
|
4434
|
+
allowed: false,
|
|
4435
|
+
reason: "mention-required"
|
|
3056
4436
|
};
|
|
4437
|
+
return { allowed: true };
|
|
3057
4438
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
if (
|
|
3085
|
-
|
|
3086
|
-
recursive: true,
|
|
3087
|
-
force: true
|
|
3088
|
-
});
|
|
4439
|
+
|
|
4440
|
+
//#endregion
|
|
4441
|
+
//#region src/redaction.ts
|
|
4442
|
+
const REDACTED_VALUE = "[REDACTED]";
|
|
4443
|
+
const SENSITIVE_KEY_PATTERNS = [
|
|
4444
|
+
/token/i,
|
|
4445
|
+
/password/i,
|
|
4446
|
+
/secret/i,
|
|
4447
|
+
/api[_-]?key/i,
|
|
4448
|
+
/^key$/i,
|
|
4449
|
+
/authorization/i,
|
|
4450
|
+
/cookie/i,
|
|
4451
|
+
/refresh/i,
|
|
4452
|
+
/access/i
|
|
4453
|
+
];
|
|
4454
|
+
function isSensitiveKey(key) {
|
|
4455
|
+
return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
|
|
4456
|
+
}
|
|
4457
|
+
function redactValue(value, seen) {
|
|
4458
|
+
if (Array.isArray(value)) return value.map((entry) => redactValue(entry, seen));
|
|
4459
|
+
if (!value || typeof value !== "object") return value;
|
|
4460
|
+
if (seen.has(value)) return "[Circular]";
|
|
4461
|
+
seen.add(value);
|
|
4462
|
+
const record = value;
|
|
4463
|
+
const result = {};
|
|
4464
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
4465
|
+
if (isSensitiveKey(key)) {
|
|
4466
|
+
result[key] = REDACTED_VALUE;
|
|
3089
4467
|
continue;
|
|
3090
4468
|
}
|
|
3091
|
-
|
|
4469
|
+
result[key] = redactValue(entry, seen);
|
|
3092
4470
|
}
|
|
3093
|
-
return
|
|
4471
|
+
return result;
|
|
3094
4472
|
}
|
|
3095
|
-
function
|
|
3096
|
-
|
|
3097
|
-
const configPath = resolveOpenClawConfigPath(openclawHome);
|
|
3098
|
-
const sharedSkillsPath = resolveOpenClawSharedSkillsPath(openclawHome);
|
|
3099
|
-
ensureDir(openclawHome);
|
|
3100
|
-
const mirrorConfig = buildMirrorConfig(config);
|
|
3101
|
-
fs.writeFileSync(configPath, `${JSON.stringify(mirrorConfig, null, 2)}\n`, {
|
|
3102
|
-
encoding: "utf8",
|
|
3103
|
-
mode: 384
|
|
3104
|
-
});
|
|
3105
|
-
fs.chmodSync(configPath, 384);
|
|
3106
|
-
return {
|
|
3107
|
-
openclawHome,
|
|
3108
|
-
configPath,
|
|
3109
|
-
sharedSkillsPath,
|
|
3110
|
-
linkedSharedSkills: syncSkillsDir(sharedSkillsPath, config.skillsDir)
|
|
3111
|
-
};
|
|
4473
|
+
function redactSensitiveData(value) {
|
|
4474
|
+
return redactValue(value, /* @__PURE__ */ new WeakSet());
|
|
3112
4475
|
}
|
|
3113
4476
|
|
|
3114
4477
|
//#endregion
|
|
@@ -3384,6 +4747,11 @@ var HovClawDb = class {
|
|
|
3384
4747
|
getAgentState(sessionKey) {
|
|
3385
4748
|
return this.db.prepare(`SELECT state_json FROM agent_state WHERE session_key = ?`).get(sessionKey)?.state_json ?? null;
|
|
3386
4749
|
}
|
|
4750
|
+
clearSession(sessionKey) {
|
|
4751
|
+
this.db.prepare(`DELETE FROM agent_state WHERE session_key = ?`).run(sessionKey);
|
|
4752
|
+
this.db.prepare(`DELETE FROM messages WHERE session_key = ?`).run(sessionKey);
|
|
4753
|
+
this.db.prepare(`DELETE FROM sessions WHERE session_key = ?`).run(sessionKey);
|
|
4754
|
+
}
|
|
3387
4755
|
recordUsageCost(record) {
|
|
3388
4756
|
this.db.prepare(`
|
|
3389
4757
|
INSERT INTO usage_costs (
|
|
@@ -3525,10 +4893,11 @@ var HovClawDb = class {
|
|
|
3525
4893
|
}));
|
|
3526
4894
|
}
|
|
3527
4895
|
appendAuditEvent(record) {
|
|
4896
|
+
const sanitizedPayload = redactSensitiveData(record.payload);
|
|
3528
4897
|
this.db.prepare(`
|
|
3529
4898
|
INSERT INTO audit_log (ts, session_key, actor, event_type, payload_json)
|
|
3530
4899
|
VALUES (?, ?, ?, ?, ?)
|
|
3531
|
-
`).run(record.ts ?? nowIso(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(
|
|
4900
|
+
`).run(record.ts ?? nowIso(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
|
|
3532
4901
|
}
|
|
3533
4902
|
getAuditEvents(eventType) {
|
|
3534
4903
|
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`;
|
|
@@ -3709,7 +5078,7 @@ function deepMerge(base, patch) {
|
|
|
3709
5078
|
return result;
|
|
3710
5079
|
}
|
|
3711
5080
|
const configGetMethod = async (_params, context) => {
|
|
3712
|
-
return context.readFileConfig();
|
|
5081
|
+
return redactSensitiveData(context.readFileConfig());
|
|
3713
5082
|
};
|
|
3714
5083
|
const configSetMethod = async (params, context) => {
|
|
3715
5084
|
const parsed = configSetParamsSchema.parse(params);
|
|
@@ -3791,7 +5160,7 @@ const logsTailMethod = async (params, context) => {
|
|
|
3791
5160
|
message: event.eventType,
|
|
3792
5161
|
sessionKey: event.sessionKey,
|
|
3793
5162
|
actor: event.actor,
|
|
3794
|
-
payload: event.payload
|
|
5163
|
+
payload: redactSensitiveData(event.payload)
|
|
3795
5164
|
})) };
|
|
3796
5165
|
};
|
|
3797
5166
|
|
|
@@ -4120,7 +5489,46 @@ function sendResponse(socket, id, ok, payload, error) {
|
|
|
4120
5489
|
function setUiSecurityHeaders(res) {
|
|
4121
5490
|
res.setHeader("X-Frame-Options", "DENY");
|
|
4122
5491
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
4123
|
-
res.setHeader("Content-Security-Policy",
|
|
5492
|
+
res.setHeader("Content-Security-Policy", [
|
|
5493
|
+
"default-src 'self'",
|
|
5494
|
+
"script-src 'self'",
|
|
5495
|
+
"style-src 'self'",
|
|
5496
|
+
"img-src 'self' data:",
|
|
5497
|
+
"font-src 'self'",
|
|
5498
|
+
"connect-src 'self'",
|
|
5499
|
+
"object-src 'none'",
|
|
5500
|
+
"base-uri 'none'",
|
|
5501
|
+
"frame-ancestors 'none'",
|
|
5502
|
+
"form-action 'none'"
|
|
5503
|
+
].join("; "));
|
|
5504
|
+
}
|
|
5505
|
+
function normalizeOrigin(value) {
|
|
5506
|
+
try {
|
|
5507
|
+
const parsed = new URL(value);
|
|
5508
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
5509
|
+
return `${parsed.protocol}//${parsed.host}`.toLowerCase();
|
|
5510
|
+
} catch {
|
|
5511
|
+
return null;
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
function isOriginAllowed(request, config) {
|
|
5515
|
+
const rawOrigin = request.headers.origin;
|
|
5516
|
+
if (typeof rawOrigin !== "string" || rawOrigin.trim().length === 0) return true;
|
|
5517
|
+
const normalizedOrigin = normalizeOrigin(rawOrigin.trim());
|
|
5518
|
+
if (!normalizedOrigin) return false;
|
|
5519
|
+
const rawHost = request.headers.host;
|
|
5520
|
+
if (typeof rawHost === "string" && rawHost.trim().length > 0) {
|
|
5521
|
+
const normalizedHost = rawHost.trim().toLowerCase();
|
|
5522
|
+
if (normalizedOrigin === `http://${normalizedHost}` || normalizedOrigin === `https://${normalizedHost}`) return true;
|
|
5523
|
+
}
|
|
5524
|
+
const allowedOrigins = config.gateway.auth.allowedOrigins;
|
|
5525
|
+
if (allowedOrigins.includes("*")) return true;
|
|
5526
|
+
for (const allowed of allowedOrigins) {
|
|
5527
|
+
const normalizedAllowed = normalizeOrigin(allowed);
|
|
5528
|
+
if (!normalizedAllowed) continue;
|
|
5529
|
+
if (normalizedAllowed === normalizedOrigin) return true;
|
|
5530
|
+
}
|
|
5531
|
+
return false;
|
|
4124
5532
|
}
|
|
4125
5533
|
function contentTypeForFile(filePath) {
|
|
4126
5534
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -4221,6 +5629,15 @@ var HovClawGatewayServer = class {
|
|
|
4221
5629
|
socket.destroy();
|
|
4222
5630
|
return;
|
|
4223
5631
|
}
|
|
5632
|
+
if (!isOriginAllowed(request, this.runtimeConfig)) {
|
|
5633
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
5634
|
+
socket.destroy();
|
|
5635
|
+
logger.warn({
|
|
5636
|
+
origin: request.headers.origin,
|
|
5637
|
+
host: request.headers.host
|
|
5638
|
+
}, "Rejected websocket upgrade due to origin policy");
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
4224
5641
|
this.wss.handleUpgrade(request, socket, head, (wsSocket) => {
|
|
4225
5642
|
this.wss?.emit("connection", wsSocket, request);
|
|
4226
5643
|
});
|
|
@@ -4335,7 +5752,14 @@ var HovClawGatewayServer = class {
|
|
|
4335
5752
|
const connectParams = parseConnectParams(request.params);
|
|
4336
5753
|
const expectedToken = this.runtimeConfig.gateway.auth.token.trim();
|
|
4337
5754
|
const expectedPassword = this.runtimeConfig.gateway.auth.password.trim();
|
|
4338
|
-
|
|
5755
|
+
const allowUnauthenticated = this.runtimeConfig.gateway.auth.allowUnauthenticated;
|
|
5756
|
+
if (!expectedToken && !expectedPassword) {
|
|
5757
|
+
if (allowUnauthenticated) return { ok: true };
|
|
5758
|
+
return {
|
|
5759
|
+
ok: false,
|
|
5760
|
+
reason: "gateway auth required"
|
|
5761
|
+
};
|
|
5762
|
+
}
|
|
4339
5763
|
if (expectedToken && connectParams.auth?.token === expectedToken) return { ok: true };
|
|
4340
5764
|
if (expectedPassword && connectParams.auth?.password === expectedPassword) return { ok: true };
|
|
4341
5765
|
return {
|
|
@@ -4612,19 +6036,67 @@ function extractCommandPrefix$1(command) {
|
|
|
4612
6036
|
if (!trimmed) return "";
|
|
4613
6037
|
return trimmed.split(/\s+/)[0] ?? "";
|
|
4614
6038
|
}
|
|
4615
|
-
function
|
|
4616
|
-
|
|
6039
|
+
function nonOptionArgs$1(args) {
|
|
6040
|
+
const out = [];
|
|
6041
|
+
let skipNext = false;
|
|
6042
|
+
for (const arg of args) {
|
|
6043
|
+
if (skipNext) {
|
|
6044
|
+
skipNext = false;
|
|
6045
|
+
continue;
|
|
6046
|
+
}
|
|
6047
|
+
if (arg === "--") break;
|
|
6048
|
+
if (arg === "-n" || arg === "-c" || arg === "--max-depth" || arg === "--glob" || arg === "--iglob" || arg === "--type" || arg === "--type-not" || arg === "-e" || arg === "-f") {
|
|
6049
|
+
skipNext = true;
|
|
6050
|
+
continue;
|
|
6051
|
+
}
|
|
6052
|
+
if (arg.startsWith("-")) continue;
|
|
6053
|
+
out.push(arg);
|
|
6054
|
+
}
|
|
6055
|
+
return out;
|
|
6056
|
+
}
|
|
6057
|
+
function isLikelyPathOperand$1(value) {
|
|
6058
|
+
const trimmed = value.trim();
|
|
6059
|
+
if (!trimmed) return false;
|
|
6060
|
+
if (/^\d+$/.test(trimmed)) return false;
|
|
6061
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return false;
|
|
6062
|
+
if (trimmed === "." || trimmed === ".." || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || trimmed.includes("/")) return true;
|
|
6063
|
+
return /^[A-Za-z0-9._-]+$/.test(trimmed);
|
|
4617
6064
|
}
|
|
4618
|
-
function
|
|
6065
|
+
function collectReadPathOperands$1(prefix, args) {
|
|
6066
|
+
if (prefix === "rg") {
|
|
6067
|
+
const plain = nonOptionArgs$1(args);
|
|
6068
|
+
if (plain.length <= 1) return [];
|
|
6069
|
+
return plain.slice(1);
|
|
6070
|
+
}
|
|
6071
|
+
if (prefix === "find") {
|
|
6072
|
+
const plain = nonOptionArgs$1(args);
|
|
6073
|
+
return plain.length > 0 ? [plain[0]] : [];
|
|
6074
|
+
}
|
|
6075
|
+
return nonOptionArgs$1(args);
|
|
6076
|
+
}
|
|
6077
|
+
function validateCommand$1(command, allowedCommandPrefixes, allowedReadRoots, workspaceDir) {
|
|
4619
6078
|
const trimmed = command.trim();
|
|
4620
6079
|
if (!trimmed) throw new Error("Command prefix not allowed: <empty>");
|
|
4621
|
-
if (/[
|
|
4622
|
-
if (/[
|
|
4623
|
-
const
|
|
4624
|
-
if (
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
6080
|
+
if (/[;&|`$()<>]/.test(trimmed) || /[\r\n]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
|
|
6081
|
+
if (/["'\\]/.test(trimmed)) throw new Error("Quoted/escaped shell syntax is not allowed");
|
|
6082
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
6083
|
+
if (tokens.length === 0) throw new Error("Command prefix not allowed: <empty>");
|
|
6084
|
+
const prefix = extractCommandPrefix$1(trimmed);
|
|
6085
|
+
if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
|
|
6086
|
+
if (![
|
|
6087
|
+
"cat",
|
|
6088
|
+
"head",
|
|
6089
|
+
"tail",
|
|
6090
|
+
"wc",
|
|
6091
|
+
"rg",
|
|
6092
|
+
"ls",
|
|
6093
|
+
"find"
|
|
6094
|
+
].includes(prefix)) return;
|
|
6095
|
+
const readRoots = buildEffectiveRoots$1(allowedReadRoots, workspaceDir);
|
|
6096
|
+
const operands = collectReadPathOperands$1(prefix, tokens.slice(1)).filter(isLikelyPathOperand$1);
|
|
6097
|
+
for (const operand of operands) {
|
|
6098
|
+
const resolved = resolveToolPath$1(operand, workspaceDir);
|
|
6099
|
+
if (!startsWithAnyRoot$1(resolved, readRoots)) throw new Error(`Command path is outside allowed read roots: ${resolved}. Allowed roots: ${readRoots.join(", ") || "<none>"}`);
|
|
4628
6100
|
}
|
|
4629
6101
|
}
|
|
4630
6102
|
function maybeTruncate$1(value, maxOutputBytes) {
|
|
@@ -4845,7 +6317,8 @@ var ContainerRuntime = class {
|
|
|
4845
6317
|
}
|
|
4846
6318
|
async exec(command, limits) {
|
|
4847
6319
|
const effective = this.mergeLimits(limits);
|
|
4848
|
-
|
|
6320
|
+
const workspaceDir = getRuntimeSessionContext()?.workspaceDir;
|
|
6321
|
+
validateCommand$1(command, effective.allowedCommandPrefixes, effective.allowedReadRoots, workspaceDir);
|
|
4849
6322
|
return this.withContainer(effective, async (container) => {
|
|
4850
6323
|
return this.runDocker([
|
|
4851
6324
|
"exec",
|
|
@@ -4985,19 +6458,67 @@ function extractCommandPrefix(command) {
|
|
|
4985
6458
|
if (!trimmed) return "";
|
|
4986
6459
|
return trimmed.split(/\s+/)[0] ?? "";
|
|
4987
6460
|
}
|
|
4988
|
-
function
|
|
4989
|
-
|
|
6461
|
+
function nonOptionArgs(args) {
|
|
6462
|
+
const out = [];
|
|
6463
|
+
let skipNext = false;
|
|
6464
|
+
for (const arg of args) {
|
|
6465
|
+
if (skipNext) {
|
|
6466
|
+
skipNext = false;
|
|
6467
|
+
continue;
|
|
6468
|
+
}
|
|
6469
|
+
if (arg === "--") break;
|
|
6470
|
+
if (arg === "-n" || arg === "-c" || arg === "--max-depth" || arg === "--glob" || arg === "--iglob" || arg === "--type" || arg === "--type-not" || arg === "-e" || arg === "-f") {
|
|
6471
|
+
skipNext = true;
|
|
6472
|
+
continue;
|
|
6473
|
+
}
|
|
6474
|
+
if (arg.startsWith("-")) continue;
|
|
6475
|
+
out.push(arg);
|
|
6476
|
+
}
|
|
6477
|
+
return out;
|
|
6478
|
+
}
|
|
6479
|
+
function isLikelyPathOperand(value) {
|
|
6480
|
+
const trimmed = value.trim();
|
|
6481
|
+
if (!trimmed) return false;
|
|
6482
|
+
if (/^\d+$/.test(trimmed)) return false;
|
|
6483
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return false;
|
|
6484
|
+
if (trimmed === "." || trimmed === ".." || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || trimmed.includes("/")) return true;
|
|
6485
|
+
return /^[A-Za-z0-9._-]+$/.test(trimmed);
|
|
6486
|
+
}
|
|
6487
|
+
function collectReadPathOperands(prefix, args) {
|
|
6488
|
+
if (prefix === "rg") {
|
|
6489
|
+
const plain = nonOptionArgs(args);
|
|
6490
|
+
if (plain.length <= 1) return [];
|
|
6491
|
+
return plain.slice(1);
|
|
6492
|
+
}
|
|
6493
|
+
if (prefix === "find") {
|
|
6494
|
+
const plain = nonOptionArgs(args);
|
|
6495
|
+
return plain.length > 0 ? [plain[0]] : [];
|
|
6496
|
+
}
|
|
6497
|
+
return nonOptionArgs(args);
|
|
4990
6498
|
}
|
|
4991
|
-
function validateCommand(command, allowedCommandPrefixes) {
|
|
6499
|
+
function validateCommand(command, allowedCommandPrefixes, allowedReadRoots, workspaceDir) {
|
|
4992
6500
|
const trimmed = command.trim();
|
|
4993
6501
|
if (!trimmed) throw new Error("Command prefix not allowed: <empty>");
|
|
4994
|
-
if (/[
|
|
4995
|
-
if (/[
|
|
4996
|
-
const
|
|
4997
|
-
if (
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
6502
|
+
if (/[;&|`$()<>]/.test(trimmed) || /[\r\n]/.test(trimmed)) throw new Error("Command contains disallowed shell syntax");
|
|
6503
|
+
if (/["'\\]/.test(trimmed)) throw new Error("Quoted/escaped shell syntax is not allowed");
|
|
6504
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
6505
|
+
if (tokens.length === 0) throw new Error("Command prefix not allowed: <empty>");
|
|
6506
|
+
const prefix = extractCommandPrefix(trimmed);
|
|
6507
|
+
if (!prefix || !allowedCommandPrefixes.includes(prefix)) throw new Error(`Command prefix not allowed: ${prefix || "<empty>"}`);
|
|
6508
|
+
if (![
|
|
6509
|
+
"cat",
|
|
6510
|
+
"head",
|
|
6511
|
+
"tail",
|
|
6512
|
+
"wc",
|
|
6513
|
+
"rg",
|
|
6514
|
+
"ls",
|
|
6515
|
+
"find"
|
|
6516
|
+
].includes(prefix)) return;
|
|
6517
|
+
const readRoots = buildEffectiveRoots(allowedReadRoots, workspaceDir);
|
|
6518
|
+
const operands = collectReadPathOperands(prefix, tokens.slice(1)).filter(isLikelyPathOperand);
|
|
6519
|
+
for (const operand of operands) {
|
|
6520
|
+
const resolved = resolveToolPath(operand, workspaceDir);
|
|
6521
|
+
if (!startsWithAnyRoot(resolved, readRoots)) throw new Error(`Command path is outside allowed read roots: ${resolved}. Allowed roots: ${readRoots.join(", ") || "<none>"}`);
|
|
5001
6522
|
}
|
|
5002
6523
|
}
|
|
5003
6524
|
function maybeTruncate(value, maxOutputBytes) {
|
|
@@ -5023,13 +6544,18 @@ var LocalHostRuntime = class {
|
|
|
5023
6544
|
}
|
|
5024
6545
|
async exec(command, limits) {
|
|
5025
6546
|
const effective = this.mergeLimits(limits);
|
|
5026
|
-
|
|
6547
|
+
const workspaceDir = getRuntimeSessionContext()?.workspaceDir;
|
|
6548
|
+
validateCommand(command, effective.allowedCommandPrefixes, effective.allowedReadRoots, workspaceDir);
|
|
6549
|
+
const cwd = workspaceDir ? path.resolve(expandUserPath(workspaceDir)) : process.cwd();
|
|
5027
6550
|
return await new Promise((resolve, reject) => {
|
|
5028
|
-
const child = spawn("bash", ["-lc", command], {
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
6551
|
+
const child = spawn("bash", ["-lc", command], {
|
|
6552
|
+
stdio: [
|
|
6553
|
+
"ignore",
|
|
6554
|
+
"pipe",
|
|
6555
|
+
"pipe"
|
|
6556
|
+
],
|
|
6557
|
+
cwd
|
|
6558
|
+
});
|
|
5033
6559
|
let stdout = "";
|
|
5034
6560
|
let stderr = "";
|
|
5035
6561
|
let timedOut = false;
|
|
@@ -5325,149 +6851,151 @@ function normalizeErrorMessage(error) {
|
|
|
5325
6851
|
if (error instanceof Error) return error.message;
|
|
5326
6852
|
return String(error);
|
|
5327
6853
|
}
|
|
5328
|
-
function createTools({ runtime, audit }) {
|
|
6854
|
+
function createTools({ runtime, audit, bashEnabled }) {
|
|
5329
6855
|
const parser = new Parser();
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
6856
|
+
const bashTool = {
|
|
6857
|
+
name: "bash",
|
|
6858
|
+
label: "Bash",
|
|
6859
|
+
description: "Run a shell command on allowed command prefixes only.",
|
|
6860
|
+
parameters: Type.Object({
|
|
6861
|
+
command: Type.String(),
|
|
6862
|
+
timeoutMs: Type.Optional(Type.Number({
|
|
6863
|
+
minimum: 1e3,
|
|
6864
|
+
maximum: 12e4
|
|
6865
|
+
}))
|
|
6866
|
+
}),
|
|
6867
|
+
execute: async (_toolCallId, params) => {
|
|
6868
|
+
audit({
|
|
6869
|
+
actor: "tool",
|
|
6870
|
+
eventType: "tool.exec",
|
|
6871
|
+
payload: { command: params.command }
|
|
6872
|
+
});
|
|
6873
|
+
const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
|
|
6874
|
+
return textResult([
|
|
6875
|
+
`exitCode: ${result.exitCode}`,
|
|
6876
|
+
result.timedOut ? "timedOut: true" : "timedOut: false",
|
|
6877
|
+
result.truncated ? "truncated: true" : "truncated: false",
|
|
6878
|
+
"",
|
|
6879
|
+
result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
|
|
6880
|
+
"",
|
|
6881
|
+
result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
|
|
6882
|
+
].join("\n"), result);
|
|
6883
|
+
}
|
|
6884
|
+
};
|
|
6885
|
+
const readFileTool = {
|
|
6886
|
+
name: "read_file",
|
|
6887
|
+
label: "Read File",
|
|
6888
|
+
description: "Read a text file from an allowlisted path.",
|
|
6889
|
+
parameters: Type.Object({
|
|
6890
|
+
path: Type.String(),
|
|
6891
|
+
maxBytes: Type.Optional(Type.Number({
|
|
6892
|
+
minimum: 128,
|
|
6893
|
+
maximum: 1e6
|
|
6894
|
+
}))
|
|
6895
|
+
}),
|
|
6896
|
+
execute: async (_toolCallId, params) => {
|
|
6897
|
+
audit({
|
|
6898
|
+
actor: "tool",
|
|
6899
|
+
eventType: "tool.read_file",
|
|
6900
|
+
payload: { path: params.path }
|
|
6901
|
+
});
|
|
6902
|
+
const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
|
|
6903
|
+
return textResult(result.content, result);
|
|
6904
|
+
}
|
|
6905
|
+
};
|
|
6906
|
+
const writeFileTool = {
|
|
6907
|
+
name: "write_file",
|
|
6908
|
+
label: "Write File",
|
|
6909
|
+
description: "Write UTF-8 text content to an allowlisted path.",
|
|
6910
|
+
parameters: Type.Object({
|
|
6911
|
+
path: Type.String(),
|
|
6912
|
+
content: Type.String()
|
|
6913
|
+
}),
|
|
6914
|
+
execute: async (_toolCallId, params) => {
|
|
6915
|
+
audit({
|
|
6916
|
+
actor: "tool",
|
|
6917
|
+
eventType: "tool.write_file",
|
|
6918
|
+
payload: {
|
|
6919
|
+
path: params.path,
|
|
6920
|
+
bytes: Buffer.byteLength(params.content, "utf8")
|
|
6921
|
+
}
|
|
6922
|
+
});
|
|
6923
|
+
const result = await runtime.writeFile(params.path, params.content);
|
|
6924
|
+
return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
|
|
6925
|
+
}
|
|
6926
|
+
};
|
|
6927
|
+
const webSearchTool = {
|
|
6928
|
+
name: "web_search",
|
|
6929
|
+
label: "Web Search",
|
|
6930
|
+
description: "Fetch a URL and return readable article text.",
|
|
6931
|
+
parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
|
|
6932
|
+
execute: async (_toolCallId, params) => {
|
|
6933
|
+
audit({
|
|
6934
|
+
actor: "tool",
|
|
6935
|
+
eventType: "tool.fetch_web",
|
|
6936
|
+
payload: { url: params.url }
|
|
6937
|
+
});
|
|
6938
|
+
const result = await runtime.fetchWeb(params.url);
|
|
6939
|
+
return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
|
|
6940
|
+
}
|
|
6941
|
+
};
|
|
6942
|
+
const fetchPodcastFeedTool = {
|
|
6943
|
+
name: "fetch_podcast_feed",
|
|
6944
|
+
label: "Fetch Podcast Feed",
|
|
6945
|
+
description: "Fetch one or more RSS podcast feeds and return recent episodes.",
|
|
6946
|
+
parameters: Type.Object({
|
|
6947
|
+
urls: Type.Array(Type.String({ format: "uri" }), {
|
|
6948
|
+
minItems: 1,
|
|
6949
|
+
maxItems: 20
|
|
5388
6950
|
}),
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
6951
|
+
limitPerFeed: Type.Optional(Type.Number({
|
|
6952
|
+
minimum: 1,
|
|
6953
|
+
maximum: 50
|
|
6954
|
+
}))
|
|
6955
|
+
}),
|
|
6956
|
+
execute: async (_toolCallId, params) => {
|
|
6957
|
+
const limit = params.limitPerFeed ?? 5;
|
|
6958
|
+
const output = [];
|
|
6959
|
+
for (const url of params.urls) try {
|
|
6960
|
+
const feed = await parser.parseURL(url);
|
|
6961
|
+
const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
|
|
6962
|
+
title: item.title ?? "Untitled episode",
|
|
6963
|
+
publishedAt: item.pubDate || item.isoDate || null,
|
|
6964
|
+
link: item.link ?? "",
|
|
6965
|
+
summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
|
|
6966
|
+
}));
|
|
6967
|
+
output.push({
|
|
6968
|
+
url,
|
|
6969
|
+
title: feed.title ?? "Untitled feed",
|
|
6970
|
+
episodes
|
|
5397
6971
|
});
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
label: "Web Search",
|
|
5405
|
-
description: "Fetch a URL and return readable article text.",
|
|
5406
|
-
parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
|
|
5407
|
-
execute: async (_toolCallId, params) => {
|
|
5408
|
-
audit({
|
|
5409
|
-
actor: "tool",
|
|
5410
|
-
eventType: "tool.fetch_web",
|
|
5411
|
-
payload: { url: params.url }
|
|
6972
|
+
} catch (error) {
|
|
6973
|
+
output.push({
|
|
6974
|
+
url,
|
|
6975
|
+
title: "Unknown feed",
|
|
6976
|
+
episodes: [],
|
|
6977
|
+
error: normalizeErrorMessage(error)
|
|
5412
6978
|
});
|
|
5413
|
-
const result = await runtime.fetchWeb(params.url);
|
|
5414
|
-
return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
|
|
5415
6979
|
}
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
urls: Type.Array(Type.String({ format: "uri" }), {
|
|
5423
|
-
minItems: 1,
|
|
5424
|
-
maxItems: 20
|
|
5425
|
-
}),
|
|
5426
|
-
limitPerFeed: Type.Optional(Type.Number({
|
|
5427
|
-
minimum: 1,
|
|
5428
|
-
maximum: 50
|
|
5429
|
-
}))
|
|
5430
|
-
}),
|
|
5431
|
-
execute: async (_toolCallId, params) => {
|
|
5432
|
-
const limit = params.limitPerFeed ?? 5;
|
|
5433
|
-
const output = [];
|
|
5434
|
-
for (const url of params.urls) try {
|
|
5435
|
-
const feed = await parser.parseURL(url);
|
|
5436
|
-
const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
|
|
5437
|
-
title: item.title ?? "Untitled episode",
|
|
5438
|
-
publishedAt: item.pubDate || item.isoDate || null,
|
|
5439
|
-
link: item.link ?? "",
|
|
5440
|
-
summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
|
|
5441
|
-
}));
|
|
5442
|
-
output.push({
|
|
5443
|
-
url,
|
|
5444
|
-
title: feed.title ?? "Untitled feed",
|
|
5445
|
-
episodes
|
|
5446
|
-
});
|
|
5447
|
-
} catch (error) {
|
|
5448
|
-
output.push({
|
|
5449
|
-
url,
|
|
5450
|
-
title: "Unknown feed",
|
|
5451
|
-
episodes: [],
|
|
5452
|
-
error: normalizeErrorMessage(error)
|
|
5453
|
-
});
|
|
6980
|
+
audit({
|
|
6981
|
+
actor: "tool",
|
|
6982
|
+
eventType: "tool.fetch_podcast_feed",
|
|
6983
|
+
payload: {
|
|
6984
|
+
urls: params.urls,
|
|
6985
|
+
limitPerFeed: limit
|
|
5454
6986
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
}
|
|
5462
|
-
});
|
|
5463
|
-
return textResult(output.map((feed) => {
|
|
5464
|
-
if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
|
|
5465
|
-
const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
|
|
5466
|
-
return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
|
|
5467
|
-
}).join("\n\n---\n\n"), output);
|
|
5468
|
-
}
|
|
6987
|
+
});
|
|
6988
|
+
return textResult(output.map((feed) => {
|
|
6989
|
+
if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
|
|
6990
|
+
const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
|
|
6991
|
+
return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
|
|
6992
|
+
}).join("\n\n---\n\n"), output);
|
|
5469
6993
|
}
|
|
5470
|
-
|
|
6994
|
+
};
|
|
6995
|
+
const tools = [];
|
|
6996
|
+
if (bashEnabled) tools.push(bashTool);
|
|
6997
|
+
tools.push(readFileTool, writeFileTool, webSearchTool, fetchPodcastFeedTool);
|
|
6998
|
+
return tools;
|
|
5471
6999
|
}
|
|
5472
7000
|
|
|
5473
7001
|
//#endregion
|
|
@@ -5475,6 +7003,19 @@ function createTools({ runtime, audit }) {
|
|
|
5475
7003
|
function normalizePrompt(msg) {
|
|
5476
7004
|
return msg.text.trim();
|
|
5477
7005
|
}
|
|
7006
|
+
function formatModelForSessionNotice(modelRef) {
|
|
7007
|
+
const match = modelRef.match(/^([^:\/]+)[:\/](.+)$/);
|
|
7008
|
+
if (!match) return modelRef;
|
|
7009
|
+
const provider = match[1];
|
|
7010
|
+
const model = match[2];
|
|
7011
|
+
if (!provider || !model) return modelRef;
|
|
7012
|
+
return `${provider}/${model}`;
|
|
7013
|
+
}
|
|
7014
|
+
function applyThinkingLevel(prompt, level, force = false) {
|
|
7015
|
+
if (/^Use (low|medium|high) reasoning effort for this task:/i.test(prompt)) return prompt;
|
|
7016
|
+
if (level === "medium" && !force) return prompt;
|
|
7017
|
+
return `Use ${level} reasoning effort for this task: ${prompt}`;
|
|
7018
|
+
}
|
|
5478
7019
|
function buildChannelTarget(msg) {
|
|
5479
7020
|
return {
|
|
5480
7021
|
channel: msg.channel,
|
|
@@ -5510,6 +7051,17 @@ var MultiNotifier = class {
|
|
|
5510
7051
|
}, text);
|
|
5511
7052
|
}
|
|
5512
7053
|
};
|
|
7054
|
+
function validateGatewaySecurityConfig() {
|
|
7055
|
+
if (!config.gateway.enabled) return;
|
|
7056
|
+
const auth = config.gateway.auth;
|
|
7057
|
+
if (auth.allowUnauthenticated) return;
|
|
7058
|
+
if (auth.token.trim() || auth.password.trim()) return;
|
|
7059
|
+
throw new Error([
|
|
7060
|
+
"Gateway auth is required when gateway is enabled.",
|
|
7061
|
+
"Set gateway.auth.token or gateway.auth.password in ~/.hovclaw/config.json,",
|
|
7062
|
+
"or explicitly set gateway.auth.allowUnauthenticated=true for insecure compatibility mode."
|
|
7063
|
+
].join(" "));
|
|
7064
|
+
}
|
|
5513
7065
|
async function main() {
|
|
5514
7066
|
const importedLegacyEnv = ensureConfigFromLegacyEnv();
|
|
5515
7067
|
if (!hasConfigFile()) {
|
|
@@ -5520,6 +7072,7 @@ async function main() {
|
|
|
5520
7072
|
configPath: config.configPath,
|
|
5521
7073
|
credentialsPath: config.credentialsPath
|
|
5522
7074
|
}, "Imported legacy env configuration into ~/.hovclaw");
|
|
7075
|
+
validateGatewaySecurityConfig();
|
|
5523
7076
|
try {
|
|
5524
7077
|
const bootstrap = await ensureWorkspaceBootstrapForConfig(config);
|
|
5525
7078
|
if (bootstrap.createdFileCount > 0) logger.info({
|
|
@@ -5550,7 +7103,8 @@ async function main() {
|
|
|
5550
7103
|
});
|
|
5551
7104
|
const agentManager = new PiAgentManager(db, createTools({
|
|
5552
7105
|
runtime,
|
|
5553
|
-
audit: (record) => db.appendAuditEvent(record)
|
|
7106
|
+
audit: (record) => db.appendAuditEvent(record),
|
|
7107
|
+
bashEnabled: config.runtime.tools.bashEnabled
|
|
5554
7108
|
}));
|
|
5555
7109
|
let lastMessageAt = null;
|
|
5556
7110
|
const telegramPairingStore = new TelegramPairingStore(config.storeDir);
|
|
@@ -5561,10 +7115,19 @@ async function main() {
|
|
|
5561
7115
|
const channels = channelPluginManager.initializeAdapters();
|
|
5562
7116
|
const handleMessage = async (msg) => {
|
|
5563
7117
|
lastMessageAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5564
|
-
const normalizedPrompt = normalizePrompt(msg);
|
|
5565
|
-
if (!normalizedPrompt) return;
|
|
5566
7118
|
const channel = channels.get(msg.channel);
|
|
5567
7119
|
if (!channel) return;
|
|
7120
|
+
const agentId = resolveAgentIdForInbound(config, msg);
|
|
7121
|
+
const sessionKey = composeSessionKey({
|
|
7122
|
+
agent: agentId,
|
|
7123
|
+
channel: msg.channel,
|
|
7124
|
+
identity: canonicalizeSessionIdentity(msg)
|
|
7125
|
+
});
|
|
7126
|
+
const target = buildChannelTarget(msg);
|
|
7127
|
+
let normalizedPrompt = normalizePrompt(msg);
|
|
7128
|
+
if (!normalizedPrompt) return;
|
|
7129
|
+
let thinkingLevel = config.commands.defaultThinkingLevel;
|
|
7130
|
+
let thinkingLevelForced = false;
|
|
5568
7131
|
if (msg.channel === "telegram") {
|
|
5569
7132
|
const policy = evaluateTelegramPolicy({
|
|
5570
7133
|
config,
|
|
@@ -5573,12 +7136,13 @@ async function main() {
|
|
|
5573
7136
|
});
|
|
5574
7137
|
if (!policy.allowed) {
|
|
5575
7138
|
if (policy.pairingCode && channel instanceof TelegramChannel) {
|
|
5576
|
-
|
|
7139
|
+
const accountId = (msg.accountId ?? config.channels.telegram.defaultAccountId).trim() || "default";
|
|
7140
|
+
await channel.sendMessage(target, `Pairing required. Ask an approver to run: hovclaw pairing approve --channel telegram --account ${accountId} ${policy.pairingCode}`);
|
|
5577
7141
|
db.appendAuditEvent({
|
|
5578
7142
|
actor: "channel",
|
|
5579
7143
|
eventType: "telegram.pair.pending",
|
|
5580
7144
|
payload: {
|
|
5581
|
-
accountId
|
|
7145
|
+
accountId,
|
|
5582
7146
|
userId: msg.userId,
|
|
5583
7147
|
code: policy.pairingCode
|
|
5584
7148
|
}
|
|
@@ -5587,20 +7151,36 @@ async function main() {
|
|
|
5587
7151
|
return;
|
|
5588
7152
|
}
|
|
5589
7153
|
if (channel instanceof TelegramChannel) {
|
|
5590
|
-
|
|
7154
|
+
const result = await handleTelegramCommand({
|
|
5591
7155
|
msg,
|
|
5592
7156
|
channel,
|
|
5593
7157
|
db,
|
|
5594
|
-
|
|
5595
|
-
|
|
7158
|
+
agentId,
|
|
7159
|
+
sessionKey
|
|
7160
|
+
});
|
|
7161
|
+
if (result.resetSession) {
|
|
7162
|
+
try {
|
|
7163
|
+
await agentManager.resetSession(sessionKey);
|
|
7164
|
+
} catch (error) {
|
|
7165
|
+
logger.error({
|
|
7166
|
+
error,
|
|
7167
|
+
sessionKey
|
|
7168
|
+
}, "Failed to reset session");
|
|
7169
|
+
await channel.sendMessage(target, "Failed to reset session. Check logs and try again.");
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
const modelLabel = formatModelForSessionNotice(config.models.interactive);
|
|
7173
|
+
await channel.sendMessage(target, `New session started. Model: ${modelLabel}`);
|
|
7174
|
+
}
|
|
7175
|
+
if (result.promptOverride?.trim()) normalizedPrompt = result.promptOverride.trim();
|
|
7176
|
+
if (result.thinkingLevelOverride) {
|
|
7177
|
+
thinkingLevel = result.thinkingLevelOverride;
|
|
7178
|
+
thinkingLevelForced = true;
|
|
7179
|
+
}
|
|
7180
|
+
if (result.handled) return;
|
|
5596
7181
|
}
|
|
5597
7182
|
}
|
|
5598
|
-
|
|
5599
|
-
agent: resolveAgentIdForInbound(config, msg),
|
|
5600
|
-
channel: msg.channel,
|
|
5601
|
-
identity: canonicalizeSessionIdentity(msg)
|
|
5602
|
-
});
|
|
5603
|
-
const target = buildChannelTarget(msg);
|
|
7183
|
+
normalizedPrompt = applyThinkingLevel(normalizedPrompt, thinkingLevel, thinkingLevelForced);
|
|
5604
7184
|
try {
|
|
5605
7185
|
await channel.setTyping?.(target, true);
|
|
5606
7186
|
let finalText = null;
|
|
@@ -5650,6 +7230,18 @@ async function main() {
|
|
|
5650
7230
|
channel.onMessage(handleMessage);
|
|
5651
7231
|
try {
|
|
5652
7232
|
await channel.start();
|
|
7233
|
+
if (channel instanceof TelegramChannel) {
|
|
7234
|
+
const commands = listTelegramNativeCommandSpecs(config, channel.getAccountId());
|
|
7235
|
+
try {
|
|
7236
|
+
await channel.setMyCommands(commands);
|
|
7237
|
+
} catch (commandError) {
|
|
7238
|
+
logger.warn({
|
|
7239
|
+
error: commandError,
|
|
7240
|
+
channel: channelName,
|
|
7241
|
+
commandCount: commands.length
|
|
7242
|
+
}, "Telegram command registration failed; continuing without native command menu sync");
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
5653
7245
|
} catch (error) {
|
|
5654
7246
|
logger.error({
|
|
5655
7247
|
error,
|
|
@@ -5689,6 +7281,7 @@ async function main() {
|
|
|
5689
7281
|
gatewayServer?.start();
|
|
5690
7282
|
const healthPortRaw = process.env.HEALTH_PORT || "8787";
|
|
5691
7283
|
const healthPort = Number(healthPortRaw);
|
|
7284
|
+
const healthHost = (process.env.HEALTH_HOST || "127.0.0.1").trim() || "127.0.0.1";
|
|
5692
7285
|
const healthServer = Number.isFinite(healthPort) && healthPort > 0 ? createServer((req, res) => {
|
|
5693
7286
|
if (req.url !== "/health") {
|
|
5694
7287
|
res.statusCode = 404;
|
|
@@ -5709,8 +7302,11 @@ async function main() {
|
|
|
5709
7302
|
port: config.gateway.port
|
|
5710
7303
|
}
|
|
5711
7304
|
}));
|
|
5712
|
-
}).listen(healthPort, () => {
|
|
5713
|
-
logger.info({
|
|
7305
|
+
}).listen(healthPort, healthHost, () => {
|
|
7306
|
+
logger.info({
|
|
7307
|
+
healthHost,
|
|
7308
|
+
healthPort
|
|
7309
|
+
}, "Health endpoint started");
|
|
5714
7310
|
}) : null;
|
|
5715
7311
|
const shutdown = async () => {
|
|
5716
7312
|
logger.info("Shutting down...");
|