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.
- package/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- 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,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,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,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
|
+
}
|