@marcopeg/hal 1.0.18 → 1.0.22
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 +90 -743
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -5
- package/dist/agent/index.js.map +1 -1
- package/dist/bot/commands/git/callback.d.ts +8 -0
- package/dist/bot/commands/git/callback.d.ts.map +1 -0
- package/dist/bot/commands/git/callback.js +72 -0
- package/dist/bot/commands/git/callback.js.map +1 -0
- package/dist/bot/commands/git/clean.d.ts +4 -0
- package/dist/bot/commands/git/clean.d.ts.map +1 -0
- package/dist/bot/commands/git/clean.js +50 -0
- package/dist/bot/commands/git/clean.js.map +1 -0
- package/dist/bot/commands/git/commit.d.ts +4 -0
- package/dist/bot/commands/git/commit.d.ts.map +1 -0
- package/dist/bot/commands/git/commit.js +72 -0
- package/dist/bot/commands/git/commit.js.map +1 -0
- package/dist/bot/commands/git/exec.d.ts +6 -0
- package/dist/bot/commands/git/exec.d.ts.map +1 -0
- package/dist/bot/commands/git/exec.js +16 -0
- package/dist/bot/commands/git/exec.js.map +1 -0
- package/dist/bot/commands/git/index.d.ts +6 -0
- package/dist/bot/commands/git/index.d.ts.map +1 -0
- package/dist/bot/commands/git/index.js +6 -0
- package/dist/bot/commands/git/index.js.map +1 -0
- package/dist/bot/commands/git/init.d.ts +4 -0
- package/dist/bot/commands/git/init.d.ts.map +1 -0
- package/dist/bot/commands/git/init.js +22 -0
- package/dist/bot/commands/git/init.js.map +1 -0
- package/dist/bot/commands/git/status.d.ts +4 -0
- package/dist/bot/commands/git/status.d.ts.map +1 -0
- package/dist/bot/commands/git/status.js +18 -0
- package/dist/bot/commands/git/status.js.map +1 -0
- package/dist/bot/commands/help.js +1 -1
- package/dist/bot/commands/help.js.map +1 -1
- package/dist/bot/commands/loader.d.ts +14 -6
- package/dist/bot/commands/loader.d.ts.map +1 -1
- package/dist/bot/commands/loader.js +126 -34
- package/dist/bot/commands/loader.js.map +1 -1
- package/dist/bot/commands/message.d.ts.map +1 -1
- package/dist/bot/commands/message.js +51 -25
- package/dist/bot/commands/message.js.map +1 -1
- package/dist/bot/commands/model-callback.d.ts +4 -0
- package/dist/bot/commands/model-callback.d.ts.map +1 -0
- package/dist/bot/commands/model-callback.js +34 -0
- package/dist/bot/commands/model-callback.js.map +1 -0
- package/dist/bot/commands/model.d.ts +4 -0
- package/dist/bot/commands/model.d.ts.map +1 -0
- package/dist/bot/commands/model.js +83 -0
- package/dist/bot/commands/model.js.map +1 -0
- package/dist/bot/commands/reset.d.ts +3 -2
- package/dist/bot/commands/reset.d.ts.map +1 -1
- package/dist/bot/commands/reset.js +73 -10
- package/dist/bot/commands/reset.js.map +1 -1
- package/dist/bot/commands/resetPrompt.d.ts +20 -0
- package/dist/bot/commands/resetPrompt.d.ts.map +1 -0
- package/dist/bot/commands/resetPrompt.js +58 -0
- package/dist/bot/commands/resetPrompt.js.map +1 -0
- package/dist/bot/commands/session.d.ts +2 -2
- package/dist/bot/commands/session.d.ts.map +1 -1
- package/dist/bot/commands/session.js +7 -4
- package/dist/bot/commands/session.js.map +1 -1
- package/dist/bot/commands/start.js +2 -2
- package/dist/bot/commands/start.js.map +1 -1
- package/dist/bot/commands/watcher.d.ts +2 -1
- package/dist/bot/commands/watcher.d.ts.map +1 -1
- package/dist/bot/commands/watcher.js +11 -5
- package/dist/bot/commands/watcher.js.map +1 -1
- package/dist/bot/handlers/text.d.ts.map +1 -1
- package/dist/bot/handlers/text.js +11 -6
- package/dist/bot/handlers/text.js.map +1 -1
- package/dist/bot/middleware/auth.d.ts +0 -3
- package/dist/bot/middleware/auth.d.ts.map +1 -1
- package/dist/bot/middleware/auth.js +13 -12
- package/dist/bot/middleware/auth.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +43 -11
- package/dist/bot.js.map +1 -1
- package/dist/cli.js +52 -29
- package/dist/cli.js.map +1 -1
- package/dist/config-watcher.d.ts +10 -0
- package/dist/config-watcher.d.ts.map +1 -0
- package/dist/config-watcher.js +55 -0
- package/dist/config-watcher.js.map +1 -0
- package/dist/config-writer.d.ts +8 -0
- package/dist/config-writer.d.ts.map +1 -0
- package/dist/config-writer.js +57 -0
- package/dist/config-writer.js.map +1 -0
- package/dist/config.d.ts +338 -53
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +337 -92
- package/dist/config.js.map +1 -1
- package/dist/context/resolver.d.ts +4 -0
- package/dist/context/resolver.d.ts.map +1 -1
- package/dist/context/resolver.js +8 -2
- package/dist/context/resolver.js.map +1 -1
- package/dist/default-models.d.ts +3 -0
- package/dist/default-models.d.ts.map +1 -0
- package/dist/default-models.js +16 -0
- package/dist/default-models.js.map +1 -0
- package/dist/engine/adapters/antigravity.d.ts +13 -0
- package/dist/engine/adapters/antigravity.d.ts.map +1 -0
- package/dist/engine/adapters/antigravity.js +230 -0
- package/dist/engine/adapters/antigravity.js.map +1 -0
- package/dist/engine/adapters/claude.js +2 -2
- package/dist/engine/adapters/claude.js.map +1 -1
- package/dist/engine/adapters/codex.d.ts.map +1 -1
- package/dist/engine/adapters/codex.js +27 -8
- package/dist/engine/adapters/codex.js.map +1 -1
- package/dist/engine/adapters/copilot.d.ts.map +1 -1
- package/dist/engine/adapters/copilot.js +6 -3
- package/dist/engine/adapters/copilot.js.map +1 -1
- package/dist/engine/adapters/cursor.d.ts +3 -0
- package/dist/engine/adapters/cursor.d.ts.map +1 -0
- package/dist/engine/adapters/cursor.js +106 -0
- package/dist/engine/adapters/cursor.js.map +1 -0
- package/dist/engine/adapters/opencode.d.ts +2 -2
- package/dist/engine/adapters/opencode.d.ts.map +1 -1
- package/dist/engine/adapters/opencode.js +33 -13
- package/dist/engine/adapters/opencode.js.map +1 -1
- package/dist/engine/detect.d.ts +26 -0
- package/dist/engine/detect.d.ts.map +1 -0
- package/dist/engine/detect.js +129 -0
- package/dist/engine/detect.js.map +1 -0
- package/dist/engine/model-cache.d.ts +25 -0
- package/dist/engine/model-cache.d.ts.map +1 -0
- package/dist/engine/model-cache.js +162 -0
- package/dist/engine/model-cache.js.map +1 -0
- package/dist/engine/model-validation.d.ts +9 -0
- package/dist/engine/model-validation.d.ts.map +1 -0
- package/dist/engine/model-validation.js +21 -0
- package/dist/engine/model-validation.js.map +1 -0
- package/dist/engine/models-data.generated.d.ts +4 -0
- package/dist/engine/models-data.generated.d.ts.map +1 -0
- package/dist/engine/models-data.generated.js +196 -0
- package/dist/engine/models-data.generated.js.map +1 -0
- package/dist/engine/models-fetch.d.ts +5 -0
- package/dist/engine/models-fetch.d.ts.map +1 -0
- package/dist/engine/models-fetch.js +54 -0
- package/dist/engine/models-fetch.js.map +1 -0
- package/dist/engine/probe-utils.d.ts +6 -0
- package/dist/engine/probe-utils.d.ts.map +1 -0
- package/dist/engine/probe-utils.js +42 -0
- package/dist/engine/probe-utils.js.map +1 -0
- package/dist/engine/prompt.d.ts.map +1 -1
- package/dist/engine/prompt.js +8 -0
- package/dist/engine/prompt.js.map +1 -1
- package/dist/engine/registry.d.ts.map +1 -1
- package/dist/engine/registry.js +4 -0
- package/dist/engine/registry.js.map +1 -1
- package/dist/engine/types.d.ts +3 -3
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/types.js +2 -0
- package/dist/engine/types.js.map +1 -1
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +73 -0
- package/dist/prompts.js.map +1 -0
- package/dist/setup-wizard.d.ts +4 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +258 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +6 -2
package/dist/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { parse as parseEnv } from "dotenv";
|
|
4
|
+
import stripJsonComments from "strip-json-comments";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
// ─── Zod helpers ──────────────────────────────────────────────────────────────
|
|
6
8
|
const TranscriptionModelSchema = z.enum([
|
|
@@ -18,7 +20,29 @@ const TranscriptionModelSchema = z.enum([
|
|
|
18
20
|
]);
|
|
19
21
|
const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
|
|
20
22
|
// ─── Globals schema (all fields optional) ─────────────────────────────────────
|
|
21
|
-
const EngineNameSchema = z.enum([
|
|
23
|
+
const EngineNameSchema = z.enum([
|
|
24
|
+
"claude",
|
|
25
|
+
"copilot",
|
|
26
|
+
"codex",
|
|
27
|
+
"opencode",
|
|
28
|
+
"cursor",
|
|
29
|
+
"antigravity",
|
|
30
|
+
]);
|
|
31
|
+
const CodexEngineConfigSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
networkAccess: z.boolean(),
|
|
34
|
+
fullDiskAccess: z.boolean(),
|
|
35
|
+
dangerouslyEnableYolo: z.boolean(),
|
|
36
|
+
})
|
|
37
|
+
.partial()
|
|
38
|
+
.optional();
|
|
39
|
+
const AntigravityEngineConfigSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
approvalMode: z.enum(["default", "auto_edit", "yolo"]),
|
|
42
|
+
sandbox: z.boolean(),
|
|
43
|
+
})
|
|
44
|
+
.partial()
|
|
45
|
+
.optional();
|
|
22
46
|
const EngineConfigSchema = z
|
|
23
47
|
.object({
|
|
24
48
|
name: EngineNameSchema,
|
|
@@ -26,6 +50,8 @@ const EngineConfigSchema = z
|
|
|
26
50
|
model: z.string(),
|
|
27
51
|
session: z.boolean(),
|
|
28
52
|
sessionMsg: z.string(),
|
|
53
|
+
codex: CodexEngineConfigSchema,
|
|
54
|
+
antigravity: AntigravityEngineConfigSchema,
|
|
29
55
|
})
|
|
30
56
|
.partial()
|
|
31
57
|
.optional();
|
|
@@ -39,30 +65,72 @@ const CommandMessageSchema = z
|
|
|
39
65
|
});
|
|
40
66
|
const StartConfigSchema = z
|
|
41
67
|
.object({
|
|
68
|
+
enabled: z.boolean().optional(),
|
|
42
69
|
session: z.object({ reset: z.boolean() }).partial().optional(),
|
|
43
|
-
message: CommandMessageSchema,
|
|
70
|
+
message: CommandMessageSchema.optional(),
|
|
44
71
|
})
|
|
45
72
|
.optional();
|
|
46
73
|
const SimpleCommandConfigSchema = z
|
|
47
74
|
.object({
|
|
48
|
-
|
|
75
|
+
enabled: z.boolean().optional(),
|
|
76
|
+
message: CommandMessageSchema.optional(),
|
|
77
|
+
})
|
|
78
|
+
.optional();
|
|
79
|
+
const ResetCommandConfigSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
enabled: z.boolean().optional(),
|
|
82
|
+
session: z.object({ reset: z.boolean() }).partial().optional(),
|
|
83
|
+
message: z
|
|
84
|
+
.object({
|
|
85
|
+
confirm: z.string().optional(),
|
|
86
|
+
done: z.string().optional(),
|
|
87
|
+
})
|
|
88
|
+
.optional(),
|
|
89
|
+
timeout: z.number().positive().optional(),
|
|
90
|
+
})
|
|
91
|
+
.optional();
|
|
92
|
+
const GitConfigSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
enabled: z.boolean().optional(),
|
|
49
95
|
})
|
|
50
96
|
.optional();
|
|
51
97
|
const CommandsConfigSchema = z
|
|
52
98
|
.object({
|
|
53
99
|
start: StartConfigSchema,
|
|
54
100
|
help: SimpleCommandConfigSchema,
|
|
55
|
-
reset:
|
|
101
|
+
reset: ResetCommandConfigSchema,
|
|
56
102
|
clean: SimpleCommandConfigSchema,
|
|
103
|
+
git: GitConfigSchema,
|
|
104
|
+
model: GitConfigSchema,
|
|
57
105
|
})
|
|
58
106
|
.optional();
|
|
107
|
+
const ProviderModelSchema = z.object({
|
|
108
|
+
name: z.string().min(1),
|
|
109
|
+
description: z.string().optional(),
|
|
110
|
+
});
|
|
111
|
+
const ProvidersConfigSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
claude: z.array(ProviderModelSchema).optional(),
|
|
114
|
+
copilot: z.array(ProviderModelSchema).optional(),
|
|
115
|
+
codex: z.array(ProviderModelSchema).optional(),
|
|
116
|
+
opencode: z.array(ProviderModelSchema).optional(),
|
|
117
|
+
cursor: z.array(ProviderModelSchema).optional(),
|
|
118
|
+
antigravity: z.array(ProviderModelSchema).optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional();
|
|
121
|
+
const AllowedUserIdSchema = z.union([z.number(), z.string()]);
|
|
122
|
+
const AccessSchema = z
|
|
123
|
+
.object({
|
|
124
|
+
allowedUserIds: z.array(AllowedUserIdSchema),
|
|
125
|
+
dangerouslyAllowUnrestrictedAccess: z.boolean(),
|
|
126
|
+
})
|
|
127
|
+
.partial()
|
|
128
|
+
.optional();
|
|
59
129
|
const GlobalsFileSchema = z
|
|
60
130
|
.object({
|
|
61
|
-
access:
|
|
62
|
-
.object({ allowedUserIds: z.array(z.number()) })
|
|
63
|
-
.partial()
|
|
64
|
-
.optional(),
|
|
131
|
+
access: AccessSchema,
|
|
65
132
|
engine: EngineConfigSchema,
|
|
133
|
+
providers: ProvidersConfigSchema,
|
|
66
134
|
logging: z
|
|
67
135
|
.object({
|
|
68
136
|
level: LogLevelSchema,
|
|
@@ -94,11 +162,9 @@ const ProjectFileSchema = z.object({
|
|
|
94
162
|
telegram: z.object({
|
|
95
163
|
botToken: z.string().min(1, "project.telegram.botToken is required"),
|
|
96
164
|
}),
|
|
97
|
-
access:
|
|
98
|
-
.object({ allowedUserIds: z.array(z.number()) })
|
|
99
|
-
.partial()
|
|
100
|
-
.optional(),
|
|
165
|
+
access: AccessSchema,
|
|
101
166
|
engine: EngineConfigSchema,
|
|
167
|
+
providers: ProvidersConfigSchema,
|
|
102
168
|
logging: z
|
|
103
169
|
.object({
|
|
104
170
|
level: LogLevelSchema,
|
|
@@ -142,6 +208,52 @@ const LocalConfigFileSchema = z
|
|
|
142
208
|
projects: z.array(LocalProjectSchema).optional(),
|
|
143
209
|
})
|
|
144
210
|
.optional();
|
|
211
|
+
// ─── Config load result & errors ───────────────────────────────────────────────
|
|
212
|
+
export class ConfigLoadError extends Error {
|
|
213
|
+
constructor(message) {
|
|
214
|
+
super(message);
|
|
215
|
+
this.name = "ConfigLoadError";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Telegram user ID range (Bot API): 1 to 0xFFFFFFFFF inclusive
|
|
219
|
+
const TELEGRAM_USER_ID_MAX = 0xfffffffff;
|
|
220
|
+
function parseTelegramUserId(value, path) {
|
|
221
|
+
const str = typeof value === "string" ? value : String(value);
|
|
222
|
+
const num = Number(str);
|
|
223
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
224
|
+
throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: "${str}" is not a valid integer`);
|
|
225
|
+
}
|
|
226
|
+
if (typeof value === "string" && String(num) !== str) {
|
|
227
|
+
throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: "${str}" (expected exact integer form, no spaces/decimals/leading zeros)`);
|
|
228
|
+
}
|
|
229
|
+
if (num < 1 || num > TELEGRAM_USER_ID_MAX) {
|
|
230
|
+
throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: ${num} is outside Telegram user ID range (1–${TELEGRAM_USER_ID_MAX})`);
|
|
231
|
+
}
|
|
232
|
+
return num;
|
|
233
|
+
}
|
|
234
|
+
function normalizeAllowedUserIdsInConfig(config) {
|
|
235
|
+
const globalsAccess = config.globals?.access;
|
|
236
|
+
if (globalsAccess?.allowedUserIds != null) {
|
|
237
|
+
const raw = globalsAccess.allowedUserIds;
|
|
238
|
+
const normalized = [];
|
|
239
|
+
for (let i = 0; i < raw.length; i++) {
|
|
240
|
+
normalized.push(parseTelegramUserId(raw[i], `globals.access.allowedUserIds[${i}]`));
|
|
241
|
+
}
|
|
242
|
+
globalsAccess.allowedUserIds = normalized;
|
|
243
|
+
}
|
|
244
|
+
for (let j = 0; j < config.projects.length; j++) {
|
|
245
|
+
const project = config.projects[j];
|
|
246
|
+
const access = project.access;
|
|
247
|
+
if (access?.allowedUserIds == null)
|
|
248
|
+
continue;
|
|
249
|
+
const raw = access.allowedUserIds;
|
|
250
|
+
const normalized = [];
|
|
251
|
+
for (let i = 0; i < raw.length; i++) {
|
|
252
|
+
normalized.push(parseTelegramUserId(raw[i], `projects[${j}].access.allowedUserIds[${i}]`));
|
|
253
|
+
}
|
|
254
|
+
access.allowedUserIds = normalized;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
145
257
|
// ─── Slug derivation ──────────────────────────────────────────────────────────
|
|
146
258
|
export function deriveSlug(name, cwd) {
|
|
147
259
|
if (name)
|
|
@@ -181,39 +293,78 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
|
|
|
181
293
|
if (msg.from) {
|
|
182
294
|
const filePath = resolve(resolvedCwd, msg.from);
|
|
183
295
|
if (!existsSync(filePath)) {
|
|
184
|
-
|
|
185
|
-
process.exit(1);
|
|
296
|
+
throw new ConfigLoadError(`Configuration error: ${label}.message.from file not found: ${filePath}`);
|
|
186
297
|
}
|
|
187
298
|
try {
|
|
188
299
|
return readFileSync(filePath, "utf-8");
|
|
189
300
|
}
|
|
190
301
|
catch (err) {
|
|
191
|
-
|
|
192
|
-
process.exit(1);
|
|
302
|
+
throw new ConfigLoadError(`Configuration error: cannot read ${label}.message.from file: ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
193
303
|
}
|
|
194
304
|
}
|
|
195
305
|
return msg.text;
|
|
196
306
|
}
|
|
307
|
+
// Resolve command enabled flags (project > globals > default)
|
|
197
308
|
const rawStart = project.commands?.start ?? globals.commands?.start;
|
|
198
|
-
let resolvedStart;
|
|
199
|
-
if (rawStart) {
|
|
200
|
-
resolvedStart = {
|
|
201
|
-
sessionReset: rawStart.session?.reset ?? false,
|
|
202
|
-
message: resolveMessageTemplate(rawStart.message, "commands.start"),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
309
|
const rawHelp = project.commands?.help ?? globals.commands?.help;
|
|
206
|
-
const resolvedHelp = rawHelp
|
|
207
|
-
? { message: resolveMessageTemplate(rawHelp.message, "commands.help") }
|
|
208
|
-
: undefined;
|
|
209
310
|
const rawReset = project.commands?.reset ?? globals.commands?.reset;
|
|
210
|
-
const resolvedReset = rawReset
|
|
211
|
-
? { message: resolveMessageTemplate(rawReset.message, "commands.reset") }
|
|
212
|
-
: undefined;
|
|
213
311
|
const rawClean = project.commands?.clean ?? globals.commands?.clean;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
312
|
+
const resolvedCommands = {
|
|
313
|
+
start: {
|
|
314
|
+
enabled: project.commands?.start?.enabled ??
|
|
315
|
+
globals.commands?.start?.enabled ??
|
|
316
|
+
true,
|
|
317
|
+
sessionReset: rawStart?.session?.reset ?? false,
|
|
318
|
+
message: rawStart?.message
|
|
319
|
+
? resolveMessageTemplate(rawStart.message, "commands.start")
|
|
320
|
+
: undefined,
|
|
321
|
+
},
|
|
322
|
+
help: {
|
|
323
|
+
enabled: project.commands?.help?.enabled ??
|
|
324
|
+
globals.commands?.help?.enabled ??
|
|
325
|
+
true,
|
|
326
|
+
message: rawHelp?.message
|
|
327
|
+
? resolveMessageTemplate(rawHelp.message, "commands.help")
|
|
328
|
+
: undefined,
|
|
329
|
+
},
|
|
330
|
+
reset: {
|
|
331
|
+
enabled: project.commands?.reset?.enabled ??
|
|
332
|
+
globals.commands?.reset?.enabled ??
|
|
333
|
+
true,
|
|
334
|
+
sessionReset: rawReset?.session?.reset ?? false,
|
|
335
|
+
message: {
|
|
336
|
+
confirm: rawReset?.message?.confirm,
|
|
337
|
+
done: rawReset?.message?.done,
|
|
338
|
+
},
|
|
339
|
+
timeout: rawReset?.timeout ?? 60,
|
|
340
|
+
},
|
|
341
|
+
clean: {
|
|
342
|
+
enabled: project.commands?.clean?.enabled ??
|
|
343
|
+
globals.commands?.clean?.enabled ??
|
|
344
|
+
true,
|
|
345
|
+
message: rawClean?.message
|
|
346
|
+
? resolveMessageTemplate(rawClean.message, "commands.clean")
|
|
347
|
+
: undefined,
|
|
348
|
+
},
|
|
349
|
+
git: {
|
|
350
|
+
enabled: project.commands?.git?.enabled ??
|
|
351
|
+
globals.commands?.git?.enabled ??
|
|
352
|
+
false,
|
|
353
|
+
},
|
|
354
|
+
model: {
|
|
355
|
+
enabled: project.commands?.model?.enabled ??
|
|
356
|
+
globals.commands?.model?.enabled ??
|
|
357
|
+
true,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
const engineName = (project.engine?.name ??
|
|
361
|
+
globals.engine?.name ??
|
|
362
|
+
"claude");
|
|
363
|
+
const rawProviderModels = project.providers?.[engineName] ?? globals.providers?.[engineName] ?? [];
|
|
364
|
+
const providerModels = rawProviderModels.map((m) => ({
|
|
365
|
+
name: m.name,
|
|
366
|
+
description: m.description,
|
|
367
|
+
}));
|
|
217
368
|
return {
|
|
218
369
|
slug,
|
|
219
370
|
name: project.name,
|
|
@@ -223,19 +374,37 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
|
|
|
223
374
|
logDir,
|
|
224
375
|
telegram: { botToken: project.telegram.botToken },
|
|
225
376
|
access: {
|
|
226
|
-
allowedUserIds: project.access
|
|
377
|
+
allowedUserIds: ((project.access !== undefined
|
|
378
|
+
? project.access.allowedUserIds
|
|
379
|
+
: globals.access?.allowedUserIds) ?? []),
|
|
380
|
+
dangerouslyAllowUnrestrictedAccess: (project.access !== undefined
|
|
381
|
+
? project.access.dangerouslyAllowUnrestrictedAccess
|
|
382
|
+
: globals.access?.dangerouslyAllowUnrestrictedAccess) ?? false,
|
|
227
383
|
},
|
|
228
|
-
engine:
|
|
229
|
-
|
|
230
|
-
"claude"),
|
|
231
|
-
engineCommand: project.engine?.command ??
|
|
232
|
-
globals.engine?.command ??
|
|
233
|
-
project.engine?.name ??
|
|
234
|
-
globals.engine?.name ??
|
|
235
|
-
"claude",
|
|
384
|
+
engine: engineName,
|
|
385
|
+
engineCommand: project.engine?.command ?? globals.engine?.command,
|
|
236
386
|
engineModel: project.engine?.model ?? globals.engine?.model,
|
|
237
387
|
engineSession: project.engine?.session ?? globals.engine?.session ?? true,
|
|
238
388
|
engineSessionMsg: project.engine?.sessionMsg ?? globals.engine?.sessionMsg ?? "hi!",
|
|
389
|
+
codex: {
|
|
390
|
+
networkAccess: project.engine?.codex?.networkAccess ??
|
|
391
|
+
globals.engine?.codex?.networkAccess ??
|
|
392
|
+
false,
|
|
393
|
+
fullDiskAccess: project.engine?.codex?.fullDiskAccess ??
|
|
394
|
+
globals.engine?.codex?.fullDiskAccess ??
|
|
395
|
+
false,
|
|
396
|
+
dangerouslyEnableYolo: project.engine?.codex?.dangerouslyEnableYolo ??
|
|
397
|
+
globals.engine?.codex?.dangerouslyEnableYolo ??
|
|
398
|
+
false,
|
|
399
|
+
},
|
|
400
|
+
antigravity: {
|
|
401
|
+
approvalMode: project.engine?.antigravity?.approvalMode ??
|
|
402
|
+
globals.engine?.antigravity?.approvalMode ??
|
|
403
|
+
"yolo",
|
|
404
|
+
sandbox: project.engine?.antigravity?.sandbox ??
|
|
405
|
+
globals.engine?.antigravity?.sandbox ??
|
|
406
|
+
false,
|
|
407
|
+
},
|
|
239
408
|
logging: {
|
|
240
409
|
level: project.logging?.level ?? globals.logging?.level ?? "info",
|
|
241
410
|
flow: project.logging?.flow ?? globals.logging?.flow ?? true,
|
|
@@ -255,13 +424,9 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
|
|
|
255
424
|
true,
|
|
256
425
|
}
|
|
257
426
|
: undefined,
|
|
427
|
+
providerModels,
|
|
258
428
|
context: hasContext ? { ...rootContext, ...project.context } : undefined,
|
|
259
|
-
commands:
|
|
260
|
-
start: resolvedStart,
|
|
261
|
-
help: resolvedHelp,
|
|
262
|
-
reset: resolvedReset,
|
|
263
|
-
clean: resolvedClean,
|
|
264
|
-
},
|
|
429
|
+
commands: resolvedCommands,
|
|
265
430
|
};
|
|
266
431
|
}
|
|
267
432
|
// ─── Boot-time uniqueness validation ──────────────────────────────────────────
|
|
@@ -271,24 +436,37 @@ export function validateProjects(projects) {
|
|
|
271
436
|
const names = new Set();
|
|
272
437
|
for (const project of projects) {
|
|
273
438
|
if (cwds.has(project.cwd)) {
|
|
274
|
-
|
|
275
|
-
process.exit(1);
|
|
439
|
+
throw new ConfigLoadError(`Configuration error: duplicate project cwd "${project.cwd}". Each project must have a unique cwd.`);
|
|
276
440
|
}
|
|
277
441
|
cwds.add(project.cwd);
|
|
278
442
|
if (tokens.has(project.telegram.botToken)) {
|
|
279
|
-
|
|
280
|
-
process.exit(1);
|
|
443
|
+
throw new ConfigLoadError(`Configuration error: duplicate botToken in project "${project.slug}". Each project must use a unique Telegram bot token.`);
|
|
281
444
|
}
|
|
282
445
|
tokens.add(project.telegram.botToken);
|
|
283
446
|
if (project.name) {
|
|
284
447
|
if (names.has(project.name)) {
|
|
285
|
-
|
|
286
|
-
process.exit(1);
|
|
448
|
+
throw new ConfigLoadError(`Configuration error: duplicate project name "${project.name}". Each named project must have a unique name.`);
|
|
287
449
|
}
|
|
288
450
|
names.add(project.name);
|
|
289
451
|
}
|
|
290
452
|
}
|
|
291
453
|
}
|
|
454
|
+
// ─── Boot-time access policy validation ───────────────────────────────────────
|
|
455
|
+
export function validateAccessPolicies(projects) {
|
|
456
|
+
const errors = [];
|
|
457
|
+
for (const project of projects) {
|
|
458
|
+
const { allowedUserIds, dangerouslyAllowUnrestrictedAccess } = project.access;
|
|
459
|
+
const hasUsers = allowedUserIds.length > 0;
|
|
460
|
+
const hasUnsafe = dangerouslyAllowUnrestrictedAccess === true;
|
|
461
|
+
if (!hasUsers && !hasUnsafe) {
|
|
462
|
+
errors.push(`Project "${project.slug}": no access policy configured. ` +
|
|
463
|
+
"Set access.allowedUserIds or access.dangerouslyAllowUnrestrictedAccess.");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (errors.length > 0) {
|
|
467
|
+
throw new ConfigLoadError(`Configuration error: invalid access policy\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
292
470
|
function loadEnvFiles(configDir, projectCwds) {
|
|
293
471
|
const loadedFiles = [];
|
|
294
472
|
const vars = {};
|
|
@@ -323,9 +501,8 @@ function substituteEnvVars(obj, env, path = "") {
|
|
|
323
501
|
return obj.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
324
502
|
const value = env[varName] ?? process.env[varName];
|
|
325
503
|
if (value === undefined) {
|
|
326
|
-
|
|
504
|
+
throw new ConfigLoadError(`Configuration error: environment variable "${varName}" is not defined\n` +
|
|
327
505
|
` (referenced in field: ${path || "<root>"})`);
|
|
328
|
-
process.exit(1);
|
|
329
506
|
}
|
|
330
507
|
return value;
|
|
331
508
|
});
|
|
@@ -369,32 +546,76 @@ function deepMerge(base, override) {
|
|
|
369
546
|
}
|
|
370
547
|
return result;
|
|
371
548
|
}
|
|
372
|
-
|
|
549
|
+
const CONFIG_EXTENSIONS = [
|
|
550
|
+
{ ext: ".json", format: "json" },
|
|
551
|
+
{ ext: ".jsonc", format: "jsonc" },
|
|
552
|
+
{ ext: ".yaml", format: "yaml" },
|
|
553
|
+
{ ext: ".yml", format: "yaml" },
|
|
554
|
+
];
|
|
555
|
+
/**
|
|
556
|
+
* Scan configDir for a config file matching the given slot basename
|
|
557
|
+
* (e.g. "hal.config" or "hal.config.local") in any supported format.
|
|
558
|
+
* Returns null when no file is found.
|
|
559
|
+
* Throws ConfigLoadError when multiple formats exist for the same slot.
|
|
560
|
+
*/
|
|
561
|
+
export function resolveConfigFile(configDir, slotBasename) {
|
|
562
|
+
const found = [];
|
|
563
|
+
for (const { ext, format } of CONFIG_EXTENSIONS) {
|
|
564
|
+
const filePath = join(configDir, `${slotBasename}${ext}`);
|
|
565
|
+
if (existsSync(filePath)) {
|
|
566
|
+
found.push({ path: filePath, format });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (found.length === 0)
|
|
570
|
+
return null;
|
|
571
|
+
if (found.length === 1)
|
|
572
|
+
return found[0];
|
|
573
|
+
const names = found.map((f) => basename(f.path)).join(", ");
|
|
574
|
+
throw new ConfigLoadError(`Configuration error: multiple config files found for "${slotBasename}": ${names}\n` +
|
|
575
|
+
" Only one format per config file is allowed. Remove the extras.");
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Parse raw config file content using the appropriate parser for the format.
|
|
579
|
+
*/
|
|
580
|
+
export function parseConfigContent(content, format, filePath) {
|
|
581
|
+
try {
|
|
582
|
+
if (format === "yaml") {
|
|
583
|
+
return parseYaml(content);
|
|
584
|
+
}
|
|
585
|
+
if (format === "jsonc") {
|
|
586
|
+
return JSON.parse(stripJsonComments(content, { trailingCommas: true }));
|
|
587
|
+
}
|
|
588
|
+
return JSON.parse(content);
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
throw new ConfigLoadError(`Configuration error: failed to parse ${basename(filePath)} — ${err instanceof Error ? err.message : String(err)}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
373
594
|
function loadLocalConfig(configDir) {
|
|
374
|
-
const
|
|
375
|
-
if (!
|
|
595
|
+
const resolved = resolveConfigFile(configDir, "hal.config.local");
|
|
596
|
+
if (!resolved)
|
|
376
597
|
return null;
|
|
377
598
|
let raw;
|
|
378
599
|
try {
|
|
379
|
-
const content = readFileSync(
|
|
380
|
-
raw =
|
|
600
|
+
const content = readFileSync(resolved.path, "utf-8");
|
|
601
|
+
raw = parseConfigContent(content, resolved.format, resolved.path);
|
|
381
602
|
}
|
|
382
603
|
catch (err) {
|
|
383
|
-
|
|
384
|
-
|
|
604
|
+
if (err instanceof ConfigLoadError)
|
|
605
|
+
throw err;
|
|
606
|
+
throw new ConfigLoadError(`Configuration error: failed to read ${basename(resolved.path)} — ${err instanceof Error ? err.message : String(err)}`);
|
|
385
607
|
}
|
|
386
608
|
const result = LocalConfigFileSchema.safeParse(raw);
|
|
387
609
|
if (!result.success) {
|
|
388
610
|
const issues = result.error.issues
|
|
389
611
|
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
390
612
|
.join("\n");
|
|
391
|
-
|
|
392
|
-
process.exit(1);
|
|
613
|
+
throw new ConfigLoadError(`Configuration error in ${basename(resolved.path)}:\n${issues}`);
|
|
393
614
|
}
|
|
394
|
-
return result.data
|
|
615
|
+
return result.data ? { config: result.data, path: resolved.path } : null;
|
|
395
616
|
}
|
|
396
617
|
// ─── Phase 3: Merge local into base ──────────────────────────────────────────
|
|
397
|
-
function mergeLocalIntoBase(base, local) {
|
|
618
|
+
function mergeLocalIntoBase(base, local, baseFileName, localFileName) {
|
|
398
619
|
const mergedGlobals = local.globals !== undefined
|
|
399
620
|
? deepMerge(base.globals ?? {}, local.globals)
|
|
400
621
|
: base.globals;
|
|
@@ -417,9 +638,8 @@ function mergeLocalIntoBase(base, local) {
|
|
|
417
638
|
return false;
|
|
418
639
|
});
|
|
419
640
|
if (idx === -1) {
|
|
420
|
-
|
|
421
|
-
` Every entry in
|
|
422
|
-
process.exit(1);
|
|
641
|
+
throw new ConfigLoadError(`Configuration error: local project "${matchKey}" not found in ${baseFileName}.\n` +
|
|
642
|
+
` Every entry in ${localFileName} projects must match a base project by name or cwd.`);
|
|
423
643
|
}
|
|
424
644
|
mergedProjects[idx] = deepMerge(mergedProjects[idx], localProject);
|
|
425
645
|
}
|
|
@@ -429,42 +649,44 @@ function mergeLocalIntoBase(base, local) {
|
|
|
429
649
|
projects: mergedProjects,
|
|
430
650
|
};
|
|
431
651
|
}
|
|
432
|
-
// ─── Phase 4: Config file loading (
|
|
433
|
-
|
|
434
|
-
const configPath = join(configDir, "hal.config.json");
|
|
435
|
-
const localPath = join(configDir, "hal.config.local.json");
|
|
652
|
+
// ─── Phase 4: Config file loading (internal: throws on error) ──────────────────
|
|
653
|
+
function loadMultiConfigInternal(configDir) {
|
|
436
654
|
const loadedFiles = [];
|
|
437
|
-
// 1.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
655
|
+
// 1. Detect and load base config
|
|
656
|
+
const baseResolved = resolveConfigFile(configDir, "hal.config");
|
|
657
|
+
const supportedExts = CONFIG_EXTENSIONS.map((e) => e.ext).join(", ");
|
|
658
|
+
if (!baseResolved) {
|
|
659
|
+
throw new ConfigLoadError(`Configuration error: no config file found in ${configDir}\n` +
|
|
660
|
+
` Looked for: hal.config{${supportedExts}}\n` +
|
|
661
|
+
` Run "npx @marcopeg/hal init" to create one.`);
|
|
442
662
|
}
|
|
663
|
+
const baseFileName = basename(baseResolved.path);
|
|
443
664
|
let rawBase;
|
|
444
665
|
try {
|
|
445
|
-
const content = readFileSync(
|
|
446
|
-
rawBase =
|
|
666
|
+
const content = readFileSync(baseResolved.path, "utf-8");
|
|
667
|
+
rawBase = parseConfigContent(content, baseResolved.format, baseResolved.path);
|
|
447
668
|
}
|
|
448
669
|
catch (err) {
|
|
449
|
-
|
|
450
|
-
|
|
670
|
+
if (err instanceof ConfigLoadError)
|
|
671
|
+
throw err;
|
|
672
|
+
throw new ConfigLoadError(`Configuration error: failed to read ${baseFileName} — ${err instanceof Error ? err.message : String(err)}`);
|
|
451
673
|
}
|
|
452
|
-
loadedFiles.push(
|
|
674
|
+
loadedFiles.push(baseResolved.path);
|
|
453
675
|
// 2. Validate base config schema
|
|
454
676
|
const baseResult = MultiConfigFileSchema.safeParse(rawBase);
|
|
455
677
|
if (!baseResult.success) {
|
|
456
678
|
const issues = baseResult.error.issues
|
|
457
679
|
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
458
680
|
.join("\n");
|
|
459
|
-
|
|
460
|
-
process.exit(1);
|
|
681
|
+
throw new ConfigLoadError(`Configuration error in ${baseFileName}:\n${issues}`);
|
|
461
682
|
}
|
|
462
683
|
let merged = baseResult.data;
|
|
463
684
|
// 3. Load and merge local config
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
685
|
+
const localResult = loadLocalConfig(configDir);
|
|
686
|
+
if (localResult !== null) {
|
|
687
|
+
const localFileName = basename(localResult.path);
|
|
688
|
+
loadedFiles.push(localResult.path);
|
|
689
|
+
merged = mergeLocalIntoBase(merged, localResult.config, baseFileName, localFileName);
|
|
468
690
|
}
|
|
469
691
|
// 4. Load .env files (using raw cwds from merged config for path resolution)
|
|
470
692
|
const rawCwds = merged.projects.map((p) => isAbsolute(p.cwd) ? p.cwd : resolve(configDir, p.cwd));
|
|
@@ -477,12 +699,35 @@ export function loadMultiConfig(configDir) {
|
|
|
477
699
|
const issues = finalResult.error.issues
|
|
478
700
|
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
479
701
|
.join("\n");
|
|
480
|
-
|
|
481
|
-
process.exit(1);
|
|
702
|
+
throw new ConfigLoadError(`Configuration error after environment variable substitution:\n${issues}`);
|
|
482
703
|
}
|
|
704
|
+
// 7. Normalize allowedUserIds (string | number)[] → number[] with validation
|
|
705
|
+
normalizeAllowedUserIdsInConfig(finalResult.data);
|
|
483
706
|
return {
|
|
484
707
|
config: finalResult.data,
|
|
485
708
|
loadedFiles: [...loadedFiles, ...envSources.loadedFiles],
|
|
486
709
|
};
|
|
487
710
|
}
|
|
711
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
712
|
+
/**
|
|
713
|
+
* Load multi-project config. On any error, logs and exits the process.
|
|
714
|
+
* Use for initial startup.
|
|
715
|
+
*/
|
|
716
|
+
export function loadMultiConfig(configDir) {
|
|
717
|
+
try {
|
|
718
|
+
return loadMultiConfigInternal(configDir);
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
722
|
+
console.error(message);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Load multi-project config without exiting. Throws on error.
|
|
728
|
+
* Use for hot-reload so callers can log and retry on next file change.
|
|
729
|
+
*/
|
|
730
|
+
export function tryLoadMultiConfig(configDir) {
|
|
731
|
+
return loadMultiConfigInternal(configDir);
|
|
732
|
+
}
|
|
488
733
|
//# sourceMappingURL=config.js.map
|