smolcoder-plus 1.0.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 +102 -0
- package/dist/agent.js +748 -0
- package/dist/attachments.js +158 -0
- package/dist/config.js +87 -0
- package/dist/context.js +498 -0
- package/dist/detect.js +474 -0
- package/dist/events.js +24 -0
- package/dist/history.js +9 -0
- package/dist/hosts.js +107 -0
- package/dist/index.js +391 -0
- package/dist/logo.js +48 -0
- package/dist/netscan.js +159 -0
- package/dist/network.js +193 -0
- package/dist/plan.js +102 -0
- package/dist/prompt.js +84 -0
- package/dist/providers/lmstudio.js +347 -0
- package/dist/providers/ollama.js +269 -0
- package/dist/providers/scheduler.js +57 -0
- package/dist/providers/transport.js +86 -0
- package/dist/providers/types.js +62 -0
- package/dist/sandbox.js +207 -0
- package/dist/session.js +639 -0
- package/dist/tools/check.js +193 -0
- package/dist/tools/fs-tools.js +431 -0
- package/dist/tools/index.js +260 -0
- package/dist/tools/search-worker.js +34 -0
- package/dist/tools/shell.js +186 -0
- package/dist/tools/tasks.js +147 -0
- package/dist/tools/web-search.js +155 -0
- package/dist/tui/editor.js +134 -0
- package/dist/tui/keys.js +145 -0
- package/dist/tui/tui.js +723 -0
- package/dist/ui.js +226 -0
- package/dist/util.js +91 -0
- package/dist/verification.js +71 -0
- package/dist/web/channel.js +260 -0
- package/dist/web/client.js +1010 -0
- package/dist/web/hub.js +952 -0
- package/dist/web/page.js +87 -0
- package/dist/web/store.js +199 -0
- package/dist/web/styles.js +333 -0
- package/dist/web/terminal.js +190 -0
- package/package.json +49 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Embedded terminal for the web panel, without a PTY (that would need a native
|
|
3
|
+
// dependency). A persistent shell reads commands from a pipe: state such as
|
|
4
|
+
// cwd and env carries across commands, output streams back, and a sentinel
|
|
5
|
+
// after every command reports its exit code and the shell's cwd. No TTY means
|
|
6
|
+
// no colors from most tools and no interactive programs — the panel says so.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.Terminal = void 0;
|
|
9
|
+
exports.toOsPath = toOsPath;
|
|
10
|
+
exports.stripControl = stripControl;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const shell_1 = require("../tools/shell");
|
|
13
|
+
const RS = "\x1e"; // record separator — never appears in normal output
|
|
14
|
+
const SENTINEL = /\x1e(-?\d+)\x1e([^\x1e]*)\x1e\r?\n?/;
|
|
15
|
+
const BUFFER_CAP = 200_000;
|
|
16
|
+
const HOLD_CAP = 600; // a partial sentinel is held back at most this long
|
|
17
|
+
class Terminal {
|
|
18
|
+
id;
|
|
19
|
+
hooks;
|
|
20
|
+
cwd;
|
|
21
|
+
/** Recent output for replay when a page (re)connects. */
|
|
22
|
+
buffer = "";
|
|
23
|
+
closed = false;
|
|
24
|
+
shell;
|
|
25
|
+
proc = null;
|
|
26
|
+
acc = "";
|
|
27
|
+
kind;
|
|
28
|
+
interrupting = false;
|
|
29
|
+
spawnedAt = 0;
|
|
30
|
+
quickExits = 0;
|
|
31
|
+
constructor(id, cwd, hooks) {
|
|
32
|
+
this.id = id;
|
|
33
|
+
this.hooks = hooks;
|
|
34
|
+
this.cwd = cwd;
|
|
35
|
+
this.shell = (0, shell_1.pickShell)();
|
|
36
|
+
this.kind = /powershell|pwsh/i.test(this.shell.exe) ? "powershell" : "posix";
|
|
37
|
+
this.emit(`\x1b[2m${this.shell.label} · no TTY: interactive programs will not work · ctrl+c interrupts\x1b[0m\n`);
|
|
38
|
+
this.spawn();
|
|
39
|
+
}
|
|
40
|
+
spawn() {
|
|
41
|
+
const args = this.kind === "posix" ? ["-l"] : ["-NoProfile", "-NonInteractive", "-Command", "-"];
|
|
42
|
+
this.spawnedAt = Date.now();
|
|
43
|
+
let proc;
|
|
44
|
+
try {
|
|
45
|
+
proc = (0, child_process_1.spawn)(this.shell.exe, args, {
|
|
46
|
+
cwd: this.cwd,
|
|
47
|
+
env: { ...process.env, TERM: "dumb" },
|
|
48
|
+
detached: process.platform !== "win32",
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
this.emit(`[could not start ${this.shell.label}: ${err?.message ?? err}]\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.proc = proc;
|
|
58
|
+
proc.stdout?.on("data", (d) => this.ingest(d.toString("utf8")));
|
|
59
|
+
proc.stderr?.on("data", (d) => this.ingest(d.toString("utf8")));
|
|
60
|
+
proc.on("error", (err) => this.emit(`[could not start ${this.shell.label}: ${err.message}]\n`));
|
|
61
|
+
proc.on("close", (code) => {
|
|
62
|
+
if (this.proc !== proc)
|
|
63
|
+
return;
|
|
64
|
+
this.proc = null;
|
|
65
|
+
if (this.acc) {
|
|
66
|
+
this.emit(this.acc);
|
|
67
|
+
this.acc = "";
|
|
68
|
+
}
|
|
69
|
+
if (this.closed)
|
|
70
|
+
return;
|
|
71
|
+
if (this.interrupting) {
|
|
72
|
+
this.interrupting = false;
|
|
73
|
+
this.emit("\n\x1b[33m[interrupted]\x1b[0m\n");
|
|
74
|
+
this.spawn();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// A shell that dies instantly, twice, is not going to work — stop.
|
|
78
|
+
const quick = Date.now() - this.spawnedAt < 1500;
|
|
79
|
+
this.quickExits = quick ? this.quickExits + 1 : 0;
|
|
80
|
+
if (this.quickExits >= 2) {
|
|
81
|
+
this.emit(`\n\x1b[31m[${this.shell.label} keeps exiting (code ${code}) — close this tab and open a new terminal]\x1b[0m\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.emit(`\n\x1b[2m[shell exited with code ${code} — starting a new one]\x1b[0m\n`);
|
|
85
|
+
this.spawn();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Run one line. Echoed into the stream first, so every connected page (and
|
|
89
|
+
* the replay) shows the command with its output. */
|
|
90
|
+
write(line) {
|
|
91
|
+
if (this.closed)
|
|
92
|
+
return;
|
|
93
|
+
const cmd = line.replace(/\r?\n$/, "");
|
|
94
|
+
this.emit(`\x1b[36m❯\x1b[0m ${cmd}\n`);
|
|
95
|
+
if (!this.proc?.stdin?.writable) {
|
|
96
|
+
this.emit("\x1b[31m[no shell running]\x1b[0m\n");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const payload = this.kind === "posix"
|
|
100
|
+
? // The group redirect keeps a stdin-hungry command (cat, ssh) from
|
|
101
|
+
// eating the next command off our pipe; 2>&1 keeps output ordered.
|
|
102
|
+
`{\n${cmd}\n} </dev/null 2>&1\nprintf '${RS}%s${RS}%s${RS}\\n' "$?" "$PWD"\n`
|
|
103
|
+
: `${cmd}\nWrite-Output ([string][char]0x1e + $(if ($?) {0} else {1}) + [char]0x1e + (Get-Location).Path + [char]0x1e)\n`;
|
|
104
|
+
try {
|
|
105
|
+
this.proc.stdin.write(payload);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.emit(`[could not write to the shell: ${err?.message ?? err}]\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** ctrl+c: kill the shell (and whatever it is running), start a fresh one
|
|
112
|
+
* in the last known cwd. */
|
|
113
|
+
interrupt() {
|
|
114
|
+
if (this.closed)
|
|
115
|
+
return;
|
|
116
|
+
if (!this.proc) {
|
|
117
|
+
this.spawn();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.interrupting = true;
|
|
121
|
+
if (this.proc.pid)
|
|
122
|
+
(0, shell_1.killTree)(this.proc.pid);
|
|
123
|
+
}
|
|
124
|
+
close() {
|
|
125
|
+
this.closed = true;
|
|
126
|
+
if (this.proc?.pid)
|
|
127
|
+
(0, shell_1.killTree)(this.proc.pid);
|
|
128
|
+
this.proc = null;
|
|
129
|
+
}
|
|
130
|
+
ingest(text) {
|
|
131
|
+
this.acc += text;
|
|
132
|
+
for (;;) {
|
|
133
|
+
const m = SENTINEL.exec(this.acc);
|
|
134
|
+
if (!m)
|
|
135
|
+
break;
|
|
136
|
+
const before = this.acc.slice(0, m.index);
|
|
137
|
+
if (before)
|
|
138
|
+
this.emit(before);
|
|
139
|
+
this.acc = this.acc.slice(m.index + m[0].length);
|
|
140
|
+
const cwd = toOsPath(m[2].trim()) || this.cwd;
|
|
141
|
+
this.cwd = cwd;
|
|
142
|
+
this.hooks.done(Number(m[1]), cwd);
|
|
143
|
+
}
|
|
144
|
+
// Hold back a partial sentinel that may complete with the next chunk,
|
|
145
|
+
// but never for long — a stray RS in real output must not stall the view.
|
|
146
|
+
const i = this.acc.indexOf(RS);
|
|
147
|
+
if (i < 0) {
|
|
148
|
+
if (this.acc)
|
|
149
|
+
this.emit(this.acc);
|
|
150
|
+
this.acc = "";
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
if (i > 0)
|
|
154
|
+
this.emit(this.acc.slice(0, i));
|
|
155
|
+
this.acc = this.acc.slice(i);
|
|
156
|
+
if (this.acc.length > HOLD_CAP) {
|
|
157
|
+
this.emit(this.acc);
|
|
158
|
+
this.acc = "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
emit(text) {
|
|
163
|
+
text = stripControl(text);
|
|
164
|
+
if (!text)
|
|
165
|
+
return;
|
|
166
|
+
this.buffer += text;
|
|
167
|
+
if (this.buffer.length > BUFFER_CAP)
|
|
168
|
+
this.buffer = this.buffer.slice(-Math.floor(BUFFER_CAP * 0.8));
|
|
169
|
+
this.hooks.output(text);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.Terminal = Terminal;
|
|
173
|
+
/** Git Bash reports /c/x for C:\x; spawn() needs the Windows form. */
|
|
174
|
+
function toOsPath(p) {
|
|
175
|
+
if (process.platform === "win32") {
|
|
176
|
+
const m = /^\/([a-zA-Z])(\/.*)?$/.exec(p);
|
|
177
|
+
if (m)
|
|
178
|
+
return `${m[1].toUpperCase()}:${(m[2] ?? "/").replace(/\//g, "\\")}`;
|
|
179
|
+
}
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
/** Keep SGR color codes (the page renders them) and \r (progress bars), drop
|
|
183
|
+
* every other escape sequence: cursor moves, screen clears, OSC titles. */
|
|
184
|
+
function stripControl(s) {
|
|
185
|
+
return s
|
|
186
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
187
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-ln-~]/g, "")
|
|
188
|
+
.replace(/\x1b[^[\]]/g, "")
|
|
189
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f]/g, "");
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smolcoder-plus",
|
|
3
|
+
"version": "1.0.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
|
+
"smolcoder-plus": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
36
|
+
"build": "npm run clean && tsc",
|
|
37
|
+
"postbuild": "node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
38
|
+
"dev": "tsc -w",
|
|
39
|
+
"prepublishOnly": "npm test",
|
|
40
|
+
"test": "npm run build && node scripts/test.cjs"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.14.0",
|
|
47
|
+
"typescript": "^5.5.0"
|
|
48
|
+
}
|
|
49
|
+
}
|