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,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withDeadline = withDeadline;
|
|
4
|
+
exports.responseLines = responseLines;
|
|
5
|
+
exports.streamJson = streamJson;
|
|
6
|
+
exports.abortableDelay = abortableDelay;
|
|
7
|
+
/** Fail visibly when a local server accepts a request but stops producing data. */
|
|
8
|
+
async function withDeadline(opts, run) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const cancel = () => controller.abort(opts.signal?.reason);
|
|
11
|
+
let idle;
|
|
12
|
+
const activity = () => {
|
|
13
|
+
clearTimeout(idle);
|
|
14
|
+
idle = setTimeout(() => controller.abort(new Error("Backend timed out waiting for data. Check the model server and available memory.")), opts.idleTimeoutMs ?? 180_000);
|
|
15
|
+
};
|
|
16
|
+
const total = setTimeout(() => controller.abort(new Error("Backend request timed out. The model did not finish within the request deadline.")), opts.timeoutMs ?? 900_000);
|
|
17
|
+
opts.signal?.addEventListener("abort", cancel, { once: true });
|
|
18
|
+
if (opts.signal?.aborted)
|
|
19
|
+
cancel();
|
|
20
|
+
activity();
|
|
21
|
+
try {
|
|
22
|
+
return await run(controller.signal, activity);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (controller.signal.aborted)
|
|
26
|
+
throw controller.signal.reason;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
clearTimeout(idle);
|
|
31
|
+
clearTimeout(total);
|
|
32
|
+
opts.signal?.removeEventListener("abort", cancel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Cancel reads and release the connection even on malformed stream data. */
|
|
36
|
+
async function* responseLines(res, activity) {
|
|
37
|
+
if (!res.body)
|
|
38
|
+
throw new Error("Backend returned an empty response body");
|
|
39
|
+
const reader = res.body.getReader();
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = "";
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done)
|
|
46
|
+
break;
|
|
47
|
+
activity();
|
|
48
|
+
buffer += decoder.decode(value, { stream: true });
|
|
49
|
+
if (buffer.length > 4_000_000)
|
|
50
|
+
throw new Error("Backend stream frame exceeded 4 MB");
|
|
51
|
+
let nl;
|
|
52
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
53
|
+
yield buffer.slice(0, nl).trim();
|
|
54
|
+
buffer = buffer.slice(nl + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
buffer += decoder.decode();
|
|
58
|
+
if (buffer.trim())
|
|
59
|
+
yield buffer.trim();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await reader.cancel().catch(() => { });
|
|
63
|
+
reader.releaseLock();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function streamJson(line) {
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = JSON.parse(line);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error("Backend stream contained malformed JSON; the response was discarded");
|
|
73
|
+
}
|
|
74
|
+
if (data?.error)
|
|
75
|
+
throw new Error(`Backend stream error: ${typeof data.error === "string" ? data.error : data.error.message ?? JSON.stringify(data.error)}`);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
function abortableDelay(ms, signal) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const abort = () => { clearTimeout(timer); signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
81
|
+
const timer = setTimeout(() => { signal.removeEventListener("abort", abort); resolve(); }, ms);
|
|
82
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
83
|
+
if (signal.aborted)
|
|
84
|
+
abort();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_OUTPUT_TOKENS = exports.EFFORT_RANK = void 0;
|
|
4
|
+
exports.nextCallId = nextCallId;
|
|
5
|
+
exports.parseArgs = parseArgs;
|
|
6
|
+
exports.estimateReplayTokens = estimateReplayTokens;
|
|
7
|
+
exports.lastUserIndex = lastUserIndex;
|
|
8
|
+
// One internal message/tool shape; each provider adapts it to its wire format.
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
/** Rank of every reasoning level any backend knows about, used to map a
|
|
11
|
+
* requested effort onto whatever a specific model actually supports. */
|
|
12
|
+
exports.EFFORT_RANK = {
|
|
13
|
+
none: 0,
|
|
14
|
+
off: 0,
|
|
15
|
+
minimal: 1,
|
|
16
|
+
low: 2,
|
|
17
|
+
medium: 3,
|
|
18
|
+
high: 4,
|
|
19
|
+
xhigh: 5,
|
|
20
|
+
on: 3,
|
|
21
|
+
};
|
|
22
|
+
exports.MAX_OUTPUT_TOKENS = 2048;
|
|
23
|
+
let callCounter = 0;
|
|
24
|
+
const callPrefix = (0, crypto_1.randomBytes)(6).toString("hex");
|
|
25
|
+
function nextCallId() {
|
|
26
|
+
return `call_${callPrefix}_${++callCounter}`;
|
|
27
|
+
}
|
|
28
|
+
function parseArgs(raw) {
|
|
29
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw))
|
|
30
|
+
return { args: raw };
|
|
31
|
+
if (typeof raw === "string") {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
35
|
+
return { args: parsed, rawArgs: raw };
|
|
36
|
+
return { args: {}, rawArgs: raw, parseError: "arguments were not a JSON object" };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { args: {}, rawArgs: raw, parseError: `invalid JSON: ${e.message}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return raw === undefined ? { args: {} } : { args: {}, parseError: "arguments were not a JSON object" };
|
|
43
|
+
}
|
|
44
|
+
/** Tokens the visible reply + tool-call JSON will occupy when replayed in the
|
|
45
|
+
* next prompt (~4 chars/token, corrected by real usage on the next response). */
|
|
46
|
+
function estimateReplayTokens(content, toolCalls) {
|
|
47
|
+
let chars = (content ?? "").length;
|
|
48
|
+
for (const tc of toolCalls)
|
|
49
|
+
chars += tc.name.length + (tc.rawArgs ?? JSON.stringify(tc.args)).length + 12;
|
|
50
|
+
return Math.ceil(chars / 4);
|
|
51
|
+
}
|
|
52
|
+
/** Index of the most recent plain user message (the current turn's start).
|
|
53
|
+
* Reasoning traces from assistant messages BEFORE it are dropped from the
|
|
54
|
+
* wire — that is what the Qwen3-family chat templates do themselves, and it
|
|
55
|
+
* is the biggest single saving on a long tool loop with a thinking model. */
|
|
56
|
+
function lastUserIndex(messages) {
|
|
57
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
58
|
+
if (messages[i].role === "user" && !messages[i].compactNote && !messages[i].historyNote)
|
|
59
|
+
return i;
|
|
60
|
+
}
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Workspace containment for file tools. Every path a tool touches must resolve
|
|
3
|
+
// inside the workspace root — including through symlinks, which is why we
|
|
4
|
+
// realpath the deepest EXISTING ancestor before comparing.
|
|
5
|
+
//
|
|
6
|
+
// Commands (run_command / task) cannot be contained this way. Edit mode scans
|
|
7
|
+
// the command text instead (commandEscapesWorkspace, below) and asks the user
|
|
8
|
+
// only when something reaches outside; bypass mode never asks.
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.SandboxError = void 0;
|
|
44
|
+
exports.resolveInWorkspace = resolveInWorkspace;
|
|
45
|
+
exports.relPath = relPath;
|
|
46
|
+
exports.commandEscapesWorkspace = commandEscapesWorkspace;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
class SandboxError extends Error {
|
|
50
|
+
}
|
|
51
|
+
exports.SandboxError = SandboxError;
|
|
52
|
+
const isWin = process.platform === "win32";
|
|
53
|
+
function normalizeForCompare(p) {
|
|
54
|
+
return isWin ? p.toLowerCase() : p;
|
|
55
|
+
}
|
|
56
|
+
/** The deepest ancestor of `abs` that exists (the path itself when it does).
|
|
57
|
+
* Throws when a path cannot be inspected for a reason other than not existing. */
|
|
58
|
+
function deepestExisting(abs) {
|
|
59
|
+
let existing = abs;
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
fs.lstatSync(existing);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR")
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
const parent = path.dirname(existing);
|
|
70
|
+
if (parent === existing)
|
|
71
|
+
break;
|
|
72
|
+
existing = parent;
|
|
73
|
+
}
|
|
74
|
+
return existing;
|
|
75
|
+
}
|
|
76
|
+
function resolveInWorkspace(root, userPath) {
|
|
77
|
+
if (typeof userPath !== "string" || userPath.trim() === "") {
|
|
78
|
+
throw new SandboxError("path is required (relative to the workspace, e.g. \"src/app.js\").");
|
|
79
|
+
}
|
|
80
|
+
const abs = path.resolve(root, userPath);
|
|
81
|
+
// realpath the deepest existing ancestor to defeat symlink escapes
|
|
82
|
+
let existing;
|
|
83
|
+
try {
|
|
84
|
+
existing = deepestExisting(abs);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new SandboxError(`cannot inspect path "${userPath}".`);
|
|
88
|
+
}
|
|
89
|
+
let realExisting;
|
|
90
|
+
let realRoot;
|
|
91
|
+
try {
|
|
92
|
+
realExisting = fs.realpathSync(existing);
|
|
93
|
+
realRoot = fs.realpathSync(root);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new SandboxError(`cannot resolve path "${userPath}".`);
|
|
97
|
+
}
|
|
98
|
+
const rel = path.relative(normalizeForCompare(realRoot), normalizeForCompare(realExisting));
|
|
99
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
100
|
+
throw new SandboxError(`"${userPath}" is outside the workspace. Only files inside ${root} can be accessed. Use a relative path like "src/app.js".`);
|
|
101
|
+
}
|
|
102
|
+
return abs;
|
|
103
|
+
}
|
|
104
|
+
function relPath(root, abs) {
|
|
105
|
+
return path.relative(root, abs).split(path.sep).join("/") || ".";
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Command containment (edit mode). A shell command cannot be sandboxed the way
|
|
109
|
+
// a file path can, so this is a best-effort scan of the command text for
|
|
110
|
+
// anything that reaches outside the workspace: absolute paths that resolve
|
|
111
|
+
// elsewhere, `..` climbing past the root, home-directory shortcuts, temp-dir
|
|
112
|
+
// variables, and package-manager global installs. A clean in-tree command
|
|
113
|
+
// (`npm install`, `node script.mjs`, `a && b`) runs without asking; a flagged
|
|
114
|
+
// one goes to the y/n prompt with the reason shown. False positives cost one
|
|
115
|
+
// prompt, false negatives are the accepted limit of a text scan.
|
|
116
|
+
const HOME_TOKENS = /^(~|\$HOME|\$\{HOME\}|%USERPROFILE%|%HOMEPATH%)([\\/]|$)/i;
|
|
117
|
+
const TEMP_TOKENS = /^(\$TMPDIR|\$\{TMPDIR\}|\$TEMP|\$TMP|%TEMP%|%TMP%|\$\{TEMP\}|\$\{TMP\})([\\/]|$)/i;
|
|
118
|
+
const GLOBAL_INSTALL = /\b(npm|pnpm|yarn|bun)\b[^;&|]*\s(-g|--global|global)(\s|$)/;
|
|
119
|
+
/** MSYS roots Git Bash maps to real places; any other one-segment `/word` on
|
|
120
|
+
* Windows is a command switch (`taskkill /pid`, `dir /s`), not a path. */
|
|
121
|
+
const MSYS_ROOTS = /^\/(tmp|usr|etc|bin|home|mnt|dev|proc|var|opt|root)(\/|$)/i;
|
|
122
|
+
/** Split a command into candidate path tokens: whitespace-separated words with
|
|
123
|
+
* quotes stripped, the value side of `--flag=value` and `VAR=value`, and the
|
|
124
|
+
* target of `>`/`<` redirections written without a space. */
|
|
125
|
+
function pathCandidates(command) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const raw of command.split(/\s+/)) {
|
|
128
|
+
if (!raw)
|
|
129
|
+
continue;
|
|
130
|
+
let tok = raw.replace(/^[<>]+|^[\d]?[<>]+&?/, "").replace(/^["'`]+|["'`,;)]+$/g, "");
|
|
131
|
+
if (!tok)
|
|
132
|
+
continue;
|
|
133
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(tok))
|
|
134
|
+
continue; // URLs
|
|
135
|
+
const eq = tok.indexOf("=");
|
|
136
|
+
if (eq > 0 && /^[-\w.]+$/.test(tok.slice(0, eq)))
|
|
137
|
+
tok = tok.slice(eq + 1);
|
|
138
|
+
if (tok)
|
|
139
|
+
out.push(tok);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** Git Bash writes C:\x as /c/x; map that back so it compares like a Windows
|
|
144
|
+
* path. A bare `/f` with nothing after it is left alone — that is a command
|
|
145
|
+
* switch far more often than the root of drive F:. */
|
|
146
|
+
function fromMsys(tok) {
|
|
147
|
+
const m = /^\/([a-zA-Z])(\/.*)$/.exec(tok);
|
|
148
|
+
if (m && isWin)
|
|
149
|
+
return `${m[1].toUpperCase()}:${m[2].replace(/\//g, "\\")}`;
|
|
150
|
+
return tok;
|
|
151
|
+
}
|
|
152
|
+
function isAbsoluteLike(tok) {
|
|
153
|
+
if (/^[a-zA-Z]:[\\/]/.test(tok))
|
|
154
|
+
return true; // C:\ or C:/
|
|
155
|
+
if (/^\\\\/.test(tok))
|
|
156
|
+
return true; // UNC
|
|
157
|
+
return tok.startsWith("/");
|
|
158
|
+
}
|
|
159
|
+
/** `abs` with symlinks resolved as far as the path exists; the part not
|
|
160
|
+
* written yet is kept as given. Falls back to the path itself. */
|
|
161
|
+
function realPathOf(abs) {
|
|
162
|
+
try {
|
|
163
|
+
const existing = deepestExisting(abs);
|
|
164
|
+
return path.join(fs.realpathSync(existing), path.relative(existing, abs));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return abs;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Both sides are resolved the same way. Resolving only the root made every
|
|
171
|
+
* in-tree path look foreign when the workspace sits behind a symlink — macOS
|
|
172
|
+
* temp folders, or `smol /tmp/project` — and resolving the path also catches
|
|
173
|
+
* a link inside the workspace that leads out of it. */
|
|
174
|
+
function insideWorkspace(root, abs) {
|
|
175
|
+
const rel = path.relative(normalizeForCompare(realPathOf(path.resolve(root))), normalizeForCompare(realPathOf(path.resolve(abs))));
|
|
176
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Why a command would reach outside the workspace, or null when every path it
|
|
180
|
+
* mentions stays inside it. Only the reason text is returned; the caller
|
|
181
|
+
* decides whether that means "ask" or "refuse".
|
|
182
|
+
*/
|
|
183
|
+
function commandEscapesWorkspace(command, root) {
|
|
184
|
+
if (GLOBAL_INSTALL.test(command))
|
|
185
|
+
return "installs a package globally, outside the workspace";
|
|
186
|
+
for (const tok of pathCandidates(command)) {
|
|
187
|
+
if (HOME_TOKENS.test(tok))
|
|
188
|
+
return `uses the home directory (${tok})`;
|
|
189
|
+
if (TEMP_TOKENS.test(tok))
|
|
190
|
+
return `uses the system temp directory (${tok})`;
|
|
191
|
+
const t = fromMsys(tok);
|
|
192
|
+
if (isAbsoluteLike(t)) {
|
|
193
|
+
// On Windows under Git Bash, a bare POSIX path like /tmp or /usr/bin is
|
|
194
|
+
// an MSYS path — never inside a Windows workspace.
|
|
195
|
+
const posixOnWin = isWin && t.startsWith("/");
|
|
196
|
+
if (posixOnWin && !MSYS_ROOTS.test(t))
|
|
197
|
+
continue; // a /switch, not a path
|
|
198
|
+
if (posixOnWin || !insideWorkspace(root, t))
|
|
199
|
+
return `reaches outside the workspace (${tok})`;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (/(^|[\\/])\.\.([\\/]|$)/.test(t) && !insideWorkspace(root, path.resolve(root, t))) {
|
|
203
|
+
return `climbs above the workspace (${tok})`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|