@zhongqian97-code/ecode 0.2.5 → 0.3.1
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/README.md +106 -13
- package/dist/index.js +1431 -128
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,10 +3,11 @@ const _ew=process.emitWarning.bind(process);process.emitWarning=function(w,...a)
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import { resolve, dirname as
|
|
6
|
+
import { resolve as resolve4, dirname as dirname6 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import React4 from "react";
|
|
9
9
|
import { render } from "ink";
|
|
10
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
10
11
|
|
|
11
12
|
// src/config.ts
|
|
12
13
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -74,7 +75,10 @@ function loadConfig() {
|
|
|
74
75
|
try {
|
|
75
76
|
const raw = readFileSync(configPath, "utf-8");
|
|
76
77
|
fileConfig = JSON.parse(raw);
|
|
77
|
-
} catch {
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
process.stderr.write(`[ecode] warning: ${configPath} parse failed (${msg}), falling back to defaults/env
|
|
81
|
+
`);
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
return {
|
|
@@ -96,7 +100,7 @@ function loadConfig() {
|
|
|
96
100
|
|
|
97
101
|
// src/ui/App.tsx
|
|
98
102
|
import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3, useMemo } from "react";
|
|
99
|
-
import { Box as
|
|
103
|
+
import { Box as Box6, useInput as useInput2, useStdout, useStdin } from "ink";
|
|
100
104
|
|
|
101
105
|
// src/llm.ts
|
|
102
106
|
import OpenAI from "openai";
|
|
@@ -120,7 +124,7 @@ function createLLMClient(config2) {
|
|
|
120
124
|
* @param messages 完整的对话历史,包含 user/assistant/tool 所有轮次
|
|
121
125
|
* @param tools 可选的工具列表,不传或传空数组时不附加 tools 字段
|
|
122
126
|
*/
|
|
123
|
-
stream(messages, tools) {
|
|
127
|
+
stream(messages, tools, signal) {
|
|
124
128
|
return {
|
|
125
129
|
[Symbol.asyncIterator]: async function* () {
|
|
126
130
|
const requestParams = {
|
|
@@ -133,7 +137,8 @@ function createLLMClient(config2) {
|
|
|
133
137
|
requestParams.tools = tools;
|
|
134
138
|
}
|
|
135
139
|
const response = await openai.chat.completions.create(
|
|
136
|
-
requestParams
|
|
140
|
+
requestParams,
|
|
141
|
+
signal ? { signal } : void 0
|
|
137
142
|
);
|
|
138
143
|
const tcAccumulator = /* @__PURE__ */ new Map();
|
|
139
144
|
let reasoningAccumulator = "";
|
|
@@ -229,9 +234,90 @@ var DEFAULT_DANGER_LIST = [
|
|
|
229
234
|
function matchesEntry(cmd, entry) {
|
|
230
235
|
return cmd === entry || cmd.startsWith(entry + " ");
|
|
231
236
|
}
|
|
232
|
-
function
|
|
237
|
+
function splitByControlOps(cmd) {
|
|
238
|
+
const segments = [];
|
|
239
|
+
let current = "";
|
|
240
|
+
let inSingle = false;
|
|
241
|
+
let inDouble = false;
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < cmd.length) {
|
|
244
|
+
const ch = cmd[i];
|
|
245
|
+
if (ch === "'" && !inDouble) {
|
|
246
|
+
inSingle = !inSingle;
|
|
247
|
+
current += ch;
|
|
248
|
+
i++;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === '"' && !inSingle) {
|
|
252
|
+
inDouble = !inDouble;
|
|
253
|
+
current += ch;
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (inSingle || inDouble) {
|
|
258
|
+
if (inDouble && ch === "\\" && i + 1 < cmd.length) {
|
|
259
|
+
current += ch + cmd[i + 1];
|
|
260
|
+
i += 2;
|
|
261
|
+
} else {
|
|
262
|
+
current += ch;
|
|
263
|
+
i++;
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (ch === "&" && cmd[i + 1] === "&") {
|
|
268
|
+
segments.push(current.trim());
|
|
269
|
+
current = "";
|
|
270
|
+
i += 2;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (ch === "|" && cmd[i + 1] === "|") {
|
|
274
|
+
segments.push(current.trim());
|
|
275
|
+
current = "";
|
|
276
|
+
i += 2;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (ch === ";" || ch === "|") {
|
|
280
|
+
segments.push(current.trim());
|
|
281
|
+
current = "";
|
|
282
|
+
i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
current += ch;
|
|
286
|
+
i++;
|
|
287
|
+
}
|
|
288
|
+
segments.push(current.trim());
|
|
289
|
+
return segments.filter((s) => s.length > 0);
|
|
290
|
+
}
|
|
291
|
+
function stripShellWrapper(cmd) {
|
|
292
|
+
const trimmed = cmd.trim();
|
|
293
|
+
const dq = trimmed.match(/^(?:bash|sh)\s+-c\s+"((?:[^"\\]|\\.)*)"/);
|
|
294
|
+
if (dq) return dq[1];
|
|
295
|
+
const sq = trimmed.match(/^(?:bash|sh)\s+-c\s+'([^']*)'/);
|
|
296
|
+
if (sq) return sq[1];
|
|
297
|
+
const nq = trimmed.match(/^(?:bash|sh)\s+-c\s+(\S+)/);
|
|
298
|
+
if (nq) return nq[1];
|
|
299
|
+
return trimmed;
|
|
300
|
+
}
|
|
301
|
+
function classifyCommand(cmd, dangerPatterns, _depth = 0) {
|
|
233
302
|
const trimmed = cmd.trim();
|
|
234
303
|
const patterns = dangerPatterns ?? DEFAULT_DANGER_LIST;
|
|
304
|
+
if (_depth < 5) {
|
|
305
|
+
const inner = stripShellWrapper(trimmed);
|
|
306
|
+
if (inner !== trimmed) {
|
|
307
|
+
const innerClass = classifyCommand(inner.trim(), dangerPatterns, _depth + 1);
|
|
308
|
+
if (innerClass === "danger") return "danger";
|
|
309
|
+
return "normal";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const segments = splitByControlOps(trimmed);
|
|
313
|
+
if (segments.length > 1) {
|
|
314
|
+
for (const seg of segments) {
|
|
315
|
+
if (classifyCommand(seg, dangerPatterns, _depth + 1) === "danger") {
|
|
316
|
+
return "danger";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return "normal";
|
|
320
|
+
}
|
|
235
321
|
for (const entry of patterns) {
|
|
236
322
|
if (matchesEntry(trimmed, entry)) return "danger";
|
|
237
323
|
}
|
|
@@ -245,18 +331,671 @@ function classifyCommand(cmd, dangerPatterns) {
|
|
|
245
331
|
import { exec } from "child_process";
|
|
246
332
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
247
333
|
function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
248
|
-
return new Promise((
|
|
334
|
+
return new Promise((resolve5) => {
|
|
249
335
|
exec(cmd, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
250
336
|
if (err) {
|
|
251
337
|
const exitCode = err.code ?? 1;
|
|
252
|
-
|
|
338
|
+
resolve5({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode });
|
|
253
339
|
} else {
|
|
254
|
-
|
|
340
|
+
resolve5({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 });
|
|
255
341
|
}
|
|
256
342
|
});
|
|
257
343
|
});
|
|
258
344
|
}
|
|
259
345
|
|
|
346
|
+
// src/tools/read.ts
|
|
347
|
+
import * as fs from "fs/promises";
|
|
348
|
+
var READ_TOOL = {
|
|
349
|
+
type: "function",
|
|
350
|
+
function: {
|
|
351
|
+
name: "read",
|
|
352
|
+
description: "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\u3002\u4F7F\u7528 offset\uFF080-based \u884C\u7D22\u5F15\uFF09\u548C limit \u53EF\u8BFB\u53D6\u6307\u5B9A\u884C\u8303\u56F4\uFF0C\u9002\u5408\u5927\u6587\u4EF6\u5C40\u90E8\u9605\u8BFB\u3002",
|
|
353
|
+
parameters: {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: {
|
|
356
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
357
|
+
offset: { type: "number", description: "\u8D77\u59CB\u884C\u7D22\u5F15\uFF080-based\uFF0C\u9ED8\u8BA4 0\uFF09\u3002" },
|
|
358
|
+
limit: { type: "number", description: "\u6700\u591A\u8FD4\u56DE\u7684\u884C\u6570\u3002" }
|
|
359
|
+
},
|
|
360
|
+
required: ["path"]
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
async function readFile2(params) {
|
|
365
|
+
const { path: path7, offset = 0, limit } = params;
|
|
366
|
+
let raw;
|
|
367
|
+
try {
|
|
368
|
+
raw = await fs.readFile(path7, "utf8");
|
|
369
|
+
} catch (err) {
|
|
370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
371
|
+
return `Error reading ${path7}: ${msg}`;
|
|
372
|
+
}
|
|
373
|
+
const lines = raw.split("\n");
|
|
374
|
+
const sliced = limit !== void 0 ? lines.slice(offset, offset + limit) : lines.slice(offset);
|
|
375
|
+
return sliced.map((line, i) => `${String(offset + i + 1).padStart(4)} ${line}`).join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/tools/write.ts
|
|
379
|
+
import * as fs2 from "fs/promises";
|
|
380
|
+
import * as path from "path";
|
|
381
|
+
var WRITE_TOOL = {
|
|
382
|
+
type: "function",
|
|
383
|
+
function: {
|
|
384
|
+
name: "write",
|
|
385
|
+
description: "\u5C06\u5185\u5BB9\u5199\u5165\u6587\u4EF6\uFF08\u8986\u76D6\u6A21\u5F0F\uFF09\uFF0C\u81EA\u52A8\u521B\u5EFA\u7236\u76EE\u5F55\u3002\u5199\u5165\u524D\u8BF7\u5148\u7528 read \u786E\u8BA4\u73B0\u6709\u5185\u5BB9\u3002",
|
|
386
|
+
parameters: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
390
|
+
content: { type: "string", description: "\u8981\u5199\u5165\u7684\u5B8C\u6574\u5185\u5BB9\u3002" }
|
|
391
|
+
},
|
|
392
|
+
required: ["path", "content"]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
async function writeFile2(params) {
|
|
397
|
+
const { path: filePath, content } = params;
|
|
398
|
+
try {
|
|
399
|
+
await fs2.mkdir(path.dirname(filePath), { recursive: true });
|
|
400
|
+
await fs2.writeFile(filePath, content, "utf8");
|
|
401
|
+
return `Written ${filePath}`;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
404
|
+
return `Error writing ${filePath}: ${msg}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/tools/edit.ts
|
|
409
|
+
import * as fs3 from "fs/promises";
|
|
410
|
+
var EDIT_TOOL = {
|
|
411
|
+
type: "function",
|
|
412
|
+
function: {
|
|
413
|
+
name: "edit",
|
|
414
|
+
description: "\u5728\u6587\u4EF6\u4E2D\u7CBE\u786E\u66FF\u6362\u4E00\u6BB5\u5B57\u7B26\u4E32\u3002old_string \u5728\u6587\u4EF6\u4E2D\u5FC5\u987B\u552F\u4E00\u51FA\u73B0\u2014\u2014\u82E5\u6709\u6B67\u4E49\u8BF7\u63D0\u4F9B\u66F4\u591A\u4E0A\u4E0B\u6587\u3002",
|
|
415
|
+
parameters: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
419
|
+
old_string: { type: "string", description: "\u8981\u67E5\u627E\u7684\u7CBE\u786E\u5B57\u7B26\u4E32\uFF08\u5728\u6587\u4EF6\u4E2D\u5FC5\u987B\u552F\u4E00\uFF09\u3002" },
|
|
420
|
+
new_string: { type: "string", description: "\u66FF\u6362\u540E\u7684\u5B57\u7B26\u4E32\u3002" }
|
|
421
|
+
},
|
|
422
|
+
required: ["path", "old_string", "new_string"]
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
async function editFile(params) {
|
|
427
|
+
const { path: path7, old_string, new_string } = params;
|
|
428
|
+
let content;
|
|
429
|
+
try {
|
|
430
|
+
content = await fs3.readFile(path7, "utf8");
|
|
431
|
+
} catch (err) {
|
|
432
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
433
|
+
return `Error reading ${path7}: ${msg}`;
|
|
434
|
+
}
|
|
435
|
+
const count = countOccurrences(content, old_string);
|
|
436
|
+
if (count === 0) {
|
|
437
|
+
return `Error: old_string not found in ${path7}`;
|
|
438
|
+
}
|
|
439
|
+
if (count > 1) {
|
|
440
|
+
return `Error: old_string appears ${count} times in ${path7} (ambiguous \u2014 add more context)`;
|
|
441
|
+
}
|
|
442
|
+
const updated = content.replace(old_string, new_string);
|
|
443
|
+
try {
|
|
444
|
+
await fs3.writeFile(path7, updated, "utf8");
|
|
445
|
+
return `Edited ${path7}`;
|
|
446
|
+
} catch (err) {
|
|
447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
448
|
+
return `Error writing ${path7}: ${msg}`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function countOccurrences(haystack, needle) {
|
|
452
|
+
if (needle === "") return 0;
|
|
453
|
+
let count = 0;
|
|
454
|
+
let pos = 0;
|
|
455
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
456
|
+
count++;
|
|
457
|
+
pos += needle.length;
|
|
458
|
+
}
|
|
459
|
+
return count;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/tools/glob.ts
|
|
463
|
+
import * as fs4 from "fs/promises";
|
|
464
|
+
import * as path2 from "path";
|
|
465
|
+
var GLOB_TOOL = {
|
|
466
|
+
type: "function",
|
|
467
|
+
function: {
|
|
468
|
+
name: "glob",
|
|
469
|
+
description: "Find files matching a glob pattern. Returns relative paths (one per line). Default excludes node_modules, .git and hidden dirs; pass includeHidden=true to include them.",
|
|
470
|
+
parameters: {
|
|
471
|
+
type: "object",
|
|
472
|
+
properties: {
|
|
473
|
+
pattern: { type: "string", description: "Glob pattern, e.g. '**/*.ts', 'src/**', '*.md'" },
|
|
474
|
+
cwd: { type: "string", description: "Base directory to search in (default: current working directory)" },
|
|
475
|
+
includeHidden: { type: "boolean", description: "If true, include hidden files/dirs (starting with '.')" },
|
|
476
|
+
limit: { type: "number", description: "Max number of results to return (default 100)" }
|
|
477
|
+
},
|
|
478
|
+
required: ["pattern"]
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
function globToRegex(pattern) {
|
|
483
|
+
const hasSlash = pattern.includes("/");
|
|
484
|
+
const DSS = "\0DSS\0";
|
|
485
|
+
const DS = "\0DS\0";
|
|
486
|
+
const SS = "\0SS\0";
|
|
487
|
+
const QQ = "\0QQ\0";
|
|
488
|
+
let p = pattern.replace(/\*\*\//g, DSS).replace(/\*\*/g, DS).replace(/\*/g, SS).replace(/\?/g, QQ);
|
|
489
|
+
p = p.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
490
|
+
p = p.replace(new RegExp(DSS, "g"), "(.*/)?").replace(new RegExp(DS, "g"), ".*").replace(new RegExp(SS, "g"), "[^/]*").replace(new RegExp(QQ, "g"), "[^/]");
|
|
491
|
+
if (!hasSlash) {
|
|
492
|
+
}
|
|
493
|
+
return new RegExp(`^${p}$`);
|
|
494
|
+
}
|
|
495
|
+
async function walk(dir, cwd, regex, includeHidden, results, total, limit) {
|
|
496
|
+
let entries;
|
|
497
|
+
try {
|
|
498
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
499
|
+
} catch {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
503
|
+
for (const entry of entries) {
|
|
504
|
+
const name = entry.name;
|
|
505
|
+
if (name === "node_modules") continue;
|
|
506
|
+
if (!includeHidden && name.startsWith(".")) continue;
|
|
507
|
+
const abs = path2.join(dir, name);
|
|
508
|
+
const rel = path2.relative(cwd, abs);
|
|
509
|
+
if (entry.isDirectory()) {
|
|
510
|
+
await walk(abs, cwd, regex, includeHidden, results, total, limit);
|
|
511
|
+
} else if (entry.isFile()) {
|
|
512
|
+
const relPosix = rel.split(path2.sep).join("/");
|
|
513
|
+
if (regex.test(relPosix)) {
|
|
514
|
+
total.count++;
|
|
515
|
+
if (results.length < limit) {
|
|
516
|
+
results.push(relPosix);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function globFiles(params) {
|
|
523
|
+
const {
|
|
524
|
+
pattern,
|
|
525
|
+
cwd = process.cwd(),
|
|
526
|
+
includeHidden = false,
|
|
527
|
+
limit = 100
|
|
528
|
+
} = params;
|
|
529
|
+
try {
|
|
530
|
+
const stat5 = await fs4.stat(cwd);
|
|
531
|
+
if (!stat5.isDirectory()) {
|
|
532
|
+
return `Error: ${cwd} is not a directory`;
|
|
533
|
+
}
|
|
534
|
+
} catch (err) {
|
|
535
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
536
|
+
return `Error: ${msg}`;
|
|
537
|
+
}
|
|
538
|
+
const regex = globToRegex(pattern);
|
|
539
|
+
const results = [];
|
|
540
|
+
const total = { count: 0 };
|
|
541
|
+
await walk(cwd, cwd, regex, includeHidden, results, total, limit);
|
|
542
|
+
if (results.length === 0) {
|
|
543
|
+
return `No files found matching pattern: ${pattern}`;
|
|
544
|
+
}
|
|
545
|
+
results.sort();
|
|
546
|
+
let output = results.join("\n");
|
|
547
|
+
if (total.count > limit) {
|
|
548
|
+
output += `
|
|
549
|
+
... (truncated, ${total.count} total matches)`;
|
|
550
|
+
}
|
|
551
|
+
return output;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/tools/grep.ts
|
|
555
|
+
import * as fs5 from "fs/promises";
|
|
556
|
+
import * as path3 from "path";
|
|
557
|
+
var GREP_TOOL = {
|
|
558
|
+
type: "function",
|
|
559
|
+
function: {
|
|
560
|
+
name: "grep",
|
|
561
|
+
description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6B63\u5219\u8868\u8FBE\u5F0F\u6A21\u5F0F\u3002\u8FD4\u56DE file:linenum:content \u683C\u5F0F\uFF0Ccontext \u884C\u7528 -- \u5206\u9694\u3002",
|
|
562
|
+
parameters: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
pattern: { type: "string", description: "\u6B63\u5219\u8868\u8FBE\u5F0F\u6A21\u5F0F\u3002" },
|
|
566
|
+
path: { type: "string", description: "\u641C\u7D22\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09\u3002" },
|
|
567
|
+
cwd: { type: "string", description: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4 process.cwd()\uFF09\u3002" },
|
|
568
|
+
context: { type: "number", description: "\u5339\u914D\u524D\u540E\u7684\u4E0A\u4E0B\u6587\u884C\u6570\uFF08\u9ED8\u8BA4 0\uFF09\u3002" },
|
|
569
|
+
includeHidden: { type: "boolean", description: "\u662F\u5426\u5305\u542B\u9690\u85CF\u76EE\u5F55\uFF08\u9ED8\u8BA4 false\uFF09\u3002" },
|
|
570
|
+
limit: { type: "number", description: "\u6700\u5927\u8FD4\u56DE\u5339\u914D\u6570\uFF08\u9ED8\u8BA4 50\uFF09\u3002" }
|
|
571
|
+
},
|
|
572
|
+
required: ["pattern"]
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
async function searchFile(absFile, displayFile, regex, matches, totalCount, limit) {
|
|
577
|
+
let text;
|
|
578
|
+
try {
|
|
579
|
+
text = await fs5.readFile(absFile, "utf8");
|
|
580
|
+
} catch {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const lines = text.split("\n");
|
|
584
|
+
const lastIdx = lines.length - 1;
|
|
585
|
+
for (let i = 0; i < lines.length; i++) {
|
|
586
|
+
if (i === lastIdx && lines[i] === "") continue;
|
|
587
|
+
if (regex.test(lines[i])) {
|
|
588
|
+
totalCount.count++;
|
|
589
|
+
if (matches.length < limit) {
|
|
590
|
+
matches.push({ file: displayFile, line: i + 1, content: lines[i] });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function walkDir(dir, cwd, regex, includeHidden, matches, totalCount, limit) {
|
|
596
|
+
let entries;
|
|
597
|
+
try {
|
|
598
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
599
|
+
} catch {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
const name = entry.name;
|
|
605
|
+
if (name === "node_modules") continue;
|
|
606
|
+
if (!includeHidden && name.startsWith(".")) continue;
|
|
607
|
+
const abs = path3.join(dir, name);
|
|
608
|
+
if (entry.isDirectory()) {
|
|
609
|
+
await walkDir(abs, cwd, regex, includeHidden, matches, totalCount, limit);
|
|
610
|
+
} else if (entry.isFile()) {
|
|
611
|
+
const rel = path3.relative(cwd, abs).split(path3.sep).join("/");
|
|
612
|
+
await searchFile(abs, rel, regex, matches, totalCount, limit);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function renderWithContext(entries, contextLines) {
|
|
617
|
+
const outputLines = [];
|
|
618
|
+
for (const { absFile, displayFile, matches: fileMatches } of entries) {
|
|
619
|
+
let allLines = [];
|
|
620
|
+
try {
|
|
621
|
+
const text = await fs5.readFile(absFile, "utf8");
|
|
622
|
+
allLines = text.split("\n");
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
if (allLines.length === 0) {
|
|
626
|
+
for (const m of fileMatches) {
|
|
627
|
+
outputLines.push(`${displayFile}:${m.line}:${m.content}`);
|
|
628
|
+
}
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const ranges = [];
|
|
632
|
+
for (const m of fileMatches) {
|
|
633
|
+
const start = Math.max(0, m.line - 1 - contextLines);
|
|
634
|
+
const end = Math.min(allLines.length - 1, m.line - 1 + contextLines);
|
|
635
|
+
const last = ranges[ranges.length - 1];
|
|
636
|
+
if (last && start <= last.end + 1) {
|
|
637
|
+
last.end = Math.max(last.end, end);
|
|
638
|
+
} else {
|
|
639
|
+
ranges.push({ start, end });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (let ri = 0; ri < ranges.length; ri++) {
|
|
643
|
+
if (ri > 0) outputLines.push("--");
|
|
644
|
+
const { start, end } = ranges[ri];
|
|
645
|
+
for (let i = start; i <= end; i++) {
|
|
646
|
+
if (i === allLines.length - 1 && allLines[i] === "") continue;
|
|
647
|
+
outputLines.push(`${displayFile}:${i + 1}:${allLines[i]}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return outputLines;
|
|
652
|
+
}
|
|
653
|
+
async function grepFiles(params) {
|
|
654
|
+
const {
|
|
655
|
+
pattern,
|
|
656
|
+
path: searchPath = ".",
|
|
657
|
+
cwd = process.cwd(),
|
|
658
|
+
context = 0,
|
|
659
|
+
includeHidden = false,
|
|
660
|
+
limit = 50
|
|
661
|
+
} = params;
|
|
662
|
+
let regex;
|
|
663
|
+
try {
|
|
664
|
+
regex = new RegExp(pattern);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
667
|
+
return `Error: Invalid regex: ${msg}`;
|
|
668
|
+
}
|
|
669
|
+
const target = path3.isAbsolute(searchPath) ? searchPath : path3.resolve(cwd, searchPath);
|
|
670
|
+
let stat5;
|
|
671
|
+
try {
|
|
672
|
+
stat5 = await fs5.stat(target);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
return `Error: ${msg}`;
|
|
676
|
+
}
|
|
677
|
+
const matches = [];
|
|
678
|
+
const totalCount = { count: 0 };
|
|
679
|
+
if (stat5.isFile()) {
|
|
680
|
+
await searchFile(target, searchPath, regex, matches, totalCount, limit);
|
|
681
|
+
} else if (stat5.isDirectory()) {
|
|
682
|
+
await walkDir(target, target, regex, includeHidden, matches, totalCount, limit);
|
|
683
|
+
} else {
|
|
684
|
+
return `Error: ${target} is neither a file nor a directory`;
|
|
685
|
+
}
|
|
686
|
+
if (matches.length === 0 && totalCount.count === 0) {
|
|
687
|
+
return "No matches found";
|
|
688
|
+
}
|
|
689
|
+
const matchesByFile = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const m of matches) {
|
|
691
|
+
let group = matchesByFile.get(m.file);
|
|
692
|
+
if (!group) {
|
|
693
|
+
group = [];
|
|
694
|
+
matchesByFile.set(m.file, group);
|
|
695
|
+
}
|
|
696
|
+
group.push(m);
|
|
697
|
+
}
|
|
698
|
+
let outputLines;
|
|
699
|
+
if (context > 0) {
|
|
700
|
+
const baseDir = stat5.isDirectory() ? target : path3.dirname(target);
|
|
701
|
+
const entries = Array.from(matchesByFile.entries()).map(([displayFile, fileMatches]) => {
|
|
702
|
+
const absFile = path3.isAbsolute(displayFile) ? displayFile : path3.resolve(baseDir, displayFile);
|
|
703
|
+
return { absFile, displayFile, matches: fileMatches };
|
|
704
|
+
});
|
|
705
|
+
outputLines = await renderWithContext(entries, context);
|
|
706
|
+
} else {
|
|
707
|
+
outputLines = [];
|
|
708
|
+
for (const m of matches) {
|
|
709
|
+
outputLines.push(`${m.file}:${m.line}:${m.content}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
let output = outputLines.join("\n");
|
|
713
|
+
if (totalCount.count > limit) {
|
|
714
|
+
const extra = totalCount.count - limit;
|
|
715
|
+
output += `
|
|
716
|
+
... (truncated, ${extra} more matches)`;
|
|
717
|
+
}
|
|
718
|
+
return output;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/tools/apply_patch.ts
|
|
722
|
+
import * as fs6 from "fs/promises";
|
|
723
|
+
import * as path4 from "path";
|
|
724
|
+
var APPLY_PATCH_TOOL = {
|
|
725
|
+
type: "function",
|
|
726
|
+
function: {
|
|
727
|
+
name: "apply_patch",
|
|
728
|
+
description: "\u5C06 unified diff \u683C\u5F0F\u7684 patch \u5E94\u7528\u5230\u6587\u4EF6\u3002\u652F\u6301 context \u4E0A\u4E0B\u6587\u884C\u6A21\u7CCA\u5B9A\u4F4D\uFF08\u5141\u8BB8\u5C11\u91CF\u884C\u53F7\u504F\u79FB\uFF09\u3002",
|
|
729
|
+
parameters: {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
patch: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "Unified diff \u683C\u5F0F\u7684 patch \u5185\u5BB9\u3002"
|
|
735
|
+
},
|
|
736
|
+
file: {
|
|
737
|
+
type: "string",
|
|
738
|
+
description: "\u76EE\u6807\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u9009\uFF0C\u82E5 patch \u4E2D\u5305\u542B --- / +++ \u5934\u5219\u4ECE\u4E2D\u89E3\u6790\uFF09\u3002"
|
|
739
|
+
},
|
|
740
|
+
cwd: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4 process.cwd()\uFF09\u3002"
|
|
743
|
+
},
|
|
744
|
+
dryRun: {
|
|
745
|
+
type: "boolean",
|
|
746
|
+
description: "\u82E5\u4E3A true\uFF0C\u4EC5\u6821\u9A8C patch \u662F\u5426\u80FD\u5E94\u7528\uFF0C\u4E0D\u4FEE\u6539\u6587\u4EF6\uFF08\u9ED8\u8BA4 false\uFF09\u3002"
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
required: ["patch"]
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
function parseFilePath(patch) {
|
|
754
|
+
for (const line of patch.split("\n")) {
|
|
755
|
+
if (line.startsWith("+++ ")) {
|
|
756
|
+
let p = line.slice(4).trim();
|
|
757
|
+
if (p.startsWith("b/")) p = p.slice(2);
|
|
758
|
+
if (p === "/dev/null" || p === "") return null;
|
|
759
|
+
return p;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
function parseHunkHeader(header) {
|
|
765
|
+
const m = header.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+\d+(?:,\d+)?\s+@@/);
|
|
766
|
+
if (!m) return null;
|
|
767
|
+
const oldStart = parseInt(m[1], 10) - 1;
|
|
768
|
+
const oldCount = m[2] !== void 0 ? parseInt(m[2], 10) : 1;
|
|
769
|
+
return { oldStart, oldCount };
|
|
770
|
+
}
|
|
771
|
+
function parseHunks(patch) {
|
|
772
|
+
const rawLines = patch.split("\n");
|
|
773
|
+
const hunks = [];
|
|
774
|
+
let current = null;
|
|
775
|
+
for (const raw of rawLines) {
|
|
776
|
+
if (raw.startsWith("diff --git ")) continue;
|
|
777
|
+
if (raw.startsWith("--- ") || raw.startsWith("+++ ")) continue;
|
|
778
|
+
if (raw.startsWith("\\ ")) continue;
|
|
779
|
+
if (raw.startsWith("@@ ")) {
|
|
780
|
+
const parsed = parseHunkHeader(raw);
|
|
781
|
+
if (!parsed) continue;
|
|
782
|
+
current = {
|
|
783
|
+
header: raw,
|
|
784
|
+
oldStart: parsed.oldStart,
|
|
785
|
+
oldCount: parsed.oldCount,
|
|
786
|
+
lines: []
|
|
787
|
+
};
|
|
788
|
+
hunks.push(current);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (current === null) continue;
|
|
792
|
+
if (raw.startsWith("+")) {
|
|
793
|
+
current.lines.push({ kind: "add", content: raw.slice(1) });
|
|
794
|
+
} else if (raw.startsWith("-")) {
|
|
795
|
+
current.lines.push({ kind: "remove", content: raw.slice(1) });
|
|
796
|
+
} else if (raw.startsWith(" ")) {
|
|
797
|
+
current.lines.push({ kind: "context", content: raw.slice(1) });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return hunks;
|
|
801
|
+
}
|
|
802
|
+
var FUZZY_RADIUS = 3;
|
|
803
|
+
function fuzzyFind(fileLines, hunk, suggestedStart) {
|
|
804
|
+
const expected = hunk.lines.filter((l) => l.kind === "context" || l.kind === "remove").map((l) => l.content);
|
|
805
|
+
if (expected.length === 0) {
|
|
806
|
+
return Math.max(0, Math.min(suggestedStart, fileLines.length));
|
|
807
|
+
}
|
|
808
|
+
const lo = Math.max(0, suggestedStart - FUZZY_RADIUS);
|
|
809
|
+
const hi = Math.min(
|
|
810
|
+
fileLines.length - expected.length,
|
|
811
|
+
suggestedStart + FUZZY_RADIUS
|
|
812
|
+
);
|
|
813
|
+
for (let start = lo; start <= hi; start++) {
|
|
814
|
+
let match = true;
|
|
815
|
+
for (let i = 0; i < expected.length; i++) {
|
|
816
|
+
if (fileLines[start + i] !== expected[i]) {
|
|
817
|
+
match = false;
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (match) return start;
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
function applyHunk(fileLines, hunk, actualStart) {
|
|
826
|
+
const oldLineCount = hunk.lines.filter(
|
|
827
|
+
(l) => l.kind === "context" || l.kind === "remove"
|
|
828
|
+
).length;
|
|
829
|
+
const newBlock = [];
|
|
830
|
+
for (const hl of hunk.lines) {
|
|
831
|
+
if (hl.kind === "context" || hl.kind === "add") {
|
|
832
|
+
newBlock.push(hl.content);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return [
|
|
836
|
+
...fileLines.slice(0, actualStart),
|
|
837
|
+
...newBlock,
|
|
838
|
+
...fileLines.slice(actualStart + oldLineCount)
|
|
839
|
+
];
|
|
840
|
+
}
|
|
841
|
+
async function applyPatch(params) {
|
|
842
|
+
const { patch, file: fileParam, cwd = process.cwd(), dryRun = false } = params;
|
|
843
|
+
let filePath;
|
|
844
|
+
if (fileParam) {
|
|
845
|
+
filePath = path4.isAbsolute(fileParam) ? fileParam : path4.resolve(cwd, fileParam);
|
|
846
|
+
} else {
|
|
847
|
+
const parsed = parseFilePath(patch);
|
|
848
|
+
if (!parsed) {
|
|
849
|
+
return "Error: no target file path found in patch headers (--- / +++ missing) and no 'file' param provided";
|
|
850
|
+
}
|
|
851
|
+
filePath = path4.isAbsolute(parsed) ? parsed : path4.resolve(cwd, parsed);
|
|
852
|
+
}
|
|
853
|
+
let originalContent;
|
|
854
|
+
try {
|
|
855
|
+
originalContent = await fs6.readFile(filePath, "utf8");
|
|
856
|
+
} catch (err) {
|
|
857
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
858
|
+
return `Error: ${msg}`;
|
|
859
|
+
}
|
|
860
|
+
const hasTrailingNewline = originalContent.endsWith("\n");
|
|
861
|
+
const rawLines = originalContent.split("\n");
|
|
862
|
+
let fileLines = hasTrailingNewline ? rawLines.slice(0, -1) : rawLines;
|
|
863
|
+
const hunks = parseHunks(patch);
|
|
864
|
+
if (hunks.length === 0) {
|
|
865
|
+
return `Error: no valid hunks found in patch`;
|
|
866
|
+
}
|
|
867
|
+
let lineOffset = 0;
|
|
868
|
+
for (const hunk of hunks) {
|
|
869
|
+
const suggestedStart = hunk.oldStart + lineOffset;
|
|
870
|
+
const actualStart = fuzzyFind(fileLines, hunk, suggestedStart);
|
|
871
|
+
if (actualStart === null) {
|
|
872
|
+
return `Error: hunk ${hunk.header} does not apply (context mismatch)`;
|
|
873
|
+
}
|
|
874
|
+
const addCount = hunk.lines.filter((l) => l.kind === "add").length;
|
|
875
|
+
const removeCount = hunk.lines.filter((l) => l.kind === "remove").length;
|
|
876
|
+
fileLines = applyHunk(fileLines, hunk, actualStart);
|
|
877
|
+
lineOffset += addCount - removeCount;
|
|
878
|
+
}
|
|
879
|
+
const displayPath = path4.relative(cwd, filePath) || filePath;
|
|
880
|
+
if (dryRun) {
|
|
881
|
+
return `Patch applies cleanly to ${displayPath} (dry run)`;
|
|
882
|
+
}
|
|
883
|
+
const newContent = fileLines.join("\n") + (hasTrailingNewline ? "\n" : "");
|
|
884
|
+
try {
|
|
885
|
+
await fs6.writeFile(filePath, newContent, "utf8");
|
|
886
|
+
} catch (err) {
|
|
887
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
888
|
+
return `Error: failed to write file: ${msg}`;
|
|
889
|
+
}
|
|
890
|
+
return `Applied ${hunks.length} hunk(s) to ${displayPath}`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/tools/todo.ts
|
|
894
|
+
var TODO_TOOL = {
|
|
895
|
+
type: "function",
|
|
896
|
+
function: {
|
|
897
|
+
name: "todo",
|
|
898
|
+
description: "\u7BA1\u7406\u5F53\u524D\u4F1A\u8BDD\u7684\u4EFB\u52A1\u6E05\u5355\u3002op=read \u8BFB\u53D6\uFF0Cop=write \u5168\u91CF\u8986\u5199\uFF0Cop=update \u66F4\u65B0\u5355\u9879\u3002",
|
|
899
|
+
parameters: {
|
|
900
|
+
type: "object",
|
|
901
|
+
properties: {
|
|
902
|
+
op: {
|
|
903
|
+
type: "string",
|
|
904
|
+
enum: ["read", "write", "update"],
|
|
905
|
+
description: "\u64CD\u4F5C\u7C7B\u578B\u3002"
|
|
906
|
+
},
|
|
907
|
+
todos: {
|
|
908
|
+
type: "array",
|
|
909
|
+
description: "write \u64CD\u4F5C\uFF1A\u5168\u91CF\u66FF\u6362\u7684\u4EFB\u52A1\u5217\u8868\u3002",
|
|
910
|
+
items: {
|
|
911
|
+
type: "object",
|
|
912
|
+
properties: {
|
|
913
|
+
id: { type: "string" },
|
|
914
|
+
content: { type: "string" },
|
|
915
|
+
status: {
|
|
916
|
+
type: "string",
|
|
917
|
+
enum: ["pending", "in_progress", "completed", "cancelled"]
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
required: ["id", "content", "status"]
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
id: {
|
|
924
|
+
type: "string",
|
|
925
|
+
description: "update \u64CD\u4F5C\uFF1A\u76EE\u6807\u4EFB\u52A1\u7684\u552F\u4E00 id\u3002"
|
|
926
|
+
},
|
|
927
|
+
status: {
|
|
928
|
+
type: "string",
|
|
929
|
+
enum: ["pending", "in_progress", "completed", "cancelled"],
|
|
930
|
+
description: "update \u64CD\u4F5C\uFF1A\u65B0\u72B6\u6001\uFF08\u53EF\u9009\uFF09\u3002"
|
|
931
|
+
},
|
|
932
|
+
content: {
|
|
933
|
+
type: "string",
|
|
934
|
+
description: "update \u64CD\u4F5C\uFF1A\u65B0\u5185\u5BB9\uFF08\u53EF\u9009\uFF09\u3002"
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
required: ["op"]
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
var _todos = [];
|
|
942
|
+
var STATUS_SYMBOL = {
|
|
943
|
+
pending: "[ ]",
|
|
944
|
+
in_progress: "[~]",
|
|
945
|
+
completed: "[x]",
|
|
946
|
+
cancelled: "[-]"
|
|
947
|
+
};
|
|
948
|
+
function opRead() {
|
|
949
|
+
if (_todos.length === 0) {
|
|
950
|
+
return "(no todos)";
|
|
951
|
+
}
|
|
952
|
+
return _todos.map((item) => `- ${STATUS_SYMBOL[item.status]} ${item.id}: ${item.content}`).join("\n");
|
|
953
|
+
}
|
|
954
|
+
function opWrite(params) {
|
|
955
|
+
if (params.todos === void 0) {
|
|
956
|
+
return "Error: write operation requires 'todos' parameter";
|
|
957
|
+
}
|
|
958
|
+
_todos = params.todos.map((t) => ({ ...t }));
|
|
959
|
+
return `Wrote ${_todos.length} todos.`;
|
|
960
|
+
}
|
|
961
|
+
function opUpdate(params) {
|
|
962
|
+
if (params.id === void 0) {
|
|
963
|
+
return "Error: update operation requires 'id' parameter";
|
|
964
|
+
}
|
|
965
|
+
if (params.status === void 0 && params.content === void 0) {
|
|
966
|
+
return "Error: update operation requires at least 'status' or 'content' parameter";
|
|
967
|
+
}
|
|
968
|
+
const idx = _todos.findIndex((t) => t.id === params.id);
|
|
969
|
+
if (idx === -1) {
|
|
970
|
+
return `Error: todo '${params.id}' not found`;
|
|
971
|
+
}
|
|
972
|
+
const updated = {
|
|
973
|
+
..._todos[idx],
|
|
974
|
+
...params.status !== void 0 ? { status: params.status } : {},
|
|
975
|
+
...params.content !== void 0 ? { content: params.content } : {}
|
|
976
|
+
};
|
|
977
|
+
_todos = [
|
|
978
|
+
..._todos.slice(0, idx),
|
|
979
|
+
updated,
|
|
980
|
+
..._todos.slice(idx + 1)
|
|
981
|
+
];
|
|
982
|
+
return `Updated todo ${params.id}.`;
|
|
983
|
+
}
|
|
984
|
+
function todo(params) {
|
|
985
|
+
switch (params.op) {
|
|
986
|
+
case "read":
|
|
987
|
+
return opRead();
|
|
988
|
+
case "write":
|
|
989
|
+
return opWrite(params);
|
|
990
|
+
case "update":
|
|
991
|
+
return opUpdate(params);
|
|
992
|
+
default: {
|
|
993
|
+
const unknownOp = params.op;
|
|
994
|
+
return `Error: unknown op '${unknownOp}'`;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
260
999
|
// src/repl.ts
|
|
261
1000
|
var SKIP_MESSAGE = "Command skipped by user.";
|
|
262
1001
|
var BASH_TOOL = {
|
|
@@ -301,12 +1040,12 @@ Proceed? (y/n) `);
|
|
|
301
1040
|
}
|
|
302
1041
|
|
|
303
1042
|
// src/logger.ts
|
|
304
|
-
import * as
|
|
305
|
-
import * as
|
|
1043
|
+
import * as fs7 from "fs";
|
|
1044
|
+
import * as path5 from "path";
|
|
306
1045
|
function createLogger(logDir, sessionStart) {
|
|
307
|
-
|
|
1046
|
+
fs7.mkdirSync(logDir, { recursive: true });
|
|
308
1047
|
const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
|
|
309
|
-
const filePath =
|
|
1048
|
+
const filePath = path5.join(logDir, filename);
|
|
310
1049
|
return {
|
|
311
1050
|
filePath,
|
|
312
1051
|
/**
|
|
@@ -320,7 +1059,7 @@ function createLogger(logDir, sessionStart) {
|
|
|
320
1059
|
*/
|
|
321
1060
|
append(entry) {
|
|
322
1061
|
try {
|
|
323
|
-
|
|
1062
|
+
fs7.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
324
1063
|
} catch (err) {
|
|
325
1064
|
process.stderr.write(`[logger] Failed to write log entry: ${err}
|
|
326
1065
|
`);
|
|
@@ -359,7 +1098,168 @@ function handleSkillInput(input, registry2) {
|
|
|
359
1098
|
const content = args ? `${skill.body}
|
|
360
1099
|
|
|
361
1100
|
${args}` : skill.body;
|
|
362
|
-
return { type: "skill", message: { role: "user", content } };
|
|
1101
|
+
return { type: "skill", message: { role: "user", content }, skill };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/skills/executor.ts
|
|
1105
|
+
import { exec as exec2 } from "child_process";
|
|
1106
|
+
import { dirname as dirname4 } from "path";
|
|
1107
|
+
import { promisify } from "util";
|
|
1108
|
+
|
|
1109
|
+
// src/skills/loader.ts
|
|
1110
|
+
import { readFile as readFile6, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
1111
|
+
import { join as join5, dirname as dirname3, basename, resolve as resolve3, sep as sep3 } from "path";
|
|
1112
|
+
function parseFrontmatter(content) {
|
|
1113
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1114
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
1115
|
+
if (!match) {
|
|
1116
|
+
return { data: {}, body: content };
|
|
1117
|
+
}
|
|
1118
|
+
const rawFrontmatter = match[1];
|
|
1119
|
+
const body = match[2] ?? "";
|
|
1120
|
+
const data = {};
|
|
1121
|
+
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
1122
|
+
const colonIdx = line.indexOf(":");
|
|
1123
|
+
if (colonIdx === -1) continue;
|
|
1124
|
+
const key = line.slice(0, colonIdx).trim();
|
|
1125
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
1126
|
+
if (key) {
|
|
1127
|
+
data[key] = value;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return { data, body };
|
|
1131
|
+
}
|
|
1132
|
+
function isTrustedSkillPath(skillDir, trustedDirs) {
|
|
1133
|
+
const normalized = resolve3(skillDir);
|
|
1134
|
+
for (const trusted of trustedDirs) {
|
|
1135
|
+
const normalizedTrusted = resolve3(trusted);
|
|
1136
|
+
if (normalized === normalizedTrusted || normalized.startsWith(normalizedTrusted + sep3)) {
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
async function fileExists(filePath) {
|
|
1143
|
+
try {
|
|
1144
|
+
await stat3(filePath);
|
|
1145
|
+
return true;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function loadTools(skillDir) {
|
|
1151
|
+
const toolsJsonPath = join5(skillDir, "tools.json");
|
|
1152
|
+
if (!await fileExists(toolsJsonPath)) return [];
|
|
1153
|
+
let raw;
|
|
1154
|
+
try {
|
|
1155
|
+
raw = await readFile6(toolsJsonPath, "utf-8");
|
|
1156
|
+
} catch {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
let parsed;
|
|
1160
|
+
try {
|
|
1161
|
+
parsed = JSON.parse(raw);
|
|
1162
|
+
} catch {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
if (!Array.isArray(parsed)) return [];
|
|
1166
|
+
const tools = [];
|
|
1167
|
+
for (const item of parsed) {
|
|
1168
|
+
if (typeof item === "object" && item !== null && typeof item["name"] === "string" && typeof item["description"] === "string") {
|
|
1169
|
+
const entry = item;
|
|
1170
|
+
tools.push({
|
|
1171
|
+
name: entry["name"],
|
|
1172
|
+
description: entry["description"],
|
|
1173
|
+
parameters: entry["parameters"] ?? {},
|
|
1174
|
+
scriptPath: join5(skillDir, `${entry["name"]}.sh`)
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return tools;
|
|
1179
|
+
}
|
|
1180
|
+
async function loadSkillFile(skillMdPath) {
|
|
1181
|
+
const content = await readFile6(skillMdPath, "utf-8");
|
|
1182
|
+
const { data, body } = parseFrontmatter(content);
|
|
1183
|
+
const skillDir = dirname3(skillMdPath);
|
|
1184
|
+
const dirName = basename(skillDir);
|
|
1185
|
+
const [tools, hasPreScript, hasPostScript] = await Promise.all([
|
|
1186
|
+
loadTools(skillDir),
|
|
1187
|
+
fileExists(join5(skillDir, "pre.sh")),
|
|
1188
|
+
fileExists(join5(skillDir, "post.sh"))
|
|
1189
|
+
]);
|
|
1190
|
+
return {
|
|
1191
|
+
name: data["name"] ?? dirName,
|
|
1192
|
+
description: data["description"] ?? "",
|
|
1193
|
+
body,
|
|
1194
|
+
source: skillMdPath,
|
|
1195
|
+
tools,
|
|
1196
|
+
preScript: hasPreScript ? join5(skillDir, "pre.sh") : null,
|
|
1197
|
+
postScript: hasPostScript ? join5(skillDir, "post.sh") : null
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function loadSkillsFromDir(dir) {
|
|
1201
|
+
let entries;
|
|
1202
|
+
try {
|
|
1203
|
+
entries = await readdir3(dir);
|
|
1204
|
+
} catch {
|
|
1205
|
+
return [];
|
|
1206
|
+
}
|
|
1207
|
+
const skills = [];
|
|
1208
|
+
for (const entry of entries) {
|
|
1209
|
+
const entryPath = join5(dir, entry);
|
|
1210
|
+
let entryStat;
|
|
1211
|
+
try {
|
|
1212
|
+
entryStat = await stat3(entryPath);
|
|
1213
|
+
} catch {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (!entryStat.isDirectory()) continue;
|
|
1217
|
+
const skillMdPath = join5(entryPath, "SKILL.md");
|
|
1218
|
+
try {
|
|
1219
|
+
await stat3(skillMdPath);
|
|
1220
|
+
} catch {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const skill = await loadSkillFile(skillMdPath);
|
|
1224
|
+
skills.push(skill);
|
|
1225
|
+
}
|
|
1226
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/skills/executor.ts
|
|
1230
|
+
var execAsync = promisify(exec2);
|
|
1231
|
+
var SecurityError = class extends Error {
|
|
1232
|
+
constructor(message) {
|
|
1233
|
+
super(message);
|
|
1234
|
+
this.name = "SecurityError";
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
async function runScript(scriptPath, args) {
|
|
1238
|
+
const quotedArgs = args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
1239
|
+
const cmd = quotedArgs ? `${scriptPath} ${quotedArgs}` : scriptPath;
|
|
1240
|
+
const { stdout } = await execAsync(cmd);
|
|
1241
|
+
return stdout;
|
|
1242
|
+
}
|
|
1243
|
+
async function executePreScript(skill, trustedDirs) {
|
|
1244
|
+
if (!skill.preScript) {
|
|
1245
|
+
throw new Error("No pre script configured for this skill");
|
|
1246
|
+
}
|
|
1247
|
+
const skillDir = dirname4(skill.source);
|
|
1248
|
+
if (!isTrustedSkillPath(skillDir, trustedDirs)) {
|
|
1249
|
+
throw new SecurityError(
|
|
1250
|
+
`Untrusted skill path: ${skillDir} is not in trusted dirs`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
return runScript(skill.preScript, []);
|
|
1254
|
+
}
|
|
1255
|
+
async function executeSkillTool(tool, args, trustedDirs) {
|
|
1256
|
+
const scriptDir = dirname4(tool.scriptPath);
|
|
1257
|
+
if (!isTrustedSkillPath(scriptDir, trustedDirs)) {
|
|
1258
|
+
throw new SecurityError(
|
|
1259
|
+
`Untrusted tool script path: ${tool.scriptPath}`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
return runScript(tool.scriptPath, [JSON.stringify(args)]);
|
|
363
1263
|
}
|
|
364
1264
|
|
|
365
1265
|
// src/ui/StatusBar.tsx
|
|
@@ -822,9 +1722,9 @@ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-run
|
|
|
822
1722
|
function SkillAutocomplete({
|
|
823
1723
|
suggestions,
|
|
824
1724
|
selectedIndex,
|
|
825
|
-
isOpen:
|
|
1725
|
+
isOpen: isOpen3
|
|
826
1726
|
}) {
|
|
827
|
-
if (!
|
|
1727
|
+
if (!isOpen3 || suggestions.length === 0) {
|
|
828
1728
|
return /* @__PURE__ */ jsx4(Fragment2, {});
|
|
829
1729
|
}
|
|
830
1730
|
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", children: suggestions.map((skill, idx) => {
|
|
@@ -843,6 +1743,27 @@ function SkillAutocomplete({
|
|
|
843
1743
|
}) });
|
|
844
1744
|
}
|
|
845
1745
|
|
|
1746
|
+
// src/ui/FileAutocomplete.tsx
|
|
1747
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1748
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1749
|
+
function FileAutocomplete({
|
|
1750
|
+
suggestions,
|
|
1751
|
+
selectedIndex,
|
|
1752
|
+
isOpen: isOpen3
|
|
1753
|
+
}) {
|
|
1754
|
+
if (!isOpen3 || suggestions.length === 0) {
|
|
1755
|
+
return /* @__PURE__ */ jsx5(Fragment3, {});
|
|
1756
|
+
}
|
|
1757
|
+
return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", children: suggestions.map((entry, idx) => {
|
|
1758
|
+
const selected = idx === selectedIndex;
|
|
1759
|
+
const label = entry.isDir ? entry.path + "/" : entry.path;
|
|
1760
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1761
|
+
/* @__PURE__ */ jsx5(Text5, { color: selected ? "yellow" : void 0, bold: selected, children: selected ? "> " : " " }),
|
|
1762
|
+
/* @__PURE__ */ jsx5(Text5, { color: selected ? "yellow" : void 0, bold: selected, children: label })
|
|
1763
|
+
] }, entry.path);
|
|
1764
|
+
}) });
|
|
1765
|
+
}
|
|
1766
|
+
|
|
846
1767
|
// src/ui/autocompleteLogic.ts
|
|
847
1768
|
function getInitialState() {
|
|
848
1769
|
return { query: "", selectedIndex: 0, dismissed: false };
|
|
@@ -870,12 +1791,166 @@ function dismiss(state) {
|
|
|
870
1791
|
return { ...state, dismissed: true };
|
|
871
1792
|
}
|
|
872
1793
|
|
|
1794
|
+
// src/ui/fileCompletion.ts
|
|
1795
|
+
import * as fs8 from "fs/promises";
|
|
1796
|
+
import * as path6 from "path";
|
|
1797
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
1798
|
+
function isHidden(name) {
|
|
1799
|
+
return name.startsWith(".");
|
|
1800
|
+
}
|
|
1801
|
+
async function walkDir2(dir, root, results, maxResults) {
|
|
1802
|
+
if (results.length >= maxResults) return;
|
|
1803
|
+
let entries;
|
|
1804
|
+
try {
|
|
1805
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1806
|
+
} catch {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
for (const entry of entries) {
|
|
1810
|
+
if (results.length >= maxResults) return;
|
|
1811
|
+
if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
|
|
1812
|
+
const relPath = path6.relative(root, path6.join(dir, entry.name));
|
|
1813
|
+
if (entry.isDirectory()) {
|
|
1814
|
+
results.push({ path: relPath, isDir: true });
|
|
1815
|
+
await walkDir2(path6.join(dir, entry.name), root, results, maxResults);
|
|
1816
|
+
} else {
|
|
1817
|
+
results.push({ path: relPath, isDir: false });
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async function listFilesForQuery(query, cwd, maxResults = 50) {
|
|
1822
|
+
if (path6.isAbsolute(query)) {
|
|
1823
|
+
return listAbsolute(query, maxResults);
|
|
1824
|
+
}
|
|
1825
|
+
const all = [];
|
|
1826
|
+
await walkDir2(cwd, cwd, all, maxResults * 4);
|
|
1827
|
+
const filtered = query ? all.filter((e) => e.path.includes(query)) : all;
|
|
1828
|
+
return filtered.slice(0, maxResults);
|
|
1829
|
+
}
|
|
1830
|
+
async function listAbsolute(query, maxResults) {
|
|
1831
|
+
let dir = query;
|
|
1832
|
+
let filter = "";
|
|
1833
|
+
try {
|
|
1834
|
+
const stat5 = await fs8.stat(dir);
|
|
1835
|
+
if (!stat5.isDirectory()) {
|
|
1836
|
+
filter = path6.basename(dir);
|
|
1837
|
+
dir = path6.dirname(dir);
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
filter = path6.basename(dir);
|
|
1841
|
+
dir = path6.dirname(dir);
|
|
1842
|
+
}
|
|
1843
|
+
let entries;
|
|
1844
|
+
try {
|
|
1845
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1846
|
+
} catch {
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
const results = [];
|
|
1850
|
+
for (const entry of entries) {
|
|
1851
|
+
if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
|
|
1852
|
+
if (filter && !entry.name.includes(filter)) continue;
|
|
1853
|
+
results.push({
|
|
1854
|
+
path: path6.join(dir, entry.name),
|
|
1855
|
+
isDir: entry.isDirectory()
|
|
1856
|
+
});
|
|
1857
|
+
if (results.length >= maxResults) break;
|
|
1858
|
+
}
|
|
1859
|
+
return results;
|
|
1860
|
+
}
|
|
1861
|
+
function extractAtQuery(text) {
|
|
1862
|
+
const lastAt = text.lastIndexOf("@");
|
|
1863
|
+
if (lastAt === -1) return null;
|
|
1864
|
+
const after = text.slice(lastAt + 1);
|
|
1865
|
+
const spaceIdx = after.indexOf(" ");
|
|
1866
|
+
if (spaceIdx !== -1) return null;
|
|
1867
|
+
return after;
|
|
1868
|
+
}
|
|
1869
|
+
async function expandFileRefs(text, cwd) {
|
|
1870
|
+
const atPattern = /@([\w./\-]+)/g;
|
|
1871
|
+
const replacements = [];
|
|
1872
|
+
let match;
|
|
1873
|
+
atPattern.lastIndex = 0;
|
|
1874
|
+
while ((match = atPattern.exec(text)) !== null) {
|
|
1875
|
+
const filePath = match[1];
|
|
1876
|
+
const fullPath = path6.isAbsolute(filePath) ? filePath : path6.join(cwd, filePath);
|
|
1877
|
+
let replacement;
|
|
1878
|
+
try {
|
|
1879
|
+
const content = await fs8.readFile(fullPath, "utf8");
|
|
1880
|
+
replacement = `\`\`\`
|
|
1881
|
+
// @${filePath}
|
|
1882
|
+
${content}
|
|
1883
|
+
\`\`\``;
|
|
1884
|
+
} catch {
|
|
1885
|
+
replacement = `@${filePath} (not found)`;
|
|
1886
|
+
}
|
|
1887
|
+
replacements.push({ start: match.index, end: match.index + match[0].length, replacement });
|
|
1888
|
+
}
|
|
1889
|
+
if (replacements.length === 0) return text;
|
|
1890
|
+
let result = text;
|
|
1891
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
1892
|
+
const { start, end, replacement } = replacements[i];
|
|
1893
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
1894
|
+
}
|
|
1895
|
+
return result;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/ui/fileAutocompleteLogic.ts
|
|
1899
|
+
function getInitialState2() {
|
|
1900
|
+
return { query: "", selectedIndex: 0, dismissed: false };
|
|
1901
|
+
}
|
|
1902
|
+
function handleInputChange2(_state, newText) {
|
|
1903
|
+
return { query: newText, selectedIndex: 0, dismissed: false };
|
|
1904
|
+
}
|
|
1905
|
+
function isOpen2(state, suggestions) {
|
|
1906
|
+
if (state.dismissed) return false;
|
|
1907
|
+
if (suggestions.length === 0) return false;
|
|
1908
|
+
return extractAtQuery(state.query) !== null;
|
|
1909
|
+
}
|
|
1910
|
+
function moveUp2(state, count) {
|
|
1911
|
+
return { ...state, selectedIndex: (state.selectedIndex - 1 + count) % count };
|
|
1912
|
+
}
|
|
1913
|
+
function moveDown2(state, count) {
|
|
1914
|
+
return { ...state, selectedIndex: (state.selectedIndex + 1) % count };
|
|
1915
|
+
}
|
|
1916
|
+
function dismiss2(state) {
|
|
1917
|
+
return { ...state, dismissed: true };
|
|
1918
|
+
}
|
|
1919
|
+
function confirmSelection(inputText, selectedPath) {
|
|
1920
|
+
const lastAt = inputText.lastIndexOf("@");
|
|
1921
|
+
if (lastAt === -1) return inputText;
|
|
1922
|
+
const prefix = inputText.slice(0, lastAt);
|
|
1923
|
+
return `${prefix}@${selectedPath} `;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/ui/mouseInput.ts
|
|
1927
|
+
var SGR_MOUSE_RE = /^\x1b\[<(\d+);\d+;\d+[Mm]/;
|
|
1928
|
+
function parseMouseScroll(data) {
|
|
1929
|
+
const s = typeof data === "string" ? data : data.toString("binary");
|
|
1930
|
+
if (!s) return null;
|
|
1931
|
+
const sgrMatch = SGR_MOUSE_RE.exec(s);
|
|
1932
|
+
if (sgrMatch) {
|
|
1933
|
+
const cb = parseInt(sgrMatch[1], 10);
|
|
1934
|
+
if (cb === 64) return { direction: "up" };
|
|
1935
|
+
if (cb === 65) return { direction: "down" };
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
if (s.length >= 6 && s.charCodeAt(0) === 27 && s[1] === "[" && s[2] === "M") {
|
|
1939
|
+
const buttonByte = s.charCodeAt(3);
|
|
1940
|
+
if (buttonByte === 96) return { direction: "up" };
|
|
1941
|
+
if (buttonByte === 97) return { direction: "down" };
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
873
1947
|
// src/ui/App.tsx
|
|
874
|
-
import { jsx as
|
|
875
|
-
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2, llmClient }) {
|
|
1948
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1949
|
+
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2, trustedSkillDirs: trustedSkillDirs2 = [], initialMessages: initialMessages2 = [], llmClient }) {
|
|
876
1950
|
const { stdout } = useStdout();
|
|
1951
|
+
const { stdin } = useStdin();
|
|
877
1952
|
const historyMaxHeight = Math.max(5, (stdout?.rows ?? 24) - 4);
|
|
878
|
-
const [messages, setMessages] = useState3(
|
|
1953
|
+
const [messages, setMessages] = useState3(initialMessages2);
|
|
879
1954
|
const [status, setStatus] = useState3("idle");
|
|
880
1955
|
const contextLimit = getContextLimit(config2.model, config2.contextLimit);
|
|
881
1956
|
const [tokenUsage, setTokenUsage] = useState3({
|
|
@@ -899,10 +1974,18 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
899
1974
|
0
|
|
900
1975
|
);
|
|
901
1976
|
}, [messages, expandTools, stdout?.columns]);
|
|
1977
|
+
const totalLinesRef = useRef2(totalLines);
|
|
1978
|
+
totalLinesRef.current = totalLines;
|
|
902
1979
|
const pendingConfirmRef = useRef2(null);
|
|
1980
|
+
const abortControllerRef = useRef2(null);
|
|
903
1981
|
const llmRef = useRef2(llmClient ?? createLLMClient(config2));
|
|
904
1982
|
const inputRef = useRef2(null);
|
|
1983
|
+
const [skillTools, setSkillTools] = useState3([]);
|
|
1984
|
+
const skillToolsRef = useRef2([]);
|
|
1985
|
+
skillToolsRef.current = skillTools;
|
|
905
1986
|
const [acState, setAcState] = useState3(getInitialState());
|
|
1987
|
+
const [fileAcState, setFileAcState] = useState3(getInitialState2());
|
|
1988
|
+
const [fileSuggestions, setFileSuggestions] = useState3([]);
|
|
906
1989
|
const loggerRef = useRef2(null);
|
|
907
1990
|
const loggedCountRef = useRef2(0);
|
|
908
1991
|
useEffect3(() => {
|
|
@@ -927,7 +2010,67 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
927
2010
|
}
|
|
928
2011
|
loggedCountRef.current = messages.length;
|
|
929
2012
|
}, [messages]);
|
|
2013
|
+
useEffect3(() => {
|
|
2014
|
+
if (!stdin || !stdout) return;
|
|
2015
|
+
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
2016
|
+
const onMouseData = (data) => {
|
|
2017
|
+
const event = parseMouseScroll(data);
|
|
2018
|
+
if (!event) return;
|
|
2019
|
+
if (event.direction === "up") {
|
|
2020
|
+
setScrollOffset((prev) => Math.min(prev + 3, Math.max(0, totalLinesRef.current - 1)));
|
|
2021
|
+
} else {
|
|
2022
|
+
setScrollOffset((prev) => Math.max(0, prev - 3));
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
stdin.on("data", onMouseData);
|
|
2026
|
+
return () => {
|
|
2027
|
+
stdout.write("\x1B[?1000l\x1B[?1006l");
|
|
2028
|
+
stdin.off("data", onMouseData);
|
|
2029
|
+
};
|
|
2030
|
+
}, [stdin, stdout]);
|
|
2031
|
+
useEffect3(() => {
|
|
2032
|
+
const atFragment = extractAtQuery(fileAcState.query);
|
|
2033
|
+
if (atFragment === null) {
|
|
2034
|
+
setFileSuggestions([]);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
let cancelled = false;
|
|
2038
|
+
listFilesForQuery(atFragment, process.cwd()).then((entries) => {
|
|
2039
|
+
if (!cancelled) setFileSuggestions(entries);
|
|
2040
|
+
}).catch(() => {
|
|
2041
|
+
});
|
|
2042
|
+
return () => {
|
|
2043
|
+
cancelled = true;
|
|
2044
|
+
};
|
|
2045
|
+
}, [fileAcState.query]);
|
|
930
2046
|
useInput2((input, key) => {
|
|
2047
|
+
if (key.escape && status === "thinking" && abortControllerRef.current) {
|
|
2048
|
+
abortControllerRef.current.abort();
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const fileOpen = isOpen2(fileAcState, fileSuggestions);
|
|
2052
|
+
if (fileOpen) {
|
|
2053
|
+
if (key.upArrow) {
|
|
2054
|
+
setFileAcState((prev) => moveUp2(prev, fileSuggestions.length));
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (key.downArrow) {
|
|
2058
|
+
setFileAcState((prev) => moveDown2(prev, fileSuggestions.length));
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
if (key.escape) {
|
|
2062
|
+
setFileAcState((prev) => dismiss2(prev));
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (key.tab) {
|
|
2066
|
+
const selected = fileSuggestions[fileAcState.selectedIndex];
|
|
2067
|
+
if (selected) {
|
|
2068
|
+
const newText = confirmSelection(fileAcState.query, selected.path);
|
|
2069
|
+
inputRef.current?.fill(newText);
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
931
2074
|
const skillList = registry2?.list() ?? [];
|
|
932
2075
|
const suggestions = computeSuggestions(skillList, acState);
|
|
933
2076
|
const open = isOpen(acState, suggestions);
|
|
@@ -999,10 +2142,10 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
999
2142
|
}
|
|
1000
2143
|
});
|
|
1001
2144
|
const confirm = useCallback((prompt) => {
|
|
1002
|
-
return new Promise((
|
|
2145
|
+
return new Promise((resolve5) => {
|
|
1003
2146
|
setStatus("awaiting_confirm");
|
|
1004
2147
|
setConfirmPrompt(prompt);
|
|
1005
|
-
pendingConfirmRef.current = { resolve:
|
|
2148
|
+
pendingConfirmRef.current = { resolve: resolve5 };
|
|
1006
2149
|
});
|
|
1007
2150
|
}, []);
|
|
1008
2151
|
const runLlmLoop = useCallback(
|
|
@@ -1017,44 +2160,69 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1017
2160
|
// autoMode=true 时自动同意 normal 级工具调用,无需用户确认
|
|
1018
2161
|
autoApproveNormal: autoMode2
|
|
1019
2162
|
};
|
|
2163
|
+
const abortController = new AbortController();
|
|
2164
|
+
abortControllerRef.current = abortController;
|
|
1020
2165
|
let currentMessages = history;
|
|
1021
2166
|
let continueLoop = true;
|
|
1022
|
-
|
|
2167
|
+
let wasAborted = false;
|
|
2168
|
+
while (continueLoop && !wasAborted) {
|
|
1023
2169
|
continueLoop = false;
|
|
1024
2170
|
setStatus("thinking");
|
|
1025
2171
|
let assistantText = "";
|
|
1026
2172
|
let assistantReasoning;
|
|
1027
2173
|
const toolCalls = [];
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
2174
|
+
const dynamicTools = skillToolsRef.current.map((t) => ({
|
|
2175
|
+
type: "function",
|
|
2176
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
2177
|
+
}));
|
|
2178
|
+
try {
|
|
2179
|
+
for await (const chunk of llmRef.current.stream(currentMessages, [BASH_TOOL, READ_TOOL, WRITE_TOOL, EDIT_TOOL, GLOB_TOOL, GREP_TOOL, APPLY_PATCH_TOOL, TODO_TOOL, ...dynamicTools], abortController.signal)) {
|
|
2180
|
+
if (chunk.text) {
|
|
2181
|
+
assistantText += chunk.text;
|
|
2182
|
+
setMessages((prev) => {
|
|
2183
|
+
const last = prev[prev.length - 1];
|
|
2184
|
+
if (last?.role === "assistant" && !last.tool_calls) {
|
|
2185
|
+
return [...prev.slice(0, -1), { ...last, content: assistantText }];
|
|
2186
|
+
}
|
|
2187
|
+
return [...prev, { role: "assistant", content: assistantText }];
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
if (chunk.done) {
|
|
2191
|
+
if (chunk.toolCalls) toolCalls.push(...chunk.toolCalls);
|
|
2192
|
+
if (chunk.reasoning) assistantReasoning = chunk.reasoning;
|
|
2193
|
+
if (chunk.usage) {
|
|
2194
|
+
setTokenUsage({
|
|
2195
|
+
used: chunk.usage.totalTokens,
|
|
2196
|
+
estimated: false,
|
|
2197
|
+
limit: getContextLimit(config2.model, config2.contextLimit)
|
|
2198
|
+
});
|
|
1035
2199
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
2200
|
+
} else {
|
|
2201
|
+
const estimatedUsed = Math.floor(
|
|
2202
|
+
currentMessages.reduce(
|
|
2203
|
+
(acc, m) => acc + (typeof m.content === "string" ? m.content.length : 0),
|
|
2204
|
+
0
|
|
2205
|
+
) * 0.25 + assistantText.length * 0.25
|
|
2206
|
+
);
|
|
2207
|
+
setTokenUsage((prev) => ({ ...prev, used: estimatedUsed, estimated: true }));
|
|
2208
|
+
}
|
|
1038
2209
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
limit: getContextLimit(config2.model, config2.contextLimit)
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
2212
|
+
if (assistantText) {
|
|
2213
|
+
setMessages((prev) => {
|
|
2214
|
+
const last = prev[prev.length - 1];
|
|
2215
|
+
const partial = { role: "assistant", content: assistantText };
|
|
2216
|
+
return last?.role === "assistant" && !last.tool_calls ? [...prev.slice(0, -1), partial] : [...prev, partial];
|
|
1047
2217
|
});
|
|
1048
2218
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
) * 0.25 + assistantText.length * 0.25
|
|
1055
|
-
);
|
|
1056
|
-
setTokenUsage((prev) => ({ ...prev, used: estimatedUsed, estimated: true }));
|
|
2219
|
+
wasAborted = true;
|
|
2220
|
+
setStatus("idle");
|
|
2221
|
+
setToolName(void 0);
|
|
2222
|
+
abortControllerRef.current = null;
|
|
2223
|
+
return;
|
|
1057
2224
|
}
|
|
2225
|
+
throw err;
|
|
1058
2226
|
}
|
|
1059
2227
|
if (toolCalls.length > 0) {
|
|
1060
2228
|
const assistantMsg = {
|
|
@@ -1077,6 +2245,8 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1077
2245
|
currentMessages = [...currentMessages, assistantMsg];
|
|
1078
2246
|
setStatus("tool_calling");
|
|
1079
2247
|
for (const tc of toolCalls) {
|
|
2248
|
+
setToolName(tc.name);
|
|
2249
|
+
let toolResult;
|
|
1080
2250
|
if (tc.name === "bash") {
|
|
1081
2251
|
let args;
|
|
1082
2252
|
try {
|
|
@@ -1084,16 +2254,48 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1084
2254
|
} catch {
|
|
1085
2255
|
args = { command: "" };
|
|
1086
2256
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
2257
|
+
toolResult = await handleBashTool(args.command, deps);
|
|
2258
|
+
} else if (tc.name === "read") {
|
|
2259
|
+
const args = JSON.parse(tc.arguments);
|
|
2260
|
+
toolResult = await readFile2(args);
|
|
2261
|
+
} else if (tc.name === "write") {
|
|
2262
|
+
const args = JSON.parse(tc.arguments);
|
|
2263
|
+
toolResult = await writeFile2(args);
|
|
2264
|
+
} else if (tc.name === "edit") {
|
|
2265
|
+
const args = JSON.parse(tc.arguments);
|
|
2266
|
+
toolResult = await editFile(args);
|
|
2267
|
+
} else if (tc.name === "glob") {
|
|
2268
|
+
const args = JSON.parse(tc.arguments);
|
|
2269
|
+
toolResult = await globFiles(args);
|
|
2270
|
+
} else if (tc.name === "grep") {
|
|
2271
|
+
const args = JSON.parse(tc.arguments);
|
|
2272
|
+
toolResult = await grepFiles(args);
|
|
2273
|
+
} else if (tc.name === "apply_patch") {
|
|
2274
|
+
const args = JSON.parse(tc.arguments);
|
|
2275
|
+
toolResult = await applyPatch(args);
|
|
2276
|
+
} else if (tc.name === "todo") {
|
|
2277
|
+
const args = JSON.parse(tc.arguments);
|
|
2278
|
+
toolResult = todo(args);
|
|
2279
|
+
} else {
|
|
2280
|
+
const skillTool = skillToolsRef.current.find((t) => t.name === tc.name);
|
|
2281
|
+
if (skillTool) {
|
|
2282
|
+
let toolArgs = {};
|
|
2283
|
+
try {
|
|
2284
|
+
toolArgs = JSON.parse(tc.arguments);
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2287
|
+
toolResult = await executeSkillTool(skillTool, toolArgs, trustedSkillDirs2);
|
|
2288
|
+
} else {
|
|
2289
|
+
toolResult = `Unknown tool: ${tc.name}`;
|
|
2290
|
+
}
|
|
1096
2291
|
}
|
|
2292
|
+
const toolMsg = {
|
|
2293
|
+
role: "tool",
|
|
2294
|
+
tool_call_id: tc.id,
|
|
2295
|
+
content: toolResult
|
|
2296
|
+
};
|
|
2297
|
+
setMessages((prev) => [...prev, toolMsg]);
|
|
2298
|
+
currentMessages = [...currentMessages, toolMsg];
|
|
1097
2299
|
}
|
|
1098
2300
|
continueLoop = true;
|
|
1099
2301
|
} else {
|
|
@@ -1123,11 +2325,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1123
2325
|
}
|
|
1124
2326
|
setStatus("idle");
|
|
1125
2327
|
setToolName(void 0);
|
|
2328
|
+
abortControllerRef.current = null;
|
|
1126
2329
|
},
|
|
1127
2330
|
[confirm, config2.dangerousPatterns, autoMode2]
|
|
1128
2331
|
);
|
|
1129
2332
|
const handleSubmit = useCallback(
|
|
1130
|
-
(text) => {
|
|
2333
|
+
async (text) => {
|
|
1131
2334
|
const trimmed = text.trim();
|
|
1132
2335
|
if (status === "awaiting_confirm") {
|
|
1133
2336
|
const pending = pendingConfirmRef.current;
|
|
@@ -1153,6 +2356,18 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1153
2356
|
return;
|
|
1154
2357
|
}
|
|
1155
2358
|
if (skillResult.type === "skill") {
|
|
2359
|
+
const { skill } = skillResult;
|
|
2360
|
+
setSkillTools(skill.tools);
|
|
2361
|
+
if (skill.preScript) {
|
|
2362
|
+
try {
|
|
2363
|
+
await executePreScript(skill, trustedSkillDirs2);
|
|
2364
|
+
} catch (preErr) {
|
|
2365
|
+
setMessages((prev) => [
|
|
2366
|
+
...prev,
|
|
2367
|
+
{ role: "assistant", content: `[pre.sh error] ${String(preErr)}` }
|
|
2368
|
+
]);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
1156
2371
|
const nextMessages2 = [...messages, skillResult.message];
|
|
1157
2372
|
setMessages(nextMessages2);
|
|
1158
2373
|
runLlmLoop(nextMessages2).catch((err) => {
|
|
@@ -1166,7 +2381,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1166
2381
|
return;
|
|
1167
2382
|
}
|
|
1168
2383
|
}
|
|
1169
|
-
|
|
2384
|
+
let content = trimmed;
|
|
2385
|
+
try {
|
|
2386
|
+
content = await expandFileRefs(trimmed, process.cwd());
|
|
2387
|
+
} catch {
|
|
2388
|
+
}
|
|
2389
|
+
const userMsg = { role: "user", content };
|
|
1170
2390
|
const nextMessages = [...messages, userMsg];
|
|
1171
2391
|
setMessages(nextMessages);
|
|
1172
2392
|
runLlmLoop(nextMessages).catch((err) => {
|
|
@@ -1174,7 +2394,6 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1174
2394
|
setToolName(void 0);
|
|
1175
2395
|
setMessages((prev) => [
|
|
1176
2396
|
...prev,
|
|
1177
|
-
// 错误消息前缀 "[error]" 便于用户识别和日志筛选
|
|
1178
2397
|
{ role: "assistant", content: `[error] ${String(err)}` }
|
|
1179
2398
|
]);
|
|
1180
2399
|
});
|
|
@@ -1190,12 +2409,13 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1190
2409
|
}
|
|
1191
2410
|
if (status !== "awaiting_confirm") {
|
|
1192
2411
|
setAcState((prev) => handleInputChange(prev, text));
|
|
2412
|
+
setFileAcState((prev) => handleInputChange2(prev, text));
|
|
1193
2413
|
}
|
|
1194
2414
|
}, [status]);
|
|
1195
2415
|
const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
|
|
1196
2416
|
const acOpen = isOpen(acState, skillSuggestions);
|
|
1197
|
-
return /* @__PURE__ */
|
|
1198
|
-
/* @__PURE__ */
|
|
2417
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: "100%", children: [
|
|
2418
|
+
/* @__PURE__ */ jsx6(Box6, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-end", children: /* @__PURE__ */ jsx6(
|
|
1199
2419
|
ConversationHistory,
|
|
1200
2420
|
{
|
|
1201
2421
|
messages,
|
|
@@ -1204,8 +2424,8 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1204
2424
|
terminalWidth: stdout?.columns,
|
|
1205
2425
|
scrollOffset
|
|
1206
2426
|
}
|
|
1207
|
-
),
|
|
1208
|
-
/* @__PURE__ */
|
|
2427
|
+
) }),
|
|
2428
|
+
/* @__PURE__ */ jsx6(
|
|
1209
2429
|
StatusBar,
|
|
1210
2430
|
{
|
|
1211
2431
|
status,
|
|
@@ -1215,7 +2435,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1215
2435
|
tokenUsage
|
|
1216
2436
|
}
|
|
1217
2437
|
),
|
|
1218
|
-
/* @__PURE__ */
|
|
2438
|
+
/* @__PURE__ */ jsx6(
|
|
1219
2439
|
SkillAutocomplete,
|
|
1220
2440
|
{
|
|
1221
2441
|
suggestions: skillSuggestions,
|
|
@@ -1223,7 +2443,15 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1223
2443
|
isOpen: acOpen
|
|
1224
2444
|
}
|
|
1225
2445
|
),
|
|
1226
|
-
/* @__PURE__ */
|
|
2446
|
+
/* @__PURE__ */ jsx6(
|
|
2447
|
+
FileAutocomplete,
|
|
2448
|
+
{
|
|
2449
|
+
suggestions: fileSuggestions,
|
|
2450
|
+
selectedIndex: fileAcState.selectedIndex,
|
|
2451
|
+
isOpen: isOpen2(fileAcState, fileSuggestions)
|
|
2452
|
+
}
|
|
2453
|
+
),
|
|
2454
|
+
/* @__PURE__ */ jsx6(
|
|
1227
2455
|
Input_default,
|
|
1228
2456
|
{
|
|
1229
2457
|
ref: inputRef,
|
|
@@ -1236,6 +2464,64 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
1236
2464
|
] });
|
|
1237
2465
|
}
|
|
1238
2466
|
|
|
2467
|
+
// src/replay.ts
|
|
2468
|
+
var KNOWN_ROLES = /* @__PURE__ */ new Set(["user", "assistant", "tool", "system"]);
|
|
2469
|
+
function entryToMessage(entry) {
|
|
2470
|
+
const { role, content } = entry;
|
|
2471
|
+
if (typeof role !== "string" || !KNOWN_ROLES.has(role)) return null;
|
|
2472
|
+
if (role === "user" || role === "system") {
|
|
2473
|
+
return { role, content: content ?? "" };
|
|
2474
|
+
}
|
|
2475
|
+
if (role === "assistant") {
|
|
2476
|
+
const msg = {
|
|
2477
|
+
role: "assistant",
|
|
2478
|
+
content: content ?? null
|
|
2479
|
+
};
|
|
2480
|
+
if (Array.isArray(entry.tool_calls)) {
|
|
2481
|
+
msg.tool_calls = entry.tool_calls;
|
|
2482
|
+
}
|
|
2483
|
+
return msg;
|
|
2484
|
+
}
|
|
2485
|
+
if (role === "tool") {
|
|
2486
|
+
if (typeof entry.tool_call_id !== "string") return null;
|
|
2487
|
+
return {
|
|
2488
|
+
role: "tool",
|
|
2489
|
+
tool_call_id: entry.tool_call_id,
|
|
2490
|
+
content: content ?? ""
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
function parseReplayLog(content) {
|
|
2496
|
+
const messages = [];
|
|
2497
|
+
for (const line of content.split("\n")) {
|
|
2498
|
+
const trimmed = line.trim();
|
|
2499
|
+
if (!trimmed) continue;
|
|
2500
|
+
let entry;
|
|
2501
|
+
try {
|
|
2502
|
+
entry = JSON.parse(trimmed);
|
|
2503
|
+
} catch {
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
const msg = entryToMessage(entry);
|
|
2507
|
+
if (msg) messages.push(msg);
|
|
2508
|
+
}
|
|
2509
|
+
return messages;
|
|
2510
|
+
}
|
|
2511
|
+
function truncateToTurn(messages, turn) {
|
|
2512
|
+
if (turn <= 0) return [];
|
|
2513
|
+
let userCount = 0;
|
|
2514
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2515
|
+
if (messages[i].role === "user") {
|
|
2516
|
+
userCount++;
|
|
2517
|
+
if (userCount > turn) {
|
|
2518
|
+
return messages.slice(0, i);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return messages.slice();
|
|
2523
|
+
}
|
|
2524
|
+
|
|
1239
2525
|
// src/skills/registry.ts
|
|
1240
2526
|
var SkillRegistry = class {
|
|
1241
2527
|
skills = /* @__PURE__ */ new Map();
|
|
@@ -1259,67 +2545,32 @@ var SkillRegistry = class {
|
|
|
1259
2545
|
}
|
|
1260
2546
|
};
|
|
1261
2547
|
|
|
1262
|
-
// src/
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
function parseFrontmatter(content) {
|
|
1266
|
-
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1267
|
-
const match = FRONTMATTER_RE.exec(content);
|
|
1268
|
-
if (!match) {
|
|
1269
|
-
return { data: {}, body: content };
|
|
1270
|
-
}
|
|
1271
|
-
const rawFrontmatter = match[1];
|
|
1272
|
-
const body = match[2] ?? "";
|
|
1273
|
-
const data = {};
|
|
1274
|
-
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
1275
|
-
const colonIdx = line.indexOf(":");
|
|
1276
|
-
if (colonIdx === -1) continue;
|
|
1277
|
-
const key = line.slice(0, colonIdx).trim();
|
|
1278
|
-
const value = line.slice(colonIdx + 1).trim();
|
|
1279
|
-
if (key) {
|
|
1280
|
-
data[key] = value;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
return { data, body };
|
|
2548
|
+
// src/pipe.ts
|
|
2549
|
+
function emit(out, event) {
|
|
2550
|
+
out.write(JSON.stringify(event) + "\n");
|
|
1284
2551
|
}
|
|
1285
|
-
async function
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
name: data["name"] ?? dirName,
|
|
1291
|
-
description: data["description"] ?? "",
|
|
1292
|
-
body,
|
|
1293
|
-
source: skillMdPath
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
async function loadSkillsFromDir(dir) {
|
|
1297
|
-
let entries;
|
|
1298
|
-
try {
|
|
1299
|
-
entries = await readdir(dir);
|
|
1300
|
-
} catch {
|
|
1301
|
-
return [];
|
|
1302
|
-
}
|
|
1303
|
-
const skills = [];
|
|
1304
|
-
for (const entry of entries) {
|
|
1305
|
-
const entryPath = join3(dir, entry);
|
|
1306
|
-
let entryStat;
|
|
1307
|
-
try {
|
|
1308
|
-
entryStat = await stat(entryPath);
|
|
1309
|
-
} catch {
|
|
1310
|
-
continue;
|
|
2552
|
+
async function runPipe(prompt, llm, out = process.stdout) {
|
|
2553
|
+
const messages = [{ role: "user", content: prompt }];
|
|
2554
|
+
for await (const chunk of llm.stream(messages)) {
|
|
2555
|
+
if (chunk.text) {
|
|
2556
|
+
emit(out, { type: "chunk", text: chunk.text });
|
|
1311
2557
|
}
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
await stat(skillMdPath);
|
|
1316
|
-
} catch {
|
|
1317
|
-
continue;
|
|
2558
|
+
if (chunk.done) {
|
|
2559
|
+
const doneEvent = chunk.usage ? { type: "done", usage: chunk.usage } : { type: "done" };
|
|
2560
|
+
emit(out, doneEvent);
|
|
1318
2561
|
}
|
|
1319
|
-
const skill = await loadSkillFile(skillMdPath);
|
|
1320
|
-
skills.push(skill);
|
|
1321
2562
|
}
|
|
1322
|
-
|
|
2563
|
+
}
|
|
2564
|
+
function readStdin() {
|
|
2565
|
+
return new Promise((resolve5, reject) => {
|
|
2566
|
+
let data = "";
|
|
2567
|
+
process.stdin.setEncoding("utf-8");
|
|
2568
|
+
process.stdin.on("data", (chunk) => {
|
|
2569
|
+
data += chunk;
|
|
2570
|
+
});
|
|
2571
|
+
process.stdin.on("end", () => resolve5(data.trim()));
|
|
2572
|
+
process.stdin.on("error", reject);
|
|
2573
|
+
});
|
|
1323
2574
|
}
|
|
1324
2575
|
|
|
1325
2576
|
// src/index.ts
|
|
@@ -1329,6 +2580,9 @@ var VERSION = version;
|
|
|
1329
2580
|
var rawArgs = process.argv.slice(2);
|
|
1330
2581
|
var autoMode = false;
|
|
1331
2582
|
var cliLogDir;
|
|
2583
|
+
var replayFile;
|
|
2584
|
+
var forkSpec;
|
|
2585
|
+
var pipeMode = false;
|
|
1332
2586
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
1333
2587
|
const arg = rawArgs[i];
|
|
1334
2588
|
if (arg === "-v" || arg === "--version") {
|
|
@@ -1345,6 +2599,30 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
1345
2599
|
i++;
|
|
1346
2600
|
}
|
|
1347
2601
|
}
|
|
2602
|
+
if (arg === "--replay") {
|
|
2603
|
+
const next = rawArgs[i + 1];
|
|
2604
|
+
if (next && !next.startsWith("-")) {
|
|
2605
|
+
replayFile = next;
|
|
2606
|
+
i++;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (arg === "--pipe") {
|
|
2610
|
+
pipeMode = true;
|
|
2611
|
+
}
|
|
2612
|
+
if (arg === "--fork") {
|
|
2613
|
+
const next = rawArgs[i + 1];
|
|
2614
|
+
if (next && !next.startsWith("-")) {
|
|
2615
|
+
const colonIdx = next.lastIndexOf(":");
|
|
2616
|
+
if (colonIdx > 0) {
|
|
2617
|
+
const file = next.slice(0, colonIdx);
|
|
2618
|
+
const turn = parseInt(next.slice(colonIdx + 1), 10);
|
|
2619
|
+
if (!isNaN(turn)) {
|
|
2620
|
+
forkSpec = { file, turn };
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
i++;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
1348
2626
|
}
|
|
1349
2627
|
var config = loadConfig();
|
|
1350
2628
|
var finalConfig = cliLogDir ? { ...config, logDir: cliLogDir } : config;
|
|
@@ -1354,15 +2632,40 @@ if (!finalConfig.apiKey) {
|
|
|
1354
2632
|
);
|
|
1355
2633
|
process.exit(1);
|
|
1356
2634
|
}
|
|
1357
|
-
var
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
2635
|
+
var initialMessages = [];
|
|
2636
|
+
if (replayFile) {
|
|
2637
|
+
try {
|
|
2638
|
+
const raw = readFileSync2(replayFile, "utf-8");
|
|
2639
|
+
initialMessages = parseReplayLog(raw);
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
console.error(`Error reading replay file: ${err}`);
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
} else if (forkSpec) {
|
|
2645
|
+
try {
|
|
2646
|
+
const raw = readFileSync2(forkSpec.file, "utf-8");
|
|
2647
|
+
initialMessages = truncateToTurn(parseReplayLog(raw), forkSpec.turn);
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
console.error(`Error reading fork file: ${err}`);
|
|
2650
|
+
process.exit(1);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
var __dirname = dirname6(fileURLToPath(import.meta.url));
|
|
2654
|
+
var builtinSkillsDir = resolve4(__dirname, "../skills");
|
|
2655
|
+
var userSkillsDir = resolve4(process.env.HOME ?? "~", ".ecode/skills");
|
|
2656
|
+
var projectSkillsDir = resolve4(process.cwd(), ".ecode/skills");
|
|
1361
2657
|
var registry = new SkillRegistry();
|
|
1362
2658
|
for (const dir of [builtinSkillsDir, userSkillsDir, projectSkillsDir]) {
|
|
1363
2659
|
const skills = await loadSkillsFromDir(dir);
|
|
1364
2660
|
for (const skill of skills) registry.register(skill);
|
|
1365
2661
|
}
|
|
2662
|
+
var trustedSkillDirs = [builtinSkillsDir, userSkillsDir, projectSkillsDir];
|
|
2663
|
+
if (pipeMode) {
|
|
2664
|
+
const prompt = await readStdin();
|
|
2665
|
+
const llm = createLLMClient(finalConfig);
|
|
2666
|
+
await runPipe(prompt, llm);
|
|
2667
|
+
process.exit(0);
|
|
2668
|
+
}
|
|
1366
2669
|
if (process.stdout.isTTY) {
|
|
1367
2670
|
process.stdout.write("\x1B[?1049h");
|
|
1368
2671
|
const exitAltScreen = () => process.stdout.write("\x1B[?1049l");
|
|
@@ -1380,4 +2683,4 @@ if (process.stdout.isTTY) {
|
|
|
1380
2683
|
process.exit(0);
|
|
1381
2684
|
});
|
|
1382
2685
|
}
|
|
1383
|
-
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry }));
|
|
2686
|
+
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry, trustedSkillDirs, initialMessages }));
|