eve-hub 0.6.19 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.6.20
4
+
5
+ ### Patch Changes
6
+
7
+ - One syncKit decides every kit stub a bot gets or loses, by where the bot runs. A hub release deletes the stubs of kit modules it removed, in bots and subagents, from a hosted workspace's repo too, so a Mac bot no longer fails to resolve a removed kit tool. A customized file that imports a removed module is reported, not deleted.
8
+
3
9
  ## 0.6.19
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,42 @@
1
+ [
2
+ "channels/eve",
3
+ "channels/eve-roster-user",
4
+ "connections/vercel-kit",
5
+ "hooks/mention-gate",
6
+ "hooks/sandbox",
7
+ "instructions/engineering",
8
+ "instructions/main",
9
+ "instructions/protocol",
10
+ "memory/bot",
11
+ "memory/workspace",
12
+ "schedules/compact",
13
+ "schedules/dash",
14
+ "schedules/prune",
15
+ "skills/onboarding-profile",
16
+ "tools/agent_send",
17
+ "tools/apply_tags",
18
+ "tools/archive_thread",
19
+ "tools/attach_image",
20
+ "tools/bash",
21
+ "tools/browser",
22
+ "tools/create_agent",
23
+ "tools/create_thread",
24
+ "tools/dash_update",
25
+ "tools/disabled",
26
+ "tools/list_agents",
27
+ "tools/list_threads",
28
+ "tools/move_path",
29
+ "tools/onboarding_scripts",
30
+ "tools/post_message",
31
+ "tools/react",
32
+ "tools/read_thread",
33
+ "tools/remove_agent",
34
+ "tools/restart_agent",
35
+ "tools/restart_hub",
36
+ "tools/set_summary",
37
+ "tools/show",
38
+ "tools/tag_threads",
39
+ "tools/unarchive_thread",
40
+ "tools/update_thread",
41
+ "tools/view_image"
42
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
6
6
  "type": "module",
package/prepare.mjs CHANGED
@@ -20,31 +20,14 @@
20
20
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
21
21
  import { basename, dirname, join } from "node:path";
22
22
  import { GITHUB_PATH } from "./github-path.mjs";
23
- import { KIT_FILES, MAIN_KIT_FILES, REMOVED_KIT_FILES } from "./stubs.mjs";
23
+ import { DEPLOYED_KIT_FILES, KIT_FILES, MAIN_KIT_FILES } from "./stubs.mjs";
24
+ import KIT_MODULES from "./kit-modules.json" with { type: "json" };
25
+ import { applyKit, readBotFiles, syncKit } from "./sync-kit.mjs";
26
+ export { DEPLOYED_KIT_FILES };
24
27
  import { deploymentFacts, hostedContext } from "./execution-context.mjs";
25
28
  import { botDomains, domainFolders, readBotMemory, readMemory, validateMemory } from "./memory.mjs";
26
29
  import { agentRunsOn, readWorkspaceFile, removeWorkspaceAgent } from "./workspace.mjs";
27
30
 
28
- // The kit stubs a deployed main keeps: ones whose whole import closure in the
29
- // published kit runs in a Vercel function and talks to the hub over HTTP alone
30
- // (test/workspace-template.test.mjs walks it). Every other kit stub needs the
31
- // hub's machine: the sandbox hook (it migrates host sessions, and deletes any
32
- // sandbox that resolves /workspace as itself, which a Vercel Sandbox does),
33
- // the kit's bash (HostSandbox's root, the daemon's env; eve's own bash runs
34
- // instead), the browser and the screenshot paths of attach_image/view_image,
35
- // the mention gate (the
36
- // daemon's per-process HUB_AGENT and patched model credential), the tools that
37
- // remove or restart bots on the hub's disk (a deployment changes its roster by
38
- // commit; create_agent is that commit, made by the hosted hub), memory (the workspace's files), and main's schedules.
39
- export const DEPLOYED_KIT_FILES = new Set([
40
- "channels/eve.ts",
41
- "skills/onboarding-profile.ts",
42
- "tools/onboarding_scripts.ts",
43
- ...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
44
- ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "agent_send", "create_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
45
- ]);
46
-
47
-
48
31
  // eve's git extension, for every bot: it points the bot at `gh` and `git` in
49
32
  // bash, whose GitHub credential is the sandbox firewall's (the Vercel sandbox lib/agent-template.mjs writes).
50
33
  // With no `github` option it brokers no credential. It only guides: its `pr`
@@ -134,6 +117,11 @@ export function prepareDeployment(dir) {
134
117
  validateMemory(dir);
135
118
  const definition = readWorkspaceFile(dir);
136
119
  const resolved = { ...definition, agents: definition.agents.map((agent) => ({ ...agent, runsOn: agentRunsOn(definition, agent.name) })) };
120
+ const dirOf = (name) => join(dir, "agents", name, "agent");
121
+ const local = new Set(resolved.agents.filter((a) => a.runsOn === "local").map((a) => a.name));
122
+ const folders = readdirSync(join(dir, "agents")).filter((name) => !local.has(name) && existsSync(join(dirOf(name), "agent.ts")));
123
+ const change = syncKit({ bots: folders.map((name) => ({ name, runsOn: "hosted", files: readBotFiles(dirOf(name)) })), modules: KIT_MODULES });
124
+ if (change.report.length) throw new Error(`these files can't ship to a hosted deployment; update or delete them:\n${change.report.map((r) => ` agents/${r.bot}/agent/${r.file} ${r.reason}`).join("\n")}`);
137
125
  writeFileSync(join(dir, MANIFEST_FILE), `${JSON.stringify(resolved, null, 2)}\n`);
138
126
  for (const { name } of definition.agents) {
139
127
  if (agentRunsOn(definition, name) !== "local") continue;
@@ -142,13 +130,10 @@ export function prepareDeployment(dir) {
142
130
  }
143
131
  mkdirSync(join(dir, "agents", "main", "agent", "channels"), { recursive: true });
144
132
  writeFileSync(join(dir, "agents", "main", "agent", "channels", "manifest.ts"), MANIFEST_CHANNEL);
133
+ applyKit(change, dirOf);
145
134
  for (const file of readdirSync(join(dir, "agents"), { recursive: true })) {
146
135
  if (!file.endsWith("/agent.ts")) continue;
147
136
  const bot = dirname(join(dir, "agents", file));
148
- for (const stub of Object.keys({ ...MAIN_KIT_FILES, "memory/workspace.ts": 0, "memory/bot.ts": 0 })) if (!DEPLOYED_KIT_FILES.has(stub) && existsSync(join(bot, stub))) removeStub(bot, stub);
149
- for (const stub of REMOVED_KIT_FILES) if (existsSync(join(bot, stub))) removeStub(bot, stub);
150
- // learning-review is not in MAIN_KIT_FILES, but also requires the host.
151
- if (existsSync(join(bot, "schedules/learning-review.ts"))) removeStub(bot, "schedules/learning-review.ts");
152
137
  const name = basename(dirname(bot));
153
138
  const code = CODE_BOTS.includes(name);
154
139
  for (const [stub, content] of Object.entries({ ...gitExtensionFiles(), ...(code ? codeExtensionFiles(name) : {}) })) {
package/stubs.d.mts ADDED
@@ -0,0 +1,6 @@
1
+ export const KIT_FILES: Record<string, string[]>;
2
+ export const MAIN_KIT_FILES: Record<string, string[]>;
3
+ export const DEPLOYED_KIT_FILES: Set<string>;
4
+ export const PROTOCOL_STUB_FILE: string;
5
+ export const PROTOCOL_STUB: string;
6
+ export function kitStub(file: string, options?: { files?: Record<string, string[]> }): string;
package/stubs.mjs CHANGED
@@ -1,10 +1,6 @@
1
1
  // eve finds tools, hooks, and channels by file (the tool name is the file
2
2
  // name), so each bot keeps a one-line stub per kit module. Bot file -> what it
3
3
  // is, and which kit module it re-exports (default: the same path).
4
- // Kit files whose module is gone. Sync retires them from every bot, and the
5
- // hosted deploy prunes them (the change-explainer: its skill and artifact tools).
6
- export const REMOVED_KIT_FILES = Object.freeze(["tools/artifact_publish.ts", "tools/artifact_status.ts", "tools/artifact_list.ts", "skills/change-explainer.ts"]);
7
-
8
4
  export const KIT_FILES = {
9
5
  "channels/eve.ts": ["Queued eve channel that resolves the registry's human as a user.", "channels/eve-roster-user.ts"],
10
6
  "hooks/sandbox.ts": ["Migrates legacy Docker sessions to the host sandbox."],
@@ -46,3 +42,106 @@ export const MAIN_KIT_FILES = {
46
42
  "schedules/dash.ts": ["When a bot's turn ends (fired by the hub), and every 8 minutes: the human's PRs, a Dash entry for each thread that changed, and a new summary for each track with news."],
47
43
  "schedules/prune.ts": ["Every 10 minutes: one prune pass over what archived or deleted threads leave on disk: worktrees, scratch, their-only uploads, idle bots' traces and workflow runs; unsaved work kept as refs/bundles."],
48
44
  };
45
+
46
+ // The kit stubs a deployed main keeps: ones whose whole import closure in the
47
+ // published kit runs in a Vercel function and talks to the hub over HTTP alone
48
+ // (test/workspace-template.test.mjs walks it). Every other kit stub needs the
49
+ // hub's machine: the sandbox hook (it migrates host sessions, and deletes any
50
+ // sandbox that resolves /workspace as itself, which a Vercel Sandbox does),
51
+ // the kit's bash (HostSandbox's root, the daemon's env; eve's own bash runs
52
+ // instead), the browser and the screenshot paths of attach_image/view_image,
53
+ // the mention gate (the
54
+ // daemon's per-process HUB_AGENT and patched model credential), the tools that
55
+ // remove or restart bots on the hub's disk (a deployment changes its roster by
56
+ // commit; create_agent is that commit, made by the hosted hub), memory (the workspace's files), and main's schedules.
57
+ export const DEPLOYED_KIT_FILES = new Set([
58
+ "channels/eve.ts",
59
+ "skills/onboarding-profile.ts",
60
+ "tools/onboarding_scripts.ts",
61
+ ...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
62
+ ...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "agent_send", "create_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
63
+ ]);
64
+
65
+ // ---- stub text: what each kit stub file says ----
66
+
67
+ export const kitPath = (kit) => `eve-hub/${kit.replace(/\.(ts|mjs|js)$/, "")}`;
68
+
69
+ export function kitStub(file, { files = KIT_FILES, kit = files[file]?.[1] ?? file, what = files[file]?.[0] } = {}) {
70
+ return `// ${what} From eve-hub (packages/agent-kit/${kit}).\nexport { default } from "${kitPath(kit)}";\n`;
71
+ }
72
+
73
+ // Every bot but main: instructions.md says who the bot is (its head), and
74
+ // instructions/protocol.ts re-exports the kit's protocol, which renders
75
+ // packages/agent-kit/templates/workspace-protocol.md at build time. eve appends the
76
+ // instructions/ entries after instructions.md, so the protocol follows the head
77
+ // and is never copied into a bot.
78
+ export const PROTOCOL_STUB_FILE = "instructions/protocol.ts";
79
+ export const PROTOCOL_STUB = `// The workspace protocol every bot shares, from packages/agent-kit/templates/workspace-protocol.md,
80
+ // through eve-hub (packages/agent-kit/instructions/protocol.ts). eve appends it after
81
+ // instructions.md, which holds only who this bot is. Don't copy the protocol there.
82
+ export { default } from "eve-hub/instructions/protocol";
83
+ `;
84
+ // Every bot (main too): instructions/execution-context.ts renders the bot's
85
+ // execution context from its definition in hub.workspace.json
86
+ // (packages/agent-kit/execution-context.mjs). A hosted bot's is replaced by
87
+ // the Vercel build with the hosted page.
88
+ export const EXECUTION_CONTEXT_STUB_FILE = "instructions/execution-context.ts";
89
+ export const executionContextStub = (name) => `import { defineInstructions } from "eve/instructions";
90
+ import { executionContext } from "eve-hub/execution-context";
91
+ import workspace from "../../../../hub.workspace.json" with { type: "json" };
92
+
93
+ // Where ${name} runs, from its definition in hub.workspace.json: local, from the kit's
94
+ // templates (packages/agent-kit/templates/execution-context-*.md). The Vercel build writes a hosted bot's.
95
+ export default defineInstructions({ content: executionContext(${JSON.stringify(name)}, workspace) });
96
+ `;
97
+
98
+ // Every bot (main too) has two memory slots: memory/workspace.ts recalls the
99
+ // workspace's memory/ entries and memory/bot.ts the bot's own
100
+ // agents/<bot>/memory/ entries, every turn (packages/agent-kit/memory/).
101
+ const memoryStub = (slot, what) => `// ${what}, recalled every turn. From eve-hub
102
+ // (packages/agent-kit/memory/${slot}.ts); add or supersede entries there, not here.
103
+ export { default } from "eve-hub/memory/${slot}";
104
+ `;
105
+ export const MEMORY_STUBS = {
106
+ "memory/workspace.ts": memoryStub("workspace", "The workspace's memory/ entries"),
107
+ "memory/bot.ts": memoryStub("bot", "This bot's own agents/<bot>/memory/ entries"),
108
+ };
109
+
110
+ // main's instructions aren't copied into a workspace: agent/instructions.ts
111
+ // re-exports the kit's, which renders packages/agent-kit/templates/main-instructions.md +
112
+ // workspace-protocol.md at build time (packages/agent-kit/main-instructions.mjs).
113
+ // Workspace-only additions go in agent/instructions/*.md; eve appends them.
114
+ export const MAIN_INSTRUCTIONS = "instructions.ts";
115
+ export const MAIN_INSTRUCTIONS_STUB = `// main's instructions, from packages/agent-kit/templates (main-instructions.md + workspace-protocol.md),
116
+ // through eve-hub (packages/agent-kit/instructions/main.ts). Don't copy them here:
117
+ // workspace-only additions go in instructions/*.md next to this file, appended after these.
118
+ export { default } from "eve-hub/instructions/main";
119
+ `;
120
+
121
+ // True when the file's only code is `export { default } from "eve-hub/<module>"`
122
+ // (main's instructions by default). Comments are stripped first, so a
123
+ // commented-out import doesn't count, and anything else in the file (an
124
+ // override, a second export) makes it custom.
125
+ export function isKitReexport(source, module = "instructions/main") {
126
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
127
+ const escaped = module.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
128
+ return new RegExp(`^export\\s*\\{\\s*default\\s*\\}\\s*from\\s*(["'])eve-hub\\/${escaped}\\1\\s*;?$`).test(code);
129
+ }
130
+
131
+
132
+ // Where a bot's instructions come from, given its files ({ "instructions.md": text, ... }):
133
+ // main ("kit", "copy", "custom", "conflict", "missing"), or any other bot (the protocol:
134
+ // "kit", "copy", "conflict", "custom", "missing").
135
+ const PROTOCOL_HEADING = /^## How this workspace works\s*$/m;
136
+ export function instructionsSource(files, main) {
137
+ const has = (file) => typeof files[file] === "string";
138
+ if (main) {
139
+ if (has(MAIN_INSTRUCTIONS) && has("instructions.md")) return "conflict";
140
+ if (has(MAIN_INSTRUCTIONS)) return isKitReexport(files[MAIN_INSTRUCTIONS]) ? "kit" : "custom";
141
+ return has("instructions.md") ? "copy" : "missing";
142
+ }
143
+ const copy = has("instructions.md") && PROTOCOL_HEADING.test(files["instructions.md"]);
144
+ if (!has(PROTOCOL_STUB_FILE)) return copy ? "copy" : "missing";
145
+ if (copy) return "conflict";
146
+ return isKitReexport(files[PROTOCOL_STUB_FILE], "instructions/protocol") ? "kit" : "custom";
147
+ }
package/sync-kit.d.mts ADDED
@@ -0,0 +1,12 @@
1
+ export type KitBot = { name: string; runsOn: 'local' | 'hosted'; files: Record<string, string> };
2
+ export type KitChange = {
3
+ write: { bot: string; file: string; content: string }[];
4
+ delete: { bot: string; file: string }[];
5
+ report: { bot: string; file: string; reason: string }[];
6
+ instructions: Record<string, string>;
7
+ };
8
+ export function syncKit(input: { bots: KitBot[]; modules: Iterable<string> }): KitChange;
9
+ export function isKitFile(file: string): boolean;
10
+ export function readBotFiles(agentDir: string): Record<string, string>;
11
+ export function applyKit(change: Pick<KitChange, 'write' | 'delete'>, dirOf: (bot: string) => string): void;
12
+ export function kitModules(kitDir: string): string[];
package/sync-kit.mjs ADDED
@@ -0,0 +1,126 @@
1
+ // The one place that decides which kit stub files a bot should have. Given each bot's files and the
2
+ // kit's modules it returns the whole change (what to write, what to delete) and touches nothing.
3
+ // The local sync (lib/agent-template.mjs), the hosted deploy (prepare.mjs) and the hosted kit bump
4
+ // (hosted-hub/src/onboard/repo-bump.ts) apply what it returns; none keeps its own list.
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import {
8
+ DEPLOYED_KIT_FILES, EXECUTION_CONTEXT_STUB_FILE, KIT_FILES, MAIN_INSTRUCTIONS, MAIN_INSTRUCTIONS_STUB, MAIN_KIT_FILES, MEMORY_STUBS, PROTOCOL_STUB, PROTOCOL_STUB_FILE,
9
+ executionContextStub, instructionsSource, isKitReexport, kitPath, kitStub,
10
+ } from "./stubs.mjs";
11
+
12
+ const KIT_DIRS = ["tools", "skills", "hooks", "channels", "schedules", "instructions", "memory", "connections"];
13
+ const SUBAGENT = /^(?:subagents\/[^/]+\/)+/;
14
+ const STUB_FILE = new RegExp(`^(${KIT_DIRS.join("|")})/[^/]+\\.ts$`);
15
+ const KIT_IMPORT = new RegExp(`^\\s*(?:import|export)\\b(?:[^;"']*?\\bfrom)?\\s*["']eve-hub/((?:${KIT_DIRS.join("|")})/[\\w-]+)["']`, "gm");
16
+ const ALL_STUBS = { ...KIT_FILES, ...MAIN_KIT_FILES };
17
+ const HOST_ONLY = [...Object.keys(ALL_STUBS), ...Object.keys(MEMORY_STUBS)].filter((file) => !DEPLOYED_KIT_FILES.has(file));
18
+ const moduleOf = (file) => (MEMORY_STUBS[file] ? file.slice(0, -3) : kitPath(ALL_STUBS[file][1] ?? file).slice("eve-hub/".length));
19
+ const HOST_ONLY_MODULES = new Set(HOST_ONLY.map(moduleOf));
20
+
21
+ /** Whether `file` (relative to a bot's agent/ folder, subagents included) is one syncKit reads. */
22
+ export const isKitFile = (file) => file === "instructions.md" || file === MAIN_INSTRUCTIONS || STUB_FILE.test(file.replace(SUBAGENT, ""));
23
+
24
+ // Every kit module the file imports, from its import and export lines only.
25
+ function kitImports(text) {
26
+ const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
27
+ return [...code.matchAll(KIT_IMPORT)].map((m) => m[1]);
28
+ }
29
+
30
+ /**
31
+ * bots: [{ name, runsOn, files }], files being { "<path under agents/<name>/agent>": text } for the files
32
+ * isKitFile accepts (a subagent's are under "subagents/<x>/"). modules: every kit module a stub may import
33
+ * ("tools/post_message"). runsOn "local" gets the whole kit, execution-context and memory stubs included;
34
+ * "hosted" only the stubs a Vercel function can run (DEPLOYED_KIT_FILES) and loses the others, which the
35
+ * build writes itself (memory, execution context) or cannot use.
36
+ * Returns { write, delete, report, instructions }: write [{ bot, file, content }] (a file that exists is
37
+ * replaced), delete [{ bot, file }], report [{ bot, file, reason }] for a customized file that imports a
38
+ * module the kit lacks, or that a hosted bot can't run (never touched: a person's call; a caller
39
+ * that ships the result should refuse it), instructions { [bot]: source }.
40
+ * Only a plain kit re-export is deleted or replaced.
41
+ */
42
+ export function syncKit({ bots, modules }) {
43
+ const kit = new Set(modules);
44
+ const change = { write: [], delete: [], report: [], instructions: {} };
45
+ for (const { name: bot, runsOn, files: found } of bots) {
46
+ if (runsOn !== "local" && runsOn !== "hosted") throw new Error(`${bot}: runsOn must be "local" or "hosted", not ${JSON.stringify(runsOn)}`);
47
+ const local = runsOn === "local";
48
+ const main = bot === "main";
49
+ const stubs = main ? MAIN_KIT_FILES : KIT_FILES;
50
+ const files = { ...found };
51
+ const write = (file, content) => {
52
+ files[file] = content;
53
+ change.write.push({ bot, file, content });
54
+ };
55
+ let source = instructionsSource(files, main);
56
+ if (source === "missing") {
57
+ write(main ? MAIN_INSTRUCTIONS : PROTOCOL_STUB_FILE, main ? MAIN_INSTRUCTIONS_STUB : PROTOCOL_STUB);
58
+ source = "kit";
59
+ }
60
+ change.instructions[bot] = source;
61
+ const wanted = new Set(Object.keys(stubs).filter((file) => local || DEPLOYED_KIT_FILES.has(file)));
62
+ if (local) {
63
+ wanted.add(EXECUTION_CONTEXT_STUB_FILE);
64
+ for (const file of Object.keys(MEMORY_STUBS)) wanted.add(file);
65
+ }
66
+ const content = (file) => (file === EXECUTION_CONTEXT_STUB_FILE ? executionContextStub(bot) : MEMORY_STUBS[file] ?? kitStub(file, { files: stubs }));
67
+ for (const file of Object.keys(files).sort()) {
68
+ const rel = file.replace(SUBAGENT, "");
69
+ if (!STUB_FILE.test(rel)) continue;
70
+ const imports = kitImports(files[file]);
71
+ const gone = imports.find((module) => !kit.has(module));
72
+ if (gone) {
73
+ if (!isKitReexport(files[file], gone)) change.report.push({ bot, file, reason: `imports eve-hub/${gone}, which the kit no longer has` });
74
+ else if (rel === file && wanted.has(file)) write(file, content(file));
75
+ else change.delete.push({ bot, file });
76
+ } else if (!local) {
77
+ const host = imports.find((module) => HOST_ONLY_MODULES.has(module));
78
+ if (!host) continue;
79
+ if (isKitReexport(files[file], host)) change.delete.push({ bot, file });
80
+ else change.report.push({ bot, file, reason: `imports eve-hub/${host}, which needs the hub's own machine` });
81
+ }
82
+ }
83
+ for (const file of wanted) if (files[file] === undefined) write(file, content(file));
84
+ }
85
+ return change;
86
+ }
87
+
88
+ /** The files syncKit looks at in one bot's agent/ folder, its subagents' too. */
89
+ export function readBotFiles(agentDir, prefix = "") {
90
+ const files = {};
91
+ const read = (file) => {
92
+ if (existsSync(join(agentDir, file))) files[prefix + file] = readFileSync(join(agentDir, file), "utf8");
93
+ };
94
+ if (!prefix) {
95
+ read("instructions.md");
96
+ read(MAIN_INSTRUCTIONS);
97
+ }
98
+ for (const dir of KIT_DIRS) {
99
+ if (!existsSync(join(agentDir, dir))) continue;
100
+ for (const entry of readdirSync(join(agentDir, dir), { withFileTypes: true })) if (entry.isFile() && entry.name.endsWith(".ts")) read(`${dir}/${entry.name}`);
101
+ }
102
+ if (existsSync(join(agentDir, "subagents"))) {
103
+ for (const entry of readdirSync(join(agentDir, "subagents"), { withFileTypes: true })) {
104
+ if (entry.isDirectory()) Object.assign(files, readBotFiles(join(agentDir, "subagents", entry.name), `${prefix}subagents/${entry.name}/`));
105
+ }
106
+ }
107
+ return files;
108
+ }
109
+
110
+ /** Applies a change to disk; dirOf(bot) is the bot's agent/ folder. Deleting a stub removes the folders it leaves empty. */
111
+ export function applyKit(change, dirOf) {
112
+ for (const { bot, file } of change.delete) {
113
+ const dir = dirOf(bot);
114
+ unlinkSync(join(dir, file));
115
+ for (let d = dirname(file); d !== "." && !readdirSync(join(dir, d)).length; d = dirname(d)) rmdirSync(join(dir, d));
116
+ }
117
+ for (const { bot, file, content } of change.write) {
118
+ mkdirSync(dirname(join(dirOf(bot), file)), { recursive: true });
119
+ writeFileSync(join(dirOf(bot), file), content);
120
+ }
121
+ }
122
+
123
+ /** Every module a stub may import, read from the kit folder; kit-modules.json is this list (test/sync-kit.test.mjs). */
124
+ export function kitModules(kitDir) {
125
+ return KIT_DIRS.flatMap((dir) => (existsSync(join(kitDir, dir)) ? readdirSync(join(kitDir, dir)).filter((f) => f.endsWith(".ts")).map((f) => `${dir}/${f.slice(0, -3)}`) : [])).sort();
126
+ }