rscot-agent 1.2.3 → 1.7.47
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/dist/agent-runtime.cjs +3824 -0
- package/dist/agent-runtime.js +3833 -0
- package/dist/index.js +404 -61
- package/package.json +1 -1
|
@@ -0,0 +1,3833 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var path8 = __toESM(require("node:path"));
|
|
28
|
+
var crypto2 = __toESM(require("node:crypto"));
|
|
29
|
+
var fs10 = __toESM(require("node:fs/promises"));
|
|
30
|
+
|
|
31
|
+
// src/apiClient.ts
|
|
32
|
+
var ApiClient = class {
|
|
33
|
+
constructor(settings, apiKey) {
|
|
34
|
+
this.settings = settings;
|
|
35
|
+
this.apiKey = apiKey;
|
|
36
|
+
}
|
|
37
|
+
headers() {
|
|
38
|
+
const h = {
|
|
39
|
+
"Content-Type": "application/json"
|
|
40
|
+
};
|
|
41
|
+
const t = this.settings.authBearer;
|
|
42
|
+
if (t) {
|
|
43
|
+
h["Authorization"] = `Bearer ${t}`;
|
|
44
|
+
h["X-CWA-Token"] = t;
|
|
45
|
+
}
|
|
46
|
+
return h;
|
|
47
|
+
}
|
|
48
|
+
async turn(req, opts) {
|
|
49
|
+
const body = {
|
|
50
|
+
...req,
|
|
51
|
+
api_key: this.apiKey,
|
|
52
|
+
provider: this.settings.provider,
|
|
53
|
+
model: this.settings.model || void 0,
|
|
54
|
+
base_url: this.settings.baseUrl || void 0,
|
|
55
|
+
temperature: this.settings.temperature,
|
|
56
|
+
stack_hint: req.stack_hint ?? (this.settings.stackHint || void 0)
|
|
57
|
+
};
|
|
58
|
+
const clean = {};
|
|
59
|
+
for (const [k, v] of Object.entries(body)) {
|
|
60
|
+
if (v !== void 0 && v !== null && v !== "") clean[k] = v;
|
|
61
|
+
}
|
|
62
|
+
if (!clean.messages || clean.messages.length === 0) {
|
|
63
|
+
throw new Error("apiClient.turn called with empty messages");
|
|
64
|
+
}
|
|
65
|
+
const url = `${this.settings.backendUrl}/code-workspace/turn`;
|
|
66
|
+
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : void 0;
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: this.headers(),
|
|
70
|
+
body: JSON.stringify(clean),
|
|
71
|
+
signal
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
let detail = "";
|
|
75
|
+
try {
|
|
76
|
+
detail = (await res.json())?.detail || "";
|
|
77
|
+
} catch {
|
|
78
|
+
detail = await res.text();
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Backend ${res.status}: ${detail || res.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
return await res.json();
|
|
83
|
+
}
|
|
84
|
+
async health() {
|
|
85
|
+
const res = await fetch(`${this.settings.backendUrl}/code-workspace/health`, {
|
|
86
|
+
headers: this.headers()
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) throw new Error(`health ${res.status}`);
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
91
|
+
async stacks() {
|
|
92
|
+
const res = await fetch(`${this.settings.backendUrl}/code-workspace/stacks`, {
|
|
93
|
+
headers: this.headers()
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) throw new Error(`stacks ${res.status}`);
|
|
96
|
+
return res.json();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Day 21 streaming sibling of turn(). Yields one StreamEvent at a time
|
|
100
|
+
* until the terminal event (done / message / tool_call / blocked / error).
|
|
101
|
+
*
|
|
102
|
+
* Event shape:
|
|
103
|
+
* {type: "delta", content?: string, reasoning?: string}
|
|
104
|
+
* {type: "status", status: "thinking", role: "architect"|"editor", model: string}
|
|
105
|
+
* {type: "assistant", assistant_message: any} // assembled, sent right before terminal
|
|
106
|
+
* {type: "tool_call" | "blocked" | "message" | "done", ...same as non-stream}
|
|
107
|
+
* {type: "error", message: string, detail?: string}
|
|
108
|
+
*/
|
|
109
|
+
async *turnStream(req, signal) {
|
|
110
|
+
const body = {
|
|
111
|
+
...req,
|
|
112
|
+
api_key: this.apiKey,
|
|
113
|
+
provider: this.settings.provider,
|
|
114
|
+
model: this.settings.model || void 0,
|
|
115
|
+
base_url: this.settings.baseUrl || void 0,
|
|
116
|
+
temperature: this.settings.temperature,
|
|
117
|
+
stack_hint: req.stack_hint ?? (this.settings.stackHint || void 0)
|
|
118
|
+
};
|
|
119
|
+
const clean = {};
|
|
120
|
+
for (const [k, v] of Object.entries(body)) {
|
|
121
|
+
if (v !== void 0 && v !== null && v !== "") clean[k] = v;
|
|
122
|
+
}
|
|
123
|
+
if (!clean.messages || clean.messages.length === 0) {
|
|
124
|
+
throw new Error("turnStream() called with empty messages");
|
|
125
|
+
}
|
|
126
|
+
const url = `${this.settings.backendUrl}/code-workspace/turn-stream`;
|
|
127
|
+
const res = await fetch(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: this.headers(),
|
|
130
|
+
body: JSON.stringify(clean),
|
|
131
|
+
signal
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok || !res.body) {
|
|
134
|
+
let detail = "";
|
|
135
|
+
try {
|
|
136
|
+
detail = (await res.json())?.detail || "";
|
|
137
|
+
} catch {
|
|
138
|
+
detail = await res.text();
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Backend ${res.status}: ${detail || res.statusText}`);
|
|
141
|
+
}
|
|
142
|
+
const reader = res.body.getReader();
|
|
143
|
+
const decoder = new TextDecoder();
|
|
144
|
+
let buf = "";
|
|
145
|
+
while (true) {
|
|
146
|
+
const { done, value } = await reader.read();
|
|
147
|
+
if (done) break;
|
|
148
|
+
buf += decoder.decode(value, { stream: true });
|
|
149
|
+
let blockEnd;
|
|
150
|
+
while ((blockEnd = buf.indexOf("\n\n")) !== -1) {
|
|
151
|
+
const block = buf.slice(0, blockEnd);
|
|
152
|
+
buf = buf.slice(blockEnd + 2);
|
|
153
|
+
const ev = parseSseBlock(block);
|
|
154
|
+
if (ev) yield ev;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function parseSseBlock(block) {
|
|
160
|
+
let event = "message";
|
|
161
|
+
let data = "";
|
|
162
|
+
for (const line of block.split("\n")) {
|
|
163
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
164
|
+
else if (line.startsWith("data:")) data = (data ? data + "\n" : "") + line.slice(5).trim();
|
|
165
|
+
}
|
|
166
|
+
if (!data) return null;
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(data);
|
|
169
|
+
return { type: event, ...parsed };
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/priorContext.ts
|
|
176
|
+
function jaccard(a, b) {
|
|
177
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
178
|
+
let intersection = 0;
|
|
179
|
+
for (const w of a) {
|
|
180
|
+
if (b.has(w)) intersection++;
|
|
181
|
+
}
|
|
182
|
+
const union = (/* @__PURE__ */ new Set([...a, ...b])).size;
|
|
183
|
+
return intersection / union;
|
|
184
|
+
}
|
|
185
|
+
function tokenize(text) {
|
|
186
|
+
return new Set(
|
|
187
|
+
text.toLowerCase().split(/\W+/).filter((w) => w.length >= 4)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
async function buildPriorContext(store, prompt2, maxResults = 3) {
|
|
191
|
+
const sessions = await store.listAll();
|
|
192
|
+
if (sessions.length === 0) return "";
|
|
193
|
+
const promptWords = tokenize(prompt2);
|
|
194
|
+
if (promptWords.size === 0) return "";
|
|
195
|
+
const scored = [];
|
|
196
|
+
for (const s of sessions) {
|
|
197
|
+
const summaryWords = tokenize(s.summary);
|
|
198
|
+
if (summaryWords.size === 0) continue;
|
|
199
|
+
const sim = jaccard(promptWords, summaryWords);
|
|
200
|
+
const bonus = s.completed ? 0.1 : 0;
|
|
201
|
+
const score = sim + bonus;
|
|
202
|
+
if (score >= 0.05) {
|
|
203
|
+
scored.push({
|
|
204
|
+
created_at: s.created_at,
|
|
205
|
+
messages_count: s.messages_count,
|
|
206
|
+
summary: s.summary,
|
|
207
|
+
score
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (scored.length === 0) return "";
|
|
212
|
+
scored.sort((a, b) => {
|
|
213
|
+
const diff = b.score - a.score;
|
|
214
|
+
if (diff !== 0) return diff;
|
|
215
|
+
return b.created_at.localeCompare(a.created_at);
|
|
216
|
+
});
|
|
217
|
+
const top = scored.slice(0, maxResults);
|
|
218
|
+
if (top.length === 0) return "";
|
|
219
|
+
const lines = [
|
|
220
|
+
"## Prior sessions in this workspace (for your context)",
|
|
221
|
+
""
|
|
222
|
+
];
|
|
223
|
+
for (let i = 0; i < top.length; i++) {
|
|
224
|
+
const s = top[i];
|
|
225
|
+
lines.push(
|
|
226
|
+
`${i + 1}. ${s.created_at}, ${s.messages_count} msgs \u2014 ${s.summary}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/conversationStore.ts
|
|
233
|
+
var fs = __toESM(require("node:fs/promises"));
|
|
234
|
+
var path = __toESM(require("node:path"));
|
|
235
|
+
var crypto = __toESM(require("node:crypto"));
|
|
236
|
+
var ConversationStore = class {
|
|
237
|
+
dir;
|
|
238
|
+
convDir;
|
|
239
|
+
current = null;
|
|
240
|
+
constructor(workspaceRoot3) {
|
|
241
|
+
this.dir = path.join(workspaceRoot3, ".rscot");
|
|
242
|
+
this.convDir = path.join(this.dir, "conversations");
|
|
243
|
+
}
|
|
244
|
+
async ensureDir() {
|
|
245
|
+
await fs.mkdir(this.convDir, { recursive: true });
|
|
246
|
+
const gi = path.join(this.dir, ".gitignore");
|
|
247
|
+
try {
|
|
248
|
+
await fs.access(gi);
|
|
249
|
+
} catch {
|
|
250
|
+
await fs.writeFile(gi, "# Auto-generated by Rscot Agent\n*\n", "utf-8");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Start a fresh session. Returns the session id. */
|
|
254
|
+
async startNew(opts) {
|
|
255
|
+
await this.ensureDir();
|
|
256
|
+
const id = crypto.randomBytes(4).toString("hex");
|
|
257
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
this.current = {
|
|
259
|
+
session_id: id,
|
|
260
|
+
created_at: now,
|
|
261
|
+
last_updated: now,
|
|
262
|
+
workspace: opts.workspaceRoot,
|
|
263
|
+
stack_hint: opts.stackHint || "",
|
|
264
|
+
messages: [],
|
|
265
|
+
summary: (opts.firstPrompt || "").slice(0, 80),
|
|
266
|
+
completed: false,
|
|
267
|
+
files_changed: [],
|
|
268
|
+
pinned: false,
|
|
269
|
+
archived: false
|
|
270
|
+
};
|
|
271
|
+
await this.flush();
|
|
272
|
+
await fs.writeFile(path.join(this.dir, "last_session_id"), id, "utf-8");
|
|
273
|
+
return id;
|
|
274
|
+
}
|
|
275
|
+
/** Load a session by id and make it `current`. Returns null if missing. */
|
|
276
|
+
async loadById(sessionId) {
|
|
277
|
+
try {
|
|
278
|
+
const candidates = await fs.readdir(this.convDir);
|
|
279
|
+
const match = candidates.find((f) => f.endsWith(`_${sessionId}.json`));
|
|
280
|
+
if (!match) return null;
|
|
281
|
+
const raw = await fs.readFile(path.join(this.convDir, match), "utf-8");
|
|
282
|
+
const session = this.applyDefaults(JSON.parse(raw));
|
|
283
|
+
this.current = session;
|
|
284
|
+
await fs.writeFile(path.join(this.dir, "last_session_id"), sessionId, "utf-8").catch(() => void 0);
|
|
285
|
+
return session;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Resume the most recent saved session, returning its messages + meta.
|
|
291
|
+
* Returns null if no prior session exists. */
|
|
292
|
+
async resumeLatest() {
|
|
293
|
+
try {
|
|
294
|
+
const idTxt = (await fs.readFile(path.join(this.dir, "last_session_id"), "utf-8")).trim();
|
|
295
|
+
if (!idTxt) return null;
|
|
296
|
+
const candidates = await fs.readdir(this.convDir);
|
|
297
|
+
const match = candidates.find((f) => f.endsWith(`_${idTxt}.json`));
|
|
298
|
+
if (!match) return null;
|
|
299
|
+
const raw = await fs.readFile(path.join(this.convDir, match), "utf-8");
|
|
300
|
+
const session = this.applyDefaults(JSON.parse(raw));
|
|
301
|
+
this.current = session;
|
|
302
|
+
return session;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Append a message + bump last_updated. Caller should call after every
|
|
308
|
+
* POST /turn returns. */
|
|
309
|
+
async appendMessage(msg) {
|
|
310
|
+
if (!this.current) return;
|
|
311
|
+
this.current.messages.push(msg);
|
|
312
|
+
this.current.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
313
|
+
await this.flush();
|
|
314
|
+
}
|
|
315
|
+
/** Mark the session as completed (after `done` is received). */
|
|
316
|
+
async markComplete(filesChanged) {
|
|
317
|
+
if (!this.current) return;
|
|
318
|
+
this.current.completed = true;
|
|
319
|
+
this.current.files_changed = filesChanged || [];
|
|
320
|
+
this.current.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
321
|
+
await this.flush();
|
|
322
|
+
}
|
|
323
|
+
getCurrentId() {
|
|
324
|
+
return this.current?.session_id ?? null;
|
|
325
|
+
}
|
|
326
|
+
getMessages() {
|
|
327
|
+
return this.current?.messages ?? [];
|
|
328
|
+
}
|
|
329
|
+
/** List all stored sessions (most recent first) for a session-picker UI. */
|
|
330
|
+
async listAll() {
|
|
331
|
+
try {
|
|
332
|
+
await fs.access(this.convDir);
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const files = await fs.readdir(this.convDir);
|
|
337
|
+
const out = [];
|
|
338
|
+
for (const f of files) {
|
|
339
|
+
if (!f.endsWith(".json")) continue;
|
|
340
|
+
try {
|
|
341
|
+
const raw = await fs.readFile(path.join(this.convDir, f), "utf-8");
|
|
342
|
+
const s = this.applyDefaults(JSON.parse(raw));
|
|
343
|
+
out.push({
|
|
344
|
+
session_id: s.session_id,
|
|
345
|
+
created_at: s.created_at,
|
|
346
|
+
summary: s.summary,
|
|
347
|
+
completed: s.completed,
|
|
348
|
+
messages_count: s.messages.length,
|
|
349
|
+
pinned: s.pinned ?? false,
|
|
350
|
+
archived: s.archived ?? false
|
|
351
|
+
});
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
out.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
// ── Backward-compat defaults ─────────────────────────────────────
|
|
359
|
+
/** Ensure old session files missing new fields get safe defaults. */
|
|
360
|
+
applyDefaults(s) {
|
|
361
|
+
if (s.pinned === void 0) s.pinned = false;
|
|
362
|
+
if (s.archived === void 0) s.archived = false;
|
|
363
|
+
return s;
|
|
364
|
+
}
|
|
365
|
+
/** Read a session by id directly (no side-effects on `current`). */
|
|
366
|
+
async readSessionById(sessionId) {
|
|
367
|
+
const filename = await this.findFilenameById(sessionId);
|
|
368
|
+
if (!filename) return null;
|
|
369
|
+
const raw = await fs.readFile(path.join(this.convDir, filename), "utf-8");
|
|
370
|
+
return { session: this.applyDefaults(JSON.parse(raw)), filename };
|
|
371
|
+
}
|
|
372
|
+
// ── A03+U01: Delete & Rename ──────────────────────────────────────
|
|
373
|
+
/** Delete a session file by id. If the deleted session is the current
|
|
374
|
+
* one, clear the in-memory current and the last_session_id pointer. */
|
|
375
|
+
async delete(sessionId) {
|
|
376
|
+
const filename = await this.findFilenameById(sessionId);
|
|
377
|
+
if (!filename) throw new Error(`Session not found: ${sessionId}`);
|
|
378
|
+
await fs.unlink(path.join(this.convDir, filename));
|
|
379
|
+
if (this.current?.session_id === sessionId) {
|
|
380
|
+
this.current = null;
|
|
381
|
+
const lastIdPath = path.join(this.dir, "last_session_id");
|
|
382
|
+
try {
|
|
383
|
+
await fs.unlink(lastIdPath);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** Rename (re-title) a session by id. Updates the `summary` field. */
|
|
389
|
+
async rename(sessionId, newTitle) {
|
|
390
|
+
const filename = await this.findFilenameById(sessionId);
|
|
391
|
+
if (!filename) throw new Error(`Session not found: ${sessionId}`);
|
|
392
|
+
const filePath = path.join(this.convDir, filename);
|
|
393
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
394
|
+
const session = JSON.parse(raw);
|
|
395
|
+
session.summary = newTitle.slice(0, 80);
|
|
396
|
+
session.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
397
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
398
|
+
if (this.current?.session_id === sessionId) {
|
|
399
|
+
this.current.summary = session.summary;
|
|
400
|
+
this.current.last_updated = session.last_updated;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ── U03: Search ─────────────────────────────────────────────────────
|
|
404
|
+
/** Full-text search across all conversation messages.
|
|
405
|
+
* Scans every session file; returns up to 50 results sorted by created_at desc.
|
|
406
|
+
* Each result includes an 80-char snippet around the first match. */
|
|
407
|
+
async search(query) {
|
|
408
|
+
const lower = query.toLowerCase();
|
|
409
|
+
const all = await this.listAll();
|
|
410
|
+
const results = [];
|
|
411
|
+
for (const entry of all) {
|
|
412
|
+
const found = await this.readSessionById(entry.session_id);
|
|
413
|
+
if (!found) continue;
|
|
414
|
+
const { session } = found;
|
|
415
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
416
|
+
const content = session.messages[i].content || "";
|
|
417
|
+
const idx = content.toLowerCase().indexOf(lower);
|
|
418
|
+
if (idx !== -1) {
|
|
419
|
+
const start = Math.max(0, idx - 40);
|
|
420
|
+
const end = Math.min(content.length, idx + query.length + 40);
|
|
421
|
+
let snippet = content.slice(start, end);
|
|
422
|
+
if (start > 0) snippet = "\u2026" + snippet;
|
|
423
|
+
if (end < content.length) snippet = snippet + "\u2026";
|
|
424
|
+
results.push({
|
|
425
|
+
sessionId: session.session_id,
|
|
426
|
+
summary: session.summary,
|
|
427
|
+
matchedSnippet: snippet,
|
|
428
|
+
matchedTurnIndex: i,
|
|
429
|
+
createdAt: session.created_at
|
|
430
|
+
});
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
436
|
+
return results.slice(0, 50).map(({ createdAt: _, ...rest }) => rest);
|
|
437
|
+
}
|
|
438
|
+
// ── U04: Export ──────────────────────────────────────────────────────
|
|
439
|
+
/** Export a conversation as a human-readable Markdown string. */
|
|
440
|
+
async exportAsMarkdown(sessionId) {
|
|
441
|
+
const found = await this.readSessionById(sessionId);
|
|
442
|
+
if (!found) throw new Error(`Session not found: ${sessionId}`);
|
|
443
|
+
const { session } = found;
|
|
444
|
+
const lines = [];
|
|
445
|
+
lines.push(`# ${session.summary || "Untitled Session"}`);
|
|
446
|
+
lines.push("");
|
|
447
|
+
lines.push(`**Session ID:** ${session.session_id}`);
|
|
448
|
+
lines.push(`**Created:** ${session.created_at}`);
|
|
449
|
+
lines.push(`**Last updated:** ${session.last_updated}`);
|
|
450
|
+
lines.push(`**Messages:** ${session.messages.length}`);
|
|
451
|
+
lines.push(`**Completed:** ${session.completed}`);
|
|
452
|
+
lines.push("");
|
|
453
|
+
for (const msg of session.messages) {
|
|
454
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
455
|
+
lines.push(`## ${role}:`);
|
|
456
|
+
lines.push("");
|
|
457
|
+
lines.push(msg.content || "");
|
|
458
|
+
lines.push("");
|
|
459
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
460
|
+
for (const tc of msg.tool_calls) {
|
|
461
|
+
lines.push(`### Tool call: ${tc.name}`);
|
|
462
|
+
lines.push("```");
|
|
463
|
+
lines.push(`args: ${JSON.stringify(tc.args, null, 2)}`);
|
|
464
|
+
if (tc.result !== void 0) {
|
|
465
|
+
lines.push(`result: ${JSON.stringify(tc.result, null, 2)}`);
|
|
466
|
+
}
|
|
467
|
+
lines.push("```");
|
|
468
|
+
lines.push("");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
/** Export a conversation as raw JSON. */
|
|
475
|
+
async exportAsJson(sessionId) {
|
|
476
|
+
const found = await this.readSessionById(sessionId);
|
|
477
|
+
if (!found) throw new Error(`Session not found: ${sessionId}`);
|
|
478
|
+
return JSON.stringify(found.session, null, 2);
|
|
479
|
+
}
|
|
480
|
+
// ── U05: Fork ────────────────────────────────────────────────────────
|
|
481
|
+
/** Fork (branch) a session at a specific turn index.
|
|
482
|
+
* Returns the new session id. */
|
|
483
|
+
async fork(sourceId, upToTurnIndex, newSummary) {
|
|
484
|
+
const found = await this.readSessionById(sourceId);
|
|
485
|
+
if (!found) throw new Error(`Session not found: ${sourceId}`);
|
|
486
|
+
const { session } = found;
|
|
487
|
+
const newId = crypto.randomBytes(4).toString("hex");
|
|
488
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
+
const slicedMessages = session.messages.slice(0, upToTurnIndex + 1);
|
|
490
|
+
const newSession = {
|
|
491
|
+
session_id: newId,
|
|
492
|
+
created_at: now,
|
|
493
|
+
last_updated: now,
|
|
494
|
+
workspace: session.workspace,
|
|
495
|
+
stack_hint: session.stack_hint,
|
|
496
|
+
messages: slicedMessages,
|
|
497
|
+
summary: newSummary ?? session.summary,
|
|
498
|
+
completed: false,
|
|
499
|
+
files_changed: [],
|
|
500
|
+
pinned: false,
|
|
501
|
+
archived: false
|
|
502
|
+
};
|
|
503
|
+
await this.ensureDir();
|
|
504
|
+
const stamp = now.replace(/[:.]/g, "-").replace(/Z$/, "Z");
|
|
505
|
+
const filename = `${stamp}_${newId}.json`;
|
|
506
|
+
await fs.writeFile(
|
|
507
|
+
path.join(this.convDir, filename),
|
|
508
|
+
JSON.stringify(newSession, null, 2),
|
|
509
|
+
"utf-8"
|
|
510
|
+
);
|
|
511
|
+
return newId;
|
|
512
|
+
}
|
|
513
|
+
// ── U06: Pin / Archive ────────────────────────────────────────────────
|
|
514
|
+
/** Set the pinned flag on a session. */
|
|
515
|
+
async setPinned(sessionId, pinned) {
|
|
516
|
+
await this.setFlag(sessionId, "pinned", pinned);
|
|
517
|
+
}
|
|
518
|
+
/** Set the archived flag on a session. */
|
|
519
|
+
async setArchived(sessionId, archived) {
|
|
520
|
+
await this.setFlag(sessionId, "archived", archived);
|
|
521
|
+
}
|
|
522
|
+
/** Generic helper to set a boolean flag on a session and persist. */
|
|
523
|
+
async setFlag(sessionId, field, value) {
|
|
524
|
+
const filename = await this.findFilenameById(sessionId);
|
|
525
|
+
if (!filename) throw new Error(`Session not found: ${sessionId}`);
|
|
526
|
+
const filePath = path.join(this.convDir, filename);
|
|
527
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
528
|
+
const session = this.applyDefaults(JSON.parse(raw));
|
|
529
|
+
session[field] = value;
|
|
530
|
+
session.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
531
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
532
|
+
if (this.current?.session_id === sessionId) {
|
|
533
|
+
this.current[field] = value;
|
|
534
|
+
this.current.last_updated = session.last_updated;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Resolve a session id to the on-disk filename. Returns null if not found. */
|
|
538
|
+
async findFilenameById(sessionId) {
|
|
539
|
+
try {
|
|
540
|
+
const candidates = await fs.readdir(this.convDir);
|
|
541
|
+
const match = candidates.find((f) => f.endsWith(`_${sessionId}.json`));
|
|
542
|
+
return match ?? null;
|
|
543
|
+
} catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async flush() {
|
|
548
|
+
if (!this.current) return;
|
|
549
|
+
const stamp = this.current.created_at.replace(/[:.]/g, "-").replace(/Z$/, "Z");
|
|
550
|
+
const filename = `${stamp}_${this.current.session_id}.json`;
|
|
551
|
+
await fs.writeFile(
|
|
552
|
+
path.join(this.convDir, filename),
|
|
553
|
+
JSON.stringify(this.current, null, 2),
|
|
554
|
+
"utf-8"
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/executors/filesystem.ts
|
|
560
|
+
var fs2 = __toESM(require("node:fs/promises"));
|
|
561
|
+
var path3 = __toESM(require("node:path"));
|
|
562
|
+
|
|
563
|
+
// src/workspace.ts
|
|
564
|
+
var path2 = __toESM(require("node:path"));
|
|
565
|
+
var NoWorkspaceError = class extends Error {
|
|
566
|
+
constructor() {
|
|
567
|
+
super("No workspace folder is open. Open a folder before invoking the agent.");
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
var PathScopeError = class extends Error {
|
|
571
|
+
constructor(reason) {
|
|
572
|
+
super(`Path scope violation: ${reason}`);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var _override;
|
|
576
|
+
function setWorkspaceRoot(absPath) {
|
|
577
|
+
_override = path2.resolve(absPath);
|
|
578
|
+
}
|
|
579
|
+
function workspaceRoot() {
|
|
580
|
+
if (_override) return _override;
|
|
581
|
+
let vscode;
|
|
582
|
+
try {
|
|
583
|
+
vscode = require("vscode");
|
|
584
|
+
} catch {
|
|
585
|
+
throw new NoWorkspaceError();
|
|
586
|
+
}
|
|
587
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
588
|
+
if (!folders || folders.length === 0) throw new NoWorkspaceError();
|
|
589
|
+
return folders[0].uri.fsPath;
|
|
590
|
+
}
|
|
591
|
+
function resolveInWorkspace(givenPath, opts) {
|
|
592
|
+
const root = workspaceRoot();
|
|
593
|
+
if (!givenPath || typeof givenPath !== "string") {
|
|
594
|
+
throw new PathScopeError("empty path");
|
|
595
|
+
}
|
|
596
|
+
const cleaned = givenPath.trim();
|
|
597
|
+
const extras = (opts?.readableExtras || []).map((e) => path2.resolve(e));
|
|
598
|
+
if (path2.isAbsolute(cleaned)) {
|
|
599
|
+
const resolved2 = path2.resolve(cleaned);
|
|
600
|
+
if (isInside(root, resolved2)) return resolved2;
|
|
601
|
+
for (const ex of extras) {
|
|
602
|
+
if (isInside(ex, resolved2)) return resolved2;
|
|
603
|
+
}
|
|
604
|
+
throw new PathScopeError(
|
|
605
|
+
`absolute path ${JSON.stringify(cleaned)} outside workspace` + (extras.length ? ` (and not under any mounted folder: ${extras.join(", ")})` : "")
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const resolved = path2.resolve(root, cleaned);
|
|
609
|
+
if (!isInside(root, resolved)) {
|
|
610
|
+
throw new PathScopeError(`relative path ${JSON.stringify(cleaned)} escapes workspace via '..'`);
|
|
611
|
+
}
|
|
612
|
+
return resolved;
|
|
613
|
+
}
|
|
614
|
+
function isInside(root, candidate) {
|
|
615
|
+
const r = path2.resolve(root);
|
|
616
|
+
const c = path2.resolve(candidate);
|
|
617
|
+
if (c === r) return true;
|
|
618
|
+
const rel = path2.relative(r, c);
|
|
619
|
+
return !!rel && !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
620
|
+
}
|
|
621
|
+
function toWorkspaceRel(abs) {
|
|
622
|
+
const root = workspaceRoot();
|
|
623
|
+
const r = path2.resolve(abs);
|
|
624
|
+
const rel = path2.relative(root, r);
|
|
625
|
+
if (rel && !rel.startsWith("..") && !path2.isAbsolute(rel)) {
|
|
626
|
+
return rel.split(path2.sep).join("/");
|
|
627
|
+
}
|
|
628
|
+
return r.split(path2.sep).join("/");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/executors/filesystem.ts
|
|
632
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
633
|
+
".git",
|
|
634
|
+
"node_modules",
|
|
635
|
+
"__pycache__",
|
|
636
|
+
".venv",
|
|
637
|
+
"venv",
|
|
638
|
+
"target",
|
|
639
|
+
"dist",
|
|
640
|
+
"build",
|
|
641
|
+
".next",
|
|
642
|
+
".cache",
|
|
643
|
+
".pytest_cache"
|
|
644
|
+
]);
|
|
645
|
+
async function listDir(args, mountedFolders) {
|
|
646
|
+
const max_depth = Math.max(1, Math.min(args.max_depth ?? 4, 10));
|
|
647
|
+
const extras = mountedFolders ? Array.from(mountedFolders) : [];
|
|
648
|
+
const root = resolveInWorkspace(args.path || ".", { readableExtras: extras });
|
|
649
|
+
const entries = [];
|
|
650
|
+
let truncated = false;
|
|
651
|
+
async function walk(dir, depth) {
|
|
652
|
+
if (depth > max_depth) return;
|
|
653
|
+
let items;
|
|
654
|
+
try {
|
|
655
|
+
items = await fs2.readdir(dir, { withFileTypes: true });
|
|
656
|
+
} catch (err) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
for (const it of items) {
|
|
660
|
+
if (entries.length >= 500) {
|
|
661
|
+
truncated = true;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (it.isDirectory() && SKIP_DIRS.has(it.name)) continue;
|
|
665
|
+
const full = path3.join(dir, it.name);
|
|
666
|
+
const rel = toWorkspaceRel(full);
|
|
667
|
+
if (it.isDirectory()) {
|
|
668
|
+
entries.push({ path: rel, size_bytes: null, is_dir: true });
|
|
669
|
+
await walk(full, depth + 1);
|
|
670
|
+
} else if (it.isFile()) {
|
|
671
|
+
let size = 0;
|
|
672
|
+
try {
|
|
673
|
+
size = (await fs2.stat(full)).size;
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
entries.push({ path: rel, size_bytes: size, is_dir: false });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
await walk(root, 1);
|
|
681
|
+
return {
|
|
682
|
+
path: toWorkspaceRel(root) || ".",
|
|
683
|
+
entries,
|
|
684
|
+
truncated
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
var MAX_FILE_BYTES = 200 * 1024;
|
|
688
|
+
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
689
|
+
".png",
|
|
690
|
+
".jpg",
|
|
691
|
+
".jpeg",
|
|
692
|
+
".gif",
|
|
693
|
+
".bmp",
|
|
694
|
+
".tiff",
|
|
695
|
+
".webp",
|
|
696
|
+
".ico",
|
|
697
|
+
".pdf",
|
|
698
|
+
".zip",
|
|
699
|
+
".tar",
|
|
700
|
+
".gz",
|
|
701
|
+
".bz2",
|
|
702
|
+
".7z",
|
|
703
|
+
".rar",
|
|
704
|
+
".so",
|
|
705
|
+
".dll",
|
|
706
|
+
".dylib",
|
|
707
|
+
".exe",
|
|
708
|
+
".bin",
|
|
709
|
+
".o",
|
|
710
|
+
".a",
|
|
711
|
+
".mp3",
|
|
712
|
+
".mp4",
|
|
713
|
+
".mov",
|
|
714
|
+
".avi",
|
|
715
|
+
".webm",
|
|
716
|
+
".wav",
|
|
717
|
+
".class",
|
|
718
|
+
".jar",
|
|
719
|
+
".pyc",
|
|
720
|
+
".pyo"
|
|
721
|
+
]);
|
|
722
|
+
async function readFile3(args, mountedFolders) {
|
|
723
|
+
const extras = mountedFolders ? Array.from(mountedFolders) : [];
|
|
724
|
+
const abs = resolveInWorkspace(args.path, { readableExtras: extras });
|
|
725
|
+
const ext = path3.extname(abs).toLowerCase();
|
|
726
|
+
if (BINARY_EXT.has(ext)) {
|
|
727
|
+
return { error: "binary file refused", path: args.path, extension: ext };
|
|
728
|
+
}
|
|
729
|
+
let stat5;
|
|
730
|
+
try {
|
|
731
|
+
stat5 = await fs2.stat(abs);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return { error: `${err.code || "ENOENT"}: ${err.message}`, path: args.path };
|
|
734
|
+
}
|
|
735
|
+
if (stat5.size > MAX_FILE_BYTES && (args.start_line == null || args.end_line == null)) {
|
|
736
|
+
return {
|
|
737
|
+
error: `file too large (${stat5.size} bytes > ${MAX_FILE_BYTES}); specify start_line + end_line`,
|
|
738
|
+
path: args.path,
|
|
739
|
+
size_bytes: stat5.size
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const content = await fs2.readFile(abs, "utf-8");
|
|
743
|
+
const lines = content.split(/\r?\n/);
|
|
744
|
+
if (args.start_line || args.end_line) {
|
|
745
|
+
const s = Math.max(1, args.start_line || 1);
|
|
746
|
+
const e = Math.min(lines.length, args.end_line || lines.length);
|
|
747
|
+
const sliced = lines.slice(s - 1, e).join("\n");
|
|
748
|
+
return {
|
|
749
|
+
path: args.path,
|
|
750
|
+
content: sliced,
|
|
751
|
+
encoding: "utf-8",
|
|
752
|
+
total_lines: lines.length,
|
|
753
|
+
returned_lines: [s, e]
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
path: args.path,
|
|
758
|
+
content,
|
|
759
|
+
encoding: "utf-8",
|
|
760
|
+
total_lines: lines.length,
|
|
761
|
+
returned_lines: [1, lines.length]
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function writeFile3(args, rb) {
|
|
765
|
+
const abs = resolveInWorkspace(args.path);
|
|
766
|
+
await fs2.mkdir(path3.dirname(abs), { recursive: true });
|
|
767
|
+
let previousSize = null;
|
|
768
|
+
let created = true;
|
|
769
|
+
try {
|
|
770
|
+
const s = await fs2.stat(abs);
|
|
771
|
+
previousSize = s.size;
|
|
772
|
+
created = false;
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
const rollback_token = await rb.snapshot(abs);
|
|
776
|
+
await fs2.writeFile(abs, args.content, "utf-8");
|
|
777
|
+
return {
|
|
778
|
+
path: args.path,
|
|
779
|
+
bytes_written: Buffer.byteLength(args.content, "utf-8"),
|
|
780
|
+
previous_size_bytes: previousSize,
|
|
781
|
+
created,
|
|
782
|
+
rollback_token
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async function editFile(args, rb) {
|
|
786
|
+
const abs = resolveInWorkspace(args.path);
|
|
787
|
+
const expected = args.expect_count ?? 1;
|
|
788
|
+
if (typeof args.search !== "string" || args.search.length === 0) {
|
|
789
|
+
return { error: "empty_search", path: args.path };
|
|
790
|
+
}
|
|
791
|
+
let content;
|
|
792
|
+
try {
|
|
793
|
+
content = await fs2.readFile(abs, "utf-8");
|
|
794
|
+
} catch (err) {
|
|
795
|
+
return { error: err.code || "ENOENT", path: args.path, message: err.message };
|
|
796
|
+
}
|
|
797
|
+
let count = 0;
|
|
798
|
+
let idx = 0;
|
|
799
|
+
while ((idx = content.indexOf(args.search, idx)) !== -1) {
|
|
800
|
+
count++;
|
|
801
|
+
idx += args.search.length;
|
|
802
|
+
}
|
|
803
|
+
if (count !== expected) {
|
|
804
|
+
const result = {
|
|
805
|
+
error: "match_count_mismatch",
|
|
806
|
+
path: args.path,
|
|
807
|
+
expected_count: expected,
|
|
808
|
+
actual_count: count,
|
|
809
|
+
hint: count === 0 ? "search string not found \u2014 see closest_match for the most-similar block actually in the file, then adjust your 'search' to match exactly OR use apply_diff for context-aware patching" : `found ${count} matches but expected ${expected}. Either widen 'search' for uniqueness, or pass expect_count=${count} if all should be replaced.`
|
|
810
|
+
};
|
|
811
|
+
if (count === 0) {
|
|
812
|
+
const cm = _findClosestMatch(content, args.search);
|
|
813
|
+
if (cm) result.closest_match = cm;
|
|
814
|
+
}
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
const rollback_token = await rb.snapshot(abs);
|
|
818
|
+
const next = content.split(args.search).join(args.replace);
|
|
819
|
+
await fs2.writeFile(abs, next, "utf-8");
|
|
820
|
+
return {
|
|
821
|
+
path: args.path,
|
|
822
|
+
applied: true,
|
|
823
|
+
matches_replaced: count,
|
|
824
|
+
rollback_token
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function _findClosestMatch(content, search) {
|
|
828
|
+
const searchLines = search.split(/\r?\n/);
|
|
829
|
+
const fileLines = content.split(/\r?\n/);
|
|
830
|
+
const N = searchLines.length;
|
|
831
|
+
if (N === 0 || fileLines.length < N) return null;
|
|
832
|
+
const firstSearch = searchLines[0].trim();
|
|
833
|
+
const firstTok = firstSearch.split(/[\s(){}.,;:=]/).filter((t) => t.length > 2);
|
|
834
|
+
let best = null;
|
|
835
|
+
for (let i = 0; i <= fileLines.length - N; i++) {
|
|
836
|
+
const candidateFirst = fileLines[i];
|
|
837
|
+
if (firstTok.length > 0 && !firstTok.some((t) => candidateFirst.includes(t))) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
let exact = 0;
|
|
841
|
+
let charSim = 0;
|
|
842
|
+
for (let j = 0; j < N; j++) {
|
|
843
|
+
const a = searchLines[j], b = fileLines[i + j];
|
|
844
|
+
if (a === b) {
|
|
845
|
+
exact++;
|
|
846
|
+
charSim += 1;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
charSim += _diceCoefficient(a, b);
|
|
850
|
+
}
|
|
851
|
+
const score = (exact * 1 + charSim) / (2 * N);
|
|
852
|
+
if (!best || score > best.score) {
|
|
853
|
+
best = { start: i, score };
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (!best || best.score < 0.25) return null;
|
|
857
|
+
const actualLines = fileLines.slice(best.start, best.start + N);
|
|
858
|
+
return {
|
|
859
|
+
start_line: best.start + 1,
|
|
860
|
+
// 1-indexed
|
|
861
|
+
end_line: best.start + N,
|
|
862
|
+
similarity: Math.round(best.score * 100),
|
|
863
|
+
actual: actualLines.join("\n")
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function _diceCoefficient(a, b) {
|
|
867
|
+
if (a === b) return 1;
|
|
868
|
+
if (a.length < 2 || b.length < 2) return 0;
|
|
869
|
+
const bigrams = (s) => {
|
|
870
|
+
const out = /* @__PURE__ */ new Map();
|
|
871
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
872
|
+
const g = s.slice(i, i + 2);
|
|
873
|
+
out.set(g, (out.get(g) || 0) + 1);
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
};
|
|
877
|
+
const A = bigrams(a), B = bigrams(b);
|
|
878
|
+
let intersection = 0;
|
|
879
|
+
for (const [g, ca] of A) {
|
|
880
|
+
const cb = B.get(g) || 0;
|
|
881
|
+
intersection += Math.min(ca, cb);
|
|
882
|
+
}
|
|
883
|
+
return 2 * intersection / (a.length - 1 + b.length - 1);
|
|
884
|
+
}
|
|
885
|
+
async function glob(args, mountedFolders) {
|
|
886
|
+
const extras = mountedFolders ? Array.from(mountedFolders) : [];
|
|
887
|
+
const root = resolveInWorkspace(args.path || ".", { readableExtras: extras });
|
|
888
|
+
const max = Math.max(1, Math.min(args.max_results ?? 200, 2e3));
|
|
889
|
+
const re = globToRegExp(args.pattern);
|
|
890
|
+
const matches = [];
|
|
891
|
+
let truncated = false;
|
|
892
|
+
async function walk(dir) {
|
|
893
|
+
if (matches.length >= max) {
|
|
894
|
+
truncated = true;
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
let items;
|
|
898
|
+
try {
|
|
899
|
+
items = await fs2.readdir(dir, { withFileTypes: true });
|
|
900
|
+
} catch {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
for (const it of items) {
|
|
904
|
+
if (matches.length >= max) {
|
|
905
|
+
truncated = true;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (it.isDirectory() && SKIP_DIRS.has(it.name)) continue;
|
|
909
|
+
const full = path3.join(dir, it.name);
|
|
910
|
+
const rel = toWorkspaceRel(full);
|
|
911
|
+
if (it.isFile() && re.test(rel)) matches.push(rel);
|
|
912
|
+
if (it.isDirectory()) await walk(full);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
await walk(root);
|
|
916
|
+
return { pattern: args.pattern, matches, truncated };
|
|
917
|
+
}
|
|
918
|
+
function globToRegExp(pattern) {
|
|
919
|
+
let re = "^";
|
|
920
|
+
let i = 0;
|
|
921
|
+
while (i < pattern.length) {
|
|
922
|
+
const c = pattern[i];
|
|
923
|
+
if (c === "*") {
|
|
924
|
+
if (pattern[i + 1] === "*") {
|
|
925
|
+
if (pattern[i + 2] === "/") {
|
|
926
|
+
re += "(?:.*/)?";
|
|
927
|
+
i += 3;
|
|
928
|
+
} else {
|
|
929
|
+
re += ".*";
|
|
930
|
+
i += 2;
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
re += "[^/]*";
|
|
934
|
+
i++;
|
|
935
|
+
}
|
|
936
|
+
} else if (c === "?") {
|
|
937
|
+
re += "[^/]";
|
|
938
|
+
i++;
|
|
939
|
+
} else if (c === ".") {
|
|
940
|
+
re += "\\.";
|
|
941
|
+
i++;
|
|
942
|
+
} else if (c === "+") {
|
|
943
|
+
re += "\\+";
|
|
944
|
+
i++;
|
|
945
|
+
} else if (c === "(" || c === ")" || c === "|" || c === "$" || c === "^") {
|
|
946
|
+
re += "\\" + c;
|
|
947
|
+
i++;
|
|
948
|
+
} else if (c === "[") {
|
|
949
|
+
const close = pattern.indexOf("]", i);
|
|
950
|
+
if (close === -1) {
|
|
951
|
+
re += "\\[";
|
|
952
|
+
i++;
|
|
953
|
+
} else {
|
|
954
|
+
re += pattern.slice(i, close + 1);
|
|
955
|
+
i = close + 1;
|
|
956
|
+
}
|
|
957
|
+
} else if (c === "{") {
|
|
958
|
+
const close = pattern.indexOf("}", i);
|
|
959
|
+
if (close === -1) {
|
|
960
|
+
re += "\\{";
|
|
961
|
+
i++;
|
|
962
|
+
} else {
|
|
963
|
+
const alts = pattern.slice(i + 1, close).split(",");
|
|
964
|
+
re += "(" + alts.map((a) => globToRegExp(a).source.slice(1, -1)).join("|") + ")";
|
|
965
|
+
i = close + 1;
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
re += c;
|
|
969
|
+
i++;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
re += "$";
|
|
973
|
+
return new RegExp(re);
|
|
974
|
+
}
|
|
975
|
+
async function grep(args, mountedFolders) {
|
|
976
|
+
const extras = mountedFolders ? Array.from(mountedFolders) : [];
|
|
977
|
+
const root = resolveInWorkspace(args.path || ".", { readableExtras: extras });
|
|
978
|
+
const isRegex = args.regex !== false;
|
|
979
|
+
const fileGlob = args.file_glob || "**/*";
|
|
980
|
+
const max = Math.max(1, Math.min(args.max_matches ?? 200, 2e3));
|
|
981
|
+
let re;
|
|
982
|
+
try {
|
|
983
|
+
re = isRegex ? new RegExp(args.pattern) : new RegExp(args.pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"));
|
|
984
|
+
} catch (err) {
|
|
985
|
+
return { error: "invalid_regex", message: err.message, pattern: args.pattern };
|
|
986
|
+
}
|
|
987
|
+
const fileRe = globToRegExp(fileGlob);
|
|
988
|
+
const hits = [];
|
|
989
|
+
let truncated = false;
|
|
990
|
+
let filesScanned = 0;
|
|
991
|
+
async function walk(dir) {
|
|
992
|
+
if (hits.length >= max) {
|
|
993
|
+
truncated = true;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
let items;
|
|
997
|
+
try {
|
|
998
|
+
items = await fs2.readdir(dir, { withFileTypes: true });
|
|
999
|
+
} catch {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
for (const it of items) {
|
|
1003
|
+
if (hits.length >= max) {
|
|
1004
|
+
truncated = true;
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (it.isDirectory() && SKIP_DIRS.has(it.name)) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const full = path3.join(dir, it.name);
|
|
1011
|
+
if (it.isDirectory()) {
|
|
1012
|
+
await walk(full);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (!it.isFile()) continue;
|
|
1016
|
+
const rel = toWorkspaceRel(full);
|
|
1017
|
+
if (!fileRe.test(rel)) continue;
|
|
1018
|
+
const ext = path3.extname(full).toLowerCase();
|
|
1019
|
+
if (BINARY_EXT.has(ext)) continue;
|
|
1020
|
+
let stat5;
|
|
1021
|
+
try {
|
|
1022
|
+
stat5 = await fs2.stat(full);
|
|
1023
|
+
} catch {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (stat5.size > 1e6) continue;
|
|
1027
|
+
filesScanned++;
|
|
1028
|
+
let content;
|
|
1029
|
+
try {
|
|
1030
|
+
content = await fs2.readFile(full, "utf-8");
|
|
1031
|
+
} catch {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const lines = content.split(/\r?\n/);
|
|
1035
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1036
|
+
if (re.test(lines[i])) {
|
|
1037
|
+
hits.push({ path: rel, line: i + 1, content: lines[i].slice(0, 400) });
|
|
1038
|
+
if (hits.length >= max) {
|
|
1039
|
+
truncated = true;
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
await walk(root);
|
|
1047
|
+
return { pattern: args.pattern, hits, truncated, files_scanned: filesScanned };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/executors/shell.ts
|
|
1051
|
+
var import_node_child_process2 = require("node:child_process");
|
|
1052
|
+
|
|
1053
|
+
// src/processRegistry.ts
|
|
1054
|
+
var import_node_child_process = require("node:child_process");
|
|
1055
|
+
var LONG_RUNNING_PATTERNS = [
|
|
1056
|
+
/^uvicorn\b/,
|
|
1057
|
+
/^gunicorn\b/,
|
|
1058
|
+
/^flask\s+run\b/,
|
|
1059
|
+
/^next\s+dev\b/,
|
|
1060
|
+
/^vite\b/,
|
|
1061
|
+
/\bnpm\s+run\s+dev\b/,
|
|
1062
|
+
/\byarn\s+dev\b/,
|
|
1063
|
+
/\bpnpm\s+dev\b/,
|
|
1064
|
+
/^cargo\s+run\b/,
|
|
1065
|
+
/^go\s+run\b/,
|
|
1066
|
+
/^python\s+-m\s+http\.server\b/,
|
|
1067
|
+
/^flutter\s+run\b/,
|
|
1068
|
+
/&\s*$/,
|
|
1069
|
+
// backgrounded
|
|
1070
|
+
/^nohup\b/
|
|
1071
|
+
];
|
|
1072
|
+
function isLongRunning(command) {
|
|
1073
|
+
const t = command.trim();
|
|
1074
|
+
return LONG_RUNNING_PATTERNS.some((re) => re.test(t));
|
|
1075
|
+
}
|
|
1076
|
+
function extractPortBindings(command) {
|
|
1077
|
+
const ports = [];
|
|
1078
|
+
const reFlag = /(?:--?port[=\s]+|:)(\d{2,5})\b/g;
|
|
1079
|
+
let m;
|
|
1080
|
+
while ((m = reFlag.exec(command)) !== null) {
|
|
1081
|
+
const p = parseInt(m[1], 10);
|
|
1082
|
+
if (p >= 1024 && p <= 65535) ports.push(p);
|
|
1083
|
+
}
|
|
1084
|
+
return [...new Set(ports)];
|
|
1085
|
+
}
|
|
1086
|
+
function _treeKill(proc, sig) {
|
|
1087
|
+
const pid = proc.pid;
|
|
1088
|
+
if (pid === void 0) return;
|
|
1089
|
+
if (process.platform === "win32") {
|
|
1090
|
+
(0, import_node_child_process.spawnSync)("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
1091
|
+
stdio: "ignore",
|
|
1092
|
+
windowsHide: true
|
|
1093
|
+
});
|
|
1094
|
+
} else {
|
|
1095
|
+
try {
|
|
1096
|
+
process.kill(-pid, sig);
|
|
1097
|
+
} catch {
|
|
1098
|
+
proc.kill(sig);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
var ProcessRegistry = class {
|
|
1103
|
+
procs = /* @__PURE__ */ new Map();
|
|
1104
|
+
finished = [];
|
|
1105
|
+
// exited procs, kept for include_finished
|
|
1106
|
+
register(proc, command) {
|
|
1107
|
+
const info = {
|
|
1108
|
+
pid: proc.pid,
|
|
1109
|
+
command,
|
|
1110
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1111
|
+
status: "running",
|
|
1112
|
+
portBindings: extractPortBindings(command),
|
|
1113
|
+
proc
|
|
1114
|
+
};
|
|
1115
|
+
this.procs.set(proc.pid, info);
|
|
1116
|
+
proc.on("exit", (code) => {
|
|
1117
|
+
info.status = "exited";
|
|
1118
|
+
info.exitCode = code ?? -1;
|
|
1119
|
+
this.procs.delete(proc.pid);
|
|
1120
|
+
this.finished.push(info);
|
|
1121
|
+
if (this.finished.length > 30) this.finished.shift();
|
|
1122
|
+
});
|
|
1123
|
+
return info;
|
|
1124
|
+
}
|
|
1125
|
+
list(includeFinished = false) {
|
|
1126
|
+
const live = Array.from(this.procs.values()).map(strip);
|
|
1127
|
+
if (!includeFinished) return live;
|
|
1128
|
+
return [...live, ...this.finished.map(strip)];
|
|
1129
|
+
}
|
|
1130
|
+
owns(pid) {
|
|
1131
|
+
return this.procs.has(pid);
|
|
1132
|
+
}
|
|
1133
|
+
async signal(pid, sig) {
|
|
1134
|
+
const info = this.procs.get(pid);
|
|
1135
|
+
if (!info) {
|
|
1136
|
+
return { killed: false, pid, reason: "PID not owned by this agent session" };
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
_treeKill(info.proc, sig);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
return { killed: false, pid, reason: err?.message || String(err) };
|
|
1142
|
+
}
|
|
1143
|
+
const exited = await new Promise((resolve3) => {
|
|
1144
|
+
const timer = setTimeout(() => resolve3(false), 5e3);
|
|
1145
|
+
info.proc.once("exit", () => {
|
|
1146
|
+
clearTimeout(timer);
|
|
1147
|
+
resolve3(true);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
if (!exited && sig === "SIGTERM") {
|
|
1151
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1152
|
+
try {
|
|
1153
|
+
_treeKill(info.proc, "SIGKILL");
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return { killed: true, pid, exitCode: info.exitCode };
|
|
1158
|
+
}
|
|
1159
|
+
/** Called from extension.deactivate() — best-effort kill all alive procs. */
|
|
1160
|
+
async cleanupAll() {
|
|
1161
|
+
const pids = Array.from(this.procs.keys());
|
|
1162
|
+
for (const pid of pids) {
|
|
1163
|
+
const info = this.procs.get(pid);
|
|
1164
|
+
if (!info) continue;
|
|
1165
|
+
try {
|
|
1166
|
+
_treeKill(info.proc, "SIGTERM");
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1171
|
+
for (const pid of pids) {
|
|
1172
|
+
const info = this.procs.get(pid);
|
|
1173
|
+
if (!info) continue;
|
|
1174
|
+
try {
|
|
1175
|
+
_treeKill(info.proc, "SIGKILL");
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return pids.length;
|
|
1180
|
+
}
|
|
1181
|
+
get size() {
|
|
1182
|
+
return this.procs.size;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
function strip(info) {
|
|
1186
|
+
const { proc, ...rest } = info;
|
|
1187
|
+
return rest;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/executors/shell.ts
|
|
1191
|
+
function normalizeWindowsCommand(cmd) {
|
|
1192
|
+
if (process.platform !== "win32") return cmd;
|
|
1193
|
+
const m = /^(\s*)python3?(\.exe)?\s+-c\s+"((?:\\.|[^"])*)"(.*)$/.exec(cmd);
|
|
1194
|
+
if (!m) return cmd;
|
|
1195
|
+
let code = m[3];
|
|
1196
|
+
const tail = m[4] || "";
|
|
1197
|
+
code = code.replace(/\\"/g, '"');
|
|
1198
|
+
if (code.includes("'")) {
|
|
1199
|
+
return cmd;
|
|
1200
|
+
}
|
|
1201
|
+
return `py -c '${code}'${tail}`;
|
|
1202
|
+
}
|
|
1203
|
+
async function runCommand(args, registry, signal) {
|
|
1204
|
+
const cmd = normalizeWindowsCommand(args.command);
|
|
1205
|
+
const timeoutMs = Math.min(Math.max(args.timeout_s ?? 60, 1), 120) * 1e3;
|
|
1206
|
+
const cwd = args.cwd ? resolveInWorkspace(args.cwd) : workspaceRoot();
|
|
1207
|
+
if (signal?.aborted) {
|
|
1208
|
+
return {
|
|
1209
|
+
command: cmd,
|
|
1210
|
+
exit_code: -1,
|
|
1211
|
+
stdout: "",
|
|
1212
|
+
stderr: "",
|
|
1213
|
+
duration_ms: 0,
|
|
1214
|
+
timed_out: false,
|
|
1215
|
+
spawned_pid: null,
|
|
1216
|
+
error: "aborted"
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const longRunning = isLongRunning(cmd);
|
|
1220
|
+
const isWin = process.platform === "win32";
|
|
1221
|
+
const proc = isWin ? (0, import_node_child_process2.spawn)("cmd.exe", ["/c", cmd], { cwd }) : (0, import_node_child_process2.spawn)("/bin/sh", ["-c", cmd], { cwd });
|
|
1222
|
+
if (signal) {
|
|
1223
|
+
const onAbort = () => {
|
|
1224
|
+
try {
|
|
1225
|
+
proc.kill("SIGTERM");
|
|
1226
|
+
} catch {
|
|
1227
|
+
}
|
|
1228
|
+
setTimeout(() => {
|
|
1229
|
+
try {
|
|
1230
|
+
proc.kill("SIGKILL");
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}, 2e3);
|
|
1234
|
+
};
|
|
1235
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1236
|
+
}
|
|
1237
|
+
if (longRunning) {
|
|
1238
|
+
registry.register(proc, cmd);
|
|
1239
|
+
proc.unref();
|
|
1240
|
+
return {
|
|
1241
|
+
command: cmd,
|
|
1242
|
+
exit_code: -1,
|
|
1243
|
+
stdout: "",
|
|
1244
|
+
stderr: "[long-running process spawned in background]",
|
|
1245
|
+
duration_ms: 0,
|
|
1246
|
+
timed_out: false,
|
|
1247
|
+
spawned_pid: proc.pid ?? null
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
const t0 = Date.now();
|
|
1251
|
+
let stdout = "";
|
|
1252
|
+
let stderr = "";
|
|
1253
|
+
let timedOut = false;
|
|
1254
|
+
proc.stdout?.setEncoding("utf-8");
|
|
1255
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1256
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1257
|
+
stdout += chunk;
|
|
1258
|
+
if (stdout.length > 256e3) stdout = stdout.slice(0, 256e3) + "\n[truncated]";
|
|
1259
|
+
});
|
|
1260
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1261
|
+
stderr += chunk;
|
|
1262
|
+
if (stderr.length > 128e3) stderr = stderr.slice(0, 128e3) + "\n[truncated]";
|
|
1263
|
+
});
|
|
1264
|
+
const exitCode = await new Promise((resolve3) => {
|
|
1265
|
+
const timer = setTimeout(() => {
|
|
1266
|
+
timedOut = true;
|
|
1267
|
+
try {
|
|
1268
|
+
proc.kill("SIGTERM");
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
setTimeout(() => {
|
|
1272
|
+
try {
|
|
1273
|
+
proc.kill("SIGKILL");
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
}, 1e3);
|
|
1277
|
+
}, timeoutMs);
|
|
1278
|
+
proc.on("exit", (code) => {
|
|
1279
|
+
clearTimeout(timer);
|
|
1280
|
+
resolve3(code ?? -1);
|
|
1281
|
+
});
|
|
1282
|
+
proc.on("error", () => {
|
|
1283
|
+
clearTimeout(timer);
|
|
1284
|
+
resolve3(-1);
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
return {
|
|
1288
|
+
command: cmd,
|
|
1289
|
+
exit_code: exitCode,
|
|
1290
|
+
stdout,
|
|
1291
|
+
stderr,
|
|
1292
|
+
duration_ms: Date.now() - t0,
|
|
1293
|
+
timed_out: timedOut,
|
|
1294
|
+
spawned_pid: null
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/executors/localPlane.ts
|
|
1299
|
+
var os = __toESM(require("node:os"));
|
|
1300
|
+
var net = __toESM(require("node:net"));
|
|
1301
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1302
|
+
var RUNTIME_PROBES = [
|
|
1303
|
+
// Day 13 P3 fix: probe `py` (Python Launcher) FIRST on Windows so the
|
|
1304
|
+
// Microsoft Store stub for `python` doesn't get reported as installed.
|
|
1305
|
+
{ key: "python", candidates: ["py", "python3", "python"] },
|
|
1306
|
+
{ key: "node", candidates: ["node"] },
|
|
1307
|
+
{ key: "go", candidates: ["go"] },
|
|
1308
|
+
{ key: "rust", candidates: ["rustc"] },
|
|
1309
|
+
{ key: "java", candidates: ["java"] },
|
|
1310
|
+
{ key: "dotnet", candidates: ["dotnet"] },
|
|
1311
|
+
{ key: "ruby", candidates: ["ruby"] },
|
|
1312
|
+
{ key: "deno", candidates: ["deno"] }
|
|
1313
|
+
];
|
|
1314
|
+
var TOOL_PROBES = ["git", "docker", "uv", "pnpm", "yarn", "npm", "make", "rg", "gh", "kubectl"];
|
|
1315
|
+
async function probeVersion(cmd, args = ["--version"]) {
|
|
1316
|
+
return new Promise((resolve3) => {
|
|
1317
|
+
let proc;
|
|
1318
|
+
try {
|
|
1319
|
+
proc = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
1320
|
+
} catch {
|
|
1321
|
+
return resolve3(null);
|
|
1322
|
+
}
|
|
1323
|
+
let out = "", err = "";
|
|
1324
|
+
proc.stdout?.setEncoding("utf-8");
|
|
1325
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1326
|
+
proc.stdout?.on("data", (c) => {
|
|
1327
|
+
out += c;
|
|
1328
|
+
});
|
|
1329
|
+
proc.stderr?.on("data", (c) => {
|
|
1330
|
+
err += c;
|
|
1331
|
+
});
|
|
1332
|
+
proc.on("error", () => resolve3(null));
|
|
1333
|
+
proc.on("exit", (code) => {
|
|
1334
|
+
if (code !== 0) return resolve3(null);
|
|
1335
|
+
const txt = (out + err).trim().split(/\r?\n/)[0] || "";
|
|
1336
|
+
if (!txt || /microsoft store|ms-windows-store/i.test(txt)) {
|
|
1337
|
+
return resolve3(null);
|
|
1338
|
+
}
|
|
1339
|
+
resolve3(txt);
|
|
1340
|
+
});
|
|
1341
|
+
setTimeout(() => {
|
|
1342
|
+
try {
|
|
1343
|
+
proc.kill();
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
resolve3(null);
|
|
1347
|
+
}, 2500);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
function osName() {
|
|
1351
|
+
const p = os.platform();
|
|
1352
|
+
if (p === "linux") return "Linux";
|
|
1353
|
+
if (p === "darwin") return "Darwin";
|
|
1354
|
+
if (p === "win32") return "Windows";
|
|
1355
|
+
return p;
|
|
1356
|
+
}
|
|
1357
|
+
async function systemInfo(args) {
|
|
1358
|
+
const level = args.level ?? "brief";
|
|
1359
|
+
const cpus2 = os.cpus() || [];
|
|
1360
|
+
const total = os.totalmem();
|
|
1361
|
+
const free = os.freemem();
|
|
1362
|
+
const base = {
|
|
1363
|
+
os: { name: osName(), release: os.release(), arch: os.arch() },
|
|
1364
|
+
cpu: {
|
|
1365
|
+
cores_logical: cpus2.length,
|
|
1366
|
+
cores_physical: Math.max(1, Math.floor(cpus2.length / 2))
|
|
1367
|
+
},
|
|
1368
|
+
memory: {
|
|
1369
|
+
total_mb: Math.round(total / 1024 / 1024),
|
|
1370
|
+
available_mb: Math.round(free / 1024 / 1024)
|
|
1371
|
+
},
|
|
1372
|
+
runtimes_present: [],
|
|
1373
|
+
tools_present: []
|
|
1374
|
+
};
|
|
1375
|
+
const runtimeResults = {};
|
|
1376
|
+
for (const probe of RUNTIME_PROBES) {
|
|
1377
|
+
const found = [];
|
|
1378
|
+
for (const cmd of probe.candidates) {
|
|
1379
|
+
const ver = await probeVersion(cmd);
|
|
1380
|
+
if (ver) found.push({ installed: true, version: ver, command: cmd });
|
|
1381
|
+
}
|
|
1382
|
+
runtimeResults[probe.key] = found;
|
|
1383
|
+
if (found.length > 0) base.runtimes_present.push(probe.key);
|
|
1384
|
+
}
|
|
1385
|
+
const toolResults = {};
|
|
1386
|
+
for (const cmd of TOOL_PROBES) {
|
|
1387
|
+
const ver = await probeVersion(cmd);
|
|
1388
|
+
toolResults[cmd] = { installed: !!ver, version: ver, command: cmd };
|
|
1389
|
+
if (ver) base.tools_present.push(cmd);
|
|
1390
|
+
}
|
|
1391
|
+
if (level === "brief") return base;
|
|
1392
|
+
const full = {
|
|
1393
|
+
...base,
|
|
1394
|
+
os: { ...base.os, kernel: os.type() },
|
|
1395
|
+
cpu: { ...base.cpu, model: (cpus2[0]?.model || "").trim() },
|
|
1396
|
+
memory: {
|
|
1397
|
+
...base.memory,
|
|
1398
|
+
percent_used: Math.round((1 - free / total) * 1e3) / 10
|
|
1399
|
+
},
|
|
1400
|
+
runtimes: runtimeResults,
|
|
1401
|
+
tools: toolResults
|
|
1402
|
+
};
|
|
1403
|
+
return full;
|
|
1404
|
+
}
|
|
1405
|
+
function listProcesses(args, registry) {
|
|
1406
|
+
const include = args.include_finished ?? false;
|
|
1407
|
+
const procs = registry.list(include);
|
|
1408
|
+
return {
|
|
1409
|
+
processes: procs.map((p) => ({
|
|
1410
|
+
pid: p.pid,
|
|
1411
|
+
command: p.command,
|
|
1412
|
+
started_at: p.startedAt,
|
|
1413
|
+
status: p.status,
|
|
1414
|
+
exit_code: p.exitCode,
|
|
1415
|
+
port_bindings: p.portBindings
|
|
1416
|
+
}))
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
async function tryBind(port, host = "127.0.0.1") {
|
|
1420
|
+
return new Promise((resolve3, reject) => {
|
|
1421
|
+
const server = net.createServer();
|
|
1422
|
+
let settled = false;
|
|
1423
|
+
server.once("error", (err) => {
|
|
1424
|
+
if (settled) return;
|
|
1425
|
+
settled = true;
|
|
1426
|
+
reject(err);
|
|
1427
|
+
});
|
|
1428
|
+
server.listen(port, host, () => {
|
|
1429
|
+
if (settled) {
|
|
1430
|
+
server.close();
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const addr = server.address();
|
|
1434
|
+
const p = typeof addr === "object" && addr ? addr.port : port;
|
|
1435
|
+
server.close(() => {
|
|
1436
|
+
if (settled) return;
|
|
1437
|
+
settled = true;
|
|
1438
|
+
resolve3(p);
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
async function allocatePort(args) {
|
|
1444
|
+
if (typeof args.prefer === "number") {
|
|
1445
|
+
try {
|
|
1446
|
+
const port = await tryBind(args.prefer);
|
|
1447
|
+
return { port, source: "preferred" };
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const [start, end] = args.range ?? [8e3, 9e3];
|
|
1452
|
+
for (let p = start; p <= end; p++) {
|
|
1453
|
+
try {
|
|
1454
|
+
const port = await tryBind(p);
|
|
1455
|
+
return { port, source: "random" };
|
|
1456
|
+
} catch {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return { error: "no_free_port", range: [start, end] };
|
|
1461
|
+
}
|
|
1462
|
+
async function killProcess(args, registry) {
|
|
1463
|
+
const sig = args.signal ?? "SIGTERM";
|
|
1464
|
+
const r = await registry.signal(args.pid, sig);
|
|
1465
|
+
return {
|
|
1466
|
+
killed: r.killed,
|
|
1467
|
+
pid: r.pid,
|
|
1468
|
+
exit_code: r.exitCode,
|
|
1469
|
+
reason: r.reason
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
async function verifyPackage(args, settings) {
|
|
1473
|
+
const url = `${settings.backendUrl}/code-workspace/verify-package?ecosystem=${encodeURIComponent(args.ecosystem)}&name=${encodeURIComponent(args.name)}`;
|
|
1474
|
+
try {
|
|
1475
|
+
const res = await fetch(url, {
|
|
1476
|
+
headers: { "Authorization": `Bearer ${settings.authBearer}` }
|
|
1477
|
+
});
|
|
1478
|
+
if (!res.ok) {
|
|
1479
|
+
return {
|
|
1480
|
+
exists: null,
|
|
1481
|
+
ecosystem: args.ecosystem,
|
|
1482
|
+
name: args.name,
|
|
1483
|
+
error: `backend ${res.status}: ${await res.text().catch(() => "")}`
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return await res.json();
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
return {
|
|
1489
|
+
exists: null,
|
|
1490
|
+
ecosystem: args.ecosystem,
|
|
1491
|
+
name: args.name,
|
|
1492
|
+
error: `client fetch failed: ${err?.message || String(err)}`
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
async function webSearch(args, settings) {
|
|
1497
|
+
const params = new URLSearchParams();
|
|
1498
|
+
params.set("query", args.query);
|
|
1499
|
+
if (args.sources && args.sources.length) params.set("sources", args.sources.join(","));
|
|
1500
|
+
if (args.max_per_source) params.set("max_per_source", String(args.max_per_source));
|
|
1501
|
+
const url = `${settings.backendUrl}/code-workspace/web-search?${params.toString()}`;
|
|
1502
|
+
try {
|
|
1503
|
+
const res = await fetch(url, {
|
|
1504
|
+
headers: { "Authorization": `Bearer ${settings.authBearer}` }
|
|
1505
|
+
});
|
|
1506
|
+
if (!res.ok) {
|
|
1507
|
+
return {
|
|
1508
|
+
error: `backend ${res.status}: ${await res.text().catch(() => "")}`,
|
|
1509
|
+
query: args.query
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
return await res.json();
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
return {
|
|
1515
|
+
error: `client fetch failed: ${err?.message || String(err)}`,
|
|
1516
|
+
query: args.query
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function verifyPackageBatch(args, settings) {
|
|
1521
|
+
const pkgs = Array.isArray(args?.packages) ? args.packages : [];
|
|
1522
|
+
if (!pkgs.length) return { error: "packages array required" };
|
|
1523
|
+
return _backend("POST", "/code-workspace/verify-package-batch", settings, {
|
|
1524
|
+
body: { packages: pkgs }
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
async function _backend(method, path9, settings, opts) {
|
|
1528
|
+
let url = `${settings.backendUrl}${path9}`;
|
|
1529
|
+
if (opts?.query) {
|
|
1530
|
+
const p = new URLSearchParams();
|
|
1531
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
1532
|
+
if (v !== void 0 && v !== null) p.set(k, String(v));
|
|
1533
|
+
}
|
|
1534
|
+
const qs = p.toString();
|
|
1535
|
+
if (qs) url += (path9.includes("?") ? "&" : "?") + qs;
|
|
1536
|
+
}
|
|
1537
|
+
const init = {
|
|
1538
|
+
method,
|
|
1539
|
+
headers: {
|
|
1540
|
+
"Authorization": `Bearer ${settings.authBearer}`,
|
|
1541
|
+
"Content-Type": "application/json"
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
if (opts?.body !== void 0) init.body = JSON.stringify(opts.body);
|
|
1545
|
+
try {
|
|
1546
|
+
const res = await fetch(url, init);
|
|
1547
|
+
if (!res.ok) {
|
|
1548
|
+
let detail = "";
|
|
1549
|
+
try {
|
|
1550
|
+
detail = await res.text();
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
return { error: `backend ${res.status}`, detail: detail.slice(0, 400) };
|
|
1554
|
+
}
|
|
1555
|
+
return await res.json();
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
return { error: `client fetch failed: ${err?.message || String(err)}` };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function gpuInfo(_args, settings) {
|
|
1561
|
+
return _backend("GET", "/code-workspace/gpu-info", settings);
|
|
1562
|
+
}
|
|
1563
|
+
async function startBackgroundTask(args, settings) {
|
|
1564
|
+
if (!args?.command) return { error: "command required" };
|
|
1565
|
+
return _backend("POST", "/code-workspace/bg-task/start", settings, {
|
|
1566
|
+
body: {
|
|
1567
|
+
command: args.command,
|
|
1568
|
+
cwd: args.cwd || "",
|
|
1569
|
+
label: args.label || "",
|
|
1570
|
+
env_extra: args.env_extra || null
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
async function checkBackgroundTask(args, settings) {
|
|
1575
|
+
if (!args?.task_id) return { error: "task_id required" };
|
|
1576
|
+
return _backend("GET", "/code-workspace/bg-task/check", settings, {
|
|
1577
|
+
query: { task_id: args.task_id, tail_lines: args.tail_lines }
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
async function listBackgroundTasks(_args, settings) {
|
|
1581
|
+
return _backend("GET", "/code-workspace/bg-task/list", settings);
|
|
1582
|
+
}
|
|
1583
|
+
async function cancelBackgroundTask(args, settings) {
|
|
1584
|
+
if (!args?.task_id) return { error: "task_id required" };
|
|
1585
|
+
return _backend("POST", "/code-workspace/bg-task/cancel", settings, {
|
|
1586
|
+
body: { task_id: args.task_id, signal: args.signal || "TERM" }
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/executors/lint.ts
|
|
1591
|
+
var import_node_child_process4 = require("node:child_process");
|
|
1592
|
+
var fs3 = __toESM(require("node:fs/promises"));
|
|
1593
|
+
var path4 = __toESM(require("node:path"));
|
|
1594
|
+
async function fileExists(p) {
|
|
1595
|
+
try {
|
|
1596
|
+
await fs3.stat(p);
|
|
1597
|
+
return true;
|
|
1598
|
+
} catch {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async function commandExists(cmd) {
|
|
1603
|
+
return new Promise((resolve3) => {
|
|
1604
|
+
let proc;
|
|
1605
|
+
try {
|
|
1606
|
+
proc = (0, import_node_child_process4.spawn)(
|
|
1607
|
+
cmd,
|
|
1608
|
+
["--version"],
|
|
1609
|
+
{ stdio: ["ignore", "ignore", "ignore"], windowsHide: true }
|
|
1610
|
+
);
|
|
1611
|
+
} catch {
|
|
1612
|
+
return resolve3(false);
|
|
1613
|
+
}
|
|
1614
|
+
proc.on("error", () => resolve3(false));
|
|
1615
|
+
proc.on("exit", (code) => resolve3(code === 0));
|
|
1616
|
+
setTimeout(() => {
|
|
1617
|
+
try {
|
|
1618
|
+
proc.kill();
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
resolve3(false);
|
|
1622
|
+
}, 1500);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
async function readJsonIfExists(p) {
|
|
1626
|
+
try {
|
|
1627
|
+
const t = await fs3.readFile(p, "utf-8");
|
|
1628
|
+
return JSON.parse(t);
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async function detect(root) {
|
|
1634
|
+
if (await fileExists(path4.join(root, "pyproject.toml")) && await commandExists("ruff")) {
|
|
1635
|
+
return {
|
|
1636
|
+
tool: "ruff",
|
|
1637
|
+
bin: "ruff",
|
|
1638
|
+
args: (sub, fix) => fix ? ["check", "--fix", "--output-format=json", sub] : ["check", "--output-format=json", sub]
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
const pkgJsonPath = path4.join(root, "package.json");
|
|
1642
|
+
const pkgJson = await readJsonIfExists(pkgJsonPath);
|
|
1643
|
+
const hasBiome = await fileExists(path4.join(root, "biome.json")) || !!(pkgJson?.devDependencies?.["@biomejs/biome"] || pkgJson?.dependencies?.["@biomejs/biome"]);
|
|
1644
|
+
if (hasBiome && (await commandExists("biome") || await commandExists("npx"))) {
|
|
1645
|
+
const useNpx = !await commandExists("biome");
|
|
1646
|
+
return {
|
|
1647
|
+
tool: "biome",
|
|
1648
|
+
bin: useNpx ? "npx" : "biome",
|
|
1649
|
+
args: (sub, fix) => {
|
|
1650
|
+
const base = useNpx ? ["biome"] : [];
|
|
1651
|
+
return [...base, fix ? "check" : "lint", fix ? "--apply" : "--reporter=json", sub];
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
const hasEslint = pkgJson?.devDependencies?.eslint || pkgJson?.dependencies?.eslint;
|
|
1656
|
+
if (hasEslint && (await commandExists("eslint") || await commandExists("npx"))) {
|
|
1657
|
+
const useNpx = !await commandExists("eslint");
|
|
1658
|
+
return {
|
|
1659
|
+
tool: "eslint",
|
|
1660
|
+
bin: useNpx ? "npx" : "eslint",
|
|
1661
|
+
args: (sub, fix) => {
|
|
1662
|
+
const base = useNpx ? ["eslint"] : [];
|
|
1663
|
+
return [...base, "--format=json", ...fix ? ["--fix"] : [], sub];
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
if (await fileExists(path4.join(root, "Cargo.toml")) && await commandExists("cargo")) {
|
|
1668
|
+
return {
|
|
1669
|
+
tool: "clippy",
|
|
1670
|
+
bin: "cargo",
|
|
1671
|
+
args: (_sub, fix) => fix ? ["clippy", "--fix", "--allow-dirty", "--message-format=json"] : ["clippy", "--message-format=json"]
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
if (await fileExists(path4.join(root, "go.mod")) && await commandExists("go")) {
|
|
1675
|
+
return {
|
|
1676
|
+
tool: "go-vet",
|
|
1677
|
+
bin: "go",
|
|
1678
|
+
args: (sub, _fix) => ["vet", `./${sub === "." ? "..." : sub + "/..."}`]
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
if ((await fileExists(path4.join(root, ".flake8")) || await fileExists(path4.join(root, "setup.cfg"))) && await commandExists("flake8")) {
|
|
1682
|
+
return {
|
|
1683
|
+
tool: "flake8",
|
|
1684
|
+
bin: "flake8",
|
|
1685
|
+
args: (sub, _fix) => ["--format=%(path)s:%(row)d:%(col)d: %(code)s %(text)s", sub]
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
function parseRuff(stdout, root) {
|
|
1691
|
+
try {
|
|
1692
|
+
const data = JSON.parse(stdout);
|
|
1693
|
+
if (!Array.isArray(data)) return [];
|
|
1694
|
+
return data.map((d) => ({
|
|
1695
|
+
path: toWorkspaceRel(d.filename || ""),
|
|
1696
|
+
line: d.location?.row ?? 0,
|
|
1697
|
+
column: d.location?.column,
|
|
1698
|
+
severity: "error",
|
|
1699
|
+
code: d.code,
|
|
1700
|
+
message: d.message || ""
|
|
1701
|
+
}));
|
|
1702
|
+
} catch {
|
|
1703
|
+
return [];
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function parseBiome(stdout) {
|
|
1707
|
+
try {
|
|
1708
|
+
const data = JSON.parse(stdout);
|
|
1709
|
+
const diagnostics = data?.diagnostics || [];
|
|
1710
|
+
return diagnostics.map((d) => ({
|
|
1711
|
+
path: d.location?.path?.file?.toString() || "",
|
|
1712
|
+
line: (d.location?.span?.[0] ?? 0) > 0 ? d.location.span[0] : 0,
|
|
1713
|
+
severity: d.severity === "warning" ? "warning" : d.severity === "info" ? "info" : "error",
|
|
1714
|
+
code: d.category,
|
|
1715
|
+
message: d.description || d.message?.text || ""
|
|
1716
|
+
}));
|
|
1717
|
+
} catch {
|
|
1718
|
+
return [];
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function parseEslint(stdout, root) {
|
|
1722
|
+
try {
|
|
1723
|
+
const data = JSON.parse(stdout);
|
|
1724
|
+
const out = [];
|
|
1725
|
+
for (const f of data || []) {
|
|
1726
|
+
for (const m of f.messages || []) {
|
|
1727
|
+
out.push({
|
|
1728
|
+
path: toWorkspaceRel(f.filePath || ""),
|
|
1729
|
+
line: m.line ?? 0,
|
|
1730
|
+
column: m.column,
|
|
1731
|
+
severity: m.severity === 2 ? "error" : "warning",
|
|
1732
|
+
code: m.ruleId || void 0,
|
|
1733
|
+
message: m.message || ""
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return out;
|
|
1738
|
+
} catch {
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function parseClippy(stdout) {
|
|
1743
|
+
const out = [];
|
|
1744
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1745
|
+
if (!line.trim()) continue;
|
|
1746
|
+
try {
|
|
1747
|
+
const obj = JSON.parse(line);
|
|
1748
|
+
if (obj.reason !== "compiler-message") continue;
|
|
1749
|
+
const msg = obj.message;
|
|
1750
|
+
if (!msg || msg.level === "help") continue;
|
|
1751
|
+
const span = (msg.spans || []).find((s) => s.is_primary) || msg.spans?.[0];
|
|
1752
|
+
if (!span) continue;
|
|
1753
|
+
out.push({
|
|
1754
|
+
path: span.file_name || "",
|
|
1755
|
+
line: span.line_start ?? 0,
|
|
1756
|
+
column: span.column_start,
|
|
1757
|
+
severity: msg.level === "error" ? "error" : msg.level === "warning" ? "warning" : "info",
|
|
1758
|
+
code: msg.code?.code,
|
|
1759
|
+
message: msg.message || ""
|
|
1760
|
+
});
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return out;
|
|
1765
|
+
}
|
|
1766
|
+
function parseFlake8(stdout) {
|
|
1767
|
+
const re = /^(.+?):(\d+):(\d+):\s*(\S+)\s+(.+)$/;
|
|
1768
|
+
const out = [];
|
|
1769
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1770
|
+
const m = re.exec(line.trim());
|
|
1771
|
+
if (m) out.push({
|
|
1772
|
+
path: m[1],
|
|
1773
|
+
line: parseInt(m[2], 10),
|
|
1774
|
+
column: parseInt(m[3], 10),
|
|
1775
|
+
severity: m[4].startsWith("E") ? "error" : "warning",
|
|
1776
|
+
code: m[4],
|
|
1777
|
+
message: m[5]
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1782
|
+
function parseGoVet(stderr) {
|
|
1783
|
+
const re = /^(.+?):(\d+):(\d+):\s+(.+)$/;
|
|
1784
|
+
const out = [];
|
|
1785
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
1786
|
+
const m = re.exec(line.trim());
|
|
1787
|
+
if (m) out.push({
|
|
1788
|
+
path: m[1],
|
|
1789
|
+
line: parseInt(m[2], 10),
|
|
1790
|
+
column: parseInt(m[3], 10),
|
|
1791
|
+
severity: "error",
|
|
1792
|
+
message: m[4]
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
async function runProc(cmd, args, cwd) {
|
|
1798
|
+
return new Promise((resolve3) => {
|
|
1799
|
+
let proc;
|
|
1800
|
+
try {
|
|
1801
|
+
proc = (0, import_node_child_process4.spawn)(cmd, args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
return resolve3({ stdout: "", stderr: e?.message || "spawn failed", exitCode: -1 });
|
|
1804
|
+
}
|
|
1805
|
+
let out = "", err = "";
|
|
1806
|
+
proc.stdout?.setEncoding("utf-8");
|
|
1807
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1808
|
+
proc.stdout?.on("data", (c) => {
|
|
1809
|
+
out += c;
|
|
1810
|
+
if (out.length > 4e5) out = out.slice(0, 4e5) + "\n[truncated]";
|
|
1811
|
+
});
|
|
1812
|
+
proc.stderr?.on("data", (c) => {
|
|
1813
|
+
err += c;
|
|
1814
|
+
if (err.length > 2e5) err = err.slice(0, 2e5) + "\n[truncated]";
|
|
1815
|
+
});
|
|
1816
|
+
proc.on("error", () => resolve3({ stdout: out, stderr: err || "spawn error", exitCode: -1 }));
|
|
1817
|
+
proc.on("exit", (code) => resolve3({ stdout: out, stderr: err, exitCode: code ?? -1 }));
|
|
1818
|
+
setTimeout(() => {
|
|
1819
|
+
try {
|
|
1820
|
+
proc.kill();
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
}, 12e4);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
async function runLint(args) {
|
|
1827
|
+
const root = workspaceRoot();
|
|
1828
|
+
const sub = args.path ? toWorkspaceRel(resolveInWorkspace(args.path)) || "." : ".";
|
|
1829
|
+
const fix = args.fix === true;
|
|
1830
|
+
const det = await detect(root);
|
|
1831
|
+
if (!det) {
|
|
1832
|
+
return {
|
|
1833
|
+
tool_used: null,
|
|
1834
|
+
ran_in: sub,
|
|
1835
|
+
issues: [],
|
|
1836
|
+
raw_summary: "no lint setup detected \u2014 scanned for pyproject.toml/package.json/Cargo.toml/go.mod/.flake8",
|
|
1837
|
+
hint: "Project has no detectable linter config. Don't install one without asking the user \u2014 they may have a preference."
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
const cmdArgs = det.args(sub, fix);
|
|
1841
|
+
const { stdout, stderr, exitCode } = await runProc(det.bin, cmdArgs, root);
|
|
1842
|
+
let issues = [];
|
|
1843
|
+
switch (det.tool) {
|
|
1844
|
+
case "ruff":
|
|
1845
|
+
issues = parseRuff(stdout, root);
|
|
1846
|
+
break;
|
|
1847
|
+
case "biome":
|
|
1848
|
+
issues = parseBiome(stdout);
|
|
1849
|
+
break;
|
|
1850
|
+
case "eslint":
|
|
1851
|
+
issues = parseEslint(stdout, root);
|
|
1852
|
+
break;
|
|
1853
|
+
case "clippy":
|
|
1854
|
+
issues = parseClippy(stdout);
|
|
1855
|
+
break;
|
|
1856
|
+
case "go-vet":
|
|
1857
|
+
issues = parseGoVet(stderr);
|
|
1858
|
+
break;
|
|
1859
|
+
case "flake8":
|
|
1860
|
+
issues = parseFlake8(stdout);
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
const sevCount = issues.reduce((acc, i) => {
|
|
1864
|
+
acc[i.severity] = (acc[i.severity] ?? 0) + 1;
|
|
1865
|
+
return acc;
|
|
1866
|
+
}, {});
|
|
1867
|
+
const summary = issues.length === 0 ? `${det.tool}: clean (exit ${exitCode})` : `${det.tool}: ${issues.length} issue(s) \u2014 ${JSON.stringify(sevCount)} (exit ${exitCode})`;
|
|
1868
|
+
return {
|
|
1869
|
+
tool_used: det.tool,
|
|
1870
|
+
ran_in: sub,
|
|
1871
|
+
fix_applied: fix,
|
|
1872
|
+
issues: issues.slice(0, 200),
|
|
1873
|
+
// cap to avoid blowing context
|
|
1874
|
+
raw_summary: summary + (issues.length > 200 ? ` (showing first 200 of ${issues.length})` : ""),
|
|
1875
|
+
exit_code: exitCode
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/executors/format.ts
|
|
1880
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1881
|
+
async function runProc2(cmd, args, cwd) {
|
|
1882
|
+
return new Promise((resolve3) => {
|
|
1883
|
+
let proc;
|
|
1884
|
+
try {
|
|
1885
|
+
proc = (0, import_node_child_process5.spawn)(cmd, args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
return resolve3({ stdout: "", stderr: e?.message || "spawn failed", exitCode: -1 });
|
|
1888
|
+
}
|
|
1889
|
+
let out = "", err = "";
|
|
1890
|
+
proc.stdout?.setEncoding("utf-8");
|
|
1891
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1892
|
+
proc.stdout?.on("data", (c) => {
|
|
1893
|
+
out += c;
|
|
1894
|
+
if (out.length > 4e5) out = out.slice(0, 4e5) + "\n[truncated]";
|
|
1895
|
+
});
|
|
1896
|
+
proc.stderr?.on("data", (c) => {
|
|
1897
|
+
err += c;
|
|
1898
|
+
if (err.length > 2e5) err = err.slice(0, 2e5) + "\n[truncated]";
|
|
1899
|
+
});
|
|
1900
|
+
proc.on("error", () => resolve3({ stdout: out, stderr: err || "spawn error", exitCode: -1 }));
|
|
1901
|
+
proc.on("exit", (code) => resolve3({ stdout: out, stderr: err, exitCode: code ?? -1 }));
|
|
1902
|
+
setTimeout(() => {
|
|
1903
|
+
try {
|
|
1904
|
+
proc.kill();
|
|
1905
|
+
} catch {
|
|
1906
|
+
}
|
|
1907
|
+
}, 6e4);
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
async function runFormat(args) {
|
|
1911
|
+
const start = performance.now();
|
|
1912
|
+
const root = workspaceRoot();
|
|
1913
|
+
const filePath = args.path ? resolveInWorkspace(args.path) : root;
|
|
1914
|
+
const checkOnly = args.check_only === true;
|
|
1915
|
+
const cmdArgs = checkOnly ? ["format", "--check", filePath] : ["format", filePath];
|
|
1916
|
+
const { stdout, stderr, exitCode } = await runProc2("ruff", cmdArgs, root);
|
|
1917
|
+
const durationMs = Math.round(performance.now() - start);
|
|
1918
|
+
const applied = checkOnly ? false : exitCode === 1 ? true : false;
|
|
1919
|
+
return {
|
|
1920
|
+
tool_used: "ruff",
|
|
1921
|
+
applied,
|
|
1922
|
+
exit_code: exitCode,
|
|
1923
|
+
stdout,
|
|
1924
|
+
stderr,
|
|
1925
|
+
duration_ms: durationMs
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// src/executors/pr.ts
|
|
1930
|
+
var import_node_child_process6 = require("node:child_process");
|
|
1931
|
+
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master", "develop", "prod", "production", "release"]);
|
|
1932
|
+
async function run(cmd, args, cwd, timeoutMs = 3e4) {
|
|
1933
|
+
return new Promise((resolve3) => {
|
|
1934
|
+
let proc;
|
|
1935
|
+
try {
|
|
1936
|
+
proc = (0, import_node_child_process6.spawn)(cmd, args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
1937
|
+
} catch (e) {
|
|
1938
|
+
return resolve3({ stdout: "", stderr: e?.message || "spawn failed", exitCode: -1 });
|
|
1939
|
+
}
|
|
1940
|
+
let out = "", err = "";
|
|
1941
|
+
proc.stdout?.setEncoding("utf-8");
|
|
1942
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1943
|
+
proc.stdout?.on("data", (c) => out += c);
|
|
1944
|
+
proc.stderr?.on("data", (c) => err += c);
|
|
1945
|
+
proc.on("error", () => resolve3({ stdout: out, stderr: err || "spawn error", exitCode: -1 }));
|
|
1946
|
+
proc.on("exit", (code) => resolve3({ stdout: out, stderr: err, exitCode: code ?? -1 }));
|
|
1947
|
+
setTimeout(() => {
|
|
1948
|
+
try {
|
|
1949
|
+
proc.kill();
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
}, timeoutMs);
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
async function createPR(args) {
|
|
1956
|
+
const root = workspaceRoot();
|
|
1957
|
+
const isRepo = await run("git", ["rev-parse", "--is-inside-work-tree"], root);
|
|
1958
|
+
if (!isRepo.exitCode || isRepo.stdout.trim() !== "true") {
|
|
1959
|
+
return {
|
|
1960
|
+
error: "not_a_git_repo",
|
|
1961
|
+
hint: "Initialize git first (run_command 'git init') and commit something before opening a PR."
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const ghVer = await run("gh", ["--version"], root, 5e3);
|
|
1965
|
+
if (ghVer.exitCode !== 0) {
|
|
1966
|
+
return {
|
|
1967
|
+
error: "gh_not_installed",
|
|
1968
|
+
hint: "Install GitHub CLI: winget install GitHub.cli (or visit https://cli.github.com). Then run `gh auth login`."
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const ghAuth = await run("gh", ["auth", "status"], root, 5e3);
|
|
1972
|
+
if (ghAuth.exitCode !== 0) {
|
|
1973
|
+
return {
|
|
1974
|
+
error: "gh_not_authenticated",
|
|
1975
|
+
hint: "Run `gh auth login` in a terminal, then retry.",
|
|
1976
|
+
stderr: ghAuth.stderr.slice(0, 400)
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
const br = await run("git", ["branch", "--show-current"], root);
|
|
1980
|
+
const branch = br.stdout.trim();
|
|
1981
|
+
if (!branch) {
|
|
1982
|
+
return { error: "detached_head", hint: "Repo is in detached HEAD \u2014 checkout a branch first." };
|
|
1983
|
+
}
|
|
1984
|
+
if (PROTECTED_BRANCHES.has(branch.toLowerCase())) {
|
|
1985
|
+
return {
|
|
1986
|
+
error: "on_protected_branch",
|
|
1987
|
+
branch,
|
|
1988
|
+
hint: `You're on '${branch}'. Create a feature branch first: run_command 'git checkout -b feat/my-change'.`
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
const ahead = await run("git", ["status", "--porcelain"], root);
|
|
1992
|
+
const hasUncommitted = ahead.stdout.trim().length > 0;
|
|
1993
|
+
const push = await run("git", ["push", "-u", "origin", branch], root, 6e4);
|
|
1994
|
+
if (push.exitCode !== 0) {
|
|
1995
|
+
return {
|
|
1996
|
+
error: "git_push_failed",
|
|
1997
|
+
branch,
|
|
1998
|
+
stderr: push.stderr.slice(0, 400),
|
|
1999
|
+
hint: "Check remote 'origin' is configured and you can push."
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
const base = args.base_branch || "main";
|
|
2003
|
+
const prArgs = [
|
|
2004
|
+
"pr",
|
|
2005
|
+
"create",
|
|
2006
|
+
"--title",
|
|
2007
|
+
args.title,
|
|
2008
|
+
"--body",
|
|
2009
|
+
args.body,
|
|
2010
|
+
"--base",
|
|
2011
|
+
base,
|
|
2012
|
+
"--head",
|
|
2013
|
+
branch
|
|
2014
|
+
];
|
|
2015
|
+
if (args.draft) prArgs.push("--draft");
|
|
2016
|
+
const created = await run("gh", prArgs, root, 3e4);
|
|
2017
|
+
if (created.exitCode !== 0) {
|
|
2018
|
+
return {
|
|
2019
|
+
error: "gh_pr_create_failed",
|
|
2020
|
+
branch,
|
|
2021
|
+
base_branch: base,
|
|
2022
|
+
stderr: created.stderr.slice(0, 400),
|
|
2023
|
+
stdout: created.stdout.slice(0, 200)
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
const url = (created.stdout.match(/https:\/\/github\.com\/[^\s]+/) || [""])[0];
|
|
2027
|
+
const numMatch = url.match(/\/pull\/(\d+)/);
|
|
2028
|
+
return {
|
|
2029
|
+
pr_url: url,
|
|
2030
|
+
pr_number: numMatch ? parseInt(numMatch[1], 10) : null,
|
|
2031
|
+
branch,
|
|
2032
|
+
base_branch: base,
|
|
2033
|
+
draft: !!args.draft,
|
|
2034
|
+
had_uncommitted: hasUncommitted
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/executors/selfEval.ts
|
|
2039
|
+
var fs4 = __toESM(require("node:fs/promises"));
|
|
2040
|
+
var MAX_FILES = 20;
|
|
2041
|
+
var MAX_FILE_BYTES2 = 4e4;
|
|
2042
|
+
var MAX_TOTAL_BYTES = 2e5;
|
|
2043
|
+
async function agentSelfEval(args, settings) {
|
|
2044
|
+
const files = (args.files || []).slice(0, MAX_FILES);
|
|
2045
|
+
const contents = {};
|
|
2046
|
+
let total = 0;
|
|
2047
|
+
let truncated = false;
|
|
2048
|
+
for (const rel of files) {
|
|
2049
|
+
let abs;
|
|
2050
|
+
try {
|
|
2051
|
+
abs = resolveInWorkspace(rel);
|
|
2052
|
+
} catch {
|
|
2053
|
+
contents[rel] = "[error: path out of workspace]";
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
try {
|
|
2057
|
+
const stat5 = await fs4.stat(abs);
|
|
2058
|
+
if (!stat5.isFile()) {
|
|
2059
|
+
contents[rel] = "[skipped: not a file]";
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
let text = await fs4.readFile(abs, "utf-8");
|
|
2063
|
+
if (text.length > MAX_FILE_BYTES2) {
|
|
2064
|
+
text = text.slice(0, MAX_FILE_BYTES2) + "\n\u2026[truncated]";
|
|
2065
|
+
truncated = true;
|
|
2066
|
+
}
|
|
2067
|
+
if (total + text.length > MAX_TOTAL_BYTES) {
|
|
2068
|
+
contents[rel] = "[skipped: total payload size limit reached]";
|
|
2069
|
+
truncated = true;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
contents[rel] = text;
|
|
2073
|
+
total += text.length;
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
contents[rel] = `[error reading: ${err?.code || err?.message || "unknown"}]`;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
const url = `${settings.backendUrl}/code-workspace/self-eval`;
|
|
2079
|
+
try {
|
|
2080
|
+
const res = await fetch(url, {
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: {
|
|
2083
|
+
"Authorization": `Bearer ${settings.authBearer}`,
|
|
2084
|
+
"Content-Type": "application/json"
|
|
2085
|
+
},
|
|
2086
|
+
body: JSON.stringify({
|
|
2087
|
+
stack_hint: settings.stackHint,
|
|
2088
|
+
files,
|
|
2089
|
+
summary: args.summary,
|
|
2090
|
+
file_contents: contents,
|
|
2091
|
+
// Reuse the workspace LLM creds — grader uses the same backend
|
|
2092
|
+
provider: settings.provider,
|
|
2093
|
+
model: settings.model,
|
|
2094
|
+
base_url: settings.baseUrl
|
|
2095
|
+
})
|
|
2096
|
+
});
|
|
2097
|
+
if (!res.ok) {
|
|
2098
|
+
return { skipped: true, verdict: `backend ${res.status}: ${await res.text().catch(() => "")}` };
|
|
2099
|
+
}
|
|
2100
|
+
const result = await res.json();
|
|
2101
|
+
if (truncated) result.payload_truncated = true;
|
|
2102
|
+
return result;
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
return { skipped: true, verdict: `client fetch failed: ${err?.message || String(err)}` };
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/executors/prComments.ts
|
|
2109
|
+
var import_node_child_process7 = require("node:child_process");
|
|
2110
|
+
async function gh(args, cwd, timeoutMs = 3e4) {
|
|
2111
|
+
return new Promise((resolve3) => {
|
|
2112
|
+
let proc;
|
|
2113
|
+
try {
|
|
2114
|
+
proc = (0, import_node_child_process7.spawn)("gh", args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
2115
|
+
} catch (e) {
|
|
2116
|
+
return resolve3({ ok: false, stdout: "", stderr: e?.message || "spawn failed", exitCode: -1 });
|
|
2117
|
+
}
|
|
2118
|
+
let out = "", err = "";
|
|
2119
|
+
proc.stdout?.setEncoding("utf-8");
|
|
2120
|
+
proc.stderr?.setEncoding("utf-8");
|
|
2121
|
+
proc.stdout?.on("data", (c) => out += c);
|
|
2122
|
+
proc.stderr?.on("data", (c) => err += c);
|
|
2123
|
+
proc.on("error", () => resolve3({ ok: false, stdout: out, stderr: err || "spawn error", exitCode: -1 }));
|
|
2124
|
+
proc.on("exit", (code) => resolve3({ ok: code === 0, stdout: out, stderr: err, exitCode: code ?? -1 }));
|
|
2125
|
+
setTimeout(() => {
|
|
2126
|
+
try {
|
|
2127
|
+
proc.kill();
|
|
2128
|
+
} catch {
|
|
2129
|
+
}
|
|
2130
|
+
}, timeoutMs);
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
async function ghAvailable(cwd) {
|
|
2134
|
+
const ver = await gh(["--version"], cwd, 5e3);
|
|
2135
|
+
if (!ver.ok) return {
|
|
2136
|
+
ok: false,
|
|
2137
|
+
reason: "gh_not_installed",
|
|
2138
|
+
hint: "Install GitHub CLI: winget install GitHub.cli (or https://cli.github.com)"
|
|
2139
|
+
};
|
|
2140
|
+
const auth = await gh(["auth", "status"], cwd, 5e3);
|
|
2141
|
+
if (!auth.ok) return {
|
|
2142
|
+
ok: false,
|
|
2143
|
+
reason: "gh_not_authenticated",
|
|
2144
|
+
hint: "Run `gh auth login` in a terminal, then retry."
|
|
2145
|
+
};
|
|
2146
|
+
return { ok: true };
|
|
2147
|
+
}
|
|
2148
|
+
async function readPRComments(args) {
|
|
2149
|
+
const root = workspaceRoot();
|
|
2150
|
+
const ghCheck = await ghAvailable(root);
|
|
2151
|
+
if (!ghCheck.ok) return ghCheck;
|
|
2152
|
+
const prId = args.pr != null ? String(args.pr) : "";
|
|
2153
|
+
const viewArgs = ["pr", "view"];
|
|
2154
|
+
if (prId) viewArgs.push(prId);
|
|
2155
|
+
viewArgs.push("--json", "number,title,state,url,comments,reviews");
|
|
2156
|
+
const r = await gh(viewArgs, root, 2e4);
|
|
2157
|
+
if (!r.ok) {
|
|
2158
|
+
return {
|
|
2159
|
+
error: "gh_pr_view_failed",
|
|
2160
|
+
stderr: r.stderr.slice(0, 400),
|
|
2161
|
+
hint: prId ? `Check PR #${prId} exists.` : "No PR found for the current branch \u2014 pass `pr` explicitly or open a PR first."
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
let data;
|
|
2165
|
+
try {
|
|
2166
|
+
data = JSON.parse(r.stdout);
|
|
2167
|
+
} catch {
|
|
2168
|
+
return { error: "gh_output_not_json", stdout: r.stdout.slice(0, 300) };
|
|
2169
|
+
}
|
|
2170
|
+
const max = Math.min(args.max_comments ?? 50, 200);
|
|
2171
|
+
const flat = [];
|
|
2172
|
+
for (const c of data.comments || []) {
|
|
2173
|
+
flat.push({
|
|
2174
|
+
kind: "issue_comment",
|
|
2175
|
+
author: c.author?.login || "(unknown)",
|
|
2176
|
+
body: c.body || "",
|
|
2177
|
+
created_at: c.createdAt || ""
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
for (const rv of data.reviews || []) {
|
|
2181
|
+
flat.push({
|
|
2182
|
+
kind: "review",
|
|
2183
|
+
author: rv.author?.login || "(unknown)",
|
|
2184
|
+
body: rv.body || "",
|
|
2185
|
+
created_at: rv.submittedAt || "",
|
|
2186
|
+
review_id: rv.id,
|
|
2187
|
+
review_state: rv.state
|
|
2188
|
+
// APPROVED / CHANGES_REQUESTED / COMMENTED
|
|
2189
|
+
});
|
|
2190
|
+
for (const ic of rv.comments || []) {
|
|
2191
|
+
flat.push({
|
|
2192
|
+
kind: "review_comment",
|
|
2193
|
+
author: ic.author?.login || rv.author?.login || "(unknown)",
|
|
2194
|
+
body: ic.body || "",
|
|
2195
|
+
created_at: ic.createdAt || rv.submittedAt || "",
|
|
2196
|
+
path: ic.path,
|
|
2197
|
+
line: ic.line ?? ic.originalLine,
|
|
2198
|
+
review_id: rv.id,
|
|
2199
|
+
review_state: rv.state
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
flat.sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
|
|
2204
|
+
return {
|
|
2205
|
+
pr_number: data.number,
|
|
2206
|
+
pr_title: data.title,
|
|
2207
|
+
pr_state: data.state,
|
|
2208
|
+
pr_url: data.url,
|
|
2209
|
+
total_comments: flat.length,
|
|
2210
|
+
comments: flat.slice(0, max),
|
|
2211
|
+
truncated: flat.length > max
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
async function replyPRComment(args) {
|
|
2215
|
+
const root = workspaceRoot();
|
|
2216
|
+
const ghCheck = await ghAvailable(root);
|
|
2217
|
+
if (!ghCheck.ok) return ghCheck;
|
|
2218
|
+
if (!args.body || typeof args.body !== "string" || args.body.trim().length === 0) {
|
|
2219
|
+
return { error: "empty_body", hint: "Provide a non-empty body." };
|
|
2220
|
+
}
|
|
2221
|
+
const prArg = args.pr != null ? String(args.pr) : "";
|
|
2222
|
+
const cmd = ["pr", "comment"];
|
|
2223
|
+
if (prArg) cmd.push(prArg);
|
|
2224
|
+
cmd.push("--body", args.body);
|
|
2225
|
+
const r = await gh(cmd, root, 2e4);
|
|
2226
|
+
if (!r.ok) {
|
|
2227
|
+
return {
|
|
2228
|
+
error: "gh_pr_comment_failed",
|
|
2229
|
+
stderr: r.stderr.slice(0, 400),
|
|
2230
|
+
hint: "Confirm the PR exists and you have write access. For thread replies (not just top-level), use create_pr_review_reply (TBD)."
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
const url = (r.stdout.match(/https:\/\/github\.com\/[^\s]+/) || [""])[0];
|
|
2234
|
+
return { posted: true, comment_url: url, pr: prArg || "current-branch" };
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/executors/applyDiff.ts
|
|
2238
|
+
var fs5 = __toESM(require("node:fs/promises"));
|
|
2239
|
+
async function applyDiff(args, rb) {
|
|
2240
|
+
const abs = resolveInWorkspace(args.path);
|
|
2241
|
+
const baseResult = {
|
|
2242
|
+
path: args.path,
|
|
2243
|
+
applied: false,
|
|
2244
|
+
hunks_total: 0,
|
|
2245
|
+
hunks_applied: 0,
|
|
2246
|
+
hunks_failed: 0,
|
|
2247
|
+
per_hunk: []
|
|
2248
|
+
};
|
|
2249
|
+
if (typeof args.diff !== "string" || !args.diff.trim()) {
|
|
2250
|
+
return { ...baseResult, hint: "empty diff" };
|
|
2251
|
+
}
|
|
2252
|
+
let content;
|
|
2253
|
+
let isNewFile = false;
|
|
2254
|
+
try {
|
|
2255
|
+
content = await fs5.readFile(abs, "utf-8");
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
if (err.code === "ENOENT") {
|
|
2258
|
+
content = "";
|
|
2259
|
+
isNewFile = true;
|
|
2260
|
+
} else return { ...baseResult, hint: `${err.code}: ${err.message}` };
|
|
2261
|
+
}
|
|
2262
|
+
let hunks;
|
|
2263
|
+
try {
|
|
2264
|
+
hunks = _parseUnifiedDiff(args.diff);
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
return { ...baseResult, hint: `parse error: ${err.message}` };
|
|
2267
|
+
}
|
|
2268
|
+
baseResult.hunks_total = hunks.length;
|
|
2269
|
+
if (hunks.length === 0) {
|
|
2270
|
+
return { ...baseResult, hint: "no @@ hunk headers found in diff" };
|
|
2271
|
+
}
|
|
2272
|
+
const rollback_token = await rb.snapshot(abs);
|
|
2273
|
+
let lines = content === "" && isNewFile ? [] : content.split(/\r?\n/);
|
|
2274
|
+
const endsWithNewline = content.endsWith("\n") || content.endsWith("\r\n");
|
|
2275
|
+
const sortedHunks = hunks.map((h, i) => ({ h, originalIndex: i })).sort((a, b) => b.h.oldStart - a.h.oldStart);
|
|
2276
|
+
for (const { h, originalIndex } of sortedHunks) {
|
|
2277
|
+
const apply = _applyOneHunk(lines, h);
|
|
2278
|
+
if (apply.applied) {
|
|
2279
|
+
lines = apply.lines;
|
|
2280
|
+
baseResult.per_hunk.push({
|
|
2281
|
+
index: originalIndex,
|
|
2282
|
+
applied: true,
|
|
2283
|
+
method: apply.method
|
|
2284
|
+
});
|
|
2285
|
+
baseResult.hunks_applied++;
|
|
2286
|
+
} else {
|
|
2287
|
+
baseResult.per_hunk.push({
|
|
2288
|
+
index: originalIndex,
|
|
2289
|
+
applied: false,
|
|
2290
|
+
reason: apply.reason,
|
|
2291
|
+
closest_match: apply.closest_match
|
|
2292
|
+
});
|
|
2293
|
+
baseResult.hunks_failed++;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
baseResult.per_hunk.sort((a, b) => a.index - b.index);
|
|
2297
|
+
if (baseResult.hunks_failed === baseResult.hunks_total) {
|
|
2298
|
+
return {
|
|
2299
|
+
...baseResult,
|
|
2300
|
+
rollback_token,
|
|
2301
|
+
hint: "no hunks could be applied \u2014 read the file and retry with fresh context"
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
const next = lines.join("\n") + (endsWithNewline && lines.length > 0 ? "\n" : "");
|
|
2305
|
+
if (isNewFile) {
|
|
2306
|
+
const path9 = await import("node:path");
|
|
2307
|
+
await fs5.mkdir(path9.dirname(abs), { recursive: true });
|
|
2308
|
+
}
|
|
2309
|
+
await fs5.writeFile(abs, next, "utf-8");
|
|
2310
|
+
baseResult.applied = baseResult.hunks_failed === 0;
|
|
2311
|
+
baseResult.rollback_token = rollback_token;
|
|
2312
|
+
if (baseResult.hunks_failed > 0) {
|
|
2313
|
+
baseResult.hint = `${baseResult.hunks_failed}/${baseResult.hunks_total} hunks failed \u2014 file partially patched. Inspect per_hunk + retry the failed ones (or apply_diff a fresh diff against the new state).`;
|
|
2314
|
+
}
|
|
2315
|
+
return baseResult;
|
|
2316
|
+
}
|
|
2317
|
+
function _parseUnifiedDiff(diff) {
|
|
2318
|
+
const hunks = [];
|
|
2319
|
+
const rawLines = diff.split(/\r?\n/);
|
|
2320
|
+
let i = 0;
|
|
2321
|
+
while (i < rawLines.length && (rawLines[i].startsWith("---") || rawLines[i].startsWith("+++"))) {
|
|
2322
|
+
i++;
|
|
2323
|
+
}
|
|
2324
|
+
const hunkHeader = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/;
|
|
2325
|
+
while (i < rawLines.length) {
|
|
2326
|
+
const m = hunkHeader.exec(rawLines[i]);
|
|
2327
|
+
if (!m) {
|
|
2328
|
+
i++;
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
const oldStart = parseInt(m[1], 10);
|
|
2332
|
+
i++;
|
|
2333
|
+
const startBody = i;
|
|
2334
|
+
while (i < rawLines.length && !rawLines[i].startsWith("@@")) i++;
|
|
2335
|
+
const body = rawLines.slice(startBody, i);
|
|
2336
|
+
const oldLines = [];
|
|
2337
|
+
const newLines = [];
|
|
2338
|
+
for (const ln of body) {
|
|
2339
|
+
if (ln.startsWith("\\ ")) continue;
|
|
2340
|
+
if (ln.length === 0) {
|
|
2341
|
+
oldLines.push("");
|
|
2342
|
+
newLines.push("");
|
|
2343
|
+
} else if (ln[0] === " ") {
|
|
2344
|
+
oldLines.push(ln.slice(1));
|
|
2345
|
+
newLines.push(ln.slice(1));
|
|
2346
|
+
} else if (ln[0] === "-") {
|
|
2347
|
+
oldLines.push(ln.slice(1));
|
|
2348
|
+
} else if (ln[0] === "+") {
|
|
2349
|
+
newLines.push(ln.slice(1));
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
hunks.push({ oldStart, oldLines, newLines, raw: rawLines.slice(startBody - 1, i).join("\n") });
|
|
2353
|
+
}
|
|
2354
|
+
return hunks;
|
|
2355
|
+
}
|
|
2356
|
+
function _applyOneHunk(lines, h) {
|
|
2357
|
+
const N = h.oldLines.length;
|
|
2358
|
+
if (N === 0) {
|
|
2359
|
+
const at = Math.max(0, Math.min(h.oldStart - 1, lines.length));
|
|
2360
|
+
return {
|
|
2361
|
+
applied: true,
|
|
2362
|
+
method: "exact_at_line",
|
|
2363
|
+
lines: [...lines.slice(0, at), ...h.newLines, ...lines.slice(at)]
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
const matchesAt = (start, flex) => {
|
|
2367
|
+
if (start < 0 || start + N > lines.length) return false;
|
|
2368
|
+
for (let j = 0; j < N; j++) {
|
|
2369
|
+
const a = lines[start + j], b = h.oldLines[j];
|
|
2370
|
+
if (flex) {
|
|
2371
|
+
if (a.trim() !== b.trim()) return false;
|
|
2372
|
+
} else {
|
|
2373
|
+
if (a !== b) return false;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
return true;
|
|
2377
|
+
};
|
|
2378
|
+
const exactAt = h.oldStart - 1;
|
|
2379
|
+
if (matchesAt(exactAt, false)) {
|
|
2380
|
+
return {
|
|
2381
|
+
applied: true,
|
|
2382
|
+
method: "exact_at_line",
|
|
2383
|
+
lines: [...lines.slice(0, exactAt), ...h.newLines, ...lines.slice(exactAt + N)]
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
for (let i = 0; i <= lines.length - N; i++) {
|
|
2387
|
+
if (matchesAt(i, false)) {
|
|
2388
|
+
return {
|
|
2389
|
+
applied: true,
|
|
2390
|
+
method: "exact_anywhere",
|
|
2391
|
+
lines: [...lines.slice(0, i), ...h.newLines, ...lines.slice(i + N)]
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
for (let i = 0; i <= lines.length - N; i++) {
|
|
2396
|
+
if (matchesAt(i, true)) {
|
|
2397
|
+
return {
|
|
2398
|
+
applied: true,
|
|
2399
|
+
method: "fuzzy_whitespace",
|
|
2400
|
+
lines: [...lines.slice(0, i), ...h.newLines, ...lines.slice(i + N)]
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
let bestScore = -1, bestStart = -1;
|
|
2405
|
+
for (let i = 0; i <= lines.length - N; i++) {
|
|
2406
|
+
let matches = 0;
|
|
2407
|
+
for (let j = 0; j < N; j++) {
|
|
2408
|
+
if (lines[i + j].trim() === h.oldLines[j].trim()) matches++;
|
|
2409
|
+
}
|
|
2410
|
+
if (matches > bestScore) {
|
|
2411
|
+
bestScore = matches;
|
|
2412
|
+
bestStart = i;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const cm = bestStart >= 0 ? {
|
|
2416
|
+
start_line: bestStart + 1,
|
|
2417
|
+
similarity: Math.round(bestScore / N * 100),
|
|
2418
|
+
actual: lines.slice(bestStart, bestStart + N).join("\n")
|
|
2419
|
+
} : void 0;
|
|
2420
|
+
return {
|
|
2421
|
+
applied: false,
|
|
2422
|
+
reason: `hunk old-block not found (claimed line ${h.oldStart}, ${N} lines)`,
|
|
2423
|
+
closest_match: cm
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/executors/extras.ts
|
|
2428
|
+
var import_node_child_process8 = require("node:child_process");
|
|
2429
|
+
var fs6 = __toESM(require("node:fs/promises"));
|
|
2430
|
+
async function git(args, cwd, timeoutMs = 2e4) {
|
|
2431
|
+
return new Promise((resolve3) => {
|
|
2432
|
+
let proc;
|
|
2433
|
+
try {
|
|
2434
|
+
proc = (0, import_node_child_process8.spawn)("git", args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
2435
|
+
} catch (e) {
|
|
2436
|
+
return resolve3({ ok: false, stdout: "", stderr: e?.message || "spawn failed" });
|
|
2437
|
+
}
|
|
2438
|
+
let out = "", err = "";
|
|
2439
|
+
proc.stdout?.setEncoding("utf-8");
|
|
2440
|
+
proc.stderr?.setEncoding("utf-8");
|
|
2441
|
+
proc.stdout?.on("data", (c) => out += c);
|
|
2442
|
+
proc.stderr?.on("data", (c) => err += c);
|
|
2443
|
+
proc.on("error", () => resolve3({ ok: false, stdout: out, stderr: err }));
|
|
2444
|
+
proc.on("exit", (code) => resolve3({ ok: code === 0, stdout: out, stderr: err }));
|
|
2445
|
+
setTimeout(() => {
|
|
2446
|
+
try {
|
|
2447
|
+
proc.kill();
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
}, timeoutMs);
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
async function gitDiffSummary(args) {
|
|
2454
|
+
const root = workspaceRoot();
|
|
2455
|
+
const isRepo = await git(["rev-parse", "--is-inside-work-tree"], root, 5e3);
|
|
2456
|
+
if (!isRepo.ok || isRepo.stdout.trim() !== "true") {
|
|
2457
|
+
return {
|
|
2458
|
+
error: "not_a_git_repo",
|
|
2459
|
+
hint: "git init this workspace first, or run from a folder that's tracked."
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
const baseArgs = ["diff"];
|
|
2463
|
+
if (args.base) baseArgs.push(args.base);
|
|
2464
|
+
if (args.head) baseArgs.push(args.head);
|
|
2465
|
+
const pathArgs = args.paths && args.paths.length ? ["--", ...args.paths] : [];
|
|
2466
|
+
const stat5 = await git([...baseArgs, "--stat", ...pathArgs], root, 3e4);
|
|
2467
|
+
if (!stat5.ok) {
|
|
2468
|
+
return { error: "git_diff_stat_failed", stderr: stat5.stderr.slice(0, 400) };
|
|
2469
|
+
}
|
|
2470
|
+
const lastLine = stat5.stdout.trim().split(/\r?\n/).pop() || "";
|
|
2471
|
+
const filesM = /(\d+)\s+files?\s+changed/.exec(lastLine);
|
|
2472
|
+
const insM = /(\d+)\s+insertions?\(\+\)/.exec(lastLine);
|
|
2473
|
+
const delM = /(\d+)\s+deletions?\(-\)/.exec(lastLine);
|
|
2474
|
+
const stat_summary = {
|
|
2475
|
+
files_changed: filesM ? parseInt(filesM[1], 10) : 0,
|
|
2476
|
+
lines_added: insM ? parseInt(insM[1], 10) : 0,
|
|
2477
|
+
lines_removed: delM ? parseInt(delM[1], 10) : 0
|
|
2478
|
+
};
|
|
2479
|
+
if (args.stat_only) {
|
|
2480
|
+
return { stat: stat5.stdout, ...stat_summary, raw_diff: null };
|
|
2481
|
+
}
|
|
2482
|
+
const diff = await git([...baseArgs, ...pathArgs], root, 3e4);
|
|
2483
|
+
if (!diff.ok) {
|
|
2484
|
+
return { error: "git_diff_failed", stderr: diff.stderr.slice(0, 400), stat: stat5.stdout, ...stat_summary };
|
|
2485
|
+
}
|
|
2486
|
+
const diffLines = diff.stdout.split(/\r?\n/);
|
|
2487
|
+
const truncated = diffLines.length > 2e4;
|
|
2488
|
+
const raw = truncated ? diffLines.slice(0, 2e4).join("\n") + "\n... [truncated; pass narrower paths]" : diff.stdout;
|
|
2489
|
+
return { stat: stat5.stdout, ...stat_summary, raw_diff: raw, truncated };
|
|
2490
|
+
}
|
|
2491
|
+
async function readPDF(args, settings) {
|
|
2492
|
+
const abs = resolveInWorkspace(args.path);
|
|
2493
|
+
let bytes;
|
|
2494
|
+
try {
|
|
2495
|
+
bytes = await fs6.readFile(abs);
|
|
2496
|
+
} catch (err) {
|
|
2497
|
+
return { error: err.code || "ENOENT", path: args.path, message: err.message };
|
|
2498
|
+
}
|
|
2499
|
+
if (bytes.length > 50 * 1024 * 1024) {
|
|
2500
|
+
return {
|
|
2501
|
+
error: "file_too_large",
|
|
2502
|
+
size_bytes: bytes.length,
|
|
2503
|
+
hint: "PDF > 50MB; split or convert before reading."
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const b64 = bytes.toString("base64");
|
|
2507
|
+
const url = `${settings.backendUrl}/code-workspace/read-pdf`;
|
|
2508
|
+
try {
|
|
2509
|
+
const res = await fetch(url, {
|
|
2510
|
+
method: "POST",
|
|
2511
|
+
headers: {
|
|
2512
|
+
"Authorization": `Bearer ${settings.authBearer}`,
|
|
2513
|
+
"Content-Type": "application/json"
|
|
2514
|
+
},
|
|
2515
|
+
body: JSON.stringify({ filename: args.path, content_b64: b64 })
|
|
2516
|
+
});
|
|
2517
|
+
if (!res.ok) {
|
|
2518
|
+
return { error: `backend ${res.status}: ${await res.text().catch(() => "")}` };
|
|
2519
|
+
}
|
|
2520
|
+
const out = await res.json();
|
|
2521
|
+
return { path: args.path, ...out };
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
return { error: `client fetch failed: ${err?.message || String(err)}`, path: args.path };
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
async function runPythonREPL(args, settings) {
|
|
2527
|
+
const url = `${settings.backendUrl}/code-workspace/python-repl`;
|
|
2528
|
+
try {
|
|
2529
|
+
const res = await fetch(url, {
|
|
2530
|
+
method: "POST",
|
|
2531
|
+
headers: {
|
|
2532
|
+
"Authorization": `Bearer ${settings.authBearer}`,
|
|
2533
|
+
"Content-Type": "application/json"
|
|
2534
|
+
},
|
|
2535
|
+
body: JSON.stringify({
|
|
2536
|
+
code: args.code,
|
|
2537
|
+
session_id: args.session_id,
|
|
2538
|
+
timeout_s: args.timeout_s ?? 30
|
|
2539
|
+
})
|
|
2540
|
+
});
|
|
2541
|
+
if (!res.ok) {
|
|
2542
|
+
return { error: `backend ${res.status}: ${await res.text().catch(() => "")}` };
|
|
2543
|
+
}
|
|
2544
|
+
return await res.json();
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
return { error: `client fetch failed: ${err?.message || String(err)}` };
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// src/executors/mounts.ts
|
|
2551
|
+
var fs7 = __toESM(require("node:fs/promises"));
|
|
2552
|
+
var path5 = __toESM(require("node:path"));
|
|
2553
|
+
var _PROTECTED_PREFIXES = [
|
|
2554
|
+
"/etc",
|
|
2555
|
+
"/usr/bin",
|
|
2556
|
+
"/usr/sbin",
|
|
2557
|
+
"/bin",
|
|
2558
|
+
"/sbin",
|
|
2559
|
+
"/sys",
|
|
2560
|
+
"/proc",
|
|
2561
|
+
"/dev",
|
|
2562
|
+
"C:\\Windows",
|
|
2563
|
+
"C:\\Program Files",
|
|
2564
|
+
"C:\\Program Files (x86)",
|
|
2565
|
+
"C:\\ProgramData"
|
|
2566
|
+
];
|
|
2567
|
+
function isProtectedHost(abs) {
|
|
2568
|
+
const norm = abs.replace(/\\/g, "/").toLowerCase();
|
|
2569
|
+
for (const p of _PROTECTED_PREFIXES) {
|
|
2570
|
+
const n = p.replace(/\\/g, "/").toLowerCase();
|
|
2571
|
+
if (norm === n || norm.startsWith(n + "/")) return p;
|
|
2572
|
+
}
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
async function mountExternalFolder(args, state, opts) {
|
|
2576
|
+
const raw = (args.path || "").trim();
|
|
2577
|
+
if (!raw) return { ok: false, error: "empty path" };
|
|
2578
|
+
const purpose = (args.purpose || "").trim();
|
|
2579
|
+
const abs = path5.resolve(raw);
|
|
2580
|
+
const proto = isProtectedHost(abs);
|
|
2581
|
+
if (proto) {
|
|
2582
|
+
return {
|
|
2583
|
+
ok: false,
|
|
2584
|
+
error: "protected_host_dir",
|
|
2585
|
+
refused: proto,
|
|
2586
|
+
hint: "system directories are never mountable"
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
try {
|
|
2590
|
+
const st = await fs7.stat(abs);
|
|
2591
|
+
if (!st.isDirectory()) {
|
|
2592
|
+
return { ok: false, error: "not_a_directory", path: abs };
|
|
2593
|
+
}
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
return { ok: false, error: "stat_failed", path: abs, message: err?.message || String(err) };
|
|
2596
|
+
}
|
|
2597
|
+
if (state.mountedFolders.has(abs)) {
|
|
2598
|
+
return { ok: true, already_mounted: true, path: abs, mode: "read-only" };
|
|
2599
|
+
}
|
|
2600
|
+
if (!opts?.autoApprove) {
|
|
2601
|
+
let vscode = null;
|
|
2602
|
+
try {
|
|
2603
|
+
vscode = require("vscode");
|
|
2604
|
+
} catch {
|
|
2605
|
+
}
|
|
2606
|
+
if (vscode) {
|
|
2607
|
+
const choice = await vscode.window.showWarningMessage(
|
|
2608
|
+
`Allow the agent READ-ONLY access to:
|
|
2609
|
+
${abs}
|
|
2610
|
+
|
|
2611
|
+
Purpose: ${purpose || "(none given)"}
|
|
2612
|
+
|
|
2613
|
+
Only first mount of each path prompts.`,
|
|
2614
|
+
{ modal: true },
|
|
2615
|
+
"Allow",
|
|
2616
|
+
"Deny"
|
|
2617
|
+
);
|
|
2618
|
+
if (choice !== "Allow") {
|
|
2619
|
+
return { ok: false, error: "user_denied", path: abs };
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
state.mountedFolders.add(abs);
|
|
2624
|
+
return {
|
|
2625
|
+
ok: true,
|
|
2626
|
+
mounted: abs,
|
|
2627
|
+
mode: "read-only",
|
|
2628
|
+
purpose,
|
|
2629
|
+
note: "subsequent read_file / list_dir / glob / grep can now target absolute paths under this folder",
|
|
2630
|
+
total_mounted: state.mountedFolders.size
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// src/executors/datasets.ts
|
|
2635
|
+
var path6 = __toESM(require("node:path"));
|
|
2636
|
+
var fs8 = __toESM(require("node:fs/promises"));
|
|
2637
|
+
var import_node_child_process9 = require("node:child_process");
|
|
2638
|
+
function sanitize(name) {
|
|
2639
|
+
return name.replace(/[/\\:?"<>|*]/g, "_").replace(/_+/g, "_").slice(0, 80);
|
|
2640
|
+
}
|
|
2641
|
+
async function which(cmd) {
|
|
2642
|
+
return new Promise((resolve3) => {
|
|
2643
|
+
const probe = (0, import_node_child_process9.spawn)(process.platform === "win32" ? "where" : "which", [cmd], { shell: false });
|
|
2644
|
+
let found = false;
|
|
2645
|
+
probe.stdout?.on("data", () => {
|
|
2646
|
+
found = true;
|
|
2647
|
+
});
|
|
2648
|
+
probe.on("close", (code) => resolve3(found && code === 0));
|
|
2649
|
+
probe.on("error", () => resolve3(false));
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
function defaultDest(source, name) {
|
|
2653
|
+
return `.datasets/${source}-${sanitize(name)}`;
|
|
2654
|
+
}
|
|
2655
|
+
function buildCommand(source, name, dest, kind) {
|
|
2656
|
+
switch (source) {
|
|
2657
|
+
case "huggingface": {
|
|
2658
|
+
const tier = kind === "model" ? "" : "--repo-type dataset";
|
|
2659
|
+
return {
|
|
2660
|
+
command: `huggingface-cli download ${tier} ${name} --local-dir ${JSON.stringify(dest)} --local-dir-use-symlinks False`,
|
|
2661
|
+
pre_check: "huggingface-cli"
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
case "kaggle":
|
|
2665
|
+
return {
|
|
2666
|
+
command: `kaggle datasets download -d ${name} -p ${JSON.stringify(dest)} --unzip`,
|
|
2667
|
+
pre_check: "kaggle"
|
|
2668
|
+
};
|
|
2669
|
+
case "github": {
|
|
2670
|
+
const url = name.startsWith("http") ? name : `https://github.com/${name}.git`;
|
|
2671
|
+
return {
|
|
2672
|
+
command: `git clone --depth=1 ${url} ${JSON.stringify(dest)}`,
|
|
2673
|
+
pre_check: "git"
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
case "url": {
|
|
2677
|
+
const filename = name.split("/").pop() || "download";
|
|
2678
|
+
const safe = sanitize(filename);
|
|
2679
|
+
return {
|
|
2680
|
+
command: process.platform === "win32" ? `curl -L -o ${JSON.stringify(path6.join(dest, safe))} ${JSON.stringify(name)}` : `mkdir -p ${JSON.stringify(dest)} && curl -L -o ${JSON.stringify(path6.join(dest, safe))} ${JSON.stringify(name)}`,
|
|
2681
|
+
pre_check: "curl"
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
async function downloadDataset(args, settings) {
|
|
2687
|
+
const source = args.source;
|
|
2688
|
+
const name = (args.name_or_url || "").trim();
|
|
2689
|
+
if (!name) return { ok: false, error: "name_or_url is empty" };
|
|
2690
|
+
if (!["huggingface", "kaggle", "github", "url"].includes(source)) {
|
|
2691
|
+
return { ok: false, error: `unknown source ${JSON.stringify(source)}` };
|
|
2692
|
+
}
|
|
2693
|
+
const dest = args.dest_path?.trim() || defaultDest(source, name);
|
|
2694
|
+
let destAbs;
|
|
2695
|
+
try {
|
|
2696
|
+
destAbs = resolveInWorkspace(dest);
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
return { ok: false, error: "dest_path outside workspace", message: err?.message };
|
|
2699
|
+
}
|
|
2700
|
+
try {
|
|
2701
|
+
await fs8.mkdir(destAbs, { recursive: true });
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
return { ok: false, error: "mkdir_failed", message: err?.message };
|
|
2704
|
+
}
|
|
2705
|
+
const { command, pre_check } = buildCommand(source, name, destAbs, args.kind);
|
|
2706
|
+
if (pre_check) {
|
|
2707
|
+
const ok = await which(pre_check);
|
|
2708
|
+
if (!ok) {
|
|
2709
|
+
const installHint = {
|
|
2710
|
+
"huggingface-cli": "pip install huggingface_hub",
|
|
2711
|
+
"kaggle": "pip install kaggle (and put your kaggle.json in ~/.kaggle/)",
|
|
2712
|
+
"git": "install git",
|
|
2713
|
+
"curl": "install curl"
|
|
2714
|
+
}[pre_check] || "install the missing tool";
|
|
2715
|
+
return {
|
|
2716
|
+
ok: false,
|
|
2717
|
+
error: "tool_not_found",
|
|
2718
|
+
missing: pre_check,
|
|
2719
|
+
install_hint: installHint,
|
|
2720
|
+
would_have_run: command
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const taskRes = await startBackgroundTask(
|
|
2725
|
+
{
|
|
2726
|
+
command,
|
|
2727
|
+
cwd: workspaceRoot(),
|
|
2728
|
+
label: `download:${source}:${sanitize(name).slice(0, 40)}`
|
|
2729
|
+
},
|
|
2730
|
+
settings
|
|
2731
|
+
);
|
|
2732
|
+
return {
|
|
2733
|
+
ok: true,
|
|
2734
|
+
source,
|
|
2735
|
+
name,
|
|
2736
|
+
dest_path: dest,
|
|
2737
|
+
dest_abs: destAbs,
|
|
2738
|
+
command_running: command,
|
|
2739
|
+
task_id: taskRes?.task_id || taskRes?.id || null,
|
|
2740
|
+
note: "download running as background task \u2014 use check_background_task to poll progress",
|
|
2741
|
+
task_info: taskRes
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/executors/index.ts
|
|
2746
|
+
var WRITES = /* @__PURE__ */ new Set(["write_file", "edit_file", "apply_diff"]);
|
|
2747
|
+
var SHELLS = /* @__PURE__ */ new Set(["run_command"]);
|
|
2748
|
+
var LOCAL_PLANE_LIFECYCLE = /* @__PURE__ */ new Set(["kill_process"]);
|
|
2749
|
+
function needsApproval(name, settings) {
|
|
2750
|
+
if (WRITES.has(name) && !settings.autoApproveWrites) return true;
|
|
2751
|
+
if (SHELLS.has(name) && !settings.autoApproveCommands) return true;
|
|
2752
|
+
if (LOCAL_PLANE_LIFECYCLE.has(name) && !settings.autoApproveCommands) return true;
|
|
2753
|
+
return false;
|
|
2754
|
+
}
|
|
2755
|
+
async function execute(name, args, ctx) {
|
|
2756
|
+
try {
|
|
2757
|
+
switch (name) {
|
|
2758
|
+
// ── Filesystem ─────────────────────────────────────────────
|
|
2759
|
+
case "list_dir":
|
|
2760
|
+
return await listDir(args, ctx.mountedFolders);
|
|
2761
|
+
case "read_file":
|
|
2762
|
+
return await readFile3(args, ctx.mountedFolders);
|
|
2763
|
+
case "write_file":
|
|
2764
|
+
return await writeFile3(args, ctx.rollback);
|
|
2765
|
+
case "edit_file":
|
|
2766
|
+
return await editFile(args, ctx.rollback);
|
|
2767
|
+
case "apply_diff":
|
|
2768
|
+
return await applyDiff(args, ctx.rollback);
|
|
2769
|
+
case "glob":
|
|
2770
|
+
return await glob(args, ctx.mountedFolders);
|
|
2771
|
+
case "grep":
|
|
2772
|
+
return await grep(args, ctx.mountedFolders);
|
|
2773
|
+
// ── Shell ─────────────────────────────────────────────────
|
|
2774
|
+
case "run_command":
|
|
2775
|
+
return await runCommand(args, ctx.registry, ctx.signal);
|
|
2776
|
+
// ── Local Environment Plane ────────────────────────────────
|
|
2777
|
+
case "system_info":
|
|
2778
|
+
return await systemInfo(args);
|
|
2779
|
+
case "list_processes":
|
|
2780
|
+
return listProcesses(args, ctx.registry);
|
|
2781
|
+
case "allocate_port":
|
|
2782
|
+
return await allocatePort(args);
|
|
2783
|
+
case "kill_process":
|
|
2784
|
+
return await killProcess(args, ctx.registry);
|
|
2785
|
+
case "verify_package":
|
|
2786
|
+
return await verifyPackage(args, ctx.settings);
|
|
2787
|
+
case "verify_package_batch":
|
|
2788
|
+
return await verifyPackageBatch(args, ctx.settings);
|
|
2789
|
+
case "web_search":
|
|
2790
|
+
return await webSearch(args, ctx.settings);
|
|
2791
|
+
case "gpu_info":
|
|
2792
|
+
return await gpuInfo(args, ctx.settings);
|
|
2793
|
+
case "start_background_task":
|
|
2794
|
+
return await startBackgroundTask(args, ctx.settings);
|
|
2795
|
+
case "check_background_task":
|
|
2796
|
+
return await checkBackgroundTask(args, ctx.settings);
|
|
2797
|
+
case "list_background_tasks":
|
|
2798
|
+
return await listBackgroundTasks(args, ctx.settings);
|
|
2799
|
+
case "cancel_background_task":
|
|
2800
|
+
return await cancelBackgroundTask(args, ctx.settings);
|
|
2801
|
+
case "run_lint":
|
|
2802
|
+
return await runLint(args);
|
|
2803
|
+
case "create_pr":
|
|
2804
|
+
return await createPR(args);
|
|
2805
|
+
case "read_pr_comments":
|
|
2806
|
+
return await readPRComments(args);
|
|
2807
|
+
case "format_file":
|
|
2808
|
+
return await runFormat(args);
|
|
2809
|
+
case "reply_pr_comment":
|
|
2810
|
+
return await replyPRComment(args);
|
|
2811
|
+
case "agent_self_eval":
|
|
2812
|
+
return await agentSelfEval(args, ctx.settings);
|
|
2813
|
+
case "git_diff_summary":
|
|
2814
|
+
return await gitDiffSummary(args);
|
|
2815
|
+
case "read_pdf":
|
|
2816
|
+
return await readPDF(args, ctx.settings);
|
|
2817
|
+
case "run_python_repl":
|
|
2818
|
+
return await runPythonREPL(args, ctx.settings);
|
|
2819
|
+
case "mount_external_folder":
|
|
2820
|
+
return await mountExternalFolder(
|
|
2821
|
+
args,
|
|
2822
|
+
{ mountedFolders: ctx.mountedFolders },
|
|
2823
|
+
{ autoApprove: ctx.settings.autoApproveCommands }
|
|
2824
|
+
);
|
|
2825
|
+
case "download_dataset":
|
|
2826
|
+
return await downloadDataset(args, ctx.settings);
|
|
2827
|
+
// ── Unknown ───────────────────────────────────────────────
|
|
2828
|
+
default:
|
|
2829
|
+
return {
|
|
2830
|
+
error: "unknown_tool",
|
|
2831
|
+
name,
|
|
2832
|
+
note: "Server schema and client dispatcher are out of sync. Update src/executors/index.ts."
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
} catch (err) {
|
|
2836
|
+
return {
|
|
2837
|
+
error: err?.code || err?.name || "ExecutorError",
|
|
2838
|
+
message: err?.message || String(err),
|
|
2839
|
+
tool: name
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/gitCheckpoint.ts
|
|
2845
|
+
var import_node_child_process10 = require("node:child_process");
|
|
2846
|
+
var GitCheckpoint = class {
|
|
2847
|
+
constructor(workspaceRoot3) {
|
|
2848
|
+
this.workspaceRoot = workspaceRoot3;
|
|
2849
|
+
}
|
|
2850
|
+
status = { active: false };
|
|
2851
|
+
/** Paths the agent wrote/edited this session, normalized to workspace-
|
|
2852
|
+
* relative forward-slash form. Used as the commit add whitelist. */
|
|
2853
|
+
agentPaths = /* @__PURE__ */ new Set();
|
|
2854
|
+
/** Snapshot of `git status --porcelain` at init. Files here that the
|
|
2855
|
+
* agent does NOT touch are reported as "preserved" at session end. */
|
|
2856
|
+
preExistingDirty = [];
|
|
2857
|
+
/** Probe + initialize. Call once per session, before the first tool call. */
|
|
2858
|
+
async init() {
|
|
2859
|
+
if (!await this.isGitRepo()) {
|
|
2860
|
+
this.status = { active: false, reason: "not a git repository" };
|
|
2861
|
+
return this.status;
|
|
2862
|
+
}
|
|
2863
|
+
const stat5 = await this.run(["status", "--porcelain", "-z"]);
|
|
2864
|
+
if (stat5.success && stat5.stdout) {
|
|
2865
|
+
this.preExistingDirty = stat5.stdout.split("\0").filter((x) => x.length > 3).map((x) => x.slice(3));
|
|
2866
|
+
}
|
|
2867
|
+
const headRes = await this.run(["rev-parse", "HEAD"]);
|
|
2868
|
+
const startSha = headRes.stdout.trim();
|
|
2869
|
+
if (!startSha) {
|
|
2870
|
+
this.status = { active: false, reason: "git rev-parse HEAD failed (empty repo?)" };
|
|
2871
|
+
return this.status;
|
|
2872
|
+
}
|
|
2873
|
+
this.status = { active: true, startedAtSha: startSha };
|
|
2874
|
+
return this.status;
|
|
2875
|
+
}
|
|
2876
|
+
/** Tool-call loop reports each successful write/edit here. Paths are
|
|
2877
|
+
* normalized to workspace-relative + forward slashes. Safe to call with
|
|
2878
|
+
* absolute paths, ./-prefixed paths, or backslashes. */
|
|
2879
|
+
recordWrite(p) {
|
|
2880
|
+
if (!this.status.active || !p || typeof p !== "string") return;
|
|
2881
|
+
let rel = p.replace(/\\/g, "/");
|
|
2882
|
+
const ws = this.workspaceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2883
|
+
if (rel.startsWith(ws + "/")) rel = rel.slice(ws.length + 1);
|
|
2884
|
+
rel = rel.replace(/^\.\/+/, "");
|
|
2885
|
+
if (rel) this.agentPaths.add(rel);
|
|
2886
|
+
}
|
|
2887
|
+
/** Read-only view of recorded agent paths. */
|
|
2888
|
+
getAgentPaths() {
|
|
2889
|
+
return [...this.agentPaths];
|
|
2890
|
+
}
|
|
2891
|
+
/** Read-only view of files the user had dirty/untracked at init. */
|
|
2892
|
+
getPreExistingDirty() {
|
|
2893
|
+
return [...this.preExistingDirty];
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Commit ONLY the paths the agent recorded.
|
|
2897
|
+
* Never uses `git add -A` — user-owned dirty/untracked files are left
|
|
2898
|
+
* exactly where they are.
|
|
2899
|
+
*/
|
|
2900
|
+
async commitOnDone(summary) {
|
|
2901
|
+
if (!this.status.active) {
|
|
2902
|
+
return { committed: false, reason: "checkpoint not active" };
|
|
2903
|
+
}
|
|
2904
|
+
const paths = [...this.agentPaths];
|
|
2905
|
+
if (paths.length === 0) {
|
|
2906
|
+
return { committed: false, reason: "no agent writes recorded" };
|
|
2907
|
+
}
|
|
2908
|
+
let unstagedConflicts;
|
|
2909
|
+
const unstagedRes = await this.run(["diff", "--name-only", "--", ...paths]);
|
|
2910
|
+
if (unstagedRes.success) {
|
|
2911
|
+
const names = unstagedRes.stdout.trim().split("\n").filter(Boolean);
|
|
2912
|
+
if (names.length > 0) unstagedConflicts = names;
|
|
2913
|
+
}
|
|
2914
|
+
const addRes = await this.run(["add", "--", ...paths]);
|
|
2915
|
+
if (!addRes.success) {
|
|
2916
|
+
return { committed: false, reason: `git add failed: ${addRes.stderr.trim()}` };
|
|
2917
|
+
}
|
|
2918
|
+
const diff = await this.run(["diff", "--cached", "--name-only"]);
|
|
2919
|
+
if (diff.success && !diff.stdout.trim()) {
|
|
2920
|
+
return { committed: false, reason: "agent writes produced no net change" };
|
|
2921
|
+
}
|
|
2922
|
+
const subject = subjectFromSummary(summary);
|
|
2923
|
+
const body = summary.length > subject.length ? `
|
|
2924
|
+
|
|
2925
|
+
${summary}` : "";
|
|
2926
|
+
const commitRes = await this.run([
|
|
2927
|
+
"-c",
|
|
2928
|
+
"user.name=Rscot Agent",
|
|
2929
|
+
"-c",
|
|
2930
|
+
"user.email=agent@code-workspace.local",
|
|
2931
|
+
"commit",
|
|
2932
|
+
"-m",
|
|
2933
|
+
`[rscot] ${subject}${body}`
|
|
2934
|
+
]);
|
|
2935
|
+
if (!commitRes.success) {
|
|
2936
|
+
return { committed: false, reason: `git commit failed: ${commitRes.stderr.trim()}` };
|
|
2937
|
+
}
|
|
2938
|
+
const sha = (await this.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
2939
|
+
const addedPaths = diff.success ? diff.stdout.trim().split("\n").filter(Boolean) : paths;
|
|
2940
|
+
const agentSet = new Set(paths);
|
|
2941
|
+
const preserved = this.preExistingDirty.filter((p) => !agentSet.has(p));
|
|
2942
|
+
return { committed: true, sha, addedPaths, preserved, unstagedConflicts };
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Hard-reset to session start. Destroys agent commit(s) from this session.
|
|
2946
|
+
* Does NOT touch user's working-tree files — they were never moved.
|
|
2947
|
+
*/
|
|
2948
|
+
async revertOnFailure() {
|
|
2949
|
+
if (!this.status.active || !this.status.startedAtSha) {
|
|
2950
|
+
return { reverted: false, reason: "checkpoint not active" };
|
|
2951
|
+
}
|
|
2952
|
+
const reset = await this.run(["reset", "--hard", this.status.startedAtSha]);
|
|
2953
|
+
if (!reset.success) {
|
|
2954
|
+
return { reverted: false, reason: `git reset failed: ${reset.stderr.trim()}` };
|
|
2955
|
+
}
|
|
2956
|
+
return { reverted: true };
|
|
2957
|
+
}
|
|
2958
|
+
getStatus() {
|
|
2959
|
+
return { ...this.status };
|
|
2960
|
+
}
|
|
2961
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
2962
|
+
async isGitRepo() {
|
|
2963
|
+
const r = await this.run(["rev-parse", "--is-inside-work-tree"]);
|
|
2964
|
+
return r.success && r.stdout.trim() === "true";
|
|
2965
|
+
}
|
|
2966
|
+
run(args) {
|
|
2967
|
+
return new Promise((resolve3) => {
|
|
2968
|
+
let out = "", err = "";
|
|
2969
|
+
let proc;
|
|
2970
|
+
try {
|
|
2971
|
+
proc = (0, import_node_child_process10.spawn)("git", args, {
|
|
2972
|
+
cwd: this.workspaceRoot,
|
|
2973
|
+
windowsHide: true,
|
|
2974
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2975
|
+
});
|
|
2976
|
+
} catch (e) {
|
|
2977
|
+
return resolve3({ success: false, stdout: "", stderr: e?.message || "spawn failed", exitCode: -1 });
|
|
2978
|
+
}
|
|
2979
|
+
proc.stdout?.setEncoding("utf-8");
|
|
2980
|
+
proc.stderr?.setEncoding("utf-8");
|
|
2981
|
+
proc.stdout?.on("data", (c) => {
|
|
2982
|
+
out += c;
|
|
2983
|
+
});
|
|
2984
|
+
proc.stderr?.on("data", (c) => {
|
|
2985
|
+
err += c;
|
|
2986
|
+
});
|
|
2987
|
+
proc.on("error", () => resolve3({ success: false, stdout: out, stderr: err || "spawn error", exitCode: -1 }));
|
|
2988
|
+
proc.on("exit", (code) => resolve3({ success: code === 0, stdout: out, stderr: err, exitCode: code ?? -1 }));
|
|
2989
|
+
setTimeout(() => {
|
|
2990
|
+
try {
|
|
2991
|
+
proc.kill();
|
|
2992
|
+
} catch {
|
|
2993
|
+
}
|
|
2994
|
+
}, 15e3);
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
};
|
|
2998
|
+
function subjectFromSummary(summary) {
|
|
2999
|
+
const first = (summary.split(/\r?\n/)[0] || "").trim() || "agent edits";
|
|
3000
|
+
return first.length > 72 ? first.slice(0, 69) + "..." : first;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// src/rollback.ts
|
|
3004
|
+
var fs9 = __toESM(require("node:fs/promises"));
|
|
3005
|
+
var path7 = __toESM(require("node:path"));
|
|
3006
|
+
function isScratchPath(absOrRel) {
|
|
3007
|
+
const base = path7.basename(absOrRel);
|
|
3008
|
+
return base.startsWith("_") && !base.startsWith("__");
|
|
3009
|
+
}
|
|
3010
|
+
var RollbackMap = class {
|
|
3011
|
+
entries = [];
|
|
3012
|
+
counter = 0;
|
|
3013
|
+
/** Per-path inflight guard — refuses concurrent snapshot() for the same
|
|
3014
|
+
* absolute path so an apply-diff racing a rollback can't push a corrupt
|
|
3015
|
+
* duplicate entry into the map. */
|
|
3016
|
+
inflightPaths = /* @__PURE__ */ new Set();
|
|
3017
|
+
/** True while restoreAll() is draining entries. snapshot()/clear() are
|
|
3018
|
+
* rejected during this window to keep the reverse-order invariant. */
|
|
3019
|
+
restoreInFlight = false;
|
|
3020
|
+
/** Snapshot a file's current state, return a rollback token. */
|
|
3021
|
+
async snapshot(absPath) {
|
|
3022
|
+
if (this.restoreInFlight) {
|
|
3023
|
+
throw new Error(`rollback in progress \u2014 refusing snapshot of ${absPath}`);
|
|
3024
|
+
}
|
|
3025
|
+
if (this.inflightPaths.has(absPath)) {
|
|
3026
|
+
throw new Error(`snapshot already in flight for ${absPath}`);
|
|
3027
|
+
}
|
|
3028
|
+
this.inflightPaths.add(absPath);
|
|
3029
|
+
try {
|
|
3030
|
+
const token = `rb_${Date.now().toString(36)}_${this.counter++}`;
|
|
3031
|
+
const scratch = isScratchPath(absPath);
|
|
3032
|
+
let prev = null;
|
|
3033
|
+
if (!scratch) {
|
|
3034
|
+
try {
|
|
3035
|
+
prev = await fs9.readFile(absPath, "utf-8");
|
|
3036
|
+
} catch (e) {
|
|
3037
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
this.entries.push({
|
|
3041
|
+
token,
|
|
3042
|
+
path: absPath,
|
|
3043
|
+
previousContent: prev,
|
|
3044
|
+
timestamp: Date.now(),
|
|
3045
|
+
scratch
|
|
3046
|
+
});
|
|
3047
|
+
return token;
|
|
3048
|
+
} finally {
|
|
3049
|
+
this.inflightPaths.delete(absPath);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
/** Restore every snapshot in reverse order. Returns count restored.
|
|
3053
|
+
* Scratch entries (underscore-prefix files, P5) are skipped — leaving
|
|
3054
|
+
* them around isn't useful and rolling them back wastes IO. */
|
|
3055
|
+
async restoreAll() {
|
|
3056
|
+
if (this.restoreInFlight) {
|
|
3057
|
+
return { restored: 0, skipped_scratch: 0, errors: ["restoreAll already in progress"] };
|
|
3058
|
+
}
|
|
3059
|
+
this.restoreInFlight = true;
|
|
3060
|
+
try {
|
|
3061
|
+
const errors = [];
|
|
3062
|
+
let restored = 0;
|
|
3063
|
+
let skipped_scratch = 0;
|
|
3064
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
3065
|
+
const e = this.entries[i];
|
|
3066
|
+
if (e.scratch) {
|
|
3067
|
+
skipped_scratch++;
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
try {
|
|
3071
|
+
if (e.previousContent === null) {
|
|
3072
|
+
await fs9.unlink(e.path).catch(() => {
|
|
3073
|
+
});
|
|
3074
|
+
} else {
|
|
3075
|
+
await fs9.writeFile(e.path, e.previousContent, "utf-8");
|
|
3076
|
+
}
|
|
3077
|
+
restored++;
|
|
3078
|
+
} catch (err) {
|
|
3079
|
+
errors.push(`${e.path}: ${err?.message || String(err)}`);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
this.entries = [];
|
|
3083
|
+
return { restored, skipped_scratch, errors };
|
|
3084
|
+
} finally {
|
|
3085
|
+
this.restoreInFlight = false;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
list() {
|
|
3089
|
+
return [...this.entries];
|
|
3090
|
+
}
|
|
3091
|
+
/** Drop the map without restoring (used after a successful `done` commit). */
|
|
3092
|
+
clear() {
|
|
3093
|
+
if (this.restoreInFlight) {
|
|
3094
|
+
throw new Error("clear() refused: restoreAll() in progress");
|
|
3095
|
+
}
|
|
3096
|
+
this.entries = [];
|
|
3097
|
+
}
|
|
3098
|
+
get size() {
|
|
3099
|
+
return this.entries.length;
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
|
|
3103
|
+
// src/cli.ts
|
|
3104
|
+
var gNoColor = false;
|
|
3105
|
+
var gQuiet = false;
|
|
3106
|
+
var gJson = false;
|
|
3107
|
+
function parseArgs(argv) {
|
|
3108
|
+
const out = {
|
|
3109
|
+
prompt: "",
|
|
3110
|
+
workspace: process.cwd(),
|
|
3111
|
+
backendUrl: process.env.CWA_BACKEND_URL || "http://localhost:8010",
|
|
3112
|
+
authBearer: (() => {
|
|
3113
|
+
if (process.env.CWA_AUTH_BEARER) return process.env.CWA_AUTH_BEARER;
|
|
3114
|
+
try {
|
|
3115
|
+
return crypto2.randomBytes(8).toString("hex");
|
|
3116
|
+
} catch {
|
|
3117
|
+
return "cli";
|
|
3118
|
+
}
|
|
3119
|
+
})(),
|
|
3120
|
+
apiKey: process.env.CWA_API_KEY || "",
|
|
3121
|
+
provider: process.env.CWA_PROVIDER || "deepseek",
|
|
3122
|
+
model: process.env.CWA_MODEL || "",
|
|
3123
|
+
baseUrl: process.env.CWA_BASE_URL || "",
|
|
3124
|
+
stackHint: process.env.CWA_STACK_HINT || "",
|
|
3125
|
+
autoApprove: true,
|
|
3126
|
+
// CLI is interactive-less; default approve
|
|
3127
|
+
maxIters: 40,
|
|
3128
|
+
temperature: 0.2,
|
|
3129
|
+
gitCheckpoint: true,
|
|
3130
|
+
// session-start stash + on-done commit if git repo
|
|
3131
|
+
persist: true,
|
|
3132
|
+
// save to .rscot/conversations/
|
|
3133
|
+
resume: false,
|
|
3134
|
+
// pick up the last saved conversation
|
|
3135
|
+
resumePrefix: "",
|
|
3136
|
+
stream: false,
|
|
3137
|
+
// Day 21 streaming — opt-in via --stream
|
|
3138
|
+
// Wave 5 defaults
|
|
3139
|
+
json: false,
|
|
3140
|
+
quiet: false,
|
|
3141
|
+
noColor: false,
|
|
3142
|
+
dryRun: false,
|
|
3143
|
+
turnTimeout: 120
|
|
3144
|
+
};
|
|
3145
|
+
const rest = [];
|
|
3146
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3147
|
+
const a = argv[i];
|
|
3148
|
+
const next = () => {
|
|
3149
|
+
i++;
|
|
3150
|
+
return argv[i];
|
|
3151
|
+
};
|
|
3152
|
+
switch (a) {
|
|
3153
|
+
case "--workspace":
|
|
3154
|
+
out.workspace = next();
|
|
3155
|
+
break;
|
|
3156
|
+
case "--backend-url":
|
|
3157
|
+
out.backendUrl = next();
|
|
3158
|
+
break;
|
|
3159
|
+
case "--auth-bearer":
|
|
3160
|
+
out.authBearer = next();
|
|
3161
|
+
break;
|
|
3162
|
+
case "--api-key":
|
|
3163
|
+
out.apiKey = next();
|
|
3164
|
+
break;
|
|
3165
|
+
case "--provider":
|
|
3166
|
+
out.provider = next();
|
|
3167
|
+
break;
|
|
3168
|
+
case "--model":
|
|
3169
|
+
out.model = next();
|
|
3170
|
+
break;
|
|
3171
|
+
case "--base-url":
|
|
3172
|
+
out.baseUrl = next();
|
|
3173
|
+
break;
|
|
3174
|
+
case "--stack-hint":
|
|
3175
|
+
out.stackHint = next();
|
|
3176
|
+
break;
|
|
3177
|
+
case "--no-approve":
|
|
3178
|
+
out.autoApprove = false;
|
|
3179
|
+
break;
|
|
3180
|
+
case "--auto-approve":
|
|
3181
|
+
out.autoApprove = true;
|
|
3182
|
+
break;
|
|
3183
|
+
case "--no-git":
|
|
3184
|
+
out.gitCheckpoint = false;
|
|
3185
|
+
break;
|
|
3186
|
+
case "--no-persist":
|
|
3187
|
+
out.persist = false;
|
|
3188
|
+
break;
|
|
3189
|
+
case "--stream":
|
|
3190
|
+
out.stream = true;
|
|
3191
|
+
break;
|
|
3192
|
+
case "--max-iters":
|
|
3193
|
+
out.maxIters = parseInt(next(), 10);
|
|
3194
|
+
break;
|
|
3195
|
+
case "--temperature":
|
|
3196
|
+
out.temperature = parseFloat(next());
|
|
3197
|
+
break;
|
|
3198
|
+
// ── Wave 5 ──────────────────────────────────────────
|
|
3199
|
+
case "--json":
|
|
3200
|
+
out.json = true;
|
|
3201
|
+
break;
|
|
3202
|
+
case "--quiet":
|
|
3203
|
+
out.quiet = true;
|
|
3204
|
+
break;
|
|
3205
|
+
case "--no-color":
|
|
3206
|
+
out.noColor = true;
|
|
3207
|
+
break;
|
|
3208
|
+
case "--dry-run":
|
|
3209
|
+
out.dryRun = true;
|
|
3210
|
+
break;
|
|
3211
|
+
case "--turn-timeout":
|
|
3212
|
+
out.turnTimeout = parseInt(next(), 10);
|
|
3213
|
+
if (out.turnTimeout < 1) {
|
|
3214
|
+
console.error("--turn-timeout must be >= 1");
|
|
3215
|
+
process.exit(2);
|
|
3216
|
+
}
|
|
3217
|
+
break;
|
|
3218
|
+
case "--resume": {
|
|
3219
|
+
out.resume = true;
|
|
3220
|
+
const peek = argv[i + 1];
|
|
3221
|
+
if (peek !== void 0 && !peek.startsWith("--")) {
|
|
3222
|
+
out.resumePrefix = next();
|
|
3223
|
+
}
|
|
3224
|
+
break;
|
|
3225
|
+
}
|
|
3226
|
+
// ── /Wave 5 ─────────────────────────────────────────
|
|
3227
|
+
case "-h":
|
|
3228
|
+
case "--help":
|
|
3229
|
+
printUsage();
|
|
3230
|
+
process.exit(0);
|
|
3231
|
+
break;
|
|
3232
|
+
default:
|
|
3233
|
+
if (a.startsWith("--")) {
|
|
3234
|
+
console.error(`unknown flag: ${a}`);
|
|
3235
|
+
printUsage();
|
|
3236
|
+
process.exit(2);
|
|
3237
|
+
}
|
|
3238
|
+
rest.push(a);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
if (rest.length > 0 && ["list", "delete", "show"].includes(rest[0])) {
|
|
3242
|
+
out.prompt = rest.join(" ").trim();
|
|
3243
|
+
return out;
|
|
3244
|
+
}
|
|
3245
|
+
out.prompt = rest.join(" ").trim();
|
|
3246
|
+
if (!out.prompt) {
|
|
3247
|
+
console.error("missing prompt");
|
|
3248
|
+
printUsage();
|
|
3249
|
+
process.exit(2);
|
|
3250
|
+
}
|
|
3251
|
+
return out;
|
|
3252
|
+
}
|
|
3253
|
+
function printUsage() {
|
|
3254
|
+
console.error(`Usage: node dist/cli.js [flags] "<prompt>"
|
|
3255
|
+
node dist/cli.js list
|
|
3256
|
+
node dist/cli.js delete <id>
|
|
3257
|
+
node dist/cli.js show <id>
|
|
3258
|
+
|
|
3259
|
+
Flags:
|
|
3260
|
+
--workspace <dir> workspace root (default: cwd)
|
|
3261
|
+
--backend-url <url> backend (default: http://localhost:8010 or $CWA_BACKEND_URL)
|
|
3262
|
+
--auth-bearer <t> LAN-gate token (default: "cli" or $CWA_AUTH_BEARER)
|
|
3263
|
+
--api-key <k> LLM API key (default: empty \u2192 backend uses its env)
|
|
3264
|
+
--provider <id> LLM provider: deepseek | openai | anthropic | ollama (default: deepseek or $CWA_PROVIDER)
|
|
3265
|
+
--model <name> Model name (default: backend picks per provider or $CWA_MODEL)
|
|
3266
|
+
--base-url <url> Override provider base URL (default: backend's known URL or $CWA_BASE_URL)
|
|
3267
|
+
--stack-hint <s> knowledge layer stack (fastapi/nextjs/flutter/pytorch/rust_cli)
|
|
3268
|
+
--no-approve require y/N before writes + run_command (default: auto-approve)
|
|
3269
|
+
--no-git disable git checkpoint (default: enabled if workspace is a git repo)
|
|
3270
|
+
--no-persist don't save conversation to .rscot/conversations/
|
|
3271
|
+
--resume [<prefix>] resume last session; with 8+ hex chars, resume by partial id
|
|
3272
|
+
--stream use SSE streaming endpoint \u2014 assistant text streams live
|
|
3273
|
+
--max-iters <n> loop iteration cap (default: 40)
|
|
3274
|
+
--temperature <f> LLM temperature (default: 0.2)
|
|
3275
|
+
--json suppress all colored output; emit JSON result at end
|
|
3276
|
+
--quiet suppress tool-result echoes, keep LLM messages + summary
|
|
3277
|
+
--no-color disable ANSI color output (overrides TTY auto-detect)
|
|
3278
|
+
--dry-run print tool calls without executing them, then continue
|
|
3279
|
+
--turn-timeout <s> per-turn timeout in seconds (default: 120)`);
|
|
3280
|
+
}
|
|
3281
|
+
function color(code, text) {
|
|
3282
|
+
if (gNoColor || !process.stdout.isTTY) return text;
|
|
3283
|
+
return `\x1B[${code}m${text}\x1B[0m`;
|
|
3284
|
+
}
|
|
3285
|
+
var cDim = (s) => color("2", s);
|
|
3286
|
+
var cBold = (s) => color("1", s);
|
|
3287
|
+
var cGreen = (s) => color("32", s);
|
|
3288
|
+
var cYellow = (s) => color("33", s);
|
|
3289
|
+
var cRed = (s) => color("31", s);
|
|
3290
|
+
var cCyan = (s) => color("36", s);
|
|
3291
|
+
var cMag = (s) => color("35", s);
|
|
3292
|
+
function osc8(url, display) {
|
|
3293
|
+
if (gNoColor || !process.stdout.isTTY) return display;
|
|
3294
|
+
return `\x1B]8;;${url}\x07${display}\x1B]8;;\x07`;
|
|
3295
|
+
}
|
|
3296
|
+
function fileLink(workspace, p, display) {
|
|
3297
|
+
const abs = path8.isAbsolute(p) ? p : path8.join(workspace, p);
|
|
3298
|
+
let urlPath = abs.replace(/\\/g, "/");
|
|
3299
|
+
if (/^[A-Za-z]:\//.test(urlPath)) urlPath = "/" + urlPath;
|
|
3300
|
+
const url = "file://" + urlPath.split("/").map(encodeURIComponent).join("/").replace(/%2F/g, "/");
|
|
3301
|
+
return osc8(url, display || p);
|
|
3302
|
+
}
|
|
3303
|
+
function logInfo(...args) {
|
|
3304
|
+
if (gQuiet || gJson) return;
|
|
3305
|
+
console.log(...args);
|
|
3306
|
+
}
|
|
3307
|
+
function logTool(...args) {
|
|
3308
|
+
if (gJson) return;
|
|
3309
|
+
console.log(...args);
|
|
3310
|
+
}
|
|
3311
|
+
function log(...args) {
|
|
3312
|
+
if (gJson) return;
|
|
3313
|
+
console.log(...args);
|
|
3314
|
+
}
|
|
3315
|
+
function emitJsonResult(payload) {
|
|
3316
|
+
const obj = {
|
|
3317
|
+
session_id: payload.session_id ?? null,
|
|
3318
|
+
files_changed: payload.files_changed ?? [],
|
|
3319
|
+
summary: payload.summary ?? "",
|
|
3320
|
+
exit_code: payload.exit_code ?? 0
|
|
3321
|
+
};
|
|
3322
|
+
console.log(JSON.stringify(obj));
|
|
3323
|
+
}
|
|
3324
|
+
async function cmdList(args) {
|
|
3325
|
+
const convStore = new ConversationStore(args.workspace);
|
|
3326
|
+
const list = await convStore.listAll();
|
|
3327
|
+
if (list.length === 0) {
|
|
3328
|
+
logInfo(cDim("No conversations found."));
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
logInfo(cBold(`${list.length} conversation(s):`));
|
|
3332
|
+
for (const s of list) {
|
|
3333
|
+
const status = s.completed ? cGreen("\u2713") : cYellow("\u2026");
|
|
3334
|
+
logInfo(` ${status} ${s.session_id} ${cDim(s.created_at.slice(0, 19))} ${s.summary} ${cDim(`(${s.messages_count} msgs)`)}`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
async function cmdDelete(args) {
|
|
3338
|
+
const parts = args.prompt.split(/\s+/);
|
|
3339
|
+
if (parts.length < 2) {
|
|
3340
|
+
console.error("Usage: node dist/cli.js delete <id>");
|
|
3341
|
+
process.exit(2);
|
|
3342
|
+
}
|
|
3343
|
+
const prefix = parts[1];
|
|
3344
|
+
const convDir = path8.join(args.workspace, ".rscot", "conversations");
|
|
3345
|
+
const found = await findConversationByPrefix(convDir, prefix);
|
|
3346
|
+
if (found.length === 0) {
|
|
3347
|
+
console.error(`No conversation matching id prefix "${prefix}"`);
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
}
|
|
3350
|
+
if (found.length > 1) {
|
|
3351
|
+
console.error(`Multiple conversations match "${prefix}": ${found.map((f) => f.id).join(", ")}. Provide more chars.`);
|
|
3352
|
+
process.exit(1);
|
|
3353
|
+
}
|
|
3354
|
+
const { filename, id } = found[0];
|
|
3355
|
+
logInfo(`Conversation to delete: ${id}`);
|
|
3356
|
+
process.stdout.write("Are you sure? [y/N] ");
|
|
3357
|
+
const confirmed = await new Promise((resolve3) => {
|
|
3358
|
+
process.stdin.setEncoding("utf-8");
|
|
3359
|
+
const onData = (chunk) => {
|
|
3360
|
+
process.stdin.removeListener("data", onData);
|
|
3361
|
+
process.stdin.pause();
|
|
3362
|
+
resolve3(chunk.trim().toLowerCase().startsWith("y"));
|
|
3363
|
+
};
|
|
3364
|
+
process.stdin.resume();
|
|
3365
|
+
process.stdin.on("data", onData);
|
|
3366
|
+
});
|
|
3367
|
+
if (!confirmed) {
|
|
3368
|
+
logInfo(cDim("Delete cancelled."));
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
await fs10.unlink(path8.join(convDir, filename));
|
|
3372
|
+
logInfo(cDim(`Deleted ${id}`));
|
|
3373
|
+
}
|
|
3374
|
+
async function cmdShow(args) {
|
|
3375
|
+
const parts = args.prompt.split(/\s+/);
|
|
3376
|
+
if (parts.length < 2) {
|
|
3377
|
+
console.error("Usage: node dist/cli.js show <id>");
|
|
3378
|
+
process.exit(2);
|
|
3379
|
+
}
|
|
3380
|
+
const prefix = parts[1];
|
|
3381
|
+
const convDir = path8.join(args.workspace, ".rscot", "conversations");
|
|
3382
|
+
const found = await findConversationByPrefix(convDir, prefix);
|
|
3383
|
+
if (found.length === 0) {
|
|
3384
|
+
console.error(`No conversation matching id prefix "${prefix}"`);
|
|
3385
|
+
process.exit(1);
|
|
3386
|
+
}
|
|
3387
|
+
if (found.length > 1) {
|
|
3388
|
+
console.error(`Multiple conversations match "${prefix}": ${found.map((f) => f.id).join(", ")}. Provide more chars.`);
|
|
3389
|
+
process.exit(1);
|
|
3390
|
+
}
|
|
3391
|
+
const raw = await fs10.readFile(path8.join(convDir, found[0].filename), "utf-8");
|
|
3392
|
+
const session = JSON.parse(raw);
|
|
3393
|
+
logInfo(cBold(`Session: ${session.session_id}`));
|
|
3394
|
+
logInfo(cDim(`Created: ${session.created_at} | ${session.messages.length} message(s) | ${session.completed ? "completed" : "incomplete"}`));
|
|
3395
|
+
logInfo(cDim(`Summary: ${session.summary}`));
|
|
3396
|
+
logInfo("");
|
|
3397
|
+
for (const msg of session.messages) {
|
|
3398
|
+
const role = msg.role.padEnd(12);
|
|
3399
|
+
if (msg.role === "user") {
|
|
3400
|
+
logInfo(cCyan(`user > ${(msg.content || "").slice(0, 300)}`));
|
|
3401
|
+
} else if (msg.role === "assistant") {
|
|
3402
|
+
const tc = msg.tool_calls;
|
|
3403
|
+
if (tc && tc.length > 0) {
|
|
3404
|
+
logInfo(cGreen(`assistant > tool_call: ${tc.map((t) => t.function?.name || "?").join(", ")}`));
|
|
3405
|
+
} else {
|
|
3406
|
+
logInfo(cGreen(`assistant > ${(msg.content || "").slice(0, 300)}`));
|
|
3407
|
+
}
|
|
3408
|
+
} else if (msg.role === "tool") {
|
|
3409
|
+
logInfo(cDim(`tool > ${(msg.name || msg.tool_call_id || "?").padEnd(12)} ${(msg.content || "").slice(0, 120)}`));
|
|
3410
|
+
} else {
|
|
3411
|
+
logInfo(cDim(`${role} ${(msg.content || "").slice(0, 120)}`));
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
async function findConversationByPrefix(convDir, prefix) {
|
|
3416
|
+
let files;
|
|
3417
|
+
try {
|
|
3418
|
+
files = await fs10.readdir(convDir);
|
|
3419
|
+
} catch {
|
|
3420
|
+
return [];
|
|
3421
|
+
}
|
|
3422
|
+
const results = [];
|
|
3423
|
+
for (const f of files) {
|
|
3424
|
+
if (!f.endsWith(".json")) continue;
|
|
3425
|
+
const lastUnderscore = f.lastIndexOf("_");
|
|
3426
|
+
if (lastUnderscore === -1) continue;
|
|
3427
|
+
const id = f.slice(lastUnderscore + 1, -5);
|
|
3428
|
+
if (id.startsWith(prefix)) {
|
|
3429
|
+
results.push({ filename: f, id });
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
return results;
|
|
3433
|
+
}
|
|
3434
|
+
async function ensureWorkspace(dir) {
|
|
3435
|
+
await fs10.mkdir(dir, { recursive: true });
|
|
3436
|
+
setWorkspaceRoot(dir);
|
|
3437
|
+
}
|
|
3438
|
+
async function main() {
|
|
3439
|
+
const args = parseArgs(process.argv.slice(2));
|
|
3440
|
+
gNoColor = args.noColor;
|
|
3441
|
+
gQuiet = args.quiet;
|
|
3442
|
+
gJson = args.json;
|
|
3443
|
+
const subcmd = args.prompt.split(/\s+/)[0];
|
|
3444
|
+
if (subcmd === "list") {
|
|
3445
|
+
await cmdList(args);
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
if (subcmd === "delete") {
|
|
3449
|
+
await cmdDelete(args);
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
if (subcmd === "show") {
|
|
3453
|
+
await cmdShow(args);
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
await ensureWorkspace(args.workspace);
|
|
3457
|
+
log(cBold("\u2500\u2500\u2500 Rscot Agent (CLI) \u2500\u2500\u2500"));
|
|
3458
|
+
log(`${cDim("workspace:")} ${args.workspace}`);
|
|
3459
|
+
log(`${cDim("backend:")} ${args.backendUrl}`);
|
|
3460
|
+
log(`${cDim("auth:")} ${args.authBearer.slice(0, 8)}... ${process.env.CWA_AUTH_BEARER ? "(env)" : "(random per-process)"}`);
|
|
3461
|
+
log(`${cDim("provider:")} ${args.provider}${args.model ? ` \xB7 ${args.model}` : " \xB7 (default model)"}`);
|
|
3462
|
+
log(`${cDim("stack hint:")} ${args.stackHint || "(none)"}`);
|
|
3463
|
+
log(`${cDim("approve:")} ${args.autoApprove ? "auto" : "prompt"}`);
|
|
3464
|
+
if (args.dryRun) log(`${cYellow("dry-run:")} enabled`);
|
|
3465
|
+
log(`${cDim("prompt:")} ${args.prompt}`);
|
|
3466
|
+
log();
|
|
3467
|
+
const settings = {
|
|
3468
|
+
backendUrl: args.backendUrl,
|
|
3469
|
+
authBearer: args.authBearer,
|
|
3470
|
+
provider: args.provider,
|
|
3471
|
+
model: args.model,
|
|
3472
|
+
baseUrl: args.baseUrl,
|
|
3473
|
+
stackHint: args.stackHint,
|
|
3474
|
+
autoApproveWrites: args.autoApprove,
|
|
3475
|
+
autoApproveCommands: args.autoApprove,
|
|
3476
|
+
temperature: args.temperature,
|
|
3477
|
+
accentColor: "#d97757"
|
|
3478
|
+
// CLI has no UI — kept for type completeness
|
|
3479
|
+
};
|
|
3480
|
+
const client = new ApiClient(settings, args.apiKey);
|
|
3481
|
+
const registry = new RollbackMap();
|
|
3482
|
+
const procRegistry = new ProcessRegistry();
|
|
3483
|
+
const cliMountedFolders = /* @__PURE__ */ new Set();
|
|
3484
|
+
const checkpoint = new GitCheckpoint(args.workspace);
|
|
3485
|
+
if (args.gitCheckpoint) {
|
|
3486
|
+
const status = await checkpoint.init();
|
|
3487
|
+
if (status.active) {
|
|
3488
|
+
const dirty = checkpoint.getPreExistingDirty();
|
|
3489
|
+
log(cDim(`git checkpoint: active (start ${status.startedAtSha?.slice(0, 7)}, ${dirty.length} pre-existing dirty/untracked file${dirty.length === 1 ? "" : "s"} preserved)`));
|
|
3490
|
+
} else {
|
|
3491
|
+
log(cDim(`git checkpoint: off (${status.reason || "not active"})`));
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const convStore = new ConversationStore(args.workspace);
|
|
3495
|
+
let history = [];
|
|
3496
|
+
let resumedFrom = null;
|
|
3497
|
+
if (args.resume) {
|
|
3498
|
+
let session = null;
|
|
3499
|
+
if (args.resumePrefix) {
|
|
3500
|
+
if (args.resumePrefix.length < 8) {
|
|
3501
|
+
console.error(`--resume prefix must be 8+ hex chars (got "${args.resumePrefix}")`);
|
|
3502
|
+
process.exit(2);
|
|
3503
|
+
}
|
|
3504
|
+
const convDir = path8.join(args.workspace, ".rscot", "conversations");
|
|
3505
|
+
const found = await findConversationByPrefix(convDir, args.resumePrefix);
|
|
3506
|
+
if (found.length === 0) {
|
|
3507
|
+
console.error(`No conversation matching prefix "${args.resumePrefix}"`);
|
|
3508
|
+
process.exit(1);
|
|
3509
|
+
}
|
|
3510
|
+
if (found.length > 1) {
|
|
3511
|
+
console.error(
|
|
3512
|
+
`Multiple conversations match "${args.resumePrefix}": ${found.map((f) => f.id).join(", ")}. Provide more chars.`
|
|
3513
|
+
);
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
session = await convStore.loadById(found[0].id);
|
|
3517
|
+
} else {
|
|
3518
|
+
session = await convStore.resumeLatest();
|
|
3519
|
+
}
|
|
3520
|
+
if (session) {
|
|
3521
|
+
history = [...session.messages, { role: "user", content: args.prompt }];
|
|
3522
|
+
resumedFrom = session.session_id;
|
|
3523
|
+
log(cDim(`resuming session ${session.session_id} (${session.messages.length} prior messages)`));
|
|
3524
|
+
} else {
|
|
3525
|
+
log(cDim("--resume requested but no prior session found \u2014 starting fresh"));
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
if (!resumedFrom) {
|
|
3529
|
+
history = [{ role: "user", content: args.prompt }];
|
|
3530
|
+
}
|
|
3531
|
+
if (args.persist) {
|
|
3532
|
+
if (resumedFrom) {
|
|
3533
|
+
await convStore.appendMessage({ role: "user", content: args.prompt });
|
|
3534
|
+
} else {
|
|
3535
|
+
const id = await convStore.startNew({
|
|
3536
|
+
workspaceRoot: args.workspace,
|
|
3537
|
+
stackHint: args.stackHint,
|
|
3538
|
+
firstPrompt: args.prompt
|
|
3539
|
+
});
|
|
3540
|
+
await convStore.appendMessage({ role: "user", content: args.prompt });
|
|
3541
|
+
log(cDim(`session: ${id} (saved to .rscot/conversations/)`));
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const shutdown = async (sig) => {
|
|
3545
|
+
log(cYellow(`
|
|
3546
|
+
[cli] ${sig} \u2014 cleaning up ${procRegistry.size} spawned process(es)\u2026`));
|
|
3547
|
+
await procRegistry.cleanupAll();
|
|
3548
|
+
process.exit(130);
|
|
3549
|
+
};
|
|
3550
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
3551
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
3552
|
+
let workspaceSummary;
|
|
3553
|
+
if (!args.resume) {
|
|
3554
|
+
workspaceSummary = await buildPriorContext(convStore, args.prompt);
|
|
3555
|
+
if (workspaceSummary) {
|
|
3556
|
+
log(cDim(`prior context: ${workspaceSummary.split("\n").length - 1} lines`));
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
let exitCode = 0;
|
|
3560
|
+
let finalSummary = "";
|
|
3561
|
+
let finalFilesChanged = [];
|
|
3562
|
+
try {
|
|
3563
|
+
for (let iter = 0; iter < args.maxIters; iter++) {
|
|
3564
|
+
logInfo(cDim(`[${String(iter + 1).padStart(2)}] `) + cMag("\u2192 LLM "));
|
|
3565
|
+
let resp;
|
|
3566
|
+
try {
|
|
3567
|
+
const turnParams = {
|
|
3568
|
+
messages: history,
|
|
3569
|
+
workspace_root: args.workspace,
|
|
3570
|
+
session_id: resumedFrom || convStore.getCurrentId() || void 0
|
|
3571
|
+
};
|
|
3572
|
+
if (iter === 0 && workspaceSummary) turnParams.workspace_summary = workspaceSummary;
|
|
3573
|
+
const timeoutMs = args.turnTimeout * 1e3;
|
|
3574
|
+
resp = args.stream ? await consumeStream(client, turnParams) : await client.turn(turnParams, { timeoutMs });
|
|
3575
|
+
} catch (err) {
|
|
3576
|
+
logInfo(cRed(`\u2717 ${err?.message || String(err)}`));
|
|
3577
|
+
exitCode = 1;
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
if (resp.type === "message") {
|
|
3581
|
+
logInfo(cGreen("\u2190 message"));
|
|
3582
|
+
log("\n" + cCyan("assistant:") + " " + resp.content + "\n");
|
|
3583
|
+
history.push(resp.assistant_message);
|
|
3584
|
+
if (args.persist) await convStore.appendMessage(resp.assistant_message);
|
|
3585
|
+
const isNudge = /^[⏸⏪]/u.test(resp.content.trim());
|
|
3586
|
+
if (isNudge) {
|
|
3587
|
+
logInfo(cDim(" [server nudge \u2014 auto-continuing]"));
|
|
3588
|
+
const followUp = {
|
|
3589
|
+
role: "user",
|
|
3590
|
+
content: "Understood \u2014 proceeding as instructed."
|
|
3591
|
+
};
|
|
3592
|
+
history.push(followUp);
|
|
3593
|
+
if (args.persist) await convStore.appendMessage(followUp);
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
break;
|
|
3597
|
+
}
|
|
3598
|
+
if (resp.type === "done") {
|
|
3599
|
+
logInfo(cGreen("\u2190 DONE"));
|
|
3600
|
+
finalSummary = resp.summary;
|
|
3601
|
+
finalFilesChanged = resp.files_changed;
|
|
3602
|
+
log("\n" + cBold("Summary: ") + resp.summary);
|
|
3603
|
+
if (resp.files_changed?.length) {
|
|
3604
|
+
log(cBold("Files changed:"));
|
|
3605
|
+
for (const p of resp.files_changed) {
|
|
3606
|
+
log(` \xB7 ${fileLink(args.workspace, p)}`);
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
if (resp.suggested_test_cmd)
|
|
3610
|
+
log(cBold("Suggested test: ") + resp.suggested_test_cmd);
|
|
3611
|
+
history.push(resp.assistant_message);
|
|
3612
|
+
if (args.persist) await convStore.appendMessage(resp.assistant_message);
|
|
3613
|
+
const doneTcId = resp.assistant_message?.tool_calls?.[0]?.id || "call_done";
|
|
3614
|
+
const doneAck = {
|
|
3615
|
+
role: "tool",
|
|
3616
|
+
tool_call_id: doneTcId,
|
|
3617
|
+
name: "done",
|
|
3618
|
+
content: JSON.stringify({
|
|
3619
|
+
acknowledged: true,
|
|
3620
|
+
summary: resp.summary,
|
|
3621
|
+
files_changed: resp.files_changed || []
|
|
3622
|
+
})
|
|
3623
|
+
};
|
|
3624
|
+
history.push(doneAck);
|
|
3625
|
+
if (args.persist) await convStore.appendMessage(doneAck);
|
|
3626
|
+
if (args.persist) await convStore.markComplete(resp.files_changed || []);
|
|
3627
|
+
if (args.gitCheckpoint && checkpoint.getStatus().active) {
|
|
3628
|
+
const r = await checkpoint.commitOnDone(resp.summary);
|
|
3629
|
+
if (r.committed) {
|
|
3630
|
+
log(cDim(`git: committed ${r.sha?.slice(0, 7)} (${r.addedPaths?.length || 0} path${r.addedPaths?.length === 1 ? "" : "s"})`));
|
|
3631
|
+
if (r.preserved && r.preserved.length > 0) {
|
|
3632
|
+
log(cDim(` ${r.preserved.length} user-owned dirty/untracked file${r.preserved.length === 1 ? "" : "s"} preserved (untouched):`));
|
|
3633
|
+
for (const p of r.preserved.slice(0, 8)) log(cDim(` \xB7 ${fileLink(args.workspace, p)}`));
|
|
3634
|
+
if (r.preserved.length > 8) log(cDim(` \u2026 and ${r.preserved.length - 8} more`));
|
|
3635
|
+
}
|
|
3636
|
+
if (r.unstagedConflicts && r.unstagedConflicts.length > 0) {
|
|
3637
|
+
log(cYellow(`\u26A0 ${r.unstagedConflicts.length} file(s) had user-unstaged edits co-committed:`));
|
|
3638
|
+
for (const p of r.unstagedConflicts) log(cYellow(` \xB7 ${fileLink(args.workspace, p)}`));
|
|
3639
|
+
log(cYellow(` If unintended, run: git reset HEAD~1; git restore --staged ${r.unstagedConflicts[0]}`));
|
|
3640
|
+
}
|
|
3641
|
+
log(cDim(` rewind: git reset --hard ${checkpoint.getStatus().startedAtSha?.slice(0, 7)}`));
|
|
3642
|
+
} else {
|
|
3643
|
+
log(cDim(`git: no commit (${r.reason})`));
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
break;
|
|
3647
|
+
}
|
|
3648
|
+
if (resp.type === "blocked") {
|
|
3649
|
+
logInfo(cRed(`\u2190 BLOCKED ${resp.name}`) + cDim(` (${resp.reason})`));
|
|
3650
|
+
history.push(resp.assistant_message);
|
|
3651
|
+
if (args.persist) await convStore.appendMessage(resp.assistant_message);
|
|
3652
|
+
const blockMsg = {
|
|
3653
|
+
role: "tool",
|
|
3654
|
+
tool_call_id: resp.tool_call_id,
|
|
3655
|
+
name: resp.name,
|
|
3656
|
+
content: JSON.stringify({ blocked: true, reason: resp.reason })
|
|
3657
|
+
};
|
|
3658
|
+
history.push(blockMsg);
|
|
3659
|
+
if (args.persist) await convStore.appendMessage(blockMsg);
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
if (resp.type === "tool_call" && args.dryRun) {
|
|
3663
|
+
logInfo(cYellow("\u2190 [DRY]") + cDim(` would call ${cBold(resp.name)} ${JSON.stringify(resp.arguments).slice(0, 200)}`));
|
|
3664
|
+
history.push(resp.assistant_message);
|
|
3665
|
+
if (args.persist) await convStore.appendMessage(resp.assistant_message);
|
|
3666
|
+
const dryResult = {
|
|
3667
|
+
role: "tool",
|
|
3668
|
+
tool_call_id: resp.tool_call_id,
|
|
3669
|
+
name: resp.name,
|
|
3670
|
+
content: JSON.stringify({ dry_run: true, skipped: true })
|
|
3671
|
+
};
|
|
3672
|
+
history.push(dryResult);
|
|
3673
|
+
if (args.persist) await convStore.appendMessage(dryResult);
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
logInfo(cGreen("\u2190 tool_call ") + cBold(resp.name) + cDim(" " + JSON.stringify(resp.arguments).slice(0, 120)));
|
|
3677
|
+
history.push(resp.assistant_message);
|
|
3678
|
+
if (args.persist) await convStore.appendMessage(resp.assistant_message);
|
|
3679
|
+
if (needsApproval(resp.name, settings)) {
|
|
3680
|
+
const ok = await prompt(` approve ${resp.name}? [y/N] `);
|
|
3681
|
+
if (!ok) {
|
|
3682
|
+
logInfo(cYellow(" refused"));
|
|
3683
|
+
const refusedMsg = {
|
|
3684
|
+
role: "tool",
|
|
3685
|
+
tool_call_id: resp.tool_call_id,
|
|
3686
|
+
name: resp.name,
|
|
3687
|
+
content: JSON.stringify({ refused_by_user: true })
|
|
3688
|
+
};
|
|
3689
|
+
history.push(refusedMsg);
|
|
3690
|
+
if (args.persist) await convStore.appendMessage(refusedMsg);
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
const t0 = Date.now();
|
|
3695
|
+
const result = await execute(resp.name, resp.arguments, {
|
|
3696
|
+
rollback: registry,
|
|
3697
|
+
registry: procRegistry,
|
|
3698
|
+
settings,
|
|
3699
|
+
mountedFolders: cliMountedFolders
|
|
3700
|
+
});
|
|
3701
|
+
const dt = Date.now() - t0;
|
|
3702
|
+
const resultStr = JSON.stringify(result);
|
|
3703
|
+
const isErr = typeof result === "object" && result && result.error;
|
|
3704
|
+
logTool(
|
|
3705
|
+
(isErr ? cRed(" [exec err]") : cDim(` [exec ${dt}ms]`)) + " " + cDim(resultStr.slice(0, 160) + (resultStr.length > 160 ? "\u2026" : ""))
|
|
3706
|
+
);
|
|
3707
|
+
if (!isErr && args.gitCheckpoint && checkpoint.getStatus().active) {
|
|
3708
|
+
const writeTools = /* @__PURE__ */ new Set([
|
|
3709
|
+
"write_file",
|
|
3710
|
+
"edit_file",
|
|
3711
|
+
"format_file",
|
|
3712
|
+
"apply_diff",
|
|
3713
|
+
"apply_unified_diff",
|
|
3714
|
+
"delete_file",
|
|
3715
|
+
"move_file",
|
|
3716
|
+
"rename_file"
|
|
3717
|
+
]);
|
|
3718
|
+
if (writeTools.has(resp.name)) {
|
|
3719
|
+
const p = resp.arguments?.path;
|
|
3720
|
+
if (typeof p === "string") checkpoint.recordWrite(p);
|
|
3721
|
+
const dest = resp.arguments?.dest ?? resp.arguments?.new_path ?? resp.arguments?.to;
|
|
3722
|
+
if (typeof dest === "string") checkpoint.recordWrite(dest);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
const toolMsg = {
|
|
3726
|
+
role: "tool",
|
|
3727
|
+
tool_call_id: resp.tool_call_id,
|
|
3728
|
+
name: resp.name,
|
|
3729
|
+
content: resultStr
|
|
3730
|
+
};
|
|
3731
|
+
history.push(toolMsg);
|
|
3732
|
+
if (args.persist) await convStore.appendMessage(toolMsg);
|
|
3733
|
+
}
|
|
3734
|
+
} finally {
|
|
3735
|
+
if (procRegistry.size > 0) {
|
|
3736
|
+
log(cYellow(`
|
|
3737
|
+
[cli] cleaning up ${procRegistry.size} spawned process(es) before exit\u2026`));
|
|
3738
|
+
await procRegistry.cleanupAll();
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
log(cDim(`
|
|
3742
|
+
Writes this session: ${registry.size}`));
|
|
3743
|
+
if (gJson) {
|
|
3744
|
+
const sid = resumedFrom || convStore.getCurrentId();
|
|
3745
|
+
emitJsonResult({
|
|
3746
|
+
session_id: sid,
|
|
3747
|
+
files_changed: finalFilesChanged,
|
|
3748
|
+
summary: finalSummary,
|
|
3749
|
+
exit_code: exitCode
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
process.exit(exitCode);
|
|
3753
|
+
}
|
|
3754
|
+
async function prompt(text) {
|
|
3755
|
+
process.stdout.write(text);
|
|
3756
|
+
return new Promise((resolve3) => {
|
|
3757
|
+
process.stdin.setEncoding("utf-8");
|
|
3758
|
+
const onData = (chunk) => {
|
|
3759
|
+
process.stdin.removeListener("data", onData);
|
|
3760
|
+
process.stdin.pause();
|
|
3761
|
+
resolve3(chunk.trim().toLowerCase().startsWith("y"));
|
|
3762
|
+
};
|
|
3763
|
+
process.stdin.resume();
|
|
3764
|
+
process.stdin.on("data", onData);
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
async function consumeStream(client, payload) {
|
|
3768
|
+
let streamedSomeContent = false;
|
|
3769
|
+
logInfo(cMag("\u2190 stream "));
|
|
3770
|
+
for await (const ev of client.turnStream(payload)) {
|
|
3771
|
+
switch (ev.type) {
|
|
3772
|
+
case "delta":
|
|
3773
|
+
if (ev.content) {
|
|
3774
|
+
if (!streamedSomeContent) {
|
|
3775
|
+
logInfo(" " + cCyan("\u2502 "));
|
|
3776
|
+
streamedSomeContent = true;
|
|
3777
|
+
}
|
|
3778
|
+
log(ev.content);
|
|
3779
|
+
}
|
|
3780
|
+
if (ev.reasoning) {
|
|
3781
|
+
}
|
|
3782
|
+
break;
|
|
3783
|
+
case "status":
|
|
3784
|
+
break;
|
|
3785
|
+
case "assistant":
|
|
3786
|
+
break;
|
|
3787
|
+
case "tool_call":
|
|
3788
|
+
if (streamedSomeContent) log("");
|
|
3789
|
+
return {
|
|
3790
|
+
type: "tool_call",
|
|
3791
|
+
tool_call_id: ev.tool_call_id,
|
|
3792
|
+
name: ev.name,
|
|
3793
|
+
arguments: ev.arguments,
|
|
3794
|
+
assistant_message: ev.assistant_message
|
|
3795
|
+
};
|
|
3796
|
+
case "blocked":
|
|
3797
|
+
if (streamedSomeContent) log("");
|
|
3798
|
+
return {
|
|
3799
|
+
type: "blocked",
|
|
3800
|
+
tool_call_id: ev.tool_call_id,
|
|
3801
|
+
name: ev.name,
|
|
3802
|
+
arguments: ev.arguments,
|
|
3803
|
+
reason: ev.reason,
|
|
3804
|
+
severity: ev.severity === "warn" ? "warn" : "block",
|
|
3805
|
+
assistant_message: ev.assistant_message
|
|
3806
|
+
};
|
|
3807
|
+
case "message":
|
|
3808
|
+
if (streamedSomeContent) log("");
|
|
3809
|
+
return {
|
|
3810
|
+
type: "message",
|
|
3811
|
+
content: ev.content,
|
|
3812
|
+
assistant_message: ev.assistant_message
|
|
3813
|
+
};
|
|
3814
|
+
case "done":
|
|
3815
|
+
if (streamedSomeContent) log("");
|
|
3816
|
+
return {
|
|
3817
|
+
type: "done",
|
|
3818
|
+
summary: ev.summary,
|
|
3819
|
+
files_changed: ev.files_changed || [],
|
|
3820
|
+
suggested_test_cmd: ev.suggested_test_cmd || "",
|
|
3821
|
+
assistant_message: ev.assistant_message
|
|
3822
|
+
};
|
|
3823
|
+
case "error":
|
|
3824
|
+
throw new Error(ev.message + (ev.detail ? `: ${ev.detail}` : ""));
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
throw new Error("stream ended without a terminal event");
|
|
3828
|
+
}
|
|
3829
|
+
main().catch((err) => {
|
|
3830
|
+
console.error("[cli] fatal:", err);
|
|
3831
|
+
process.exit(1);
|
|
3832
|
+
});
|
|
3833
|
+
//# sourceMappingURL=cli.js.map
|