openvole 0.1.0
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/LICENSE +21 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3250 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +676 -0
- package/dist/index.js +2242 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config/index.ts
|
|
13
|
+
var config_exports = {};
|
|
14
|
+
__export(config_exports, {
|
|
15
|
+
addPawToConfig: () => addPawToConfig,
|
|
16
|
+
addPawToLock: () => addPawToLock,
|
|
17
|
+
addSkillToConfig: () => addSkillToConfig,
|
|
18
|
+
addSkillToLock: () => addSkillToLock,
|
|
19
|
+
defineConfig: () => defineConfig,
|
|
20
|
+
loadConfig: () => loadConfig,
|
|
21
|
+
normalizePawConfig: () => normalizePawConfig,
|
|
22
|
+
readConfigFile: () => readConfigFile,
|
|
23
|
+
readLockFile: () => readLockFile,
|
|
24
|
+
removePawFromConfig: () => removePawFromConfig,
|
|
25
|
+
removePawFromLock: () => removePawFromLock,
|
|
26
|
+
removeSkillFromConfig: () => removeSkillFromConfig,
|
|
27
|
+
removeSkillFromLock: () => removeSkillFromLock,
|
|
28
|
+
writeConfigFile: () => writeConfigFile,
|
|
29
|
+
writeLockFile: () => writeLockFile
|
|
30
|
+
});
|
|
31
|
+
import * as fs from "fs/promises";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
function normalizePawConfig(entry) {
|
|
34
|
+
if (typeof entry === "string") {
|
|
35
|
+
return { name: entry };
|
|
36
|
+
}
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
function defineConfig(config) {
|
|
40
|
+
return {
|
|
41
|
+
brain: config.brain,
|
|
42
|
+
paws: config.paws ?? [],
|
|
43
|
+
skills: config.skills ?? [],
|
|
44
|
+
loop: {
|
|
45
|
+
...DEFAULT_LOOP_CONFIG,
|
|
46
|
+
...config.loop
|
|
47
|
+
},
|
|
48
|
+
heartbeat: {
|
|
49
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
50
|
+
...config.heartbeat
|
|
51
|
+
},
|
|
52
|
+
toolProfiles: config.toolProfiles
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function loadConfig(configPath) {
|
|
56
|
+
const userConfig = await loadUserConfig(configPath);
|
|
57
|
+
const lockPath = path.join(path.dirname(configPath), ".openvole", "vole.lock.json");
|
|
58
|
+
const lock = await loadLockFile(lockPath);
|
|
59
|
+
return mergeConfigWithLock(userConfig, lock);
|
|
60
|
+
}
|
|
61
|
+
async function loadUserConfig(configPath) {
|
|
62
|
+
const dir = path.dirname(configPath);
|
|
63
|
+
const jsonPath = path.join(dir, "vole.config.json");
|
|
64
|
+
try {
|
|
65
|
+
const raw = await fs.readFile(jsonPath, "utf-8");
|
|
66
|
+
const config = JSON.parse(raw);
|
|
67
|
+
return {
|
|
68
|
+
brain: config.brain,
|
|
69
|
+
paws: config.paws ?? [],
|
|
70
|
+
skills: config.skills ?? [],
|
|
71
|
+
loop: {
|
|
72
|
+
...DEFAULT_LOOP_CONFIG,
|
|
73
|
+
...config.loop
|
|
74
|
+
},
|
|
75
|
+
heartbeat: {
|
|
76
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
77
|
+
...config.heartbeat
|
|
78
|
+
},
|
|
79
|
+
toolProfiles: config.toolProfiles
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
const candidates = [configPath];
|
|
84
|
+
if (configPath.endsWith(".ts")) {
|
|
85
|
+
candidates.push(configPath.replace(/\.ts$/, ".mjs"));
|
|
86
|
+
candidates.push(configPath.replace(/\.ts$/, ".js"));
|
|
87
|
+
}
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
try {
|
|
90
|
+
const module = await import(candidate);
|
|
91
|
+
const config = module.default ?? module;
|
|
92
|
+
return {
|
|
93
|
+
brain: config.brain,
|
|
94
|
+
paws: config.paws ?? [],
|
|
95
|
+
skills: config.skills ?? [],
|
|
96
|
+
loop: {
|
|
97
|
+
...DEFAULT_LOOP_CONFIG,
|
|
98
|
+
...config.loop
|
|
99
|
+
},
|
|
100
|
+
heartbeat: {
|
|
101
|
+
...DEFAULT_HEARTBEAT_CONFIG,
|
|
102
|
+
...config.heartbeat
|
|
103
|
+
},
|
|
104
|
+
toolProfiles: config.toolProfiles
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.warn(`[config] No config found (tried: ${jsonPath}, ${candidates.join(", ")}), using defaults`);
|
|
111
|
+
return defineConfig({});
|
|
112
|
+
}
|
|
113
|
+
async function loadLockFile(lockPath) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = await fs.readFile(lockPath, "utf-8");
|
|
116
|
+
return JSON.parse(raw);
|
|
117
|
+
} catch {
|
|
118
|
+
return { paws: [], skills: [] };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function mergeConfigWithLock(userConfig, lock) {
|
|
122
|
+
const userPawNames = new Set(
|
|
123
|
+
userConfig.paws.map((p) => typeof p === "string" ? p : p.name)
|
|
124
|
+
);
|
|
125
|
+
const userSkillNames = new Set(userConfig.skills);
|
|
126
|
+
const mergedPaws = [...userConfig.paws];
|
|
127
|
+
for (const lockPaw of lock.paws) {
|
|
128
|
+
if (!userPawNames.has(lockPaw.name)) {
|
|
129
|
+
mergedPaws.push(
|
|
130
|
+
lockPaw.allow ? { name: lockPaw.name, allow: lockPaw.allow } : lockPaw.name
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const mergedSkills = [...userConfig.skills];
|
|
135
|
+
for (const lockSkill of lock.skills) {
|
|
136
|
+
if (!userSkillNames.has(lockSkill.name)) {
|
|
137
|
+
mergedSkills.push(lockSkill.name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...userConfig,
|
|
142
|
+
paws: mergedPaws,
|
|
143
|
+
skills: mergedSkills
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function readLockFile(projectRoot) {
|
|
147
|
+
const lockPath = path.join(projectRoot, ".openvole", "vole.lock.json");
|
|
148
|
+
try {
|
|
149
|
+
const raw = await fs.readFile(lockPath, "utf-8");
|
|
150
|
+
return JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
return { paws: [], skills: [] };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function writeLockFile(projectRoot, lock) {
|
|
156
|
+
const openvoleDir = path.join(projectRoot, ".openvole");
|
|
157
|
+
await fs.mkdir(openvoleDir, { recursive: true });
|
|
158
|
+
const lockPath = path.join(openvoleDir, "vole.lock.json");
|
|
159
|
+
await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
160
|
+
}
|
|
161
|
+
async function addPawToLock(projectRoot, name, version, allow) {
|
|
162
|
+
const lock = await readLockFile(projectRoot);
|
|
163
|
+
const existing = lock.paws.findIndex((p) => p.name === name);
|
|
164
|
+
const entry = { name, version, allow };
|
|
165
|
+
if (existing >= 0) {
|
|
166
|
+
lock.paws[existing] = entry;
|
|
167
|
+
} else {
|
|
168
|
+
lock.paws.push(entry);
|
|
169
|
+
}
|
|
170
|
+
await writeLockFile(projectRoot, lock);
|
|
171
|
+
}
|
|
172
|
+
async function removePawFromLock(projectRoot, name) {
|
|
173
|
+
const lock = await readLockFile(projectRoot);
|
|
174
|
+
lock.paws = lock.paws.filter((p) => p.name !== name);
|
|
175
|
+
await writeLockFile(projectRoot, lock);
|
|
176
|
+
}
|
|
177
|
+
async function addSkillToLock(projectRoot, name, version) {
|
|
178
|
+
const lock = await readLockFile(projectRoot);
|
|
179
|
+
const existing = lock.skills.findIndex((s) => s.name === name);
|
|
180
|
+
const entry = { name, version };
|
|
181
|
+
if (existing >= 0) {
|
|
182
|
+
lock.skills[existing] = entry;
|
|
183
|
+
} else {
|
|
184
|
+
lock.skills.push(entry);
|
|
185
|
+
}
|
|
186
|
+
await writeLockFile(projectRoot, lock);
|
|
187
|
+
}
|
|
188
|
+
async function removeSkillFromLock(projectRoot, name) {
|
|
189
|
+
const lock = await readLockFile(projectRoot);
|
|
190
|
+
lock.skills = lock.skills.filter((s) => s.name !== name);
|
|
191
|
+
await writeLockFile(projectRoot, lock);
|
|
192
|
+
}
|
|
193
|
+
async function readConfigFile(projectRoot) {
|
|
194
|
+
const configPath = path.join(projectRoot, "vole.config.json");
|
|
195
|
+
try {
|
|
196
|
+
const raw = await fs.readFile(configPath, "utf-8");
|
|
197
|
+
return JSON.parse(raw);
|
|
198
|
+
} catch {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function writeConfigFile(projectRoot, config) {
|
|
203
|
+
const configPath = path.join(projectRoot, "vole.config.json");
|
|
204
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
205
|
+
}
|
|
206
|
+
async function addPawToConfig(projectRoot, name, allow) {
|
|
207
|
+
const config = await readConfigFile(projectRoot);
|
|
208
|
+
const paws = config.paws ?? [];
|
|
209
|
+
const existing = paws.find(
|
|
210
|
+
(p) => typeof p === "string" ? p === name : p.name === name
|
|
211
|
+
);
|
|
212
|
+
if (existing) return;
|
|
213
|
+
paws.push(allow ? { name, allow } : name);
|
|
214
|
+
config.paws = paws;
|
|
215
|
+
await writeConfigFile(projectRoot, config);
|
|
216
|
+
}
|
|
217
|
+
async function removePawFromConfig(projectRoot, name) {
|
|
218
|
+
const config = await readConfigFile(projectRoot);
|
|
219
|
+
const paws = config.paws ?? [];
|
|
220
|
+
config.paws = paws.filter(
|
|
221
|
+
(p) => typeof p === "string" ? p !== name : p.name !== name
|
|
222
|
+
);
|
|
223
|
+
await writeConfigFile(projectRoot, config);
|
|
224
|
+
}
|
|
225
|
+
async function addSkillToConfig(projectRoot, name) {
|
|
226
|
+
const config = await readConfigFile(projectRoot);
|
|
227
|
+
const skills = config.skills ?? [];
|
|
228
|
+
if (skills.includes(name)) return;
|
|
229
|
+
skills.push(name);
|
|
230
|
+
config.skills = skills;
|
|
231
|
+
await writeConfigFile(projectRoot, config);
|
|
232
|
+
}
|
|
233
|
+
async function removeSkillFromConfig(projectRoot, name) {
|
|
234
|
+
const config = await readConfigFile(projectRoot);
|
|
235
|
+
const skills = config.skills ?? [];
|
|
236
|
+
config.skills = skills.filter((s) => s !== name);
|
|
237
|
+
await writeConfigFile(projectRoot, config);
|
|
238
|
+
}
|
|
239
|
+
var DEFAULT_LOOP_CONFIG, DEFAULT_HEARTBEAT_CONFIG;
|
|
240
|
+
var init_config = __esm({
|
|
241
|
+
"src/config/index.ts"() {
|
|
242
|
+
"use strict";
|
|
243
|
+
DEFAULT_LOOP_CONFIG = {
|
|
244
|
+
maxIterations: 10,
|
|
245
|
+
confirmBeforeAct: true,
|
|
246
|
+
taskConcurrency: 1,
|
|
247
|
+
compactThreshold: 50,
|
|
248
|
+
logLevel: "info",
|
|
249
|
+
rateLimits: void 0
|
|
250
|
+
};
|
|
251
|
+
DEFAULT_HEARTBEAT_CONFIG = {
|
|
252
|
+
enabled: false,
|
|
253
|
+
intervalMinutes: 30
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// src/core/bus.ts
|
|
259
|
+
var bus_exports = {};
|
|
260
|
+
__export(bus_exports, {
|
|
261
|
+
createMessageBus: () => createMessageBus
|
|
262
|
+
});
|
|
263
|
+
import mitt from "mitt";
|
|
264
|
+
function createMessageBus() {
|
|
265
|
+
return mitt();
|
|
266
|
+
}
|
|
267
|
+
var init_bus = __esm({
|
|
268
|
+
"src/core/bus.ts"() {
|
|
269
|
+
"use strict";
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/core/logger.ts
|
|
274
|
+
function currentLevel() {
|
|
275
|
+
const env = (process.env.VOLE_LOG_LEVEL ?? "info").toLowerCase();
|
|
276
|
+
return LEVELS[env] ?? LEVELS.info;
|
|
277
|
+
}
|
|
278
|
+
function createLogger(tag) {
|
|
279
|
+
const prefix = `[${tag}]`;
|
|
280
|
+
return {
|
|
281
|
+
error: (msg, ...args) => currentLevel() >= LEVELS.error && console.error(prefix, msg, ...args),
|
|
282
|
+
warn: (msg, ...args) => currentLevel() >= LEVELS.warn && console.warn(prefix, msg, ...args),
|
|
283
|
+
info: (msg, ...args) => currentLevel() >= LEVELS.info && console.info(prefix, msg, ...args),
|
|
284
|
+
debug: (msg, ...args) => currentLevel() >= LEVELS.debug && console.debug(prefix, msg, ...args),
|
|
285
|
+
trace: (msg, ...args) => currentLevel() >= LEVELS.trace && console.debug(prefix, msg, ...args)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
var LEVELS;
|
|
289
|
+
var init_logger = __esm({
|
|
290
|
+
"src/core/logger.ts"() {
|
|
291
|
+
"use strict";
|
|
292
|
+
LEVELS = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 };
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// src/skill/resolver.ts
|
|
297
|
+
import { execFileSync } from "child_process";
|
|
298
|
+
function resolveSkills(skills, toolRegistry) {
|
|
299
|
+
for (const skill of skills) {
|
|
300
|
+
const missing = [];
|
|
301
|
+
for (const toolName of skill.definition.requiredTools) {
|
|
302
|
+
if (!toolRegistry.has(toolName)) {
|
|
303
|
+
missing.push(`tool:${toolName}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const requires = skill.definition.requires;
|
|
307
|
+
if (requires) {
|
|
308
|
+
for (const envVar of requires.env) {
|
|
309
|
+
if (!process.env[envVar]) {
|
|
310
|
+
missing.push(`env:${envVar}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const bin of requires.bins) {
|
|
314
|
+
if (!isBinaryAvailable(bin)) {
|
|
315
|
+
missing.push(`bin:${bin}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (requires.anyBins.length > 0) {
|
|
319
|
+
const hasAny = requires.anyBins.some(isBinaryAvailable);
|
|
320
|
+
if (!hasAny) {
|
|
321
|
+
missing.push(`anyBin:${requires.anyBins.join("|")}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const wasActive = skill.active;
|
|
326
|
+
skill.active = missing.length === 0;
|
|
327
|
+
skill.missingTools = missing;
|
|
328
|
+
if (skill.active && !wasActive) {
|
|
329
|
+
const providers = skill.definition.requiredTools.map((t) => toolRegistry.get(t)?.pawName).filter(Boolean);
|
|
330
|
+
const providerInfo = providers.length > 0 ? ` (tools provided by: ${[...new Set(providers)].join(", ")})` : "";
|
|
331
|
+
logger.info(`Skill "${skill.name}" activated${providerInfo}`);
|
|
332
|
+
} else if (!skill.active && wasActive) {
|
|
333
|
+
logger.warn(
|
|
334
|
+
`Skill "${skill.name}" deactivated (missing: ${missing.join(", ")})`
|
|
335
|
+
);
|
|
336
|
+
} else if (!skill.active) {
|
|
337
|
+
logger.debug(
|
|
338
|
+
`Skill "${skill.name}" inactive (missing: ${missing.join(", ")})`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function buildActiveSkills(skills, toolRegistry) {
|
|
344
|
+
return skills.filter((s) => s.active).map((s) => {
|
|
345
|
+
const satisfiedBy = s.definition.requiredTools.map((t) => toolRegistry.get(t)?.pawName).filter((name) => name != null);
|
|
346
|
+
return {
|
|
347
|
+
name: s.name,
|
|
348
|
+
description: s.definition.description,
|
|
349
|
+
satisfiedBy: [...new Set(satisfiedBy)]
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function isBinaryAvailable(name) {
|
|
354
|
+
try {
|
|
355
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
356
|
+
execFileSync(cmd, [name], { stdio: "ignore" });
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
var logger;
|
|
363
|
+
var init_resolver = __esm({
|
|
364
|
+
"src/skill/resolver.ts"() {
|
|
365
|
+
"use strict";
|
|
366
|
+
init_logger();
|
|
367
|
+
logger = createLogger("skill-resolver");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// src/core/task.ts
|
|
372
|
+
var task_exports = {};
|
|
373
|
+
__export(task_exports, {
|
|
374
|
+
TaskQueue: () => TaskQueue
|
|
375
|
+
});
|
|
376
|
+
import * as crypto from "crypto";
|
|
377
|
+
var logger3, TaskQueue;
|
|
378
|
+
var init_task = __esm({
|
|
379
|
+
"src/core/task.ts"() {
|
|
380
|
+
"use strict";
|
|
381
|
+
init_logger();
|
|
382
|
+
logger3 = createLogger("task-queue");
|
|
383
|
+
TaskQueue = class {
|
|
384
|
+
constructor(bus, concurrency = 1, rateLimiter, rateLimits) {
|
|
385
|
+
this.bus = bus;
|
|
386
|
+
this.concurrency = concurrency;
|
|
387
|
+
this.rateLimiter = rateLimiter;
|
|
388
|
+
this.rateLimits = rateLimits;
|
|
389
|
+
}
|
|
390
|
+
queue = [];
|
|
391
|
+
running = /* @__PURE__ */ new Map();
|
|
392
|
+
completed = [];
|
|
393
|
+
runner;
|
|
394
|
+
draining = false;
|
|
395
|
+
/** Set the task runner function (called by the agent loop) */
|
|
396
|
+
setRunner(runner) {
|
|
397
|
+
this.runner = runner;
|
|
398
|
+
}
|
|
399
|
+
/** Enqueue a new task */
|
|
400
|
+
enqueue(input, source = "user", options) {
|
|
401
|
+
const task = {
|
|
402
|
+
id: crypto.randomUUID(),
|
|
403
|
+
source,
|
|
404
|
+
input,
|
|
405
|
+
status: "queued",
|
|
406
|
+
createdAt: Date.now(),
|
|
407
|
+
sessionId: options?.sessionId,
|
|
408
|
+
metadata: options?.metadata
|
|
409
|
+
};
|
|
410
|
+
if (this.rateLimiter && this.rateLimits?.tasksPerHour) {
|
|
411
|
+
const limit = this.rateLimits.tasksPerHour[source];
|
|
412
|
+
if (limit != null) {
|
|
413
|
+
const bucket = `tasks:per-hour:${source}`;
|
|
414
|
+
if (!this.rateLimiter.tryConsume(bucket, limit, 36e5)) {
|
|
415
|
+
logger3.warn(
|
|
416
|
+
`Rate limit warning: tasksPerHour for source "${source}" exceeded (limit: ${limit}). Task will still be enqueued.`
|
|
417
|
+
);
|
|
418
|
+
this.bus.emit("rate:limited", { bucket, source });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this.queue.push(task);
|
|
423
|
+
logger3.info(`Task ${task.id} queued (source: ${source})`);
|
|
424
|
+
this.bus.emit("task:queued", { taskId: task.id });
|
|
425
|
+
this.drain();
|
|
426
|
+
return task;
|
|
427
|
+
}
|
|
428
|
+
/** Cancel a task by ID */
|
|
429
|
+
cancel(taskId) {
|
|
430
|
+
const queueIdx = this.queue.findIndex((t) => t.id === taskId);
|
|
431
|
+
if (queueIdx !== -1) {
|
|
432
|
+
const task = this.queue.splice(queueIdx, 1)[0];
|
|
433
|
+
task.status = "cancelled";
|
|
434
|
+
task.completedAt = Date.now();
|
|
435
|
+
this.completed.push(task);
|
|
436
|
+
logger3.info(`Task ${taskId} cancelled (was queued)`);
|
|
437
|
+
this.bus.emit("task:cancelled", { taskId });
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
const running = this.running.get(taskId);
|
|
441
|
+
if (running) {
|
|
442
|
+
running.status = "cancelled";
|
|
443
|
+
logger3.info(`Task ${taskId} marked for cancellation (running)`);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
/** Get all tasks (queued + running + completed) */
|
|
449
|
+
list() {
|
|
450
|
+
return [
|
|
451
|
+
...this.queue,
|
|
452
|
+
...Array.from(this.running.values()),
|
|
453
|
+
...this.completed.slice(-50)
|
|
454
|
+
// keep last 50 completed
|
|
455
|
+
];
|
|
456
|
+
}
|
|
457
|
+
/** Get a task by ID */
|
|
458
|
+
get(taskId) {
|
|
459
|
+
return this.queue.find((t) => t.id === taskId) ?? this.running.get(taskId) ?? this.completed.find((t) => t.id === taskId);
|
|
460
|
+
}
|
|
461
|
+
/** Check if a task has been cancelled */
|
|
462
|
+
isCancelled(taskId) {
|
|
463
|
+
const task = this.running.get(taskId);
|
|
464
|
+
return task?.status === "cancelled";
|
|
465
|
+
}
|
|
466
|
+
async drain() {
|
|
467
|
+
if (this.draining) return;
|
|
468
|
+
this.draining = true;
|
|
469
|
+
try {
|
|
470
|
+
while (this.queue.length > 0 && this.running.size < this.concurrency) {
|
|
471
|
+
if (!this.runner) {
|
|
472
|
+
logger3.error("No task runner configured");
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
const task = this.queue.shift();
|
|
476
|
+
task.status = "running";
|
|
477
|
+
task.startedAt = Date.now();
|
|
478
|
+
this.running.set(task.id, task);
|
|
479
|
+
logger3.info(`Task ${task.id} started`);
|
|
480
|
+
this.bus.emit("task:started", { taskId: task.id });
|
|
481
|
+
this.runTask(task);
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
this.draining = false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async runTask(task) {
|
|
488
|
+
try {
|
|
489
|
+
await this.runner(task);
|
|
490
|
+
if (task.status !== "cancelled") {
|
|
491
|
+
task.status = "completed";
|
|
492
|
+
}
|
|
493
|
+
task.completedAt = Date.now();
|
|
494
|
+
logger3.info(`Task ${task.id} ${task.status}`);
|
|
495
|
+
this.bus.emit("task:completed", { taskId: task.id, result: task.result });
|
|
496
|
+
} catch (err) {
|
|
497
|
+
task.status = "failed";
|
|
498
|
+
task.completedAt = Date.now();
|
|
499
|
+
task.error = err instanceof Error ? err.message : String(err);
|
|
500
|
+
logger3.error(`Task ${task.id} failed: ${task.error}`);
|
|
501
|
+
this.bus.emit("task:failed", { taskId: task.id, error: err });
|
|
502
|
+
} finally {
|
|
503
|
+
this.running.delete(task.id);
|
|
504
|
+
this.completed.push(task);
|
|
505
|
+
this.drain();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// src/core/scheduler.ts
|
|
513
|
+
var scheduler_exports = {};
|
|
514
|
+
__export(scheduler_exports, {
|
|
515
|
+
SchedulerStore: () => SchedulerStore
|
|
516
|
+
});
|
|
517
|
+
var logger4, SchedulerStore;
|
|
518
|
+
var init_scheduler = __esm({
|
|
519
|
+
"src/core/scheduler.ts"() {
|
|
520
|
+
"use strict";
|
|
521
|
+
init_logger();
|
|
522
|
+
logger4 = createLogger("scheduler");
|
|
523
|
+
SchedulerStore = class {
|
|
524
|
+
schedules = /* @__PURE__ */ new Map();
|
|
525
|
+
/** Create or replace a recurring schedule */
|
|
526
|
+
add(id, input, intervalMinutes, onTick) {
|
|
527
|
+
if (this.schedules.has(id)) {
|
|
528
|
+
this.cancel(id);
|
|
529
|
+
}
|
|
530
|
+
const intervalMs = intervalMinutes * 6e4;
|
|
531
|
+
const timer = setInterval(onTick, intervalMs);
|
|
532
|
+
this.schedules.set(id, {
|
|
533
|
+
id,
|
|
534
|
+
input,
|
|
535
|
+
intervalMinutes,
|
|
536
|
+
timer,
|
|
537
|
+
createdAt: Date.now()
|
|
538
|
+
});
|
|
539
|
+
logger4.info(`Schedule "${id}" created \u2014 every ${intervalMinutes}m: "${input.substring(0, 80)}"`);
|
|
540
|
+
}
|
|
541
|
+
/** Cancel a schedule by ID */
|
|
542
|
+
cancel(id) {
|
|
543
|
+
const entry = this.schedules.get(id);
|
|
544
|
+
if (!entry) return false;
|
|
545
|
+
clearInterval(entry.timer);
|
|
546
|
+
this.schedules.delete(id);
|
|
547
|
+
logger4.info(`Schedule "${id}" cancelled`);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
/** List all active schedules */
|
|
551
|
+
list() {
|
|
552
|
+
return Array.from(this.schedules.values()).map(({ id, input, intervalMinutes, createdAt }) => ({
|
|
553
|
+
id,
|
|
554
|
+
input,
|
|
555
|
+
intervalMinutes,
|
|
556
|
+
createdAt
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
/** Clear all schedules (for shutdown) */
|
|
560
|
+
clearAll() {
|
|
561
|
+
for (const entry of this.schedules.values()) {
|
|
562
|
+
clearInterval(entry.timer);
|
|
563
|
+
}
|
|
564
|
+
this.schedules.clear();
|
|
565
|
+
logger4.info("All schedules cleared");
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// src/tool/registry.ts
|
|
572
|
+
var registry_exports = {};
|
|
573
|
+
__export(registry_exports, {
|
|
574
|
+
ToolRegistry: () => ToolRegistry
|
|
575
|
+
});
|
|
576
|
+
var logger6, ToolRegistry;
|
|
577
|
+
var init_registry = __esm({
|
|
578
|
+
"src/tool/registry.ts"() {
|
|
579
|
+
"use strict";
|
|
580
|
+
init_logger();
|
|
581
|
+
logger6 = createLogger("tool-registry");
|
|
582
|
+
ToolRegistry = class {
|
|
583
|
+
constructor(bus) {
|
|
584
|
+
this.bus = bus;
|
|
585
|
+
}
|
|
586
|
+
tools = /* @__PURE__ */ new Map();
|
|
587
|
+
/** Register tools from a Paw. Skips tools with conflicting names. */
|
|
588
|
+
register(pawName, tools, inProcess) {
|
|
589
|
+
for (const tool of tools) {
|
|
590
|
+
if (this.tools.has(tool.name)) {
|
|
591
|
+
const existing = this.tools.get(tool.name);
|
|
592
|
+
logger6.warn(
|
|
593
|
+
`Tool name conflict: "${tool.name}" already registered by "${existing.pawName}", ignoring registration from "${pawName}"`
|
|
594
|
+
);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
this.tools.set(tool.name, {
|
|
598
|
+
name: tool.name,
|
|
599
|
+
description: tool.description,
|
|
600
|
+
parameters: tool.parameters,
|
|
601
|
+
pawName,
|
|
602
|
+
inProcess,
|
|
603
|
+
execute: tool.execute
|
|
604
|
+
});
|
|
605
|
+
logger6.info(`Registered tool "${tool.name}" from "${pawName}"`);
|
|
606
|
+
this.bus.emit("tool:registered", { toolName: tool.name, pawName });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/** Remove all tools owned by a specific Paw */
|
|
610
|
+
unregister(pawName) {
|
|
611
|
+
const toRemove = [];
|
|
612
|
+
for (const [name, entry] of this.tools) {
|
|
613
|
+
if (entry.pawName === pawName) {
|
|
614
|
+
toRemove.push(name);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const name of toRemove) {
|
|
618
|
+
this.tools.delete(name);
|
|
619
|
+
logger6.info(`Unregistered tool "${name}" from "${pawName}"`);
|
|
620
|
+
this.bus.emit("tool:unregistered", { toolName: name, pawName });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** Get a tool entry by name */
|
|
624
|
+
get(toolName) {
|
|
625
|
+
return this.tools.get(toolName);
|
|
626
|
+
}
|
|
627
|
+
/** List all registered tools */
|
|
628
|
+
list() {
|
|
629
|
+
return Array.from(this.tools.values());
|
|
630
|
+
}
|
|
631
|
+
/** Check if a tool exists */
|
|
632
|
+
has(toolName) {
|
|
633
|
+
return this.tools.has(toolName);
|
|
634
|
+
}
|
|
635
|
+
/** Get tool summaries for AgentContext */
|
|
636
|
+
summaries() {
|
|
637
|
+
return this.list().map((t) => ({
|
|
638
|
+
name: t.name,
|
|
639
|
+
description: t.description,
|
|
640
|
+
pawName: t.pawName
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
/** Get all tool names owned by a specific Paw */
|
|
644
|
+
toolsForPaw(pawName) {
|
|
645
|
+
return this.list().filter((t) => t.pawName === pawName).map((t) => t.name);
|
|
646
|
+
}
|
|
647
|
+
/** Clear all tools (for shutdown) */
|
|
648
|
+
clear() {
|
|
649
|
+
this.tools.clear();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// src/paw/manifest.ts
|
|
656
|
+
var manifest_exports = {};
|
|
657
|
+
__export(manifest_exports, {
|
|
658
|
+
readPawManifest: () => readPawManifest,
|
|
659
|
+
resolvePawPath: () => resolvePawPath
|
|
660
|
+
});
|
|
661
|
+
import * as fs2 from "fs/promises";
|
|
662
|
+
import { accessSync } from "fs";
|
|
663
|
+
import * as path2 from "path";
|
|
664
|
+
import { z } from "zod";
|
|
665
|
+
function resolvePawPath(name, projectRoot) {
|
|
666
|
+
if (name.startsWith(".") || name.startsWith("/")) {
|
|
667
|
+
return path2.resolve(projectRoot, name);
|
|
668
|
+
}
|
|
669
|
+
const openvoleDir = path2.resolve(projectRoot, ".openvole", "paws", name);
|
|
670
|
+
try {
|
|
671
|
+
accessSync(path2.join(openvoleDir, "vole-paw.json"));
|
|
672
|
+
return openvoleDir;
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
return path2.resolve(projectRoot, "node_modules", name);
|
|
676
|
+
}
|
|
677
|
+
async function readPawManifest(pawPath) {
|
|
678
|
+
const manifestPath = path2.join(pawPath, "vole-paw.json");
|
|
679
|
+
try {
|
|
680
|
+
const raw = await fs2.readFile(manifestPath, "utf-8");
|
|
681
|
+
const parsed = JSON.parse(raw);
|
|
682
|
+
const result = pawManifestSchema.safeParse(parsed);
|
|
683
|
+
if (!result.success) {
|
|
684
|
+
logger7.error(
|
|
685
|
+
"Invalid manifest at %s: %s",
|
|
686
|
+
manifestPath,
|
|
687
|
+
result.error.message
|
|
688
|
+
);
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return result.data;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err.code === "ENOENT") {
|
|
694
|
+
logger7.error("Manifest not found: %s", manifestPath);
|
|
695
|
+
} else {
|
|
696
|
+
logger7.error("Failed to read manifest %s: %s", manifestPath, err);
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
var logger7, pawManifestSchema;
|
|
702
|
+
var init_manifest = __esm({
|
|
703
|
+
"src/paw/manifest.ts"() {
|
|
704
|
+
"use strict";
|
|
705
|
+
init_logger();
|
|
706
|
+
logger7 = createLogger("paw-manifest");
|
|
707
|
+
pawManifestSchema = z.object({
|
|
708
|
+
name: z.string().min(1),
|
|
709
|
+
version: z.string().min(1),
|
|
710
|
+
description: z.string(),
|
|
711
|
+
entry: z.string().min(1),
|
|
712
|
+
brain: z.boolean().default(false),
|
|
713
|
+
inProcess: z.boolean().optional().default(false),
|
|
714
|
+
transport: z.enum(["ipc", "stdio"]).optional().default("ipc"),
|
|
715
|
+
tools: z.array(
|
|
716
|
+
z.object({
|
|
717
|
+
name: z.string().min(1),
|
|
718
|
+
description: z.string()
|
|
719
|
+
})
|
|
720
|
+
).default([]),
|
|
721
|
+
permissions: z.object({
|
|
722
|
+
network: z.array(z.string()).optional().default([]),
|
|
723
|
+
listen: z.array(z.number().int().positive()).optional().default([]),
|
|
724
|
+
filesystem: z.array(z.string()).optional().default([]),
|
|
725
|
+
env: z.array(z.string()).optional().default([])
|
|
726
|
+
}).optional().default({})
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// src/skill/loader.ts
|
|
732
|
+
var loader_exports = {};
|
|
733
|
+
__export(loader_exports, {
|
|
734
|
+
loadSkillFromDirectory: () => loadSkillFromDirectory
|
|
735
|
+
});
|
|
736
|
+
import * as fs3 from "fs/promises";
|
|
737
|
+
import * as path4 from "path";
|
|
738
|
+
import { parse as parseYaml } from "yaml";
|
|
739
|
+
async function loadSkillFromDirectory(skillDir) {
|
|
740
|
+
const skillMdPath = path4.join(skillDir, "SKILL.md");
|
|
741
|
+
try {
|
|
742
|
+
const raw = await fs3.readFile(skillMdPath, "utf-8");
|
|
743
|
+
return parseSkillMd(raw, skillMdPath);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (err.code === "ENOENT") {
|
|
746
|
+
logger12.error("SKILL.md not found: %s", skillMdPath);
|
|
747
|
+
} else {
|
|
748
|
+
logger12.error("Failed to read SKILL.md at %s: %s", skillMdPath, err);
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function parseSkillMd(content, filePath) {
|
|
754
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
755
|
+
if (!frontmatter) {
|
|
756
|
+
logger12.error("No YAML frontmatter found in %s", filePath);
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
let meta;
|
|
760
|
+
try {
|
|
761
|
+
meta = parseYaml(frontmatter);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
logger12.error("Invalid YAML frontmatter in %s: %s", filePath, err);
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
if (!meta.name || typeof meta.name !== "string") {
|
|
767
|
+
logger12.error('SKILL.md missing "name" in frontmatter: %s', filePath);
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
if (!meta.description || typeof meta.description !== "string") {
|
|
771
|
+
logger12.error('SKILL.md missing "description" in frontmatter: %s', filePath);
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const instructions = body.trim();
|
|
775
|
+
if (!instructions) {
|
|
776
|
+
logger12.error("SKILL.md has no instructions (empty body): %s", filePath);
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const openclaw = extractOpenClawMetadata(meta);
|
|
780
|
+
return {
|
|
781
|
+
name: meta.name,
|
|
782
|
+
description: meta.description,
|
|
783
|
+
version: typeof meta.version === "string" ? meta.version : typeof meta.version === "number" ? String(meta.version) : void 0,
|
|
784
|
+
requiredTools: toStringArray(meta.requiredTools),
|
|
785
|
+
optionalTools: toStringArray(meta.optionalTools),
|
|
786
|
+
instructions,
|
|
787
|
+
tags: toStringArray(meta.tags),
|
|
788
|
+
// OpenClaw compatibility fields
|
|
789
|
+
requires: openclaw.requires
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function extractOpenClawMetadata(meta) {
|
|
793
|
+
const metadata = meta.metadata;
|
|
794
|
+
if (!metadata) return { requires: void 0 };
|
|
795
|
+
const oc = metadata.openclaw ?? metadata.clawdbot;
|
|
796
|
+
if (!oc) return { requires: void 0 };
|
|
797
|
+
const req = oc.requires;
|
|
798
|
+
if (!req) return { requires: void 0 };
|
|
799
|
+
return {
|
|
800
|
+
requires: {
|
|
801
|
+
env: toStringArray(req.env),
|
|
802
|
+
bins: toStringArray(req.bins),
|
|
803
|
+
anyBins: toStringArray(req.anyBins)
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function extractFrontmatter(content) {
|
|
808
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
809
|
+
if (!match) {
|
|
810
|
+
return { frontmatter: null, body: content };
|
|
811
|
+
}
|
|
812
|
+
return { frontmatter: match[1], body: match[2] };
|
|
813
|
+
}
|
|
814
|
+
function toStringArray(value) {
|
|
815
|
+
if (!value) return [];
|
|
816
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
var logger12;
|
|
820
|
+
var init_loader = __esm({
|
|
821
|
+
"src/skill/loader.ts"() {
|
|
822
|
+
"use strict";
|
|
823
|
+
init_logger();
|
|
824
|
+
logger12 = createLogger("skill-loader");
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// src/skill/registry.ts
|
|
829
|
+
var registry_exports2 = {};
|
|
830
|
+
__export(registry_exports2, {
|
|
831
|
+
SkillRegistry: () => SkillRegistry
|
|
832
|
+
});
|
|
833
|
+
import * as fs4 from "fs/promises";
|
|
834
|
+
import * as path5 from "path";
|
|
835
|
+
async function resolveSkillPath(name, projectRoot) {
|
|
836
|
+
if (name.startsWith(".") || name.startsWith("/")) {
|
|
837
|
+
return path5.resolve(projectRoot, name);
|
|
838
|
+
}
|
|
839
|
+
if (name.startsWith("clawhub/")) {
|
|
840
|
+
return path5.resolve(projectRoot, ".openvole", "skills", name);
|
|
841
|
+
}
|
|
842
|
+
const localPath = path5.resolve(projectRoot, ".openvole", "skills", name);
|
|
843
|
+
if (await exists(path5.join(localPath, "SKILL.md"))) return localPath;
|
|
844
|
+
const clawHubPath = path5.resolve(projectRoot, ".openvole", "skills", "clawhub", name);
|
|
845
|
+
if (await exists(path5.join(clawHubPath, "SKILL.md"))) return clawHubPath;
|
|
846
|
+
return path5.resolve(projectRoot, "node_modules", name);
|
|
847
|
+
}
|
|
848
|
+
async function exists(filePath) {
|
|
849
|
+
try {
|
|
850
|
+
await fs4.access(filePath);
|
|
851
|
+
return true;
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
var logger13, SkillRegistry;
|
|
857
|
+
var init_registry2 = __esm({
|
|
858
|
+
"src/skill/registry.ts"() {
|
|
859
|
+
"use strict";
|
|
860
|
+
init_loader();
|
|
861
|
+
init_resolver();
|
|
862
|
+
init_logger();
|
|
863
|
+
logger13 = createLogger("skill-registry");
|
|
864
|
+
SkillRegistry = class {
|
|
865
|
+
constructor(bus, toolRegistry, projectRoot) {
|
|
866
|
+
this.bus = bus;
|
|
867
|
+
this.toolRegistry = toolRegistry;
|
|
868
|
+
this.projectRoot = projectRoot;
|
|
869
|
+
this.bus.on("tool:registered", () => this.resolve());
|
|
870
|
+
this.bus.on("tool:unregistered", () => this.resolve());
|
|
871
|
+
}
|
|
872
|
+
skills = /* @__PURE__ */ new Map();
|
|
873
|
+
/** Load a Skill from a directory containing SKILL.md */
|
|
874
|
+
async load(nameOrPath) {
|
|
875
|
+
const skillDir = await resolveSkillPath(nameOrPath, this.projectRoot);
|
|
876
|
+
const definition = await loadSkillFromDirectory(skillDir);
|
|
877
|
+
if (!definition) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
const registryKey = nameOrPath.startsWith(".") || nameOrPath.startsWith("/") ? definition.name : nameOrPath;
|
|
881
|
+
if (this.skills.has(registryKey)) {
|
|
882
|
+
logger13.warn(`Skill "${registryKey}" is already loaded`);
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
const instance = {
|
|
886
|
+
name: registryKey,
|
|
887
|
+
definition,
|
|
888
|
+
path: skillDir,
|
|
889
|
+
active: false,
|
|
890
|
+
missingTools: [...definition.requiredTools]
|
|
891
|
+
};
|
|
892
|
+
this.skills.set(registryKey, instance);
|
|
893
|
+
logger13.info(`Skill "${registryKey}" loaded from ${skillDir}`);
|
|
894
|
+
this.resolve();
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
/** Unload a Skill */
|
|
898
|
+
unload(name) {
|
|
899
|
+
if (!this.skills.has(name)) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
this.skills.delete(name);
|
|
903
|
+
logger13.info(`Skill "${name}" unloaded`);
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
/** Re-run the resolver against the current tool registry */
|
|
907
|
+
resolve() {
|
|
908
|
+
resolveSkills(Array.from(this.skills.values()), this.toolRegistry);
|
|
909
|
+
}
|
|
910
|
+
/** Get all Skill instances */
|
|
911
|
+
list() {
|
|
912
|
+
return Array.from(this.skills.values());
|
|
913
|
+
}
|
|
914
|
+
/** Get active Skills only */
|
|
915
|
+
active() {
|
|
916
|
+
return this.list().filter((s) => s.active);
|
|
917
|
+
}
|
|
918
|
+
/** Get a Skill by name */
|
|
919
|
+
get(name) {
|
|
920
|
+
return this.skills.get(name);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// src/tool/core-tools.ts
|
|
927
|
+
var core_tools_exports = {};
|
|
928
|
+
__export(core_tools_exports, {
|
|
929
|
+
createCoreTools: () => createCoreTools
|
|
930
|
+
});
|
|
931
|
+
import * as fs5 from "fs/promises";
|
|
932
|
+
import * as path6 from "path";
|
|
933
|
+
import { z as z2 } from "zod";
|
|
934
|
+
function createCoreTools(scheduler, taskQueue, projectRoot, skillRegistry) {
|
|
935
|
+
const heartbeatPath = path6.resolve(projectRoot, "HEARTBEAT.md");
|
|
936
|
+
return [
|
|
937
|
+
// === Scheduling tools ===
|
|
938
|
+
{
|
|
939
|
+
name: "schedule_task",
|
|
940
|
+
description: "Create a recurring scheduled task that runs at a fixed interval",
|
|
941
|
+
parameters: z2.object({
|
|
942
|
+
id: z2.string().describe("Unique schedule ID (for cancellation)"),
|
|
943
|
+
input: z2.string().describe("The task input to enqueue each time"),
|
|
944
|
+
intervalMinutes: z2.number().describe("How often to run (in minutes)")
|
|
945
|
+
}),
|
|
946
|
+
async execute(params) {
|
|
947
|
+
const { id, input, intervalMinutes } = params;
|
|
948
|
+
scheduler.add(id, input, intervalMinutes, () => {
|
|
949
|
+
taskQueue.enqueue(input, "schedule");
|
|
950
|
+
});
|
|
951
|
+
return { ok: true, id, intervalMinutes };
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
name: "cancel_schedule",
|
|
956
|
+
description: "Cancel a previously created scheduled task",
|
|
957
|
+
parameters: z2.object({
|
|
958
|
+
id: z2.string().describe("Schedule ID to cancel")
|
|
959
|
+
}),
|
|
960
|
+
async execute(params) {
|
|
961
|
+
const { id } = params;
|
|
962
|
+
const cancelled = scheduler.cancel(id);
|
|
963
|
+
return { ok: cancelled, id };
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "list_schedules",
|
|
968
|
+
description: "List all active scheduled tasks",
|
|
969
|
+
parameters: z2.object({}),
|
|
970
|
+
async execute() {
|
|
971
|
+
return scheduler.list();
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
// === Heartbeat tools ===
|
|
975
|
+
{
|
|
976
|
+
name: "heartbeat_read",
|
|
977
|
+
description: "Read the HEARTBEAT.md file containing recurring job definitions",
|
|
978
|
+
parameters: z2.object({}),
|
|
979
|
+
async execute() {
|
|
980
|
+
try {
|
|
981
|
+
const content = await fs5.readFile(heartbeatPath, "utf-8");
|
|
982
|
+
return { ok: true, content };
|
|
983
|
+
} catch {
|
|
984
|
+
return { ok: true, content: "" };
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
name: "heartbeat_write",
|
|
990
|
+
description: "Update the HEARTBEAT.md file with new recurring job definitions",
|
|
991
|
+
parameters: z2.object({
|
|
992
|
+
content: z2.string().describe("The full content to write to HEARTBEAT.md")
|
|
993
|
+
}),
|
|
994
|
+
async execute(params) {
|
|
995
|
+
const { content } = params;
|
|
996
|
+
await fs5.writeFile(heartbeatPath, content, "utf-8");
|
|
997
|
+
return { ok: true };
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
// === Skill tools (on-demand loading) ===
|
|
1001
|
+
{
|
|
1002
|
+
name: "skill_read",
|
|
1003
|
+
description: "Read the full SKILL.md instructions for a skill by name. Use this when a skill is relevant to the current task.",
|
|
1004
|
+
parameters: z2.object({
|
|
1005
|
+
name: z2.string().describe("Skill name to read")
|
|
1006
|
+
}),
|
|
1007
|
+
async execute(params) {
|
|
1008
|
+
const { name } = params;
|
|
1009
|
+
const skill = skillRegistry.get(name);
|
|
1010
|
+
if (!skill) {
|
|
1011
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const content = await fs5.readFile(
|
|
1015
|
+
path6.join(skill.path, "SKILL.md"),
|
|
1016
|
+
"utf-8"
|
|
1017
|
+
);
|
|
1018
|
+
return { ok: true, name, content };
|
|
1019
|
+
} catch {
|
|
1020
|
+
return { ok: false, error: `Failed to read SKILL.md for "${name}"` };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "skill_read_reference",
|
|
1026
|
+
description: "Read a reference file from a skill's references/ directory. Use this for API docs, schemas, or detailed guides.",
|
|
1027
|
+
parameters: z2.object({
|
|
1028
|
+
name: z2.string().describe("Skill name"),
|
|
1029
|
+
file: z2.string().describe("File path relative to the skill's references/ directory")
|
|
1030
|
+
}),
|
|
1031
|
+
async execute(params) {
|
|
1032
|
+
const { name, file } = params;
|
|
1033
|
+
const skill = skillRegistry.get(name);
|
|
1034
|
+
if (!skill) {
|
|
1035
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
1036
|
+
}
|
|
1037
|
+
const resolved = path6.resolve(skill.path, "references", file);
|
|
1038
|
+
if (!resolved.startsWith(path6.resolve(skill.path, "references"))) {
|
|
1039
|
+
return { ok: false, error: "Invalid file path" };
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const content = await fs5.readFile(resolved, "utf-8");
|
|
1043
|
+
return { ok: true, name, file, content };
|
|
1044
|
+
} catch {
|
|
1045
|
+
return { ok: false, error: `File not found: references/${file}` };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
name: "skill_list_files",
|
|
1051
|
+
description: "List all files in a skill's directory including scripts, references, and assets.",
|
|
1052
|
+
parameters: z2.object({
|
|
1053
|
+
name: z2.string().describe("Skill name")
|
|
1054
|
+
}),
|
|
1055
|
+
async execute(params) {
|
|
1056
|
+
const { name } = params;
|
|
1057
|
+
const skill = skillRegistry.get(name);
|
|
1058
|
+
if (!skill) {
|
|
1059
|
+
return { ok: false, error: `Skill "${name}" not found` };
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const files = await listFilesRecursive(skill.path);
|
|
1063
|
+
return { ok: true, name, files };
|
|
1064
|
+
} catch {
|
|
1065
|
+
return { ok: false, error: `Failed to list files for "${name}"` };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
];
|
|
1070
|
+
}
|
|
1071
|
+
async function listFilesRecursive(dir, prefix = "") {
|
|
1072
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1073
|
+
const files = [];
|
|
1074
|
+
for (const entry of entries) {
|
|
1075
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1076
|
+
if (entry.isDirectory()) {
|
|
1077
|
+
files.push(...await listFilesRecursive(path6.join(dir, entry.name), rel));
|
|
1078
|
+
} else {
|
|
1079
|
+
files.push(rel);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return files;
|
|
1083
|
+
}
|
|
1084
|
+
var init_core_tools = __esm({
|
|
1085
|
+
"src/tool/core-tools.ts"() {
|
|
1086
|
+
"use strict";
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// src/cli.ts
|
|
1091
|
+
import "dotenv/config";
|
|
1092
|
+
import * as path8 from "path";
|
|
1093
|
+
|
|
1094
|
+
// src/index.ts
|
|
1095
|
+
init_config();
|
|
1096
|
+
init_bus();
|
|
1097
|
+
|
|
1098
|
+
// src/context/types.ts
|
|
1099
|
+
function createAgentContext(taskId, maxIterations) {
|
|
1100
|
+
return {
|
|
1101
|
+
taskId,
|
|
1102
|
+
messages: [],
|
|
1103
|
+
availableTools: [],
|
|
1104
|
+
activeSkills: [],
|
|
1105
|
+
metadata: {},
|
|
1106
|
+
iteration: 0,
|
|
1107
|
+
maxIterations
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/core/errors.ts
|
|
1112
|
+
function createActionError(code, message, opts) {
|
|
1113
|
+
return {
|
|
1114
|
+
code,
|
|
1115
|
+
message,
|
|
1116
|
+
toolName: opts?.toolName,
|
|
1117
|
+
pawName: opts?.pawName,
|
|
1118
|
+
details: opts?.details
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function successResult(toolName, pawName, output, durationMs) {
|
|
1122
|
+
return { toolName, pawName, success: true, output, durationMs };
|
|
1123
|
+
}
|
|
1124
|
+
function failureResult(toolName, pawName, error, durationMs) {
|
|
1125
|
+
return { toolName, pawName, success: false, error, durationMs };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/core/loop.ts
|
|
1129
|
+
init_resolver();
|
|
1130
|
+
|
|
1131
|
+
// src/core/hooks.ts
|
|
1132
|
+
var PHASE_ORDER = [
|
|
1133
|
+
"perceive",
|
|
1134
|
+
"think",
|
|
1135
|
+
"act",
|
|
1136
|
+
"observe"
|
|
1137
|
+
];
|
|
1138
|
+
|
|
1139
|
+
// src/core/loop.ts
|
|
1140
|
+
init_logger();
|
|
1141
|
+
var logger2 = createLogger("loop");
|
|
1142
|
+
var MAX_BRAIN_FAILURES = 3;
|
|
1143
|
+
async function runAgentLoop(task, deps) {
|
|
1144
|
+
const { bus, toolRegistry, pawRegistry, skillRegistry, io, config, toolProfiles, rateLimiter } = deps;
|
|
1145
|
+
const rateLimits = config.rateLimits;
|
|
1146
|
+
let toolExecutionCount = 0;
|
|
1147
|
+
logger2.info(`Agent loop started for task ${task.id}: "${task.input}"`);
|
|
1148
|
+
let context = createAgentContext(task.id, config.maxIterations);
|
|
1149
|
+
context.metadata.taskSource = task.source;
|
|
1150
|
+
context.metadata.sessionId = task.sessionId;
|
|
1151
|
+
if (task.metadata) {
|
|
1152
|
+
Object.assign(context.metadata, task.metadata);
|
|
1153
|
+
}
|
|
1154
|
+
if (task.source === "heartbeat") {
|
|
1155
|
+
context.metadata.heartbeat = true;
|
|
1156
|
+
}
|
|
1157
|
+
context.messages.push({
|
|
1158
|
+
role: "user",
|
|
1159
|
+
content: task.input,
|
|
1160
|
+
timestamp: Date.now()
|
|
1161
|
+
});
|
|
1162
|
+
logger2.debug("Phase: bootstrap");
|
|
1163
|
+
context = await pawRegistry.runBootstrapHooks(context);
|
|
1164
|
+
let consecutiveBrainFailures = 0;
|
|
1165
|
+
for (context.iteration = 0; context.iteration < config.maxIterations; context.iteration++) {
|
|
1166
|
+
if (task.status === "cancelled") {
|
|
1167
|
+
logger2.info(`Task ${task.id} cancelled at iteration ${context.iteration}`);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (config.compactThreshold > 0 && context.messages.length > config.compactThreshold) {
|
|
1171
|
+
logger2.info(
|
|
1172
|
+
`Context has ${context.messages.length} messages (threshold: ${config.compactThreshold}), running compact`
|
|
1173
|
+
);
|
|
1174
|
+
context = await pawRegistry.runCompactHooks(context);
|
|
1175
|
+
}
|
|
1176
|
+
logger2.info(
|
|
1177
|
+
`Loop running \u2014 iteration ${context.iteration + 1}/${config.maxIterations}`
|
|
1178
|
+
);
|
|
1179
|
+
logger2.debug(`Phase: ${PHASE_ORDER[0]}`);
|
|
1180
|
+
const enrichedContext = await runPerceive(context, pawRegistry, toolRegistry, skillRegistry);
|
|
1181
|
+
if (rateLimiter && rateLimits) {
|
|
1182
|
+
if (rateLimits.llmCallsPerMinute != null) {
|
|
1183
|
+
if (!rateLimiter.tryConsume("llm:per-minute", rateLimits.llmCallsPerMinute, 6e4)) {
|
|
1184
|
+
logger2.warn(`Rate limit hit: llmCallsPerMinute (${rateLimits.llmCallsPerMinute})`);
|
|
1185
|
+
bus.emit("rate:limited", { bucket: "llm:per-minute", source: task.source });
|
|
1186
|
+
enrichedContext.messages.push({
|
|
1187
|
+
role: "error",
|
|
1188
|
+
content: `Rate limit exceeded: LLM calls per minute (limit: ${rateLimits.llmCallsPerMinute}). Retrying next iteration.`,
|
|
1189
|
+
timestamp: Date.now()
|
|
1190
|
+
});
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (rateLimits.llmCallsPerHour != null) {
|
|
1195
|
+
if (!rateLimiter.tryConsume("llm:per-hour", rateLimits.llmCallsPerHour, 36e5)) {
|
|
1196
|
+
logger2.warn(`Rate limit hit: llmCallsPerHour (${rateLimits.llmCallsPerHour})`);
|
|
1197
|
+
bus.emit("rate:limited", { bucket: "llm:per-hour", source: task.source });
|
|
1198
|
+
enrichedContext.messages.push({
|
|
1199
|
+
role: "error",
|
|
1200
|
+
content: `Rate limit exceeded: LLM calls per hour (limit: ${rateLimits.llmCallsPerHour}). Retrying next iteration.`,
|
|
1201
|
+
timestamp: Date.now()
|
|
1202
|
+
});
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
logger2.debug(`Phase: ${PHASE_ORDER[1]}`);
|
|
1208
|
+
const plan = await runThink(enrichedContext, pawRegistry);
|
|
1209
|
+
if (!plan) {
|
|
1210
|
+
logger2.debug("Think phase returned null (no Brain Paw)");
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
if (plan === "BRAIN_ERROR") {
|
|
1214
|
+
consecutiveBrainFailures++;
|
|
1215
|
+
if (consecutiveBrainFailures >= MAX_BRAIN_FAILURES) {
|
|
1216
|
+
io.notify(
|
|
1217
|
+
`Brain Paw failed ${MAX_BRAIN_FAILURES} consecutive times. Halting task ${task.id}.`
|
|
1218
|
+
);
|
|
1219
|
+
task.error = `Brain failed ${MAX_BRAIN_FAILURES} consecutive times`;
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
consecutiveBrainFailures = 0;
|
|
1225
|
+
if (plan.done) {
|
|
1226
|
+
if (plan.response) {
|
|
1227
|
+
task.result = plan.response;
|
|
1228
|
+
io.notify(plan.response);
|
|
1229
|
+
enrichedContext.messages.push({
|
|
1230
|
+
role: "brain",
|
|
1231
|
+
content: plan.response,
|
|
1232
|
+
timestamp: Date.now()
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
logger2.info(`Task ${task.id} completed by Brain at iteration ${context.iteration + 1}`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (plan.response) {
|
|
1239
|
+
io.notify(plan.response);
|
|
1240
|
+
enrichedContext.messages.push({
|
|
1241
|
+
role: "brain",
|
|
1242
|
+
content: plan.response,
|
|
1243
|
+
timestamp: Date.now()
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
logger2.debug(`Phase: ${PHASE_ORDER[2]}`);
|
|
1247
|
+
if (plan.actions.length > 0) {
|
|
1248
|
+
if (rateLimiter && rateLimits?.toolExecutionsPerTask != null) {
|
|
1249
|
+
const incoming = plan.actions.length;
|
|
1250
|
+
if (toolExecutionCount + incoming > rateLimits.toolExecutionsPerTask) {
|
|
1251
|
+
logger2.warn(
|
|
1252
|
+
`Rate limit hit: toolExecutionsPerTask (${toolExecutionCount + incoming}/${rateLimits.toolExecutionsPerTask})`
|
|
1253
|
+
);
|
|
1254
|
+
bus.emit("rate:limited", { bucket: "tools:per-task", source: task.source });
|
|
1255
|
+
enrichedContext.messages.push({
|
|
1256
|
+
role: "error",
|
|
1257
|
+
content: `Rate limit exceeded: tool executions per task (limit: ${rateLimits.toolExecutionsPerTask}, used: ${toolExecutionCount}). Stopping task.`,
|
|
1258
|
+
timestamp: Date.now()
|
|
1259
|
+
});
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const profile = toolProfiles?.[task.source];
|
|
1264
|
+
if (profile) {
|
|
1265
|
+
const blocked = [];
|
|
1266
|
+
plan.actions = plan.actions.filter((a) => {
|
|
1267
|
+
if (profile.allow && !profile.allow.includes(a.tool)) {
|
|
1268
|
+
blocked.push(a.tool);
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
if (profile.deny?.includes(a.tool)) {
|
|
1272
|
+
blocked.push(a.tool);
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
return true;
|
|
1276
|
+
});
|
|
1277
|
+
if (blocked.length > 0) {
|
|
1278
|
+
logger2.warn(`Blocked tools for source "${task.source}": ${blocked.join(", ")}`);
|
|
1279
|
+
enrichedContext.messages.push({
|
|
1280
|
+
role: "error",
|
|
1281
|
+
content: `Tools blocked by security profile for "${task.source}" source: ${blocked.join(", ")}`,
|
|
1282
|
+
timestamp: Date.now()
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (plan.actions.length === 0) continue;
|
|
1286
|
+
}
|
|
1287
|
+
for (const action of plan.actions) {
|
|
1288
|
+
logger2.info(`Tool call: ${action.tool}(${JSON.stringify(action.params)})`);
|
|
1289
|
+
}
|
|
1290
|
+
if (config.confirmBeforeAct) {
|
|
1291
|
+
const toolNames = plan.actions.map((a) => a.tool).join(", ");
|
|
1292
|
+
const confirmed = await io.confirm(
|
|
1293
|
+
`Execute tools: ${toolNames}?`
|
|
1294
|
+
);
|
|
1295
|
+
if (!confirmed) {
|
|
1296
|
+
enrichedContext.messages.push({
|
|
1297
|
+
role: "error",
|
|
1298
|
+
content: "User declined to execute planned actions",
|
|
1299
|
+
timestamp: Date.now()
|
|
1300
|
+
});
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const results = await runAct(
|
|
1305
|
+
plan.actions,
|
|
1306
|
+
plan.execution ?? "sequential",
|
|
1307
|
+
enrichedContext,
|
|
1308
|
+
toolRegistry,
|
|
1309
|
+
pawRegistry
|
|
1310
|
+
);
|
|
1311
|
+
toolExecutionCount += results.length;
|
|
1312
|
+
logger2.debug(`Phase: ${PHASE_ORDER[3]}`);
|
|
1313
|
+
for (const result of results) {
|
|
1314
|
+
if (result.success) {
|
|
1315
|
+
enrichedContext.messages.push({
|
|
1316
|
+
role: "tool_result",
|
|
1317
|
+
content: typeof result.output === "string" ? result.output : JSON.stringify(result.output),
|
|
1318
|
+
toolCall: {
|
|
1319
|
+
name: result.toolName,
|
|
1320
|
+
params: null
|
|
1321
|
+
},
|
|
1322
|
+
timestamp: Date.now()
|
|
1323
|
+
});
|
|
1324
|
+
} else {
|
|
1325
|
+
enrichedContext.messages.push({
|
|
1326
|
+
role: "error",
|
|
1327
|
+
content: result.error?.message ?? "Unknown tool error",
|
|
1328
|
+
toolCall: {
|
|
1329
|
+
name: result.toolName,
|
|
1330
|
+
params: null
|
|
1331
|
+
},
|
|
1332
|
+
timestamp: Date.now()
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
pawRegistry.runObserveHooks(result);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
Object.assign(context, enrichedContext);
|
|
1339
|
+
}
|
|
1340
|
+
if (context.iteration >= config.maxIterations) {
|
|
1341
|
+
logger2.warn(
|
|
1342
|
+
`Task ${task.id} reached max iterations (${config.maxIterations})`
|
|
1343
|
+
);
|
|
1344
|
+
io.notify(
|
|
1345
|
+
`Task reached maximum iterations (${config.maxIterations}). Stopping.`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function runPerceive(context, pawRegistry, toolRegistry, skillRegistry) {
|
|
1350
|
+
const enriched = { ...context };
|
|
1351
|
+
enriched.availableTools = toolRegistry.summaries();
|
|
1352
|
+
enriched.activeSkills = buildActiveSkills(
|
|
1353
|
+
skillRegistry.list(),
|
|
1354
|
+
toolRegistry
|
|
1355
|
+
);
|
|
1356
|
+
return pawRegistry.runGlobalPerceiveHooks(enriched);
|
|
1357
|
+
}
|
|
1358
|
+
async function runThink(context, pawRegistry) {
|
|
1359
|
+
try {
|
|
1360
|
+
return await pawRegistry.think(context);
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1363
|
+
logger2.error(`Brain error: ${message}`);
|
|
1364
|
+
context.messages.push({
|
|
1365
|
+
role: "error",
|
|
1366
|
+
content: `Brain error: ${message}`,
|
|
1367
|
+
timestamp: Date.now()
|
|
1368
|
+
});
|
|
1369
|
+
return "BRAIN_ERROR";
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function runAct(actions, execution, context, toolRegistry, pawRegistry) {
|
|
1373
|
+
const pawNames = /* @__PURE__ */ new Set();
|
|
1374
|
+
for (const action of actions) {
|
|
1375
|
+
const tool = toolRegistry.get(action.tool);
|
|
1376
|
+
if (tool) pawNames.add(tool.pawName);
|
|
1377
|
+
}
|
|
1378
|
+
for (const pawName of pawNames) {
|
|
1379
|
+
await pawRegistry.runLazyPerceive(pawName, context);
|
|
1380
|
+
}
|
|
1381
|
+
if (execution === "parallel") {
|
|
1382
|
+
return Promise.all(
|
|
1383
|
+
actions.map(
|
|
1384
|
+
(action) => executeSingleAction(action, toolRegistry, pawRegistry)
|
|
1385
|
+
)
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
const results = [];
|
|
1389
|
+
for (const action of actions) {
|
|
1390
|
+
const result = await executeSingleAction(action, toolRegistry, pawRegistry);
|
|
1391
|
+
results.push(result);
|
|
1392
|
+
}
|
|
1393
|
+
return results;
|
|
1394
|
+
}
|
|
1395
|
+
async function executeSingleAction(action, toolRegistry, pawRegistry) {
|
|
1396
|
+
const startTime = Date.now();
|
|
1397
|
+
const tool = toolRegistry.get(action.tool);
|
|
1398
|
+
if (!tool) {
|
|
1399
|
+
return failureResult(
|
|
1400
|
+
action.tool,
|
|
1401
|
+
"unknown",
|
|
1402
|
+
createActionError("TOOL_NOT_FOUND", `Tool "${action.tool}" not found`, {
|
|
1403
|
+
toolName: action.tool
|
|
1404
|
+
}),
|
|
1405
|
+
Date.now() - startTime
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (!tool.inProcess && !pawRegistry.isHealthy(tool.pawName)) {
|
|
1409
|
+
return failureResult(
|
|
1410
|
+
action.tool,
|
|
1411
|
+
tool.pawName,
|
|
1412
|
+
createActionError("PAW_CRASHED", `Paw "${tool.pawName}" is not healthy`, {
|
|
1413
|
+
toolName: action.tool,
|
|
1414
|
+
pawName: tool.pawName
|
|
1415
|
+
}),
|
|
1416
|
+
Date.now() - startTime
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
if (tool.parameters && typeof tool.parameters.parse === "function") {
|
|
1421
|
+
tool.parameters.parse(action.params);
|
|
1422
|
+
}
|
|
1423
|
+
const output = await tool.execute(action.params);
|
|
1424
|
+
return successResult(action.tool, tool.pawName, output, Date.now() - startTime);
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1427
|
+
const isTimeout = message.toLowerCase().includes("timeout");
|
|
1428
|
+
const isPermission = message.toLowerCase().includes("permission");
|
|
1429
|
+
const code = isTimeout ? "TOOL_TIMEOUT" : isPermission ? "PERMISSION_DENIED" : "TOOL_EXCEPTION";
|
|
1430
|
+
return failureResult(
|
|
1431
|
+
action.tool,
|
|
1432
|
+
tool.pawName,
|
|
1433
|
+
createActionError(code, message, {
|
|
1434
|
+
toolName: action.tool,
|
|
1435
|
+
pawName: tool.pawName,
|
|
1436
|
+
details: err instanceof Error ? err.stack : void 0
|
|
1437
|
+
}),
|
|
1438
|
+
Date.now() - startTime
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// src/index.ts
|
|
1444
|
+
init_task();
|
|
1445
|
+
init_scheduler();
|
|
1446
|
+
|
|
1447
|
+
// src/core/rate-limiter.ts
|
|
1448
|
+
init_logger();
|
|
1449
|
+
var logger5 = createLogger("rate-limiter");
|
|
1450
|
+
var RateLimiter = class {
|
|
1451
|
+
buckets = /* @__PURE__ */ new Map();
|
|
1452
|
+
/**
|
|
1453
|
+
* Try to consume one token from the bucket.
|
|
1454
|
+
* Returns true if the request is under the limit, false if rate-limited.
|
|
1455
|
+
*/
|
|
1456
|
+
tryConsume(bucket, limit, windowMs) {
|
|
1457
|
+
const now = Date.now();
|
|
1458
|
+
this.cleanup(bucket, now, windowMs);
|
|
1459
|
+
const timestamps = this.buckets.get(bucket) ?? [];
|
|
1460
|
+
if (timestamps.length >= limit) {
|
|
1461
|
+
logger5.debug(`Bucket "${bucket}" rate-limited: ${timestamps.length}/${limit} in ${windowMs}ms window`);
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
timestamps.push(now);
|
|
1465
|
+
this.buckets.set(bucket, timestamps);
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Returns the number of remaining tokens in the bucket for the current window.
|
|
1470
|
+
*/
|
|
1471
|
+
remaining(bucket, limit, windowMs) {
|
|
1472
|
+
const now = Date.now();
|
|
1473
|
+
this.cleanup(bucket, now, windowMs);
|
|
1474
|
+
const timestamps = this.buckets.get(bucket) ?? [];
|
|
1475
|
+
return Math.max(0, limit - timestamps.length);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Remove expired timestamps from a bucket.
|
|
1479
|
+
*/
|
|
1480
|
+
cleanup(bucket, now, windowMs) {
|
|
1481
|
+
const timestamps = this.buckets.get(bucket);
|
|
1482
|
+
if (!timestamps) return;
|
|
1483
|
+
const cutoff = now - windowMs;
|
|
1484
|
+
const filtered = timestamps.filter((t) => t > cutoff);
|
|
1485
|
+
if (filtered.length === 0) {
|
|
1486
|
+
this.buckets.delete(bucket);
|
|
1487
|
+
} else {
|
|
1488
|
+
this.buckets.set(bucket, filtered);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/index.ts
|
|
1494
|
+
init_registry();
|
|
1495
|
+
|
|
1496
|
+
// src/paw/registry.ts
|
|
1497
|
+
init_manifest();
|
|
1498
|
+
|
|
1499
|
+
// src/paw/loader.ts
|
|
1500
|
+
import * as path3 from "path";
|
|
1501
|
+
import { execa } from "execa";
|
|
1502
|
+
|
|
1503
|
+
// src/core/ipc.ts
|
|
1504
|
+
init_logger();
|
|
1505
|
+
import * as crypto2 from "crypto";
|
|
1506
|
+
var logger8 = createLogger("ipc");
|
|
1507
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1508
|
+
var IpcTransport = class {
|
|
1509
|
+
constructor(type, childProcess, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1510
|
+
this.type = type;
|
|
1511
|
+
this.childProcess = childProcess;
|
|
1512
|
+
this.timeoutMs = timeoutMs;
|
|
1513
|
+
if (type === "ipc") {
|
|
1514
|
+
this.setupIpcListeners();
|
|
1515
|
+
} else {
|
|
1516
|
+
this.setupStdioListeners();
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
pending = /* @__PURE__ */ new Map();
|
|
1520
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1521
|
+
disposed = false;
|
|
1522
|
+
/** Register a handler for incoming requests from the Paw */
|
|
1523
|
+
onRequest(method, handler) {
|
|
1524
|
+
this.handlers.set(method, handler);
|
|
1525
|
+
}
|
|
1526
|
+
/** Send a request to the Paw and wait for a response */
|
|
1527
|
+
async request(method, params) {
|
|
1528
|
+
if (this.disposed) {
|
|
1529
|
+
throw new Error("Transport has been disposed");
|
|
1530
|
+
}
|
|
1531
|
+
const id = crypto2.randomUUID();
|
|
1532
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
1533
|
+
logger8.trace("Sending request: %s %s", method, JSON.stringify(params ?? "", null, 2));
|
|
1534
|
+
return new Promise((resolve7, reject) => {
|
|
1535
|
+
const timer = setTimeout(() => {
|
|
1536
|
+
this.pending.delete(id);
|
|
1537
|
+
reject(new Error(`IPC request "${method}" timed out after ${this.timeoutMs}ms`));
|
|
1538
|
+
}, this.timeoutMs);
|
|
1539
|
+
this.pending.set(id, { resolve: resolve7, reject, timer });
|
|
1540
|
+
this.send(message);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
/** Send a notification (no response expected) */
|
|
1544
|
+
notify(method, params) {
|
|
1545
|
+
if (this.disposed) return;
|
|
1546
|
+
const message = { jsonrpc: "2.0", method, params };
|
|
1547
|
+
this.send(message);
|
|
1548
|
+
}
|
|
1549
|
+
/** Clean up resources */
|
|
1550
|
+
dispose() {
|
|
1551
|
+
this.disposed = true;
|
|
1552
|
+
for (const [, pending] of this.pending) {
|
|
1553
|
+
clearTimeout(pending.timer);
|
|
1554
|
+
pending.reject(new Error("Transport disposed"));
|
|
1555
|
+
}
|
|
1556
|
+
this.pending.clear();
|
|
1557
|
+
this.handlers.clear();
|
|
1558
|
+
}
|
|
1559
|
+
send(message) {
|
|
1560
|
+
if (this.type === "ipc") {
|
|
1561
|
+
this.childProcess.send?.(message);
|
|
1562
|
+
} else {
|
|
1563
|
+
const json = JSON.stringify(message);
|
|
1564
|
+
const header = `Content-Length: ${Buffer.byteLength(json)}\r
|
|
1565
|
+
\r
|
|
1566
|
+
`;
|
|
1567
|
+
const stdin = this.childProcess.stdin;
|
|
1568
|
+
if (stdin?.writable) {
|
|
1569
|
+
stdin.write(header + json);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
handleMessage(msg) {
|
|
1574
|
+
logger8.trace("Received message: %s %s", msg.method ?? msg.id ?? "unknown", JSON.stringify(msg.params ?? msg.result ?? "", null, 2));
|
|
1575
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
1576
|
+
const pending = this.pending.get(msg.id);
|
|
1577
|
+
this.pending.delete(msg.id);
|
|
1578
|
+
clearTimeout(pending.timer);
|
|
1579
|
+
if (msg.error) {
|
|
1580
|
+
pending.reject(new Error(`${msg.error.message} (code: ${msg.error.code})`));
|
|
1581
|
+
} else {
|
|
1582
|
+
pending.resolve(msg.result);
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (msg.method && this.handlers.has(msg.method)) {
|
|
1587
|
+
const handler = this.handlers.get(msg.method);
|
|
1588
|
+
handler(msg.params).then((result) => {
|
|
1589
|
+
if (msg.id) {
|
|
1590
|
+
this.send({ jsonrpc: "2.0", id: msg.id, result });
|
|
1591
|
+
}
|
|
1592
|
+
}).catch((err) => {
|
|
1593
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1594
|
+
logger8.error('Handler error for "%s": %s', msg.method, errorMessage);
|
|
1595
|
+
if (msg.id) {
|
|
1596
|
+
this.send({
|
|
1597
|
+
jsonrpc: "2.0",
|
|
1598
|
+
id: msg.id,
|
|
1599
|
+
error: { code: -32603, message: errorMessage }
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
setupIpcListeners() {
|
|
1606
|
+
this.childProcess.on("message", (msg) => {
|
|
1607
|
+
this.handleMessage(msg);
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
setupStdioListeners() {
|
|
1611
|
+
const stdout = this.childProcess.stdout;
|
|
1612
|
+
if (!stdout) {
|
|
1613
|
+
logger8.error("No stdout stream available for stdio transport");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
let buffer = "";
|
|
1617
|
+
stdout.setEncoding("utf-8");
|
|
1618
|
+
stdout.on("data", (chunk) => {
|
|
1619
|
+
buffer += chunk;
|
|
1620
|
+
while (buffer.length > 0) {
|
|
1621
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
1622
|
+
if (headerEnd === -1) break;
|
|
1623
|
+
const header = buffer.substring(0, headerEnd);
|
|
1624
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
1625
|
+
if (!match) {
|
|
1626
|
+
logger8.error("Invalid header in stdio transport: %s", header);
|
|
1627
|
+
buffer = buffer.substring(headerEnd + 4);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const contentLength = Number.parseInt(match[1], 10);
|
|
1631
|
+
const bodyStart = headerEnd + 4;
|
|
1632
|
+
if (buffer.length < bodyStart + contentLength) break;
|
|
1633
|
+
const body = buffer.substring(bodyStart, bodyStart + contentLength);
|
|
1634
|
+
buffer = buffer.substring(bodyStart + contentLength);
|
|
1635
|
+
try {
|
|
1636
|
+
const msg = JSON.parse(body);
|
|
1637
|
+
this.handleMessage(msg);
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
logger8.error("Failed to parse stdio JSON-RPC message: %s", err);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
function createTransport(type, childProcess, timeoutMs) {
|
|
1646
|
+
return new IpcTransport(type, childProcess, timeoutMs);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/paw/sandbox.ts
|
|
1650
|
+
init_logger();
|
|
1651
|
+
var logger9 = createLogger("paw-sandbox");
|
|
1652
|
+
function computeEffectivePermissions(manifest, config) {
|
|
1653
|
+
const requested = manifest.permissions ?? { network: [], listen: [], filesystem: [], env: [] };
|
|
1654
|
+
const granted = config.allow;
|
|
1655
|
+
if (!granted) {
|
|
1656
|
+
return {
|
|
1657
|
+
network: requested.network ?? [],
|
|
1658
|
+
listen: requested.listen ?? [],
|
|
1659
|
+
filesystem: requested.filesystem ?? [],
|
|
1660
|
+
env: requested.env ?? []
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
network: intersectStrings(requested.network ?? [], granted.network ?? []),
|
|
1665
|
+
listen: intersectNumbers(requested.listen ?? [], granted.listen ?? []),
|
|
1666
|
+
filesystem: intersectStrings(requested.filesystem ?? [], granted.filesystem ?? []),
|
|
1667
|
+
env: intersectStrings(requested.env ?? [], granted.env ?? [])
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function intersectStrings(a, b) {
|
|
1671
|
+
const setB = new Set(b);
|
|
1672
|
+
return a.filter((item) => setB.has(item));
|
|
1673
|
+
}
|
|
1674
|
+
function intersectNumbers(a, b) {
|
|
1675
|
+
const setB = new Set(b);
|
|
1676
|
+
return a.filter((item) => setB.has(item));
|
|
1677
|
+
}
|
|
1678
|
+
function buildSandboxEnv(permissions) {
|
|
1679
|
+
const env = {
|
|
1680
|
+
// Always pass NODE_ENV and PATH
|
|
1681
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
1682
|
+
PATH: process.env.PATH,
|
|
1683
|
+
// Debug flag
|
|
1684
|
+
VOLE_LOG_LEVEL: process.env.VOLE_LOG_LEVEL
|
|
1685
|
+
};
|
|
1686
|
+
if (permissions.listen.length > 0) {
|
|
1687
|
+
env.VOLE_LISTEN_PORTS = permissions.listen.join(",");
|
|
1688
|
+
}
|
|
1689
|
+
for (const key of permissions.env) {
|
|
1690
|
+
if (process.env[key] !== void 0) {
|
|
1691
|
+
env[key] = process.env[key];
|
|
1692
|
+
} else {
|
|
1693
|
+
logger9.warn(`Env var "${key}" is permitted but not set in environment`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return env;
|
|
1697
|
+
}
|
|
1698
|
+
function validatePermissions(manifest, config) {
|
|
1699
|
+
const warnings = [];
|
|
1700
|
+
const effective = computeEffectivePermissions(manifest, config);
|
|
1701
|
+
const requested = manifest.permissions ?? {};
|
|
1702
|
+
for (const domain of requested.network ?? []) {
|
|
1703
|
+
if (!effective.network.includes(domain)) {
|
|
1704
|
+
warnings.push(
|
|
1705
|
+
`Network access to "${domain}" requested by ${manifest.name} but not granted in config`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
for (const port of requested.listen ?? []) {
|
|
1710
|
+
if (!effective.listen.includes(port)) {
|
|
1711
|
+
warnings.push(
|
|
1712
|
+
`Listen on port ${port} requested by ${manifest.name} but not granted in config`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
for (const fspath of requested.filesystem ?? []) {
|
|
1717
|
+
if (!effective.filesystem.includes(fspath)) {
|
|
1718
|
+
warnings.push(
|
|
1719
|
+
`Filesystem access to "${fspath}" requested by ${manifest.name} but not granted in config`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
for (const envVar of requested.env ?? []) {
|
|
1724
|
+
if (!effective.env.includes(envVar)) {
|
|
1725
|
+
warnings.push(
|
|
1726
|
+
`Env var "${envVar}" requested by ${manifest.name} but not granted in config`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (warnings.length > 0) {
|
|
1731
|
+
for (const w of warnings) {
|
|
1732
|
+
logger9.info(w);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return warnings;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/paw/loader.ts
|
|
1739
|
+
init_logger();
|
|
1740
|
+
var logger10 = createLogger("paw-loader");
|
|
1741
|
+
async function loadInProcessPaw(pawPath, manifest, config) {
|
|
1742
|
+
const entryPath = path3.resolve(pawPath, manifest.entry);
|
|
1743
|
+
logger10.info(`Loading in-process Paw "${manifest.name}" from ${entryPath}`);
|
|
1744
|
+
const module = await import(entryPath);
|
|
1745
|
+
const definition = module.default ?? module;
|
|
1746
|
+
if (definition.onLoad) {
|
|
1747
|
+
await definition.onLoad(config);
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
name: manifest.name,
|
|
1751
|
+
manifest,
|
|
1752
|
+
config,
|
|
1753
|
+
healthy: true,
|
|
1754
|
+
transport: "ipc",
|
|
1755
|
+
inProcess: true,
|
|
1756
|
+
definition
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
async function loadSubprocessPaw(pawPath, manifest, config) {
|
|
1760
|
+
const entryPath = path3.resolve(pawPath, manifest.entry);
|
|
1761
|
+
const transport = manifest.transport ?? "ipc";
|
|
1762
|
+
const permissions = computeEffectivePermissions(manifest, config);
|
|
1763
|
+
const env = buildSandboxEnv(permissions);
|
|
1764
|
+
logger10.info(
|
|
1765
|
+
`Spawning subprocess Paw "${manifest.name}" (transport: ${transport}) from ${entryPath}`
|
|
1766
|
+
);
|
|
1767
|
+
const stdioConfig = transport === "ipc" ? ["pipe", "pipe", "pipe", "ipc"] : ["pipe", "pipe", "pipe"];
|
|
1768
|
+
const child = execa("node", [entryPath], {
|
|
1769
|
+
env,
|
|
1770
|
+
stdio: stdioConfig,
|
|
1771
|
+
reject: false,
|
|
1772
|
+
cleanup: true
|
|
1773
|
+
});
|
|
1774
|
+
child.stderr?.on("data", (data) => {
|
|
1775
|
+
logger10.warn(`[${manifest.name}] ${data.toString().trimEnd()}`);
|
|
1776
|
+
});
|
|
1777
|
+
const ipcTransport = createTransport(transport, child);
|
|
1778
|
+
const instance = {
|
|
1779
|
+
name: manifest.name,
|
|
1780
|
+
manifest,
|
|
1781
|
+
config,
|
|
1782
|
+
healthy: true,
|
|
1783
|
+
transport,
|
|
1784
|
+
inProcess: false,
|
|
1785
|
+
process: {
|
|
1786
|
+
kill: () => child.kill(),
|
|
1787
|
+
pid: child.pid
|
|
1788
|
+
},
|
|
1789
|
+
sendRequest: (method, params) => ipcTransport.request(method, params)
|
|
1790
|
+
};
|
|
1791
|
+
child.then?.((result) => {
|
|
1792
|
+
if (instance.healthy) {
|
|
1793
|
+
instance.healthy = false;
|
|
1794
|
+
logger10.error(
|
|
1795
|
+
`Paw "${manifest.name}" exited unexpectedly (code: ${result.exitCode})`
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
}).catch?.(() => {
|
|
1799
|
+
instance.healthy = false;
|
|
1800
|
+
});
|
|
1801
|
+
return { instance, transport: ipcTransport };
|
|
1802
|
+
}
|
|
1803
|
+
async function shutdownPaw(instance) {
|
|
1804
|
+
if (instance.inProcess) {
|
|
1805
|
+
if (instance.definition?.onUnload) {
|
|
1806
|
+
await instance.definition.onUnload();
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (instance.sendRequest) {
|
|
1811
|
+
try {
|
|
1812
|
+
await Promise.race([
|
|
1813
|
+
instance.sendRequest("shutdown"),
|
|
1814
|
+
new Promise((resolve7) => setTimeout(resolve7, 5e3))
|
|
1815
|
+
]);
|
|
1816
|
+
} catch {
|
|
1817
|
+
logger10.warn(`Shutdown request to "${instance.name}" failed, killing process`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
instance.process?.kill();
|
|
1821
|
+
instance.healthy = false;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/paw/registry.ts
|
|
1825
|
+
init_logger();
|
|
1826
|
+
var logger11 = createLogger("paw-registry");
|
|
1827
|
+
var PawRegistry = class {
|
|
1828
|
+
constructor(bus, toolRegistry, projectRoot) {
|
|
1829
|
+
this.bus = bus;
|
|
1830
|
+
this.toolRegistry = toolRegistry;
|
|
1831
|
+
this.projectRoot = projectRoot;
|
|
1832
|
+
this.bus.on("paw:crashed", ({ pawName }) => {
|
|
1833
|
+
const instance = this.paws.get(pawName);
|
|
1834
|
+
if (instance) {
|
|
1835
|
+
instance.healthy = false;
|
|
1836
|
+
this.toolRegistry.unregister(pawName);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
paws = /* @__PURE__ */ new Map();
|
|
1841
|
+
transports = /* @__PURE__ */ new Map();
|
|
1842
|
+
perceiveHooks = [];
|
|
1843
|
+
observeHookPaws = [];
|
|
1844
|
+
bootstrapPaws = [];
|
|
1845
|
+
compactPaws = [];
|
|
1846
|
+
brainPawName;
|
|
1847
|
+
/** Maps config path → manifest name (e.g. "./paws/paw-ollama" → "@openvole/paw-ollama") */
|
|
1848
|
+
configToManifest = /* @__PURE__ */ new Map();
|
|
1849
|
+
skillRegistry;
|
|
1850
|
+
taskQueue;
|
|
1851
|
+
scheduler;
|
|
1852
|
+
/** Inject queryable registries (called after construction to avoid circular deps) */
|
|
1853
|
+
setQuerySources(skills, tasks, scheduler) {
|
|
1854
|
+
this.skillRegistry = skills;
|
|
1855
|
+
this.taskQueue = tasks;
|
|
1856
|
+
this.scheduler = scheduler;
|
|
1857
|
+
}
|
|
1858
|
+
/** Load and register a Paw */
|
|
1859
|
+
async load(config) {
|
|
1860
|
+
const pawPath = resolvePawPath(config.name, this.projectRoot);
|
|
1861
|
+
const manifest = await readPawManifest(pawPath);
|
|
1862
|
+
if (!manifest) {
|
|
1863
|
+
logger11.error(`Failed to read manifest for "${config.name}"`);
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
const pawName = manifest.name;
|
|
1867
|
+
if (this.paws.has(pawName)) {
|
|
1868
|
+
logger11.warn(`Paw "${pawName}" is already loaded`);
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
this.configToManifest.set(config.name, pawName);
|
|
1872
|
+
validatePermissions(manifest, config);
|
|
1873
|
+
try {
|
|
1874
|
+
let instance;
|
|
1875
|
+
if (manifest.inProcess) {
|
|
1876
|
+
instance = await loadInProcessPaw(pawPath, manifest, config);
|
|
1877
|
+
this.registerInProcessTools(instance);
|
|
1878
|
+
} else {
|
|
1879
|
+
const result = await loadSubprocessPaw(pawPath, manifest, config);
|
|
1880
|
+
instance = result.instance;
|
|
1881
|
+
this.transports.set(pawName, result.transport);
|
|
1882
|
+
this.setupTransportHandlers(pawName, result.transport);
|
|
1883
|
+
await this.waitForRegistration(pawName, result.transport);
|
|
1884
|
+
}
|
|
1885
|
+
this.paws.set(pawName, instance);
|
|
1886
|
+
if (instance.definition?.hooks?.onPerceive) {
|
|
1887
|
+
const hookConfig = config.hooks?.perceive;
|
|
1888
|
+
const hasTools = (instance.definition?.tools?.length ?? 0) > 0 || manifest.tools.length > 0;
|
|
1889
|
+
this.perceiveHooks.push({
|
|
1890
|
+
pawName,
|
|
1891
|
+
order: hookConfig?.order ?? 100,
|
|
1892
|
+
pipeline: hookConfig?.pipeline ?? true,
|
|
1893
|
+
hasTools
|
|
1894
|
+
});
|
|
1895
|
+
this.perceiveHooks.sort((a, b) => a.order - b.order);
|
|
1896
|
+
}
|
|
1897
|
+
if (instance.definition?.hooks?.onObserve) {
|
|
1898
|
+
this.observeHookPaws.push(pawName);
|
|
1899
|
+
}
|
|
1900
|
+
if (instance.definition?.hooks?.onBootstrap) {
|
|
1901
|
+
this.bootstrapPaws.push(pawName);
|
|
1902
|
+
}
|
|
1903
|
+
if (instance.definition?.hooks?.onCompact) {
|
|
1904
|
+
this.compactPaws.push(pawName);
|
|
1905
|
+
}
|
|
1906
|
+
logger11.info(`Paw "${pawName}" loaded successfully`);
|
|
1907
|
+
this.bus.emit("paw:registered", { pawName });
|
|
1908
|
+
return true;
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
logger11.error(`Failed to load Paw "${pawName}": ${err}`);
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
/** Unload a Paw (accepts config path or manifest name) */
|
|
1915
|
+
async unload(name) {
|
|
1916
|
+
const pawName = this.configToManifest.get(name) ?? name;
|
|
1917
|
+
const instance = this.paws.get(pawName);
|
|
1918
|
+
if (!instance) {
|
|
1919
|
+
logger11.warn(`Paw "${pawName}" is not loaded`);
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
name = pawName;
|
|
1923
|
+
await shutdownPaw(instance);
|
|
1924
|
+
const transport = this.transports.get(name);
|
|
1925
|
+
if (transport) {
|
|
1926
|
+
transport.dispose();
|
|
1927
|
+
this.transports.delete(name);
|
|
1928
|
+
}
|
|
1929
|
+
this.toolRegistry.unregister(name);
|
|
1930
|
+
this.perceiveHooks = this.perceiveHooks.filter((h) => h.pawName !== name);
|
|
1931
|
+
this.observeHookPaws = this.observeHookPaws.filter((n) => n !== name);
|
|
1932
|
+
this.bootstrapPaws = this.bootstrapPaws.filter((n) => n !== name);
|
|
1933
|
+
this.compactPaws = this.compactPaws.filter((n) => n !== name);
|
|
1934
|
+
this.paws.delete(name);
|
|
1935
|
+
for (const [configName, manifestName] of this.configToManifest) {
|
|
1936
|
+
if (manifestName === name) {
|
|
1937
|
+
this.configToManifest.delete(configName);
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
logger11.info(`Paw "${name}" unloaded`);
|
|
1942
|
+
this.bus.emit("paw:unregistered", { pawName: name });
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1945
|
+
/** Resolve a config name to its manifest name */
|
|
1946
|
+
resolveManifestName(configName) {
|
|
1947
|
+
return this.configToManifest.get(configName) ?? configName;
|
|
1948
|
+
}
|
|
1949
|
+
/** Set the Brain Paw name (accepts config path or manifest name) */
|
|
1950
|
+
setBrain(name) {
|
|
1951
|
+
this.brainPawName = this.configToManifest.get(name) ?? name;
|
|
1952
|
+
}
|
|
1953
|
+
/** Get the Brain Paw name */
|
|
1954
|
+
getBrainName() {
|
|
1955
|
+
return this.brainPawName;
|
|
1956
|
+
}
|
|
1957
|
+
/** Get a Paw instance */
|
|
1958
|
+
get(name) {
|
|
1959
|
+
return this.paws.get(name);
|
|
1960
|
+
}
|
|
1961
|
+
/** List all loaded Paws */
|
|
1962
|
+
list() {
|
|
1963
|
+
return Array.from(this.paws.values());
|
|
1964
|
+
}
|
|
1965
|
+
/** Check if a Paw is healthy */
|
|
1966
|
+
isHealthy(name) {
|
|
1967
|
+
return this.paws.get(name)?.healthy ?? false;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Run GLOBAL perceive hooks — only Paws without tools.
|
|
1971
|
+
* Paws with tools use lazy perceive (called just before their tool executes).
|
|
1972
|
+
*/
|
|
1973
|
+
async runGlobalPerceiveHooks(context) {
|
|
1974
|
+
let chainedContext = { ...context };
|
|
1975
|
+
const globalChained = this.perceiveHooks.filter((h) => h.pipeline && !h.hasTools);
|
|
1976
|
+
const globalUnchained = this.perceiveHooks.filter((h) => !h.pipeline && !h.hasTools);
|
|
1977
|
+
for (const hook of globalChained) {
|
|
1978
|
+
const instance = this.paws.get(hook.pawName);
|
|
1979
|
+
if (!instance?.healthy) continue;
|
|
1980
|
+
try {
|
|
1981
|
+
chainedContext = await this.callPerceive(hook.pawName, chainedContext);
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
logger11.error(`Perceive hook error from "${hook.pawName}": ${err}`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (globalUnchained.length > 0) {
|
|
1987
|
+
const results = await Promise.allSettled(
|
|
1988
|
+
globalUnchained.map(async (hook) => {
|
|
1989
|
+
const instance = this.paws.get(hook.pawName);
|
|
1990
|
+
if (!instance?.healthy) return null;
|
|
1991
|
+
try {
|
|
1992
|
+
return await this.callPerceive(hook.pawName, context);
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
logger11.error(`Unchained perceive error from "${hook.pawName}": ${err}`);
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1999
|
+
for (const result of results) {
|
|
2000
|
+
if (result.status === "fulfilled" && result.value) {
|
|
2001
|
+
Object.assign(chainedContext.metadata, result.value.metadata);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
return chainedContext;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Run LAZY perceive for a specific Paw — called just before its tool executes.
|
|
2009
|
+
* Only runs if the Paw has an onPerceive hook registered.
|
|
2010
|
+
*/
|
|
2011
|
+
async runLazyPerceive(pawName, context) {
|
|
2012
|
+
const hook = this.perceiveHooks.find((h) => h.pawName === pawName && h.hasTools);
|
|
2013
|
+
if (!hook) return context;
|
|
2014
|
+
const instance = this.paws.get(pawName);
|
|
2015
|
+
if (!instance?.healthy) return context;
|
|
2016
|
+
try {
|
|
2017
|
+
logger11.debug(`Lazy perceive for "${pawName}" before tool execution`);
|
|
2018
|
+
return await this.callPerceive(pawName, context);
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
logger11.error(`Lazy perceive error from "${pawName}": ${err}`);
|
|
2021
|
+
return context;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
/** Run all Observe hooks concurrently (fire-and-forget) */
|
|
2025
|
+
runObserveHooks(result) {
|
|
2026
|
+
for (const pawName of this.observeHookPaws) {
|
|
2027
|
+
const instance = this.paws.get(pawName);
|
|
2028
|
+
if (!instance?.healthy) continue;
|
|
2029
|
+
this.callObserve(pawName, result).catch((err) => {
|
|
2030
|
+
logger11.error(`Observe hook error from "${pawName}": ${err}`);
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/** Run bootstrap hooks — called once at the start of a task */
|
|
2035
|
+
async runBootstrapHooks(context) {
|
|
2036
|
+
let ctx = { ...context };
|
|
2037
|
+
for (const pawName of this.bootstrapPaws) {
|
|
2038
|
+
const instance = this.paws.get(pawName);
|
|
2039
|
+
if (!instance?.healthy) continue;
|
|
2040
|
+
try {
|
|
2041
|
+
if (instance.inProcess && instance.definition?.hooks?.onBootstrap) {
|
|
2042
|
+
ctx = await instance.definition.hooks.onBootstrap(ctx);
|
|
2043
|
+
} else {
|
|
2044
|
+
const transport = this.transports.get(pawName);
|
|
2045
|
+
if (transport) {
|
|
2046
|
+
ctx = await transport.request("bootstrap", ctx);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
logger11.error(`Bootstrap hook error from "${pawName}": ${err}`);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return ctx;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Run compact hooks — called when context exceeds size threshold.
|
|
2057
|
+
* Paws can compress/summarize messages to free up context window space.
|
|
2058
|
+
*/
|
|
2059
|
+
async runCompactHooks(context) {
|
|
2060
|
+
let ctx = { ...context };
|
|
2061
|
+
for (const pawName of this.compactPaws) {
|
|
2062
|
+
const instance = this.paws.get(pawName);
|
|
2063
|
+
if (!instance?.healthy) continue;
|
|
2064
|
+
try {
|
|
2065
|
+
if (instance.inProcess && instance.definition?.hooks?.onCompact) {
|
|
2066
|
+
ctx = await instance.definition.hooks.onCompact(ctx);
|
|
2067
|
+
} else {
|
|
2068
|
+
const transport = this.transports.get(pawName);
|
|
2069
|
+
if (transport) {
|
|
2070
|
+
ctx = await transport.request("compact", ctx);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
logger11.info(`Compact hook from "${pawName}" reduced messages from ${context.messages.length} to ${ctx.messages.length}`);
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
logger11.error(`Compact hook error from "${pawName}": ${err}`);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return ctx;
|
|
2079
|
+
}
|
|
2080
|
+
/** Call the Brain Paw's think function */
|
|
2081
|
+
async think(context) {
|
|
2082
|
+
if (!this.brainPawName) return null;
|
|
2083
|
+
const instance = this.paws.get(this.brainPawName);
|
|
2084
|
+
if (!instance?.healthy) {
|
|
2085
|
+
logger11.error(`Brain Paw "${this.brainPawName}" is not healthy`);
|
|
2086
|
+
return null;
|
|
2087
|
+
}
|
|
2088
|
+
if (instance.inProcess && instance.definition?.think) {
|
|
2089
|
+
return instance.definition.think(context);
|
|
2090
|
+
}
|
|
2091
|
+
const transport = this.transports.get(this.brainPawName);
|
|
2092
|
+
if (!transport) {
|
|
2093
|
+
logger11.error(`No transport for Brain Paw "${this.brainPawName}"`);
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
return await transport.request("think", context);
|
|
2097
|
+
}
|
|
2098
|
+
/** Execute a tool on a subprocess Paw */
|
|
2099
|
+
async executeRemoteTool(pawName, toolName, params) {
|
|
2100
|
+
const transport = this.transports.get(pawName);
|
|
2101
|
+
if (!transport) {
|
|
2102
|
+
throw new Error(`No transport for Paw "${pawName}"`);
|
|
2103
|
+
}
|
|
2104
|
+
return transport.request("execute_tool", { toolName, params });
|
|
2105
|
+
}
|
|
2106
|
+
async callPerceive(pawName, context) {
|
|
2107
|
+
const instance = this.paws.get(pawName);
|
|
2108
|
+
if (instance.inProcess && instance.definition?.hooks?.onPerceive) {
|
|
2109
|
+
return instance.definition.hooks.onPerceive(context);
|
|
2110
|
+
}
|
|
2111
|
+
const transport = this.transports.get(pawName);
|
|
2112
|
+
if (transport) {
|
|
2113
|
+
return await transport.request("perceive", context);
|
|
2114
|
+
}
|
|
2115
|
+
return context;
|
|
2116
|
+
}
|
|
2117
|
+
async callObserve(pawName, result) {
|
|
2118
|
+
const instance = this.paws.get(pawName);
|
|
2119
|
+
if (instance.inProcess && instance.definition?.hooks?.onObserve) {
|
|
2120
|
+
await instance.definition.hooks.onObserve(result);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const transport = this.transports.get(pawName);
|
|
2124
|
+
if (transport) {
|
|
2125
|
+
await transport.request("observe", result);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
registerInProcessTools(instance) {
|
|
2129
|
+
if (instance.definition?.tools) {
|
|
2130
|
+
this.toolRegistry.register(
|
|
2131
|
+
instance.name,
|
|
2132
|
+
instance.definition.tools,
|
|
2133
|
+
true
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
setupTransportHandlers(pawName, transport) {
|
|
2138
|
+
transport.onRequest("log", async (params) => {
|
|
2139
|
+
const { level, message } = params;
|
|
2140
|
+
const prefix = `[${pawName}]`;
|
|
2141
|
+
switch (level) {
|
|
2142
|
+
case "error":
|
|
2143
|
+
console.error(prefix, message);
|
|
2144
|
+
break;
|
|
2145
|
+
case "warn":
|
|
2146
|
+
console.warn(prefix, message);
|
|
2147
|
+
break;
|
|
2148
|
+
case "info":
|
|
2149
|
+
console.info(prefix, message);
|
|
2150
|
+
break;
|
|
2151
|
+
default:
|
|
2152
|
+
console.debug(prefix, message);
|
|
2153
|
+
}
|
|
2154
|
+
return { ok: true };
|
|
2155
|
+
});
|
|
2156
|
+
transport.onRequest("emit", async (params) => {
|
|
2157
|
+
const { event } = params;
|
|
2158
|
+
logger11.info(`Paw "${pawName}" emitted event: ${event}`);
|
|
2159
|
+
return { ok: true };
|
|
2160
|
+
});
|
|
2161
|
+
transport.onRequest("subscribe", async (params) => {
|
|
2162
|
+
const { events } = params;
|
|
2163
|
+
const instance = this.paws.get(pawName);
|
|
2164
|
+
if (instance) {
|
|
2165
|
+
instance.subscriptions = events;
|
|
2166
|
+
this.setupBusForwarding(pawName, events, transport);
|
|
2167
|
+
logger11.info(`Paw "${pawName}" subscribed to events: ${events.join(", ")}`);
|
|
2168
|
+
}
|
|
2169
|
+
return { ok: true };
|
|
2170
|
+
});
|
|
2171
|
+
transport.onRequest("query", async (params) => {
|
|
2172
|
+
const { type } = params;
|
|
2173
|
+
return this.handleQuery(type);
|
|
2174
|
+
});
|
|
2175
|
+
transport.onRequest("create_task", async (params) => {
|
|
2176
|
+
const { input, source, sessionId, metadata } = params;
|
|
2177
|
+
if (!this.taskQueue) {
|
|
2178
|
+
return { error: "Task queue not available" };
|
|
2179
|
+
}
|
|
2180
|
+
const task = this.taskQueue.enqueue(input, source ?? "paw", { sessionId, metadata });
|
|
2181
|
+
logger11.info(`Paw "${pawName}" created task ${task.id}: "${input.substring(0, 50)}"`);
|
|
2182
|
+
return { taskId: task.id };
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
/** Forward bus events to a Paw that subscribed */
|
|
2186
|
+
setupBusForwarding(pawName, events, transport) {
|
|
2187
|
+
for (const eventName of events) {
|
|
2188
|
+
this.bus.on(eventName, (data) => {
|
|
2189
|
+
const instance = this.paws.get(pawName);
|
|
2190
|
+
if (!instance?.healthy) return;
|
|
2191
|
+
transport.notify("bus_event", { event: eventName, data });
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/** Handle state queries from Paws */
|
|
2196
|
+
handleQuery(type) {
|
|
2197
|
+
switch (type) {
|
|
2198
|
+
case "tools":
|
|
2199
|
+
return this.toolRegistry.list().map((t) => ({
|
|
2200
|
+
name: t.name,
|
|
2201
|
+
description: t.description,
|
|
2202
|
+
pawName: t.pawName,
|
|
2203
|
+
inProcess: t.inProcess
|
|
2204
|
+
}));
|
|
2205
|
+
case "paws":
|
|
2206
|
+
return Array.from(this.paws.values()).map((p) => ({
|
|
2207
|
+
name: p.name,
|
|
2208
|
+
healthy: p.healthy,
|
|
2209
|
+
inProcess: p.inProcess,
|
|
2210
|
+
transport: p.transport,
|
|
2211
|
+
toolCount: this.toolRegistry.toolsForPaw(p.name).length
|
|
2212
|
+
}));
|
|
2213
|
+
case "skills":
|
|
2214
|
+
return this.skillRegistry?.list().map((s) => ({
|
|
2215
|
+
name: s.name,
|
|
2216
|
+
active: s.active,
|
|
2217
|
+
missingTools: s.missingTools,
|
|
2218
|
+
description: s.definition.description
|
|
2219
|
+
})) ?? [];
|
|
2220
|
+
case "tasks":
|
|
2221
|
+
return this.taskQueue?.list().map((t) => ({
|
|
2222
|
+
id: t.id,
|
|
2223
|
+
source: t.source,
|
|
2224
|
+
input: t.input,
|
|
2225
|
+
status: t.status,
|
|
2226
|
+
createdAt: t.createdAt,
|
|
2227
|
+
startedAt: t.startedAt,
|
|
2228
|
+
completedAt: t.completedAt
|
|
2229
|
+
})) ?? [];
|
|
2230
|
+
case "schedules":
|
|
2231
|
+
return this.scheduler?.list() ?? [];
|
|
2232
|
+
default:
|
|
2233
|
+
return { error: `Unknown query type: ${type}` };
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async waitForRegistration(pawName, transport) {
|
|
2237
|
+
return new Promise((resolve7, reject) => {
|
|
2238
|
+
const timeout = setTimeout(() => {
|
|
2239
|
+
reject(new Error(`Paw "${pawName}" did not register within 10 seconds`));
|
|
2240
|
+
}, 1e4);
|
|
2241
|
+
transport.onRequest("register", async (params) => {
|
|
2242
|
+
clearTimeout(timeout);
|
|
2243
|
+
const registration = params;
|
|
2244
|
+
if (registration.tools) {
|
|
2245
|
+
const toolDefs = registration.tools.map((t) => ({
|
|
2246
|
+
name: t.name,
|
|
2247
|
+
description: t.description,
|
|
2248
|
+
parameters: {},
|
|
2249
|
+
// Schema validated on Paw side
|
|
2250
|
+
execute: async (toolParams) => this.executeRemoteTool(pawName, t.name, toolParams)
|
|
2251
|
+
}));
|
|
2252
|
+
this.toolRegistry.register(pawName, toolDefs, false);
|
|
2253
|
+
}
|
|
2254
|
+
logger11.info(`Paw "${pawName}" registered with ${registration.tools?.length ?? 0} tools`);
|
|
2255
|
+
resolve7();
|
|
2256
|
+
return { ok: true };
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
// src/index.ts
|
|
2263
|
+
init_manifest();
|
|
2264
|
+
init_registry2();
|
|
2265
|
+
init_resolver();
|
|
2266
|
+
|
|
2267
|
+
// src/io/tty.ts
|
|
2268
|
+
import * as readline from "readline";
|
|
2269
|
+
function createTtyIO() {
|
|
2270
|
+
return {
|
|
2271
|
+
async confirm(message) {
|
|
2272
|
+
const rl = readline.createInterface({
|
|
2273
|
+
input: process.stdin,
|
|
2274
|
+
output: process.stdout
|
|
2275
|
+
});
|
|
2276
|
+
try {
|
|
2277
|
+
return await new Promise((resolve7) => {
|
|
2278
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
2279
|
+
resolve7(answer.trim().toLowerCase() === "y");
|
|
2280
|
+
});
|
|
2281
|
+
});
|
|
2282
|
+
} finally {
|
|
2283
|
+
rl.close();
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
async prompt(message) {
|
|
2287
|
+
const rl = readline.createInterface({
|
|
2288
|
+
input: process.stdin,
|
|
2289
|
+
output: process.stdout
|
|
2290
|
+
});
|
|
2291
|
+
try {
|
|
2292
|
+
return await new Promise((resolve7) => {
|
|
2293
|
+
rl.question(`${message} `, (answer) => {
|
|
2294
|
+
resolve7(answer.trim());
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
} finally {
|
|
2298
|
+
rl.close();
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
notify(message) {
|
|
2302
|
+
process.stdout.write(`${message}
|
|
2303
|
+
`);
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/index.ts
|
|
2309
|
+
init_bus();
|
|
2310
|
+
init_registry();
|
|
2311
|
+
import * as path7 from "path";
|
|
2312
|
+
init_registry2();
|
|
2313
|
+
init_task();
|
|
2314
|
+
init_config();
|
|
2315
|
+
init_scheduler();
|
|
2316
|
+
init_core_tools();
|
|
2317
|
+
import * as fs6 from "fs/promises";
|
|
2318
|
+
var engineLogger = {
|
|
2319
|
+
info: (msg, ...args) => console.info(`[openvole] ${msg}`, ...args),
|
|
2320
|
+
error: (msg, ...args) => console.error(`[openvole] ${msg}`, ...args)
|
|
2321
|
+
};
|
|
2322
|
+
async function createEngine(projectRoot, options) {
|
|
2323
|
+
const configPath = options?.configPath ?? path7.resolve(projectRoot, "vole.config.ts");
|
|
2324
|
+
const config = await loadConfig(configPath);
|
|
2325
|
+
const bus = createMessageBus();
|
|
2326
|
+
const toolRegistry = new ToolRegistry(bus);
|
|
2327
|
+
const pawRegistry = new PawRegistry(bus, toolRegistry, projectRoot);
|
|
2328
|
+
const skillRegistry = new SkillRegistry(bus, toolRegistry, projectRoot);
|
|
2329
|
+
const io = options?.io ?? createTtyIO();
|
|
2330
|
+
const rateLimiter = new RateLimiter();
|
|
2331
|
+
const taskQueue = new TaskQueue(bus, config.loop.taskConcurrency, rateLimiter, config.loop.rateLimits);
|
|
2332
|
+
const scheduler = new SchedulerStore();
|
|
2333
|
+
const coreTools = createCoreTools(scheduler, taskQueue, projectRoot, skillRegistry);
|
|
2334
|
+
toolRegistry.register("__core__", coreTools, true);
|
|
2335
|
+
pawRegistry.setQuerySources(skillRegistry, taskQueue, scheduler);
|
|
2336
|
+
taskQueue.setRunner(async (task) => {
|
|
2337
|
+
await runAgentLoop(task, {
|
|
2338
|
+
bus,
|
|
2339
|
+
toolRegistry,
|
|
2340
|
+
pawRegistry,
|
|
2341
|
+
skillRegistry,
|
|
2342
|
+
io,
|
|
2343
|
+
config: config.loop,
|
|
2344
|
+
toolProfiles: config.toolProfiles,
|
|
2345
|
+
rateLimiter
|
|
2346
|
+
});
|
|
2347
|
+
});
|
|
2348
|
+
const engine = {
|
|
2349
|
+
bus,
|
|
2350
|
+
toolRegistry,
|
|
2351
|
+
pawRegistry,
|
|
2352
|
+
skillRegistry,
|
|
2353
|
+
taskQueue,
|
|
2354
|
+
io,
|
|
2355
|
+
config,
|
|
2356
|
+
async start() {
|
|
2357
|
+
engineLogger.info("Starting OpenVole...");
|
|
2358
|
+
if (config.brain) {
|
|
2359
|
+
} else {
|
|
2360
|
+
engineLogger.info("No Brain Paw configured \u2014 Think step will be a no-op");
|
|
2361
|
+
}
|
|
2362
|
+
const pawConfigs = config.paws.map(normalizePawConfig);
|
|
2363
|
+
const brainConfig = pawConfigs.find((p) => p.name === config.brain);
|
|
2364
|
+
const subprocessPaws = pawConfigs.filter(
|
|
2365
|
+
(p) => p.name !== config.brain
|
|
2366
|
+
);
|
|
2367
|
+
if (brainConfig) {
|
|
2368
|
+
const ok = await pawRegistry.load(brainConfig);
|
|
2369
|
+
if (ok) {
|
|
2370
|
+
pawRegistry.setBrain(config.brain);
|
|
2371
|
+
engineLogger.info(`Brain Paw: ${pawRegistry.resolveManifestName(config.brain)}`);
|
|
2372
|
+
} else {
|
|
2373
|
+
engineLogger.error(
|
|
2374
|
+
`Brain Paw "${brainConfig.name}" failed to load \u2014 running in no-op Think mode`
|
|
2375
|
+
);
|
|
2376
|
+
pawRegistry.setBrain("");
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
for (const pawConfig of subprocessPaws) {
|
|
2380
|
+
await pawRegistry.load(pawConfig);
|
|
2381
|
+
}
|
|
2382
|
+
for (const skillName of config.skills) {
|
|
2383
|
+
await skillRegistry.load(skillName);
|
|
2384
|
+
}
|
|
2385
|
+
skillRegistry.resolve();
|
|
2386
|
+
if (config.heartbeat.enabled) {
|
|
2387
|
+
const heartbeatMdPath = path7.resolve(projectRoot, "HEARTBEAT.md");
|
|
2388
|
+
scheduler.add(
|
|
2389
|
+
"__heartbeat__",
|
|
2390
|
+
"Heartbeat wake-up",
|
|
2391
|
+
config.heartbeat.intervalMinutes,
|
|
2392
|
+
async () => {
|
|
2393
|
+
let heartbeatContent = "";
|
|
2394
|
+
try {
|
|
2395
|
+
heartbeatContent = await fs6.readFile(heartbeatMdPath, "utf-8");
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2398
|
+
const input = heartbeatContent ? `Heartbeat wake-up. Review your HEARTBEAT.md jobs and act on what is needed:
|
|
2399
|
+
|
|
2400
|
+
${heartbeatContent}` : "Heartbeat wake-up. Check active skills and decide if any actions are needed.";
|
|
2401
|
+
taskQueue.enqueue(input, "heartbeat");
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
engineLogger.info(`Heartbeat enabled \u2014 interval: ${config.heartbeat.intervalMinutes}m`);
|
|
2405
|
+
}
|
|
2406
|
+
engineLogger.info(
|
|
2407
|
+
`Ready \u2014 ${toolRegistry.list().length} tools, ${pawRegistry.list().length} paws, ${skillRegistry.active().length}/${skillRegistry.list().length} skills active`
|
|
2408
|
+
);
|
|
2409
|
+
},
|
|
2410
|
+
run(input, source = "user", sessionId) {
|
|
2411
|
+
taskQueue.enqueue(input, source, sessionId ? { sessionId } : void 0);
|
|
2412
|
+
},
|
|
2413
|
+
async shutdown() {
|
|
2414
|
+
engineLogger.info("Shutting down...");
|
|
2415
|
+
scheduler.clearAll();
|
|
2416
|
+
for (const paw of pawRegistry.list()) {
|
|
2417
|
+
await pawRegistry.unload(paw.name);
|
|
2418
|
+
}
|
|
2419
|
+
toolRegistry.clear();
|
|
2420
|
+
engineLogger.info("Shutdown complete");
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
return engine;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// src/cli.ts
|
|
2427
|
+
init_config();
|
|
2428
|
+
init_manifest();
|
|
2429
|
+
init_logger();
|
|
2430
|
+
var logger14 = createLogger("cli");
|
|
2431
|
+
async function main() {
|
|
2432
|
+
const args = process.argv.slice(2);
|
|
2433
|
+
const command = args[0];
|
|
2434
|
+
const projectRoot = process.cwd();
|
|
2435
|
+
if (command !== "init" && command !== "help" && command !== "--help" && command !== "-h" && command !== "--version" && command !== "-v" && command !== void 0) {
|
|
2436
|
+
const fsCheck = await import("fs/promises");
|
|
2437
|
+
try {
|
|
2438
|
+
await fsCheck.access(path8.join(projectRoot, "vole.config.json"));
|
|
2439
|
+
} catch {
|
|
2440
|
+
logger14.error("vole.config.json not found in current directory");
|
|
2441
|
+
logger14.info('Run "vole init" to create a new project, or cd to your project root');
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
switch (command) {
|
|
2446
|
+
case "start":
|
|
2447
|
+
await startInteractive(projectRoot);
|
|
2448
|
+
break;
|
|
2449
|
+
case "run": {
|
|
2450
|
+
const input = args.slice(1).join(" ");
|
|
2451
|
+
if (!input) {
|
|
2452
|
+
logger14.error('Usage: vole run "<task>"');
|
|
2453
|
+
process.exit(1);
|
|
2454
|
+
}
|
|
2455
|
+
await runSingle(projectRoot, input);
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
case "init":
|
|
2459
|
+
await initProject(projectRoot);
|
|
2460
|
+
break;
|
|
2461
|
+
case "paw":
|
|
2462
|
+
await handlePawCommand(args.slice(1), projectRoot);
|
|
2463
|
+
break;
|
|
2464
|
+
case "skill":
|
|
2465
|
+
await handleSkillCommand(args.slice(1), projectRoot);
|
|
2466
|
+
break;
|
|
2467
|
+
case "tool":
|
|
2468
|
+
await handleToolCommand(args.slice(1), projectRoot);
|
|
2469
|
+
break;
|
|
2470
|
+
case "task":
|
|
2471
|
+
await handleTaskCommand(args.slice(1), projectRoot);
|
|
2472
|
+
break;
|
|
2473
|
+
case "clawhub":
|
|
2474
|
+
await handleClawHubCommand(args.slice(1), projectRoot);
|
|
2475
|
+
break;
|
|
2476
|
+
case void 0:
|
|
2477
|
+
case "help":
|
|
2478
|
+
case "--help":
|
|
2479
|
+
case "-h":
|
|
2480
|
+
printHelp();
|
|
2481
|
+
break;
|
|
2482
|
+
case "--version":
|
|
2483
|
+
case "-v":
|
|
2484
|
+
logger14.info("openvole v0.1.0");
|
|
2485
|
+
break;
|
|
2486
|
+
default:
|
|
2487
|
+
logger14.error(`Unknown command: ${command}`);
|
|
2488
|
+
printHelp();
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
function printHelp() {
|
|
2493
|
+
logger14.info(`
|
|
2494
|
+
OpenVole \u2014 Micro Agent Core
|
|
2495
|
+
|
|
2496
|
+
Usage:
|
|
2497
|
+
vole init Initialize a new project
|
|
2498
|
+
vole start Start the agent loop (interactive)
|
|
2499
|
+
vole run "<task>" Run a single task
|
|
2500
|
+
|
|
2501
|
+
Paw management:
|
|
2502
|
+
vole paw create <name> Scaffold a new Paw in paws/
|
|
2503
|
+
vole paw list List loaded Paws and their tools
|
|
2504
|
+
vole paw add <name> Install and register a Paw
|
|
2505
|
+
vole paw remove <name> Uninstall and deregister a Paw
|
|
2506
|
+
|
|
2507
|
+
Skill management:
|
|
2508
|
+
vole skill create <name> Scaffold a new Skill in skills/
|
|
2509
|
+
vole skill list List Skills and activation status
|
|
2510
|
+
vole skill add <name> Install and register a Skill
|
|
2511
|
+
vole skill remove <name> Uninstall and deregister a Skill
|
|
2512
|
+
|
|
2513
|
+
Tool management:
|
|
2514
|
+
vole tool list List all registered tools
|
|
2515
|
+
vole tool call <name> [json-params] Call a tool directly (deterministic, no Brain)
|
|
2516
|
+
|
|
2517
|
+
ClawHub (OpenClaw skill registry):
|
|
2518
|
+
vole clawhub install <skill> Install a skill from ClawHub
|
|
2519
|
+
vole clawhub remove <skill> Remove a ClawHub-installed skill
|
|
2520
|
+
vole clawhub search <query> Search for skills on ClawHub
|
|
2521
|
+
|
|
2522
|
+
Task management:
|
|
2523
|
+
vole task list Show task queue
|
|
2524
|
+
vole task cancel <id> Cancel a task
|
|
2525
|
+
|
|
2526
|
+
Options:
|
|
2527
|
+
-h, --help Show this help
|
|
2528
|
+
-v, --version Show version
|
|
2529
|
+
`);
|
|
2530
|
+
}
|
|
2531
|
+
async function startInteractive(projectRoot) {
|
|
2532
|
+
const engine = await createEngine(projectRoot);
|
|
2533
|
+
await engine.start();
|
|
2534
|
+
logger14.info('\nOpenVole is running. Type a task or "exit" to quit.\n');
|
|
2535
|
+
const readline2 = await import("readline");
|
|
2536
|
+
const rl = readline2.createInterface({
|
|
2537
|
+
input: process.stdin,
|
|
2538
|
+
output: process.stdout
|
|
2539
|
+
});
|
|
2540
|
+
const promptUser = () => {
|
|
2541
|
+
rl.question("vole> ", (input) => {
|
|
2542
|
+
const trimmed = input.trim();
|
|
2543
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
2544
|
+
rl.close();
|
|
2545
|
+
engine.shutdown().then(() => process.exit(0));
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (trimmed) {
|
|
2549
|
+
engine.run(trimmed);
|
|
2550
|
+
}
|
|
2551
|
+
promptUser();
|
|
2552
|
+
});
|
|
2553
|
+
};
|
|
2554
|
+
promptUser();
|
|
2555
|
+
const gracefulShutdown = () => {
|
|
2556
|
+
logger14.info("\nShutting down...");
|
|
2557
|
+
rl.close();
|
|
2558
|
+
engine.shutdown().then(() => process.exit(0));
|
|
2559
|
+
};
|
|
2560
|
+
process.on("SIGINT", gracefulShutdown);
|
|
2561
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
2562
|
+
}
|
|
2563
|
+
async function runSingle(projectRoot, input) {
|
|
2564
|
+
const engine = await createEngine(projectRoot);
|
|
2565
|
+
await engine.start();
|
|
2566
|
+
engine.run(input);
|
|
2567
|
+
return new Promise((resolve7) => {
|
|
2568
|
+
engine.bus.on("task:completed", () => {
|
|
2569
|
+
engine.shutdown().then(resolve7);
|
|
2570
|
+
});
|
|
2571
|
+
engine.bus.on("task:failed", () => {
|
|
2572
|
+
engine.shutdown().then(() => {
|
|
2573
|
+
process.exit(1);
|
|
2574
|
+
});
|
|
2575
|
+
});
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
async function initProject(projectRoot) {
|
|
2579
|
+
const fs7 = await import("fs/promises");
|
|
2580
|
+
const configPath = path8.resolve(projectRoot, "vole.config.json");
|
|
2581
|
+
try {
|
|
2582
|
+
await fs7.access(configPath);
|
|
2583
|
+
logger14.error("vole.config.json already exists");
|
|
2584
|
+
return;
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
const config = {
|
|
2588
|
+
paws: [],
|
|
2589
|
+
skills: [],
|
|
2590
|
+
loop: {
|
|
2591
|
+
maxIterations: 10,
|
|
2592
|
+
confirmBeforeAct: true,
|
|
2593
|
+
taskConcurrency: 1,
|
|
2594
|
+
logLevel: "info"
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
await fs7.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2598
|
+
logger14.info("Created vole.config.json");
|
|
2599
|
+
}
|
|
2600
|
+
async function handlePawCommand(args, projectRoot) {
|
|
2601
|
+
const subcommand = args[0];
|
|
2602
|
+
switch (subcommand) {
|
|
2603
|
+
case "create": {
|
|
2604
|
+
const name = args[1];
|
|
2605
|
+
if (!name) {
|
|
2606
|
+
logger14.error("Usage: vole paw create <name>");
|
|
2607
|
+
process.exit(1);
|
|
2608
|
+
}
|
|
2609
|
+
await scaffoldPaw(projectRoot, name);
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
case "list": {
|
|
2613
|
+
const engine = await createEngine(projectRoot);
|
|
2614
|
+
await engine.start();
|
|
2615
|
+
const paws = engine.pawRegistry.list();
|
|
2616
|
+
if (paws.length === 0) {
|
|
2617
|
+
logger14.info("No Paws loaded");
|
|
2618
|
+
} else {
|
|
2619
|
+
logger14.info("PAW TOOLS TYPE HEALTHY");
|
|
2620
|
+
for (const paw of paws) {
|
|
2621
|
+
const tools = engine.toolRegistry.toolsForPaw(paw.name);
|
|
2622
|
+
const type = paw.inProcess ? "in-process" : "subprocess";
|
|
2623
|
+
const healthy = paw.healthy ? "yes" : "NO";
|
|
2624
|
+
logger14.info(
|
|
2625
|
+
`${paw.name.padEnd(24)}${String(tools.length).padEnd(9)}${type.padEnd(13)}${healthy}`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
await engine.shutdown();
|
|
2630
|
+
break;
|
|
2631
|
+
}
|
|
2632
|
+
case "add": {
|
|
2633
|
+
const name = args[1];
|
|
2634
|
+
if (!name) {
|
|
2635
|
+
logger14.error("Usage: vole paw add <name>");
|
|
2636
|
+
process.exit(1);
|
|
2637
|
+
}
|
|
2638
|
+
logger14.info(`Installing ${name}...`);
|
|
2639
|
+
const { execa: execaFn } = await import("execa");
|
|
2640
|
+
await execaFn("npm", ["install", name], { cwd: projectRoot, stdio: "inherit" });
|
|
2641
|
+
const pawPath = resolvePawPath(name, projectRoot);
|
|
2642
|
+
const manifest = await readPawManifest(pawPath);
|
|
2643
|
+
if (manifest) {
|
|
2644
|
+
const defaultAllow = manifest.permissions ? {
|
|
2645
|
+
network: manifest.permissions.network,
|
|
2646
|
+
listen: manifest.permissions.listen,
|
|
2647
|
+
filesystem: manifest.permissions.filesystem,
|
|
2648
|
+
env: manifest.permissions.env
|
|
2649
|
+
} : void 0;
|
|
2650
|
+
await addPawToLock(projectRoot, name, manifest.version, defaultAllow);
|
|
2651
|
+
await addPawToConfig(projectRoot, name, defaultAllow);
|
|
2652
|
+
logger14.info(`Added ${name}@${manifest.version} to vole.config.json`);
|
|
2653
|
+
if (manifest.permissions?.listen?.length) {
|
|
2654
|
+
logger14.info(` listen ports: ${manifest.permissions.listen.join(", ")}`);
|
|
2655
|
+
}
|
|
2656
|
+
if (manifest.tools.length > 0) {
|
|
2657
|
+
logger14.info(` provides ${manifest.tools.length} tools: ${manifest.tools.map((t) => t.name).join(", ")}`);
|
|
2658
|
+
}
|
|
2659
|
+
} else {
|
|
2660
|
+
logger14.error(`Installed ${name} but could not read vole-paw.json \u2014 add it to vole.config.json manually`);
|
|
2661
|
+
}
|
|
2662
|
+
break;
|
|
2663
|
+
}
|
|
2664
|
+
case "remove": {
|
|
2665
|
+
const name = args[1];
|
|
2666
|
+
if (!name) {
|
|
2667
|
+
logger14.error("Usage: vole paw remove <name>");
|
|
2668
|
+
process.exit(1);
|
|
2669
|
+
}
|
|
2670
|
+
const { execa: execaFn } = await import("execa");
|
|
2671
|
+
await execaFn("npm", ["uninstall", name], { cwd: projectRoot, stdio: "inherit" });
|
|
2672
|
+
await removePawFromLock(projectRoot, name);
|
|
2673
|
+
await removePawFromConfig(projectRoot, name);
|
|
2674
|
+
logger14.info(`Removed ${name} from vole.config.json`);
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
default:
|
|
2678
|
+
logger14.error(`Unknown paw command: ${subcommand}`);
|
|
2679
|
+
logger14.info("Available: list, add, remove");
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
async function handleSkillCommand(args, projectRoot) {
|
|
2684
|
+
const subcommand = args[0];
|
|
2685
|
+
switch (subcommand) {
|
|
2686
|
+
case "create": {
|
|
2687
|
+
const name = args[1];
|
|
2688
|
+
if (!name) {
|
|
2689
|
+
logger14.error("Usage: vole skill create <name>");
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
await scaffoldSkill(projectRoot, name);
|
|
2693
|
+
break;
|
|
2694
|
+
}
|
|
2695
|
+
case "list": {
|
|
2696
|
+
const engine = await createEngine(projectRoot);
|
|
2697
|
+
await engine.start();
|
|
2698
|
+
const skills = engine.skillRegistry.list();
|
|
2699
|
+
if (skills.length === 0) {
|
|
2700
|
+
logger14.info("No Skills loaded");
|
|
2701
|
+
} else {
|
|
2702
|
+
logger14.info("SKILL STATUS MISSING TOOLS");
|
|
2703
|
+
for (const skill of skills) {
|
|
2704
|
+
const status = skill.active ? "active" : "inactive";
|
|
2705
|
+
const missing = skill.missingTools.length > 0 ? skill.missingTools.join(", ") : "\u2014";
|
|
2706
|
+
logger14.info(
|
|
2707
|
+
`${skill.name.padEnd(31)}${status.padEnd(11)}${missing}`
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
await engine.shutdown();
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
case "add": {
|
|
2715
|
+
const name = args[1];
|
|
2716
|
+
if (!name) {
|
|
2717
|
+
logger14.error("Usage: vole skill add <path-to-skill>");
|
|
2718
|
+
process.exit(1);
|
|
2719
|
+
}
|
|
2720
|
+
const skillPath = path8.resolve(projectRoot, name);
|
|
2721
|
+
const { loadSkillFromDirectory: loadSkillFromDirectory2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
2722
|
+
const definition = await loadSkillFromDirectory2(skillPath);
|
|
2723
|
+
if (!definition) {
|
|
2724
|
+
logger14.error(`No valid SKILL.md found at ${skillPath}`);
|
|
2725
|
+
process.exit(1);
|
|
2726
|
+
}
|
|
2727
|
+
await addSkillToLock(projectRoot, name, definition.version ?? "0.0.0");
|
|
2728
|
+
await addSkillToConfig(projectRoot, name);
|
|
2729
|
+
logger14.info(`Added "${definition.name}" to vole.config.json`);
|
|
2730
|
+
if (definition.requiredTools.length > 0) {
|
|
2731
|
+
logger14.info(` requires tools: ${definition.requiredTools.join(", ")}`);
|
|
2732
|
+
}
|
|
2733
|
+
if (definition.requires?.env.length) {
|
|
2734
|
+
logger14.info(` requires env: ${definition.requires.env.join(", ")}`);
|
|
2735
|
+
}
|
|
2736
|
+
break;
|
|
2737
|
+
}
|
|
2738
|
+
case "remove": {
|
|
2739
|
+
const name = args[1];
|
|
2740
|
+
if (!name) {
|
|
2741
|
+
logger14.error("Usage: vole skill remove <name>");
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
await removeSkillFromLock(projectRoot, name);
|
|
2745
|
+
await removeSkillFromConfig(projectRoot, name);
|
|
2746
|
+
const fsModule = await import("fs/promises");
|
|
2747
|
+
const skillPath = name.startsWith(".") || name.startsWith("/") ? path8.resolve(projectRoot, name) : path8.resolve(projectRoot, ".openvole", "skills", name);
|
|
2748
|
+
try {
|
|
2749
|
+
await fsModule.rm(skillPath, { recursive: true });
|
|
2750
|
+
logger14.info(`Deleted ${skillPath}`);
|
|
2751
|
+
} catch {
|
|
2752
|
+
}
|
|
2753
|
+
logger14.info(`Removed "${name}" from vole.config.json`);
|
|
2754
|
+
break;
|
|
2755
|
+
}
|
|
2756
|
+
default:
|
|
2757
|
+
logger14.error(`Unknown skill command: ${subcommand}`);
|
|
2758
|
+
logger14.info("Available: list, add, remove");
|
|
2759
|
+
process.exit(1);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
async function handleToolCommand(args, projectRoot) {
|
|
2763
|
+
const subcommand = args[0];
|
|
2764
|
+
switch (subcommand) {
|
|
2765
|
+
case "list": {
|
|
2766
|
+
const config = await (await Promise.resolve().then(() => (init_config(), config_exports))).loadConfig(
|
|
2767
|
+
path8.resolve(projectRoot, "vole.config.json")
|
|
2768
|
+
);
|
|
2769
|
+
const tools = [];
|
|
2770
|
+
const { createMessageBus: createMessageBus2 } = await Promise.resolve().then(() => (init_bus(), bus_exports));
|
|
2771
|
+
const { ToolRegistry: ToolRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
2772
|
+
const { SchedulerStore: SchedulerStore2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
|
|
2773
|
+
const { TaskQueue: TaskQueue2 } = await Promise.resolve().then(() => (init_task(), task_exports));
|
|
2774
|
+
const { SkillRegistry: SkillRegistry2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports2));
|
|
2775
|
+
const { createCoreTools: createCoreTools2 } = await Promise.resolve().then(() => (init_core_tools(), core_tools_exports));
|
|
2776
|
+
const bus = createMessageBus2();
|
|
2777
|
+
const toolRegistry = new ToolRegistry2(bus);
|
|
2778
|
+
const skillRegistry = new SkillRegistry2(bus, toolRegistry, projectRoot);
|
|
2779
|
+
const taskQueue = new TaskQueue2(bus, 1);
|
|
2780
|
+
const scheduler = new SchedulerStore2();
|
|
2781
|
+
const coreTools = createCoreTools2(scheduler, taskQueue, projectRoot, skillRegistry);
|
|
2782
|
+
toolRegistry.register("__core__", coreTools, true);
|
|
2783
|
+
for (const entry of toolRegistry.list()) {
|
|
2784
|
+
tools.push({ name: entry.name, pawName: entry.pawName, type: "in-process" });
|
|
2785
|
+
}
|
|
2786
|
+
const { normalizePawConfig: normalizePawConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2787
|
+
const { readPawManifest: readPawManifest2, resolvePawPath: resolvePawPath2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
|
|
2788
|
+
for (const pawEntry of config.paws) {
|
|
2789
|
+
const pawConfig = normalizePawConfig2(pawEntry);
|
|
2790
|
+
const pawPath = resolvePawPath2(pawConfig.name, projectRoot);
|
|
2791
|
+
const manifest = await readPawManifest2(pawPath);
|
|
2792
|
+
if (manifest?.tools) {
|
|
2793
|
+
for (const t of manifest.tools) {
|
|
2794
|
+
tools.push({
|
|
2795
|
+
name: t.name,
|
|
2796
|
+
pawName: pawConfig.name,
|
|
2797
|
+
type: manifest.inProcess ? "in-process" : "subprocess"
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (tools.length === 0) {
|
|
2803
|
+
logger14.info("No tools registered");
|
|
2804
|
+
} else {
|
|
2805
|
+
logger14.info("TOOL PAW TYPE");
|
|
2806
|
+
for (const tool of tools) {
|
|
2807
|
+
logger14.info(
|
|
2808
|
+
`${tool.name.padEnd(21)}${tool.pawName.padEnd(23)}${tool.type}`
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
break;
|
|
2813
|
+
}
|
|
2814
|
+
case "call": {
|
|
2815
|
+
const toolName = args[1];
|
|
2816
|
+
const paramsJson = args[2];
|
|
2817
|
+
if (!toolName) {
|
|
2818
|
+
logger14.error("Usage: vole tool call <tool-name> [json-params]");
|
|
2819
|
+
process.exit(1);
|
|
2820
|
+
}
|
|
2821
|
+
let params = {};
|
|
2822
|
+
if (paramsJson) {
|
|
2823
|
+
try {
|
|
2824
|
+
params = JSON.parse(paramsJson);
|
|
2825
|
+
} catch {
|
|
2826
|
+
logger14.error("Invalid JSON params");
|
|
2827
|
+
process.exit(1);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
const engine = await createEngine(projectRoot);
|
|
2831
|
+
await engine.start();
|
|
2832
|
+
const tool = engine.toolRegistry.get(toolName);
|
|
2833
|
+
if (!tool) {
|
|
2834
|
+
logger14.error(`Tool "${toolName}" not found`);
|
|
2835
|
+
logger14.info('Run "vole tool list" to see available tools');
|
|
2836
|
+
await engine.shutdown();
|
|
2837
|
+
process.exit(1);
|
|
2838
|
+
}
|
|
2839
|
+
try {
|
|
2840
|
+
if (tool.parameters && typeof tool.parameters.parse === "function") {
|
|
2841
|
+
tool.parameters.parse(params);
|
|
2842
|
+
}
|
|
2843
|
+
const result = await tool.execute(params);
|
|
2844
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2845
|
+
} catch (err) {
|
|
2846
|
+
logger14.error(`Tool execution failed: ${err instanceof Error ? err.message : err}`);
|
|
2847
|
+
process.exit(1);
|
|
2848
|
+
}
|
|
2849
|
+
await engine.shutdown();
|
|
2850
|
+
break;
|
|
2851
|
+
}
|
|
2852
|
+
default:
|
|
2853
|
+
logger14.error(`Unknown tool command: ${subcommand}`);
|
|
2854
|
+
logger14.info("Available: list, call");
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
async function handleTaskCommand(args, _projectRoot) {
|
|
2859
|
+
const subcommand = args[0];
|
|
2860
|
+
switch (subcommand) {
|
|
2861
|
+
case "list":
|
|
2862
|
+
logger14.info("Task list requires a running vole instance.");
|
|
2863
|
+
break;
|
|
2864
|
+
case "cancel": {
|
|
2865
|
+
const id = args[1];
|
|
2866
|
+
if (!id) {
|
|
2867
|
+
logger14.error("Usage: vole task cancel <id>");
|
|
2868
|
+
process.exit(1);
|
|
2869
|
+
}
|
|
2870
|
+
logger14.info("Task cancellation requires a running vole instance.");
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
default:
|
|
2874
|
+
logger14.error(`Unknown task command: ${subcommand}`);
|
|
2875
|
+
logger14.info("Available: list, cancel");
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
async function ask(question) {
|
|
2880
|
+
const rl = (await import("readline")).createInterface({
|
|
2881
|
+
input: process.stdin,
|
|
2882
|
+
output: process.stdout
|
|
2883
|
+
});
|
|
2884
|
+
return new Promise((resolve7) => {
|
|
2885
|
+
rl.question(question, (answer) => {
|
|
2886
|
+
rl.close();
|
|
2887
|
+
resolve7(answer.trim());
|
|
2888
|
+
});
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
async function confirm(question) {
|
|
2892
|
+
const answer = await ask(`${question} [y/N] `);
|
|
2893
|
+
return answer.toLowerCase() === "y";
|
|
2894
|
+
}
|
|
2895
|
+
async function askList(question) {
|
|
2896
|
+
const answer = await ask(question);
|
|
2897
|
+
if (!answer) return [];
|
|
2898
|
+
return answer.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2899
|
+
}
|
|
2900
|
+
async function scaffoldPaw(projectRoot, name) {
|
|
2901
|
+
const fs7 = await import("fs/promises");
|
|
2902
|
+
const pawName = name.startsWith("paw-") ? name : `paw-${name}`;
|
|
2903
|
+
const pawDir = path8.resolve(projectRoot, "paws", pawName);
|
|
2904
|
+
try {
|
|
2905
|
+
await fs7.access(pawDir);
|
|
2906
|
+
logger14.error(`Directory paws/${pawName} already exists`);
|
|
2907
|
+
process.exit(1);
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
logger14.info(`
|
|
2911
|
+
Creating Paw: ${pawName}
|
|
2912
|
+
`);
|
|
2913
|
+
const description = await ask("Description: ");
|
|
2914
|
+
const tools = [];
|
|
2915
|
+
logger14.info("\nTools are actions the agent can perform (e.g., send_email, search_docs).");
|
|
2916
|
+
if (await confirm("Add tools?")) {
|
|
2917
|
+
let addMore = true;
|
|
2918
|
+
while (addMore) {
|
|
2919
|
+
const toolName = await ask(" Tool name (e.g., send_message): ");
|
|
2920
|
+
if (!toolName) break;
|
|
2921
|
+
const toolDesc = await ask(" Tool description: ");
|
|
2922
|
+
tools.push({ name: toolName, description: toolDesc });
|
|
2923
|
+
addMore = await confirm(" Add another tool?");
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
logger14.info("\nHooks let your Paw react to agent activity automatically.");
|
|
2927
|
+
const wantObserve = await confirm("Log every tool execution? (observe hook)");
|
|
2928
|
+
const wantPerceive = await confirm("Inject context before the agent thinks? (perceive hook)");
|
|
2929
|
+
logger14.info("\nPermissions control what this Paw can access.");
|
|
2930
|
+
const networkDomains = await askList("Network domains (comma-separated, e.g., api.telegram.org): ");
|
|
2931
|
+
const listenPorts = (await askList("Ports to listen on (comma-separated, e.g., 3000): ")).map(Number).filter((n) => !Number.isNaN(n));
|
|
2932
|
+
const envVars = await askList("Env variables needed (comma-separated, e.g., TELEGRAM_TOKEN): ");
|
|
2933
|
+
logger14.info("");
|
|
2934
|
+
await fs7.mkdir(path8.join(pawDir, "src"), { recursive: true });
|
|
2935
|
+
await fs7.writeFile(
|
|
2936
|
+
path8.join(pawDir, "vole-paw.json"),
|
|
2937
|
+
JSON.stringify(
|
|
2938
|
+
{
|
|
2939
|
+
name: pawName,
|
|
2940
|
+
version: "0.1.0",
|
|
2941
|
+
description,
|
|
2942
|
+
entry: "./dist/index.js",
|
|
2943
|
+
brain: false,
|
|
2944
|
+
inProcess: false,
|
|
2945
|
+
transport: "ipc",
|
|
2946
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description })),
|
|
2947
|
+
permissions: {
|
|
2948
|
+
network: networkDomains,
|
|
2949
|
+
listen: listenPorts,
|
|
2950
|
+
filesystem: [],
|
|
2951
|
+
env: envVars
|
|
2952
|
+
}
|
|
2953
|
+
},
|
|
2954
|
+
null,
|
|
2955
|
+
2
|
|
2956
|
+
) + "\n"
|
|
2957
|
+
);
|
|
2958
|
+
await fs7.writeFile(
|
|
2959
|
+
path8.join(pawDir, "package.json"),
|
|
2960
|
+
JSON.stringify(
|
|
2961
|
+
{
|
|
2962
|
+
name: pawName,
|
|
2963
|
+
version: "0.1.0",
|
|
2964
|
+
description,
|
|
2965
|
+
type: "module",
|
|
2966
|
+
main: "./dist/index.js",
|
|
2967
|
+
scripts: {
|
|
2968
|
+
build: "tsup",
|
|
2969
|
+
typecheck: "tsc --noEmit"
|
|
2970
|
+
},
|
|
2971
|
+
dependencies: {
|
|
2972
|
+
"@openvole/paw-sdk": "workspace:*"
|
|
2973
|
+
},
|
|
2974
|
+
devDependencies: {
|
|
2975
|
+
"@types/node": "^22.0.0",
|
|
2976
|
+
tsup: "^8.3.0",
|
|
2977
|
+
typescript: "^5.6.0"
|
|
2978
|
+
}
|
|
2979
|
+
},
|
|
2980
|
+
null,
|
|
2981
|
+
2
|
|
2982
|
+
) + "\n"
|
|
2983
|
+
);
|
|
2984
|
+
await fs7.writeFile(
|
|
2985
|
+
path8.join(pawDir, "tsconfig.json"),
|
|
2986
|
+
JSON.stringify(
|
|
2987
|
+
{
|
|
2988
|
+
extends: "../../tsconfig.base.json",
|
|
2989
|
+
compilerOptions: { outDir: "./dist", rootDir: "./src" },
|
|
2990
|
+
include: ["src/**/*.ts"]
|
|
2991
|
+
},
|
|
2992
|
+
null,
|
|
2993
|
+
2
|
|
2994
|
+
) + "\n"
|
|
2995
|
+
);
|
|
2996
|
+
await fs7.writeFile(
|
|
2997
|
+
path8.join(pawDir, "tsup.config.ts"),
|
|
2998
|
+
`import { defineConfig } from 'tsup'
|
|
2999
|
+
|
|
3000
|
+
export default defineConfig({
|
|
3001
|
+
entry: ['src/index.ts'],
|
|
3002
|
+
format: ['esm'],
|
|
3003
|
+
dts: true,
|
|
3004
|
+
clean: true,
|
|
3005
|
+
sourcemap: true,
|
|
3006
|
+
target: 'node20',
|
|
3007
|
+
splitting: false,
|
|
3008
|
+
})
|
|
3009
|
+
`
|
|
3010
|
+
);
|
|
3011
|
+
await fs7.writeFile(
|
|
3012
|
+
path8.join(pawDir, "src", "index.ts"),
|
|
3013
|
+
`import { definePaw } from '@openvole/paw-sdk'
|
|
3014
|
+
import { paw } from './paw.js'
|
|
3015
|
+
|
|
3016
|
+
export default definePaw(paw)
|
|
3017
|
+
`
|
|
3018
|
+
);
|
|
3019
|
+
const toolsCode = tools.length > 0 ? tools.map((t) => ` {
|
|
3020
|
+
name: '${t.name}',
|
|
3021
|
+
description: '${t.description.replace(/'/g, "\\'")}',
|
|
3022
|
+
parameters: z.object({
|
|
3023
|
+
// Define your parameters here
|
|
3024
|
+
}),
|
|
3025
|
+
async execute(params) {
|
|
3026
|
+
// TODO: implement ${t.name}
|
|
3027
|
+
throw new Error('Not implemented')
|
|
3028
|
+
},
|
|
3029
|
+
},`).join("\n") : "";
|
|
3030
|
+
let hooksCode = "";
|
|
3031
|
+
if (wantObserve || wantPerceive) {
|
|
3032
|
+
const hookParts = [];
|
|
3033
|
+
if (wantObserve) {
|
|
3034
|
+
hookParts.push(` onObserve: async (result) => {
|
|
3035
|
+
const status = result.success ? 'OK' : 'FAIL'
|
|
3036
|
+
console.log(\`[${pawName}] \${result.toolName} \u2192 \${status} (\${result.durationMs}ms)\`)
|
|
3037
|
+
},`);
|
|
3038
|
+
}
|
|
3039
|
+
if (wantPerceive) {
|
|
3040
|
+
hookParts.push(` onPerceive: async (context) => {
|
|
3041
|
+
// Add data to context.metadata before the agent thinks
|
|
3042
|
+
// context.metadata.myData = { ... }
|
|
3043
|
+
return context
|
|
3044
|
+
},`);
|
|
3045
|
+
}
|
|
3046
|
+
hooksCode = `
|
|
3047
|
+
hooks: {
|
|
3048
|
+
${hookParts.join("\n")}
|
|
3049
|
+
},
|
|
3050
|
+
`;
|
|
3051
|
+
}
|
|
3052
|
+
await fs7.writeFile(
|
|
3053
|
+
path8.join(pawDir, "src", "paw.ts"),
|
|
3054
|
+
`import { z, type PawDefinition } from '@openvole/paw-sdk'
|
|
3055
|
+
|
|
3056
|
+
export const paw: PawDefinition = {
|
|
3057
|
+
name: '${pawName}',
|
|
3058
|
+
version: '0.1.0',
|
|
3059
|
+
description: '${description.replace(/'/g, "\\'")}',
|
|
3060
|
+
|
|
3061
|
+
tools: [
|
|
3062
|
+
${toolsCode}
|
|
3063
|
+
],
|
|
3064
|
+
${hooksCode}
|
|
3065
|
+
async onLoad() {
|
|
3066
|
+
console.log('[${pawName}] loaded')
|
|
3067
|
+
},
|
|
3068
|
+
|
|
3069
|
+
async onUnload() {
|
|
3070
|
+
console.log('[${pawName}] unloaded')
|
|
3071
|
+
},
|
|
3072
|
+
}
|
|
3073
|
+
`
|
|
3074
|
+
);
|
|
3075
|
+
const allow = {};
|
|
3076
|
+
if (networkDomains.length > 0) allow.network = networkDomains;
|
|
3077
|
+
if (listenPorts.length > 0) allow.listen = listenPorts;
|
|
3078
|
+
if (envVars.length > 0) allow.env = envVars;
|
|
3079
|
+
await addPawToLock(
|
|
3080
|
+
projectRoot,
|
|
3081
|
+
`./paws/${pawName}`,
|
|
3082
|
+
"0.1.0",
|
|
3083
|
+
Object.keys(allow).length > 0 ? allow : void 0
|
|
3084
|
+
);
|
|
3085
|
+
logger14.info(`Created paws/${pawName}/`);
|
|
3086
|
+
logger14.info(`Registered in vole.lock.json`);
|
|
3087
|
+
if (tools.length > 0) {
|
|
3088
|
+
logger14.info(`Generated ${tools.length} tool${tools.length > 1 ? "s" : ""}: ${tools.map((t) => t.name).join(", ")}`);
|
|
3089
|
+
}
|
|
3090
|
+
logger14.info("");
|
|
3091
|
+
logger14.info("Next: implement your tool logic in src/paw.ts, then build:");
|
|
3092
|
+
logger14.info(" pnpm install && pnpm build");
|
|
3093
|
+
}
|
|
3094
|
+
async function scaffoldSkill(projectRoot, name) {
|
|
3095
|
+
const fs7 = await import("fs/promises");
|
|
3096
|
+
const skillName = name.startsWith("skill-") ? name : `skill-${name}`;
|
|
3097
|
+
const skillDir = path8.resolve(projectRoot, ".openvole", "skills", skillName);
|
|
3098
|
+
try {
|
|
3099
|
+
await fs7.access(skillDir);
|
|
3100
|
+
logger14.error(`Skill "${skillName}" already exists`);
|
|
3101
|
+
process.exit(1);
|
|
3102
|
+
} catch {
|
|
3103
|
+
}
|
|
3104
|
+
logger14.info(`
|
|
3105
|
+
Creating Skill: ${skillName}
|
|
3106
|
+
`);
|
|
3107
|
+
const description = await ask("Description: ");
|
|
3108
|
+
logger14.info("\nSkills describe behavior \u2014 what the agent should do, using tools provided by Paws.");
|
|
3109
|
+
const requiredTools = await askList("Required tools (comma-separated, e.g., email_search, email_send): ");
|
|
3110
|
+
const optionalTools = await askList("Optional tools (comma-separated, or empty): ");
|
|
3111
|
+
const tags = await askList("Tags (comma-separated, e.g., email, productivity): ");
|
|
3112
|
+
logger14.info("");
|
|
3113
|
+
const instructions = await ask("Instructions (what should the agent do?): ");
|
|
3114
|
+
logger14.info("");
|
|
3115
|
+
await fs7.mkdir(skillDir, { recursive: true });
|
|
3116
|
+
const frontmatterLines = [
|
|
3117
|
+
`name: ${skillName}`,
|
|
3118
|
+
`description: "${description.replace(/"/g, '\\"')}"`
|
|
3119
|
+
];
|
|
3120
|
+
if (requiredTools.length > 0) {
|
|
3121
|
+
frontmatterLines.push("requiredTools:");
|
|
3122
|
+
for (const t of requiredTools) {
|
|
3123
|
+
frontmatterLines.push(` - ${t}`);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (optionalTools.length > 0) {
|
|
3127
|
+
frontmatterLines.push("optionalTools:");
|
|
3128
|
+
for (const t of optionalTools) {
|
|
3129
|
+
frontmatterLines.push(` - ${t}`);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (tags.length > 0) {
|
|
3133
|
+
frontmatterLines.push("tags:");
|
|
3134
|
+
for (const t of tags) {
|
|
3135
|
+
frontmatterLines.push(` - ${t}`);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
const skillMd = `---
|
|
3139
|
+
${frontmatterLines.join("\n")}
|
|
3140
|
+
---
|
|
3141
|
+
|
|
3142
|
+
# ${skillName}
|
|
3143
|
+
|
|
3144
|
+
${instructions}
|
|
3145
|
+
`;
|
|
3146
|
+
await fs7.writeFile(path8.join(skillDir, "SKILL.md"), skillMd);
|
|
3147
|
+
await addSkillToLock(projectRoot, skillName, "0.1.0");
|
|
3148
|
+
await addSkillToConfig(projectRoot, skillName);
|
|
3149
|
+
logger14.info(`Created .openvole/skills/${skillName}/`);
|
|
3150
|
+
logger14.info(` SKILL.md \u2014 edit to refine instructions`);
|
|
3151
|
+
logger14.info(`Added to vole.config.json`);
|
|
3152
|
+
if (requiredTools.length > 0) {
|
|
3153
|
+
logger14.info(`Requires tools: ${requiredTools.join(", ")}`);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
async function handleClawHubCommand(args, projectRoot) {
|
|
3157
|
+
const subcommand = args[0];
|
|
3158
|
+
switch (subcommand) {
|
|
3159
|
+
case "install": {
|
|
3160
|
+
const skillName = args[1];
|
|
3161
|
+
if (!skillName) {
|
|
3162
|
+
logger14.error("Usage: vole clawhub install <skill-name>");
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
}
|
|
3165
|
+
const fsModule = await import("fs/promises");
|
|
3166
|
+
const clawHubDir = path8.resolve(projectRoot, ".openvole", "skills", "clawhub");
|
|
3167
|
+
await fsModule.mkdir(clawHubDir, { recursive: true });
|
|
3168
|
+
logger14.info(`Installing "${skillName}" from ClawHub...`);
|
|
3169
|
+
const { execa: execaFn } = await import("execa");
|
|
3170
|
+
try {
|
|
3171
|
+
await execaFn("npx", ["clawhub", "install", skillName, "--dir", clawHubDir], {
|
|
3172
|
+
cwd: projectRoot,
|
|
3173
|
+
stdio: "inherit"
|
|
3174
|
+
});
|
|
3175
|
+
} catch {
|
|
3176
|
+
logger14.error(`Failed to install "${skillName}" from ClawHub`);
|
|
3177
|
+
process.exit(1);
|
|
3178
|
+
}
|
|
3179
|
+
const installed = await fsModule.readdir(clawHubDir).catch(() => []);
|
|
3180
|
+
const skillDir = installed.find((d) => d === skillName || d.includes(skillName));
|
|
3181
|
+
if (!skillDir) {
|
|
3182
|
+
logger14.error(`Skill installed but directory not found in .openvole/skills/clawhub/`);
|
|
3183
|
+
process.exit(1);
|
|
3184
|
+
}
|
|
3185
|
+
const localPath = `clawhub/${skillDir}`;
|
|
3186
|
+
const { loadSkillFromDirectory: loadSkillFromDirectory2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
3187
|
+
const definition = await loadSkillFromDirectory2(path8.resolve(projectRoot, ".openvole", "skills", "clawhub", skillDir));
|
|
3188
|
+
if (definition) {
|
|
3189
|
+
await addSkillToLock(projectRoot, localPath, definition.version ?? "0.0.0");
|
|
3190
|
+
await addSkillToConfig(projectRoot, localPath);
|
|
3191
|
+
logger14.info(`Added "${definition.name}" to vole.config.json`);
|
|
3192
|
+
if (definition.requiredTools.length > 0) {
|
|
3193
|
+
logger14.info(` requires tools: ${definition.requiredTools.join(", ")}`);
|
|
3194
|
+
}
|
|
3195
|
+
if (definition.requires?.env.length) {
|
|
3196
|
+
logger14.info(` requires env: ${definition.requires.env.join(", ")}`);
|
|
3197
|
+
}
|
|
3198
|
+
} else {
|
|
3199
|
+
logger14.warn(`Installed "${skillName}" but could not parse SKILL.md \u2014 add to vole.config.json manually`);
|
|
3200
|
+
}
|
|
3201
|
+
break;
|
|
3202
|
+
}
|
|
3203
|
+
case "search": {
|
|
3204
|
+
const query = args.slice(1).join(" ");
|
|
3205
|
+
if (!query) {
|
|
3206
|
+
logger14.error("Usage: vole clawhub search <query>");
|
|
3207
|
+
process.exit(1);
|
|
3208
|
+
}
|
|
3209
|
+
const { execa: execaFn } = await import("execa");
|
|
3210
|
+
try {
|
|
3211
|
+
await execaFn("npx", ["clawhub", "search", query], {
|
|
3212
|
+
cwd: projectRoot,
|
|
3213
|
+
stdio: "inherit"
|
|
3214
|
+
});
|
|
3215
|
+
} catch {
|
|
3216
|
+
logger14.error("Search failed \u2014 make sure clawhub is available (npx clawhub)");
|
|
3217
|
+
}
|
|
3218
|
+
break;
|
|
3219
|
+
}
|
|
3220
|
+
case "remove": {
|
|
3221
|
+
const skillName = args[1];
|
|
3222
|
+
if (!skillName) {
|
|
3223
|
+
logger14.error("Usage: vole clawhub remove <skill-name>");
|
|
3224
|
+
process.exit(1);
|
|
3225
|
+
}
|
|
3226
|
+
const fsModule = await import("fs/promises");
|
|
3227
|
+
const skillPath = path8.resolve(projectRoot, ".openvole", "skills", "clawhub", skillName);
|
|
3228
|
+
try {
|
|
3229
|
+
await fsModule.rm(skillPath, { recursive: true });
|
|
3230
|
+
logger14.info(`Deleted .openvole/skills/clawhub/${skillName}`);
|
|
3231
|
+
} catch {
|
|
3232
|
+
logger14.error(`Skill directory not found: .openvole/skills/clawhub/${skillName}`);
|
|
3233
|
+
process.exit(1);
|
|
3234
|
+
}
|
|
3235
|
+
await removeSkillFromLock(projectRoot, `clawhub/${skillName}`);
|
|
3236
|
+
await removeSkillFromConfig(projectRoot, `clawhub/${skillName}`);
|
|
3237
|
+
logger14.info(`Removed "${skillName}" from vole.config.json`);
|
|
3238
|
+
break;
|
|
3239
|
+
}
|
|
3240
|
+
default:
|
|
3241
|
+
logger14.error(`Unknown clawhub command: ${subcommand}`);
|
|
3242
|
+
logger14.info("Available: install, remove, search");
|
|
3243
|
+
process.exit(1);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
main().catch((err) => {
|
|
3247
|
+
console.error("Fatal error:", err);
|
|
3248
|
+
process.exit(1);
|
|
3249
|
+
});
|
|
3250
|
+
//# sourceMappingURL=cli.js.map
|