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,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Files the user attaches to a message in the web UI: a screenshot pasted from
|
|
3
|
+
// the clipboard, a dropped image, or a text file. Images travel to the model
|
|
4
|
+
// as vision input when the model can see; text files are inlined into the
|
|
5
|
+
// user turn. The bytes live under ~/.smolcoder/uploads/<session>/, never
|
|
6
|
+
// inside the workspace, so an attachment can never dirty the user's repo.
|
|
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.IMAGE_TOKENS = exports.MAX_TEXT_BYTES = exports.MAX_UPLOAD_BYTES = void 0;
|
|
42
|
+
exports.safeName = safeName;
|
|
43
|
+
exports.extOf = extOf;
|
|
44
|
+
exports.mimeForExt = mimeForExt;
|
|
45
|
+
exports.looksLikeText = looksLikeText;
|
|
46
|
+
exports.classifyUpload = classifyUpload;
|
|
47
|
+
exports.imageBase64 = imageBase64;
|
|
48
|
+
exports.imageDataUrl = imageDataUrl;
|
|
49
|
+
exports.renderAttachmentsForModel = renderAttachmentsForModel;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
/** Largest single upload the hub accepts. */
|
|
52
|
+
exports.MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
53
|
+
/** Text files are inlined into the prompt, so they must stay small enough for
|
|
54
|
+
* a local model's context window. */
|
|
55
|
+
exports.MAX_TEXT_BYTES = 48 * 1024;
|
|
56
|
+
/** Rough prompt cost of one image for the context gauge: a screenshot on a
|
|
57
|
+
* qwen-vl class model lands around here after the backend's own resize. */
|
|
58
|
+
exports.IMAGE_TOKENS = 1500;
|
|
59
|
+
const IMAGE_MIMES = {
|
|
60
|
+
png: "image/png",
|
|
61
|
+
jpg: "image/jpeg",
|
|
62
|
+
jpeg: "image/jpeg",
|
|
63
|
+
gif: "image/gif",
|
|
64
|
+
webp: "image/webp",
|
|
65
|
+
};
|
|
66
|
+
/** Strip any path and control characters from a user-supplied file name. */
|
|
67
|
+
function safeName(name) {
|
|
68
|
+
const base = String(name ?? "").split(/[\\/]/).pop() ?? "";
|
|
69
|
+
const cleaned = base.replace(/[\x00-\x1f\x7f]/g, "").trim().slice(0, 80);
|
|
70
|
+
return cleaned || "file";
|
|
71
|
+
}
|
|
72
|
+
/** Extension for the stored copy: from the name when it has one, else from the
|
|
73
|
+
* declared type (a clipboard screenshot arrives as image/png with no name). */
|
|
74
|
+
function extOf(name, mime) {
|
|
75
|
+
const m = /\.([a-z0-9]{1,8})$/i.exec(name);
|
|
76
|
+
if (m)
|
|
77
|
+
return m[1].toLowerCase();
|
|
78
|
+
const type = mime.toLowerCase().split(";")[0].trim();
|
|
79
|
+
for (const [ext, t] of Object.entries(IMAGE_MIMES))
|
|
80
|
+
if (t === type)
|
|
81
|
+
return ext;
|
|
82
|
+
return type.startsWith("text/") ? "txt" : "bin";
|
|
83
|
+
}
|
|
84
|
+
/** Content type to serve a stored upload with. Everything that is not a
|
|
85
|
+
* raster image was accepted as text. */
|
|
86
|
+
function mimeForExt(ext) {
|
|
87
|
+
return IMAGE_MIMES[ext.toLowerCase()] ?? "text/plain; charset=utf-8";
|
|
88
|
+
}
|
|
89
|
+
function looksLikeText(bytes) {
|
|
90
|
+
const n = Math.min(bytes.length, 8192);
|
|
91
|
+
for (let i = 0; i < n; i++)
|
|
92
|
+
if (bytes[i] === 0)
|
|
93
|
+
return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Decide how an upload is used, or say why it cannot be. */
|
|
97
|
+
function classifyUpload(name, mime, bytes) {
|
|
98
|
+
if (bytes.length === 0)
|
|
99
|
+
return { error: `"${name}" is empty.` };
|
|
100
|
+
const ext = extOf(name, mime);
|
|
101
|
+
if (IMAGE_MIMES[ext])
|
|
102
|
+
return { kind: "image", mime: IMAGE_MIMES[ext] };
|
|
103
|
+
if (!looksLikeText(bytes)) {
|
|
104
|
+
return { error: `"${name}" is not a supported attachment. Images (png, jpg, gif, webp) and text files can be attached.` };
|
|
105
|
+
}
|
|
106
|
+
if (bytes.length > exports.MAX_TEXT_BYTES) {
|
|
107
|
+
return {
|
|
108
|
+
error: `"${name}" is too large to attach as text (${Math.round(bytes.length / 1024)} KB; the limit is ${exports.MAX_TEXT_BYTES / 1024} KB). Put it in the workspace and ask the agent to read it.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { kind: "text", mime: "text/plain; charset=utf-8" };
|
|
112
|
+
}
|
|
113
|
+
/** Base64 of a stored image, or null when the file is gone: a deleted upload
|
|
114
|
+
* must not fail the whole request. */
|
|
115
|
+
function imageBase64(ref) {
|
|
116
|
+
try {
|
|
117
|
+
return fs.readFileSync(ref.path).toString("base64");
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function imageDataUrl(ref) {
|
|
124
|
+
const b64 = imageBase64(ref);
|
|
125
|
+
return b64 === null ? null : `data:${ref.mime};base64,${b64}`;
|
|
126
|
+
}
|
|
127
|
+
const FENCE = "```";
|
|
128
|
+
/** The prompt text and image references for a turn's attachments. Text files
|
|
129
|
+
* are inlined verbatim; images become vision input when the model can see,
|
|
130
|
+
* otherwise a note so the model can tell the user what it cannot do. */
|
|
131
|
+
function renderAttachmentsForModel(attachments, canSeeImages) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
const images = [];
|
|
134
|
+
for (const a of attachments) {
|
|
135
|
+
if (a.kind === "image") {
|
|
136
|
+
if (canSeeImages) {
|
|
137
|
+
images.push({ path: a.path, mime: a.mime, name: a.name });
|
|
138
|
+
parts.push(`[Attached image: ${a.name}]`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
parts.push(`[Attached image: ${a.name} — this model cannot view images. If the image matters, tell the user to switch to a vision-capable model with /models.]`);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let body;
|
|
146
|
+
try {
|
|
147
|
+
body = fs.readFileSync(a.path, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
body = "(the file is no longer available)";
|
|
151
|
+
}
|
|
152
|
+
if (body.length > exports.MAX_TEXT_BYTES)
|
|
153
|
+
body = body.slice(0, exports.MAX_TEXT_BYTES) + `\n… [truncated: ${a.size} bytes in total]`;
|
|
154
|
+
const fence = body.includes(FENCE) ? FENCE + "`" : FENCE;
|
|
155
|
+
parts.push(`[Attached file: ${a.name} (${a.size} bytes)]\n${fence}\n${body}\n${fence}`);
|
|
156
|
+
}
|
|
157
|
+
return { text: parts.join("\n\n"), images };
|
|
158
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// User config (~/.smolcoder.json) and the data directory (~/.smolcoder/) that
|
|
3
|
+
// holds saved web sessions and the workspace list. Shared by the CLI entry
|
|
4
|
+
// point, the session loop and the web hub.
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DATA_DIR = exports.CONFIG_PATH = void 0;
|
|
40
|
+
exports.loadConfig = loadConfig;
|
|
41
|
+
exports.saveConfig = saveConfig;
|
|
42
|
+
exports.updateConfig = updateConfig;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
// SMOLCODER_CONFIG points tests at a scratch file so they never touch the
|
|
47
|
+
// real one.
|
|
48
|
+
exports.CONFIG_PATH = process.env.SMOLCODER_CONFIG || path.join(os.homedir(), ".smolcoder.json");
|
|
49
|
+
exports.DATA_DIR = path.join(os.homedir(), ".smolcoder");
|
|
50
|
+
function loadConfig() {
|
|
51
|
+
try {
|
|
52
|
+
const cfg = JSON.parse(fs.readFileSync(exports.CONFIG_PATH, "utf8"));
|
|
53
|
+
// Never let bypass be inherited implicitly from a past session — a single
|
|
54
|
+
// shift+tab into it would otherwise silently persist unattended, unchecked
|
|
55
|
+
// command execution into every later run, including headless -p in CI.
|
|
56
|
+
// Requires an explicit flag (-m bypass / --bypass) each time. Old configs
|
|
57
|
+
// saved "write"/"yolo" under the previous mode names.
|
|
58
|
+
if (cfg.lastMode === "write")
|
|
59
|
+
cfg.lastMode = "edit";
|
|
60
|
+
if (cfg.lastMode === "yolo" || cfg.lastMode === "bypass")
|
|
61
|
+
cfg.lastMode = "edit";
|
|
62
|
+
cfg.hosts = Array.isArray(cfg.hosts)
|
|
63
|
+
? cfg.hosts
|
|
64
|
+
.filter((h) => h && typeof h.address === "string" && h.address.trim())
|
|
65
|
+
.map((h) => ({ address: h.address.trim(), ...(typeof h.name === "string" && h.name.trim() ? { name: h.name.trim() } : {}) }))
|
|
66
|
+
: [];
|
|
67
|
+
return cfg;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function saveConfig(cfg) {
|
|
74
|
+
try {
|
|
75
|
+
fs.writeFileSync(exports.CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* non-fatal */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Change some settings and keep the rest. Re-reads the file first: several
|
|
82
|
+
* web sessions share it, and each one only knows about its own fields. */
|
|
83
|
+
function updateConfig(patch) {
|
|
84
|
+
const next = { ...loadConfig(), ...patch };
|
|
85
|
+
saveConfig(next);
|
|
86
|
+
return next;
|
|
87
|
+
}
|