skimpyclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +230 -0
  2. package/dist/__tests__/agent.test.d.ts +1 -0
  3. package/dist/__tests__/agent.test.js +131 -0
  4. package/dist/__tests__/api.test.d.ts +1 -0
  5. package/dist/__tests__/api.test.js +1227 -0
  6. package/dist/__tests__/audit.test.d.ts +1 -0
  7. package/dist/__tests__/audit.test.js +122 -0
  8. package/dist/__tests__/cache.test.d.ts +1 -0
  9. package/dist/__tests__/cache.test.js +65 -0
  10. package/dist/__tests__/channels.test.d.ts +1 -0
  11. package/dist/__tests__/channels.test.js +85 -0
  12. package/dist/__tests__/cli.integration.test.d.ts +1 -0
  13. package/dist/__tests__/cli.integration.test.js +16 -0
  14. package/dist/__tests__/cli.test.d.ts +1 -0
  15. package/dist/__tests__/cli.test.js +230 -0
  16. package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
  17. package/dist/__tests__/code-agents-executor.test.js +75 -0
  18. package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
  19. package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
  20. package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
  21. package/dist/__tests__/code-agents-parser.test.js +39 -0
  22. package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
  23. package/dist/__tests__/code-agents-utils.test.js +41 -0
  24. package/dist/__tests__/config.test.d.ts +1 -0
  25. package/dist/__tests__/config.test.js +46 -0
  26. package/dist/__tests__/cron.test.d.ts +1 -0
  27. package/dist/__tests__/cron.test.js +66 -0
  28. package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
  29. package/dist/__tests__/dashboard-mode.test.js +145 -0
  30. package/dist/__tests__/dashboard.test.d.ts +1 -0
  31. package/dist/__tests__/dashboard.test.js +43 -0
  32. package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
  33. package/dist/__tests__/doctor.formatters.test.js +65 -0
  34. package/dist/__tests__/doctor.index.test.d.ts +1 -0
  35. package/dist/__tests__/doctor.index.test.js +48 -0
  36. package/dist/__tests__/doctor.runner.test.d.ts +1 -0
  37. package/dist/__tests__/doctor.runner.test.js +204 -0
  38. package/dist/__tests__/exec-approval.test.d.ts +1 -0
  39. package/dist/__tests__/exec-approval.test.js +323 -0
  40. package/dist/__tests__/file-lock.test.d.ts +1 -0
  41. package/dist/__tests__/file-lock.test.js +92 -0
  42. package/dist/__tests__/langfuse.test.d.ts +1 -0
  43. package/dist/__tests__/langfuse.test.js +40 -0
  44. package/dist/__tests__/model-selection.test.d.ts +1 -0
  45. package/dist/__tests__/model-selection.test.js +62 -0
  46. package/dist/__tests__/orchestrator.test.d.ts +1 -0
  47. package/dist/__tests__/orchestrator.test.js +425 -0
  48. package/dist/__tests__/providers-init.test.d.ts +1 -0
  49. package/dist/__tests__/providers-init.test.js +32 -0
  50. package/dist/__tests__/providers-routing.test.d.ts +1 -0
  51. package/dist/__tests__/providers-routing.test.js +25 -0
  52. package/dist/__tests__/providers-utils.test.d.ts +1 -0
  53. package/dist/__tests__/providers-utils.test.js +54 -0
  54. package/dist/__tests__/security.test.d.ts +1 -0
  55. package/dist/__tests__/security.test.js +22 -0
  56. package/dist/__tests__/sessions.test.d.ts +1 -0
  57. package/dist/__tests__/sessions.test.js +147 -0
  58. package/dist/__tests__/setup.test.d.ts +1 -0
  59. package/dist/__tests__/setup.test.js +114 -0
  60. package/dist/__tests__/skills.test.d.ts +1 -0
  61. package/dist/__tests__/skills.test.js +333 -0
  62. package/dist/__tests__/subagent.test.d.ts +1 -0
  63. package/dist/__tests__/subagent.test.js +240 -0
  64. package/dist/__tests__/telegram-utils.test.d.ts +1 -0
  65. package/dist/__tests__/telegram-utils.test.js +22 -0
  66. package/dist/__tests__/telegram.test.d.ts +1 -0
  67. package/dist/__tests__/telegram.test.js +42 -0
  68. package/dist/__tests__/token-efficiency.test.d.ts +1 -0
  69. package/dist/__tests__/token-efficiency.test.js +38 -0
  70. package/dist/__tests__/tool-guard.test.d.ts +1 -0
  71. package/dist/__tests__/tool-guard.test.js +105 -0
  72. package/dist/__tests__/tools.test.d.ts +1 -0
  73. package/dist/__tests__/tools.test.js +589 -0
  74. package/dist/__tests__/usage.test.d.ts +1 -0
  75. package/dist/__tests__/usage.test.js +197 -0
  76. package/dist/__tests__/voice.test.d.ts +1 -0
  77. package/dist/__tests__/voice.test.js +214 -0
  78. package/dist/agent.d.ts +24 -0
  79. package/dist/agent.js +269 -0
  80. package/dist/api.d.ts +3 -0
  81. package/dist/api.js +943 -0
  82. package/dist/audit.d.ts +26 -0
  83. package/dist/audit.js +121 -0
  84. package/dist/cache.d.ts +8 -0
  85. package/dist/cache.js +24 -0
  86. package/dist/channels/telegram/handlers.d.ts +41 -0
  87. package/dist/channels/telegram/handlers.js +498 -0
  88. package/dist/channels/telegram/index.d.ts +14 -0
  89. package/dist/channels/telegram/index.js +326 -0
  90. package/dist/channels/telegram/types.d.ts +26 -0
  91. package/dist/channels/telegram/types.js +31 -0
  92. package/dist/channels/telegram/utils.d.ts +25 -0
  93. package/dist/channels/telegram/utils.js +256 -0
  94. package/dist/channels.d.ts +11 -0
  95. package/dist/channels.js +118 -0
  96. package/dist/cli.d.ts +5 -0
  97. package/dist/cli.js +768 -0
  98. package/dist/code-agents/executor.d.ts +5 -0
  99. package/dist/code-agents/executor.js +463 -0
  100. package/dist/code-agents/index.d.ts +22 -0
  101. package/dist/code-agents/index.js +199 -0
  102. package/dist/code-agents/orchestrator.d.ts +23 -0
  103. package/dist/code-agents/orchestrator.js +403 -0
  104. package/dist/code-agents/parser.d.ts +21 -0
  105. package/dist/code-agents/parser.js +197 -0
  106. package/dist/code-agents/registry.d.ts +27 -0
  107. package/dist/code-agents/registry.js +147 -0
  108. package/dist/code-agents/types.d.ts +66 -0
  109. package/dist/code-agents/types.js +4 -0
  110. package/dist/code-agents/utils.d.ts +36 -0
  111. package/dist/code-agents/utils.js +236 -0
  112. package/dist/config.d.ts +19 -0
  113. package/dist/config.js +123 -0
  114. package/dist/cron.d.ts +49 -0
  115. package/dist/cron.js +400 -0
  116. package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
  117. package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
  118. package/dist/dashboard/favicon.svg +3 -0
  119. package/dist/dashboard/index.html +21 -0
  120. package/dist/dashboard-frontend.d.ts +7 -0
  121. package/dist/dashboard-frontend.js +86 -0
  122. package/dist/dashboard.d.ts +8 -0
  123. package/dist/dashboard.js +4071 -0
  124. package/dist/digests.d.ts +36 -0
  125. package/dist/digests.js +338 -0
  126. package/dist/discord.d.ts +8 -0
  127. package/dist/discord.js +828 -0
  128. package/dist/doctor/checks.d.ts +18 -0
  129. package/dist/doctor/checks.js +368 -0
  130. package/dist/doctor/formatters.d.ts +3 -0
  131. package/dist/doctor/formatters.js +44 -0
  132. package/dist/doctor/index.d.ts +8 -0
  133. package/dist/doctor/index.js +7 -0
  134. package/dist/doctor/runner.d.ts +3 -0
  135. package/dist/doctor/runner.js +109 -0
  136. package/dist/doctor/types.d.ts +20 -0
  137. package/dist/doctor/types.js +1 -0
  138. package/dist/exec-approval.d.ts +101 -0
  139. package/dist/exec-approval.js +432 -0
  140. package/dist/file-lock.d.ts +34 -0
  141. package/dist/file-lock.js +81 -0
  142. package/dist/gateway.d.ts +8 -0
  143. package/dist/gateway.js +114 -0
  144. package/dist/heartbeat.d.ts +4 -0
  145. package/dist/heartbeat.js +101 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.js +75 -0
  148. package/dist/langfuse.d.ts +34 -0
  149. package/dist/langfuse.js +145 -0
  150. package/dist/mcp-context-a8c.d.ts +13 -0
  151. package/dist/mcp-context-a8c.js +34 -0
  152. package/dist/model-selection.d.ts +18 -0
  153. package/dist/model-selection.js +50 -0
  154. package/dist/orchestrator.d.ts +15 -0
  155. package/dist/orchestrator.js +676 -0
  156. package/dist/providers/anthropic.d.ts +7 -0
  157. package/dist/providers/anthropic.js +319 -0
  158. package/dist/providers/codex.d.ts +17 -0
  159. package/dist/providers/codex.js +508 -0
  160. package/dist/providers/content.d.ts +21 -0
  161. package/dist/providers/content.js +55 -0
  162. package/dist/providers/index.d.ts +13 -0
  163. package/dist/providers/index.js +138 -0
  164. package/dist/providers/observability.d.ts +19 -0
  165. package/dist/providers/observability.js +94 -0
  166. package/dist/providers/openai.d.ts +10 -0
  167. package/dist/providers/openai.js +310 -0
  168. package/dist/providers/tool-guard.d.ts +30 -0
  169. package/dist/providers/tool-guard.js +89 -0
  170. package/dist/providers/types.d.ts +34 -0
  171. package/dist/providers/types.js +2 -0
  172. package/dist/providers/utils.d.ts +65 -0
  173. package/dist/providers/utils.js +199 -0
  174. package/dist/security.d.ts +8 -0
  175. package/dist/security.js +113 -0
  176. package/dist/service.d.ts +8 -0
  177. package/dist/service.js +38 -0
  178. package/dist/sessions.d.ts +35 -0
  179. package/dist/sessions.js +142 -0
  180. package/dist/setup.d.ts +36 -0
  181. package/dist/setup.js +821 -0
  182. package/dist/skills-types.d.ts +65 -0
  183. package/dist/skills-types.js +2 -0
  184. package/dist/skills.d.ts +32 -0
  185. package/dist/skills.js +260 -0
  186. package/dist/subagent.d.ts +19 -0
  187. package/dist/subagent.js +376 -0
  188. package/dist/telegram.d.ts +2 -0
  189. package/dist/telegram.js +11 -0
  190. package/dist/tools/bash-tool.d.ts +3 -0
  191. package/dist/tools/bash-tool.js +59 -0
  192. package/dist/tools/browser-tool.d.ts +3 -0
  193. package/dist/tools/browser-tool.js +265 -0
  194. package/dist/tools/definitions.d.ts +432 -0
  195. package/dist/tools/definitions.js +181 -0
  196. package/dist/tools/execute-context.d.ts +26 -0
  197. package/dist/tools/execute-context.js +1 -0
  198. package/dist/tools/file-tools.d.ts +8 -0
  199. package/dist/tools/file-tools.js +67 -0
  200. package/dist/tools/path-utils.d.ts +1 -0
  201. package/dist/tools/path-utils.js +8 -0
  202. package/dist/tools.d.ts +24 -0
  203. package/dist/tools.js +281 -0
  204. package/dist/types.d.ts +259 -0
  205. package/dist/types.js +2 -0
  206. package/dist/usage.d.ts +76 -0
  207. package/dist/usage.js +150 -0
  208. package/dist/voice.d.ts +37 -0
  209. package/dist/voice.js +461 -0
  210. package/package.json +70 -0
  211. package/templates/AGENTS.md +38 -0
  212. package/templates/BOOT.md +23 -0
  213. package/templates/BOOTSTRAP.md +26 -0
  214. package/templates/HEARTBEAT.md +5 -0
  215. package/templates/IDENTITY.md +5 -0
  216. package/templates/MEMORY.md +24 -0
  217. package/templates/SOUL.md +92 -0
  218. package/templates/TOOLS.md +30 -0
  219. package/templates/USER.md +31 -0
