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,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// WebUI: the --web session surface. Same AgentUI/SessionUI contract as the
|
|
3
|
+
// terminal Tui, but events stream to a browser page over SSE and input comes
|
|
4
|
+
// back over JSON POSTs. Localhost-only, guarded by a random URL token so a
|
|
5
|
+
// stray local process or webpage can't drive the agent.
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.WebUI = void 0;
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const http = __importStar(require("http"));
|
|
43
|
+
const ui_1 = require("../ui");
|
|
44
|
+
const page_1 = require("./page");
|
|
45
|
+
class WebUI {
|
|
46
|
+
port;
|
|
47
|
+
slashCommands = [];
|
|
48
|
+
getStatus = () => "";
|
|
49
|
+
/** Structured snapshot for the page's status bar; assigned by index.ts. */
|
|
50
|
+
getState = () => ({});
|
|
51
|
+
hintLeft = "";
|
|
52
|
+
onModeCycle = null;
|
|
53
|
+
onCancel = null;
|
|
54
|
+
onExit = null;
|
|
55
|
+
authToken = crypto.randomBytes(9).toString("base64url");
|
|
56
|
+
server = null;
|
|
57
|
+
clients = new Set();
|
|
58
|
+
replay = [];
|
|
59
|
+
pendingInput = null;
|
|
60
|
+
inputQueue = [];
|
|
61
|
+
pending = new Map();
|
|
62
|
+
askId = 0;
|
|
63
|
+
constructor(port) {
|
|
64
|
+
this.port = port;
|
|
65
|
+
}
|
|
66
|
+
url() {
|
|
67
|
+
return `http://127.0.0.1:${this.port}/?k=${this.authToken}`;
|
|
68
|
+
}
|
|
69
|
+
start() {
|
|
70
|
+
this.server = http.createServer((req, res) => this.route(req, res));
|
|
71
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
72
|
+
console.log(`\n smolcoder web UI: ${this.url()}\n`);
|
|
73
|
+
});
|
|
74
|
+
process.on("SIGINT", () => this.onExit?.());
|
|
75
|
+
}
|
|
76
|
+
close() {
|
|
77
|
+
for (const c of this.clients)
|
|
78
|
+
c.end();
|
|
79
|
+
this.server?.close();
|
|
80
|
+
}
|
|
81
|
+
// ---- http ----------------------------------------------------------------
|
|
82
|
+
route(req, res) {
|
|
83
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.port}`);
|
|
84
|
+
const origin = req.headers.origin;
|
|
85
|
+
const sameOrigin = !origin || origin === `http://127.0.0.1:${this.port}` || origin === `http://localhost:${this.port}`;
|
|
86
|
+
if (url.searchParams.get("k") !== this.authToken || !sameOrigin) {
|
|
87
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
88
|
+
res.end("forbidden — open smolcoder's printed URL (it includes the session key)");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
92
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
93
|
+
res.end(page_1.PAGE_HTML);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (req.method === "GET" && url.pathname === "/events") {
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
"content-type": "text/event-stream",
|
|
99
|
+
"cache-control": "no-cache",
|
|
100
|
+
connection: "keep-alive",
|
|
101
|
+
});
|
|
102
|
+
for (const line of this.replay)
|
|
103
|
+
res.write(line);
|
|
104
|
+
this.pushStateTo(res);
|
|
105
|
+
this.clients.add(res);
|
|
106
|
+
req.on("close", () => this.clients.delete(res));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (req.method === "POST") {
|
|
110
|
+
let body = "";
|
|
111
|
+
req.on("data", (d) => {
|
|
112
|
+
body += d;
|
|
113
|
+
if (body.length > 1_000_000)
|
|
114
|
+
req.destroy();
|
|
115
|
+
});
|
|
116
|
+
req.on("end", () => {
|
|
117
|
+
let data = {};
|
|
118
|
+
try {
|
|
119
|
+
data = body ? JSON.parse(body) : {};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
this.handlePost(url.pathname, data);
|
|
125
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
126
|
+
res.end("{}");
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
res.writeHead(404);
|
|
131
|
+
res.end();
|
|
132
|
+
}
|
|
133
|
+
handlePost(path, data) {
|
|
134
|
+
switch (path) {
|
|
135
|
+
case "/msg": {
|
|
136
|
+
const text = String(data.text ?? "").trim();
|
|
137
|
+
if (!text)
|
|
138
|
+
return;
|
|
139
|
+
if (this.pendingInput) {
|
|
140
|
+
const resolve = this.pendingInput;
|
|
141
|
+
this.pendingInput = null;
|
|
142
|
+
this.broadcast({ t: "user", s: text });
|
|
143
|
+
resolve(text);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.inputQueue.push(text);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "/confirm":
|
|
151
|
+
case "/select": {
|
|
152
|
+
const resolve = this.pending.get(Number(data.id));
|
|
153
|
+
if (resolve) {
|
|
154
|
+
this.pending.delete(Number(data.id));
|
|
155
|
+
resolve(path === "/confirm" ? data.answer : data.index);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "/cycle":
|
|
160
|
+
this.onModeCycle?.();
|
|
161
|
+
this.refresh();
|
|
162
|
+
break;
|
|
163
|
+
case "/cancel":
|
|
164
|
+
this.onCancel?.();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ---- event plumbing ------------------------------------------------------
|
|
169
|
+
broadcast(ev) {
|
|
170
|
+
const line = `data: ${JSON.stringify(ev)}\n\n`;
|
|
171
|
+
if (ev.t !== "busy" && ev.t !== "state") {
|
|
172
|
+
this.replay.push(line);
|
|
173
|
+
if (this.replay.length > 2000)
|
|
174
|
+
this.replay.shift();
|
|
175
|
+
}
|
|
176
|
+
for (const c of this.clients)
|
|
177
|
+
c.write(line);
|
|
178
|
+
}
|
|
179
|
+
pushStateTo(res) {
|
|
180
|
+
const line = `data: ${JSON.stringify({ t: "state", s: this.getState() })}\n\n`;
|
|
181
|
+
if (res)
|
|
182
|
+
res.write(line);
|
|
183
|
+
else
|
|
184
|
+
for (const c of this.clients)
|
|
185
|
+
c.write(line);
|
|
186
|
+
}
|
|
187
|
+
refresh() {
|
|
188
|
+
this.pushStateTo();
|
|
189
|
+
}
|
|
190
|
+
// ---- SessionUI -----------------------------------------------------------
|
|
191
|
+
readInput() {
|
|
192
|
+
this.pushStateTo();
|
|
193
|
+
if (this.inputQueue.length) {
|
|
194
|
+
const text = this.inputQueue.shift();
|
|
195
|
+
this.broadcast({ t: "user", s: text });
|
|
196
|
+
return Promise.resolve(text);
|
|
197
|
+
}
|
|
198
|
+
return new Promise((resolve) => (this.pendingInput = resolve));
|
|
199
|
+
}
|
|
200
|
+
select(title, options) {
|
|
201
|
+
const id = ++this.askId;
|
|
202
|
+
this.broadcast({ t: "select", id, title, options });
|
|
203
|
+
return new Promise((resolve) => this.pending.set(id, (i) => resolve(typeof i === "number" ? i : null)));
|
|
204
|
+
}
|
|
205
|
+
confirmCommand(command, reason) {
|
|
206
|
+
const id = ++this.askId;
|
|
207
|
+
this.broadcast({ t: "confirm", id, command, reason });
|
|
208
|
+
return new Promise((resolve) => this.pending.set(id, (a) => resolve(a === "always" ? "always" : a === "yes" ? "yes" : "no")));
|
|
209
|
+
}
|
|
210
|
+
// ---- AgentUI -------------------------------------------------------------
|
|
211
|
+
token(text) {
|
|
212
|
+
this.broadcast({ t: "token", s: text });
|
|
213
|
+
}
|
|
214
|
+
thinking(text) {
|
|
215
|
+
this.broadcast({ t: "thinking", s: text });
|
|
216
|
+
}
|
|
217
|
+
toolCall(name, args) {
|
|
218
|
+
this.broadcast({ t: "tool", name, summary: (0, ui_1.summarizeArgs)(name, args) });
|
|
219
|
+
}
|
|
220
|
+
toolResult(result) {
|
|
221
|
+
const lines = result.split("\n");
|
|
222
|
+
const first = lines[0] ?? "";
|
|
223
|
+
this.broadcast({
|
|
224
|
+
t: "result",
|
|
225
|
+
line: first.slice(0, 160),
|
|
226
|
+
err: first.startsWith("Error"),
|
|
227
|
+
extra: lines.length > 1 ? lines.length - 1 : 0,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
planUpdated(plan) {
|
|
231
|
+
this.broadcast({ t: "plan", steps: plan.steps, current: plan.currentIndex });
|
|
232
|
+
this.pushStateTo();
|
|
233
|
+
}
|
|
234
|
+
println(s = "") {
|
|
235
|
+
if (s.trim())
|
|
236
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
237
|
+
}
|
|
238
|
+
status(s) {
|
|
239
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
240
|
+
}
|
|
241
|
+
warn(s) {
|
|
242
|
+
this.broadcast({ t: "line", kind: "warn", s: stripAnsi(s) });
|
|
243
|
+
}
|
|
244
|
+
error(s) {
|
|
245
|
+
this.broadcast({ t: "line", kind: "error", s: stripAnsi(s) });
|
|
246
|
+
}
|
|
247
|
+
turnEnd(label) {
|
|
248
|
+
this.broadcast({ t: "turnend", label });
|
|
249
|
+
this.pushStateTo();
|
|
250
|
+
}
|
|
251
|
+
startSpinner(label) {
|
|
252
|
+
this.broadcast({ t: "busy", label });
|
|
253
|
+
}
|
|
254
|
+
stopSpinner() {
|
|
255
|
+
this.broadcast({ t: "busy", label: null });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
exports.WebUI = WebUI;
|
|
259
|
+
function stripAnsi(s) {
|
|
260
|
+
// eslint-disable-next-line no-control-regex
|
|
261
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
262
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smolcoder",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A smol, zero-config CLI coding agent for local models (Ollama & LM Studio). It just works.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"coding-agent",
|
|
8
|
+
"agent",
|
|
9
|
+
"ollama",
|
|
10
|
+
"lm-studio",
|
|
11
|
+
"local-llm",
|
|
12
|
+
"local-models",
|
|
13
|
+
"smol",
|
|
14
|
+
"ai"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Leon van Zyl",
|
|
18
|
+
"homepage": "https://github.com/leonvanzyl/smolcoder#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/leonvanzyl/smolcoder.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/leonvanzyl/smolcoder/issues"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"smol": "dist/index.js",
|
|
28
|
+
"smolcoder": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"dev": "tsc -w",
|
|
38
|
+
"prepublishOnly": "tsc && node --test",
|
|
39
|
+
"test": "tsc && node --test"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.14.0",
|
|
46
|
+
"typescript": "^5.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|