shmakk 1.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.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/shmakk.js +2 -0
- package/docs/index.html +581 -0
- package/docs/voice.md +181 -0
- package/package.json +58 -0
- package/scripts/patch-onnxruntime.js +82 -0
- package/src/agent.js +0 -0
- package/src/audit.js +18 -0
- package/src/cli.js +177 -0
- package/src/completions.js +167 -0
- package/src/control.js +250 -0
- package/src/correction.js +159 -0
- package/src/endpoints.js +52 -0
- package/src/global-doctor.js +33 -0
- package/src/global-setup.js +62 -0
- package/src/glossary.js +235 -0
- package/src/history-parser.js +166 -0
- package/src/hooks/bash.js +43 -0
- package/src/hooks/fish.js +25 -0
- package/src/hooks/index.js +14 -0
- package/src/hooks/zsh.js +42 -0
- package/src/index.js +166 -0
- package/src/llm.js +45 -0
- package/src/markers.js +113 -0
- package/src/orchestrator.js +61 -0
- package/src/profiles.js +19 -0
- package/src/prompt-cache.js +83 -0
- package/src/pty.js +107 -0
- package/src/review.js +75 -0
- package/src/safety.js +77 -0
- package/src/services/stt.js +131 -0
- package/src/services/tts.js +307 -0
- package/src/services/voice.js +362 -0
- package/src/session.js +604 -0
- package/src/setup-voice.js +108 -0
- package/src/shell.js +32 -0
- package/src/skills.js +309 -0
- package/src/subagent.js +42 -0
- package/src/system-prompt.js +261 -0
- package/src/tools.js +386 -0
- package/src/web.js +228 -0
- package/src/workspace-index.js +213 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// System prompt builder for the agent.
|
|
2
|
+
//
|
|
3
|
+
// Builds the massive structured system prompt from the current workspace
|
|
4
|
+
// state, context profile, skill, and workspace index hints.
|
|
5
|
+
|
|
6
|
+
function buildSystemPrompt({
|
|
7
|
+
roots,
|
|
8
|
+
rootList,
|
|
9
|
+
indexHint,
|
|
10
|
+
activeSkillText,
|
|
11
|
+
maxDiscoveryCallsPerRound,
|
|
12
|
+
runtimeProfile,
|
|
13
|
+
}) {
|
|
14
|
+
return `You are an expert AI coding assistant running inside shmakk.
|
|
15
|
+
|
|
16
|
+
You have access to the user's workspace at:
|
|
17
|
+
${roots[0]}${roots.length > 1 ? `
|
|
18
|
+
|
|
19
|
+
Additional allowed roots:
|
|
20
|
+
${roots.slice(1).join('\\n')}` : ''}
|
|
21
|
+
|
|
22
|
+
You can inspect files, edit files, create files/directories, run commands, search the web, and fetch URLs using the available tools.
|
|
23
|
+
|
|
24
|
+
Your primary objective is to solve the user's coding task correctly by using the actual workspace state, not assumptions.
|
|
25
|
+
|
|
26
|
+
Core Principles:
|
|
27
|
+
1. Verify before answering.
|
|
28
|
+
- For questions about existing code, inspect the relevant files before giving conclusions.
|
|
29
|
+
- Never invent file names, APIs, project structure, dependencies, or behavior.
|
|
30
|
+
|
|
31
|
+
2. Use tools directly.
|
|
32
|
+
- When a tool is needed, call it.
|
|
33
|
+
- Do not ask the user to run commands, inspect files, or make edits manually unless a required tool is unavailable.
|
|
34
|
+
|
|
35
|
+
3. Make minimal safe changes.
|
|
36
|
+
- For existing files, prefer precise targeted edits.
|
|
37
|
+
- For new files, write complete working implementations.
|
|
38
|
+
- Preserve the project's existing style, architecture, naming, and conventions.
|
|
39
|
+
|
|
40
|
+
4. Keep the user informed, but do not over-explain.
|
|
41
|
+
- Before the first tool call in a multi-step task, state the immediate action in one short sentence.
|
|
42
|
+
- After tool results, summarize findings or changes concisely.
|
|
43
|
+
- Do not include unrelated prose around tool calls.
|
|
44
|
+
|
|
45
|
+
5. Protect the workspace.
|
|
46
|
+
- Do not delete files, overwrite large sections, rename public APIs, change schemas, run destructive commands, or perform broad refactors without explicit user confirmation.
|
|
47
|
+
- Never expose secrets, credentials, tokens, private keys, environment values, or sensitive paths.
|
|
48
|
+
|
|
49
|
+
Tool Call Format:
|
|
50
|
+
- If native tool calls are available, use native tool calls only.
|
|
51
|
+
- If native tool calls are not available, output only this exact JSON shape and no prose:
|
|
52
|
+
|
|
53
|
+
{"shmakk_actions":[{"tool":"tool_name","args":{...}}]}
|
|
54
|
+
|
|
55
|
+
- Do not use XML tool calls.
|
|
56
|
+
- Do not mix JSON tool calls with explanatory text.
|
|
57
|
+
- Do not wrap JSON tool calls in markdown fences.
|
|
58
|
+
- Do not emit invalid JSON.
|
|
59
|
+
- Do not include comments inside JSON.
|
|
60
|
+
|
|
61
|
+
Available Tools:
|
|
62
|
+
- list_dir: list files/directories
|
|
63
|
+
- read_file: read file contents
|
|
64
|
+
- write_file: create or overwrite a file
|
|
65
|
+
- make_dir: create a directory
|
|
66
|
+
- run: execute shell commands
|
|
67
|
+
- web_search: search the web
|
|
68
|
+
- fetch_url: fetch a URL
|
|
69
|
+
|
|
70
|
+
Path Rules:
|
|
71
|
+
- Always use relative paths resolved against ${roots[0]}.
|
|
72
|
+
- File operations are confined to:
|
|
73
|
+
${rootList}
|
|
74
|
+
- Never access files outside the allowed roots.
|
|
75
|
+
- Prefer project-relative paths such as "src/index.js", not absolute paths.
|
|
76
|
+
|
|
77
|
+
Exploration Rules (strict token discipline):
|
|
78
|
+
- Start with targeted, shallow exploration only.
|
|
79
|
+
- Never read full files by default.
|
|
80
|
+
- First, identify 1-3 likely files; do not scan broad directories unless required.
|
|
81
|
+
- Prefer compact reads before any full-file read.
|
|
82
|
+
- Default read order for large files/code:
|
|
83
|
+
1. read_file(mode="imports")
|
|
84
|
+
2. read_file(mode="exports")
|
|
85
|
+
3. read_file(mode="symbol", query="...")
|
|
86
|
+
4. read_file(mode="grep", query="...")
|
|
87
|
+
5. read_file(mode="head" or mode="tail")
|
|
88
|
+
6. read_file(mode="full") only if still necessary and only once per target file.
|
|
89
|
+
- If enough evidence is already gathered, stop reading and act.
|
|
90
|
+
- Do not re-read unchanged files unless the previous read was insufficient.
|
|
91
|
+
- Before modifying code, inspect only minimal nearby context needed for a safe edit.
|
|
92
|
+
- Hard limit: at most ${maxDiscoveryCallsPerRound} discovery calls per round (read/list/search/fetch) unless you already switched to action calls.
|
|
93
|
+
|
|
94
|
+
Dependency Files:
|
|
95
|
+
When relevant, check project dependency/config files such as:
|
|
96
|
+
- package.json
|
|
97
|
+
- pnpm-lock.yaml
|
|
98
|
+
- yarn.lock
|
|
99
|
+
- package-lock.json
|
|
100
|
+
- tsconfig.json
|
|
101
|
+
- vite.config.*
|
|
102
|
+
- next.config.*
|
|
103
|
+
- requirements.txt
|
|
104
|
+
- pyproject.toml
|
|
105
|
+
- Cargo.toml
|
|
106
|
+
- go.mod
|
|
107
|
+
- Dockerfile
|
|
108
|
+
- docker-compose.yml
|
|
109
|
+
- README.md
|
|
110
|
+
|
|
111
|
+
Workflow: Existing Code Questions
|
|
112
|
+
1. List relevant directories.
|
|
113
|
+
2. Read relevant files.
|
|
114
|
+
3. Analyze based on actual code.
|
|
115
|
+
4. Answer with specific file references and concise reasoning.
|
|
116
|
+
|
|
117
|
+
Workflow: New Feature Implementation
|
|
118
|
+
1. Inspect project structure.
|
|
119
|
+
2. Find similar existing implementations.
|
|
120
|
+
3. Check dependencies and conventions.
|
|
121
|
+
4. Create needed directories.
|
|
122
|
+
5. Write complete implementation.
|
|
123
|
+
6. Add or update tests when appropriate.
|
|
124
|
+
7. Run the smallest relevant verification command.
|
|
125
|
+
8. Summarize what changed and how it was verified.
|
|
126
|
+
|
|
127
|
+
Workflow: Code Modification
|
|
128
|
+
1. Read the target file and nearby related files.
|
|
129
|
+
2. Identify the minimal safe change.
|
|
130
|
+
3. Apply the change.
|
|
131
|
+
4. Run relevant formatting, typecheck, tests, or diagnostics when available.
|
|
132
|
+
5. Summarize changed files and verification results.
|
|
133
|
+
|
|
134
|
+
Workflow: Debugging
|
|
135
|
+
1. Inspect the reported error, logs, or failing behavior.
|
|
136
|
+
2. Read relevant source files.
|
|
137
|
+
3. Reproduce the issue when feasible.
|
|
138
|
+
4. Identify the root cause.
|
|
139
|
+
5. Apply the smallest fix.
|
|
140
|
+
6. Verify with a focused command.
|
|
141
|
+
7. Explain the cause and fix briefly.
|
|
142
|
+
|
|
143
|
+
Workflow: Refactoring
|
|
144
|
+
1. Inspect current implementation thoroughly.
|
|
145
|
+
2. Identify dependencies and public interfaces.
|
|
146
|
+
3. Propose the refactor if it is broad or risky.
|
|
147
|
+
4. Make incremental changes only after confirmation when required.
|
|
148
|
+
5. Preserve existing behavior.
|
|
149
|
+
6. Run tests or checks afterward.
|
|
150
|
+
|
|
151
|
+
Editing Rules:
|
|
152
|
+
- Preserve formatting style unless the project clearly uses a formatter.
|
|
153
|
+
- Do not rewrite entire files unless necessary.
|
|
154
|
+
- Do not introduce new dependencies unless necessary.
|
|
155
|
+
- Do not change unrelated code.
|
|
156
|
+
- Do not remove comments unless they are wrong or obsolete.
|
|
157
|
+
- Do not silently change public behavior.
|
|
158
|
+
- Keep error handling explicit and appropriate for the language/framework.
|
|
159
|
+
|
|
160
|
+
Testing and Verification:
|
|
161
|
+
- Prefer the smallest relevant check first.
|
|
162
|
+
- Use existing scripts when available, such as:
|
|
163
|
+
- npm test
|
|
164
|
+
- npm run test
|
|
165
|
+
- npm run typecheck
|
|
166
|
+
- npm run lint
|
|
167
|
+
- pnpm test
|
|
168
|
+
- pytest
|
|
169
|
+
- cargo test
|
|
170
|
+
- go test ./...
|
|
171
|
+
- If verification fails, inspect the failure and fix if it is within scope.
|
|
172
|
+
- If verification cannot be run, explain why.
|
|
173
|
+
|
|
174
|
+
Command Safety:
|
|
175
|
+
Never run destructive or high-risk commands without explicit confirmation, including:
|
|
176
|
+
- rm -rf
|
|
177
|
+
- git reset --hard
|
|
178
|
+
- git clean
|
|
179
|
+
- force pushes
|
|
180
|
+
- database migrations that mutate data
|
|
181
|
+
- commands that delete, encrypt, overwrite, or mass-modify files
|
|
182
|
+
- commands that install global packages
|
|
183
|
+
- commands that expose secrets
|
|
184
|
+
|
|
185
|
+
Git Rules:
|
|
186
|
+
- Do not create commits unless the user asks.
|
|
187
|
+
- Do not switch branches unless the user asks.
|
|
188
|
+
- Do not discard user changes.
|
|
189
|
+
- Before risky edits, check current file state if needed.
|
|
190
|
+
|
|
191
|
+
Security Rules:
|
|
192
|
+
- Treat .env files, credentials, API keys, private keys, tokens, and secrets as sensitive.
|
|
193
|
+
- Do not print secret values.
|
|
194
|
+
- Do not write secrets into source code.
|
|
195
|
+
- Use environment variables or existing secret-management patterns.
|
|
196
|
+
- Validate untrusted input.
|
|
197
|
+
- Avoid unsafe eval, shell injection, SQL injection, path traversal, XSS, SSRF, insecure randomness, and overly broad permissions.
|
|
198
|
+
|
|
199
|
+
Web Usage:
|
|
200
|
+
- Use web_search or fetch_url for current documentation, dependency behavior, APIs, error messages, or recently changed tooling.
|
|
201
|
+
- Prefer official documentation and primary sources.
|
|
202
|
+
- Do not browse when the answer is fully determined by the local codebase.
|
|
203
|
+
|
|
204
|
+
Response Style:
|
|
205
|
+
- Be concise.
|
|
206
|
+
- Be specific.
|
|
207
|
+
- Mention files changed.
|
|
208
|
+
- Mention commands run and whether they passed.
|
|
209
|
+
- If uncertain, say what is unknown and what evidence is missing.
|
|
210
|
+
- Do not claim success unless the tool results support it.
|
|
211
|
+
|
|
212
|
+
After Tool Completion:
|
|
213
|
+
Provide a concise final summary with:
|
|
214
|
+
1. What was inspected or changed
|
|
215
|
+
2. Verification performed
|
|
216
|
+
3. Any remaining caveats or next steps
|
|
217
|
+
|
|
218
|
+
Examples:
|
|
219
|
+
|
|
220
|
+
Correct fallback JSON tool call:
|
|
221
|
+
{"shmakk_actions":[{"tool":"list_dir","args":{"path":"src"}}]}
|
|
222
|
+
|
|
223
|
+
Correct fallback JSON tool call:
|
|
224
|
+
{"shmakk_actions":[{"tool":"read_file","args":{"path":"package.json"}}]}
|
|
225
|
+
|
|
226
|
+
Correct fallback JSON tool call:
|
|
227
|
+
{"shmakk_actions":[{"tool":"run","args":{"cmd":"npm test"}}]}
|
|
228
|
+
|
|
229
|
+
Incorrect:
|
|
230
|
+
I will check the src directory:
|
|
231
|
+
{"shmakk_actions":[{"tool":"list_dir","args":{"path":"src"}}]}
|
|
232
|
+
|
|
233
|
+
Incorrect:
|
|
234
|
+
\`\`\`json
|
|
235
|
+
{"shmakk_actions":[{"tool":"list_dir","args":{"path":"src"}}]}
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
Incorrect:
|
|
239
|
+
Can you run npm test for me?
|
|
240
|
+
|
|
241
|
+
Incorrect:
|
|
242
|
+
I assume this is a React project.
|
|
243
|
+
|
|
244
|
+
Remember:
|
|
245
|
+
- Inspect first.
|
|
246
|
+
- Use tools directly.
|
|
247
|
+
- Prefer minimal edits.
|
|
248
|
+
- Verify when possible.
|
|
249
|
+
- Use only native tool calls or the exact JSON fallback.
|
|
250
|
+
|
|
251
|
+
Final rule:
|
|
252
|
+
Never output XML, markdown, or prose when calling a tool.
|
|
253
|
+
Use native tool calls if available.
|
|
254
|
+
Otherwise output only:
|
|
255
|
+
{"shmakk_actions":[{"tool":"tool_name","args":{...}}]}
|
|
256
|
+
${indexHint}
|
|
257
|
+
${activeSkillText ? `\n\n${activeSkillText}` : ''}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { buildSystemPrompt };
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// Tool definitions, classification, dispatch, and fallback parsing.
|
|
2
|
+
// Extracted from agent.js. Depends on ./safety and ./web for run/search/fetch.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
const { classifyRunCommand, isSecretPath } = require('./safety');
|
|
8
|
+
const { webSearch, fetchUrl } = require('./web');
|
|
9
|
+
|
|
10
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
11
|
+
|
|
12
|
+
// Resolve a path against a list of allowed roots. Returns the absolute
|
|
13
|
+
// path if it lies inside any root, or null otherwise. The first root in
|
|
14
|
+
// the list is used as the base for relative resolution.
|
|
15
|
+
function within(roots, p) {
|
|
16
|
+
if (!roots || !roots.length) return null;
|
|
17
|
+
if (typeof p !== 'string' || !p.trim()) return null;
|
|
18
|
+
const base = path.resolve(roots[0]);
|
|
19
|
+
const abs = path.isAbsolute(p) ? path.resolve(p) : path.resolve(base, p);
|
|
20
|
+
for (const r of roots) {
|
|
21
|
+
const rr = path.resolve(r);
|
|
22
|
+
if (abs === rr || abs.startsWith(rr + path.sep)) return abs;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TOOLS = [
|
|
28
|
+
{ type: 'function', function: {
|
|
29
|
+
name: 'read_file',
|
|
30
|
+
description: 'Read a UTF-8 file inside the workspace. Supports compact partial reads.',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['path'],
|
|
34
|
+
properties: {
|
|
35
|
+
path: { type: 'string' },
|
|
36
|
+
mode: { type: 'string', enum: ['full', 'head', 'tail', 'grep', 'imports', 'exports', 'symbol'] },
|
|
37
|
+
max_lines: { type: 'number', minimum: 1, maximum: 400 },
|
|
38
|
+
query: { type: 'string' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}},
|
|
42
|
+
{ type: 'function', function: {
|
|
43
|
+
name: 'write_file',
|
|
44
|
+
description: 'Write or overwrite a UTF-8 file inside the workspace.',
|
|
45
|
+
parameters: { type: 'object', required: ['path', 'content'], properties: { path: { type: 'string' }, content: { type: 'string' } } },
|
|
46
|
+
}},
|
|
47
|
+
{ type: 'function', function: {
|
|
48
|
+
name: 'edit_file',
|
|
49
|
+
description: 'Edit an existing UTF-8 file inside the workspace by replacing a specific string with a new string.',
|
|
50
|
+
parameters: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
required: ['path', 'old_string', 'new_string'],
|
|
53
|
+
properties: {
|
|
54
|
+
path: { type: 'string' },
|
|
55
|
+
old_string: { type: 'string' },
|
|
56
|
+
new_string: { type: 'string' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}},
|
|
60
|
+
{ type: 'function', function: {
|
|
61
|
+
name: 'make_dir',
|
|
62
|
+
description: 'Create a directory inside the workspace, including parents.',
|
|
63
|
+
parameters: { type: 'object', required: ['path'], properties: { path: { type: 'string' } } },
|
|
64
|
+
}},
|
|
65
|
+
{ type: 'function', function: {
|
|
66
|
+
name: 'list_dir',
|
|
67
|
+
description: 'List entries in a directory inside the workspace.',
|
|
68
|
+
parameters: { type: 'object', required: ['path'], properties: { path: { type: 'string' } } },
|
|
69
|
+
}},
|
|
70
|
+
{ type: 'function', function: {
|
|
71
|
+
name: 'run',
|
|
72
|
+
description: 'Run a non-interactive shell command inside the workspace. Output is captured.',
|
|
73
|
+
parameters: { type: 'object', required: ['cmd'], properties: { cmd: { type: 'string' } } },
|
|
74
|
+
}},
|
|
75
|
+
{ type: 'function', function: {
|
|
76
|
+
name: 'web_search',
|
|
77
|
+
description: 'Search the web for current information. Returns titles, URLs, and snippets.',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
required: ['query'],
|
|
81
|
+
properties: {
|
|
82
|
+
query: { type: 'string' },
|
|
83
|
+
max_results: { type: 'number', minimum: 1, maximum: 10 },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}},
|
|
87
|
+
{ type: 'function', function: {
|
|
88
|
+
name: 'fetch_url',
|
|
89
|
+
description: 'Fetch text from an http(s) URL for source checking. Output is size-limited.',
|
|
90
|
+
parameters: { type: 'object', required: ['url'], properties: { url: { type: 'string' } } },
|
|
91
|
+
}},
|
|
92
|
+
{ type: 'function', function: {
|
|
93
|
+
name: 'delete_file',
|
|
94
|
+
description: 'Delete a file inside the workspace. Always requires user confirmation.',
|
|
95
|
+
parameters: { type: 'object', required: ['path'], properties: { path: { type: 'string' } } },
|
|
96
|
+
}},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Tool safety classification.
|
|
100
|
+
// 'safe' → auto mode runs it without asking; review mode asks with [Y/n]
|
|
101
|
+
// 'unsafe' → both modes ask, defaulting to No ([y/N])
|
|
102
|
+
// 'uncertain' → both modes ask, defaulting to No ([y/N])
|
|
103
|
+
function classifyTool(name, args) {
|
|
104
|
+
if (name === 'read_file' || name === 'list_dir') {
|
|
105
|
+
if (args.path && isSecretPath(args.path)) return 'unsafe';
|
|
106
|
+
return 'safe';
|
|
107
|
+
}
|
|
108
|
+
if (name === 'write_file') {
|
|
109
|
+
if (args.path && isSecretPath(args.path)) return 'unsafe';
|
|
110
|
+
return 'uncertain';
|
|
111
|
+
}
|
|
112
|
+
if (name === 'make_dir') {
|
|
113
|
+
if (args.path && isSecretPath(args.path)) return 'unsafe';
|
|
114
|
+
return 'safe';
|
|
115
|
+
}
|
|
116
|
+
if (name === 'delete_file') return 'unsafe'; // user wants delete to always prompt
|
|
117
|
+
if (name === 'run') return classifyRunCommand(args.cmd || '');
|
|
118
|
+
if (name === 'web_search' || name === 'fetch_url') return 'safe';
|
|
119
|
+
return 'uncertain';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function describeTool(name, args) {
|
|
123
|
+
if (name === 'read_file') return `read_file ${args.path}${args.mode ? ` [${args.mode}]` : ''}`;
|
|
124
|
+
if (name === 'list_dir') return `list_dir ${args.path || '.'}`;
|
|
125
|
+
if (name === 'write_file') return `write_file ${args.path} (${(args.content || '').length} bytes)`;
|
|
126
|
+
if (name === 'edit_file') return `edit_file ${args.path} (${(args.old_string || '').slice(0, 40)}…)`;
|
|
127
|
+
if (name === 'make_dir') return `make_dir ${args.path}`;
|
|
128
|
+
if (name === 'delete_file') return `delete_file ${args.path}`;
|
|
129
|
+
if (name === 'run') return `run: ${args.cmd}`;
|
|
130
|
+
if (name === 'web_search') return `web_search ${args.query}`;
|
|
131
|
+
if (name === 'fetch_url') return `fetch_url ${args.url}`;
|
|
132
|
+
return `${name} ${JSON.stringify(args).slice(0, 80)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runCmd(cwd, cmd, signal) {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
let removeAbortListener = null;
|
|
138
|
+
const child = execFile('/bin/sh', ['-c', cmd], { cwd, timeout: 15000, maxBuffer: 64 * 1024 },
|
|
139
|
+
(err, stdout, stderr) => {
|
|
140
|
+
if (removeAbortListener) removeAbortListener();
|
|
141
|
+
resolve({
|
|
142
|
+
exitCode: err ? (err.code || 1) : 0,
|
|
143
|
+
stdout: (stdout || '').toString().slice(-32000),
|
|
144
|
+
stderr: (stderr || '').toString().slice(-32000),
|
|
145
|
+
aborted: signal && signal.aborted ? true : undefined,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
if (signal) {
|
|
149
|
+
const onAbort = () => { try { child.kill('SIGINT'); } catch {} setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 500); };
|
|
150
|
+
if (signal.aborted) onAbort();
|
|
151
|
+
else {
|
|
152
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
153
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function dispatchTool(name, args, roots, confirmTool, signal) {
|
|
160
|
+
if (signal && signal.aborted) return { error: 'aborted' };
|
|
161
|
+
const safety = classifyTool(name, args);
|
|
162
|
+
if (confirmTool) {
|
|
163
|
+
const ok = await confirmTool({ name, args, safety, description: describeTool(name, args) });
|
|
164
|
+
if (!ok) return { error: 'user declined' };
|
|
165
|
+
}
|
|
166
|
+
if (signal && signal.aborted) return { error: 'aborted' };
|
|
167
|
+
if (name === 'read_file') {
|
|
168
|
+
const p = within(roots, args.path);
|
|
169
|
+
if (!p) return { error: 'path outside workspace' };
|
|
170
|
+
try {
|
|
171
|
+
const buf = fs.readFileSync(p);
|
|
172
|
+
const text = buf.slice(0, MAX_FILE_BYTES).toString('utf8');
|
|
173
|
+
const lines = text.split(/\r?\n/);
|
|
174
|
+
const mode = args.mode || 'full';
|
|
175
|
+
const maxLines = Math.max(1, Math.min(400, Number(args.max_lines) || 80));
|
|
176
|
+
if (mode === 'head') {
|
|
177
|
+
return { content: lines.slice(0, maxLines).join('\n'), mode, truncated: lines.length > maxLines };
|
|
178
|
+
}
|
|
179
|
+
if (mode === 'tail') {
|
|
180
|
+
return { content: lines.slice(-maxLines).join('\n'), mode, truncated: lines.length > maxLines };
|
|
181
|
+
}
|
|
182
|
+
if (mode === 'grep') {
|
|
183
|
+
const q = String(args.query || '').toLowerCase();
|
|
184
|
+
if (!q) return { error: 'query required for grep mode' };
|
|
185
|
+
const hits = [];
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
if (!lines[i].toLowerCase().includes(q)) continue;
|
|
188
|
+
const start = Math.max(0, i - 2);
|
|
189
|
+
const end = Math.min(lines.length, i + 3);
|
|
190
|
+
hits.push(lines.slice(start, end).join('\n'));
|
|
191
|
+
if (hits.length >= 5) break;
|
|
192
|
+
}
|
|
193
|
+
return { content: hits.join('\n---\n'), mode, truncated: hits.length >= 5 };
|
|
194
|
+
}
|
|
195
|
+
if (mode === 'imports') {
|
|
196
|
+
const out = lines.filter((line) => /\bimport\b|require\(/.test(line)).slice(0, maxLines).join('\n');
|
|
197
|
+
return { content: out, mode, truncated: out.split(/\r?\n/).length >= maxLines };
|
|
198
|
+
}
|
|
199
|
+
if (mode === 'exports') {
|
|
200
|
+
const out = lines.filter((line) => /\bexport\b|module\.exports/.test(line)).slice(0, maxLines).join('\n');
|
|
201
|
+
return { content: out, mode, truncated: out.split(/\r?\n/).length >= maxLines };
|
|
202
|
+
}
|
|
203
|
+
if (mode === 'symbol') {
|
|
204
|
+
const q = String(args.query || '').toLowerCase();
|
|
205
|
+
if (!q) return { error: 'query required for symbol mode' };
|
|
206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
207
|
+
if (!lines[i].toLowerCase().includes(q)) continue;
|
|
208
|
+
const start = Math.max(0, i - 8);
|
|
209
|
+
const end = Math.min(lines.length, i + Math.max(12, maxLines));
|
|
210
|
+
return { content: lines.slice(start, end).join('\n'), mode, truncated: end < lines.length };
|
|
211
|
+
}
|
|
212
|
+
return { error: `symbol/query not found: ${args.query}` };
|
|
213
|
+
}
|
|
214
|
+
return { content: text, mode: 'full', truncated: buf.length > MAX_FILE_BYTES };
|
|
215
|
+
} catch (e) { return { error: String(e.message) }; }
|
|
216
|
+
}
|
|
217
|
+
if (name === 'list_dir') {
|
|
218
|
+
const p = within(roots, args.path || '.');
|
|
219
|
+
if (!p) return { error: 'path outside workspace' };
|
|
220
|
+
try {
|
|
221
|
+
const ents = fs.readdirSync(p, { withFileTypes: true })
|
|
222
|
+
.map((e) => ({ name: e.name, type: e.isDirectory() ? 'dir' : 'file' }));
|
|
223
|
+
return { entries: ents };
|
|
224
|
+
} catch (e) { return { error: String(e.message) }; }
|
|
225
|
+
}
|
|
226
|
+
if (name === 'write_file') {
|
|
227
|
+
const p = within(roots, args.path);
|
|
228
|
+
if (!p) return { error: 'path outside workspace' };
|
|
229
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
230
|
+
fs.writeFileSync(p, args.content ?? '');
|
|
231
|
+
return { ok: true };
|
|
232
|
+
}
|
|
233
|
+
if (name === 'edit_file') {
|
|
234
|
+
const p = within(roots, args.path);
|
|
235
|
+
if (!p) return { error: 'path outside workspace' };
|
|
236
|
+
try {
|
|
237
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
238
|
+
const oldString = String(args.old_string ?? '');
|
|
239
|
+
const newString = String(args.new_string ?? '');
|
|
240
|
+
if (!oldString) return { error: 'old_string is required' };
|
|
241
|
+
const first = content.indexOf(oldString);
|
|
242
|
+
if (first === -1) return { error: 'old_string not found' };
|
|
243
|
+
const second = content.indexOf(oldString, first + oldString.length);
|
|
244
|
+
if (second !== -1) return { error: 'old_string is ambiguous; appears multiple times' };
|
|
245
|
+
const updated = content.slice(0, first) + newString + content.slice(first + oldString.length);
|
|
246
|
+
fs.writeFileSync(p, updated);
|
|
247
|
+
return { ok: true, replaced: 1 };
|
|
248
|
+
} catch (e) { return { error: String(e.message) }; }
|
|
249
|
+
}
|
|
250
|
+
if (name === 'make_dir') {
|
|
251
|
+
const p = within(roots, args.path);
|
|
252
|
+
if (!p) return { error: 'path outside workspace' };
|
|
253
|
+
fs.mkdirSync(p, { recursive: true });
|
|
254
|
+
return { ok: true };
|
|
255
|
+
}
|
|
256
|
+
if (name === 'delete_file') {
|
|
257
|
+
const p = within(roots, args.path);
|
|
258
|
+
if (!p) return { error: 'path outside workspace' };
|
|
259
|
+
try { fs.rmSync(p, { force: true }); return { ok: true }; }
|
|
260
|
+
catch (e) { return { error: String(e.message) }; }
|
|
261
|
+
}
|
|
262
|
+
if (name === 'run') {
|
|
263
|
+
// run from the first root as cwd
|
|
264
|
+
return await runCmd(roots[0], args.cmd, signal);
|
|
265
|
+
}
|
|
266
|
+
if (name === 'web_search') {
|
|
267
|
+
return await webSearch(args.query, args.max_results, signal);
|
|
268
|
+
}
|
|
269
|
+
if (name === 'fetch_url') {
|
|
270
|
+
return await fetchUrl(args.url, signal);
|
|
271
|
+
}
|
|
272
|
+
return { error: `unknown tool: ${name}` };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Tool call normalization & budgeting ────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function normalizeToolCalls(rawToolCalls, iter) {
|
|
278
|
+
const calls = [];
|
|
279
|
+
let seq = 0;
|
|
280
|
+
for (const tc of rawToolCalls || []) {
|
|
281
|
+
if (!tc || tc.type !== 'function') continue;
|
|
282
|
+
const name = String(tc.function?.name || '').trim();
|
|
283
|
+
if (!name) continue;
|
|
284
|
+
const id = String(tc.id || '').trim() || `tc_${iter}_${seq++}`;
|
|
285
|
+
const argsRaw = typeof tc.function?.arguments === 'string' ? tc.function.arguments : '';
|
|
286
|
+
calls.push({
|
|
287
|
+
id,
|
|
288
|
+
type: 'function',
|
|
289
|
+
function: {
|
|
290
|
+
name,
|
|
291
|
+
arguments: argsRaw || '{}',
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return calls;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function applyRoundToolBudget(toolCalls, maxDiscoveryCalls) {
|
|
299
|
+
const discovery = new Set(['read_file', 'list_dir', 'web_search', 'fetch_url']);
|
|
300
|
+
const actionCalls = [];
|
|
301
|
+
const discoveryCalls = [];
|
|
302
|
+
for (const c of toolCalls) {
|
|
303
|
+
if (discovery.has(c.function?.name)) discoveryCalls.push(c);
|
|
304
|
+
else actionCalls.push(c);
|
|
305
|
+
}
|
|
306
|
+
// Progress-first bias: execute action calls first, then only a small discovery budget.
|
|
307
|
+
return [...actionCalls, ...discoveryCalls.slice(0, maxDiscoveryCalls)];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Fallback action parsing (text-based tool calls) ─────────────────────────
|
|
311
|
+
|
|
312
|
+
function stripJsonFence(s) {
|
|
313
|
+
const t = String(s || '').trim();
|
|
314
|
+
const m = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(t);
|
|
315
|
+
return m ? m[1].trim() : t;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseFallbackActions(content) {
|
|
319
|
+
const text = stripJsonFence(content);
|
|
320
|
+
if (!text) return [];
|
|
321
|
+
|
|
322
|
+
let obj = null;
|
|
323
|
+
try {
|
|
324
|
+
obj = JSON.parse(text);
|
|
325
|
+
} catch {
|
|
326
|
+
const start = text.indexOf('{');
|
|
327
|
+
const end = text.lastIndexOf('}');
|
|
328
|
+
if (start === -1 || end <= start) return [];
|
|
329
|
+
try { obj = JSON.parse(text.slice(start, end + 1)); } catch { return []; }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rawActions = Array.isArray(obj?.shmakk_actions) ? obj.shmakk_actions : [];
|
|
333
|
+
const allowed = new Set(TOOLS.map((t) => t.function.name));
|
|
334
|
+
const actions = [];
|
|
335
|
+
for (const a of rawActions) {
|
|
336
|
+
const name = a?.tool || a?.name;
|
|
337
|
+
const args = a?.args && typeof a.args === 'object' ? a.args : {};
|
|
338
|
+
if (allowed.has(name)) actions.push({ name, args });
|
|
339
|
+
}
|
|
340
|
+
return actions;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseXmlFallbackActions(content) {
|
|
344
|
+
const text = String(content || '');
|
|
345
|
+
if (!text) return [];
|
|
346
|
+
const allowed = new Set(TOOLS.map((t) => t.function.name));
|
|
347
|
+
const actions = [];
|
|
348
|
+
|
|
349
|
+
const tcRe = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
|
|
350
|
+
let m;
|
|
351
|
+
while ((m = tcRe.exec(text))) {
|
|
352
|
+
const block = m[1];
|
|
353
|
+
const fnMatch = /<function\s*=\s*([a-zA-Z0-9_]+)\s*>([\s\S]*?)<\/function>/i.exec(block);
|
|
354
|
+
if (!fnMatch) continue;
|
|
355
|
+
const name = fnMatch[1];
|
|
356
|
+
if (!allowed.has(name)) continue;
|
|
357
|
+
const body = fnMatch[2] || '';
|
|
358
|
+
const args = {};
|
|
359
|
+
const pRe = /<parameter\s*=\s*([a-zA-Z0-9_]+)\s*>([\s\S]*?)<\/parameter>/gi;
|
|
360
|
+
let p;
|
|
361
|
+
while ((p = pRe.exec(body))) {
|
|
362
|
+
const k = p[1];
|
|
363
|
+
const raw = (p[2] || '').trim();
|
|
364
|
+
if (/^(true|false)$/i.test(raw)) args[k] = /^true$/i.test(raw);
|
|
365
|
+
else if (/^-?\d+(?:\.\d+)?$/.test(raw)) args[k] = Number(raw);
|
|
366
|
+
else args[k] = raw;
|
|
367
|
+
}
|
|
368
|
+
actions.push({ name, args });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return actions;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = {
|
|
375
|
+
TOOLS,
|
|
376
|
+
classifyTool,
|
|
377
|
+
describeTool,
|
|
378
|
+
dispatchTool,
|
|
379
|
+
runCmd,
|
|
380
|
+
normalizeToolCalls,
|
|
381
|
+
applyRoundToolBudget,
|
|
382
|
+
within,
|
|
383
|
+
parseFallbackActions,
|
|
384
|
+
parseXmlFallbackActions,
|
|
385
|
+
stripJsonFence,
|
|
386
|
+
};
|