@@ -0,0 +1,181 @@
1
+ // Claude Code canonical tool names (stealth mode for OAuth compatibility)
2
+ // Maps our internal names to Claude Code's exact casing
3
+ const TOOL_NAME_MAP = {
4
+ 'Read': 'read_file',
5
+ 'Write': 'write_file',
6
+ 'Glob': 'list_directory',
7
+ 'Bash': 'bash',
8
+ 'Browser': 'browser',
9
+ };
10
+ // Reverse map: internal name -> Claude Code name
11
+ const INTERNAL_TO_CC = Object.fromEntries(Object.entries(TOOL_NAME_MAP).map(([cc, internal]) => [internal, cc]));
12
+ /** Convert Claude Code tool name back to our internal name */
13
+ export function fromClaudeCodeName(name) {
14
+ return TOOL_NAME_MAP[name] || name;
15
+ }
16
+ /** Convert our internal name to Claude Code tool name */
17
+ export function toClaudeCodeName(name) {
18
+ return INTERNAL_TO_CC[name] || name;
19
+ }
20
+ // Built-in tool definitions — always available
21
+ export const BUILTIN_TOOL_DEFINITIONS = [
22
+ {
23
+ name: 'Read',
24
+ description: 'Read the contents of a file at the given absolute path.',
25
+ input_schema: {
26
+ type: 'object',
27
+ properties: {
28
+ file_path: { type: 'string', description: 'Absolute path to the file to read' },
29
+ },
30
+ required: ['file_path'],
31
+ },
32
+ },
33
+ {
34
+ name: 'Write',
35
+ description: 'Write content to a file. Creates parent directories if needed. Overwrites existing files.',
36
+ input_schema: {
37
+ type: 'object',
38
+ properties: {
39
+ file_path: { type: 'string', description: 'Absolute path to the file to write' },
40
+ content: { type: 'string', description: 'Content to write to the file' },
41
+ },
42
+ required: ['file_path', 'content'],
43
+ },
44
+ },
45
+ {
46
+ name: 'Glob',
47
+ description: 'List files and directories at the given path. Returns name, type (file/dir), and size.',
48
+ input_schema: {
49
+ type: 'object',
50
+ properties: {
51
+ path: { type: 'string', description: 'Absolute path to the directory to list' },
52
+ },
53
+ required: ['path'],
54
+ },
55
+ },
56
+ {
57
+ name: 'Bash',
58
+ description: 'Execute a shell command and return stdout/stderr. Use for CLI tools like gh, icalBuddy, date, etc.',
59
+ input_schema: {
60
+ type: 'object',
61
+ properties: {
62
+ command: { type: 'string', description: 'Shell command to execute' },
63
+ cwd: { type: 'string', description: 'Working directory (optional)' },
64
+ },
65
+ required: ['command'],
66
+ },
67
+ },
68
+ ];
69
+ export const BROWSER_TOOL_DEFINITION = {
70
+ name: 'Browser',
71
+ description: `Control a browser via Playwright. Actions (case-insensitive):
72
+ - open: Navigate to URL. Required: url
73
+ - click: Click element. Required: selector
74
+ - type: Fill input. Required: selector, text
75
+ - select: Pick dropdown option. Required: selector, text (value)
76
+ - hover: Hover element. Required: selector
77
+ - scroll: Scroll page. Optional: selector (scrollIntoView), direction (up/down), amount (pixels)
78
+ - waitFor: Wait for element/text. Required: selector OR text
79
+ - evaluate: Run JavaScript in page. Required: script. Returns JSON result.
80
+ - getText: Get visible text. Optional: selector (defaults to full page body text)
81
+ - screenshot: Capture page. Optional: file_path
82
+ - wait: Delay. Required: timeMs
83
+ - close: Close browser`,
84
+ input_schema: {
85
+ type: 'object',
86
+ properties: {
87
+ action: { type: 'string', description: 'Action to perform (see description)' },
88
+ type: { type: 'string', description: 'Browser type: chromium | firefox | webkit (optional, config default)' },
89
+ url: { type: 'string', description: 'URL to open (open action)' },
90
+ selector: { type: 'string', description: 'CSS selector (click/type/waitFor/getText/scroll/select/hover)' },
91
+ text: { type: 'string', description: 'Text to type, wait for, or select value (type/waitFor/select)' },
92
+ script: { type: 'string', description: 'JavaScript code to evaluate in page (evaluate action)' },
93
+ direction: { type: 'string', description: 'Scroll direction: up or down (scroll action, default: down)' },
94
+ amount: { type: 'number', description: 'Pixels to scroll (scroll action, default: one viewport height)' },
95
+ file_path: { type: 'string', description: 'Absolute path to save screenshot (optional)' },
96
+ timeoutMs: { type: 'number', description: 'Timeout in ms (optional)' },
97
+ timeMs: { type: 'number', description: 'Time to wait in ms (wait action)' },
98
+ headless: { type: 'boolean', description: 'Override headless for open (optional)' },
99
+ slowMoMs: { type: 'number', description: 'Slow motion delay per action (ms) (optional)' },
100
+ userAgent: { type: 'string', description: 'Override user agent (optional)' },
101
+ viewport: {
102
+ type: 'object',
103
+ properties: {
104
+ width: { type: 'number' },
105
+ height: { type: 'number' },
106
+ },
107
+ },
108
+ // executablePath and profileDir are config-only for security (no model overrides)
109
+ },
110
+ required: ['action'],
111
+ },
112
+ };
113
+ // Legacy export for backward compat — static list (built-ins + browser + no MCP)
114
+ export const TOOL_DEFINITIONS = [...BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION];
115
+ export const SPAWN_SUBAGENT_TOOL = {
116
+ name: 'spawn_subagent',
117
+ description: 'Spawn a background subagent to handle a task independently. Returns immediately with a run ID. Results are announced back to this chat when done. Use for tasks that benefit from parallel work or long-running operations.',
118
+ input_schema: {
119
+ type: 'object',
120
+ properties: {
121
+ task: { type: 'string', description: 'What the subagent should do — be specific and self-contained' },
122
+ type: {
123
+ type: 'string',
124
+ enum: ['coding', 'research'],
125
+ description: 'Agent type: coding (code/files/bash), research (investigation/reading)',
126
+ },
127
+ model: { type: 'string', description: 'Optional model override (e.g. claude-opus, claude-think)' },
128
+ label: { type: 'string', description: 'Short label for status display (e.g. "write tests", "check logs")' },
129
+ allowedPaths: {
130
+ type: 'array',
131
+ items: { type: 'string' },
132
+ description: 'Additional file paths the subagent can access beyond defaults',
133
+ },
134
+ },
135
+ required: ['task', 'type'],
136
+ },
137
+ };
138
+ export const CODE_WITH_AGENT_TOOL = {
139
+ name: 'code_with_agent',
140
+ description: 'Delegate a coding task to a coding agent CLI (Claude Code or Codex). The agent will edit files, run commands, and return results. Always use this for code changes instead of writing code directly.',
141
+ input_schema: {
142
+ type: 'object',
143
+ properties: {
144
+ task: { type: 'string', description: 'Detailed coding task. Be specific: what to change, why, which files, expected behavior.' },
145
+ agent: { type: 'string', enum: ['claude', 'codex', 'kimi'], description: 'Which coding CLI to use. Omit to use configured default.' },
146
+ workdir: { type: 'string', description: 'Working directory (default: SkimpyClaw repo root)' },
147
+ model: { type: 'string', description: 'Model override (e.g. opus, gpt-5.3-codex)' },
148
+ max_turns: { type: 'number', description: 'Max agentic turns, Claude only (default: 30)' },
149
+ timeout_minutes: { type: 'number', description: 'Timeout in minutes (default: 10, max: 30)' },
150
+ validate: { type: 'boolean', description: 'Run pnpm build && pnpm test after (default: true)' },
151
+ },
152
+ required: ['task'],
153
+ },
154
+ };
155
+ export const CODE_WITH_TEAM_TOOL = {
156
+ name: 'code_with_team',
157
+ description: 'Decompose a complex task into subtasks and run multiple code_with_agent instances in parallel. SkimpyClaw manages coordination: decomposes the task, spawns N parallel agents, monitors progress, synthesizes results, and validates. Use for multi-file refactors, cross-layer changes, or tasks with independent subtasks.',
158
+ input_schema: {
159
+ type: 'object',
160
+ properties: {
161
+ task: { type: 'string', description: 'Detailed task description. Be specific: what to change, why, which files, expected behavior.' },
162
+ team_size: { type: 'number', description: 'Number of parallel agents (2-5, default 3)' },
163
+ workdir: { type: 'string', description: 'Working directory or project name (default: SkimpyClaw repo root)' },
164
+ agent: { type: 'string', enum: ['claude', 'codex', 'kimi'], description: 'Which coding CLI to use for all team workers. Omit to use configured default.' },
165
+ model: { type: 'string', description: 'Model override (e.g. claude-sonnet-4-5, gpt-5.3-codex)' },
166
+ timeout_minutes: { type: 'number', description: 'Total timeout in minutes (default: 20, max: 60)' },
167
+ validate: { type: 'boolean', description: 'Run pnpm build && pnpm test after all agents complete (default: true)' },
168
+ },
169
+ required: ['task'],
170
+ },
171
+ };
172
+ export const CHECK_CODE_AGENT_TOOL = {
173
+ name: 'check_code_agent',
174
+ description: 'Check status of running coding agents. Call with no args to list all, or with id to get details.',
175
+ input_schema: {
176
+ type: 'object',
177
+ properties: {
178
+ id: { type: 'string', description: 'Agent ID (e.g. ca-1). Omit to list all active agents.' },
179
+ },
180
+ },
181
+ };
@@ -0,0 +1,26 @@
1
+ export interface ExecuteToolContext {
2
+ /** Task ID for file lock acquisition (subagent writes) */
3
+ lockTaskId?: string;
4
+ /** Abort signal for cancelling long-running tool loops */
5
+ abortSignal?: AbortSignal;
6
+ /** Chat ID for spawn_subagent dispatch */
7
+ chatId?: number;
8
+ /** Full config for spawn_subagent */
9
+ fullConfig?: import('../types.js').Config;
10
+ /** Conversation history for spawn_subagent */
11
+ history?: import('../types.js').ChatMessage[];
12
+ /** Audit trace ID for recording tool events */
13
+ auditTraceId?: string;
14
+ /** Originating channel for approval routing */
15
+ channel?: 'telegram' | 'discord' | string;
16
+ /** Channel-specific target ID (Telegram chat ID or Discord channel snowflake) */
17
+ channelTargetId?: string | number;
18
+ /** User ID of the person who can approve */
19
+ approverUserId?: string;
20
+ /** Username of the approver */
21
+ approverUsername?: string;
22
+ /** Trigger source for usage tracking */
23
+ trigger?: string;
24
+ /** Agent ID for usage tracking */
25
+ agentId?: string;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { ToolConfig } from '../types.js';
2
+ export declare function executeReadFile(path: string, config: ToolConfig): string;
3
+ /**
4
+ * Write with file locking when a lockTaskId is provided (subagent context).
5
+ * Falls back to unlocked write when no lockTaskId.
6
+ */
7
+ export declare function executeWriteFileLocked(path: string, content: string, config: ToolConfig, lockTaskId?: string): Promise<string>;
8
+ export declare function executeListDirectory(path: string, config: ToolConfig): string;
@@ -0,0 +1,67 @@
1
+ import { readFileSync, writeFileSync, readdirSync, existsSync, statSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { isPathAllowed } from './path-utils.js';
4
+ export function executeReadFile(path, config) {
5
+ if (!isPathAllowed(path, config.allowedPaths)) {
6
+ return `Error: Path not allowed. Permitted: ${config.allowedPaths.join(', ')}`;
7
+ }
8
+ if (!existsSync(path)) {
9
+ return `Error: File not found: ${path}`;
10
+ }
11
+ const content = readFileSync(path, 'utf-8');
12
+ if (content.length > 100_000) {
13
+ return content.slice(0, 100_000) + '\n\n[TRUNCATED - file exceeds 100KB]';
14
+ }
15
+ return content;
16
+ }
17
+ function executeWriteFile(path, content, config) {
18
+ if (!isPathAllowed(path, config.allowedPaths)) {
19
+ return `Error: Path not allowed. Permitted: ${config.allowedPaths.join(', ')}`;
20
+ }
21
+ const dir = dirname(path);
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ writeFileSync(path, content, 'utf-8');
26
+ return `Written: ${path} (${content.length} bytes)`;
27
+ }
28
+ /**
29
+ * Write with file locking when a lockTaskId is provided (subagent context).
30
+ * Falls back to unlocked write when no lockTaskId.
31
+ */
32
+ export async function executeWriteFileLocked(path, content, config, lockTaskId) {
33
+ if (!lockTaskId) {
34
+ return executeWriteFile(path, content, config);
35
+ }
36
+ const { acquireLock, releaseLock } = await import('../file-lock.js');
37
+ const acquired = await acquireLock(path, lockTaskId);
38
+ if (!acquired) {
39
+ return `Error: Could not acquire file lock on ${path} (timed out after 30s)`;
40
+ }
41
+ try {
42
+ return executeWriteFile(path, content, config);
43
+ }
44
+ finally {
45
+ releaseLock(path, lockTaskId);
46
+ }
47
+ }
48
+ export function executeListDirectory(path, config) {
49
+ if (!isPathAllowed(path, config.allowedPaths)) {
50
+ return `Error: Path not allowed. Permitted: ${config.allowedPaths.join(', ')}`;
51
+ }
52
+ if (!existsSync(path)) {
53
+ return `Error: Directory not found: ${path}`;
54
+ }
55
+ const entries = readdirSync(path, { withFileTypes: true });
56
+ if (entries.length === 0)
57
+ return '(empty directory)';
58
+ const lines = entries.map(e => {
59
+ const type = e.isDirectory() ? 'dir' : 'file';
60
+ if (e.isFile()) {
61
+ const stat = statSync(join(path, e.name));
62
+ return `${type}\t${e.name}\t${stat.size}`;
63
+ }
64
+ return `${type}\t${e.name}`;
65
+ });
66
+ return lines.join('\n');
67
+ }
@@ -0,0 +1 @@
1
+ export declare function isPathAllowed(filePath: string, allowedPaths: string[]): boolean;
@@ -0,0 +1,8 @@
1
+ import { resolve, sep } from 'path';
2
+ export function isPathAllowed(filePath, allowedPaths) {
3
+ const resolved = resolve(filePath);
4
+ return allowedPaths.some((allowed) => {
5
+ const allowedRoot = resolve(allowed);
6
+ return resolved === allowedRoot || resolved.startsWith(`${allowedRoot}${sep}`);
7
+ });
8
+ }
@@ -0,0 +1,24 @@
1
+ import type { ToolConfig } from './types.js';
2
+ import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, SPAWN_SUBAGENT_TOOL, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL } from './tools/definitions.js';
3
+ import type { ExecuteToolContext } from './tools/execute-context.js';
4
+ import { cleanupBrowser } from './tools/browser-tool.js';
5
+ export { getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks, setCodeAgentConfig, computeWaves, decomposeTask, synthesizeResults, runValidation, runCodeAgentBackground, buildCodeAgentArgs, readTeamState, parseStreamJsonForLive, } from './code-agents/index.js';
6
+ export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, SPAWN_SUBAGENT_TOOL, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
7
+ export type { ExecuteToolContext };
8
+ export type { CodeAgentTask, DecomposedSubtask } from './code-agents/index.js';
9
+ export declare function discoverMcpTools(): Promise<any[]>;
10
+ export declare function clearMcpToolCache(): void;
11
+ /**
12
+ * Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + spawn_subagent.
13
+ * This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
14
+ * Pass includeSpawnSubagent: true to include the spawn_subagent tool (e.g. for Telegram conversations).
15
+ * Results are cached for 60s to avoid rebuilding the array on every agent turn.
16
+ */
17
+ export declare function getToolDefinitions(config?: ToolConfig, options?: {
18
+ includeSpawnSubagent?: boolean;
19
+ includeMcp?: boolean;
20
+ projects?: Record<string, string>;
21
+ }): Promise<any[]>;
22
+ export declare function clearToolDefsCache(): void;
23
+ export declare function cleanupMcp(): Promise<void>;
24
+ export declare function executeTool(name: string, input: Record<string, any>, config: ToolConfig, context?: ExecuteToolContext): Promise<string>;
package/dist/tools.js ADDED
@@ -0,0 +1,281 @@
1
+ // Tool definitions and executors for Anthropic API tool_use
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { TTLCache } from './cache.js';
5
+ import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, SPAWN_SUBAGENT_TOOL, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, } from './tools/definitions.js';
6
+ import { executeReadFile, executeWriteFileLocked, executeListDirectory } from './tools/file-tools.js';
7
+ import { executeBash } from './tools/bash-tool.js';
8
+ import { executeBrowser, cleanupBrowser } from './tools/browser-tool.js';
9
+ // Re-export from code-agents module for backward compatibility
10
+ export {
11
+ // Registry functions
12
+ getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks,
13
+ // Config
14
+ setCodeAgentConfig,
15
+ // Orchestrator functions
16
+ computeWaves, decomposeTask, synthesizeResults,
17
+ // Executor functions
18
+ runValidation, runCodeAgentBackground,
19
+ // Utilities
20
+ buildCodeAgentArgs, readTeamState,
21
+ // Parsers
22
+ parseStreamJsonForLive, } from './code-agents/index.js';
23
+ // Re-export from definitions
24
+ export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, SPAWN_SUBAGENT_TOOL, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
25
+ // --- MCP (mcporter) ---
26
+ let mcpRuntime = null;
27
+ async function getMcpRuntime() {
28
+ if (!mcpRuntime) {
29
+ const { createRuntime } = await import('mcporter');
30
+ mcpRuntime = await createRuntime({
31
+ configPath: join(homedir(), '.mcporter', 'mcporter.json'),
32
+ });
33
+ }
34
+ return mcpRuntime;
35
+ }
36
+ // --- MCP Auto-Discovery ---
37
+ let discoveredMcpTools = null;
38
+ /** Sanitize a name to match OpenAI/Codex tool name pattern: [a-zA-Z0-9_-] */
39
+ function sanitizeToolName(name) {
40
+ return name.replace(/[^a-zA-Z0-9_-]/g, '_');
41
+ }
42
+ // Maps sanitized tool name → { server (original), tool (original) } for routing
43
+ const mcpToolNameMap = new Map();
44
+ export async function discoverMcpTools() {
45
+ if (discoveredMcpTools !== null)
46
+ return discoveredMcpTools;
47
+ const tools = [];
48
+ mcpToolNameMap.clear();
49
+ try {
50
+ const runtime = await getMcpRuntime();
51
+ const servers = runtime.listServers();
52
+ for (const server of servers) {
53
+ try {
54
+ const serverTools = await runtime.listTools(server, { includeSchema: true });
55
+ const sanitizedServer = sanitizeToolName(server);
56
+ for (const tool of serverTools) {
57
+ const sanitizedTool = sanitizeToolName(tool.name);
58
+ const name = `mcp__${sanitizedServer}__${sanitizedTool}`;
59
+ // Store mapping from sanitized name to original names for routing
60
+ mcpToolNameMap.set(name, { server, tool: tool.name });
61
+ tools.push({
62
+ name,
63
+ description: tool.description || `MCP tool ${tool.name} from ${server}`,
64
+ input_schema: tool.inputSchema || { type: 'object', properties: {} },
65
+ });
66
+ }
67
+ }
68
+ catch (err) {
69
+ console.warn(`[mcp] Failed to list tools for server "${server}":`, err instanceof Error ? err.message : err);
70
+ }
71
+ }
72
+ }
73
+ catch (err) {
74
+ console.warn('[mcp] Failed to create runtime for tool discovery:', err instanceof Error ? err.message : err);
75
+ }
76
+ discoveredMcpTools = tools;
77
+ return tools;
78
+ }
79
+ export function clearMcpToolCache() {
80
+ discoveredMcpTools = null;
81
+ }
82
+ const toolDefsCache = new TTLCache(60_000);
83
+ /**
84
+ * Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + spawn_subagent.
85
+ * This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
86
+ * Pass includeSpawnSubagent: true to include the spawn_subagent tool (e.g. for Telegram conversations).
87
+ * Results are cached for 60s to avoid rebuilding the array on every agent turn.
88
+ */
89
+ export async function getToolDefinitions(config, options) {
90
+ const includeMcp = options?.includeMcp !== false; // default true for backwards compat
91
+ const cacheKey = JSON.stringify({
92
+ browser: config?.browser?.enabled,
93
+ spawn: options?.includeSpawnSubagent,
94
+ mcp: includeMcp,
95
+ projects: options?.projects,
96
+ });
97
+ const cached = toolDefsCache.get(cacheKey);
98
+ if (cached)
99
+ return cached;
100
+ const tools = [...BUILTIN_TOOL_DEFINITIONS];
101
+ // Include browser tool only when explicitly enabled
102
+ if (config?.browser?.enabled) {
103
+ tools.push(BROWSER_TOOL_DEFINITION);
104
+ }
105
+ // Auto-discover MCP tools from mcporter config (only for Anthropic models)
106
+ if (includeMcp) {
107
+ const mcpTools = await discoverMcpTools();
108
+ tools.push(...mcpTools);
109
+ }
110
+ // Include spawn_subagent, code_with_agent, and check_code_agent tools when requested
111
+ if (options?.includeSpawnSubagent) {
112
+ tools.push(SPAWN_SUBAGENT_TOOL);
113
+ // Inject project names into code_with_agent description so the model knows what to use
114
+ const projects = options.projects;
115
+ if (projects && Object.keys(projects).length > 0) {
116
+ const projectList = Object.entries(projects)
117
+ .map(([name, path]) => `"${name}" → ${path}`)
118
+ .join(', ');
119
+ const codeAgentWithProjects = {
120
+ ...CODE_WITH_AGENT_TOOL,
121
+ input_schema: {
122
+ ...CODE_WITH_AGENT_TOOL.input_schema,
123
+ properties: {
124
+ ...CODE_WITH_AGENT_TOOL.input_schema.properties,
125
+ workdir: {
126
+ type: 'string',
127
+ description: `Working directory or project name. Named projects: ${projectList}. Default: SkimpyClaw repo root.`,
128
+ },
129
+ },
130
+ },
131
+ };
132
+ tools.push(codeAgentWithProjects);
133
+ }
134
+ else {
135
+ tools.push(CODE_WITH_AGENT_TOOL);
136
+ }
137
+ // Inject project names into code_with_team description too
138
+ if (projects && Object.keys(projects).length > 0) {
139
+ const projectList = Object.entries(projects)
140
+ .map(([name, path]) => `"${name}" → ${path}`)
141
+ .join(', ');
142
+ const codeTeamWithProjects = {
143
+ ...CODE_WITH_TEAM_TOOL,
144
+ input_schema: {
145
+ ...CODE_WITH_TEAM_TOOL.input_schema,
146
+ properties: {
147
+ ...CODE_WITH_TEAM_TOOL.input_schema.properties,
148
+ workdir: {
149
+ type: 'string',
150
+ description: `Working directory or project name. Named projects: ${projectList}. Default: SkimpyClaw repo root.`,
151
+ },
152
+ },
153
+ },
154
+ };
155
+ tools.push(codeTeamWithProjects);
156
+ }
157
+ else {
158
+ tools.push(CODE_WITH_TEAM_TOOL);
159
+ }
160
+ tools.push(CHECK_CODE_AGENT_TOOL);
161
+ }
162
+ toolDefsCache.set(cacheKey, tools);
163
+ return tools;
164
+ }
165
+ export function clearToolDefsCache() {
166
+ toolDefsCache.clear();
167
+ }
168
+ // --- MCP Tool Execution (generic) ---
169
+ async function executeMcpToolGeneric(fullName, args) {
170
+ // Look up original server/tool names from the sanitized name map
171
+ const mapping = mcpToolNameMap.get(fullName);
172
+ if (!mapping) {
173
+ // Fallback: parse from the name directly (works when names don't need sanitizing)
174
+ const parts = fullName.split('__');
175
+ if (parts.length < 3)
176
+ return `Error: Invalid MCP tool name "${fullName}"`;
177
+ const server = parts[1];
178
+ const toolName = parts.slice(2).join('__');
179
+ const runtime = await getMcpRuntime();
180
+ const result = await runtime.callTool(server, toolName, { args });
181
+ const content = result?.content;
182
+ if (Array.isArray(content)) {
183
+ return content.map((c) => c.text || JSON.stringify(c)).join('\n');
184
+ }
185
+ return JSON.stringify(result);
186
+ }
187
+ const runtime = await getMcpRuntime();
188
+ const result = await runtime.callTool(mapping.server, mapping.tool, { args });
189
+ const content = result?.content;
190
+ if (Array.isArray(content)) {
191
+ return content.map((c) => c.text || JSON.stringify(c)).join('\n');
192
+ }
193
+ return JSON.stringify(result);
194
+ }
195
+ export async function cleanupMcp() {
196
+ if (mcpRuntime) {
197
+ await mcpRuntime.close().catch(() => { });
198
+ mcpRuntime = null;
199
+ }
200
+ }
201
+ // --- Tool Executor ---
202
+ export async function executeTool(name, input, config, context) {
203
+ try {
204
+ // Route MCP tools BEFORE normalization to preserve server/tool name casing
205
+ if (name.startsWith('mcp__')) {
206
+ return await executeMcpToolGeneric(name, input);
207
+ }
208
+ // Route spawn_subagent
209
+ if (name === 'spawn_subagent') {
210
+ return await executeSpawnSubagent(input, context);
211
+ }
212
+ // Route code_with_agent - delegate to code-agents module
213
+ if (name === 'code_with_agent') {
214
+ const { executeCodeWithAgent } = await import('./code-agents/index.js');
215
+ return await executeCodeWithAgent(input, config, context);
216
+ }
217
+ // Route code_with_team - delegate to code-agents module
218
+ if (name === 'code_with_team') {
219
+ const { executeCodeWithTeam } = await import('./code-agents/index.js');
220
+ return await executeCodeWithTeam(input, config, context);
221
+ }
222
+ // Route check_code_agent - delegate to code-agents module
223
+ if (name === 'check_code_agent') {
224
+ const { executeCheckCodeAgent } = await import('./code-agents/index.js');
225
+ return executeCheckCodeAgent(input);
226
+ }
227
+ // Map Claude Code names to internal names for built-in tools
228
+ const normalized = fromClaudeCodeName(name).toLowerCase().replace(/-/g, '_');
229
+ switch (normalized) {
230
+ case 'read_file':
231
+ return executeReadFile(input.file_path || input.path, config);
232
+ case 'write_file':
233
+ return await executeWriteFileLocked(input.file_path || input.path, input.content, config, context?.lockTaskId);
234
+ case 'list_directory':
235
+ return executeListDirectory(input.path, config);
236
+ case 'bash':
237
+ return await executeBash(input.command, input.cwd, config, context);
238
+ case 'browser':
239
+ return await executeBrowser(input, config);
240
+ default:
241
+ return `Error: Unknown tool "${name}"`;
242
+ }
243
+ }
244
+ catch (err) {
245
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
246
+ }
247
+ }
248
+ // --- Individual Tool Implementations ---
249
+ /**
250
+ * Execute spawn_subagent tool — dispatches a background subagent.
251
+ */
252
+ async function executeSpawnSubagent(input, context) {
253
+ if (!context?.fullConfig || !context?.chatId) {
254
+ return 'Error: spawn_subagent requires a chat context (not available in this mode)';
255
+ }
256
+ const { dispatchSubagent } = await import('./subagent.js');
257
+ const task = input.task;
258
+ const type = input.type;
259
+ const model = input.model;
260
+ const label = input.label;
261
+ const allowedPaths = input.allowedPaths;
262
+ if (!task || !type) {
263
+ return 'Error: task and type are required';
264
+ }
265
+ if (!['coding', 'research'].includes(type)) {
266
+ return `Error: Invalid type "${type}". Must be coding or research.`;
267
+ }
268
+ try {
269
+ const subagentTask = dispatchSubagent(type, task, context.chatId, context.fullConfig, model, context.history, { label, allowedPaths });
270
+ const labelStr = label ? ` "${label}"` : '';
271
+ return JSON.stringify({
272
+ status: 'accepted',
273
+ runId: subagentTask.id,
274
+ label: label || subagentTask.type,
275
+ message: `Subagent ${subagentTask.id}${labelStr} dispatched (${subagentTask.type}, model: ${subagentTask.model}). Results will be announced when done.`,
276
+ });
277
+ }
278
+ catch (err) {
279
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
280
+ }
281
+ }