@sesamespace/hivemind 0.7.2 → 0.8.0

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