@sesamespace/hivemind 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-GXNDXM5K.js → chunk-ELFD4Y4W.js} +2 -2
- package/dist/{chunk-EQRUGDTR.js → chunk-ERR5JR42.js} +2 -2
- package/dist/{chunk-JLI4IPKA.js → chunk-TL4GV2TJ.js} +3 -3
- package/dist/{chunk-KL44NCSP.js → chunk-WAX2THXK.js} +2 -2
- package/dist/{chunk-G6PPTVAS.js → chunk-WSLVHVNP.js} +2650 -207
- package/dist/chunk-WSLVHVNP.js.map +1 -0
- package/dist/commands/fleet.js +3 -3
- package/dist/commands/start.js +3 -3
- package/dist/commands/watchdog.js +3 -3
- package/dist/index.js +4 -2
- package/dist/main.js +5 -5
- package/dist/start.js +1 -1
- package/install.sh +120 -0
- package/package.json +25 -16
- package/packages/memory/Cargo.lock +6480 -0
- package/packages/memory/Cargo.toml +21 -0
- package/packages/memory/src/src/context.rs +179 -0
- package/packages/memory/src/src/embeddings.rs +51 -0
- package/packages/memory/src/src/main.rs +626 -0
- package/packages/memory/src/src/promotion.rs +637 -0
- package/packages/memory/src/src/scoring.rs +131 -0
- package/packages/memory/src/src/store.rs +460 -0
- package/packages/memory/src/src/tasks.rs +321 -0
- package/.pnpmrc.json +0 -1
- package/DASHBOARD-PLAN.md +0 -206
- package/TOOL-USE-DESIGN.md +0 -173
- package/dist/chunk-G6PPTVAS.js.map +0 -1
- /package/dist/{chunk-GXNDXM5K.js.map → chunk-ELFD4Y4W.js.map} +0 -0
- /package/dist/{chunk-EQRUGDTR.js.map → chunk-ERR5JR42.js.map} +0 -0
- /package/dist/{chunk-JLI4IPKA.js.map → chunk-TL4GV2TJ.js.map} +0 -0
- /package/dist/{chunk-KL44NCSP.js.map → chunk-WAX2THXK.js.map} +0 -0
|
@@ -753,10 +753,37 @@ ${memoryFiles.context}
|
|
|
753
753
|
}
|
|
754
754
|
};
|
|
755
755
|
}
|
|
756
|
-
function
|
|
756
|
+
function estimateTokens(text) {
|
|
757
|
+
return Math.ceil(text.length / 4);
|
|
758
|
+
}
|
|
759
|
+
function buildMessages(systemPrompt, conversationHistory, currentMessage, contextLimit = 2e5, reserveForResponse = 4096) {
|
|
760
|
+
const budget = contextLimit - reserveForResponse;
|
|
761
|
+
const systemTokens = estimateTokens(systemPrompt);
|
|
762
|
+
const userTokens = estimateTokens(currentMessage);
|
|
763
|
+
const fixedTokens = systemTokens + userTokens;
|
|
764
|
+
if (fixedTokens >= budget) {
|
|
765
|
+
console.warn(`[prompt] System prompt (${systemTokens} est. tokens) + user message (${userTokens}) exceeds budget (${budget}). Sending without history.`);
|
|
766
|
+
return [
|
|
767
|
+
{ role: "system", content: systemPrompt },
|
|
768
|
+
{ role: "user", content: currentMessage }
|
|
769
|
+
];
|
|
770
|
+
}
|
|
771
|
+
let remainingBudget = budget - fixedTokens;
|
|
772
|
+
const fittingHistory = [];
|
|
773
|
+
for (let i = conversationHistory.length - 1; i >= 0; i--) {
|
|
774
|
+
const msg = conversationHistory[i];
|
|
775
|
+
const msgTokens = estimateTokens(msg.content ?? "");
|
|
776
|
+
if (msgTokens > remainingBudget) break;
|
|
777
|
+
fittingHistory.unshift(msg);
|
|
778
|
+
remainingBudget -= msgTokens;
|
|
779
|
+
}
|
|
780
|
+
if (fittingHistory.length < conversationHistory.length) {
|
|
781
|
+
const dropped = conversationHistory.length - fittingHistory.length;
|
|
782
|
+
console.log(`[prompt] Token budget: dropped ${dropped} oldest L1 turns (kept ${fittingHistory.length}/${conversationHistory.length})`);
|
|
783
|
+
}
|
|
757
784
|
return [
|
|
758
785
|
{ role: "system", content: systemPrompt },
|
|
759
|
-
...
|
|
786
|
+
...fittingHistory,
|
|
760
787
|
{ role: "user", content: currentMessage }
|
|
761
788
|
];
|
|
762
789
|
}
|
|
@@ -873,18 +900,18 @@ var SessionStore = class {
|
|
|
873
900
|
return content.split("\n").length;
|
|
874
901
|
}
|
|
875
902
|
};
|
|
876
|
-
function
|
|
903
|
+
function estimateTokens2(text) {
|
|
877
904
|
if (!text) return 0;
|
|
878
905
|
return Math.ceil(text.length / 4);
|
|
879
906
|
}
|
|
880
907
|
function estimateMessageTokens(messages) {
|
|
881
908
|
let total = 0;
|
|
882
909
|
for (const msg of messages) {
|
|
883
|
-
if (msg.content) total +=
|
|
910
|
+
if (msg.content) total += estimateTokens2(msg.content);
|
|
884
911
|
if (msg.tool_calls) {
|
|
885
912
|
for (const call of msg.tool_calls) {
|
|
886
|
-
total +=
|
|
887
|
-
total +=
|
|
913
|
+
total += estimateTokens2(call.function.name);
|
|
914
|
+
total += estimateTokens2(call.function.arguments);
|
|
888
915
|
}
|
|
889
916
|
}
|
|
890
917
|
}
|
|
@@ -1119,7 +1146,7 @@ var Agent = class {
|
|
|
1119
1146
|
l3Knowledge,
|
|
1120
1147
|
dataDir: this.dataDir
|
|
1121
1148
|
});
|
|
1122
|
-
const systemPromptTokens =
|
|
1149
|
+
const systemPromptTokens = estimateTokens2(systemPromptResult.text);
|
|
1123
1150
|
if (this.compactionManager.shouldCompact(conversationHistory, systemPromptTokens, this.config.llm.model)) {
|
|
1124
1151
|
console.log("[compaction] Context approaching limit, compacting...");
|
|
1125
1152
|
const result = await this.compactionManager.compact(
|
|
@@ -1134,7 +1161,8 @@ var Agent = class {
|
|
|
1134
1161
|
conversationHistory.length = 0;
|
|
1135
1162
|
conversationHistory.push(...updatedHistory);
|
|
1136
1163
|
}
|
|
1137
|
-
const
|
|
1164
|
+
const contextLimit = this.config.llm.context_limit ?? 2e5;
|
|
1165
|
+
const messages = buildMessages(systemPromptResult.text, conversationHistory, userMessage, contextLimit, this.config.llm.max_tokens);
|
|
1138
1166
|
const llmStart = Date.now();
|
|
1139
1167
|
const toolDefs = this.toolRegistry?.getDefinitions() ?? [];
|
|
1140
1168
|
let response = await this.llm.chatWithTools(messages, toolDefs.length > 0 ? toolDefs : void 0);
|
|
@@ -2513,8 +2541,233 @@ var SesameClient2 = class {
|
|
|
2513
2541
|
}
|
|
2514
2542
|
};
|
|
2515
2543
|
|
|
2544
|
+
// packages/runtime/src/skills.ts
|
|
2545
|
+
import { execSync } from "child_process";
|
|
2546
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, watch as watch2, mkdirSync as mkdirSync3 } from "fs";
|
|
2547
|
+
import { resolve as resolve5 } from "path";
|
|
2548
|
+
function shellEscape(value) {
|
|
2549
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
2550
|
+
}
|
|
2551
|
+
function renderCommand(template, params, skillDir, workspaceDir) {
|
|
2552
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
2553
|
+
if (key === "skill_dir") return shellEscape(skillDir);
|
|
2554
|
+
if (key === "workspace") return shellEscape(workspaceDir);
|
|
2555
|
+
const val = params[key];
|
|
2556
|
+
if (val === void 0 || val === null) return "''";
|
|
2557
|
+
return shellEscape(String(val));
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
function parseSkillMd(content, fallbackName) {
|
|
2561
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2562
|
+
let name = fallbackName;
|
|
2563
|
+
let description = "";
|
|
2564
|
+
if (fmMatch) {
|
|
2565
|
+
const fm = fmMatch[1];
|
|
2566
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
2567
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
2568
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2569
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2570
|
+
}
|
|
2571
|
+
return { name, description };
|
|
2572
|
+
}
|
|
2573
|
+
var SkillsEngine = class {
|
|
2574
|
+
workspaceDir;
|
|
2575
|
+
skillsDir;
|
|
2576
|
+
toolRegistry;
|
|
2577
|
+
loadedSkills = /* @__PURE__ */ new Map();
|
|
2578
|
+
watcher = null;
|
|
2579
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
2580
|
+
constructor(workspaceDir, toolRegistry) {
|
|
2581
|
+
this.workspaceDir = workspaceDir;
|
|
2582
|
+
this.toolRegistry = toolRegistry;
|
|
2583
|
+
this.skillsDir = resolve5(workspaceDir, "skills");
|
|
2584
|
+
}
|
|
2585
|
+
async loadAll() {
|
|
2586
|
+
if (!existsSync5(this.skillsDir)) return;
|
|
2587
|
+
let entries;
|
|
2588
|
+
try {
|
|
2589
|
+
entries = readdirSync3(this.skillsDir);
|
|
2590
|
+
} catch {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
for (const entry of entries) {
|
|
2594
|
+
const skillMdPath = resolve5(this.skillsDir, entry, "SKILL.md");
|
|
2595
|
+
if (!existsSync5(skillMdPath)) continue;
|
|
2596
|
+
try {
|
|
2597
|
+
await this.loadSkill(entry);
|
|
2598
|
+
} catch (err) {
|
|
2599
|
+
console.error(`[skills] Failed to load skill "${entry}":`, err.message);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
async loadSkill(dirName) {
|
|
2604
|
+
const skillDir = resolve5(this.skillsDir, dirName);
|
|
2605
|
+
const skillMdPath = resolve5(skillDir, "SKILL.md");
|
|
2606
|
+
if (!existsSync5(skillMdPath)) {
|
|
2607
|
+
throw new Error(`SKILL.md not found in ${skillDir}`);
|
|
2608
|
+
}
|
|
2609
|
+
const mdContent = readFileSync6(skillMdPath, "utf-8");
|
|
2610
|
+
const { name, description } = parseSkillMd(mdContent, dirName);
|
|
2611
|
+
const setupPath = resolve5(skillDir, "setup.sh");
|
|
2612
|
+
if (existsSync5(setupPath)) {
|
|
2613
|
+
try {
|
|
2614
|
+
execSync("bash setup.sh", { cwd: skillDir, timeout: 3e4, stdio: "pipe" });
|
|
2615
|
+
console.log(`[skills] Ran setup.sh for "${name}"`);
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
console.warn(`[skills] setup.sh failed for "${name}":`, err.message);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
const toolNames = [];
|
|
2621
|
+
const toolsJsonPath = resolve5(skillDir, "tools.json");
|
|
2622
|
+
if (existsSync5(toolsJsonPath)) {
|
|
2623
|
+
try {
|
|
2624
|
+
const toolsData = JSON.parse(readFileSync6(toolsJsonPath, "utf-8"));
|
|
2625
|
+
const toolDefs = toolsData.tools || [];
|
|
2626
|
+
for (const toolDef of toolDefs) {
|
|
2627
|
+
if (!toolDef.name || !toolDef.command) continue;
|
|
2628
|
+
const toolName = toolDef.name;
|
|
2629
|
+
const toolDesc = toolDef.description || `Tool from skill "${name}"`;
|
|
2630
|
+
const toolParams = toolDef.parameters || { type: "object", properties: {}, required: [] };
|
|
2631
|
+
const commandTemplate = toolDef.command;
|
|
2632
|
+
this.toolRegistry.register(
|
|
2633
|
+
toolName,
|
|
2634
|
+
toolDesc,
|
|
2635
|
+
toolParams,
|
|
2636
|
+
async (params) => {
|
|
2637
|
+
const cmd = renderCommand(commandTemplate, params, skillDir, this.workspaceDir);
|
|
2638
|
+
try {
|
|
2639
|
+
const output = execSync(cmd, {
|
|
2640
|
+
cwd: skillDir,
|
|
2641
|
+
timeout: 6e4,
|
|
2642
|
+
encoding: "utf-8",
|
|
2643
|
+
maxBuffer: 1024 * 1024,
|
|
2644
|
+
shell: "/bin/bash"
|
|
2645
|
+
});
|
|
2646
|
+
return output || "(no output)";
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
const execErr = err;
|
|
2649
|
+
return `Error: ${execErr.stderr || execErr.message}`;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
);
|
|
2653
|
+
toolNames.push(toolName);
|
|
2654
|
+
}
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
console.warn(`[skills] Failed to parse tools.json for "${name}":`, err.message);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
this.loadedSkills.set(dirName, {
|
|
2660
|
+
name,
|
|
2661
|
+
dirName,
|
|
2662
|
+
description,
|
|
2663
|
+
path: skillDir,
|
|
2664
|
+
tools: toolNames
|
|
2665
|
+
});
|
|
2666
|
+
const toolSuffix = toolNames.length > 0 ? ` (${toolNames.length} tools: ${toolNames.join(", ")})` : "";
|
|
2667
|
+
console.log(`[skills] Loaded "${name}"${toolSuffix}`);
|
|
2668
|
+
}
|
|
2669
|
+
async unloadSkill(dirName) {
|
|
2670
|
+
const skill = this.loadedSkills.get(dirName);
|
|
2671
|
+
if (!skill) return;
|
|
2672
|
+
for (const toolName of skill.tools) {
|
|
2673
|
+
this.toolRegistry.unregister(toolName);
|
|
2674
|
+
}
|
|
2675
|
+
const teardownPath = resolve5(skill.path, "teardown.sh");
|
|
2676
|
+
if (existsSync5(teardownPath)) {
|
|
2677
|
+
try {
|
|
2678
|
+
execSync("bash teardown.sh", { cwd: skill.path, timeout: 3e4, stdio: "pipe" });
|
|
2679
|
+
console.log(`[skills] Ran teardown.sh for "${skill.name}"`);
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
console.warn(`[skills] teardown.sh failed for "${skill.name}":`, err.message);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
this.loadedSkills.delete(dirName);
|
|
2685
|
+
console.log(`[skills] Unloaded "${skill.name}"`);
|
|
2686
|
+
}
|
|
2687
|
+
async reloadSkill(dirName) {
|
|
2688
|
+
await this.unloadSkill(dirName);
|
|
2689
|
+
await this.loadSkill(dirName);
|
|
2690
|
+
}
|
|
2691
|
+
startWatching() {
|
|
2692
|
+
if (this.watcher) return;
|
|
2693
|
+
if (!existsSync5(this.skillsDir)) {
|
|
2694
|
+
mkdirSync3(this.skillsDir, { recursive: true });
|
|
2695
|
+
}
|
|
2696
|
+
try {
|
|
2697
|
+
this.watcher = watch2(this.skillsDir, { recursive: true }, (_event, filename) => {
|
|
2698
|
+
if (!filename) return;
|
|
2699
|
+
const parts = filename.split("/");
|
|
2700
|
+
if (parts.length < 2) return;
|
|
2701
|
+
const skillDirName = parts[0];
|
|
2702
|
+
const changedFile = parts.slice(1).join("/");
|
|
2703
|
+
if (changedFile !== "tools.json" && changedFile !== "SKILL.md") return;
|
|
2704
|
+
const existing = this.debounceTimers.get(skillDirName);
|
|
2705
|
+
if (existing) clearTimeout(existing);
|
|
2706
|
+
this.debounceTimers.set(
|
|
2707
|
+
skillDirName,
|
|
2708
|
+
setTimeout(async () => {
|
|
2709
|
+
this.debounceTimers.delete(skillDirName);
|
|
2710
|
+
const skillDir = resolve5(this.skillsDir, skillDirName);
|
|
2711
|
+
const hasMd = existsSync5(resolve5(skillDir, "SKILL.md"));
|
|
2712
|
+
if (hasMd && this.loadedSkills.has(skillDirName)) {
|
|
2713
|
+
console.log(`[skills] Detected change in "${skillDirName}", reloading...`);
|
|
2714
|
+
try {
|
|
2715
|
+
await this.reloadSkill(skillDirName);
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
console.error(`[skills] Reload failed for "${skillDirName}":`, err.message);
|
|
2718
|
+
}
|
|
2719
|
+
} else if (hasMd && !this.loadedSkills.has(skillDirName)) {
|
|
2720
|
+
console.log(`[skills] Detected new skill "${skillDirName}", loading...`);
|
|
2721
|
+
try {
|
|
2722
|
+
await this.loadSkill(skillDirName);
|
|
2723
|
+
} catch (err) {
|
|
2724
|
+
console.error(`[skills] Load failed for "${skillDirName}":`, err.message);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}, 500)
|
|
2728
|
+
);
|
|
2729
|
+
});
|
|
2730
|
+
console.log(`[skills] Watching ${this.skillsDir} for changes`);
|
|
2731
|
+
} catch (err) {
|
|
2732
|
+
console.warn(`[skills] Failed to start watcher:`, err.message);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
stopWatching() {
|
|
2736
|
+
if (this.watcher) {
|
|
2737
|
+
this.watcher.close();
|
|
2738
|
+
this.watcher = null;
|
|
2739
|
+
}
|
|
2740
|
+
for (const timer of this.debounceTimers.values()) {
|
|
2741
|
+
clearTimeout(timer);
|
|
2742
|
+
}
|
|
2743
|
+
this.debounceTimers.clear();
|
|
2744
|
+
}
|
|
2745
|
+
listSkills() {
|
|
2746
|
+
return Array.from(this.loadedSkills.values()).map((s) => ({
|
|
2747
|
+
name: s.name,
|
|
2748
|
+
dirName: s.dirName,
|
|
2749
|
+
description: s.description,
|
|
2750
|
+
path: s.path,
|
|
2751
|
+
tools: [...s.tools],
|
|
2752
|
+
loaded: true
|
|
2753
|
+
}));
|
|
2754
|
+
}
|
|
2755
|
+
getSkill(dirName) {
|
|
2756
|
+
const s = this.loadedSkills.get(dirName);
|
|
2757
|
+
if (!s) return void 0;
|
|
2758
|
+
return {
|
|
2759
|
+
name: s.name,
|
|
2760
|
+
dirName: s.dirName,
|
|
2761
|
+
description: s.description,
|
|
2762
|
+
path: s.path,
|
|
2763
|
+
tools: [...s.tools],
|
|
2764
|
+
loaded: true
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
|
|
2516
2769
|
// packages/runtime/src/pipeline.ts
|
|
2517
|
-
import { createServer as
|
|
2770
|
+
import { createServer as createServer3 } from "http";
|
|
2518
2771
|
|
|
2519
2772
|
// packages/runtime/src/health.ts
|
|
2520
2773
|
var HEALTH_PORT = 9484;
|
|
@@ -2522,7 +2775,7 @@ var HEALTH_PATH = "/health";
|
|
|
2522
2775
|
|
|
2523
2776
|
// packages/runtime/src/request-logger.ts
|
|
2524
2777
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2525
|
-
import { mkdirSync as
|
|
2778
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync6, appendFileSync as appendFileSync2, readFileSync as readFileSync7, writeFileSync } from "fs";
|
|
2526
2779
|
import { dirname as dirname4 } from "path";
|
|
2527
2780
|
var RequestLogger = class {
|
|
2528
2781
|
logPath;
|
|
@@ -2531,14 +2784,14 @@ var RequestLogger = class {
|
|
|
2531
2784
|
this.logPath = dbPath.replace(/\.db$/, ".jsonl");
|
|
2532
2785
|
if (this.logPath === dbPath) this.logPath = dbPath + ".jsonl";
|
|
2533
2786
|
const dir = dirname4(this.logPath);
|
|
2534
|
-
if (!
|
|
2787
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
2535
2788
|
this.prune();
|
|
2536
2789
|
}
|
|
2537
2790
|
prune() {
|
|
2538
|
-
if (!
|
|
2791
|
+
if (!existsSync6(this.logPath)) return;
|
|
2539
2792
|
const cutoff = new Date(Date.now() - this.maxAgeDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
2540
2793
|
try {
|
|
2541
|
-
const lines =
|
|
2794
|
+
const lines = readFileSync7(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
2542
2795
|
const kept = [];
|
|
2543
2796
|
let pruned = 0;
|
|
2544
2797
|
for (const line of lines) {
|
|
@@ -2614,9 +2867,9 @@ var RequestLogger = class {
|
|
|
2614
2867
|
close() {
|
|
2615
2868
|
}
|
|
2616
2869
|
readAll() {
|
|
2617
|
-
if (!
|
|
2870
|
+
if (!existsSync6(this.logPath)) return [];
|
|
2618
2871
|
try {
|
|
2619
|
-
const lines =
|
|
2872
|
+
const lines = readFileSync7(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
2620
2873
|
const entries = [];
|
|
2621
2874
|
for (const line of lines) {
|
|
2622
2875
|
try {
|
|
@@ -2633,17 +2886,17 @@ var RequestLogger = class {
|
|
|
2633
2886
|
|
|
2634
2887
|
// packages/runtime/src/dashboard.ts
|
|
2635
2888
|
import { createServer } from "http";
|
|
2636
|
-
import { readFileSync as
|
|
2637
|
-
import { resolve as
|
|
2889
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2890
|
+
import { resolve as resolve6, dirname as dirname5 } from "path";
|
|
2638
2891
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2639
2892
|
var __dirname = dirname5(fileURLToPath2(import.meta.url));
|
|
2640
2893
|
var DASHBOARD_PORT = 9485;
|
|
2641
2894
|
var spaHtml = null;
|
|
2642
2895
|
function getSpaHtml() {
|
|
2643
2896
|
if (!spaHtml) {
|
|
2644
|
-
for (const dir of [__dirname,
|
|
2897
|
+
for (const dir of [__dirname, resolve6(__dirname, "../src")]) {
|
|
2645
2898
|
try {
|
|
2646
|
-
spaHtml =
|
|
2899
|
+
spaHtml = readFileSync8(resolve6(dir, "dashboard.html"), "utf-8");
|
|
2647
2900
|
break;
|
|
2648
2901
|
} catch {
|
|
2649
2902
|
}
|
|
@@ -2782,6 +3035,9 @@ var ToolRegistry = class {
|
|
|
2782
3035
|
has(name) {
|
|
2783
3036
|
return this.tools.has(name);
|
|
2784
3037
|
}
|
|
3038
|
+
unregister(name) {
|
|
3039
|
+
return this.tools.delete(name);
|
|
3040
|
+
}
|
|
2785
3041
|
async execute(name, params) {
|
|
2786
3042
|
const tool = this.tools.get(name);
|
|
2787
3043
|
if (!tool) {
|
|
@@ -2796,7 +3052,8 @@ var ToolRegistry = class {
|
|
|
2796
3052
|
async executeCall(call) {
|
|
2797
3053
|
let params = {};
|
|
2798
3054
|
try {
|
|
2799
|
-
|
|
3055
|
+
const raw = call.function.arguments;
|
|
3056
|
+
params = raw && raw.trim() ? JSON.parse(raw) : {};
|
|
2800
3057
|
} catch {
|
|
2801
3058
|
return {
|
|
2802
3059
|
tool_call_id: call.id,
|
|
@@ -2814,8 +3071,8 @@ var ToolRegistry = class {
|
|
|
2814
3071
|
};
|
|
2815
3072
|
|
|
2816
3073
|
// packages/runtime/src/tools/shell.ts
|
|
2817
|
-
import { execSync } from "child_process";
|
|
2818
|
-
import { resolve as
|
|
3074
|
+
import { execSync as execSync2 } from "child_process";
|
|
3075
|
+
import { resolve as resolve7 } from "path";
|
|
2819
3076
|
var MAX_OUTPUT = 5e4;
|
|
2820
3077
|
function registerShellTool(registry, workspaceDir) {
|
|
2821
3078
|
registry.register(
|
|
@@ -2842,9 +3099,9 @@ function registerShellTool(registry, workspaceDir) {
|
|
|
2842
3099
|
async (params) => {
|
|
2843
3100
|
const command = params.command;
|
|
2844
3101
|
const timeout = (params.timeout || 30) * 1e3;
|
|
2845
|
-
const cwd = params.workdir ?
|
|
3102
|
+
const cwd = params.workdir ? resolve7(workspaceDir, params.workdir) : workspaceDir;
|
|
2846
3103
|
try {
|
|
2847
|
-
const output =
|
|
3104
|
+
const output = execSync2(command, {
|
|
2848
3105
|
cwd,
|
|
2849
3106
|
timeout,
|
|
2850
3107
|
encoding: "utf-8",
|
|
@@ -2869,8 +3126,8 @@ ${output || err.message}`;
|
|
|
2869
3126
|
}
|
|
2870
3127
|
|
|
2871
3128
|
// packages/runtime/src/tools/files.ts
|
|
2872
|
-
import { readFileSync as
|
|
2873
|
-
import { resolve as
|
|
3129
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
3130
|
+
import { resolve as resolve8, dirname as dirname6 } from "path";
|
|
2874
3131
|
var MAX_READ = 1e5;
|
|
2875
3132
|
function registerFileTools(registry, workspaceDir) {
|
|
2876
3133
|
registry.register(
|
|
@@ -2896,11 +3153,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2896
3153
|
},
|
|
2897
3154
|
async (params) => {
|
|
2898
3155
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2899
|
-
if (!
|
|
3156
|
+
if (!existsSync7(filePath)) {
|
|
2900
3157
|
return `Error: File not found: ${filePath}`;
|
|
2901
3158
|
}
|
|
2902
3159
|
try {
|
|
2903
|
-
let content =
|
|
3160
|
+
let content = readFileSync9(filePath, "utf-8");
|
|
2904
3161
|
if (params.offset || params.limit) {
|
|
2905
3162
|
const lines = content.split("\n");
|
|
2906
3163
|
const start = (params.offset || 1) - 1;
|
|
@@ -2938,7 +3195,7 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2938
3195
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2939
3196
|
try {
|
|
2940
3197
|
const dir = dirname6(filePath);
|
|
2941
|
-
if (!
|
|
3198
|
+
if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
|
|
2942
3199
|
writeFileSync2(filePath, params.content);
|
|
2943
3200
|
return `Written ${params.content.length} bytes to ${filePath}`;
|
|
2944
3201
|
} catch (err) {
|
|
@@ -2969,11 +3226,11 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
2969
3226
|
},
|
|
2970
3227
|
async (params) => {
|
|
2971
3228
|
const filePath = resolvePath(workspaceDir, params.path);
|
|
2972
|
-
if (!
|
|
3229
|
+
if (!existsSync7(filePath)) {
|
|
2973
3230
|
return `Error: File not found: ${filePath}`;
|
|
2974
3231
|
}
|
|
2975
3232
|
try {
|
|
2976
|
-
const content =
|
|
3233
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2977
3234
|
const oldText = params.old_text;
|
|
2978
3235
|
const newText = params.new_text;
|
|
2979
3236
|
if (!content.includes(oldText)) {
|
|
@@ -3001,18 +3258,18 @@ function registerFileTools(registry, workspaceDir) {
|
|
|
3001
3258
|
required: []
|
|
3002
3259
|
},
|
|
3003
3260
|
async (params) => {
|
|
3004
|
-
const { readdirSync:
|
|
3261
|
+
const { readdirSync: readdirSync5, statSync: statSync3 } = await import("fs");
|
|
3005
3262
|
const dirPath = params.path ? resolvePath(workspaceDir, params.path) : workspaceDir;
|
|
3006
|
-
if (!
|
|
3263
|
+
if (!existsSync7(dirPath)) {
|
|
3007
3264
|
return `Error: Directory not found: ${dirPath}`;
|
|
3008
3265
|
}
|
|
3009
3266
|
try {
|
|
3010
|
-
const entries =
|
|
3267
|
+
const entries = readdirSync5(dirPath);
|
|
3011
3268
|
const results = [];
|
|
3012
3269
|
for (const entry of entries) {
|
|
3013
3270
|
if (entry.startsWith(".")) continue;
|
|
3014
3271
|
try {
|
|
3015
|
-
const stat =
|
|
3272
|
+
const stat = statSync3(resolve8(dirPath, entry));
|
|
3016
3273
|
results.push(stat.isDirectory() ? `${entry}/` : entry);
|
|
3017
3274
|
} catch {
|
|
3018
3275
|
results.push(entry);
|
|
@@ -3029,7 +3286,7 @@ function resolvePath(workspace, path) {
|
|
|
3029
3286
|
if (path.startsWith("/") || path.startsWith("~")) {
|
|
3030
3287
|
return path.replace(/^~/, process.env.HOME || "/root");
|
|
3031
3288
|
}
|
|
3032
|
-
return
|
|
3289
|
+
return resolve8(workspace, path);
|
|
3033
3290
|
}
|
|
3034
3291
|
|
|
3035
3292
|
// packages/runtime/src/tools/web.ts
|
|
@@ -3281,172 +3538,2314 @@ Contexts:
|
|
|
3281
3538
|
);
|
|
3282
3539
|
}
|
|
3283
3540
|
|
|
3284
|
-
// packages/runtime/src/tools/
|
|
3285
|
-
import {
|
|
3286
|
-
import {
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3541
|
+
// packages/runtime/src/tools/events.ts
|
|
3542
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3543
|
+
import { join as join4 } from "path";
|
|
3544
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3545
|
+
function registerEventTools(registry, dataDir) {
|
|
3546
|
+
const eventsDir = join4(dataDir, "events");
|
|
3547
|
+
if (!existsSync8(eventsDir)) {
|
|
3548
|
+
mkdirSync6(eventsDir, { recursive: true });
|
|
3291
3549
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3550
|
+
registry.register(
|
|
3551
|
+
"create_event",
|
|
3552
|
+
"Create a scheduled event. Immediate events fire right away. One-shot events fire at a specific time. Periodic events fire on a cron schedule. The event will trigger a message to the specified Sesame channel.",
|
|
3553
|
+
{
|
|
3554
|
+
type: "object",
|
|
3555
|
+
properties: {
|
|
3556
|
+
type: {
|
|
3557
|
+
type: "string",
|
|
3558
|
+
enum: ["immediate", "one-shot", "periodic"],
|
|
3559
|
+
description: "Event type: 'immediate' fires now, 'one-shot' fires at a specific time, 'periodic' fires on a cron schedule"
|
|
3560
|
+
},
|
|
3561
|
+
channelId: {
|
|
3562
|
+
type: "string",
|
|
3563
|
+
description: "Sesame channel ID to send the event result to"
|
|
3564
|
+
},
|
|
3565
|
+
text: {
|
|
3566
|
+
type: "string",
|
|
3567
|
+
description: "The prompt/message for the event"
|
|
3568
|
+
},
|
|
3569
|
+
at: {
|
|
3570
|
+
type: "string",
|
|
3571
|
+
description: "ISO 8601 datetime for one-shot events (e.g. '2026-03-01T15:00:00Z')"
|
|
3572
|
+
},
|
|
3573
|
+
schedule: {
|
|
3574
|
+
type: "string",
|
|
3575
|
+
description: "Cron expression for periodic events (e.g. '0 9 * * 1-5' for weekdays at 9am)"
|
|
3576
|
+
},
|
|
3577
|
+
timezone: {
|
|
3578
|
+
type: "string",
|
|
3579
|
+
description: "IANA timezone for periodic events (default: 'UTC')"
|
|
3580
|
+
}
|
|
3581
|
+
},
|
|
3582
|
+
required: ["type", "channelId", "text"]
|
|
3583
|
+
},
|
|
3584
|
+
async (params) => {
|
|
3585
|
+
const type = params.type;
|
|
3586
|
+
const channelId = params.channelId;
|
|
3587
|
+
const text = params.text;
|
|
3588
|
+
if (type === "one-shot" && !params.at) {
|
|
3589
|
+
return "Error: 'at' parameter is required for one-shot events (ISO 8601 datetime)";
|
|
3590
|
+
}
|
|
3591
|
+
if (type === "periodic" && !params.schedule) {
|
|
3592
|
+
return "Error: 'schedule' parameter is required for periodic events (cron expression)";
|
|
3593
|
+
}
|
|
3594
|
+
let event;
|
|
3595
|
+
switch (type) {
|
|
3596
|
+
case "immediate":
|
|
3597
|
+
event = { type: "immediate", channelId, text };
|
|
3598
|
+
break;
|
|
3599
|
+
case "one-shot":
|
|
3600
|
+
event = { type: "one-shot", channelId, text, at: params.at };
|
|
3601
|
+
break;
|
|
3602
|
+
case "periodic":
|
|
3603
|
+
event = {
|
|
3604
|
+
type: "periodic",
|
|
3605
|
+
channelId,
|
|
3606
|
+
text,
|
|
3607
|
+
schedule: params.schedule,
|
|
3608
|
+
timezone: params.timezone || "UTC"
|
|
3609
|
+
};
|
|
3610
|
+
break;
|
|
3611
|
+
default:
|
|
3612
|
+
return `Error: Unknown event type '${type}'. Use 'immediate', 'one-shot', or 'periodic'.`;
|
|
3613
|
+
}
|
|
3614
|
+
const filename = `${randomUUID3()}.json`;
|
|
3615
|
+
const filePath = join4(eventsDir, filename);
|
|
3616
|
+
try {
|
|
3617
|
+
writeFileSync3(filePath, JSON.stringify(event, null, 2));
|
|
3618
|
+
return `Event created: ${filename} (type: ${type})`;
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
return `Error creating event: ${err.message}`;
|
|
3621
|
+
}
|
|
3333
3622
|
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
function cleanupPidFile(path) {
|
|
3345
|
-
try {
|
|
3346
|
-
unlinkSync2(path);
|
|
3347
|
-
} catch {
|
|
3348
|
-
}
|
|
3349
|
-
}
|
|
3350
|
-
async function startPipeline(configPath) {
|
|
3351
|
-
const config = loadConfig(configPath);
|
|
3352
|
-
const sentinel = config.sentinel ?? defaultSentinelConfig();
|
|
3353
|
-
const healthPort = sentinel.health_port || HEALTH_PORT;
|
|
3354
|
-
const pidFile = sentinel.pid_file;
|
|
3355
|
-
console.log(`[hivemind] Starting ${config.agent.name} (pid ${process.pid})`);
|
|
3356
|
-
writePidFile(pidFile);
|
|
3357
|
-
const healthServer = startHealthServer(healthPort);
|
|
3358
|
-
const cleanupOnExit = () => {
|
|
3359
|
-
cleanupPidFile(pidFile);
|
|
3360
|
-
healthServer.close();
|
|
3361
|
-
};
|
|
3362
|
-
process.on("exit", cleanupOnExit);
|
|
3363
|
-
const memory = new MemoryClient(config.memory);
|
|
3364
|
-
const memoryOk = await memory.healthCheck();
|
|
3365
|
-
if (!memoryOk) {
|
|
3366
|
-
console.warn("[hivemind] Memory daemon unreachable at", config.memory.daemon_url);
|
|
3367
|
-
console.warn("[hivemind] Continuing without persistent memory \u2014 episodes will not be stored");
|
|
3368
|
-
} else {
|
|
3369
|
-
memoryConnected = true;
|
|
3370
|
-
console.log("[hivemind] Memory daemon connected");
|
|
3371
|
-
}
|
|
3372
|
-
const requestLogger = new RequestLogger(resolve9(dirname7(configPath), "data", "dashboard.db"));
|
|
3373
|
-
startDashboardServer(requestLogger, config.memory);
|
|
3374
|
-
const agent = new Agent(config);
|
|
3375
|
-
agent.setRequestLogger(requestLogger);
|
|
3376
|
-
const hivemindHome = process.env.HIVEMIND_HOME || resolve9(process.env.HOME || "/root", "hivemind");
|
|
3377
|
-
const toolRegistry = registerAllTools(hivemindHome, {
|
|
3378
|
-
enabled: true,
|
|
3379
|
-
workspace: config.agent.workspace || "workspace",
|
|
3380
|
-
braveApiKey: process.env.BRAVE_API_KEY,
|
|
3381
|
-
memoryDaemonUrl: config.memory.daemon_url
|
|
3382
|
-
});
|
|
3383
|
-
agent.setToolRegistry(toolRegistry);
|
|
3384
|
-
console.log(`[hivemind] Context manager initialized (active: ${agent.getActiveContext()})`);
|
|
3385
|
-
const dataDir = resolve9(hivemindHome, "data");
|
|
3386
|
-
if (config.sesame.api_key) {
|
|
3387
|
-
await startSesameLoop(config, agent, dataDir);
|
|
3388
|
-
} else {
|
|
3389
|
-
console.log("[hivemind] No Sesame API key configured \u2014 running in stdin mode");
|
|
3390
|
-
await startStdinLoop(agent);
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
async function startSesameLoop(config, agent, dataDir) {
|
|
3394
|
-
const sesame = new SesameClient2(config.sesame);
|
|
3395
|
-
let eventsWatcher = null;
|
|
3396
|
-
if (dataDir) {
|
|
3397
|
-
eventsWatcher = new EventsWatcher(dataDir, async (channelId, text, filename, eventType) => {
|
|
3398
|
-
console.log(`[events] Firing ${eventType} event from ${filename}`);
|
|
3399
|
-
const eventMessage = `[EVENT:${filename}:${eventType}] ${text}`;
|
|
3623
|
+
);
|
|
3624
|
+
registry.register(
|
|
3625
|
+
"list_events",
|
|
3626
|
+
"List all scheduled events with their type and configuration.",
|
|
3627
|
+
{
|
|
3628
|
+
type: "object",
|
|
3629
|
+
properties: {},
|
|
3630
|
+
required: []
|
|
3631
|
+
},
|
|
3632
|
+
async () => {
|
|
3400
3633
|
try {
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
console.log(`[events] Silent response for ${filename}`);
|
|
3404
|
-
return;
|
|
3634
|
+
if (!existsSync8(eventsDir)) {
|
|
3635
|
+
return "No events directory found.";
|
|
3405
3636
|
}
|
|
3406
|
-
|
|
3407
|
-
if (
|
|
3408
|
-
|
|
3637
|
+
const files = readdirSync4(eventsDir).filter((f) => f.endsWith(".json"));
|
|
3638
|
+
if (files.length === 0) {
|
|
3639
|
+
return "No events scheduled.";
|
|
3640
|
+
}
|
|
3641
|
+
const lines = [];
|
|
3642
|
+
for (const file of files) {
|
|
3643
|
+
try {
|
|
3644
|
+
const content = readFileSync10(join4(eventsDir, file), "utf-8");
|
|
3645
|
+
const event = JSON.parse(content);
|
|
3646
|
+
let detail = `${file} \u2014 type: ${event.type}, channel: ${event.channelId}`;
|
|
3647
|
+
if (event.type === "one-shot") {
|
|
3648
|
+
detail += `, at: ${event.at}`;
|
|
3649
|
+
} else if (event.type === "periodic") {
|
|
3650
|
+
detail += `, schedule: ${event.schedule}, tz: ${event.timezone}`;
|
|
3651
|
+
}
|
|
3652
|
+
detail += `, text: "${event.text.slice(0, 80)}${event.text.length > 80 ? "..." : ""}"`;
|
|
3653
|
+
lines.push(detail);
|
|
3654
|
+
} catch {
|
|
3655
|
+
lines.push(`${file} \u2014 (unreadable)`);
|
|
3656
|
+
}
|
|
3409
3657
|
}
|
|
3658
|
+
return `${files.length} event(s):
|
|
3659
|
+
${lines.join("\n")}`;
|
|
3410
3660
|
} catch (err) {
|
|
3411
|
-
|
|
3661
|
+
return `Error listing events: ${err.message}`;
|
|
3412
3662
|
}
|
|
3413
|
-
});
|
|
3414
|
-
eventsWatcher.start();
|
|
3415
|
-
}
|
|
3416
|
-
let shuttingDown = false;
|
|
3417
|
-
const shutdown = (signal) => {
|
|
3418
|
-
if (shuttingDown) return;
|
|
3419
|
-
shuttingDown = true;
|
|
3420
|
-
console.log(`
|
|
3421
|
-
[hivemind] Received ${signal}, shutting down...`);
|
|
3422
|
-
try {
|
|
3423
|
-
sesame.updatePresence("offline", { emoji: "\u2B58" });
|
|
3424
|
-
sesame.disconnect();
|
|
3425
|
-
console.log("[hivemind] Sesame disconnected cleanly");
|
|
3426
|
-
} catch (err) {
|
|
3427
|
-
console.error("[hivemind] Error during disconnect:", err.message);
|
|
3428
|
-
}
|
|
3429
|
-
process.exit(0);
|
|
3430
|
-
};
|
|
3431
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
3432
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3433
|
-
const processedIds = /* @__PURE__ */ new Set();
|
|
3434
|
-
const MAX_SEEN = 500;
|
|
3435
|
-
let processing = false;
|
|
3436
|
-
const messageQueue = [];
|
|
3437
|
-
async function processQueue() {
|
|
3438
|
-
if (processing || messageQueue.length === 0) return;
|
|
3439
|
-
processing = true;
|
|
3440
|
-
while (messageQueue.length > 0) {
|
|
3441
|
-
const msg = messageQueue.shift();
|
|
3442
|
-
await handleMessage(msg);
|
|
3443
3663
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3664
|
+
);
|
|
3665
|
+
registry.register(
|
|
3666
|
+
"delete_event",
|
|
3667
|
+
"Delete a scheduled event by filename.",
|
|
3668
|
+
{
|
|
3669
|
+
type: "object",
|
|
3670
|
+
properties: {
|
|
3671
|
+
filename: {
|
|
3672
|
+
type: "string",
|
|
3673
|
+
description: "The event filename to delete (e.g. 'abc123.json')"
|
|
3674
|
+
}
|
|
3675
|
+
},
|
|
3676
|
+
required: ["filename"]
|
|
3677
|
+
},
|
|
3678
|
+
async (params) => {
|
|
3679
|
+
const filename = params.filename;
|
|
3680
|
+
const filePath = join4(eventsDir, filename);
|
|
3681
|
+
if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
|
|
3682
|
+
return "Error: Invalid filename.";
|
|
3683
|
+
}
|
|
3684
|
+
if (!existsSync8(filePath)) {
|
|
3685
|
+
return `Event not found: ${filename}`;
|
|
3686
|
+
}
|
|
3687
|
+
try {
|
|
3688
|
+
unlinkSync2(filePath);
|
|
3689
|
+
return `Event deleted: ${filename}`;
|
|
3690
|
+
} catch (err) {
|
|
3691
|
+
return `Error deleting event: ${err.message}`;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
);
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// packages/runtime/src/tools/spawn.ts
|
|
3698
|
+
import { spawn } from "child_process";
|
|
3699
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
|
|
3700
|
+
import { join as join5, resolve as resolve9 } from "path";
|
|
3701
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3702
|
+
var spawnedAgents = /* @__PURE__ */ new Map();
|
|
3703
|
+
function registerSpawnTools(registry, hivemindHome, dataDir, configPath) {
|
|
3704
|
+
const spawnDir = join5(dataDir, "spawn");
|
|
3705
|
+
if (!existsSync9(spawnDir)) {
|
|
3706
|
+
mkdirSync7(spawnDir, { recursive: true });
|
|
3707
|
+
}
|
|
3708
|
+
const hivemindBin = resolve9(hivemindHome, "node_modules", ".bin", "hivemind");
|
|
3709
|
+
registry.register(
|
|
3710
|
+
"spawn_agent",
|
|
3711
|
+
"Fork a new hivemind process to run an isolated task. The sub-agent gets its own context, processes the task, and exits. Use for parallel work, long-running tasks, or tasks that need isolation.",
|
|
3712
|
+
{
|
|
3713
|
+
type: "object",
|
|
3714
|
+
properties: {
|
|
3715
|
+
task: {
|
|
3716
|
+
type: "string",
|
|
3717
|
+
description: "The prompt/instruction for the sub-agent to process"
|
|
3718
|
+
},
|
|
3719
|
+
context: {
|
|
3720
|
+
type: "string",
|
|
3721
|
+
description: "Context name for the sub-agent (default: 'spawn-<uuid>')"
|
|
3722
|
+
},
|
|
3723
|
+
channelId: {
|
|
3724
|
+
type: "string",
|
|
3725
|
+
description: "Sesame channel ID to post results to when done"
|
|
3726
|
+
},
|
|
3727
|
+
timeoutSeconds: {
|
|
3728
|
+
type: "number",
|
|
3729
|
+
description: "Timeout in seconds (default: 300)"
|
|
3730
|
+
}
|
|
3731
|
+
},
|
|
3732
|
+
required: ["task"]
|
|
3733
|
+
},
|
|
3734
|
+
async (params) => {
|
|
3735
|
+
const task = params.task;
|
|
3736
|
+
const spawnId = randomUUID4();
|
|
3737
|
+
const context = params.context || `spawn-${spawnId.slice(0, 8)}`;
|
|
3738
|
+
const channelId = params.channelId;
|
|
3739
|
+
const timeoutSeconds = params.timeoutSeconds || 300;
|
|
3740
|
+
const spawnWorkDir = join5(spawnDir, spawnId);
|
|
3741
|
+
mkdirSync7(spawnWorkDir, { recursive: true });
|
|
3742
|
+
writeFileSync4(join5(spawnWorkDir, "task.md"), task);
|
|
3743
|
+
const childEnv = {
|
|
3744
|
+
...process.env,
|
|
3745
|
+
SPAWN_TASK: task,
|
|
3746
|
+
SPAWN_ID: spawnId,
|
|
3747
|
+
SPAWN_CONTEXT: context,
|
|
3748
|
+
SPAWN_DIR: spawnWorkDir,
|
|
3749
|
+
// Disable health server and PID file for spawned agents
|
|
3750
|
+
SENTINEL_HEALTH_PORT: "0",
|
|
3751
|
+
SENTINEL_PID_FILE: join5(spawnWorkDir, ".pid")
|
|
3752
|
+
};
|
|
3753
|
+
if (channelId) {
|
|
3754
|
+
childEnv.SPAWN_CHANNEL_ID = channelId;
|
|
3755
|
+
}
|
|
3756
|
+
try {
|
|
3757
|
+
const child = spawn(hivemindBin, ["start", "--config", configPath], {
|
|
3758
|
+
env: childEnv,
|
|
3759
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3760
|
+
detached: false
|
|
3761
|
+
});
|
|
3762
|
+
if (!child.pid) {
|
|
3763
|
+
return `Error: Failed to spawn sub-agent process`;
|
|
3764
|
+
}
|
|
3765
|
+
const logPath = join5(spawnWorkDir, "output.log");
|
|
3766
|
+
const logChunks = [];
|
|
3767
|
+
child.stdout?.on("data", (chunk) => logChunks.push(chunk));
|
|
3768
|
+
child.stderr?.on("data", (chunk) => logChunks.push(chunk));
|
|
3769
|
+
const agent = {
|
|
3770
|
+
pid: child.pid,
|
|
3771
|
+
context,
|
|
3772
|
+
task,
|
|
3773
|
+
channelId,
|
|
3774
|
+
startTime: Date.now(),
|
|
3775
|
+
process: child,
|
|
3776
|
+
exitCode: null,
|
|
3777
|
+
exited: false
|
|
3778
|
+
};
|
|
3779
|
+
spawnedAgents.set(spawnId, agent);
|
|
3780
|
+
child.on("exit", (code) => {
|
|
3781
|
+
agent.exitCode = code;
|
|
3782
|
+
agent.exited = true;
|
|
3783
|
+
try {
|
|
3784
|
+
writeFileSync4(logPath, Buffer.concat(logChunks).toString("utf-8"));
|
|
3785
|
+
} catch {
|
|
3786
|
+
}
|
|
3787
|
+
});
|
|
3788
|
+
const timer = setTimeout(() => {
|
|
3789
|
+
if (!agent.exited) {
|
|
3790
|
+
try {
|
|
3791
|
+
child.kill("SIGTERM");
|
|
3792
|
+
} catch {
|
|
3793
|
+
}
|
|
3794
|
+
agent.exited = true;
|
|
3795
|
+
agent.exitCode = -1;
|
|
3796
|
+
try {
|
|
3797
|
+
writeFileSync4(
|
|
3798
|
+
join5(spawnWorkDir, "result.txt"),
|
|
3799
|
+
`[TIMEOUT] Sub-agent killed after ${timeoutSeconds}s`
|
|
3800
|
+
);
|
|
3801
|
+
} catch {
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}, timeoutSeconds * 1e3);
|
|
3805
|
+
child.on("exit", () => clearTimeout(timer));
|
|
3806
|
+
return [
|
|
3807
|
+
`Sub-agent spawned successfully.`,
|
|
3808
|
+
` ID: ${spawnId}`,
|
|
3809
|
+
` PID: ${child.pid}`,
|
|
3810
|
+
` Context: ${context}`,
|
|
3811
|
+
` Timeout: ${timeoutSeconds}s`,
|
|
3812
|
+
channelId ? ` Channel: ${channelId}` : null,
|
|
3813
|
+
` Log: ${logPath}`
|
|
3814
|
+
].filter(Boolean).join("\n");
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
return `Error spawning sub-agent: ${err.message}`;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
);
|
|
3820
|
+
registry.register(
|
|
3821
|
+
"list_agents",
|
|
3822
|
+
"List all spawned sub-agent processes with their status (running/completed/failed/timeout).",
|
|
3823
|
+
{
|
|
3824
|
+
type: "object",
|
|
3825
|
+
properties: {},
|
|
3826
|
+
required: []
|
|
3827
|
+
},
|
|
3828
|
+
async () => {
|
|
3829
|
+
if (spawnedAgents.size === 0) {
|
|
3830
|
+
return "No spawned agents.";
|
|
3831
|
+
}
|
|
3832
|
+
const lines = [];
|
|
3833
|
+
for (const [id, agent] of spawnedAgents) {
|
|
3834
|
+
const runtime = Math.floor((Date.now() - agent.startTime) / 1e3);
|
|
3835
|
+
let status;
|
|
3836
|
+
if (!agent.exited) {
|
|
3837
|
+
status = "running";
|
|
3838
|
+
} else if (agent.exitCode === 0) {
|
|
3839
|
+
status = "completed";
|
|
3840
|
+
} else if (agent.exitCode === -1) {
|
|
3841
|
+
status = "timeout";
|
|
3842
|
+
} else {
|
|
3843
|
+
status = `failed (exit ${agent.exitCode})`;
|
|
3844
|
+
}
|
|
3845
|
+
const taskPreview = agent.task.length > 80 ? agent.task.slice(0, 80) + "..." : agent.task;
|
|
3846
|
+
let detail = `${id} \u2014 status: ${status}, pid: ${agent.pid}, context: ${agent.context}, runtime: ${runtime}s`;
|
|
3847
|
+
if (agent.channelId) detail += `, channel: ${agent.channelId}`;
|
|
3848
|
+
detail += `
|
|
3849
|
+
task: "${taskPreview}"`;
|
|
3850
|
+
if (agent.exited) {
|
|
3851
|
+
const resultPath = join5(spawnDir, id, "result.txt");
|
|
3852
|
+
if (existsSync9(resultPath)) {
|
|
3853
|
+
try {
|
|
3854
|
+
const result = readFileSync11(resultPath, "utf-8");
|
|
3855
|
+
const preview = result.length > 200 ? result.slice(0, 200) + "..." : result;
|
|
3856
|
+
detail += `
|
|
3857
|
+
result: "${preview}"`;
|
|
3858
|
+
} catch {
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
lines.push(detail);
|
|
3863
|
+
}
|
|
3864
|
+
return `${spawnedAgents.size} agent(s):
|
|
3865
|
+
${lines.join("\n\n")}`;
|
|
3866
|
+
}
|
|
3867
|
+
);
|
|
3868
|
+
registry.register(
|
|
3869
|
+
"kill_agent",
|
|
3870
|
+
"Kill a spawned sub-agent process by its ID.",
|
|
3871
|
+
{
|
|
3872
|
+
type: "object",
|
|
3873
|
+
properties: {
|
|
3874
|
+
id: {
|
|
3875
|
+
type: "string",
|
|
3876
|
+
description: "The spawn ID of the agent to kill"
|
|
3877
|
+
}
|
|
3878
|
+
},
|
|
3879
|
+
required: ["id"]
|
|
3880
|
+
},
|
|
3881
|
+
async (params) => {
|
|
3882
|
+
const id = params.id;
|
|
3883
|
+
const agent = spawnedAgents.get(id);
|
|
3884
|
+
if (!agent) {
|
|
3885
|
+
return `Error: No spawned agent found with ID ${id}`;
|
|
3886
|
+
}
|
|
3887
|
+
if (agent.exited) {
|
|
3888
|
+
return `Agent ${id} has already exited (code ${agent.exitCode})`;
|
|
3889
|
+
}
|
|
3890
|
+
try {
|
|
3891
|
+
agent.process.kill("SIGTERM");
|
|
3892
|
+
agent.exited = true;
|
|
3893
|
+
agent.exitCode = -2;
|
|
3894
|
+
writeFileSync4(
|
|
3895
|
+
join5(spawnDir, id, "result.txt"),
|
|
3896
|
+
`[KILLED] Sub-agent killed by parent agent`
|
|
3897
|
+
);
|
|
3898
|
+
return `Agent ${id} (pid ${agent.pid}) killed`;
|
|
3899
|
+
} catch (err) {
|
|
3900
|
+
return `Error killing agent ${id}: ${err.message}`;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
// packages/runtime/src/tools/vision.ts
|
|
3907
|
+
function registerVisionTools(registry) {
|
|
3908
|
+
const apiKey = process.env.LLM_API_KEY;
|
|
3909
|
+
registry.register(
|
|
3910
|
+
"analyze_image",
|
|
3911
|
+
"Analyze an image using a vision-capable LLM. Accepts an image URL or base64 data URI. Returns a text description/analysis of the image.",
|
|
3912
|
+
{
|
|
3913
|
+
type: "object",
|
|
3914
|
+
properties: {
|
|
3915
|
+
image: {
|
|
3916
|
+
type: "string",
|
|
3917
|
+
description: "URL to the image, or a base64 data URI (e.g. 'data:image/png;base64,...')"
|
|
3918
|
+
},
|
|
3919
|
+
prompt: {
|
|
3920
|
+
type: "string",
|
|
3921
|
+
description: "What to analyze about the image (default: 'Describe this image in detail')"
|
|
3922
|
+
}
|
|
3923
|
+
},
|
|
3924
|
+
required: ["image"]
|
|
3925
|
+
},
|
|
3926
|
+
async (params) => {
|
|
3927
|
+
const key = apiKey || process.env.LLM_API_KEY;
|
|
3928
|
+
if (!key) {
|
|
3929
|
+
return "Error: No LLM_API_KEY configured. Set LLM_API_KEY env var for vision analysis.";
|
|
3930
|
+
}
|
|
3931
|
+
const image = params.image;
|
|
3932
|
+
const prompt = params.prompt || "Describe this image in detail";
|
|
3933
|
+
if (!image) {
|
|
3934
|
+
return "Error: 'image' parameter is required (URL or base64 data URI).";
|
|
3935
|
+
}
|
|
3936
|
+
try {
|
|
3937
|
+
const resp = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
3938
|
+
method: "POST",
|
|
3939
|
+
headers: {
|
|
3940
|
+
"Authorization": `Bearer ${key}`,
|
|
3941
|
+
"Content-Type": "application/json"
|
|
3942
|
+
},
|
|
3943
|
+
body: JSON.stringify({
|
|
3944
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
3945
|
+
messages: [
|
|
3946
|
+
{
|
|
3947
|
+
role: "user",
|
|
3948
|
+
content: [
|
|
3949
|
+
{ type: "image_url", image_url: { url: image } },
|
|
3950
|
+
{ type: "text", text: prompt }
|
|
3951
|
+
]
|
|
3952
|
+
}
|
|
3953
|
+
]
|
|
3954
|
+
})
|
|
3955
|
+
});
|
|
3956
|
+
if (!resp.ok) {
|
|
3957
|
+
const body = await resp.text();
|
|
3958
|
+
return `Vision API error (${resp.status}): ${body}`;
|
|
3959
|
+
}
|
|
3960
|
+
const data = await resp.json();
|
|
3961
|
+
if (data.error) {
|
|
3962
|
+
return `Vision API error: ${data.error.message}`;
|
|
3963
|
+
}
|
|
3964
|
+
const content = data.choices?.[0]?.message?.content;
|
|
3965
|
+
if (!content) {
|
|
3966
|
+
return "Error: No response content from vision model.";
|
|
3967
|
+
}
|
|
3968
|
+
return content;
|
|
3969
|
+
} catch (err) {
|
|
3970
|
+
return `Vision analysis error: ${err.message}`;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
// packages/runtime/src/tools/git.ts
|
|
3977
|
+
import { execSync as execSync3 } from "child_process";
|
|
3978
|
+
import { resolve as resolve10, normalize } from "path";
|
|
3979
|
+
function resolveRepoPath(workspaceDir, repoPath) {
|
|
3980
|
+
if (!repoPath) return workspaceDir;
|
|
3981
|
+
const resolved = resolve10(workspaceDir, repoPath);
|
|
3982
|
+
const normalizedResolved = normalize(resolved);
|
|
3983
|
+
const normalizedWorkspace = normalize(workspaceDir);
|
|
3984
|
+
if (!normalizedResolved.startsWith(normalizedWorkspace)) {
|
|
3985
|
+
throw new Error("Path traversal detected: repoPath must be within the workspace.");
|
|
3986
|
+
}
|
|
3987
|
+
return resolved;
|
|
3988
|
+
}
|
|
3989
|
+
function runGit(args, cwd) {
|
|
3990
|
+
const output = execSync3(`git ${args}`, {
|
|
3991
|
+
cwd,
|
|
3992
|
+
timeout: 3e4,
|
|
3993
|
+
encoding: "utf-8",
|
|
3994
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
3995
|
+
env: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}` }
|
|
3996
|
+
});
|
|
3997
|
+
return output;
|
|
3998
|
+
}
|
|
3999
|
+
function registerGitTools(registry, workspaceDir) {
|
|
4000
|
+
registry.register(
|
|
4001
|
+
"git_status",
|
|
4002
|
+
"Get repository status: modified, staged, and untracked files in a structured format.",
|
|
4003
|
+
{
|
|
4004
|
+
type: "object",
|
|
4005
|
+
properties: {
|
|
4006
|
+
repoPath: {
|
|
4007
|
+
type: "string",
|
|
4008
|
+
description: "Path to the git repository (relative to workspace). Defaults to workspace root."
|
|
4009
|
+
}
|
|
4010
|
+
},
|
|
4011
|
+
required: []
|
|
4012
|
+
},
|
|
4013
|
+
async (params) => {
|
|
4014
|
+
try {
|
|
4015
|
+
const cwd = resolveRepoPath(workspaceDir, params.repoPath);
|
|
4016
|
+
const output = runGit("status --porcelain", cwd);
|
|
4017
|
+
if (!output.trim()) {
|
|
4018
|
+
return "Working tree clean \u2014 no changes.";
|
|
4019
|
+
}
|
|
4020
|
+
const staged = [];
|
|
4021
|
+
const modified = [];
|
|
4022
|
+
const untracked = [];
|
|
4023
|
+
for (const line of output.split("\n")) {
|
|
4024
|
+
if (!line.trim()) continue;
|
|
4025
|
+
const index = line[0];
|
|
4026
|
+
const worktree = line[1];
|
|
4027
|
+
const file = line.slice(3);
|
|
4028
|
+
if (index === "?" && worktree === "?") {
|
|
4029
|
+
untracked.push(file);
|
|
4030
|
+
} else {
|
|
4031
|
+
if (index !== " " && index !== "?") staged.push(`${index} ${file}`);
|
|
4032
|
+
if (worktree !== " " && worktree !== "?") modified.push(`${worktree} ${file}`);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
const sections = [];
|
|
4036
|
+
if (staged.length) sections.push(`Staged (${staged.length}):
|
|
4037
|
+
${staged.join("\n")}`);
|
|
4038
|
+
if (modified.length) sections.push(`Modified (${modified.length}):
|
|
4039
|
+
${modified.join("\n")}`);
|
|
4040
|
+
if (untracked.length) sections.push(`Untracked (${untracked.length}):
|
|
4041
|
+
${untracked.join("\n")}`);
|
|
4042
|
+
return sections.join("\n\n");
|
|
4043
|
+
} catch (err) {
|
|
4044
|
+
return `git_status error: ${err.message}`;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
);
|
|
4048
|
+
registry.register(
|
|
4049
|
+
"git_diff",
|
|
4050
|
+
"Show changes in the working directory or between commits. Returns a diff output.",
|
|
4051
|
+
{
|
|
4052
|
+
type: "object",
|
|
4053
|
+
properties: {
|
|
4054
|
+
repoPath: {
|
|
4055
|
+
type: "string",
|
|
4056
|
+
description: "Path to the git repository (relative to workspace). Defaults to workspace root."
|
|
4057
|
+
},
|
|
4058
|
+
target: {
|
|
4059
|
+
type: "string",
|
|
4060
|
+
description: "Diff target: a branch name, commit hash, or ref like 'HEAD~1' or 'main'."
|
|
4061
|
+
},
|
|
4062
|
+
staged: {
|
|
4063
|
+
type: "boolean",
|
|
4064
|
+
description: "If true, show staged changes (--staged). Default: false."
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
required: []
|
|
4068
|
+
},
|
|
4069
|
+
async (params) => {
|
|
4070
|
+
try {
|
|
4071
|
+
const cwd = resolveRepoPath(workspaceDir, params.repoPath);
|
|
4072
|
+
const args = ["diff"];
|
|
4073
|
+
if (params.staged) args.push("--staged");
|
|
4074
|
+
if (params.target) args.push(params.target);
|
|
4075
|
+
let output = runGit(args.join(" "), cwd);
|
|
4076
|
+
if (!output.trim()) {
|
|
4077
|
+
return "No changes.";
|
|
4078
|
+
}
|
|
4079
|
+
if (output.length > 2e4) {
|
|
4080
|
+
output = output.slice(0, 2e4) + `
|
|
4081
|
+
... (truncated, ${output.length} total chars)`;
|
|
4082
|
+
}
|
|
4083
|
+
return output;
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
return `git_diff error: ${err.message}`;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
);
|
|
4089
|
+
registry.register(
|
|
4090
|
+
"git_commit",
|
|
4091
|
+
"Stage and commit changes to the git repository.",
|
|
4092
|
+
{
|
|
4093
|
+
type: "object",
|
|
4094
|
+
properties: {
|
|
4095
|
+
message: {
|
|
4096
|
+
type: "string",
|
|
4097
|
+
description: "The commit message."
|
|
4098
|
+
},
|
|
4099
|
+
repoPath: {
|
|
4100
|
+
type: "string",
|
|
4101
|
+
description: "Path to the git repository (relative to workspace). Defaults to workspace root."
|
|
4102
|
+
},
|
|
4103
|
+
all: {
|
|
4104
|
+
type: "boolean",
|
|
4105
|
+
description: "If true, stage all tracked changes (-a). Default: false."
|
|
4106
|
+
},
|
|
4107
|
+
files: {
|
|
4108
|
+
type: "array",
|
|
4109
|
+
items: { type: "string" },
|
|
4110
|
+
description: "Specific files to stage before committing."
|
|
4111
|
+
}
|
|
4112
|
+
},
|
|
4113
|
+
required: ["message"]
|
|
4114
|
+
},
|
|
4115
|
+
async (params) => {
|
|
4116
|
+
try {
|
|
4117
|
+
const cwd = resolveRepoPath(workspaceDir, params.repoPath);
|
|
4118
|
+
const message = params.message;
|
|
4119
|
+
const files = params.files;
|
|
4120
|
+
if (files && files.length > 0) {
|
|
4121
|
+
const fileArgs = files.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(" ");
|
|
4122
|
+
runGit(`add ${fileArgs}`, cwd);
|
|
4123
|
+
}
|
|
4124
|
+
const commitArgs = ["commit"];
|
|
4125
|
+
if (params.all) commitArgs.push("-a");
|
|
4126
|
+
commitArgs.push("-m", `"${message.replace(/"/g, '\\"')}"`);
|
|
4127
|
+
const output = runGit(commitArgs.join(" "), cwd);
|
|
4128
|
+
const firstLine = output.split("\n")[0] || "";
|
|
4129
|
+
return `Committed: ${firstLine.trim()}`;
|
|
4130
|
+
} catch (err) {
|
|
4131
|
+
const stderr = err.stderr?.toString() || "";
|
|
4132
|
+
const stdout = err.stdout?.toString() || "";
|
|
4133
|
+
return `git_commit error: ${(stdout + "\n" + stderr).trim() || err.message}`;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
);
|
|
4137
|
+
registry.register(
|
|
4138
|
+
"git_log",
|
|
4139
|
+
"Show recent commit history in a concise one-line-per-commit format.",
|
|
4140
|
+
{
|
|
4141
|
+
type: "object",
|
|
4142
|
+
properties: {
|
|
4143
|
+
repoPath: {
|
|
4144
|
+
type: "string",
|
|
4145
|
+
description: "Path to the git repository (relative to workspace). Defaults to workspace root."
|
|
4146
|
+
},
|
|
4147
|
+
count: {
|
|
4148
|
+
type: "number",
|
|
4149
|
+
description: "Number of recent commits to show (default: 10)."
|
|
4150
|
+
}
|
|
4151
|
+
},
|
|
4152
|
+
required: []
|
|
4153
|
+
},
|
|
4154
|
+
async (params) => {
|
|
4155
|
+
try {
|
|
4156
|
+
const cwd = resolveRepoPath(workspaceDir, params.repoPath);
|
|
4157
|
+
const count = Math.min(Math.max(params.count || 10, 1), 100);
|
|
4158
|
+
const output = runGit(`log --oneline -n ${count}`, cwd);
|
|
4159
|
+
if (!output.trim()) {
|
|
4160
|
+
return "No commits found.";
|
|
4161
|
+
}
|
|
4162
|
+
return output.trim();
|
|
4163
|
+
} catch (err) {
|
|
4164
|
+
return `git_log error: ${err.message}`;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
);
|
|
4168
|
+
registry.register(
|
|
4169
|
+
"git_push",
|
|
4170
|
+
"Push commits to a remote repository.",
|
|
4171
|
+
{
|
|
4172
|
+
type: "object",
|
|
4173
|
+
properties: {
|
|
4174
|
+
repoPath: {
|
|
4175
|
+
type: "string",
|
|
4176
|
+
description: "Path to the git repository (relative to workspace). Defaults to workspace root."
|
|
4177
|
+
},
|
|
4178
|
+
remote: {
|
|
4179
|
+
type: "string",
|
|
4180
|
+
description: "Remote name (default: 'origin')."
|
|
4181
|
+
},
|
|
4182
|
+
branch: {
|
|
4183
|
+
type: "string",
|
|
4184
|
+
description: "Branch to push. Defaults to the current branch."
|
|
4185
|
+
}
|
|
4186
|
+
},
|
|
4187
|
+
required: []
|
|
4188
|
+
},
|
|
4189
|
+
async (params) => {
|
|
4190
|
+
try {
|
|
4191
|
+
const cwd = resolveRepoPath(workspaceDir, params.repoPath);
|
|
4192
|
+
const remote = params.remote || "origin";
|
|
4193
|
+
const args = ["push", remote];
|
|
4194
|
+
if (params.branch) {
|
|
4195
|
+
args.push(params.branch);
|
|
4196
|
+
}
|
|
4197
|
+
const output = runGit(args.join(" "), cwd);
|
|
4198
|
+
return `Push successful.${output.trim() ? "\n" + output.trim() : ""}`;
|
|
4199
|
+
} catch (err) {
|
|
4200
|
+
const stderr = err.stderr?.toString() || "";
|
|
4201
|
+
const stdout = err.stdout?.toString() || "";
|
|
4202
|
+
const combined = (stdout + "\n" + stderr).trim();
|
|
4203
|
+
if (err.status === 0 || combined.includes("->")) {
|
|
4204
|
+
return `Push successful.
|
|
4205
|
+
${combined}`;
|
|
4206
|
+
}
|
|
4207
|
+
return `git_push error: ${combined || err.message}`;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
);
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
// packages/runtime/src/tools/browser.ts
|
|
4214
|
+
import { resolve as resolve11 } from "path";
|
|
4215
|
+
import { mkdirSync as mkdirSync8, existsSync as existsSync10 } from "fs";
|
|
4216
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4217
|
+
var MAX_OUTPUT2 = 5e4;
|
|
4218
|
+
var browserInstance = null;
|
|
4219
|
+
var lastUsed = 0;
|
|
4220
|
+
var idleTimer = null;
|
|
4221
|
+
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4222
|
+
async function getBrowser() {
|
|
4223
|
+
try {
|
|
4224
|
+
const modName = "playwright";
|
|
4225
|
+
const pw = await Function("m", "return import(m)")(modName);
|
|
4226
|
+
if (!browserInstance) {
|
|
4227
|
+
browserInstance = await pw.chromium.launch({ headless: true });
|
|
4228
|
+
if (!idleTimer) {
|
|
4229
|
+
idleTimer = setInterval(async () => {
|
|
4230
|
+
if (browserInstance && Date.now() - lastUsed > IDLE_TIMEOUT_MS) {
|
|
4231
|
+
await closeBrowser();
|
|
4232
|
+
}
|
|
4233
|
+
}, 6e4);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
lastUsed = Date.now();
|
|
4237
|
+
return browserInstance;
|
|
4238
|
+
} catch {
|
|
4239
|
+
throw new Error(
|
|
4240
|
+
"Playwright is not installed. Run:\n npm install playwright\n npx playwright install chromium"
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
async function closeBrowser() {
|
|
4245
|
+
if (browserInstance) {
|
|
4246
|
+
try {
|
|
4247
|
+
await browserInstance.close();
|
|
4248
|
+
} catch {
|
|
4249
|
+
}
|
|
4250
|
+
browserInstance = null;
|
|
4251
|
+
}
|
|
4252
|
+
if (idleTimer) {
|
|
4253
|
+
clearInterval(idleTimer);
|
|
4254
|
+
idleTimer = null;
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
function registerBrowserTools(registry, workspaceDir) {
|
|
4258
|
+
const screenshotDir = resolve11(workspaceDir, "screenshots");
|
|
4259
|
+
registry.register(
|
|
4260
|
+
"browse",
|
|
4261
|
+
[
|
|
4262
|
+
"Navigate to a URL and interact with the page using a headless browser.",
|
|
4263
|
+
"Actions:",
|
|
4264
|
+
" extract (default) \u2014 get the readable text content of the page",
|
|
4265
|
+
" screenshot \u2014 take a PNG screenshot and return the file path",
|
|
4266
|
+
" click \u2014 click an element matching a CSS selector",
|
|
4267
|
+
" type \u2014 type text into an element matching a CSS selector",
|
|
4268
|
+
" evaluate \u2014 run arbitrary JavaScript in the page and return the result",
|
|
4269
|
+
"Supports waitForSelector to wait for dynamic content before acting."
|
|
4270
|
+
].join("\n"),
|
|
4271
|
+
{
|
|
4272
|
+
type: "object",
|
|
4273
|
+
properties: {
|
|
4274
|
+
url: {
|
|
4275
|
+
type: "string",
|
|
4276
|
+
description: "URL to navigate to."
|
|
4277
|
+
},
|
|
4278
|
+
action: {
|
|
4279
|
+
type: "string",
|
|
4280
|
+
enum: ["extract", "screenshot", "click", "type", "evaluate"],
|
|
4281
|
+
description: "Action to perform (default: extract)."
|
|
4282
|
+
},
|
|
4283
|
+
selector: {
|
|
4284
|
+
type: "string",
|
|
4285
|
+
description: "CSS selector for click/type actions."
|
|
4286
|
+
},
|
|
4287
|
+
text: {
|
|
4288
|
+
type: "string",
|
|
4289
|
+
description: "Text to type (for 'type' action)."
|
|
4290
|
+
},
|
|
4291
|
+
javascript: {
|
|
4292
|
+
type: "string",
|
|
4293
|
+
description: "JavaScript to evaluate in the page (for 'evaluate' action)."
|
|
4294
|
+
},
|
|
4295
|
+
waitForSelector: {
|
|
4296
|
+
type: "string",
|
|
4297
|
+
description: "CSS selector to wait for before performing the action."
|
|
4298
|
+
},
|
|
4299
|
+
timeout: {
|
|
4300
|
+
type: "number",
|
|
4301
|
+
description: "Timeout in milliseconds (default: 30000)."
|
|
4302
|
+
}
|
|
4303
|
+
},
|
|
4304
|
+
required: ["url"]
|
|
4305
|
+
},
|
|
4306
|
+
async (params) => {
|
|
4307
|
+
const url = params.url;
|
|
4308
|
+
const action = params.action || "extract";
|
|
4309
|
+
const selector = params.selector;
|
|
4310
|
+
const text = params.text;
|
|
4311
|
+
const javascript = params.javascript;
|
|
4312
|
+
const waitForSelector = params.waitForSelector;
|
|
4313
|
+
const timeout = params.timeout || 3e4;
|
|
4314
|
+
let browser;
|
|
4315
|
+
try {
|
|
4316
|
+
browser = await getBrowser();
|
|
4317
|
+
} catch (err) {
|
|
4318
|
+
return err.message;
|
|
4319
|
+
}
|
|
4320
|
+
let page;
|
|
4321
|
+
try {
|
|
4322
|
+
page = await browser.newPage();
|
|
4323
|
+
page.setDefaultTimeout(timeout);
|
|
4324
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
4325
|
+
if (waitForSelector) {
|
|
4326
|
+
await page.waitForSelector(waitForSelector, { timeout });
|
|
4327
|
+
}
|
|
4328
|
+
switch (action) {
|
|
4329
|
+
case "extract": {
|
|
4330
|
+
const content = await page.evaluate(() => {
|
|
4331
|
+
const clone = document.body.cloneNode(true);
|
|
4332
|
+
clone.querySelectorAll("script, style, noscript").forEach((el) => el.remove());
|
|
4333
|
+
return clone.innerText || clone.textContent || "";
|
|
4334
|
+
});
|
|
4335
|
+
const trimmed = content.length > MAX_OUTPUT2 ? content.slice(0, MAX_OUTPUT2) + `
|
|
4336
|
+
... (truncated, ${content.length} total chars)` : content;
|
|
4337
|
+
return trimmed;
|
|
4338
|
+
}
|
|
4339
|
+
case "screenshot": {
|
|
4340
|
+
if (!existsSync10(screenshotDir)) {
|
|
4341
|
+
mkdirSync8(screenshotDir, { recursive: true });
|
|
4342
|
+
}
|
|
4343
|
+
const filename = `${randomUUID5()}.png`;
|
|
4344
|
+
const filepath = resolve11(screenshotDir, filename);
|
|
4345
|
+
await page.screenshot({ path: filepath, fullPage: true });
|
|
4346
|
+
return `Screenshot saved: ${filepath}`;
|
|
4347
|
+
}
|
|
4348
|
+
case "click": {
|
|
4349
|
+
if (!selector) {
|
|
4350
|
+
return "Error: 'selector' parameter is required for click action.";
|
|
4351
|
+
}
|
|
4352
|
+
await page.click(selector);
|
|
4353
|
+
return `Clicked: ${selector}`;
|
|
4354
|
+
}
|
|
4355
|
+
case "type": {
|
|
4356
|
+
if (!selector) {
|
|
4357
|
+
return "Error: 'selector' parameter is required for type action.";
|
|
4358
|
+
}
|
|
4359
|
+
if (text === void 0) {
|
|
4360
|
+
return "Error: 'text' parameter is required for type action.";
|
|
4361
|
+
}
|
|
4362
|
+
await page.fill(selector, text);
|
|
4363
|
+
return `Typed into ${selector}: "${text}"`;
|
|
4364
|
+
}
|
|
4365
|
+
case "evaluate": {
|
|
4366
|
+
if (!javascript) {
|
|
4367
|
+
return "Error: 'javascript' parameter is required for evaluate action.";
|
|
4368
|
+
}
|
|
4369
|
+
const result = await page.evaluate(javascript);
|
|
4370
|
+
const str = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
4371
|
+
if (str.length > MAX_OUTPUT2) {
|
|
4372
|
+
return str.slice(0, MAX_OUTPUT2) + `
|
|
4373
|
+
... (truncated, ${str.length} total chars)`;
|
|
4374
|
+
}
|
|
4375
|
+
return str ?? "(no result)";
|
|
4376
|
+
}
|
|
4377
|
+
default:
|
|
4378
|
+
return `Unknown action: ${action}. Use: extract, screenshot, click, type, evaluate.`;
|
|
4379
|
+
}
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
return `browse error: ${err.message}`;
|
|
4382
|
+
} finally {
|
|
4383
|
+
if (page) {
|
|
4384
|
+
try {
|
|
4385
|
+
await page.close();
|
|
4386
|
+
} catch {
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// packages/runtime/src/tools/system.ts
|
|
4395
|
+
import { execSync as execSync4 } from "child_process";
|
|
4396
|
+
import * as os from "os";
|
|
4397
|
+
var MAX_OUTPUT3 = 5e4;
|
|
4398
|
+
function truncate(output) {
|
|
4399
|
+
if (output.length > MAX_OUTPUT3) {
|
|
4400
|
+
return output.slice(0, MAX_OUTPUT3) + `
|
|
4401
|
+
... (truncated, ${output.length} total chars)`;
|
|
4402
|
+
}
|
|
4403
|
+
return output;
|
|
4404
|
+
}
|
|
4405
|
+
function exec(cmd, timeoutS = 10) {
|
|
4406
|
+
return execSync4(cmd, {
|
|
4407
|
+
encoding: "utf-8",
|
|
4408
|
+
timeout: timeoutS * 1e3,
|
|
4409
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
4410
|
+
shell: "/bin/sh",
|
|
4411
|
+
env: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}` }
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
function registerSystemTools(registry) {
|
|
4415
|
+
registry.register(
|
|
4416
|
+
"system_info",
|
|
4417
|
+
"Get system information: hostname, OS, architecture, uptime, CPU, memory, and disk usage.",
|
|
4418
|
+
{
|
|
4419
|
+
type: "object",
|
|
4420
|
+
properties: {},
|
|
4421
|
+
required: []
|
|
4422
|
+
},
|
|
4423
|
+
async () => {
|
|
4424
|
+
try {
|
|
4425
|
+
const cpus2 = os.cpus();
|
|
4426
|
+
const cpuModel = cpus2.length > 0 ? cpus2[0].model : "unknown";
|
|
4427
|
+
const totalMem = os.totalmem();
|
|
4428
|
+
const freeMem = os.freemem();
|
|
4429
|
+
let disk = "unavailable";
|
|
4430
|
+
try {
|
|
4431
|
+
const dfOut = exec("df -h /");
|
|
4432
|
+
const lines = dfOut.trim().split("\n");
|
|
4433
|
+
if (lines.length >= 2) {
|
|
4434
|
+
const parts = lines[1].split(/\s+/);
|
|
4435
|
+
disk = `total=${parts[1]}, used=${parts[2]}, free=${parts[3]}`;
|
|
4436
|
+
}
|
|
4437
|
+
} catch {
|
|
4438
|
+
}
|
|
4439
|
+
const info = [
|
|
4440
|
+
`hostname: ${os.hostname()}`,
|
|
4441
|
+
`os: ${os.platform()} ${os.release()}`,
|
|
4442
|
+
`arch: ${os.arch()}`,
|
|
4443
|
+
`uptime: ${Math.floor(os.uptime() / 3600)}h ${Math.floor(os.uptime() % 3600 / 60)}m`,
|
|
4444
|
+
`cpu: ${cpuModel} (${cpus2.length} cores)`,
|
|
4445
|
+
`memory: ${fmt(freeMem)} free / ${fmt(totalMem)} total`,
|
|
4446
|
+
`disk (/): ${disk}`
|
|
4447
|
+
];
|
|
4448
|
+
return info.join("\n");
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
return `Error getting system info: ${err.message}`;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
);
|
|
4454
|
+
registry.register(
|
|
4455
|
+
"process_list",
|
|
4456
|
+
"List running processes. Optionally filter by name and limit results.",
|
|
4457
|
+
{
|
|
4458
|
+
type: "object",
|
|
4459
|
+
properties: {
|
|
4460
|
+
filter: {
|
|
4461
|
+
type: "string",
|
|
4462
|
+
description: "Filter processes by name (grep pattern)"
|
|
4463
|
+
},
|
|
4464
|
+
limit: {
|
|
4465
|
+
type: "number",
|
|
4466
|
+
description: "Maximum number of processes to return (default 20)"
|
|
4467
|
+
}
|
|
4468
|
+
},
|
|
4469
|
+
required: []
|
|
4470
|
+
},
|
|
4471
|
+
async (params) => {
|
|
4472
|
+
const filter = params.filter;
|
|
4473
|
+
const limit = params.limit || 20;
|
|
4474
|
+
try {
|
|
4475
|
+
let cmd;
|
|
4476
|
+
if (filter) {
|
|
4477
|
+
cmd = `ps aux | grep -i '${filter.replace(/'/g, "\\'")}' | grep -v grep | head -n ${limit}`;
|
|
4478
|
+
} else {
|
|
4479
|
+
cmd = `ps aux -r | head -n ${limit + 1}`;
|
|
4480
|
+
}
|
|
4481
|
+
const output = exec(cmd, 15);
|
|
4482
|
+
return truncate(output) || "(no matching processes)";
|
|
4483
|
+
} catch (err) {
|
|
4484
|
+
return `Error listing processes: ${err.message}`;
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
);
|
|
4488
|
+
registry.register(
|
|
4489
|
+
"process_kill",
|
|
4490
|
+
"Kill a process by PID. Refuses to kill PID 1 or the agent's own process.",
|
|
4491
|
+
{
|
|
4492
|
+
type: "object",
|
|
4493
|
+
properties: {
|
|
4494
|
+
pid: {
|
|
4495
|
+
type: "number",
|
|
4496
|
+
description: "Process ID to kill"
|
|
4497
|
+
},
|
|
4498
|
+
signal: {
|
|
4499
|
+
type: "string",
|
|
4500
|
+
description: "Signal to send (default: TERM). Examples: TERM, KILL, HUP, INT"
|
|
4501
|
+
}
|
|
4502
|
+
},
|
|
4503
|
+
required: ["pid"]
|
|
4504
|
+
},
|
|
4505
|
+
async (params) => {
|
|
4506
|
+
const pid = params.pid;
|
|
4507
|
+
const signal = params.signal || "TERM";
|
|
4508
|
+
if (pid === 1) {
|
|
4509
|
+
return "Error: Refusing to kill PID 1 (init/launchd).";
|
|
4510
|
+
}
|
|
4511
|
+
if (pid === process.pid) {
|
|
4512
|
+
return "Error: Refusing to kill the agent's own process.";
|
|
4513
|
+
}
|
|
4514
|
+
try {
|
|
4515
|
+
const sig = signal.toUpperCase().startsWith("SIG") ? signal.toUpperCase() : `SIG${signal.toUpperCase()}`;
|
|
4516
|
+
process.kill(pid, sig);
|
|
4517
|
+
return `Sent ${sig} to PID ${pid}.`;
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
return `Error killing process ${pid}: ${err.message}`;
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
);
|
|
4523
|
+
registry.register(
|
|
4524
|
+
"service_control",
|
|
4525
|
+
"Control launchd services on macOS. List, start, stop, restart, or check status of services.",
|
|
4526
|
+
{
|
|
4527
|
+
type: "object",
|
|
4528
|
+
properties: {
|
|
4529
|
+
action: {
|
|
4530
|
+
type: "string",
|
|
4531
|
+
enum: ["start", "stop", "restart", "status", "list"],
|
|
4532
|
+
description: "Action to perform"
|
|
4533
|
+
},
|
|
4534
|
+
service: {
|
|
4535
|
+
type: "string",
|
|
4536
|
+
description: "Service label (required for start/stop/restart/status)"
|
|
4537
|
+
}
|
|
4538
|
+
},
|
|
4539
|
+
required: ["action"]
|
|
4540
|
+
},
|
|
4541
|
+
async (params) => {
|
|
4542
|
+
const action = params.action;
|
|
4543
|
+
const service = params.service;
|
|
4544
|
+
try {
|
|
4545
|
+
switch (action) {
|
|
4546
|
+
case "list": {
|
|
4547
|
+
const filter = service || "hivemind";
|
|
4548
|
+
const output = exec(`launchctl list | grep -i '${filter.replace(/'/g, "\\'")}'`, 15);
|
|
4549
|
+
return truncate(output) || `(no services matching '${filter}')`;
|
|
4550
|
+
}
|
|
4551
|
+
case "status": {
|
|
4552
|
+
if (!service) return "Error: 'service' parameter required for status.";
|
|
4553
|
+
const output = exec(`launchctl list '${service.replace(/'/g, "\\'")}'`, 15);
|
|
4554
|
+
return truncate(output);
|
|
4555
|
+
}
|
|
4556
|
+
case "start": {
|
|
4557
|
+
if (!service) return "Error: 'service' parameter required for start.";
|
|
4558
|
+
exec(`launchctl start '${service.replace(/'/g, "\\'")}'`);
|
|
4559
|
+
return `Started service: ${service}`;
|
|
4560
|
+
}
|
|
4561
|
+
case "stop": {
|
|
4562
|
+
if (!service) return "Error: 'service' parameter required for stop.";
|
|
4563
|
+
exec(`launchctl stop '${service.replace(/'/g, "\\'")}'`);
|
|
4564
|
+
return `Stopped service: ${service}`;
|
|
4565
|
+
}
|
|
4566
|
+
case "restart": {
|
|
4567
|
+
if (!service) return "Error: 'service' parameter required for restart.";
|
|
4568
|
+
const escaped = service.replace(/'/g, "\\'");
|
|
4569
|
+
exec(`launchctl stop '${escaped}'`);
|
|
4570
|
+
exec(`launchctl start '${escaped}'`);
|
|
4571
|
+
return `Restarted service: ${service}`;
|
|
4572
|
+
}
|
|
4573
|
+
default:
|
|
4574
|
+
return `Error: Unknown action '${action}'. Use: start, stop, restart, status, list.`;
|
|
4575
|
+
}
|
|
4576
|
+
} catch (err) {
|
|
4577
|
+
return `Error controlling service: ${err.message}`;
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
);
|
|
4581
|
+
registry.register(
|
|
4582
|
+
"disk_usage",
|
|
4583
|
+
"Get disk usage for a directory. Returns human-readable sizes.",
|
|
4584
|
+
{
|
|
4585
|
+
type: "object",
|
|
4586
|
+
properties: {
|
|
4587
|
+
path: {
|
|
4588
|
+
type: "string",
|
|
4589
|
+
description: "Directory path (default: /)"
|
|
4590
|
+
},
|
|
4591
|
+
depth: {
|
|
4592
|
+
type: "number",
|
|
4593
|
+
description: "Directory depth to report (default: 1)"
|
|
4594
|
+
}
|
|
4595
|
+
},
|
|
4596
|
+
required: []
|
|
4597
|
+
},
|
|
4598
|
+
async (params) => {
|
|
4599
|
+
const target = params.path || "/";
|
|
4600
|
+
const depth = params.depth || 1;
|
|
4601
|
+
try {
|
|
4602
|
+
const output = exec(`du -d ${depth} -h '${target.replace(/'/g, "\\'")}'`, 30);
|
|
4603
|
+
return truncate(output) || "(no output)";
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
return `Error getting disk usage: ${err.message}`;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
);
|
|
4609
|
+
registry.register(
|
|
4610
|
+
"network_info",
|
|
4611
|
+
"Get network interface information and external IP address.",
|
|
4612
|
+
{
|
|
4613
|
+
type: "object",
|
|
4614
|
+
properties: {},
|
|
4615
|
+
required: []
|
|
4616
|
+
},
|
|
4617
|
+
async () => {
|
|
4618
|
+
try {
|
|
4619
|
+
const lines = [];
|
|
4620
|
+
const interfaces = os.networkInterfaces();
|
|
4621
|
+
lines.push("Local interfaces:");
|
|
4622
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
4623
|
+
if (!addrs) continue;
|
|
4624
|
+
for (const addr of addrs) {
|
|
4625
|
+
if (addr.family === "IPv4") {
|
|
4626
|
+
lines.push(` ${name}: ${addr.address}`);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
try {
|
|
4631
|
+
const extIp = exec("curl -s --max-time 5 ifconfig.me", 10).trim();
|
|
4632
|
+
lines.push(`
|
|
4633
|
+
External IP: ${extIp}`);
|
|
4634
|
+
} catch {
|
|
4635
|
+
lines.push("\nExternal IP: unavailable");
|
|
4636
|
+
}
|
|
4637
|
+
lines.push(`Hostname: ${os.hostname()}`);
|
|
4638
|
+
return lines.join("\n");
|
|
4639
|
+
} catch (err) {
|
|
4640
|
+
return `Error getting network info: ${err.message}`;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
);
|
|
4644
|
+
}
|
|
4645
|
+
function fmt(bytes) {
|
|
4646
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}GB`;
|
|
4647
|
+
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(0)}MB`;
|
|
4648
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// packages/runtime/src/tools/http-server.ts
|
|
4652
|
+
import { createServer as createServer2 } from "http";
|
|
4653
|
+
import { createReadStream, existsSync as existsSync11, statSync as statSync2 } from "fs";
|
|
4654
|
+
import { join as join6, extname, resolve as resolve12 } from "path";
|
|
4655
|
+
var MAX_BODY = 5e4;
|
|
4656
|
+
var activeServers = /* @__PURE__ */ new Map();
|
|
4657
|
+
var MIME_TYPES = {
|
|
4658
|
+
".html": "text/html",
|
|
4659
|
+
".css": "text/css",
|
|
4660
|
+
".js": "application/javascript",
|
|
4661
|
+
".json": "application/json",
|
|
4662
|
+
".png": "image/png",
|
|
4663
|
+
".jpg": "image/jpeg",
|
|
4664
|
+
".gif": "image/gif",
|
|
4665
|
+
".svg": "image/svg+xml",
|
|
4666
|
+
".txt": "text/plain"
|
|
4667
|
+
};
|
|
4668
|
+
function serveStatic(baseDir, req, res) {
|
|
4669
|
+
const urlPath = (req.url || "/").split("?")[0];
|
|
4670
|
+
let filePath = join6(baseDir, urlPath === "/" ? "index.html" : urlPath);
|
|
4671
|
+
if (!resolve12(filePath).startsWith(resolve12(baseDir))) {
|
|
4672
|
+
res.writeHead(403);
|
|
4673
|
+
res.end("Forbidden");
|
|
4674
|
+
return true;
|
|
4675
|
+
}
|
|
4676
|
+
if (!existsSync11(filePath)) return false;
|
|
4677
|
+
const stat = statSync2(filePath);
|
|
4678
|
+
if (stat.isDirectory()) {
|
|
4679
|
+
filePath = join6(filePath, "index.html");
|
|
4680
|
+
if (!existsSync11(filePath)) return false;
|
|
4681
|
+
}
|
|
4682
|
+
const ext = extname(filePath);
|
|
4683
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
4684
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
4685
|
+
createReadStream(filePath).pipe(res);
|
|
4686
|
+
return true;
|
|
4687
|
+
}
|
|
4688
|
+
function matchRoute(routes, req) {
|
|
4689
|
+
const method = (req.method || "GET").toUpperCase();
|
|
4690
|
+
const urlPath = (req.url || "/").split("?")[0];
|
|
4691
|
+
return routes.find(
|
|
4692
|
+
(r) => r.path === urlPath && r.method.toUpperCase() === method
|
|
4693
|
+
);
|
|
4694
|
+
}
|
|
4695
|
+
function registerHttpTools(registry, workspaceDir) {
|
|
4696
|
+
registry.register(
|
|
4697
|
+
"http_serve",
|
|
4698
|
+
"Start a simple HTTP server. Can serve static files from a directory and/or respond to configured routes. Returns the URL and port. The server persists until stopped with http_stop.",
|
|
4699
|
+
{
|
|
4700
|
+
type: "object",
|
|
4701
|
+
properties: {
|
|
4702
|
+
port: {
|
|
4703
|
+
type: "number",
|
|
4704
|
+
description: "Port to listen on (default: 8080)"
|
|
4705
|
+
},
|
|
4706
|
+
directory: {
|
|
4707
|
+
type: "string",
|
|
4708
|
+
description: "Serve static files from this directory (relative to workspace)"
|
|
4709
|
+
},
|
|
4710
|
+
routes: {
|
|
4711
|
+
type: "array",
|
|
4712
|
+
description: "Array of route handlers: { path, method, response }",
|
|
4713
|
+
items: {
|
|
4714
|
+
type: "object",
|
|
4715
|
+
properties: {
|
|
4716
|
+
path: { type: "string", description: "URL path (e.g. '/health')" },
|
|
4717
|
+
method: { type: "string", description: "HTTP method (e.g. 'GET', 'POST')" },
|
|
4718
|
+
response: { type: "string", description: "Response body to return" }
|
|
4719
|
+
},
|
|
4720
|
+
required: ["path", "method", "response"]
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
},
|
|
4724
|
+
required: []
|
|
4725
|
+
},
|
|
4726
|
+
async (params) => {
|
|
4727
|
+
const port = params.port || 8080;
|
|
4728
|
+
const directory = params.directory;
|
|
4729
|
+
const routes = params.routes || [];
|
|
4730
|
+
if (activeServers.has(port)) {
|
|
4731
|
+
return `Error: A server is already running on port ${port}. Stop it first with http_stop.`;
|
|
4732
|
+
}
|
|
4733
|
+
const staticDir = directory ? resolve12(workspaceDir, directory) : void 0;
|
|
4734
|
+
if (staticDir && !existsSync11(staticDir)) {
|
|
4735
|
+
return `Error: Directory not found: ${directory}`;
|
|
4736
|
+
}
|
|
4737
|
+
return new Promise((resolvePromise) => {
|
|
4738
|
+
const server = createServer2((req, res) => {
|
|
4739
|
+
const route = matchRoute(routes, req);
|
|
4740
|
+
if (route) {
|
|
4741
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
4742
|
+
res.end(route.response);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
if (staticDir && serveStatic(staticDir, req, res)) {
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
res.writeHead(404);
|
|
4749
|
+
res.end("Not Found");
|
|
4750
|
+
});
|
|
4751
|
+
server.on("error", (err) => {
|
|
4752
|
+
resolvePromise(`Error starting server: ${err.message}`);
|
|
4753
|
+
});
|
|
4754
|
+
server.listen(port, () => {
|
|
4755
|
+
activeServers.set(port, server);
|
|
4756
|
+
const parts = [`Server started on http://localhost:${port}`];
|
|
4757
|
+
if (staticDir) parts.push(`Serving static files from: ${directory}`);
|
|
4758
|
+
if (routes.length > 0) parts.push(`Routes: ${routes.map((r) => `${r.method} ${r.path}`).join(", ")}`);
|
|
4759
|
+
resolvePromise(parts.join("\n"));
|
|
4760
|
+
});
|
|
4761
|
+
});
|
|
4762
|
+
}
|
|
4763
|
+
);
|
|
4764
|
+
registry.register(
|
|
4765
|
+
"http_stop",
|
|
4766
|
+
"Stop a running HTTP server by port number.",
|
|
4767
|
+
{
|
|
4768
|
+
type: "object",
|
|
4769
|
+
properties: {
|
|
4770
|
+
port: {
|
|
4771
|
+
type: "number",
|
|
4772
|
+
description: "Port of the server to stop"
|
|
4773
|
+
}
|
|
4774
|
+
},
|
|
4775
|
+
required: ["port"]
|
|
4776
|
+
},
|
|
4777
|
+
async (params) => {
|
|
4778
|
+
const port = params.port;
|
|
4779
|
+
const server = activeServers.get(port);
|
|
4780
|
+
if (!server) {
|
|
4781
|
+
return `No server running on port ${port}. Active ports: ${activeServers.size > 0 ? [...activeServers.keys()].join(", ") : "none"}`;
|
|
4782
|
+
}
|
|
4783
|
+
return new Promise((resolvePromise) => {
|
|
4784
|
+
server.close((err) => {
|
|
4785
|
+
activeServers.delete(port);
|
|
4786
|
+
if (err) {
|
|
4787
|
+
resolvePromise(`Server on port ${port} stopped with warning: ${err.message}`);
|
|
4788
|
+
} else {
|
|
4789
|
+
resolvePromise(`Server on port ${port} stopped.`);
|
|
4790
|
+
}
|
|
4791
|
+
});
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
);
|
|
4795
|
+
registry.register(
|
|
4796
|
+
"http_request",
|
|
4797
|
+
"Make an HTTP request (like curl). Supports GET, POST, PUT, PATCH, DELETE. Returns status, headers, and body.",
|
|
4798
|
+
{
|
|
4799
|
+
type: "object",
|
|
4800
|
+
properties: {
|
|
4801
|
+
url: {
|
|
4802
|
+
type: "string",
|
|
4803
|
+
description: "The URL to request"
|
|
4804
|
+
},
|
|
4805
|
+
method: {
|
|
4806
|
+
type: "string",
|
|
4807
|
+
description: "HTTP method (default: 'GET')",
|
|
4808
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
|
4809
|
+
},
|
|
4810
|
+
headers: {
|
|
4811
|
+
type: "object",
|
|
4812
|
+
description: "Request headers as key-value pairs"
|
|
4813
|
+
},
|
|
4814
|
+
body: {
|
|
4815
|
+
type: "string",
|
|
4816
|
+
description: "Request body (for POST, PUT, PATCH)"
|
|
4817
|
+
},
|
|
4818
|
+
timeout: {
|
|
4819
|
+
type: "number",
|
|
4820
|
+
description: "Timeout in milliseconds (default: 30000)"
|
|
4821
|
+
}
|
|
4822
|
+
},
|
|
4823
|
+
required: ["url"]
|
|
4824
|
+
},
|
|
4825
|
+
async (params) => {
|
|
4826
|
+
const url = params.url;
|
|
4827
|
+
const method = (params.method || "GET").toUpperCase();
|
|
4828
|
+
const headers = params.headers || {};
|
|
4829
|
+
const body = params.body;
|
|
4830
|
+
const timeout = params.timeout || 3e4;
|
|
4831
|
+
try {
|
|
4832
|
+
const controller = new AbortController();
|
|
4833
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
4834
|
+
const response = await fetch(url, {
|
|
4835
|
+
method,
|
|
4836
|
+
headers,
|
|
4837
|
+
body: body || void 0,
|
|
4838
|
+
signal: controller.signal
|
|
4839
|
+
});
|
|
4840
|
+
clearTimeout(timer);
|
|
4841
|
+
const responseHeaders = {};
|
|
4842
|
+
response.headers.forEach((value, key) => {
|
|
4843
|
+
responseHeaders[key] = value;
|
|
4844
|
+
});
|
|
4845
|
+
let responseBody = await response.text();
|
|
4846
|
+
const truncated = responseBody.length > MAX_BODY;
|
|
4847
|
+
if (truncated) {
|
|
4848
|
+
responseBody = responseBody.slice(0, MAX_BODY);
|
|
4849
|
+
}
|
|
4850
|
+
const result = {
|
|
4851
|
+
status: response.status,
|
|
4852
|
+
statusText: response.statusText,
|
|
4853
|
+
headers: responseHeaders,
|
|
4854
|
+
body: responseBody,
|
|
4855
|
+
truncated
|
|
4856
|
+
};
|
|
4857
|
+
return JSON.stringify(result, null, 2);
|
|
4858
|
+
} catch (err) {
|
|
4859
|
+
const message = err.message;
|
|
4860
|
+
if (message.includes("abort")) {
|
|
4861
|
+
return `Error: Request timed out after ${timeout}ms`;
|
|
4862
|
+
}
|
|
4863
|
+
return `Error: ${message}`;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
// packages/runtime/src/tools/watch.ts
|
|
4870
|
+
import { watch as watch3, existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
4871
|
+
import { join as join7, resolve as resolve13 } from "path";
|
|
4872
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4873
|
+
var activeWatchers = /* @__PURE__ */ new Map();
|
|
4874
|
+
function registerWatchTools(registry, workspaceDir, dataDir) {
|
|
4875
|
+
const eventsDir = join7(dataDir, "events");
|
|
4876
|
+
registry.register(
|
|
4877
|
+
"watch_start",
|
|
4878
|
+
"Start watching a file or directory for changes. When a change is detected, an immediate event is written to the events directory so the agent can react. Optionally specify a Sesame channel to notify.",
|
|
4879
|
+
{
|
|
4880
|
+
type: "object",
|
|
4881
|
+
properties: {
|
|
4882
|
+
path: {
|
|
4883
|
+
type: "string",
|
|
4884
|
+
description: "File or directory to watch (relative to workspace)"
|
|
4885
|
+
},
|
|
4886
|
+
id: {
|
|
4887
|
+
type: "string",
|
|
4888
|
+
description: "Optional identifier for this watcher (auto-generated if omitted)"
|
|
4889
|
+
},
|
|
4890
|
+
channelId: {
|
|
4891
|
+
type: "string",
|
|
4892
|
+
description: "Sesame channel to notify when changes are detected"
|
|
4893
|
+
}
|
|
4894
|
+
},
|
|
4895
|
+
required: ["path"]
|
|
4896
|
+
},
|
|
4897
|
+
async (params) => {
|
|
4898
|
+
const relPath = params.path;
|
|
4899
|
+
const id = params.id || randomUUID6().slice(0, 8);
|
|
4900
|
+
const channelId = params.channelId;
|
|
4901
|
+
if (activeWatchers.has(id)) {
|
|
4902
|
+
return `Error: A watcher with id '${id}' already exists. Stop it first or use a different id.`;
|
|
4903
|
+
}
|
|
4904
|
+
const absolutePath = resolve13(workspaceDir, relPath);
|
|
4905
|
+
if (!absolutePath.startsWith(resolve13(workspaceDir))) {
|
|
4906
|
+
return "Error: Path must be within the workspace directory.";
|
|
4907
|
+
}
|
|
4908
|
+
if (!existsSync12(absolutePath)) {
|
|
4909
|
+
return `Error: Path not found: ${relPath}`;
|
|
4910
|
+
}
|
|
4911
|
+
if (!existsSync12(eventsDir)) {
|
|
4912
|
+
mkdirSync9(eventsDir, { recursive: true });
|
|
4913
|
+
}
|
|
4914
|
+
try {
|
|
4915
|
+
let debounceTimer = null;
|
|
4916
|
+
const fsWatcher = watch3(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
4917
|
+
if (debounceTimer) return;
|
|
4918
|
+
debounceTimer = setTimeout(() => {
|
|
4919
|
+
debounceTimer = null;
|
|
4920
|
+
}, 500);
|
|
4921
|
+
const event = {
|
|
4922
|
+
type: "immediate",
|
|
4923
|
+
channelId: channelId || "default",
|
|
4924
|
+
text: `File watcher '${id}': ${eventType} detected on ${filename || relPath}`
|
|
4925
|
+
};
|
|
4926
|
+
const eventFile = join7(eventsDir, `watch-${id}-${Date.now()}.json`);
|
|
4927
|
+
try {
|
|
4928
|
+
writeFileSync5(eventFile, JSON.stringify(event, null, 2));
|
|
4929
|
+
} catch {
|
|
4930
|
+
}
|
|
4931
|
+
});
|
|
4932
|
+
const entry = {
|
|
4933
|
+
id,
|
|
4934
|
+
path: relPath,
|
|
4935
|
+
absolutePath,
|
|
4936
|
+
channelId,
|
|
4937
|
+
watcher: fsWatcher,
|
|
4938
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4939
|
+
};
|
|
4940
|
+
activeWatchers.set(id, entry);
|
|
4941
|
+
return `Watcher started: id='${id}', path='${relPath}'${channelId ? `, channel='${channelId}'` : ""}`;
|
|
4942
|
+
} catch (err) {
|
|
4943
|
+
return `Error starting watcher: ${err.message}`;
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
);
|
|
4947
|
+
registry.register(
|
|
4948
|
+
"watch_stop",
|
|
4949
|
+
"Stop a file watcher by its ID.",
|
|
4950
|
+
{
|
|
4951
|
+
type: "object",
|
|
4952
|
+
properties: {
|
|
4953
|
+
id: {
|
|
4954
|
+
type: "string",
|
|
4955
|
+
description: "Watcher ID to stop"
|
|
4956
|
+
}
|
|
4957
|
+
},
|
|
4958
|
+
required: ["id"]
|
|
4959
|
+
},
|
|
4960
|
+
async (params) => {
|
|
4961
|
+
const id = params.id;
|
|
4962
|
+
const entry = activeWatchers.get(id);
|
|
4963
|
+
if (!entry) {
|
|
4964
|
+
const ids = activeWatchers.size > 0 ? [...activeWatchers.keys()].join(", ") : "none";
|
|
4965
|
+
return `No watcher found with id '${id}'. Active watchers: ${ids}`;
|
|
4966
|
+
}
|
|
4967
|
+
entry.watcher.close();
|
|
4968
|
+
activeWatchers.delete(id);
|
|
4969
|
+
return `Watcher '${id}' stopped (was watching: ${entry.path}).`;
|
|
4970
|
+
}
|
|
4971
|
+
);
|
|
4972
|
+
registry.register(
|
|
4973
|
+
"watch_list",
|
|
4974
|
+
"List all active file watchers with their paths and IDs.",
|
|
4975
|
+
{
|
|
4976
|
+
type: "object",
|
|
4977
|
+
properties: {},
|
|
4978
|
+
required: []
|
|
4979
|
+
},
|
|
4980
|
+
async () => {
|
|
4981
|
+
if (activeWatchers.size === 0) {
|
|
4982
|
+
return "No active watchers.";
|
|
4983
|
+
}
|
|
4984
|
+
const lines = [];
|
|
4985
|
+
for (const [id, entry] of activeWatchers) {
|
|
4986
|
+
let line = `${id}: ${entry.path} (since ${entry.createdAt})`;
|
|
4987
|
+
if (entry.channelId) line += ` \u2192 channel: ${entry.channelId}`;
|
|
4988
|
+
lines.push(line);
|
|
4989
|
+
}
|
|
4990
|
+
return `${activeWatchers.size} active watcher(s):
|
|
4991
|
+
${lines.join("\n")}`;
|
|
4992
|
+
}
|
|
4993
|
+
);
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
// packages/runtime/src/tools/macos.ts
|
|
4997
|
+
import { execSync as execSync5 } from "child_process";
|
|
4998
|
+
import { resolve as resolve14, normalize as normalize2 } from "path";
|
|
4999
|
+
import { mkdirSync as mkdirSync10, existsSync as existsSync13 } from "fs";
|
|
5000
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
5001
|
+
var MAX_OUTPUT4 = 5e4;
|
|
5002
|
+
function shellExec(command, timeoutMs) {
|
|
5003
|
+
return execSync5(command, {
|
|
5004
|
+
timeout: timeoutMs,
|
|
5005
|
+
encoding: "utf-8",
|
|
5006
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
5007
|
+
shell: "/bin/sh",
|
|
5008
|
+
env: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}` }
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
5011
|
+
function registerMacOSTools(registry, workspaceDir) {
|
|
5012
|
+
registry.register(
|
|
5013
|
+
"run_applescript",
|
|
5014
|
+
"Execute AppleScript code via osascript. Enables controlling any Mac application (Finder, Safari, Mail, System Events, etc.).",
|
|
5015
|
+
{
|
|
5016
|
+
type: "object",
|
|
5017
|
+
properties: {
|
|
5018
|
+
script: {
|
|
5019
|
+
type: "string",
|
|
5020
|
+
description: "AppleScript code to execute."
|
|
5021
|
+
}
|
|
5022
|
+
},
|
|
5023
|
+
required: ["script"]
|
|
5024
|
+
},
|
|
5025
|
+
async (params) => {
|
|
5026
|
+
const script = params.script;
|
|
5027
|
+
try {
|
|
5028
|
+
const output = shellExec(`osascript -e ${escapeShellArg(script)}`, 15e3);
|
|
5029
|
+
const trimmed = output.length > MAX_OUTPUT4 ? output.slice(0, MAX_OUTPUT4) + `
|
|
5030
|
+
... (truncated, ${output.length} total chars)` : output;
|
|
5031
|
+
return trimmed || "(no output)";
|
|
5032
|
+
} catch (err) {
|
|
5033
|
+
const stderr = err.stderr?.toString() || "";
|
|
5034
|
+
const stdout = err.stdout?.toString() || "";
|
|
5035
|
+
const output = (stdout + "\n" + stderr).trim();
|
|
5036
|
+
return `AppleScript error (exit code ${err.status ?? "unknown"}):
|
|
5037
|
+
${output || err.message}`;
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
);
|
|
5041
|
+
registry.register(
|
|
5042
|
+
"notify",
|
|
5043
|
+
"Send a macOS notification with a title, message, and optional sound.",
|
|
5044
|
+
{
|
|
5045
|
+
type: "object",
|
|
5046
|
+
properties: {
|
|
5047
|
+
title: {
|
|
5048
|
+
type: "string",
|
|
5049
|
+
description: "Notification title."
|
|
5050
|
+
},
|
|
5051
|
+
message: {
|
|
5052
|
+
type: "string",
|
|
5053
|
+
description: "Notification body text."
|
|
5054
|
+
},
|
|
5055
|
+
sound: {
|
|
5056
|
+
type: "string",
|
|
5057
|
+
description: "Sound name (e.g. 'Glass', 'Ping', 'Pop'). Default: 'default'."
|
|
5058
|
+
}
|
|
5059
|
+
},
|
|
5060
|
+
required: ["title", "message"]
|
|
5061
|
+
},
|
|
5062
|
+
async (params) => {
|
|
5063
|
+
const title = params.title;
|
|
5064
|
+
const message = params.message;
|
|
5065
|
+
const sound = params.sound || "default";
|
|
5066
|
+
try {
|
|
5067
|
+
const script = `display notification ${escapeAppleString(message)} with title ${escapeAppleString(title)} sound name ${escapeAppleString(sound)}`;
|
|
5068
|
+
shellExec(`osascript -e ${escapeShellArg(script)}`, 1e4);
|
|
5069
|
+
return `Notification sent: "${title}"`;
|
|
5070
|
+
} catch (err) {
|
|
5071
|
+
const stderr = err.stderr?.toString() || "";
|
|
5072
|
+
return `Notification error: ${stderr.trim() || err.message}`;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
);
|
|
5076
|
+
registry.register(
|
|
5077
|
+
"clipboard_read",
|
|
5078
|
+
"Read the current contents of the macOS system clipboard.",
|
|
5079
|
+
{
|
|
5080
|
+
type: "object",
|
|
5081
|
+
properties: {},
|
|
5082
|
+
required: []
|
|
5083
|
+
},
|
|
5084
|
+
async () => {
|
|
5085
|
+
try {
|
|
5086
|
+
const output = shellExec("pbpaste", 5e3);
|
|
5087
|
+
if (!output) return "(clipboard is empty)";
|
|
5088
|
+
if (output.length > MAX_OUTPUT4) {
|
|
5089
|
+
return output.slice(0, MAX_OUTPUT4) + `
|
|
5090
|
+
... (truncated, ${output.length} total chars)`;
|
|
5091
|
+
}
|
|
5092
|
+
return output;
|
|
5093
|
+
} catch (err) {
|
|
5094
|
+
return `Clipboard read error: ${err.message}`;
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
);
|
|
5098
|
+
registry.register(
|
|
5099
|
+
"clipboard_write",
|
|
5100
|
+
"Write text content to the macOS system clipboard.",
|
|
5101
|
+
{
|
|
5102
|
+
type: "object",
|
|
5103
|
+
properties: {
|
|
5104
|
+
content: {
|
|
5105
|
+
type: "string",
|
|
5106
|
+
description: "Text to write to the clipboard."
|
|
5107
|
+
}
|
|
5108
|
+
},
|
|
5109
|
+
required: ["content"]
|
|
5110
|
+
},
|
|
5111
|
+
async (params) => {
|
|
5112
|
+
const content = params.content;
|
|
5113
|
+
try {
|
|
5114
|
+
execSync5("pbcopy", {
|
|
5115
|
+
input: content,
|
|
5116
|
+
timeout: 5e3,
|
|
5117
|
+
encoding: "utf-8",
|
|
5118
|
+
shell: "/bin/sh"
|
|
5119
|
+
});
|
|
5120
|
+
return `Copied ${content.length} chars to clipboard.`;
|
|
5121
|
+
} catch (err) {
|
|
5122
|
+
return `Clipboard write error: ${err.message}`;
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
);
|
|
5126
|
+
registry.register(
|
|
5127
|
+
"open_url",
|
|
5128
|
+
"Open a URL in the default browser or a specified application.",
|
|
5129
|
+
{
|
|
5130
|
+
type: "object",
|
|
5131
|
+
properties: {
|
|
5132
|
+
url: {
|
|
5133
|
+
type: "string",
|
|
5134
|
+
description: "The URL to open."
|
|
5135
|
+
},
|
|
5136
|
+
app: {
|
|
5137
|
+
type: "string",
|
|
5138
|
+
description: "Application name to open the URL with (e.g. 'Google Chrome', 'Safari')."
|
|
5139
|
+
}
|
|
5140
|
+
},
|
|
5141
|
+
required: ["url"]
|
|
5142
|
+
},
|
|
5143
|
+
async (params) => {
|
|
5144
|
+
const url = params.url;
|
|
5145
|
+
const app = params.app;
|
|
5146
|
+
try {
|
|
5147
|
+
const args = app ? `open -a ${escapeShellArg(app)} ${escapeShellArg(url)}` : `open ${escapeShellArg(url)}`;
|
|
5148
|
+
shellExec(args, 1e4);
|
|
5149
|
+
return `Opened ${url}${app ? ` in ${app}` : ""}`;
|
|
5150
|
+
} catch (err) {
|
|
5151
|
+
const stderr = err.stderr?.toString() || "";
|
|
5152
|
+
return `open_url error: ${stderr.trim() || err.message}`;
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
);
|
|
5156
|
+
registry.register(
|
|
5157
|
+
"screenshot",
|
|
5158
|
+
"Take a screenshot of the screen and save it to a file.",
|
|
5159
|
+
{
|
|
5160
|
+
type: "object",
|
|
5161
|
+
properties: {
|
|
5162
|
+
path: {
|
|
5163
|
+
type: "string",
|
|
5164
|
+
description: "Save path for the screenshot (relative to workspace). Defaults to screenshots/<uuid>.png."
|
|
5165
|
+
},
|
|
5166
|
+
region: {
|
|
5167
|
+
type: "boolean",
|
|
5168
|
+
description: "If true, enable interactive region selection. Default: false."
|
|
5169
|
+
}
|
|
5170
|
+
},
|
|
5171
|
+
required: []
|
|
5172
|
+
},
|
|
5173
|
+
async (params) => {
|
|
5174
|
+
try {
|
|
5175
|
+
let savePath;
|
|
5176
|
+
if (params.path) {
|
|
5177
|
+
savePath = resolve14(workspaceDir, params.path);
|
|
5178
|
+
const normalizedSave = normalize2(savePath);
|
|
5179
|
+
const normalizedWorkspace = normalize2(workspaceDir);
|
|
5180
|
+
if (!normalizedSave.startsWith(normalizedWorkspace)) {
|
|
5181
|
+
return "Error: path must be within the workspace.";
|
|
5182
|
+
}
|
|
5183
|
+
} else {
|
|
5184
|
+
const screenshotDir = resolve14(workspaceDir, "screenshots");
|
|
5185
|
+
if (!existsSync13(screenshotDir)) {
|
|
5186
|
+
mkdirSync10(screenshotDir, { recursive: true });
|
|
5187
|
+
}
|
|
5188
|
+
savePath = resolve14(screenshotDir, `${randomUUID7()}.png`);
|
|
5189
|
+
}
|
|
5190
|
+
const parentDir = resolve14(savePath, "..");
|
|
5191
|
+
if (!existsSync13(parentDir)) {
|
|
5192
|
+
mkdirSync10(parentDir, { recursive: true });
|
|
5193
|
+
}
|
|
5194
|
+
const args = params.region ? `screencapture -i ${escapeShellArg(savePath)}` : `screencapture -x ${escapeShellArg(savePath)}`;
|
|
5195
|
+
shellExec(args, 15e3);
|
|
5196
|
+
if (!existsSync13(savePath)) {
|
|
5197
|
+
return "Screenshot cancelled or failed \u2014 no file was created.";
|
|
5198
|
+
}
|
|
5199
|
+
return `Screenshot saved: ${savePath}`;
|
|
5200
|
+
} catch (err) {
|
|
5201
|
+
return `Screenshot error: ${err.message}`;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
function escapeShellArg(s) {
|
|
5207
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5208
|
+
}
|
|
5209
|
+
function escapeAppleString(s) {
|
|
5210
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
// packages/runtime/src/tools/data.ts
|
|
5214
|
+
import { execSync as execSync6 } from "child_process";
|
|
5215
|
+
import { resolve as resolve15, normalize as normalize3, extname as extname2 } from "path";
|
|
5216
|
+
import { mkdirSync as mkdirSync11, existsSync as existsSync14 } from "fs";
|
|
5217
|
+
var MAX_OUTPUT5 = 5e4;
|
|
5218
|
+
function shellExec2(command, cwd, timeoutMs) {
|
|
5219
|
+
return execSync6(command, {
|
|
5220
|
+
cwd,
|
|
5221
|
+
timeout: timeoutMs,
|
|
5222
|
+
encoding: "utf-8",
|
|
5223
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
5224
|
+
shell: "/bin/sh",
|
|
5225
|
+
env: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}` }
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
function safePath(workspaceDir, filePath) {
|
|
5229
|
+
const resolved = resolve15(workspaceDir, filePath);
|
|
5230
|
+
const normalizedResolved = normalize3(resolved);
|
|
5231
|
+
const normalizedWorkspace = normalize3(workspaceDir);
|
|
5232
|
+
if (!normalizedResolved.startsWith(normalizedWorkspace)) {
|
|
5233
|
+
throw new Error("Path traversal detected: path must be within the workspace.");
|
|
5234
|
+
}
|
|
5235
|
+
return resolved;
|
|
5236
|
+
}
|
|
5237
|
+
function escapeShellArg2(s) {
|
|
5238
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5239
|
+
}
|
|
5240
|
+
function registerDataTools(registry, workspaceDir) {
|
|
5241
|
+
registry.register(
|
|
5242
|
+
"sqlite_query",
|
|
5243
|
+
"Execute a SQL query against a SQLite database file. Returns JSON for SELECT queries, affected row info for INSERT/UPDATE/DELETE.",
|
|
5244
|
+
{
|
|
5245
|
+
type: "object",
|
|
5246
|
+
properties: {
|
|
5247
|
+
database: {
|
|
5248
|
+
type: "string",
|
|
5249
|
+
description: "Path to the .db file (relative to workspace)."
|
|
5250
|
+
},
|
|
5251
|
+
query: {
|
|
5252
|
+
type: "string",
|
|
5253
|
+
description: "SQL query to execute."
|
|
5254
|
+
},
|
|
5255
|
+
params: {
|
|
5256
|
+
type: "array",
|
|
5257
|
+
items: { type: "string" },
|
|
5258
|
+
description: "Query parameters for ? placeholders."
|
|
5259
|
+
}
|
|
5260
|
+
},
|
|
5261
|
+
required: ["database", "query"]
|
|
5262
|
+
},
|
|
5263
|
+
async (params) => {
|
|
5264
|
+
const dbRelative = params.database;
|
|
5265
|
+
const query = params.query;
|
|
5266
|
+
const queryParams = params.params;
|
|
5267
|
+
try {
|
|
5268
|
+
const dbPath = safePath(workspaceDir, dbRelative);
|
|
5269
|
+
if (!existsSync14(dbPath) && !query.trim().toUpperCase().startsWith("CREATE")) {
|
|
5270
|
+
return `Error: database file not found: ${dbRelative}`;
|
|
5271
|
+
}
|
|
5272
|
+
let fullQuery = query;
|
|
5273
|
+
if (queryParams && queryParams.length > 0) {
|
|
5274
|
+
let paramIdx = 0;
|
|
5275
|
+
fullQuery = query.replace(/\?/g, () => {
|
|
5276
|
+
if (paramIdx < queryParams.length) {
|
|
5277
|
+
const val = queryParams[paramIdx++];
|
|
5278
|
+
return `'${val.replace(/'/g, "''")}'`;
|
|
5279
|
+
}
|
|
5280
|
+
return "?";
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
const isSelect = fullQuery.trim().toUpperCase().startsWith("SELECT") || fullQuery.trim().toUpperCase().startsWith("PRAGMA");
|
|
5284
|
+
const modeFlag = isSelect ? "-json" : "";
|
|
5285
|
+
const command = `sqlite3 ${modeFlag} ${escapeShellArg2(dbPath)} ${escapeShellArg2(fullQuery)}`;
|
|
5286
|
+
const output = shellExec2(command, workspaceDir, 3e4);
|
|
5287
|
+
if (!output.trim()) {
|
|
5288
|
+
return isSelect ? "[]" : "Query executed successfully (no output).";
|
|
5289
|
+
}
|
|
5290
|
+
const trimmed = output.length > MAX_OUTPUT5 ? output.slice(0, MAX_OUTPUT5) + `
|
|
5291
|
+
... (truncated, ${output.length} total chars)` : output;
|
|
5292
|
+
return trimmed;
|
|
5293
|
+
} catch (err) {
|
|
5294
|
+
const stderr = err.stderr?.toString() || "";
|
|
5295
|
+
const stdout = err.stdout?.toString() || "";
|
|
5296
|
+
const output = (stdout + "\n" + stderr).trim();
|
|
5297
|
+
return `SQLite error: ${output || err.message}`;
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
);
|
|
5301
|
+
registry.register(
|
|
5302
|
+
"archive_create",
|
|
5303
|
+
"Create a zip or tar.gz archive from a file or directory.",
|
|
5304
|
+
{
|
|
5305
|
+
type: "object",
|
|
5306
|
+
properties: {
|
|
5307
|
+
source: {
|
|
5308
|
+
type: "string",
|
|
5309
|
+
description: "File or directory path to archive (relative to workspace)."
|
|
5310
|
+
},
|
|
5311
|
+
output: {
|
|
5312
|
+
type: "string",
|
|
5313
|
+
description: "Output archive path (relative to workspace)."
|
|
5314
|
+
},
|
|
5315
|
+
format: {
|
|
5316
|
+
type: "string",
|
|
5317
|
+
enum: ["zip", "tar.gz"],
|
|
5318
|
+
description: "Archive format. Default: 'zip'."
|
|
5319
|
+
}
|
|
5320
|
+
},
|
|
5321
|
+
required: ["source", "output"]
|
|
5322
|
+
},
|
|
5323
|
+
async (params) => {
|
|
5324
|
+
const source = params.source;
|
|
5325
|
+
const output = params.output;
|
|
5326
|
+
const format = params.format || "zip";
|
|
5327
|
+
try {
|
|
5328
|
+
const sourcePath = safePath(workspaceDir, source);
|
|
5329
|
+
const outputPath = safePath(workspaceDir, output);
|
|
5330
|
+
if (!existsSync14(sourcePath)) {
|
|
5331
|
+
return `Error: source not found: ${source}`;
|
|
5332
|
+
}
|
|
5333
|
+
const outputParent = resolve15(outputPath, "..");
|
|
5334
|
+
if (!existsSync14(outputParent)) {
|
|
5335
|
+
mkdirSync11(outputParent, { recursive: true });
|
|
5336
|
+
}
|
|
5337
|
+
let command;
|
|
5338
|
+
if (format === "tar.gz") {
|
|
5339
|
+
command = `tar czf ${escapeShellArg2(outputPath)} -C ${escapeShellArg2(resolve15(sourcePath, ".."))} ${escapeShellArg2(sourcePath.split("/").pop())}`;
|
|
5340
|
+
} else {
|
|
5341
|
+
command = `zip -r ${escapeShellArg2(outputPath)} ${escapeShellArg2(source)}`;
|
|
5342
|
+
}
|
|
5343
|
+
shellExec2(command, workspaceDir, 6e4);
|
|
5344
|
+
return `Archive created: ${output}`;
|
|
5345
|
+
} catch (err) {
|
|
5346
|
+
const stderr = err.stderr?.toString() || "";
|
|
5347
|
+
return `Archive error: ${stderr.trim() || err.message}`;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
);
|
|
5351
|
+
registry.register(
|
|
5352
|
+
"archive_extract",
|
|
5353
|
+
"Extract a zip or tar.gz archive.",
|
|
5354
|
+
{
|
|
5355
|
+
type: "object",
|
|
5356
|
+
properties: {
|
|
5357
|
+
archive: {
|
|
5358
|
+
type: "string",
|
|
5359
|
+
description: "Path to the archive file (relative to workspace)."
|
|
5360
|
+
},
|
|
5361
|
+
destination: {
|
|
5362
|
+
type: "string",
|
|
5363
|
+
description: "Extraction directory (relative to workspace). Defaults to workspace root."
|
|
5364
|
+
}
|
|
5365
|
+
},
|
|
5366
|
+
required: ["archive"]
|
|
5367
|
+
},
|
|
5368
|
+
async (params) => {
|
|
5369
|
+
const archive = params.archive;
|
|
5370
|
+
const destination = params.destination;
|
|
5371
|
+
try {
|
|
5372
|
+
const archivePath = safePath(workspaceDir, archive);
|
|
5373
|
+
const destPath = destination ? safePath(workspaceDir, destination) : workspaceDir;
|
|
5374
|
+
if (!existsSync14(archivePath)) {
|
|
5375
|
+
return `Error: archive not found: ${archive}`;
|
|
5376
|
+
}
|
|
5377
|
+
if (!existsSync14(destPath)) {
|
|
5378
|
+
mkdirSync11(destPath, { recursive: true });
|
|
5379
|
+
}
|
|
5380
|
+
const ext = extname2(archivePath).toLowerCase();
|
|
5381
|
+
const isGz = archivePath.endsWith(".tar.gz") || archivePath.endsWith(".tgz");
|
|
5382
|
+
let command;
|
|
5383
|
+
if (isGz || ext === ".tar") {
|
|
5384
|
+
const flags = isGz ? "xzf" : "xf";
|
|
5385
|
+
command = `tar ${flags} ${escapeShellArg2(archivePath)} -C ${escapeShellArg2(destPath)}`;
|
|
5386
|
+
} else if (ext === ".zip") {
|
|
5387
|
+
command = `unzip -o ${escapeShellArg2(archivePath)} -d ${escapeShellArg2(destPath)}`;
|
|
5388
|
+
} else {
|
|
5389
|
+
return `Error: unsupported archive format: ${ext}. Supported: .zip, .tar, .tar.gz, .tgz`;
|
|
5390
|
+
}
|
|
5391
|
+
const output = shellExec2(command, workspaceDir, 6e4);
|
|
5392
|
+
const trimmed = output.length > MAX_OUTPUT5 ? output.slice(0, MAX_OUTPUT5) + `
|
|
5393
|
+
... (truncated)` : output;
|
|
5394
|
+
return `Extracted to ${destination || "workspace root"}.
|
|
5395
|
+
${trimmed}`.trim();
|
|
5396
|
+
} catch (err) {
|
|
5397
|
+
const stderr = err.stderr?.toString() || "";
|
|
5398
|
+
return `Extract error: ${stderr.trim() || err.message}`;
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
);
|
|
5402
|
+
registry.register(
|
|
5403
|
+
"pdf_extract",
|
|
5404
|
+
"Extract text content from a PDF file. Tries pdftotext (poppler), then textutil, then basic string extraction.",
|
|
5405
|
+
{
|
|
5406
|
+
type: "object",
|
|
5407
|
+
properties: {
|
|
5408
|
+
path: {
|
|
5409
|
+
type: "string",
|
|
5410
|
+
description: "Path to the PDF file (relative to workspace)."
|
|
5411
|
+
}
|
|
5412
|
+
},
|
|
5413
|
+
required: ["path"]
|
|
5414
|
+
},
|
|
5415
|
+
async (params) => {
|
|
5416
|
+
const filePath = params.path;
|
|
5417
|
+
try {
|
|
5418
|
+
const pdfPath = safePath(workspaceDir, filePath);
|
|
5419
|
+
if (!existsSync14(pdfPath)) {
|
|
5420
|
+
return `Error: PDF not found: ${filePath}`;
|
|
5421
|
+
}
|
|
5422
|
+
try {
|
|
5423
|
+
const output = shellExec2(`pdftotext ${escapeShellArg2(pdfPath)} -`, workspaceDir, 3e4);
|
|
5424
|
+
if (output.trim()) {
|
|
5425
|
+
return truncate2(output);
|
|
5426
|
+
}
|
|
5427
|
+
} catch {
|
|
5428
|
+
}
|
|
5429
|
+
try {
|
|
5430
|
+
const output = shellExec2(`textutil -convert txt -stdout ${escapeShellArg2(pdfPath)}`, workspaceDir, 3e4);
|
|
5431
|
+
if (output.trim()) {
|
|
5432
|
+
return truncate2(output);
|
|
5433
|
+
}
|
|
5434
|
+
} catch {
|
|
5435
|
+
}
|
|
5436
|
+
try {
|
|
5437
|
+
const output = shellExec2(`strings ${escapeShellArg2(pdfPath)} | head -1000`, workspaceDir, 15e3);
|
|
5438
|
+
if (output.trim()) {
|
|
5439
|
+
return `(basic extraction \u2014 install poppler for better results)
|
|
5440
|
+
${truncate2(output)}`;
|
|
5441
|
+
}
|
|
5442
|
+
} catch {
|
|
5443
|
+
}
|
|
5444
|
+
return "Error: could not extract text from PDF. Install poppler (brew install poppler) for best results.";
|
|
5445
|
+
} catch (err) {
|
|
5446
|
+
return `PDF extraction error: ${err.message}`;
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
);
|
|
5450
|
+
}
|
|
5451
|
+
function truncate2(text) {
|
|
5452
|
+
if (text.length > MAX_OUTPUT5) {
|
|
5453
|
+
return text.slice(0, MAX_OUTPUT5) + `
|
|
5454
|
+
... (truncated, ${text.length} total chars)`;
|
|
5455
|
+
}
|
|
5456
|
+
return text;
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
// packages/runtime/src/tools/register.ts
|
|
5460
|
+
import { resolve as resolve16 } from "path";
|
|
5461
|
+
import { mkdirSync as mkdirSync12, existsSync as existsSync15 } from "fs";
|
|
5462
|
+
function registerAllTools(hivemindHome, config) {
|
|
5463
|
+
const registry = new ToolRegistry();
|
|
5464
|
+
if (config?.enabled === false) {
|
|
5465
|
+
return registry;
|
|
5466
|
+
}
|
|
5467
|
+
const workspaceDir = resolve16(hivemindHome, config?.workspace || "workspace");
|
|
5468
|
+
if (!existsSync15(workspaceDir)) {
|
|
5469
|
+
mkdirSync12(workspaceDir, { recursive: true });
|
|
5470
|
+
}
|
|
5471
|
+
registerShellTool(registry, workspaceDir);
|
|
5472
|
+
registerFileTools(registry, workspaceDir);
|
|
5473
|
+
registerWebTools(registry, { braveApiKey: config?.braveApiKey });
|
|
5474
|
+
registerMemoryTools(registry, config?.memoryDaemonUrl || "http://localhost:3434");
|
|
5475
|
+
const dataDir = resolve16(hivemindHome, "data");
|
|
5476
|
+
registerEventTools(registry, dataDir);
|
|
5477
|
+
if (config?.configPath && !process.env.SPAWN_TASK) {
|
|
5478
|
+
registerSpawnTools(registry, hivemindHome, dataDir, config.configPath);
|
|
5479
|
+
}
|
|
5480
|
+
registerVisionTools(registry);
|
|
5481
|
+
registerGitTools(registry, workspaceDir);
|
|
5482
|
+
registerBrowserTools(registry, workspaceDir);
|
|
5483
|
+
registerSystemTools(registry);
|
|
5484
|
+
registerHttpTools(registry, workspaceDir);
|
|
5485
|
+
registerWatchTools(registry, workspaceDir, dataDir);
|
|
5486
|
+
registerMacOSTools(registry, workspaceDir);
|
|
5487
|
+
registerDataTools(registry, workspaceDir);
|
|
5488
|
+
return registry;
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5491
|
+
// packages/runtime/src/tools/messaging.ts
|
|
5492
|
+
function registerMessagingTools(registry, sesame) {
|
|
5493
|
+
registry.register(
|
|
5494
|
+
"send_message",
|
|
5495
|
+
"Send a message to a Sesame channel. Use this to proactively reach out to a user or group, not just reply to incoming messages.",
|
|
5496
|
+
{
|
|
5497
|
+
type: "object",
|
|
5498
|
+
properties: {
|
|
5499
|
+
channelId: {
|
|
5500
|
+
type: "string",
|
|
5501
|
+
description: "The Sesame channel ID to send the message to"
|
|
5502
|
+
},
|
|
5503
|
+
content: {
|
|
5504
|
+
type: "string",
|
|
5505
|
+
description: "The message content to send"
|
|
5506
|
+
}
|
|
5507
|
+
},
|
|
5508
|
+
required: ["channelId", "content"]
|
|
5509
|
+
},
|
|
5510
|
+
async (params) => {
|
|
5511
|
+
const channelId = params.channelId;
|
|
5512
|
+
const content = params.content;
|
|
5513
|
+
try {
|
|
5514
|
+
await sesame.sendMessage(channelId, content);
|
|
5515
|
+
return `Message sent to channel ${channelId}`;
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
return `Error sending message: ${err.message}`;
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
);
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// packages/runtime/src/tools/skills-tools.ts
|
|
5524
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync6, rmSync } from "fs";
|
|
5525
|
+
import { resolve as resolve17 } from "path";
|
|
5526
|
+
function registerSkillsTools(registry, skillsEngine, workspaceDir) {
|
|
5527
|
+
const skillsDir = resolve17(workspaceDir, "skills");
|
|
5528
|
+
registry.register(
|
|
5529
|
+
"skill_list",
|
|
5530
|
+
"List all loaded skills with their registered tools and status.",
|
|
5531
|
+
{
|
|
5532
|
+
type: "object",
|
|
5533
|
+
properties: {},
|
|
5534
|
+
required: []
|
|
5535
|
+
},
|
|
5536
|
+
async () => {
|
|
5537
|
+
const skills = skillsEngine.listSkills();
|
|
5538
|
+
if (skills.length === 0) {
|
|
5539
|
+
return "No skills loaded. Create one with skill_create or add a SKILL.md to workspace/skills/<name>/.";
|
|
5540
|
+
}
|
|
5541
|
+
const lines = [];
|
|
5542
|
+
for (const skill of skills) {
|
|
5543
|
+
let line = `${skill.dirName} \u2014 "${skill.name}"`;
|
|
5544
|
+
if (skill.description) line += `: ${skill.description}`;
|
|
5545
|
+
if (skill.tools.length > 0) {
|
|
5546
|
+
line += `
|
|
5547
|
+
tools: ${skill.tools.join(", ")}`;
|
|
5548
|
+
} else {
|
|
5549
|
+
line += `
|
|
5550
|
+
tools: (none)`;
|
|
5551
|
+
}
|
|
5552
|
+
lines.push(line);
|
|
5553
|
+
}
|
|
5554
|
+
return `${skills.length} skill(s) loaded:
|
|
5555
|
+
|
|
5556
|
+
${lines.join("\n\n")}`;
|
|
5557
|
+
}
|
|
5558
|
+
);
|
|
5559
|
+
registry.register(
|
|
5560
|
+
"skill_create",
|
|
5561
|
+
"Create a new skill with optional tool definitions. Creates the skill directory, SKILL.md, and optionally tools.json, then auto-loads it.",
|
|
5562
|
+
{
|
|
5563
|
+
type: "object",
|
|
5564
|
+
properties: {
|
|
5565
|
+
name: {
|
|
5566
|
+
type: "string",
|
|
5567
|
+
description: "Directory name for the skill (lowercase, hyphens ok)"
|
|
5568
|
+
},
|
|
5569
|
+
description: {
|
|
5570
|
+
type: "string",
|
|
5571
|
+
description: "Human-readable description of what the skill does"
|
|
5572
|
+
},
|
|
5573
|
+
tools: {
|
|
5574
|
+
type: "array",
|
|
5575
|
+
description: "Optional array of tool definitions (each with name, description, parameters, command)",
|
|
5576
|
+
items: {
|
|
5577
|
+
type: "object",
|
|
5578
|
+
properties: {
|
|
5579
|
+
name: { type: "string" },
|
|
5580
|
+
description: { type: "string" },
|
|
5581
|
+
parameters: { type: "object" },
|
|
5582
|
+
command: { type: "string" }
|
|
5583
|
+
},
|
|
5584
|
+
required: ["name", "command"]
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
},
|
|
5588
|
+
required: ["name", "description"]
|
|
5589
|
+
},
|
|
5590
|
+
async (params) => {
|
|
5591
|
+
const name = params.name;
|
|
5592
|
+
const description = params.description;
|
|
5593
|
+
const tools = params.tools;
|
|
5594
|
+
const skillDir = resolve17(skillsDir, name);
|
|
5595
|
+
if (existsSync16(skillDir)) {
|
|
5596
|
+
return `Error: Skill directory "${name}" already exists. Use skill_reload to update it.`;
|
|
5597
|
+
}
|
|
5598
|
+
mkdirSync13(skillDir, { recursive: true });
|
|
5599
|
+
const skillMd = `---
|
|
5600
|
+
name: "${name}"
|
|
5601
|
+
description: "${description}"
|
|
5602
|
+
---
|
|
5603
|
+
|
|
5604
|
+
# ${name}
|
|
5605
|
+
|
|
5606
|
+
${description}
|
|
5607
|
+
`;
|
|
5608
|
+
writeFileSync6(resolve17(skillDir, "SKILL.md"), skillMd);
|
|
5609
|
+
if (tools && tools.length > 0) {
|
|
5610
|
+
const toolsDef = {
|
|
5611
|
+
tools: tools.map((t) => ({
|
|
5612
|
+
name: t.name,
|
|
5613
|
+
description: t.description || `Tool from skill "${name}"`,
|
|
5614
|
+
parameters: t.parameters || { type: "object", properties: {}, required: [] },
|
|
5615
|
+
command: t.command
|
|
5616
|
+
}))
|
|
5617
|
+
};
|
|
5618
|
+
writeFileSync6(resolve17(skillDir, "tools.json"), JSON.stringify(toolsDef, null, 2) + "\n");
|
|
5619
|
+
}
|
|
5620
|
+
try {
|
|
5621
|
+
await skillsEngine.loadSkill(name);
|
|
5622
|
+
} catch (err) {
|
|
5623
|
+
return `Skill files created at ${skillDir} but failed to load: ${err.message}`;
|
|
5624
|
+
}
|
|
5625
|
+
const toolSuffix = tools && tools.length > 0 ? ` with ${tools.length} tool(s): ${tools.map((t) => t.name).join(", ")}` : "";
|
|
5626
|
+
return `Skill "${name}" created${toolSuffix} and loaded.
|
|
5627
|
+
Path: ${skillDir}`;
|
|
5628
|
+
}
|
|
5629
|
+
);
|
|
5630
|
+
registry.register(
|
|
5631
|
+
"skill_reload",
|
|
5632
|
+
"Reload a skill to pick up file changes (re-reads SKILL.md and tools.json, re-runs setup.sh).",
|
|
5633
|
+
{
|
|
5634
|
+
type: "object",
|
|
5635
|
+
properties: {
|
|
5636
|
+
name: {
|
|
5637
|
+
type: "string",
|
|
5638
|
+
description: "The directory name of the skill to reload"
|
|
5639
|
+
}
|
|
5640
|
+
},
|
|
5641
|
+
required: ["name"]
|
|
5642
|
+
},
|
|
5643
|
+
async (params) => {
|
|
5644
|
+
const name = params.name;
|
|
5645
|
+
try {
|
|
5646
|
+
await skillsEngine.reloadSkill(name);
|
|
5647
|
+
const skill = skillsEngine.getSkill(name);
|
|
5648
|
+
if (!skill) return `Error: Skill "${name}" not found after reload.`;
|
|
5649
|
+
const toolSuffix = skill.tools.length > 0 ? ` (tools: ${skill.tools.join(", ")})` : " (no tools)";
|
|
5650
|
+
return `Skill "${skill.name}" reloaded${toolSuffix}.`;
|
|
5651
|
+
} catch (err) {
|
|
5652
|
+
return `Error reloading skill "${name}": ${err.message}`;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
);
|
|
5656
|
+
registry.register(
|
|
5657
|
+
"skill_delete",
|
|
5658
|
+
"Delete a skill entirely \u2014 unloads it, removes its tools, and deletes the skill directory.",
|
|
5659
|
+
{
|
|
5660
|
+
type: "object",
|
|
5661
|
+
properties: {
|
|
5662
|
+
name: {
|
|
5663
|
+
type: "string",
|
|
5664
|
+
description: "The directory name of the skill to delete"
|
|
5665
|
+
}
|
|
5666
|
+
},
|
|
5667
|
+
required: ["name"]
|
|
5668
|
+
},
|
|
5669
|
+
async (params) => {
|
|
5670
|
+
const name = params.name;
|
|
5671
|
+
const skillDir = resolve17(skillsDir, name);
|
|
5672
|
+
if (!existsSync16(skillDir)) {
|
|
5673
|
+
return `Error: Skill directory "${name}" does not exist.`;
|
|
5674
|
+
}
|
|
5675
|
+
try {
|
|
5676
|
+
await skillsEngine.unloadSkill(name);
|
|
5677
|
+
} catch (err) {
|
|
5678
|
+
console.warn(`[skills] Error during unload of "${name}":`, err.message);
|
|
5679
|
+
}
|
|
5680
|
+
try {
|
|
5681
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
5682
|
+
} catch (err) {
|
|
5683
|
+
return `Skill unloaded but failed to delete directory: ${err.message}`;
|
|
5684
|
+
}
|
|
5685
|
+
return `Skill "${name}" deleted.`;
|
|
5686
|
+
}
|
|
5687
|
+
);
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
// packages/runtime/src/pipeline.ts
|
|
5691
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
5692
|
+
import { resolve as resolve18, dirname as dirname7 } from "path";
|
|
5693
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5694
|
+
var PACKAGE_VERSION = "unknown";
|
|
5695
|
+
try {
|
|
5696
|
+
const __dirname2 = dirname7(fileURLToPath3(import.meta.url));
|
|
5697
|
+
const pkg = JSON.parse(readFileSync12(resolve18(__dirname2, "../package.json"), "utf-8"));
|
|
5698
|
+
PACKAGE_VERSION = pkg.version ?? "unknown";
|
|
5699
|
+
} catch {
|
|
5700
|
+
}
|
|
5701
|
+
var sesameConnected = false;
|
|
5702
|
+
var memoryConnected = false;
|
|
5703
|
+
var startTime = Date.now();
|
|
5704
|
+
function startHealthServer(port) {
|
|
5705
|
+
const server = createServer3((req, res) => {
|
|
5706
|
+
if (req.method === "GET" && req.url === HEALTH_PATH) {
|
|
5707
|
+
const status = {
|
|
5708
|
+
status: sesameConnected ? "ok" : "degraded",
|
|
5709
|
+
pid: process.pid,
|
|
5710
|
+
uptime_s: Math.floor((Date.now() - startTime) / 1e3),
|
|
5711
|
+
sesame_connected: sesameConnected,
|
|
5712
|
+
memory_connected: memoryConnected,
|
|
5713
|
+
version: PACKAGE_VERSION
|
|
5714
|
+
};
|
|
5715
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5716
|
+
res.end(JSON.stringify(status));
|
|
5717
|
+
} else {
|
|
5718
|
+
res.writeHead(404);
|
|
5719
|
+
res.end();
|
|
5720
|
+
}
|
|
5721
|
+
});
|
|
5722
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5723
|
+
console.log(`[hivemind] Health endpoint listening on http://127.0.0.1:${port}${HEALTH_PATH}`);
|
|
5724
|
+
});
|
|
5725
|
+
return server;
|
|
5726
|
+
}
|
|
5727
|
+
function writePidFile(path) {
|
|
5728
|
+
writeFileSync7(path, String(process.pid));
|
|
5729
|
+
console.log(`[hivemind] PID file written: ${path}`);
|
|
5730
|
+
}
|
|
5731
|
+
function cleanupPidFile(path) {
|
|
5732
|
+
try {
|
|
5733
|
+
unlinkSync3(path);
|
|
5734
|
+
} catch {
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
async function startPipeline(configPath) {
|
|
5738
|
+
const config = loadConfig(configPath);
|
|
5739
|
+
if (process.env.SPAWN_TASK) {
|
|
5740
|
+
await runSpawnTask(config, configPath);
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
const sentinel = config.sentinel ?? defaultSentinelConfig();
|
|
5744
|
+
const healthPort = sentinel.health_port || HEALTH_PORT;
|
|
5745
|
+
const pidFile = sentinel.pid_file;
|
|
5746
|
+
console.log(`[hivemind] Starting ${config.agent.name} (pid ${process.pid})`);
|
|
5747
|
+
writePidFile(pidFile);
|
|
5748
|
+
const healthServer = startHealthServer(healthPort);
|
|
5749
|
+
const cleanupOnExit = () => {
|
|
5750
|
+
cleanupPidFile(pidFile);
|
|
5751
|
+
healthServer.close();
|
|
5752
|
+
};
|
|
5753
|
+
process.on("exit", cleanupOnExit);
|
|
5754
|
+
const memory = new MemoryClient(config.memory);
|
|
5755
|
+
const memoryOk = await memory.healthCheck();
|
|
5756
|
+
if (!memoryOk) {
|
|
5757
|
+
console.warn("[hivemind] Memory daemon unreachable at", config.memory.daemon_url);
|
|
5758
|
+
console.warn("[hivemind] Continuing without persistent memory \u2014 episodes will not be stored");
|
|
5759
|
+
} else {
|
|
5760
|
+
memoryConnected = true;
|
|
5761
|
+
console.log("[hivemind] Memory daemon connected");
|
|
5762
|
+
}
|
|
5763
|
+
const requestLogger = new RequestLogger(resolve18(dirname7(configPath), "data", "dashboard.db"));
|
|
5764
|
+
startDashboardServer(requestLogger, config.memory);
|
|
5765
|
+
const agent = new Agent(config);
|
|
5766
|
+
agent.setRequestLogger(requestLogger);
|
|
5767
|
+
const hivemindHome = process.env.HIVEMIND_HOME || resolve18(process.env.HOME || "/root", "hivemind");
|
|
5768
|
+
const toolRegistry = registerAllTools(hivemindHome, {
|
|
5769
|
+
enabled: true,
|
|
5770
|
+
workspace: config.agent.workspace || "workspace",
|
|
5771
|
+
braveApiKey: process.env.BRAVE_API_KEY,
|
|
5772
|
+
memoryDaemonUrl: config.memory.daemon_url,
|
|
5773
|
+
configPath
|
|
5774
|
+
});
|
|
5775
|
+
const workspaceDir = resolve18(hivemindHome, config.agent.workspace || "workspace");
|
|
5776
|
+
const skillsEngine = new SkillsEngine(workspaceDir, toolRegistry);
|
|
5777
|
+
await skillsEngine.loadAll();
|
|
5778
|
+
registerSkillsTools(toolRegistry, skillsEngine, workspaceDir);
|
|
5779
|
+
skillsEngine.startWatching();
|
|
5780
|
+
process.on("exit", () => skillsEngine.stopWatching());
|
|
5781
|
+
agent.setToolRegistry(toolRegistry);
|
|
5782
|
+
console.log(`[hivemind] Context manager initialized (active: ${agent.getActiveContext()})`);
|
|
5783
|
+
const dataDir = resolve18(hivemindHome, "data");
|
|
5784
|
+
if (config.sesame.api_key) {
|
|
5785
|
+
await startSesameLoop(config, agent, toolRegistry, dataDir);
|
|
5786
|
+
} else {
|
|
5787
|
+
console.log("[hivemind] No Sesame API key configured \u2014 running in stdin mode");
|
|
5788
|
+
await startStdinLoop(agent);
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
async function startSesameLoop(config, agent, toolRegistry, dataDir) {
|
|
5792
|
+
const sesame = new SesameClient2(config.sesame);
|
|
5793
|
+
registerMessagingTools(toolRegistry, sesame);
|
|
5794
|
+
let eventsWatcher = null;
|
|
5795
|
+
if (dataDir) {
|
|
5796
|
+
eventsWatcher = new EventsWatcher(dataDir, async (channelId, text, filename, eventType) => {
|
|
5797
|
+
console.log(`[events] Firing ${eventType} event from ${filename}`);
|
|
5798
|
+
const eventMessage = `[EVENT:${filename}:${eventType}] ${text}`;
|
|
5799
|
+
try {
|
|
5800
|
+
const response = await agent.processMessage(eventMessage);
|
|
5801
|
+
if (response.content.trim() === "[SILENT]" || response.content.trim().startsWith("[SILENT]")) {
|
|
5802
|
+
console.log(`[events] Silent response for ${filename}`);
|
|
5803
|
+
return;
|
|
5804
|
+
}
|
|
5805
|
+
if (response.content.trim() === "__SKIP__") return;
|
|
5806
|
+
if (channelId) {
|
|
5807
|
+
await sesame.sendMessage(channelId, response.content);
|
|
5808
|
+
}
|
|
5809
|
+
} catch (err) {
|
|
5810
|
+
console.error(`[events] Error processing event ${filename}:`, err.message);
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
5813
|
+
eventsWatcher.start();
|
|
5814
|
+
}
|
|
5815
|
+
let shuttingDown = false;
|
|
5816
|
+
const shutdown = (signal) => {
|
|
5817
|
+
if (shuttingDown) return;
|
|
5818
|
+
shuttingDown = true;
|
|
5819
|
+
console.log(`
|
|
5820
|
+
[hivemind] Received ${signal}, shutting down...`);
|
|
5821
|
+
try {
|
|
5822
|
+
sesame.updatePresence("offline", { emoji: "\u2B58" });
|
|
5823
|
+
sesame.disconnect();
|
|
5824
|
+
console.log("[hivemind] Sesame disconnected cleanly");
|
|
5825
|
+
} catch (err) {
|
|
5826
|
+
console.error("[hivemind] Error during disconnect:", err.message);
|
|
5827
|
+
}
|
|
5828
|
+
process.exit(0);
|
|
5829
|
+
};
|
|
5830
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5831
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5832
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
5833
|
+
const MAX_SEEN = 500;
|
|
5834
|
+
let processing = false;
|
|
5835
|
+
const messageQueue = [];
|
|
5836
|
+
async function processQueue() {
|
|
5837
|
+
if (processing || messageQueue.length === 0) return;
|
|
5838
|
+
processing = true;
|
|
5839
|
+
while (messageQueue.length > 0) {
|
|
5840
|
+
const msg = messageQueue.shift();
|
|
5841
|
+
await handleMessage(msg);
|
|
5842
|
+
}
|
|
5843
|
+
processing = false;
|
|
5844
|
+
}
|
|
5845
|
+
sesame.onMessage(async (msg) => {
|
|
5846
|
+
if (shuttingDown) return;
|
|
5847
|
+
if (processedIds.has(msg.id)) {
|
|
5848
|
+
console.log(`[sesame] Skipping duplicate message ${msg.id}`);
|
|
3450
5849
|
return;
|
|
3451
5850
|
}
|
|
3452
5851
|
processedIds.add(msg.id);
|
|
@@ -3555,13 +5954,56 @@ ${response.content}
|
|
|
3555
5954
|
console.error("Error:", err.message);
|
|
3556
5955
|
}
|
|
3557
5956
|
});
|
|
3558
|
-
return new Promise((
|
|
3559
|
-
rl.on("close",
|
|
5957
|
+
return new Promise((resolve19) => {
|
|
5958
|
+
rl.on("close", resolve19);
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
async function runSpawnTask(config, configPath) {
|
|
5962
|
+
const task = process.env.SPAWN_TASK;
|
|
5963
|
+
const spawnId = process.env.SPAWN_ID || "unknown";
|
|
5964
|
+
const context = process.env.SPAWN_CONTEXT || `spawn-${spawnId.slice(0, 8)}`;
|
|
5965
|
+
const channelId = process.env.SPAWN_CHANNEL_ID;
|
|
5966
|
+
const spawnDir = process.env.SPAWN_DIR;
|
|
5967
|
+
console.log(`[spawn] Sub-agent starting (id: ${spawnId}, context: ${context})`);
|
|
5968
|
+
const agent = new Agent(config, context);
|
|
5969
|
+
const hivemindHome = process.env.HIVEMIND_HOME || resolve18(process.env.HOME || "/root", "hivemind");
|
|
5970
|
+
const toolRegistry = registerAllTools(hivemindHome, {
|
|
5971
|
+
enabled: true,
|
|
5972
|
+
workspace: config.agent.workspace || "workspace",
|
|
5973
|
+
braveApiKey: process.env.BRAVE_API_KEY,
|
|
5974
|
+
memoryDaemonUrl: config.memory.daemon_url
|
|
3560
5975
|
});
|
|
5976
|
+
agent.setToolRegistry(toolRegistry);
|
|
5977
|
+
try {
|
|
5978
|
+
const response = await agent.processMessage(task);
|
|
5979
|
+
const result = response.content;
|
|
5980
|
+
console.log(`[spawn] Task completed (context: ${response.context})`);
|
|
5981
|
+
if (spawnDir) {
|
|
5982
|
+
writeFileSync7(resolve18(spawnDir, "result.txt"), result);
|
|
5983
|
+
}
|
|
5984
|
+
if (channelId && config.sesame.api_key) {
|
|
5985
|
+
try {
|
|
5986
|
+
const sesame = new SesameClient2(config.sesame);
|
|
5987
|
+
await sesame.connect();
|
|
5988
|
+
await sesame.sendMessage(channelId, result);
|
|
5989
|
+
sesame.disconnect();
|
|
5990
|
+
console.log(`[spawn] Result sent to channel ${channelId}`);
|
|
5991
|
+
} catch (err) {
|
|
5992
|
+
console.error(`[spawn] Failed to send result to channel:`, err.message);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
} catch (err) {
|
|
5996
|
+
const errorMsg = `[SPAWN ERROR] ${err.message}`;
|
|
5997
|
+
console.error(`[spawn] ${errorMsg}`);
|
|
5998
|
+
if (spawnDir) {
|
|
5999
|
+
writeFileSync7(resolve18(spawnDir, "result.txt"), errorMsg);
|
|
6000
|
+
}
|
|
6001
|
+
process.exitCode = 1;
|
|
6002
|
+
}
|
|
3561
6003
|
}
|
|
3562
6004
|
|
|
3563
6005
|
// packages/runtime/src/fleet/worker-server.ts
|
|
3564
|
-
import { createServer as
|
|
6006
|
+
import { createServer as createServer4 } from "http";
|
|
3565
6007
|
var WorkerServer = class {
|
|
3566
6008
|
server = null;
|
|
3567
6009
|
workerId;
|
|
@@ -3585,20 +6027,20 @@ var WorkerServer = class {
|
|
|
3585
6027
|
}
|
|
3586
6028
|
/** Start listening. */
|
|
3587
6029
|
async start() {
|
|
3588
|
-
return new Promise((
|
|
3589
|
-
this.server =
|
|
6030
|
+
return new Promise((resolve19, reject) => {
|
|
6031
|
+
this.server = createServer4((req, res) => this.handleRequest(req, res));
|
|
3590
6032
|
this.server.on("error", reject);
|
|
3591
|
-
this.server.listen(this.port, () =>
|
|
6033
|
+
this.server.listen(this.port, () => resolve19());
|
|
3592
6034
|
});
|
|
3593
6035
|
}
|
|
3594
6036
|
/** Stop the server. */
|
|
3595
6037
|
async stop() {
|
|
3596
|
-
return new Promise((
|
|
6038
|
+
return new Promise((resolve19) => {
|
|
3597
6039
|
if (!this.server) {
|
|
3598
|
-
|
|
6040
|
+
resolve19();
|
|
3599
6041
|
return;
|
|
3600
6042
|
}
|
|
3601
|
-
this.server.close(() =>
|
|
6043
|
+
this.server.close(() => resolve19());
|
|
3602
6044
|
});
|
|
3603
6045
|
}
|
|
3604
6046
|
getPort() {
|
|
@@ -3721,10 +6163,10 @@ var WorkerServer = class {
|
|
|
3721
6163
|
}
|
|
3722
6164
|
};
|
|
3723
6165
|
function readBody(req) {
|
|
3724
|
-
return new Promise((
|
|
6166
|
+
return new Promise((resolve19, reject) => {
|
|
3725
6167
|
const chunks = [];
|
|
3726
6168
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
3727
|
-
req.on("end", () =>
|
|
6169
|
+
req.on("end", () => resolve19(Buffer.concat(chunks).toString("utf-8")));
|
|
3728
6170
|
req.on("error", reject);
|
|
3729
6171
|
});
|
|
3730
6172
|
}
|
|
@@ -3998,7 +6440,7 @@ export {
|
|
|
3998
6440
|
buildSystemPrompt,
|
|
3999
6441
|
buildMessages,
|
|
4000
6442
|
SessionStore,
|
|
4001
|
-
estimateTokens,
|
|
6443
|
+
estimateTokens2 as estimateTokens,
|
|
4002
6444
|
estimateMessageTokens,
|
|
4003
6445
|
getModelContextWindow,
|
|
4004
6446
|
CompactionManager,
|
|
@@ -4008,6 +6450,7 @@ export {
|
|
|
4008
6450
|
loadConfig,
|
|
4009
6451
|
SesameClient2 as SesameClient,
|
|
4010
6452
|
HEALTH_PATH,
|
|
6453
|
+
SkillsEngine,
|
|
4011
6454
|
startPipeline,
|
|
4012
6455
|
PRIMARY_ROUTES,
|
|
4013
6456
|
WORKER_ROUTES,
|
|
@@ -4057,4 +6500,4 @@ smol-toml/dist/index.js:
|
|
|
4057
6500
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
4058
6501
|
*)
|
|
4059
6502
|
*/
|
|
4060
|
-
//# sourceMappingURL=chunk-
|
|
6503
|
+
//# sourceMappingURL=chunk-WSLVHVNP.js.map
|