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/ui.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Terminal UI: one readline interface for everything (prompt, pickers,
|
|
3
|
+
// approval), a spinner for the silent gap before the first streamed token,
|
|
4
|
+
// and small helpers for status lines. No dependencies.
|
|
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.UI = void 0;
|
|
40
|
+
exports.renderPlan = renderPlan;
|
|
41
|
+
exports.summarizeArgs = summarizeArgs;
|
|
42
|
+
const readline = __importStar(require("readline"));
|
|
43
|
+
const util_1 = require("./util");
|
|
44
|
+
/** Shared checklist renderer: ✔ done (dim) · ▶ current (cyan) · ○ pending. */
|
|
45
|
+
function renderPlan(plan, indent = " ") {
|
|
46
|
+
const cur = plan.currentIndex;
|
|
47
|
+
const header = `${indent}${util_1.c.bold("Plan")} ${util_1.c.dim(`${plan.doneCount}/${plan.steps.length}`)}`;
|
|
48
|
+
const rows = plan.steps.map((s, i) => {
|
|
49
|
+
if (s.done)
|
|
50
|
+
return `${indent}${util_1.c.dim("✔ " + s.text)}`;
|
|
51
|
+
if (i === cur)
|
|
52
|
+
return `${indent}${util_1.c.cyan("▶ " + s.text)}`;
|
|
53
|
+
return `${indent}${util_1.c.gray("○ " + s.text)}`;
|
|
54
|
+
});
|
|
55
|
+
return [header, ...rows].join("\n");
|
|
56
|
+
}
|
|
57
|
+
class UI {
|
|
58
|
+
rl;
|
|
59
|
+
spinnerTimer = null;
|
|
60
|
+
spinnerActive = false;
|
|
61
|
+
atLineStart = true;
|
|
62
|
+
onInterrupt = null;
|
|
63
|
+
constructor() {
|
|
64
|
+
this.rl = readline.createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
historySize: 100,
|
|
68
|
+
});
|
|
69
|
+
this.rl.on("SIGINT", () => {
|
|
70
|
+
if (this.onInterrupt) {
|
|
71
|
+
this.onInterrupt();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.println("");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
ask(prompt) {
|
|
80
|
+
return new Promise((resolve) => this.rl.question(prompt, (a) => resolve(a)));
|
|
81
|
+
}
|
|
82
|
+
/** y / n / a(lways allow this program for the session) */
|
|
83
|
+
async confirmCommand(command, reason) {
|
|
84
|
+
this.stopSpinner();
|
|
85
|
+
const answer = await this.ask(`${util_1.c.yellow("run?")} ${util_1.c.bold(command)}\n` +
|
|
86
|
+
(reason ? ` ${util_1.c.dim(reason)}\n` : "") +
|
|
87
|
+
` ${util_1.c.dim("[y]es / [n]o / [a]lways allow this program this session:")} `);
|
|
88
|
+
const ch = answer.trim().toLowerCase();
|
|
89
|
+
if (ch === "a" || ch === "always")
|
|
90
|
+
return "always";
|
|
91
|
+
if (ch === "y" || ch === "yes" || ch === "")
|
|
92
|
+
return "yes";
|
|
93
|
+
return "no";
|
|
94
|
+
}
|
|
95
|
+
startSpinner(label) {
|
|
96
|
+
if (!process.stdout.isTTY) {
|
|
97
|
+
// Headless liveness heartbeat: a working local model can generate for
|
|
98
|
+
// minutes with nothing visible — tick on stderr so a log-follower can
|
|
99
|
+
// tell "working" from "hung".
|
|
100
|
+
this.stopSpinner();
|
|
101
|
+
const started = Date.now();
|
|
102
|
+
this.spinnerTimer = setInterval(() => {
|
|
103
|
+
const secs = Math.round((Date.now() - started) / 1000);
|
|
104
|
+
process.stderr.write(util_1.c.gray(`· ${label}… ${secs}s\n`));
|
|
105
|
+
}, 15000);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.stopSpinner();
|
|
109
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
110
|
+
let i = 0;
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
this.spinnerActive = true;
|
|
113
|
+
this.spinnerTimer = setInterval(() => {
|
|
114
|
+
const secs = Math.floor((Date.now() - started) / 1000);
|
|
115
|
+
process.stdout.write(`\r${util_1.c.cyan(frames[i++ % frames.length])} ${util_1.c.dim(label + (secs > 2 ? ` ${secs}s` : ""))} `);
|
|
116
|
+
}, 100);
|
|
117
|
+
}
|
|
118
|
+
stopSpinner() {
|
|
119
|
+
if (this.spinnerTimer) {
|
|
120
|
+
clearInterval(this.spinnerTimer);
|
|
121
|
+
this.spinnerTimer = null;
|
|
122
|
+
}
|
|
123
|
+
if (this.spinnerActive) {
|
|
124
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
125
|
+
this.spinnerActive = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
ensureLine() {
|
|
129
|
+
if (!this.atLineStart) {
|
|
130
|
+
process.stdout.write("\n");
|
|
131
|
+
this.atLineStart = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Meta output (tool lines, status, warnings) goes to stderr so headless
|
|
135
|
+
* consumers get a clean stdout with just the model's answer. */
|
|
136
|
+
meta(s) {
|
|
137
|
+
this.stopSpinner();
|
|
138
|
+
this.ensureLine();
|
|
139
|
+
process.stderr.write(s + "\n");
|
|
140
|
+
}
|
|
141
|
+
/** Streamed model text goes straight to stdout. */
|
|
142
|
+
token(text) {
|
|
143
|
+
this.stopSpinner();
|
|
144
|
+
process.stdout.write(text);
|
|
145
|
+
this.atLineStart = text.endsWith("\n");
|
|
146
|
+
}
|
|
147
|
+
/** Headless mode: reasoning noise is suppressed — automations want the answer. */
|
|
148
|
+
thinking(_text) { }
|
|
149
|
+
toolCall(name, args) {
|
|
150
|
+
const summary = summarizeArgs(name, args);
|
|
151
|
+
this.meta(`${util_1.c.cyan("→")} ${util_1.c.bold(name)} ${util_1.c.dim(summary)}`);
|
|
152
|
+
}
|
|
153
|
+
toolResult(result) {
|
|
154
|
+
const firstLine = result.split("\n")[0] ?? "";
|
|
155
|
+
const isError = firstLine.startsWith("Error");
|
|
156
|
+
const lines = result.split("\n").length;
|
|
157
|
+
const label = isError
|
|
158
|
+
? util_1.c.red(firstLine.slice(0, 120))
|
|
159
|
+
: util_1.c.dim(firstLine.slice(0, 100) + (lines > 1 ? ` (+${lines - 1} lines)` : ""));
|
|
160
|
+
this.meta(` ${isError ? util_1.c.red("✗") : util_1.c.green("✓")} ${label}`);
|
|
161
|
+
}
|
|
162
|
+
println(s = "") {
|
|
163
|
+
this.stopSpinner();
|
|
164
|
+
this.ensureLine();
|
|
165
|
+
process.stdout.write(s + "\n");
|
|
166
|
+
this.atLineStart = true;
|
|
167
|
+
}
|
|
168
|
+
status(s) {
|
|
169
|
+
this.meta(util_1.c.gray(s));
|
|
170
|
+
}
|
|
171
|
+
turnEnd(label) {
|
|
172
|
+
this.meta(util_1.c.dim(`■ ${label}`));
|
|
173
|
+
}
|
|
174
|
+
planUpdated(plan) {
|
|
175
|
+
this.meta(renderPlan(plan));
|
|
176
|
+
}
|
|
177
|
+
warn(s) {
|
|
178
|
+
this.meta(util_1.c.yellow(s));
|
|
179
|
+
}
|
|
180
|
+
error(s) {
|
|
181
|
+
this.meta(util_1.c.red(s));
|
|
182
|
+
}
|
|
183
|
+
close() {
|
|
184
|
+
this.stopSpinner();
|
|
185
|
+
this.rl.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.UI = UI;
|
|
189
|
+
function summarizeArgs(name, args) {
|
|
190
|
+
try {
|
|
191
|
+
// Unparseable (usually cut-off) arguments: show what arrived, not "undefined".
|
|
192
|
+
if (args && typeof args.__raw === "string") {
|
|
193
|
+
return `(arguments could not be parsed: ${args.__raw.replace(/\s+/g, " ")}…)`;
|
|
194
|
+
}
|
|
195
|
+
switch (name) {
|
|
196
|
+
case "read_file":
|
|
197
|
+
return String(args.path ?? "") + (args.offset ? ` from line ${args.offset}` : "");
|
|
198
|
+
case "write_file":
|
|
199
|
+
return `${args.path} (${String(args.content ?? "").split("\n").length} lines)`;
|
|
200
|
+
case "edit_file":
|
|
201
|
+
return String(args.path ?? "");
|
|
202
|
+
case "list_files":
|
|
203
|
+
return String(args.path ?? ".");
|
|
204
|
+
case "search":
|
|
205
|
+
return `"${args.pattern}"${args.path ? ` in ${args.path}` : ""}`;
|
|
206
|
+
case "run_command":
|
|
207
|
+
return String(args.command ?? "");
|
|
208
|
+
case "task":
|
|
209
|
+
return [args.action, args.command ?? args.task_id ?? ""].filter(Boolean).join(" ");
|
|
210
|
+
case "plan": {
|
|
211
|
+
if (args.action === "set")
|
|
212
|
+
return `set (${String(args.steps ?? "").split("\n").filter(Boolean).length} steps)`;
|
|
213
|
+
if (args.action === "done")
|
|
214
|
+
return `done${args.step ? " " + args.step : ""}`;
|
|
215
|
+
if (args.action === "add")
|
|
216
|
+
return `add: ${String(args.text ?? "").slice(0, 60)}`;
|
|
217
|
+
return String(args.action ?? "");
|
|
218
|
+
}
|
|
219
|
+
default:
|
|
220
|
+
return JSON.stringify(args).slice(0, 80);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Small shared helpers. Zero dependencies: ANSI codes are written by hand.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.c = void 0;
|
|
5
|
+
exports.estimateTokens = estimateTokens;
|
|
6
|
+
exports.truncateMiddle = truncateMiddle;
|
|
7
|
+
exports.truncateEnd = truncateEnd;
|
|
8
|
+
exports.plural = plural;
|
|
9
|
+
exports.fmtDuration = fmtDuration;
|
|
10
|
+
exports.probeJson = probeJson;
|
|
11
|
+
exports.tryFetchJson = tryFetchJson;
|
|
12
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
13
|
+
function wrap(code, s) {
|
|
14
|
+
return useColor ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
15
|
+
}
|
|
16
|
+
exports.c = {
|
|
17
|
+
dim: (s) => wrap("2", s),
|
|
18
|
+
bold: (s) => wrap("1", s),
|
|
19
|
+
cyan: (s) => wrap("36", s),
|
|
20
|
+
green: (s) => wrap("32", s),
|
|
21
|
+
yellow: (s) => wrap("33", s),
|
|
22
|
+
red: (s) => wrap("31", s),
|
|
23
|
+
magenta: (s) => wrap("35", s),
|
|
24
|
+
gray: (s) => wrap("90", s),
|
|
25
|
+
};
|
|
26
|
+
/** Rough token estimate: ~4 chars per token. Corrected by real usage after each request. */
|
|
27
|
+
function estimateTokens(text) {
|
|
28
|
+
return Math.ceil(text.length / 4);
|
|
29
|
+
}
|
|
30
|
+
/** Cap a string in the middle, keeping head and tail — best shape for command output. */
|
|
31
|
+
function truncateMiddle(s, maxChars) {
|
|
32
|
+
if (s.length <= maxChars)
|
|
33
|
+
return s;
|
|
34
|
+
const head = Math.floor(maxChars * 0.6);
|
|
35
|
+
const tail = maxChars - head;
|
|
36
|
+
const omitted = s.length - maxChars;
|
|
37
|
+
return (s.slice(0, head) +
|
|
38
|
+
`\n... [${omitted} characters omitted to save context] ...\n` +
|
|
39
|
+
s.slice(s.length - tail));
|
|
40
|
+
}
|
|
41
|
+
function truncateEnd(s, maxChars, note) {
|
|
42
|
+
if (s.length <= maxChars)
|
|
43
|
+
return s;
|
|
44
|
+
return s.slice(0, maxChars) + `\n... [truncated${note ? ": " + note : ""}]`;
|
|
45
|
+
}
|
|
46
|
+
function plural(n, word) {
|
|
47
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
48
|
+
}
|
|
49
|
+
function fmtDuration(ms) {
|
|
50
|
+
if (ms < 1000)
|
|
51
|
+
return `${ms}ms`;
|
|
52
|
+
const s = ms / 1000;
|
|
53
|
+
if (s < 60)
|
|
54
|
+
return `${s.toFixed(1)}s`;
|
|
55
|
+
return `${Math.floor(s / 60)}m${Math.round(s % 60)}s`;
|
|
56
|
+
}
|
|
57
|
+
/** Like tryFetchJson, but also says whether anything answered at all — a 404
|
|
58
|
+
* from a live server is worth a follow-up request, a dead address is not. */
|
|
59
|
+
async function probeJson(url, timeoutMs = 1500) {
|
|
60
|
+
const ctrl = new AbortController();
|
|
61
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
return { reached: true, data: null };
|
|
66
|
+
return { reached: true, data: await res.json().catch(() => null) };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { reached: false, data: null };
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(t);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** fetch with a hard timeout; returns null on any failure (used for detection probes). */
|
|
76
|
+
async function tryFetchJson(url, init, timeoutMs = 1500) {
|
|
77
|
+
const ctrl = new AbortController();
|
|
78
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
return null;
|
|
83
|
+
return await res.json();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(t);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.failureSignature = failureSignature;
|
|
37
|
+
exports.projectVerification = projectVerification;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const sandbox_1 = require("./sandbox");
|
|
40
|
+
/** A repeat hint only: timings, coordinates and stack line numbers may vary.
|
|
41
|
+
* The original output remains the evidence presented to the model. */
|
|
42
|
+
function failureSignature(output) {
|
|
43
|
+
return output.replace(/\x1b\[[0-9;]*m/g, "").replace(/\d+(?:\.\d+)?/g, "#").replace(/\s+/g, " ").trim();
|
|
44
|
+
}
|
|
45
|
+
/** Conventional project checks, without executing or interpolating package data.
|
|
46
|
+
* Caller-owned acceptance commands take precedence in the final gate. */
|
|
47
|
+
function projectVerification(workspace) {
|
|
48
|
+
let file;
|
|
49
|
+
try {
|
|
50
|
+
file = (0, sandbox_1.resolveInWorkspace)(workspace, "package.json");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!fs.existsSync(file))
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
if (fs.statSync(file).size > 1024 * 1024)
|
|
59
|
+
return "npm run build --if-present";
|
|
60
|
+
const scripts = JSON.parse(fs.readFileSync(file, "utf8")).scripts;
|
|
61
|
+
if (!scripts || typeof scripts !== "object")
|
|
62
|
+
return;
|
|
63
|
+
const commands = ["build", "test", "test:e2e"].filter(name => typeof scripts[name] === "string" && scripts[name].trim())
|
|
64
|
+
.map(name => `npm run ${name}`);
|
|
65
|
+
return commands.length ? commands.join(" && ") : undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// npm reports malformed package metadata precisely; never silently bless it.
|
|
69
|
+
return "npm run build --if-present";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SessionChannel: the SessionUI a web session talks to. Same contract the
|
|
3
|
+
// terminal Tui implements, but every event is tagged with the session id and
|
|
4
|
+
// handed to the hub, which multiplexes all sessions onto one SSE stream.
|
|
5
|
+
// Keeps a replay buffer (so a page that connects later sees the transcript),
|
|
6
|
+
// the pending input/approval promises, and a coarse status for the sidebar.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SessionChannel = void 0;
|
|
9
|
+
exports.uploadUrl = uploadUrl;
|
|
10
|
+
exports.stripAnsi = stripAnsi;
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
const REPLAY_CAP = 2000;
|
|
13
|
+
class SessionChannel {
|
|
14
|
+
id;
|
|
15
|
+
host;
|
|
16
|
+
slashCommands = [];
|
|
17
|
+
getStatus = () => "";
|
|
18
|
+
/** Structured snapshot for the page's status bar; assigned by the hub. */
|
|
19
|
+
getState = () => ({});
|
|
20
|
+
hintLeft = "";
|
|
21
|
+
onModeCycle = null;
|
|
22
|
+
onCancel = null;
|
|
23
|
+
onExit = null;
|
|
24
|
+
phase = "starting";
|
|
25
|
+
title = "";
|
|
26
|
+
busyLabel = null;
|
|
27
|
+
replay = [];
|
|
28
|
+
closed = false;
|
|
29
|
+
pendingInput = null;
|
|
30
|
+
inputQueue = [];
|
|
31
|
+
exitRequested = false;
|
|
32
|
+
pending = new Map();
|
|
33
|
+
askId = 0;
|
|
34
|
+
restoreReplay(events) {
|
|
35
|
+
this.replay = events.slice(-REPLAY_CAP).filter((e) => e.t !== "confirm" && e.t !== "select" && e.t !== "prompt" && e.t !== "answered");
|
|
36
|
+
this.askId = Math.max(0, ...events.map((e) => typeof e.id === "number" ? e.id : 0));
|
|
37
|
+
}
|
|
38
|
+
constructor(id, host) {
|
|
39
|
+
this.id = id;
|
|
40
|
+
this.host = host;
|
|
41
|
+
}
|
|
42
|
+
// ---- lifecycle -----------------------------------------------------------
|
|
43
|
+
start() { }
|
|
44
|
+
close() {
|
|
45
|
+
this.closed = true;
|
|
46
|
+
this.busyLabel = null;
|
|
47
|
+
this.resolvePending(null);
|
|
48
|
+
this.host.changed(this.id);
|
|
49
|
+
}
|
|
50
|
+
/** Make the session loop's next readInput() return "/exit" without echoing
|
|
51
|
+
* it as a user message. Used when a session is closed from the sidebar. */
|
|
52
|
+
requestExit() {
|
|
53
|
+
this.exitRequested = true;
|
|
54
|
+
if (this.pendingInput) {
|
|
55
|
+
const r = this.pendingInput;
|
|
56
|
+
this.pendingInput = null;
|
|
57
|
+
r("/exit");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---- input from the page -------------------------------------------------
|
|
61
|
+
/** A message from the page: text, attachments, or both. */
|
|
62
|
+
handleMessage(text, attachments = []) {
|
|
63
|
+
text = text.trim();
|
|
64
|
+
if ((!text && !attachments.length) || this.closed)
|
|
65
|
+
return;
|
|
66
|
+
const input = attachments.length ? { text, attachments } : text;
|
|
67
|
+
if (this.pendingInput) {
|
|
68
|
+
const resolve = this.pendingInput;
|
|
69
|
+
this.pendingInput = null;
|
|
70
|
+
this.accept(input);
|
|
71
|
+
resolve(input);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.inputQueue.push(input);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
handleAnswer(id, value) {
|
|
78
|
+
if (!this.pending.has(id))
|
|
79
|
+
return;
|
|
80
|
+
this.setStatus("busy");
|
|
81
|
+
this.settle(id, value);
|
|
82
|
+
}
|
|
83
|
+
/** Stop button / esc: answer any open prompt with "no"/cancel, then abort
|
|
84
|
+
* the turn. Without the first step a cancel during an approval prompt
|
|
85
|
+
* would leave the agent waiting on a promise nothing will resolve. */
|
|
86
|
+
cancel() {
|
|
87
|
+
this.resolvePending(null);
|
|
88
|
+
this.onCancel?.();
|
|
89
|
+
}
|
|
90
|
+
resolvePending(value) {
|
|
91
|
+
for (const id of [...this.pending.keys()])
|
|
92
|
+
this.settle(id, value);
|
|
93
|
+
}
|
|
94
|
+
/** Resolve one prompt and record the outcome in the transcript, so a page
|
|
95
|
+
* that replays it later (reconnect, resume) sees the answer, not live
|
|
96
|
+
* buttons for a question nobody is asking any more. */
|
|
97
|
+
settle(id, value) {
|
|
98
|
+
const p = this.pending.get(id);
|
|
99
|
+
if (!p)
|
|
100
|
+
return;
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
this.broadcast({ t: "answered", id });
|
|
103
|
+
if (p.kind === "confirm") {
|
|
104
|
+
const a = value === "always" ? "always allow" : value === "yes" ? "yes" : "no";
|
|
105
|
+
this.broadcast({ t: "line", kind: "status", s: `${a} — ${p.label}` });
|
|
106
|
+
}
|
|
107
|
+
else if (value === null || value === undefined || (p.kind === "prompt" && !String(value).trim())) {
|
|
108
|
+
this.broadcast({ t: "line", kind: "status", s: `cancelled — ${p.label}` });
|
|
109
|
+
}
|
|
110
|
+
else if (p.kind === "prompt") {
|
|
111
|
+
this.broadcast({ t: "line", kind: "status", s: `${p.label}: ${String(value).trim().slice(0, 200)}` });
|
|
112
|
+
}
|
|
113
|
+
p.resolve(value);
|
|
114
|
+
}
|
|
115
|
+
accept(input) {
|
|
116
|
+
const text = typeof input === "string" ? input : input.text;
|
|
117
|
+
const files = typeof input === "string"
|
|
118
|
+
? []
|
|
119
|
+
: input.attachments.map((a) => ({ id: a.id, name: a.name, kind: a.kind, size: a.size, url: uploadUrl(this.id, a.id) }));
|
|
120
|
+
this.setStatus("busy");
|
|
121
|
+
this.broadcast({ t: "user", s: text, ...(files.length ? { files } : {}) });
|
|
122
|
+
const label = text || files.map((f) => f.name).join(", ");
|
|
123
|
+
if (!this.title && !label.startsWith("/")) {
|
|
124
|
+
this.title = label.replace(/\s+/g, " ").slice(0, 60);
|
|
125
|
+
this.host.changed(this.id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
setStatus(s) {
|
|
129
|
+
if (this.phase === s)
|
|
130
|
+
return;
|
|
131
|
+
this.phase = s;
|
|
132
|
+
this.host.changed(this.id);
|
|
133
|
+
}
|
|
134
|
+
// ---- event plumbing ------------------------------------------------------
|
|
135
|
+
broadcast(ev) {
|
|
136
|
+
ev.sid = this.id;
|
|
137
|
+
if (ev.t !== "busy" && ev.t !== "state") {
|
|
138
|
+
const prev = this.replay[this.replay.length - 1];
|
|
139
|
+
if ((ev.t === "token" || ev.t === "thinking") && prev?.t === ev.t && String(prev.s).length + String(ev.s).length < 100_000) {
|
|
140
|
+
prev.s += ev.s;
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
this.replay.push({ ...ev });
|
|
144
|
+
if (this.replay.length > REPLAY_CAP)
|
|
145
|
+
this.replay.shift();
|
|
146
|
+
this.host.touched(this.id);
|
|
147
|
+
}
|
|
148
|
+
this.host.send(ev);
|
|
149
|
+
}
|
|
150
|
+
stateEvent() {
|
|
151
|
+
return { t: "state", sid: this.id, s: { ...this.getState(), busy: this.busyLabel, title: this.title } };
|
|
152
|
+
}
|
|
153
|
+
pushState() {
|
|
154
|
+
this.host.send(this.stateEvent());
|
|
155
|
+
}
|
|
156
|
+
refresh() {
|
|
157
|
+
this.pushState();
|
|
158
|
+
}
|
|
159
|
+
// ---- SessionUI -----------------------------------------------------------
|
|
160
|
+
readInput() {
|
|
161
|
+
this.busyLabel = null;
|
|
162
|
+
this.setStatus("idle");
|
|
163
|
+
this.pushState();
|
|
164
|
+
if (this.exitRequested || this.closed)
|
|
165
|
+
return Promise.resolve("/exit");
|
|
166
|
+
if (this.inputQueue.length) {
|
|
167
|
+
const text = this.inputQueue.shift();
|
|
168
|
+
this.accept(text);
|
|
169
|
+
return Promise.resolve(text);
|
|
170
|
+
}
|
|
171
|
+
return new Promise((resolve) => (this.pendingInput = resolve));
|
|
172
|
+
}
|
|
173
|
+
select(title, options) {
|
|
174
|
+
const id = ++this.askId;
|
|
175
|
+
this.broadcast({ t: "select", id, title, options });
|
|
176
|
+
this.setStatus("waiting");
|
|
177
|
+
return new Promise((resolve) => this.pending.set(id, { kind: "select", label: title, resolve: (i) => resolve(typeof i === "number" ? i : null) }));
|
|
178
|
+
}
|
|
179
|
+
prompt(title, placeholder = "") {
|
|
180
|
+
const id = ++this.askId;
|
|
181
|
+
this.broadcast({ t: "prompt", id, title, placeholder });
|
|
182
|
+
this.setStatus("waiting");
|
|
183
|
+
return new Promise((resolve) => this.pending.set(id, {
|
|
184
|
+
kind: "prompt",
|
|
185
|
+
label: title,
|
|
186
|
+
resolve: (s) => resolve(typeof s === "string" && s.trim() ? s.trim().slice(0, 500) : null),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
confirmCommand(command, reason) {
|
|
190
|
+
const id = ++this.askId;
|
|
191
|
+
this.broadcast({ t: "confirm", id, command, reason });
|
|
192
|
+
this.setStatus("waiting");
|
|
193
|
+
return new Promise((resolve) => this.pending.set(id, {
|
|
194
|
+
kind: "confirm",
|
|
195
|
+
label: command,
|
|
196
|
+
resolve: (a) => resolve(a === "always" ? "always" : a === "yes" ? "yes" : "no"),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
token(text) {
|
|
200
|
+
this.broadcast({ t: "token", s: text });
|
|
201
|
+
}
|
|
202
|
+
resetResponse() {
|
|
203
|
+
this.broadcast({ t: "response_reset" });
|
|
204
|
+
}
|
|
205
|
+
thinking(text) {
|
|
206
|
+
this.broadcast({ t: "thinking", s: text });
|
|
207
|
+
}
|
|
208
|
+
toolCall(name, args) {
|
|
209
|
+
this.broadcast({ t: "tool", name, summary: (0, ui_1.summarizeArgs)(name, args) });
|
|
210
|
+
}
|
|
211
|
+
toolResult(result) {
|
|
212
|
+
const lines = result.split("\n");
|
|
213
|
+
const first = lines[0] ?? "";
|
|
214
|
+
this.broadcast({
|
|
215
|
+
t: "result",
|
|
216
|
+
line: first.slice(0, 160),
|
|
217
|
+
err: first.startsWith("Error"),
|
|
218
|
+
extra: lines.length > 1 ? lines.length - 1 : 0,
|
|
219
|
+
body: result,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
planUpdated(plan) {
|
|
223
|
+
this.broadcast({ t: "plan", steps: plan.steps, current: plan.currentIndex });
|
|
224
|
+
this.pushState();
|
|
225
|
+
}
|
|
226
|
+
println(s = "") {
|
|
227
|
+
if (s.trim())
|
|
228
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
229
|
+
}
|
|
230
|
+
status(s) {
|
|
231
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
232
|
+
}
|
|
233
|
+
warn(s) {
|
|
234
|
+
this.broadcast({ t: "line", kind: "warn", s: stripAnsi(s) });
|
|
235
|
+
}
|
|
236
|
+
error(s) {
|
|
237
|
+
this.broadcast({ t: "line", kind: "error", s: stripAnsi(s) });
|
|
238
|
+
}
|
|
239
|
+
turnEnd(label) {
|
|
240
|
+
this.broadcast({ t: "turnend", label });
|
|
241
|
+
this.pushState();
|
|
242
|
+
}
|
|
243
|
+
startSpinner(label) {
|
|
244
|
+
this.busyLabel = label;
|
|
245
|
+
this.broadcast({ t: "busy", label });
|
|
246
|
+
}
|
|
247
|
+
stopSpinner() {
|
|
248
|
+
this.busyLabel = null;
|
|
249
|
+
this.broadcast({ t: "busy", label: null });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.SessionChannel = SessionChannel;
|
|
253
|
+
/** Where the page fetches a stored upload (it appends its own key). */
|
|
254
|
+
function uploadUrl(sid, id) {
|
|
255
|
+
return `/upload?sid=${sid}&id=${id}`;
|
|
256
|
+
}
|
|
257
|
+
function stripAnsi(s) {
|
|
258
|
+
// eslint-disable-next-line no-control-regex
|
|
259
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
260
|
+
}
|