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
package/dist/network.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The "models on other machines" flows, driven from the model picker in both
|
|
3
|
+
// UIs: search the network, type an address, and manage what was added. Nobody
|
|
4
|
+
// edits a file or runs a command — hosts are saved for them.
|
|
5
|
+
//
|
|
6
|
+
// A machine found by a search is never used until the user picks it: smolcoder
|
|
7
|
+
// sends source code to the server it talks to and runs the tool calls that
|
|
8
|
+
// come back, so choosing a host is choosing to trust it.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.notFoundHelp = notFoundHelp;
|
|
11
|
+
exports.findModelsOnNetwork = findModelsOnNetwork;
|
|
12
|
+
exports.manageHosts = manageHosts;
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const detect_1 = require("./detect");
|
|
15
|
+
const hosts_1 = require("./hosts");
|
|
16
|
+
const netscan_1 = require("./netscan");
|
|
17
|
+
const util_1 = require("./util");
|
|
18
|
+
const BACKEND_NAMES = { ollama: "Ollama", lmstudio: "LM Studio" };
|
|
19
|
+
function describeServers(servers) {
|
|
20
|
+
return servers.map((s) => `${BACKEND_NAMES[s.backend]} · ${(0, util_1.plural)(s.models, "model")}`).join(" + ");
|
|
21
|
+
}
|
|
22
|
+
/** Why a search or an address can come up empty. Both servers only listen to
|
|
23
|
+
* their own machine until told otherwise, which is the usual reason. */
|
|
24
|
+
function notFoundHelp(platform = process.platform) {
|
|
25
|
+
return ("A model server only answers other machines once it is told to. On the machine that runs the models:\n" +
|
|
26
|
+
" · Ollama: turn on \"Expose Ollama to the network\" in its settings (Linux or headless: set OLLAMA_HOST=0.0.0.0 and restart it).\n" +
|
|
27
|
+
" · LM Studio: Developer tab → Local Server → turn on \"Serve on Local Network\".\n" +
|
|
28
|
+
" · Windows: allow the server through the firewall for Private networks when it asks.\n" +
|
|
29
|
+
(platform === "darwin"
|
|
30
|
+
? "On this Mac: System Settings → Privacy & Security → Local Network must allow your terminal app, or nothing on the network is visible.\n"
|
|
31
|
+
: "") +
|
|
32
|
+
"Machines on a VPN or another network are not searched — use \"Enter an address\" for those.");
|
|
33
|
+
}
|
|
34
|
+
function save(hosts) {
|
|
35
|
+
return (0, config_1.updateConfig)({ hosts }).hosts ?? [];
|
|
36
|
+
}
|
|
37
|
+
async function confirmOutsideNetwork(ui, hostname, url) {
|
|
38
|
+
if (url.startsWith("https://") || (0, hosts_1.isPrivateHost)(hostname))
|
|
39
|
+
return true;
|
|
40
|
+
ui.warn(`${hostname} is outside your own network and the connection is plain http: your code and prompts would travel unencrypted.`);
|
|
41
|
+
return (await ui.select(`Add ${hostname} anyway?`, [{ label: "Cancel" }, { label: "Add it anyway" }])) === 1;
|
|
42
|
+
}
|
|
43
|
+
/** "Enter an address": returns true when a host was added. */
|
|
44
|
+
async function enterAddress(ui, replace) {
|
|
45
|
+
const typed = await ui.prompt("Address of the machine", "192.168.1.50, gpu-box.local or https://…");
|
|
46
|
+
if (!typed)
|
|
47
|
+
return false;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = (0, hosts_1.parseAddress)(typed);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
ui.warn(String(err?.message ?? err));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
ui.startSpinner(`checking ${parsed.hostname}`);
|
|
57
|
+
const found = (await Promise.all(parsed.urls.map((u) => (0, detect_1.identifyServer)(u, 4000)))).filter((s) => !!s);
|
|
58
|
+
ui.stopSpinner();
|
|
59
|
+
if (!found.length) {
|
|
60
|
+
ui.warn(`Nothing answered at ${parsed.hostname} as Ollama or LM Studio.`);
|
|
61
|
+
ui.status(notFoundHelp());
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!(await confirmOutsideNetwork(ui, parsed.hostname, found[0].baseUrl)))
|
|
65
|
+
return false;
|
|
66
|
+
let hosts = (0, config_1.loadConfig)().hosts ?? [];
|
|
67
|
+
if (replace)
|
|
68
|
+
hosts = (0, hosts_1.removeHost)(hosts, replace.address);
|
|
69
|
+
save((0, hosts_1.addHost)(hosts, { address: parsed.address, ...(replace?.name ? { name: replace.name } : {}) }));
|
|
70
|
+
ui.status(`· added ${replace?.name ?? parsed.hostname} — ${describeServers(found.map((s) => ({ backend: s.backend, models: s.models.length })))}`);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function savedAddressesOf(found, hosts) {
|
|
74
|
+
const mine = [found.ip, found.name, ...found.servers.map((s) => s.url)].filter(Boolean).map((s) => String(s).toLowerCase());
|
|
75
|
+
return hosts.some((h) => mine.includes(h.address.toLowerCase()));
|
|
76
|
+
}
|
|
77
|
+
/** "Search my network": returns true when at least one host was added. */
|
|
78
|
+
async function searchNetwork(ui, subnets, replace) {
|
|
79
|
+
const range = subnets.map((s) => s.cidr).join(" and ");
|
|
80
|
+
let lastTenth = -1;
|
|
81
|
+
ui.startSpinner(`searching ${range}`);
|
|
82
|
+
const found = await (0, netscan_1.scanSubnets)(subnets, {
|
|
83
|
+
onProgress: (done, total) => {
|
|
84
|
+
const tenth = Math.floor((done / total) * 10);
|
|
85
|
+
if (tenth === lastTenth || tenth >= 10)
|
|
86
|
+
return;
|
|
87
|
+
lastTenth = tenth;
|
|
88
|
+
ui.startSpinner(`searching ${range} · ${tenth * 10}%`);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
ui.stopSpinner();
|
|
92
|
+
if (!found.length) {
|
|
93
|
+
ui.warn(`No Ollama or LM Studio found on ${range}.`);
|
|
94
|
+
ui.status(notFoundHelp());
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
let added = false;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const hosts = (0, config_1.loadConfig)().hosts ?? [];
|
|
100
|
+
const options = found.map((f) => {
|
|
101
|
+
const known = savedAddressesOf(f, hosts);
|
|
102
|
+
return {
|
|
103
|
+
label: f.name ? `${f.name} (${f.ip})` : f.ip,
|
|
104
|
+
hint: describeServers(f.servers) + (known ? " · already added" : ""),
|
|
105
|
+
current: known,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
if (added)
|
|
109
|
+
options.push({ label: "Done" });
|
|
110
|
+
const pick = await ui.select(added ? "Add another machine?" : "Found on your network — pick one to use", options);
|
|
111
|
+
if (pick === null || pick >= found.length)
|
|
112
|
+
return added;
|
|
113
|
+
const f = found[pick];
|
|
114
|
+
if (savedAddressesOf(f, hosts)) {
|
|
115
|
+
ui.status(`· ${f.name ?? f.ip} is already added`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// The name survives the router handing out a new IP; fall back to the IP.
|
|
119
|
+
const base = replace ? (0, hosts_1.removeHost)(hosts, replace.address) : hosts;
|
|
120
|
+
save((0, hosts_1.addHost)(base, { address: f.name ?? f.ip, ...(replace?.name ? { name: replace.name } : {}) }));
|
|
121
|
+
ui.status(`· added ${replace?.name ?? f.name ?? f.ip} — ${describeServers(f.servers)}`);
|
|
122
|
+
added = true;
|
|
123
|
+
if (replace || found.every((x) => savedAddressesOf(x, (0, config_1.loadConfig)().hosts ?? [])))
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** The entry point behind "Find models on my network…". Returns true when the
|
|
128
|
+
* host list changed, so the caller can look for models again. `replace`
|
|
129
|
+
* swaps a stale entry for whatever is picked, keeping its name. */
|
|
130
|
+
async function findModelsOnNetwork(ui, replace) {
|
|
131
|
+
const subnets = (0, netscan_1.localSubnets)();
|
|
132
|
+
const options = [
|
|
133
|
+
...(subnets.length
|
|
134
|
+
? [{ label: "Search my network", hint: `${subnets.map((s) => s.cidr).join(", ")} — looks for Ollama and LM Studio` }]
|
|
135
|
+
: []),
|
|
136
|
+
{ label: "Enter an address", hint: "IP, name or URL — also for VPNs and other networks" },
|
|
137
|
+
];
|
|
138
|
+
if (!subnets.length)
|
|
139
|
+
ui.status("· this computer is not on a home or office network I can search — you can still enter an address");
|
|
140
|
+
const pick = await ui.select("Find models on another machine", options);
|
|
141
|
+
if (pick === null)
|
|
142
|
+
return false;
|
|
143
|
+
return options[pick].label === "Search my network" ? searchNetwork(ui, subnets, replace) : enterAddress(ui, replace);
|
|
144
|
+
}
|
|
145
|
+
/** "Network hosts…": see what each added machine serves, rename or remove
|
|
146
|
+
* it, or look for it again when its address changed. Returns true when the
|
|
147
|
+
* list changed. */
|
|
148
|
+
async function manageHosts(ui) {
|
|
149
|
+
let changed = false;
|
|
150
|
+
for (;;) {
|
|
151
|
+
const hosts = (0, config_1.loadConfig)().hosts ?? [];
|
|
152
|
+
if (!hosts.length) {
|
|
153
|
+
if (!changed)
|
|
154
|
+
ui.status("· no network hosts added yet");
|
|
155
|
+
return changed;
|
|
156
|
+
}
|
|
157
|
+
ui.startSpinner("checking hosts");
|
|
158
|
+
const statuses = await (0, detect_1.probeHosts)(hosts);
|
|
159
|
+
ui.stopSpinner();
|
|
160
|
+
const pick = await ui.select("Network hosts", statuses.map((s) => ({
|
|
161
|
+
label: (0, hosts_1.hostLabel)(s.host),
|
|
162
|
+
hint: (s.host.name ? `${s.host.address} · ` : "") +
|
|
163
|
+
(s.servers.length ? describeServers(s.servers.map((x) => ({ backend: x.backend, models: x.models.length }))) : "not reachable right now"),
|
|
164
|
+
})));
|
|
165
|
+
if (pick === null)
|
|
166
|
+
return changed;
|
|
167
|
+
const { host, servers } = statuses[pick];
|
|
168
|
+
const actions = [
|
|
169
|
+
{ label: "Rename", run: "rename" },
|
|
170
|
+
{ label: "Remove", run: "remove" },
|
|
171
|
+
];
|
|
172
|
+
if (!servers.length)
|
|
173
|
+
actions.push({ label: "Look for it again", hint: "its address may have changed", run: "refind" });
|
|
174
|
+
const act = await ui.select((0, hosts_1.hostLabel)(host), actions.map(({ label, hint }) => ({ label, hint })));
|
|
175
|
+
if (act === null)
|
|
176
|
+
continue;
|
|
177
|
+
if (actions[act].run === "rename") {
|
|
178
|
+
const name = await ui.prompt(`New name for ${(0, hosts_1.hostLabel)(host)}`, (0, hosts_1.hostLabel)(host));
|
|
179
|
+
if (name) {
|
|
180
|
+
save((0, hosts_1.renameHost)(hosts, host.address, name));
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (actions[act].run === "remove") {
|
|
185
|
+
save((0, hosts_1.removeHost)(hosts, host.address));
|
|
186
|
+
ui.status(`· removed ${(0, hosts_1.hostLabel)(host)}`);
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
else if (await findModelsOnNetwork(ui, host)) {
|
|
190
|
+
changed = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The agent's plan: a harness-held checklist. This is deliberately NOT a file
|
|
3
|
+
// and NOT model-formatted text — the harness owns the state, so rendering it
|
|
4
|
+
// to the user costs zero tokens, and compaction can never destroy it. For a
|
|
5
|
+
// small model it works as a compass: every `done` result re-states what comes
|
|
6
|
+
// next, and after compaction the whole checklist is re-injected verbatim.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.Plan = void 0;
|
|
9
|
+
class Plan {
|
|
10
|
+
steps = [];
|
|
11
|
+
get exists() {
|
|
12
|
+
return this.steps.length > 0;
|
|
13
|
+
}
|
|
14
|
+
get doneCount() {
|
|
15
|
+
return this.steps.filter((s) => s.done).length;
|
|
16
|
+
}
|
|
17
|
+
/** Index of the current (first undone) step, or -1 when all done/empty. */
|
|
18
|
+
get currentIndex() {
|
|
19
|
+
return this.steps.findIndex((s) => !s.done);
|
|
20
|
+
}
|
|
21
|
+
reset() {
|
|
22
|
+
this.steps = [];
|
|
23
|
+
}
|
|
24
|
+
/** Replace the plan. Accept the semicolon lists local models often return
|
|
25
|
+
* when asked for a newline-separated string. Keep explicit multiline steps
|
|
26
|
+
* intact, including punctuation or code within a step. */
|
|
27
|
+
set(stepsText) {
|
|
28
|
+
const lines = stepsText
|
|
29
|
+
.split(stepsText.includes("\n") ? "\n" : /;\s*/)
|
|
30
|
+
.map((l) => l.replace(/^\s*(?:[-*]|\d+[.)])?\s*(?:\[.\]\s*)?/, "").trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.slice(0, 20);
|
|
33
|
+
if (lines.length === 0) {
|
|
34
|
+
return 'Error: steps is required — one step per line. Example: {"action": "set", "steps": "create index.html\\ncreate game.js\\ntest the page"}';
|
|
35
|
+
}
|
|
36
|
+
this.steps = lines.map((text) => ({ text, done: false }));
|
|
37
|
+
return `Plan set (${this.steps.length} steps). Current: 1. ${this.steps[0].text}`;
|
|
38
|
+
}
|
|
39
|
+
/** Mark a step done. No index = the current step. Returns a compact
|
|
40
|
+
* "what's next" line — cheap tokens that keep the model on course. */
|
|
41
|
+
markDone(stepNumber) {
|
|
42
|
+
if (!this.exists)
|
|
43
|
+
return 'Error: no plan yet. Create one first with {"action": "set", "steps": "..."}';
|
|
44
|
+
let idx;
|
|
45
|
+
if (stepNumber === undefined || stepNumber === null) {
|
|
46
|
+
idx = this.currentIndex;
|
|
47
|
+
if (idx < 0)
|
|
48
|
+
return "All steps are already done.";
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
idx = Math.floor(stepNumber) - 1;
|
|
52
|
+
if (idx < 0 || idx >= this.steps.length) {
|
|
53
|
+
return `Error: step ${stepNumber} does not exist. The plan has ${this.steps.length} steps.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.steps[idx].done = true;
|
|
57
|
+
const next = this.currentIndex;
|
|
58
|
+
return next < 0
|
|
59
|
+
? `Done: ${idx + 1}. All ${this.steps.length} steps complete.`
|
|
60
|
+
: `Done: ${idx + 1}. Next: ${next + 1}. ${this.steps[next].text}`;
|
|
61
|
+
}
|
|
62
|
+
add(text) {
|
|
63
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
64
|
+
return 'Error: text is required. Example: {"action": "add", "text": "fix the collision bug"}';
|
|
65
|
+
}
|
|
66
|
+
if (this.steps.length >= 20)
|
|
67
|
+
return "Error: the plan already has 20 steps — finish some first.";
|
|
68
|
+
this.steps.push({ text: text.trim(), done: false });
|
|
69
|
+
return `Added step ${this.steps.length}: ${text.trim()}`;
|
|
70
|
+
}
|
|
71
|
+
checkpoint(text) {
|
|
72
|
+
const idx = this.currentIndex;
|
|
73
|
+
if (idx < 0)
|
|
74
|
+
return 'Error: create a plan with an unfinished step before recording a checkpoint.';
|
|
75
|
+
if (!text.trim() || text.length > 1000)
|
|
76
|
+
return 'Error: checkpoint text must be 1–1000 characters. Keep exact APIs, the unresolved error and the next small edit.';
|
|
77
|
+
this.steps[idx].note = text.trim();
|
|
78
|
+
return `Checkpoint saved for step ${idx + 1}: ${text.trim()}`;
|
|
79
|
+
}
|
|
80
|
+
/** Compact model-facing checklist (used by action "show" and after compaction). */
|
|
81
|
+
modelView() {
|
|
82
|
+
if (!this.exists)
|
|
83
|
+
return "No plan set.";
|
|
84
|
+
return this.steps
|
|
85
|
+
.map((s, i) => `${i + 1}.[${s.done ? "x" : i === this.currentIndex ? ">" : " "}] ${s.text}` +
|
|
86
|
+
(s.note && i === this.currentIndex ? `\nWorking checkpoint (agent notes; verify against files): ${s.note}` : ""))
|
|
87
|
+
.join("\n");
|
|
88
|
+
}
|
|
89
|
+
/** One-line summary for the compaction state note. */
|
|
90
|
+
compactLine() {
|
|
91
|
+
if (!this.exists)
|
|
92
|
+
return null;
|
|
93
|
+
return `Plan (${this.doneCount}/${this.steps.length} done):\n${this.modelView()}`;
|
|
94
|
+
}
|
|
95
|
+
pendingSummary() {
|
|
96
|
+
return this.steps
|
|
97
|
+
.filter((s) => !s.done)
|
|
98
|
+
.map((s) => s.text)
|
|
99
|
+
.join("; ");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.Plan = Plan;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The core system prompt. Short instructions; tool details live in schemas.
|
|
3
|
+
// Everything else
|
|
4
|
+
// the model needs lives in the tool schemas and in coaching error messages.
|
|
5
|
+
// If the workspace has an AGENTS.md, its contents ride along directly after
|
|
6
|
+
// the prompt (size-capped) — and because they are part of message[0], they
|
|
7
|
+
// survive compaction the same way the system prompt does.
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.loadAgentsMd = loadAgentsMd;
|
|
43
|
+
exports.buildSystemPrompt = buildSystemPrompt;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const AGENTS_MD_CAP_CHARS = 8000; // ~2k tokens — small-context friendly
|
|
47
|
+
/** Read the workspace's AGENTS.md memory file, if any. */
|
|
48
|
+
function loadAgentsMd(workspace) {
|
|
49
|
+
try {
|
|
50
|
+
const p = path.join(workspace, "AGENTS.md");
|
|
51
|
+
if (!fs.existsSync(p))
|
|
52
|
+
return null;
|
|
53
|
+
let text = fs.readFileSync(p, "utf8").trim();
|
|
54
|
+
if (!text)
|
|
55
|
+
return null;
|
|
56
|
+
if (text.length > AGENTS_MD_CAP_CHARS) {
|
|
57
|
+
text =
|
|
58
|
+
text.slice(0, AGENTS_MD_CAP_CHARS) +
|
|
59
|
+
"\n[AGENTS.md was truncated here to save context]";
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function buildSystemPrompt(opts) {
|
|
68
|
+
const os = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
69
|
+
const modeLine = opts.mode === "ro"
|
|
70
|
+
? "You are in read-only mode: you can read and search files but not change anything."
|
|
71
|
+
: opts.mode === "edit"
|
|
72
|
+
? "Read, edit and run commands inside the workspace. Commands reaching outside it need approval; keep scratch files in .scratch/."
|
|
73
|
+
: "You have full access to files and commands; nothing asks the user for approval.";
|
|
74
|
+
return (`You are smolcoder, a coding agent working in the workspace ${opts.workspace} on ${os}. ` +
|
|
75
|
+
`Commands run in ${opts.shellLabel} with the workspace as the working directory. ` +
|
|
76
|
+
`File paths are relative to the workspace; you cannot access files outside it. ${modeLine}\n\n` +
|
|
77
|
+
`For multi-step work, first set a short plan of runnable increments; mark each step done as you finish it. The plan survives compaction. Use plan checkpoint to retain exact APIs, errors and your next edit during an investigation. Establish a working entry point early and run the build after wiring modules. ` +
|
|
78
|
+
`Read relevant files before editing. Search narrowly and read small line ranges. Inspect a local module's actual exports before importing it. Make one tool call at a time; use small modules and write large files in parts. ` +
|
|
79
|
+
`Read errors and change your approach when a call fails. Put test programs in files rather than long inline shell commands. Verify changes with the relevant test or command; a failed check is not success. ` +
|
|
80
|
+
`Continue until the request is finished or explain the blocker. Summarize the result and verification briefly.` +
|
|
81
|
+
(opts.agentsMd
|
|
82
|
+
? `\n\nWorkspace instructions from AGENTS.md — follow these:\n${opts.agentsMd}`
|
|
83
|
+
: ""));
|
|
84
|
+
}
|