zoe-agent 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,244 @@
1
+ import { spawn } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ export const ShellTool = {
6
+ name: "Shell Execution",
7
+ risk: "destructive",
8
+ definition: {
9
+ type: "function",
10
+ function: {
11
+ name: "execute_shell_command",
12
+ description: "Execute a shell command on the host machine. Use this to run scripts, list files, or interact with the system.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ command: { type: "string", description: "The shell command to execute." },
17
+ rationale: { type: "string", description: "Explain why you are running this command." }
18
+ },
19
+ required: ["command", "rationale"]
20
+ }
21
+ }
22
+ },
23
+ // Runs via `spawn(shell:true)` (same shell as the former `exec`) so stdout
24
+ // can stream live via onUpdate; the resolved string matches the old format
25
+ // (stdout + optional "Stderr:" suffix) so headless output is unchanged.
26
+ handler: async (args, _config, extra) => {
27
+ const onUpdate = extra?.onUpdate;
28
+ return new Promise((resolve) => {
29
+ let stdout = '';
30
+ let stderr = '';
31
+ const child = spawn(args.command, { shell: true });
32
+ child.stdout?.on('data', (data) => {
33
+ const chunk = data.toString();
34
+ stdout += chunk;
35
+ if (onUpdate)
36
+ onUpdate({ message: chunk });
37
+ });
38
+ child.stderr?.on('data', (data) => {
39
+ stderr += data.toString();
40
+ });
41
+ child.on('error', (err) => {
42
+ resolve(`Command failed: ${err.message}\nStdout: ${stdout}\nStderr: ${stderr}`);
43
+ });
44
+ child.on('close', (code) => {
45
+ if (code === 0) {
46
+ resolve(stdout + (stderr ? `\nStderr: ${stderr}` : ''));
47
+ }
48
+ else {
49
+ resolve(`Command failed: ${args.command} (exit ${code ?? 'null'})\nStdout: ${stdout}\nStderr: ${stderr}`);
50
+ }
51
+ });
52
+ });
53
+ }
54
+ };
55
+ export const ReadFileTool = {
56
+ name: "File Reader",
57
+ risk: "safe",
58
+ definition: {
59
+ type: "function",
60
+ function: {
61
+ name: "read_file",
62
+ description: "Read the content of a file.",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {
66
+ path: { type: "string", description: "The path to the file to read." }
67
+ },
68
+ required: ["path"]
69
+ }
70
+ }
71
+ },
72
+ handler: async (args) => {
73
+ try {
74
+ const content = await fs.readFile(args.path, 'utf-8');
75
+ return content;
76
+ }
77
+ catch (error) {
78
+ return `Error reading file: ${error.message}`;
79
+ }
80
+ }
81
+ };
82
+ /** Files larger than this (bytes OR lines) skip the inline diff to avoid
83
+ * dumping a huge render into the TUI. */
84
+ const DIFF_BYTE_CAP = 64 * 1024;
85
+ const DIFF_LINE_CAP = 2000;
86
+ /** Line count that treats a trailing newline as the end of the last line, not
87
+ * the start of an empty one: "a\nb\n" is 2 lines, "a\nb" is 2, "a\n" is 1. */
88
+ const lineCount = (text) => {
89
+ if (text === "")
90
+ return 0;
91
+ const newlines = text.split("\n").length - 1;
92
+ return text.endsWith("\n") ? newlines : newlines + 1;
93
+ };
94
+ /** Temps younger than this are assumed to belong to a live write (possibly a
95
+ * concurrent process) and are left alone; only older orphans are swept. */
96
+ const STALE_TEMP_AGE_MS = 60_000;
97
+ /** Remove orphaned `.zoe-*.tmp` write temps for `basename` in `dir` — left
98
+ * behind by a hard kill (SIGKILL/power loss) in the window between temp-write
99
+ * and rename, which the handler's catch block can't reach. Only temps older
100
+ * than STALE_TEMP_AGE_MS are removed, so a peer's in-flight temp is never
101
+ * touched (no cross-process race). Best-effort; errors swallowed. */
102
+ async function cleanStaleTemps(dir, basename) {
103
+ let entries;
104
+ try {
105
+ entries = await fs.readdir(dir);
106
+ }
107
+ catch {
108
+ return; // dir doesn't exist yet (new file in a new dir) — nothing to sweep
109
+ }
110
+ const prefix = `${basename}.zoe-`;
111
+ const cutoff = Date.now() - STALE_TEMP_AGE_MS;
112
+ await Promise.all(entries
113
+ .filter((e) => e.startsWith(prefix) && e.endsWith(".tmp"))
114
+ .map(async (e) => {
115
+ const full = path.join(dir, e);
116
+ try {
117
+ const st = await fs.stat(full);
118
+ if (st.mtimeMs < cutoff)
119
+ await fs.unlink(full);
120
+ }
121
+ catch { /* raced with another sweeper or already gone — ignore */ }
122
+ }));
123
+ }
124
+ export const WriteFileTool = {
125
+ name: "File Writer",
126
+ risk: "edit",
127
+ definition: {
128
+ type: "function",
129
+ function: {
130
+ name: "write_file",
131
+ description: "Write content to a file. Overwrites existing files.",
132
+ parameters: {
133
+ type: "object",
134
+ properties: {
135
+ path: { type: "string", description: "The path to the file to write." },
136
+ content: { type: "string", description: "The content to write." }
137
+ },
138
+ required: ["path", "content"]
139
+ }
140
+ }
141
+ },
142
+ handler: async (args) => {
143
+ const filePath = args.path;
144
+ const newContent = args.content;
145
+ // 1. Stat the target (size only — never slurp a huge file). Read the old
146
+ // content ONLY when neither side already exceeds the byte/line caps, so a
147
+ // large write doesn't pay for a pointless read. The line cap applies to
148
+ // BOTH sides — a small-but-thousands-of-lines file diffs just as badly.
149
+ let fileExists = false;
150
+ let oldBytes = 0;
151
+ try {
152
+ const st = await fs.stat(filePath);
153
+ fileExists = true;
154
+ oldBytes = st.size;
155
+ }
156
+ catch {
157
+ // absent ⇒ new file
158
+ }
159
+ const isNewFile = !fileExists;
160
+ const newBytes = Buffer.byteLength(newContent, "utf-8");
161
+ const newOverCap = newBytes > DIFF_BYTE_CAP || lineCount(newContent) > DIFF_LINE_CAP;
162
+ const oldByteOverCap = oldBytes > DIFF_BYTE_CAP;
163
+ let oldContent = null;
164
+ let oldLineOverCap = false;
165
+ if (!newOverCap && !oldByteOverCap) {
166
+ try {
167
+ oldContent = await fs.readFile(filePath, "utf-8");
168
+ oldLineOverCap = lineCount(oldContent) > DIFF_LINE_CAP;
169
+ }
170
+ catch {
171
+ // absent ⇒ new file (oldContent stays null)
172
+ }
173
+ }
174
+ const overCap = newOverCap || oldByteOverCap || oldLineOverCap;
175
+ // 2. Atomic write: temp file in the SAME directory (same filesystem ⇒
176
+ // fs.rename is atomic on POSIX), then rename. On any failure the temp
177
+ // is unlinked and the original is never partially written. A same-path
178
+ // temp orphaned by a prior hard kill is swept first.
179
+ const dir = path.dirname(filePath);
180
+ const tmpPath = `${filePath}.zoe-${randomUUID().slice(0, 8)}.tmp`;
181
+ await cleanStaleTemps(dir, path.basename(filePath));
182
+ try {
183
+ await fs.mkdir(dir, { recursive: true });
184
+ await fs.writeFile(tmpPath, newContent, "utf-8");
185
+ await fs.rename(tmpPath, filePath);
186
+ }
187
+ catch (error) {
188
+ try {
189
+ await fs.unlink(tmpPath);
190
+ }
191
+ catch { /* temp may not have been created */ }
192
+ return { output: `Error writing file: ${error.message}`, success: false };
193
+ }
194
+ // 3. Build metadata for the diff viewer. `overCap` already accounts for both
195
+ // sides (byte AND line), so a small edit to a large/many-line file skips
196
+ // the diff instead of storing/rendering the whole old file. Full content
197
+ // is omitted when over cap.
198
+ const metadata = overCap
199
+ ? {
200
+ path: filePath,
201
+ isNewFile,
202
+ byteDelta: newBytes - oldBytes,
203
+ diffSkipped: true,
204
+ skipReason: `file exceeds ${DIFF_BYTE_CAP} bytes or ${DIFF_LINE_CAP} lines`,
205
+ }
206
+ : {
207
+ path: filePath,
208
+ oldContent,
209
+ newContent,
210
+ isNewFile,
211
+ byteDelta: newBytes - oldBytes,
212
+ };
213
+ return {
214
+ output: `Successfully wrote to ${filePath} (${lineCount(oldContent ?? "")} -> ${lineCount(newContent)} lines)`,
215
+ success: true,
216
+ metadata,
217
+ };
218
+ }
219
+ };
220
+ export const DateTimeTool = {
221
+ name: "Date & Time",
222
+ risk: "safe",
223
+ definition: {
224
+ type: "function",
225
+ function: {
226
+ name: "get_current_datetime",
227
+ description: "Get the current system date and time. Use this when the user refers to relative dates (like 'today', 'next week', 'this March') to ensure accuracy.",
228
+ parameters: {
229
+ type: "object",
230
+ properties: {},
231
+ required: []
232
+ }
233
+ }
234
+ },
235
+ handler: async () => {
236
+ const now = new Date();
237
+ return JSON.stringify({
238
+ iso: now.toISOString(),
239
+ local: now.toLocaleString(),
240
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
241
+ weekday: now.toLocaleDateString('en-US', { weekday: 'long' })
242
+ }, null, 2);
243
+ }
244
+ };
@@ -0,0 +1,2 @@
1
+ import { ToolModule } from './interface.js';
2
+ export declare const EmailTool: ToolModule;
@@ -0,0 +1,61 @@
1
+ import nodemailer from 'nodemailer';
2
+ export const EmailTool = {
3
+ name: "Email Service",
4
+ risk: "communications",
5
+ configKeys: ["smtpHost", "smtpPort", "smtpUser", "smtpPass", "smtpFrom"],
6
+ definition: {
7
+ type: "function",
8
+ function: {
9
+ name: "send_email",
10
+ description: "Send an email using configured SMTP settings. Can include optional file attachments.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ to: { type: "string", description: "Recipient email address." },
15
+ subject: { type: "string", description: "Email subject." },
16
+ body: { type: "string", description: "Email body content (text)." },
17
+ attachments: {
18
+ type: "array",
19
+ items: { type: "string" },
20
+ description: "Optional list of local file paths to attach to the email."
21
+ }
22
+ },
23
+ required: ["to", "subject", "body"]
24
+ }
25
+ }
26
+ },
27
+ handler: async (args, config) => {
28
+ // Validate config
29
+ if (!config?.smtpHost || !config?.smtpUser || !config?.smtpPass) {
30
+ return "Error: Email tool is not configured. Please run 'zoe setup' to configure SMTP settings.";
31
+ }
32
+ try {
33
+ const transporter = nodemailer.createTransport({
34
+ host: config.smtpHost,
35
+ port: parseInt(config.smtpPort || '587'),
36
+ secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
37
+ auth: {
38
+ user: config.smtpUser,
39
+ pass: config.smtpPass,
40
+ },
41
+ });
42
+ const emailAttachments = args.attachments?.map((filePath) => ({
43
+ path: filePath
44
+ })) || [];
45
+ const info = await transporter.sendMail({
46
+ from: config.smtpFrom || config.smtpUser, // sender address
47
+ to: args.to, // list of receivers
48
+ subject: args.subject, // Subject line
49
+ text: args.body, // plain text body
50
+ attachments: emailAttachments
51
+ });
52
+ return `Email sent successfully. Message ID: ${info.messageId}`;
53
+ }
54
+ catch (error) {
55
+ // Return detailed error info for debugging
56
+ const code = error.code ? `[Code: ${error.code}] ` : '';
57
+ const response = error.response ? ` (Server Response: ${error.response})` : '';
58
+ return `Failed to send email: ${code}${error.message}${response}`;
59
+ }
60
+ }
61
+ };
@@ -0,0 +1,2 @@
1
+ import { ToolModule } from './interface.js';
2
+ export declare const ImageTool: ToolModule;
@@ -0,0 +1,257 @@
1
+ import { OpenAI } from 'openai';
2
+ import chalk from 'chalk';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ const toolDefinition = {
6
+ type: "function",
7
+ function: {
8
+ name: "generate_image",
9
+ description: "Generates or edits images using AI models (DALL-E 3/2). Supports text-to-image, image variation, and image editing. Allows control over size, resolution (quality), and model selection.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ prompt: {
14
+ type: "string",
15
+ description: "Text description of the desired image. Required for text-to-image and edit modes."
16
+ },
17
+ image_path: {
18
+ type: "string",
19
+ description: "Path to an existing image file (local path). Required for variation and editing modes."
20
+ },
21
+ mask_path: {
22
+ type: "string",
23
+ description: "Path to a mask image file (local path). Optional, used only for editing."
24
+ },
25
+ mode: {
26
+ type: "string",
27
+ enum: ["text-to-image", "variation", "edit"],
28
+ description: "Operation mode. Inferred if not provided."
29
+ },
30
+ model: {
31
+ type: "string",
32
+ description: "The AI model to use. 'dall-e-3' for high quality (default), 'dall-e-2' for editing, or a custom model like 'doubao-seedream-4-5-251128'.",
33
+ default: "dall-e-3"
34
+ },
35
+ n: {
36
+ type: "integer",
37
+ description: "Number of images to generate. Default is 1.",
38
+ default: 1
39
+ },
40
+ size: {
41
+ type: "string",
42
+ description: "Resolution/Aspect Ratio. YOU should infer the best size based on the prompt content.\n- DALL-E 3: '1024x1024' (Square), '1792x1024' (Landscape), '1024x1792' (Portrait).\n- Doubao/High-Res: MUST be >3.6M pixels. Use '2048x2048' (Square), '2560x1440' (Landscape), '1440x2560' (Portrait).",
43
+ default: "1024x1024"
44
+ },
45
+ quality: {
46
+ type: "string",
47
+ enum: ["standard", "hd"],
48
+ description: "Image quality (DALL-E 3 only). 'hd' creates more detailed images. Default is 'standard'.",
49
+ default: "standard"
50
+ },
51
+ style: {
52
+ type: "string",
53
+ enum: ["vivid", "natural"],
54
+ description: "Image style (DALL-E 3 only). Default is 'vivid'.",
55
+ default: "vivid"
56
+ },
57
+ output_dir: {
58
+ type: "string",
59
+ description: "Directory to save the generated images. Defaults to current directory."
60
+ }
61
+ },
62
+ required: []
63
+ }
64
+ }
65
+ };
66
+ async function downloadImage(url, destPath) {
67
+ const response = await fetch(url);
68
+ if (!response.ok)
69
+ throw new Error(`Failed to download image: ${response.statusText}`);
70
+ const buffer = await response.arrayBuffer();
71
+ fs.writeFileSync(destPath, Buffer.from(buffer));
72
+ }
73
+ const handler = async (args, config) => {
74
+ const apiKey = config.imageApiKey || config.apiKey || process.env.OPENAI_API_KEY;
75
+ const baseURL = config.imageBaseUrl || config.baseUrl || process.env.OPENAI_COMPAT_BASE_URL || process.env.OPENAI_BASE_URL;
76
+ if (!apiKey) {
77
+ return "Error: Image Service API Key is missing. Please configure it in .zoe/setting.json (imageApiKey or apiKey).";
78
+ }
79
+ const client = new OpenAI({
80
+ apiKey: apiKey,
81
+ baseURL: baseURL
82
+ });
83
+ const { prompt, image_path, mask_path, output_dir = "." } = args;
84
+ const n = args.n || config.imageN || 1;
85
+ // Model-specific default size
86
+ let mode = args.mode;
87
+ let model = args.model;
88
+ if (config.imageModel && (!model || model === 'dall-e-3')) {
89
+ model = config.imageModel;
90
+ }
91
+ model = model || "dall-e-3";
92
+ let defaultSize = "1024x1024";
93
+ if (model.toLowerCase().includes("doubao")) {
94
+ defaultSize = "2048x2048";
95
+ }
96
+ const size = args.size || config.imageSize || defaultSize;
97
+ const quality = args.quality || config.imageQuality || "standard";
98
+ const style = args.style || config.imageStyle || "vivid";
99
+ // Infer mode if not provided
100
+ if (!mode) {
101
+ if (image_path && mask_path)
102
+ mode = "edit";
103
+ else if (image_path)
104
+ mode = "variation";
105
+ else
106
+ mode = "text-to-image";
107
+ }
108
+ // Model-specific validations
109
+ if (mode === "text-to-image") {
110
+ // DALL-E 3 Validation
111
+ if (model === "dall-e-3") {
112
+ const validSizes = ["1024x1024", "1024x1792", "1792x1024"];
113
+ if (!validSizes.includes(size)) {
114
+ return `Error: Invalid size '${size}' for DALL-E 3. Supported sizes are: ${validSizes.join(", ")}.`;
115
+ }
116
+ }
117
+ // DALL-E 2 Validation
118
+ else if (model === "dall-e-2") {
119
+ const validSizes = ["256x256", "512x512", "1024x1024"];
120
+ if (!validSizes.includes(size)) {
121
+ return `Error: Invalid size '${size}' for DALL-E 2. Supported sizes are: ${validSizes.join(", ")}.`;
122
+ }
123
+ }
124
+ }
125
+ else {
126
+ // Variation and Edit only support DALL-E 2 currently
127
+ if (model === "dall-e-3") {
128
+ console.log("Note: DALL-E 3 does not support variation/edit. Falling back to DALL-E 2.");
129
+ model = "dall-e-2";
130
+ }
131
+ }
132
+ // Resolve output directory
133
+ const resolvedOutputDir = path.resolve(process.cwd(), output_dir);
134
+ if (!fs.existsSync(resolvedOutputDir)) {
135
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
136
+ }
137
+ const generatedFiles = [];
138
+ try {
139
+ if (mode === "text-to-image") {
140
+ if (!prompt)
141
+ return "Error: 'prompt' is required for text-to-image mode.";
142
+ console.log(`Generating ${n} image(s) with ${model} (${size}, ${quality})...`);
143
+ if (model === "dall-e-3") {
144
+ for (let i = 0; i < n; i++) {
145
+ const response = await client.images.generate({
146
+ model: "dall-e-3",
147
+ prompt: prompt,
148
+ n: 1, // DALL-E 3 constraint
149
+ size: size,
150
+ quality: quality,
151
+ style: style,
152
+ response_format: "url"
153
+ });
154
+ const imageUrl = response.data?.[0]?.url;
155
+ if (imageUrl) {
156
+ const fileName = `generated-${Date.now()}-${i + 1}.png`;
157
+ const filePath = path.join(resolvedOutputDir, fileName);
158
+ await downloadImage(imageUrl, filePath);
159
+ generatedFiles.push(filePath);
160
+ }
161
+ }
162
+ }
163
+ else {
164
+ // DALL-E 2 or Custom Model
165
+ const response = await client.images.generate({
166
+ model: model,
167
+ prompt: prompt,
168
+ n: n,
169
+ size: size,
170
+ response_format: "url"
171
+ });
172
+ const data = response.data || [];
173
+ for (let i = 0; i < data.length; i++) {
174
+ const imageUrl = data[i].url;
175
+ if (imageUrl) {
176
+ const fileName = `generated-${Date.now()}-${i + 1}.png`;
177
+ const filePath = path.join(resolvedOutputDir, fileName);
178
+ await downloadImage(imageUrl, filePath);
179
+ generatedFiles.push(filePath);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ else if (mode === "variation") {
185
+ if (!image_path)
186
+ return "Error: 'image_path' is required for variation mode.";
187
+ if (!fs.existsSync(image_path))
188
+ return `Error: Image file not found at ${image_path}`;
189
+ console.log(`Generating ${n} variation(s) with ${model}...`);
190
+ const response = await client.images.createVariation({
191
+ image: fs.createReadStream(image_path),
192
+ n: n,
193
+ model: "dall-e-2", // Explicitly set model just in case, though it's the default/only option
194
+ size: size,
195
+ response_format: "url"
196
+ });
197
+ const data = response.data || [];
198
+ for (let i = 0; i < data.length; i++) {
199
+ const imageUrl = data[i].url;
200
+ if (imageUrl) {
201
+ const fileName = `variation-${Date.now()}-${i + 1}.png`;
202
+ const filePath = path.join(resolvedOutputDir, fileName);
203
+ await downloadImage(imageUrl, filePath);
204
+ generatedFiles.push(filePath);
205
+ }
206
+ }
207
+ }
208
+ else if (mode === "edit") {
209
+ if (!image_path)
210
+ return "Error: 'image_path' is required for edit mode.";
211
+ if (!prompt)
212
+ return "Error: 'prompt' is required for edit mode.";
213
+ if (!fs.existsSync(image_path))
214
+ return `Error: Image file not found at ${image_path}`;
215
+ console.log(`Editing image with ${model}...`);
216
+ const params = {
217
+ image: fs.createReadStream(image_path),
218
+ prompt: prompt,
219
+ n: n,
220
+ model: "dall-e-2",
221
+ size: size,
222
+ response_format: "url"
223
+ };
224
+ if (mask_path && fs.existsSync(mask_path)) {
225
+ params.mask = fs.createReadStream(mask_path);
226
+ }
227
+ const response = await client.images.edit(params);
228
+ const data = response.data || [];
229
+ for (let i = 0; i < data.length; i++) {
230
+ const imageUrl = data[i].url;
231
+ if (imageUrl) {
232
+ const fileName = `edited-${Date.now()}-${i + 1}.png`;
233
+ const filePath = path.join(resolvedOutputDir, fileName);
234
+ await downloadImage(imageUrl, filePath);
235
+ generatedFiles.push(filePath);
236
+ }
237
+ }
238
+ }
239
+ else {
240
+ return `Error: Unknown mode '${mode}'.`;
241
+ }
242
+ return `Successfully generated ${generatedFiles.length} image(s):\n${generatedFiles.join('\n')}`;
243
+ }
244
+ catch (error) {
245
+ console.error(chalk.red(`Image Generation Failed: ${error.message}`));
246
+ if (error.response && error.response.data) {
247
+ console.error(chalk.dim(JSON.stringify(error.response.data)));
248
+ }
249
+ return `Error generating image: ${error.message}`;
250
+ }
251
+ };
252
+ export const ImageTool = {
253
+ name: "Image Generation",
254
+ risk: "edit",
255
+ definition: toolDefinition,
256
+ handler: handler
257
+ };
@@ -0,0 +1,2 @@
1
+ import { ToolModule } from './interface.js';
2
+ export declare const builtInTools: ToolModule[];
@@ -0,0 +1,88 @@
1
+ import { ShellTool, ReadFileTool, WriteFileTool, DateTimeTool } from './core.js';
2
+ import { TodoTool } from './todos.js';
3
+ import { EmailTool } from './email.js';
4
+ import { SearchTool } from './search.js';
5
+ import { NotifyTool } from './notify.js';
6
+ import { BrowserTool } from './browser.js';
7
+ import { ScreenshotTool } from './screenshot.js';
8
+ import { ImageTool } from './image.js';
9
+ import { PromptOptimizerTool } from './prompt-optimizer.js';
10
+ import { getSkillRegistry } from '../skills/index.js';
11
+ import { limitSkillBody } from '../skills/types.js';
12
+ const UseSkillTool = {
13
+ name: "Skill Invocation",
14
+ risk: "safe",
15
+ definition: {
16
+ type: "function",
17
+ function: {
18
+ name: "use_skill",
19
+ description: "Load and activate a specific skill to gain specialized knowledge and procedures. Use when the user's request matches a skill's description.",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ skill_name: {
24
+ type: "string",
25
+ description: "Name of the skill to activate (e.g. 'docker-ops', 'k8s-deploy')"
26
+ },
27
+ args: {
28
+ type: "object",
29
+ description: "Optional arguments to pass to the skill (e.g. {environment: 'staging', service: 'myapp'})",
30
+ properties: {}
31
+ }
32
+ },
33
+ required: ["skill_name"]
34
+ }
35
+ }
36
+ },
37
+ handler: async (args) => {
38
+ const registry = getSkillRegistry();
39
+ if (!registry)
40
+ return "Error: Skill system not initialized.";
41
+ const { skill_name, args: skillArgs } = args;
42
+ const skill = registry.get(skill_name);
43
+ if (!skill) {
44
+ return `Error: Skill '${skill_name}' not found. Available skills: ${registry.getAll().map(s => s.name).join(', ')}`;
45
+ }
46
+ const body = await registry.getBody(skill_name);
47
+ if (!body)
48
+ return `Error: Skill '${skill_name}' has no content.`;
49
+ // If skillArgs provided, substitute positional variables
50
+ let resolvedBody = body;
51
+ if (skillArgs && typeof skillArgs === 'object') {
52
+ const argsValues = Object.values(skillArgs);
53
+ if (argsValues.length > 0) {
54
+ const { substituteArgs } = await import('../skills/args.js');
55
+ resolvedBody = substituteArgs(body, {
56
+ positional: argsValues.map(String),
57
+ raw: argsValues.join(' '),
58
+ });
59
+ }
60
+ }
61
+ // Enforce body size limits
62
+ const { body: limitedBody, truncated, originalTokenEstimate, finalTokenEstimate } = limitSkillBody(resolvedBody);
63
+ let result = `# ${skill.name} Skill Activated\n\n${limitedBody}`;
64
+ if (truncated) {
65
+ result += `\n\n> Note: Skill body was truncated (${originalTokenEstimate} -> ${finalTokenEstimate} estimated tokens). The skill may not function as intended.`;
66
+ }
67
+ if (skillArgs && Object.keys(skillArgs).length > 0) {
68
+ result += `\n\n## Skill Arguments\n${JSON.stringify(skillArgs, null, 2)}`;
69
+ }
70
+ return result;
71
+ }
72
+ };
73
+ // Central Registry of all available tools
74
+ export const builtInTools = [
75
+ ShellTool,
76
+ ReadFileTool,
77
+ WriteFileTool,
78
+ DateTimeTool,
79
+ TodoTool,
80
+ PromptOptimizerTool,
81
+ EmailTool,
82
+ SearchTool,
83
+ NotifyTool,
84
+ BrowserTool,
85
+ ScreenshotTool,
86
+ ImageTool,
87
+ UseSkillTool
88
+ ];
@@ -0,0 +1,22 @@
1
+ import type { ToolRiskCategory, ToolContext, ToolResult } from "../core/types.js";
2
+ /** Optional execution context pieces a caller (the agent loop) can pass in. */
3
+ export type ToolExecExtra = Pick<ToolContext, "onUpdate" | "signal">;
4
+ export interface ToolDefinition {
5
+ type: "function";
6
+ function: {
7
+ name: "execute_shell_command" | "read_file" | "write_file" | "send_email" | string;
8
+ description: string;
9
+ parameters: {
10
+ type: "object";
11
+ properties: Record<string, unknown>;
12
+ required: string[];
13
+ };
14
+ };
15
+ }
16
+ export interface ToolModule {
17
+ name: string;
18
+ configKeys?: string[];
19
+ risk?: ToolRiskCategory;
20
+ definition: ToolDefinition;
21
+ handler: (args: any, config?: any, extra?: ToolExecExtra) => Promise<string | ToolResult>;
22
+ }