smolcoder 0.4.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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/agent.js +411 -0
- package/dist/context.js +337 -0
- package/dist/detect.js +199 -0
- package/dist/events.js +24 -0
- package/dist/index.js +680 -0
- package/dist/plan.js +91 -0
- package/dist/prompt.js +85 -0
- package/dist/providers/lmstudio.js +326 -0
- package/dist/providers/ollama.js +264 -0
- package/dist/providers/types.js +60 -0
- package/dist/sandbox.js +179 -0
- package/dist/tools/check.js +193 -0
- package/dist/tools/fs-tools.js +417 -0
- package/dist/tools/index.js +236 -0
- package/dist/tools/shell.js +168 -0
- package/dist/tools/tasks.js +128 -0
- package/dist/tui/editor.js +134 -0
- package/dist/tui/keys.js +145 -0
- package/dist/tui/tui.js +632 -0
- package/dist/ui.js +226 -0
- package/dist/util.js +72 -0
- package/dist/web/page.js +381 -0
- package/dist/web/webui.js +262 -0
- package/package.json +48 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tool registry. Seven tools, flat parameters only (strings and one enum — no
|
|
3
|
+
// nested objects or arrays: small models mangle them), an example call inside
|
|
4
|
+
// every description (small models imitate better than they infer), and the
|
|
5
|
+
// mode decides which tools EXIST — the permission model is "which schemas were
|
|
6
|
+
// sent", not a runtime policy engine.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MODE_LABELS = void 0;
|
|
9
|
+
exports.buildToolSpecs = buildToolSpecs;
|
|
10
|
+
exports.executeTool = executeTool;
|
|
11
|
+
exports.commandOf = commandOf;
|
|
12
|
+
const fs_tools_1 = require("./fs-tools");
|
|
13
|
+
const check_1 = require("./check");
|
|
14
|
+
const shell_1 = require("./shell");
|
|
15
|
+
const sandbox_1 = require("../sandbox");
|
|
16
|
+
const util_1 = require("../util");
|
|
17
|
+
exports.MODE_LABELS = {
|
|
18
|
+
ro: "read-only",
|
|
19
|
+
edit: "edit",
|
|
20
|
+
bypass: "bypass permissions",
|
|
21
|
+
};
|
|
22
|
+
const TOOL_RESULT_CAP = 10000; // chars — final safety net over per-tool caps
|
|
23
|
+
function buildToolSpecs(mode) {
|
|
24
|
+
const read = [
|
|
25
|
+
{
|
|
26
|
+
name: "read_file",
|
|
27
|
+
description: 'Read a text file in the workspace. Example: {"path": "src/app.js"}. Long files are returned in chunks; pass "offset" (a line number) to continue reading.',
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
path: { type: "string", description: "File path relative to the workspace" },
|
|
32
|
+
offset: { type: "number", description: "Line number to start from (optional)" },
|
|
33
|
+
limit: { type: "number", description: "Max lines to return (optional)" },
|
|
34
|
+
},
|
|
35
|
+
required: ["path"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "list_files",
|
|
40
|
+
description: 'List files and folders in the workspace. Example: {} for everything, or {"path": "src"} for one folder. Folders end with "/".',
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
path: { type: "string", description: "Folder to list (optional, default: whole workspace)" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "search",
|
|
50
|
+
description: 'Search inside files for a pattern (regular expression; plain text also works). Example: {"pattern": "TODO"}. Returns file:line: matching text.',
|
|
51
|
+
parameters: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
pattern: { type: "string", description: "Text or regex to find" },
|
|
55
|
+
path: { type: "string", description: "Folder to search in (optional)" },
|
|
56
|
+
},
|
|
57
|
+
required: ["pattern"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "plan",
|
|
62
|
+
description: 'Your to-do list for multi-step tasks — shown to the user and kept for you across context compaction. Create it first: {"action": "set", "steps": "create index.html\\ncreate game.js\\ntest the page"} (one step per line). Mark the current step finished with {"action": "done"} (or {"action": "done", "step": 2}). Append with {"action": "add", "text": "..."}. {"action": "show"} displays it.',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
action: { type: "string", enum: ["set", "done", "add", "show"] },
|
|
67
|
+
steps: { type: "string", description: 'The steps, one per line (only for "set")' },
|
|
68
|
+
step: { type: "number", description: 'Step number to mark done (optional, for "done")' },
|
|
69
|
+
text: { type: "string", description: 'Step to append (only for "add")' },
|
|
70
|
+
},
|
|
71
|
+
required: ["action"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
const write = [
|
|
76
|
+
{
|
|
77
|
+
name: "write_file",
|
|
78
|
+
description: 'Create a new file or completely overwrite an existing one. Example: {"path": "src/new.js", "content": "..."}. Parent folders are created automatically. To change part of an existing file, prefer edit_file.',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
path: { type: "string", description: "File path relative to the workspace" },
|
|
83
|
+
content: { type: "string", description: "The full file content" },
|
|
84
|
+
},
|
|
85
|
+
required: ["path", "content"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "edit_file",
|
|
90
|
+
description: 'Replace text inside an existing file. Copy old_text EXACTLY from the file (a few lines, enough to be unique), and give the replacement as new_text. Example: {"path": "src/app.js", "old_text": "const x = 1;", "new_text": "const x = 2;"}',
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
path: { type: "string", description: "File path relative to the workspace" },
|
|
95
|
+
old_text: { type: "string", description: "Exact text currently in the file" },
|
|
96
|
+
new_text: { type: "string", description: "Text to replace it with" },
|
|
97
|
+
},
|
|
98
|
+
required: ["path", "old_text", "new_text"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
const exec = [
|
|
103
|
+
{
|
|
104
|
+
name: "run_command",
|
|
105
|
+
description: 'Run a shell command in the workspace and wait for it to finish. Example: {"command": "npm test"}. Times out after 120s — for servers or watchers use the task tool instead.',
|
|
106
|
+
parameters: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
command: { type: "string", description: "The command to run" },
|
|
110
|
+
},
|
|
111
|
+
required: ["command"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "task",
|
|
116
|
+
description: 'Manage background tasks (things that keep running, like dev servers). action "start" runs a command in the background: {"action": "start", "command": "npm run dev"}. action "logs" shows recent output: {"action": "logs", "task_id": "t1"}. action "list" shows all tasks. action "stop" kills one: {"action": "stop", "task_id": "t1"}.',
|
|
117
|
+
parameters: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
action: { type: "string", enum: ["start", "list", "logs", "stop"] },
|
|
121
|
+
command: { type: "string", description: 'Command to run (only for "start")' },
|
|
122
|
+
task_id: { type: "string", description: 'Task id like "t1" (for "logs" and "stop")' },
|
|
123
|
+
},
|
|
124
|
+
required: ["action"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
if (mode === "ro")
|
|
129
|
+
return read;
|
|
130
|
+
return [...read, ...write, ...exec];
|
|
131
|
+
}
|
|
132
|
+
async function executeTool(name, args, ctx, signal) {
|
|
133
|
+
try {
|
|
134
|
+
let result;
|
|
135
|
+
switch (name) {
|
|
136
|
+
case "read_file":
|
|
137
|
+
result = (0, fs_tools_1.readFile)(ctx.workspace, args);
|
|
138
|
+
break;
|
|
139
|
+
case "list_files":
|
|
140
|
+
result = (0, fs_tools_1.listFiles)(ctx.workspace, args);
|
|
141
|
+
break;
|
|
142
|
+
case "search":
|
|
143
|
+
result = (0, fs_tools_1.searchFiles)(ctx.workspace, args);
|
|
144
|
+
break;
|
|
145
|
+
case "plan": {
|
|
146
|
+
const action = args.action;
|
|
147
|
+
if (action === "set")
|
|
148
|
+
result = ctx.plan.set(String(args.steps ?? ""));
|
|
149
|
+
else if (action === "done")
|
|
150
|
+
result = ctx.plan.markDone(args.step === undefined ? undefined : Number(args.step));
|
|
151
|
+
else if (action === "add")
|
|
152
|
+
result = ctx.plan.add(String(args.text ?? ""));
|
|
153
|
+
else if (action === "show")
|
|
154
|
+
result = ctx.plan.modelView();
|
|
155
|
+
else
|
|
156
|
+
return 'Error: action must be one of "set", "done", "add", "show". Example: {"action": "done"}';
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "write_file":
|
|
160
|
+
result = (0, fs_tools_1.writeFile)(ctx.workspace, args);
|
|
161
|
+
if (!result.startsWith("Error")) {
|
|
162
|
+
ctx.filesTouched.add(String(args.path));
|
|
163
|
+
result += afterWrite(ctx.workspace, String(args.path));
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
case "edit_file":
|
|
167
|
+
result = (0, fs_tools_1.editFile)(ctx.workspace, args);
|
|
168
|
+
if (!result.startsWith("Error")) {
|
|
169
|
+
ctx.filesTouched.add(String(args.path));
|
|
170
|
+
result += afterWrite(ctx.workspace, String(args.path));
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "run_command":
|
|
174
|
+
if (typeof args.command !== "string" || !args.command.trim()) {
|
|
175
|
+
return 'Error: command is required. Example: {"command": "npm test"}';
|
|
176
|
+
}
|
|
177
|
+
ctx.commandsRun.push(args.command);
|
|
178
|
+
result = await (0, shell_1.runCommand)(args.command, ctx.workspace, signal);
|
|
179
|
+
break;
|
|
180
|
+
case "task": {
|
|
181
|
+
const action = args.action;
|
|
182
|
+
if (action === "start") {
|
|
183
|
+
if (typeof args.command !== "string" || !args.command.trim()) {
|
|
184
|
+
return 'Error: "start" needs a command. Example: {"action": "start", "command": "npm run dev"}';
|
|
185
|
+
}
|
|
186
|
+
ctx.commandsRun.push(`[bg] ${args.command}`);
|
|
187
|
+
result = await ctx.taskManager.startWithEarlyOutput(args.command);
|
|
188
|
+
}
|
|
189
|
+
else if (action === "logs") {
|
|
190
|
+
result = ctx.taskManager.logs(String(args.task_id ?? ""), Number(args.lines) || 50);
|
|
191
|
+
}
|
|
192
|
+
else if (action === "stop") {
|
|
193
|
+
result = ctx.taskManager.stop(String(args.task_id ?? ""));
|
|
194
|
+
}
|
|
195
|
+
else if (action === "list") {
|
|
196
|
+
result = ctx.taskManager.list();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return 'Error: action must be one of "start", "list", "logs", "stop". Example: {"action": "list"}';
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
default:
|
|
204
|
+
return `Error: unknown tool "${name}". Available tools are listed in your tool definitions — use one of those.`;
|
|
205
|
+
}
|
|
206
|
+
return (0, util_1.truncateMiddle)(result, TOOL_RESULT_CAP);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (err instanceof sandbox_1.SandboxError)
|
|
210
|
+
return `Error: ${err.message}`;
|
|
211
|
+
return `Error: ${err?.message ?? String(err)}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Post-write hook: parse what was just written and coach on the first
|
|
215
|
+
* syntax error. A one-line warning riding on the success message is the
|
|
216
|
+
* cheapest possible feedback loop for a local model. */
|
|
217
|
+
function afterWrite(workspace, relPath) {
|
|
218
|
+
try {
|
|
219
|
+
const abs = (0, sandbox_1.resolveInWorkspace)(workspace, relPath);
|
|
220
|
+
const warning = (0, check_1.syntaxCheck)(abs, relPath);
|
|
221
|
+
return warning ? `
|
|
222
|
+
Warning: ${warning} Fix this before moving on (use edit_file).` : "";
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** The command a call would run, if it is an exec call (edit mode may gate it;
|
|
229
|
+
* bypass never asks; in ro mode the tool does not exist). */
|
|
230
|
+
function commandOf(name, args) {
|
|
231
|
+
if (name === "run_command")
|
|
232
|
+
return String(args.command ?? "");
|
|
233
|
+
if (name === "task" && args.action === "start")
|
|
234
|
+
return String(args.command ?? "");
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// run_command: one-shot foreground commands, cwd-locked to the workspace.
|
|
3
|
+
// Shell picking matters on Windows: local models emit POSIX commands, so we
|
|
4
|
+
// prefer Git Bash when it exists, skip WSL's System32 bash (different
|
|
5
|
+
// filesystem world), and fall back to PowerShell. The chosen shell is named in
|
|
6
|
+
// the system prompt so the model knows what dialect to write.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.pickShell = pickShell;
|
|
42
|
+
exports.killTree = killTree;
|
|
43
|
+
exports.runCommand = runCommand;
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const util_1 = require("../util");
|
|
48
|
+
let cached = null;
|
|
49
|
+
function pickShell() {
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
if (process.platform === "win32") {
|
|
53
|
+
const candidates = [
|
|
54
|
+
path.join(process.env["ProgramFiles"] ?? "C:\\Program Files", "Git", "bin", "bash.exe"),
|
|
55
|
+
path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Git", "bin", "bash.exe"),
|
|
56
|
+
path.join(process.env["LOCALAPPDATA"] ?? "", "Programs", "Git", "bin", "bash.exe"),
|
|
57
|
+
];
|
|
58
|
+
for (const p of candidates) {
|
|
59
|
+
if (p && fs.existsSync(p)) {
|
|
60
|
+
cached = { exe: p, argsFor: (cmd) => ["-lc", cmd], label: "bash (Git Bash)" };
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// any bash on PATH that is not WSL's System32 shim
|
|
65
|
+
const where = (0, child_process_1.spawnSync)("where.exe", ["bash"], { encoding: "utf8" });
|
|
66
|
+
if (where.status === 0) {
|
|
67
|
+
const found = where.stdout
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.find((p) => p && !p.toLowerCase().includes("system32"));
|
|
71
|
+
if (found) {
|
|
72
|
+
cached = { exe: found, argsFor: (cmd) => ["-lc", cmd], label: "bash (Git Bash)" };
|
|
73
|
+
return cached;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
cached = {
|
|
77
|
+
exe: "powershell.exe",
|
|
78
|
+
argsFor: (cmd) => ["-NoProfile", "-NonInteractive", "-Command", cmd],
|
|
79
|
+
label: "PowerShell",
|
|
80
|
+
};
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
const sh = fs.existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh";
|
|
84
|
+
cached = { exe: sh, argsFor: (cmd) => ["-lc", cmd], label: path.basename(sh) };
|
|
85
|
+
return cached;
|
|
86
|
+
}
|
|
87
|
+
function killTree(pid) {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
(0, child_process_1.spawnSync)("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore" });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
process.kill(-pid, "SIGKILL");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
try {
|
|
98
|
+
process.kill(pid, "SIGKILL");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* already gone */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const OUTPUT_CAP = 8000;
|
|
106
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
107
|
+
function runCommand(command, cwd, signal) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const shell = pickShell();
|
|
110
|
+
let output = "";
|
|
111
|
+
let finished = false;
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
const proc = (0, child_process_1.spawn)(shell.exe, shell.argsFor(command), {
|
|
114
|
+
cwd,
|
|
115
|
+
env: process.env,
|
|
116
|
+
detached: process.platform !== "win32", // process group for killTree
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
});
|
|
120
|
+
const append = (chunk) => {
|
|
121
|
+
if (output.length < OUTPUT_CAP * 4)
|
|
122
|
+
output += chunk.toString("utf8");
|
|
123
|
+
};
|
|
124
|
+
proc.stdout.on("data", append);
|
|
125
|
+
proc.stderr.on("data", append);
|
|
126
|
+
// User interrupt (esc / ctrl+c / web stop button): kill the whole tree now.
|
|
127
|
+
const onAbort = () => {
|
|
128
|
+
if (finished)
|
|
129
|
+
return;
|
|
130
|
+
finished = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
killTree(proc.pid);
|
|
133
|
+
resolve((output.trim() ? (0, util_1.truncateMiddle)(output, OUTPUT_CAP) + "\n" : "") +
|
|
134
|
+
"[command cancelled by the user before it finished]");
|
|
135
|
+
};
|
|
136
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
const cleanup = () => signal?.removeEventListener("abort", onAbort);
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
if (finished)
|
|
140
|
+
return;
|
|
141
|
+
finished = true;
|
|
142
|
+
cleanup();
|
|
143
|
+
killTree(proc.pid);
|
|
144
|
+
resolve((0, util_1.truncateMiddle)(output, OUTPUT_CAP) +
|
|
145
|
+
`\n[command timed out after ${DEFAULT_TIMEOUT_MS / 1000}s and was killed. For long-running commands like dev servers, use the task tool with {"action": "start"} instead.]`);
|
|
146
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
147
|
+
proc.on("error", (err) => {
|
|
148
|
+
if (finished)
|
|
149
|
+
return;
|
|
150
|
+
finished = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
cleanup();
|
|
153
|
+
resolve(`Error: could not start command: ${err.message}`);
|
|
154
|
+
});
|
|
155
|
+
proc.on("close", (code) => {
|
|
156
|
+
if (finished)
|
|
157
|
+
return;
|
|
158
|
+
finished = true;
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
cleanup();
|
|
161
|
+
const secs = ((Date.now() - started) / 1000).toFixed(1);
|
|
162
|
+
const body = output.trim() ? (0, util_1.truncateMiddle)(output, OUTPUT_CAP) : "(no output)";
|
|
163
|
+
resolve(`${body}\n[exit code ${code ?? "?"} in ${secs}s]`);
|
|
164
|
+
});
|
|
165
|
+
if (signal?.aborted)
|
|
166
|
+
onAbort();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Background tasks: one `task` tool with an action enum instead of four
|
|
3
|
+
// separate tools — one schema costs fewer context tokens, and small models
|
|
4
|
+
// handle enum dispatch on a single tool fine. Each task keeps a ring buffer of
|
|
5
|
+
// recent output so the agent (and the user, via /tasks and /logs) has
|
|
6
|
+
// visibility. All tasks are killed when smolcoder exits.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.TaskManager = void 0;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const shell_1 = require("./shell");
|
|
11
|
+
const RING_SIZE = 300;
|
|
12
|
+
class TaskManager {
|
|
13
|
+
cwd;
|
|
14
|
+
tasks = new Map();
|
|
15
|
+
counter = 0;
|
|
16
|
+
constructor(cwd) {
|
|
17
|
+
this.cwd = cwd;
|
|
18
|
+
}
|
|
19
|
+
start(command) {
|
|
20
|
+
const shell = (0, shell_1.pickShell)();
|
|
21
|
+
const id = `t${++this.counter}`;
|
|
22
|
+
const proc = (0, child_process_1.spawn)(shell.exe, shell.argsFor(command), {
|
|
23
|
+
cwd: this.cwd,
|
|
24
|
+
env: process.env,
|
|
25
|
+
detached: process.platform !== "win32",
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
});
|
|
29
|
+
const task = {
|
|
30
|
+
id,
|
|
31
|
+
command,
|
|
32
|
+
proc,
|
|
33
|
+
lines: [],
|
|
34
|
+
status: "running",
|
|
35
|
+
exitCode: null,
|
|
36
|
+
startedAt: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
const push = (chunk) => {
|
|
39
|
+
for (const line of chunk.toString("utf8").split(/\r?\n/)) {
|
|
40
|
+
if (line === "")
|
|
41
|
+
continue;
|
|
42
|
+
task.lines.push(line);
|
|
43
|
+
if (task.lines.length > RING_SIZE)
|
|
44
|
+
task.lines.shift();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
proc.stdout?.on("data", push);
|
|
48
|
+
proc.stderr?.on("data", push);
|
|
49
|
+
proc.on("error", (err) => {
|
|
50
|
+
task.lines.push(`[failed to start: ${err.message}]`);
|
|
51
|
+
task.status = "exited";
|
|
52
|
+
task.exitCode = -1;
|
|
53
|
+
});
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
if (task.status === "running") {
|
|
56
|
+
task.status = "exited";
|
|
57
|
+
task.exitCode = code;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.tasks.set(id, task);
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
63
|
+
/** Wait briefly after start so early output (or an instant crash) is visible. */
|
|
64
|
+
async startWithEarlyOutput(command) {
|
|
65
|
+
const id = this.start(command);
|
|
66
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
67
|
+
const task = this.tasks.get(id);
|
|
68
|
+
const early = task.lines.slice(-15).join("\n");
|
|
69
|
+
const status = task.status === "running"
|
|
70
|
+
? `Task ${id} is running in the background.`
|
|
71
|
+
: `Task ${id} exited almost immediately (exit code ${task.exitCode}).`;
|
|
72
|
+
return `${status} Command: ${command}\n${early ? `Early output:\n${early}\n` : ""}Use task {"action": "logs", "task_id": "${id}"} to check on it, {"action": "stop"} to kill it.`;
|
|
73
|
+
}
|
|
74
|
+
logs(taskId, lineCount = 50) {
|
|
75
|
+
const task = this.tasks.get(taskId);
|
|
76
|
+
if (!task)
|
|
77
|
+
return this.unknownTask(taskId);
|
|
78
|
+
const tail = task.lines.slice(-Math.min(Math.max(lineCount, 1), RING_SIZE));
|
|
79
|
+
const header = `Task ${task.id} [${task.status}${task.exitCode !== null ? ` code ${task.exitCode}` : ""}] ${task.command}`;
|
|
80
|
+
return `${header}\n${tail.length ? tail.join("\n") : "(no output yet)"}`;
|
|
81
|
+
}
|
|
82
|
+
stop(taskId) {
|
|
83
|
+
const task = this.tasks.get(taskId);
|
|
84
|
+
if (!task)
|
|
85
|
+
return this.unknownTask(taskId);
|
|
86
|
+
if (task.status !== "running")
|
|
87
|
+
return `Task ${taskId} already ${task.status}.`;
|
|
88
|
+
task.status = "stopped";
|
|
89
|
+
(0, shell_1.killTree)(task.proc.pid);
|
|
90
|
+
return `Task ${taskId} stopped. (${task.command})`;
|
|
91
|
+
}
|
|
92
|
+
list() {
|
|
93
|
+
if (this.tasks.size === 0)
|
|
94
|
+
return "No background tasks. Start one with task {\"action\": \"start\", \"command\": \"...\"}.";
|
|
95
|
+
const rows = [...this.tasks.values()].map((t) => {
|
|
96
|
+
const age = Math.round((Date.now() - t.startedAt) / 1000);
|
|
97
|
+
const status = t.status === "running" ? "running" : `${t.status}${t.exitCode !== null ? `(${t.exitCode})` : ""}`;
|
|
98
|
+
return `${t.id} ${status} ${age}s ${t.command}`;
|
|
99
|
+
});
|
|
100
|
+
return "id status age command\n" + rows.join("\n");
|
|
101
|
+
}
|
|
102
|
+
hasRunning() {
|
|
103
|
+
return [...this.tasks.values()].some((t) => t.status === "running");
|
|
104
|
+
}
|
|
105
|
+
runningSummary() {
|
|
106
|
+
return [...this.tasks.values()]
|
|
107
|
+
.filter((t) => t.status === "running")
|
|
108
|
+
.map((t) => `${t.id}: ${t.command}`);
|
|
109
|
+
}
|
|
110
|
+
killAll() {
|
|
111
|
+
for (const t of this.tasks.values()) {
|
|
112
|
+
if (t.status === "running") {
|
|
113
|
+
t.status = "stopped";
|
|
114
|
+
try {
|
|
115
|
+
(0, shell_1.killTree)(t.proc.pid);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* ignore */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
unknownTask(taskId) {
|
|
124
|
+
const known = [...this.tasks.keys()].join(", ") || "none";
|
|
125
|
+
return `Error: no task with id "${taskId}". Known tasks: ${known}. Use task {"action": "list"} to see them.`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.TaskManager = TaskManager;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Line editor state: a buffer + cursor index, with logical-line navigation
|
|
3
|
+
// for multi-line input, and a wrap-aware layout used by the renderer.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.LineEditor = void 0;
|
|
6
|
+
exports.layoutBuffer = layoutBuffer;
|
|
7
|
+
class LineEditor {
|
|
8
|
+
buffer = "";
|
|
9
|
+
cursor = 0;
|
|
10
|
+
insert(text) {
|
|
11
|
+
this.buffer = this.buffer.slice(0, this.cursor) + text + this.buffer.slice(this.cursor);
|
|
12
|
+
this.cursor += text.length;
|
|
13
|
+
}
|
|
14
|
+
backspace() {
|
|
15
|
+
if (this.cursor === 0)
|
|
16
|
+
return;
|
|
17
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
18
|
+
this.cursor--;
|
|
19
|
+
}
|
|
20
|
+
del() {
|
|
21
|
+
if (this.cursor >= this.buffer.length)
|
|
22
|
+
return;
|
|
23
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
24
|
+
}
|
|
25
|
+
left() {
|
|
26
|
+
if (this.cursor > 0)
|
|
27
|
+
this.cursor--;
|
|
28
|
+
}
|
|
29
|
+
right() {
|
|
30
|
+
if (this.cursor < this.buffer.length)
|
|
31
|
+
this.cursor++;
|
|
32
|
+
}
|
|
33
|
+
lineStartIdx() {
|
|
34
|
+
return this.buffer.lastIndexOf("\n", this.cursor - 1) + 1;
|
|
35
|
+
}
|
|
36
|
+
lineEndIdx() {
|
|
37
|
+
const nl = this.buffer.indexOf("\n", this.cursor);
|
|
38
|
+
return nl < 0 ? this.buffer.length : nl;
|
|
39
|
+
}
|
|
40
|
+
home() {
|
|
41
|
+
this.cursor = this.lineStartIdx();
|
|
42
|
+
}
|
|
43
|
+
end() {
|
|
44
|
+
this.cursor = this.lineEndIdx();
|
|
45
|
+
}
|
|
46
|
+
/** Line the cursor is on (0-based) and column within it. */
|
|
47
|
+
lineCol() {
|
|
48
|
+
const before = this.buffer.slice(0, this.cursor);
|
|
49
|
+
const line = (before.match(/\n/g) ?? []).length;
|
|
50
|
+
const col = this.cursor - this.lineStartIdx();
|
|
51
|
+
return { line, col };
|
|
52
|
+
}
|
|
53
|
+
lineCount() {
|
|
54
|
+
return (this.buffer.match(/\n/g) ?? []).length + 1;
|
|
55
|
+
}
|
|
56
|
+
/** Move cursor up one logical line; returns false if already on the first. */
|
|
57
|
+
upLine() {
|
|
58
|
+
const { line, col } = this.lineCol();
|
|
59
|
+
if (line === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const lines = this.buffer.split("\n");
|
|
62
|
+
let idx = 0;
|
|
63
|
+
for (let i = 0; i < line - 1; i++)
|
|
64
|
+
idx += lines[i].length + 1;
|
|
65
|
+
this.cursor = idx + Math.min(col, lines[line - 1].length);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
downLine() {
|
|
69
|
+
const { line, col } = this.lineCol();
|
|
70
|
+
const lines = this.buffer.split("\n");
|
|
71
|
+
if (line >= lines.length - 1)
|
|
72
|
+
return false;
|
|
73
|
+
let idx = 0;
|
|
74
|
+
for (let i = 0; i <= line; i++)
|
|
75
|
+
idx += lines[i].length + 1;
|
|
76
|
+
this.cursor = idx + Math.min(col, lines[line + 1].length);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
killToLineStart() {
|
|
80
|
+
const start = this.lineStartIdx();
|
|
81
|
+
this.buffer = this.buffer.slice(0, start) + this.buffer.slice(this.cursor);
|
|
82
|
+
this.cursor = start;
|
|
83
|
+
}
|
|
84
|
+
deleteWordBack() {
|
|
85
|
+
if (this.cursor === 0)
|
|
86
|
+
return;
|
|
87
|
+
let i = this.cursor;
|
|
88
|
+
while (i > 0 && /\s/.test(this.buffer[i - 1]))
|
|
89
|
+
i--;
|
|
90
|
+
while (i > 0 && !/\s/.test(this.buffer[i - 1]))
|
|
91
|
+
i--;
|
|
92
|
+
this.buffer = this.buffer.slice(0, i) + this.buffer.slice(this.cursor);
|
|
93
|
+
this.cursor = i;
|
|
94
|
+
}
|
|
95
|
+
clear() {
|
|
96
|
+
this.buffer = "";
|
|
97
|
+
this.cursor = 0;
|
|
98
|
+
}
|
|
99
|
+
set(text) {
|
|
100
|
+
this.buffer = text;
|
|
101
|
+
this.cursor = text.length;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.LineEditor = LineEditor;
|
|
105
|
+
/** Wrap the buffer into display rows of at most `width` chars, tracking where
|
|
106
|
+
* the cursor lands. Explicit newlines always break a row. */
|
|
107
|
+
function layoutBuffer(buffer, cursor, width) {
|
|
108
|
+
const rows = [];
|
|
109
|
+
let row = "";
|
|
110
|
+
let curRow = 0;
|
|
111
|
+
let curCol = 0;
|
|
112
|
+
for (let i = 0; i <= buffer.length; i++) {
|
|
113
|
+
if (i === cursor) {
|
|
114
|
+
curRow = rows.length;
|
|
115
|
+
curCol = row.length;
|
|
116
|
+
}
|
|
117
|
+
if (i === buffer.length)
|
|
118
|
+
break;
|
|
119
|
+
const ch = buffer[i];
|
|
120
|
+
if (ch === "\n") {
|
|
121
|
+
rows.push(row);
|
|
122
|
+
row = "";
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
row += ch;
|
|
126
|
+
if (row.length >= width) {
|
|
127
|
+
rows.push(row);
|
|
128
|
+
row = "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
rows.push(row);
|
|
133
|
+
return { rows, curRow, curCol };
|
|
134
|
+
}
|