@yeaft/webchat-agent 1.0.208 → 1.0.210
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/connection/message-router.js +5 -1
- package/index.js +1 -1
- package/local-runtime/server/handlers/agent-output.js +18 -0
- package/local-runtime/server/handlers/client-conversation.js +18 -0
- package/local-runtime/server/ws-agent.js +1 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +137 -115
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +124 -18
- package/yeaft/engine.js +6 -2
- package/yeaft/tools/bash.js +6 -5
- package/yeaft/tools/file-read.js +83 -15
- package/yeaft/tools/glob.js +26 -25
- package/yeaft/tools/grep.js +178 -89
- package/yeaft/tools/mcp-tools.js +11 -2
- package/yeaft/tools/read-task-log.js +4 -3
- package/yeaft/tools/registry.js +38 -13
- package/yeaft/tools/types.js +4 -0
- package/yeaft/web-bridge.js +42 -10
|
Binary file
|
package/package.json
CHANGED
|
@@ -1577,40 +1577,72 @@ export class ConversationStore {
|
|
|
1577
1577
|
const limit = Math.min(50, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 20));
|
|
1578
1578
|
const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
|
|
1579
1579
|
const results = [];
|
|
1580
|
-
const seen = new Set();
|
|
1581
1580
|
let hasMore = false;
|
|
1582
1581
|
|
|
1583
|
-
for (const
|
|
1584
|
-
|
|
1585
|
-
if (message.role !== 'user' && message.role !== 'assistant') continue;
|
|
1586
|
-
if (!message.id || seen.has(message.id)) continue;
|
|
1587
|
-
seen.add(message.id);
|
|
1588
|
-
|
|
1589
|
-
const text = this.#visibleSearchText(message.content);
|
|
1582
|
+
for (const entry of this.#iterateVisibleResponseEntries(sessionId, { beforeSeq })) {
|
|
1583
|
+
const text = entry.textParts.join(' ');
|
|
1590
1584
|
const matchIndex = text.toLocaleLowerCase().indexOf(needle);
|
|
1591
1585
|
if (matchIndex < 0) continue;
|
|
1592
1586
|
if (results.length >= limit) {
|
|
1593
1587
|
hasMore = true;
|
|
1594
1588
|
break;
|
|
1595
1589
|
}
|
|
1596
|
-
|
|
1597
|
-
const seq = parseSeqFromId(message.id);
|
|
1598
|
-
if (!Number.isFinite(seq)) continue;
|
|
1599
1590
|
results.push({
|
|
1600
|
-
|
|
1601
|
-
turnId: message.turnId || message.threadId || message.id,
|
|
1602
|
-
seq,
|
|
1603
|
-
role: message.role,
|
|
1604
|
-
speakerVpId: message.speakerVpId || null,
|
|
1605
|
-
timestamp: message.ts || message.time || null,
|
|
1591
|
+
...this.#projectVisibleResponseEntry(entry),
|
|
1606
1592
|
snippet: this.#searchSnippet(text, matchIndex, needle.length),
|
|
1607
1593
|
});
|
|
1608
1594
|
}
|
|
1609
1595
|
|
|
1596
|
+
const lastResult = results[results.length - 1] || null;
|
|
1597
|
+
return {
|
|
1598
|
+
results: results.map(({ _beforeSeq, ...result }) => result),
|
|
1599
|
+
hasMore,
|
|
1600
|
+
nextBeforeSeq: hasMore && lastResult ? lastResult._beforeSeq : null,
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Load a lightweight outline page for one Session. Only user and assistant
|
|
1606
|
+
* text metadata is projected; tool payloads, attachments and full message
|
|
1607
|
+
* bodies never leave the Agent through this API.
|
|
1608
|
+
*
|
|
1609
|
+
* @param {string} sessionId
|
|
1610
|
+
* @param {{ limit?: number, beforeSeq?: number|null, includeTotal?: boolean }} [opts]
|
|
1611
|
+
* @returns {{ results: object[], hasMore: boolean, nextBeforeSeq: number|null, totalCount: number|null }}
|
|
1612
|
+
*/
|
|
1613
|
+
loadVisibleOutlineBySession(sessionId, opts = {}) {
|
|
1614
|
+
if (!sessionId) return { results: [], hasMore: false, nextBeforeSeq: null, totalCount: 0 };
|
|
1615
|
+
|
|
1616
|
+
const limit = Math.min(100, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 50));
|
|
1617
|
+
const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
|
|
1618
|
+
const newestFirst = [];
|
|
1619
|
+
let hasMore = false;
|
|
1620
|
+
|
|
1621
|
+
for (const entry of this.#iterateVisibleResponseEntries(sessionId, { beforeSeq })) {
|
|
1622
|
+
if (newestFirst.length >= limit) {
|
|
1623
|
+
hasMore = true;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
const projected = this.#projectVisibleResponseEntry(entry);
|
|
1627
|
+
newestFirst.push({
|
|
1628
|
+
...projected,
|
|
1629
|
+
snippet: this.#outlineSnippet(entry.textParts.join(' ')),
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
let totalCount = null;
|
|
1634
|
+
if (opts.includeTotal !== false) {
|
|
1635
|
+
totalCount = 0;
|
|
1636
|
+
for (const _entry of this.#iterateVisibleResponseEntries(sessionId)) totalCount += 1;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const oldestEntry = newestFirst[newestFirst.length - 1] || null;
|
|
1640
|
+
const results = newestFirst.reverse().map(({ _beforeSeq, ...entry }) => entry);
|
|
1610
1641
|
return {
|
|
1611
1642
|
results,
|
|
1612
1643
|
hasMore,
|
|
1613
|
-
nextBeforeSeq: hasMore &&
|
|
1644
|
+
nextBeforeSeq: hasMore && oldestEntry ? oldestEntry._beforeSeq : null,
|
|
1645
|
+
totalCount,
|
|
1614
1646
|
};
|
|
1615
1647
|
}
|
|
1616
1648
|
|
|
@@ -2381,6 +2413,75 @@ export class ConversationStore {
|
|
|
2381
2413
|
};
|
|
2382
2414
|
}
|
|
2383
2415
|
|
|
2416
|
+
*#iterateVisibleResponseEntries(sessionId, opts = {}) {
|
|
2417
|
+
const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
|
|
2418
|
+
const seen = new Set();
|
|
2419
|
+
let current = null;
|
|
2420
|
+
|
|
2421
|
+
const visibleRow = (message) => {
|
|
2422
|
+
if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) return null;
|
|
2423
|
+
if (message.role !== 'user' && message.role !== 'assistant') return null;
|
|
2424
|
+
if (!message.id || seen.has(message.id)) return null;
|
|
2425
|
+
seen.add(message.id);
|
|
2426
|
+
const seq = parseSeqFromId(message.id);
|
|
2427
|
+
if (!Number.isFinite(seq)) return null;
|
|
2428
|
+
const text = this.#visibleSearchText(message.content);
|
|
2429
|
+
const speakerVpId = message.speakerVpId || null;
|
|
2430
|
+
return {
|
|
2431
|
+
message,
|
|
2432
|
+
seq,
|
|
2433
|
+
text,
|
|
2434
|
+
speakerVpId,
|
|
2435
|
+
groupKey: message.role === 'assistant'
|
|
2436
|
+
? `assistant:${message.turnId || message.id}:${speakerVpId || ''}`
|
|
2437
|
+
: `user:${message.id}`,
|
|
2438
|
+
};
|
|
2439
|
+
};
|
|
2440
|
+
const startEntry = (row) => ({
|
|
2441
|
+
groupKey: row.groupKey,
|
|
2442
|
+
role: row.message.role,
|
|
2443
|
+
turnId: row.message.turnId || row.message.threadId || row.message.id,
|
|
2444
|
+
speakerVpId: row.speakerVpId,
|
|
2445
|
+
oldestSeq: row.seq,
|
|
2446
|
+
anchor: row,
|
|
2447
|
+
anchorHasText: !!row.text,
|
|
2448
|
+
textParts: row.text ? [row.text] : [],
|
|
2449
|
+
});
|
|
2450
|
+
const mergeRow = (entry, row) => {
|
|
2451
|
+
entry.oldestSeq = Math.min(entry.oldestSeq, row.seq);
|
|
2452
|
+
if (row.text) entry.textParts.unshift(row.text);
|
|
2453
|
+
if (!entry.anchorHasText && row.text) {
|
|
2454
|
+
entry.anchor = row;
|
|
2455
|
+
entry.anchorHasText = true;
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
|
|
2459
|
+
for (const message of this.#iterateSessionRows(sessionId, { beforeSeq, desc: true })) {
|
|
2460
|
+
const row = visibleRow(message);
|
|
2461
|
+
if (!row) continue;
|
|
2462
|
+
if (current && current.groupKey === row.groupKey) {
|
|
2463
|
+
mergeRow(current, row);
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
if (current) yield current;
|
|
2467
|
+
current = startEntry(row);
|
|
2468
|
+
}
|
|
2469
|
+
if (current) yield current;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
#projectVisibleResponseEntry(entry) {
|
|
2473
|
+
return {
|
|
2474
|
+
messageId: entry.anchor.message.id,
|
|
2475
|
+
...(entry.anchor.message.clientMessageId ? { clientMessageId: entry.anchor.message.clientMessageId } : {}),
|
|
2476
|
+
turnId: entry.turnId,
|
|
2477
|
+
seq: entry.anchor.seq,
|
|
2478
|
+
role: entry.role,
|
|
2479
|
+
speakerVpId: entry.speakerVpId,
|
|
2480
|
+
timestamp: entry.anchor.message.ts || entry.anchor.message.time || null,
|
|
2481
|
+
_beforeSeq: entry.oldestSeq,
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2384
2485
|
#visibleSearchText(content) {
|
|
2385
2486
|
if (typeof content === 'string') return content.replace(/\s+/g, ' ').trim();
|
|
2386
2487
|
if (!Array.isArray(content)) return '';
|
|
@@ -2399,6 +2500,11 @@ export class ConversationStore {
|
|
|
2399
2500
|
return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
|
|
2400
2501
|
}
|
|
2401
2502
|
|
|
2503
|
+
#outlineSnippet(text) {
|
|
2504
|
+
const limit = 180;
|
|
2505
|
+
return text.length > limit ? `${text.slice(0, limit).trimEnd()}…` : text;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2402
2508
|
#readSegmentRows(conversationDir, opts = {}) {
|
|
2403
2509
|
return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
|
|
2404
2510
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -46,7 +46,7 @@ import { countTurns } from './turn-utils.js';
|
|
|
46
46
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
47
47
|
import { resolveThinking } from './router/thinking.js';
|
|
48
48
|
import { approxTokens } from './memory/budget.js';
|
|
49
|
-
import { COLLAB_TOOL_POLICY, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
|
|
49
|
+
import { COLLAB_TOOL_POLICY, isToolErrorOutput, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
|
|
50
50
|
import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
|
|
51
51
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
52
52
|
import {
|
|
@@ -3344,6 +3344,7 @@ export class Engine {
|
|
|
3344
3344
|
let output;
|
|
3345
3345
|
let displayImages = [];
|
|
3346
3346
|
let isError = false;
|
|
3347
|
+
let toolErrorOutput = null;
|
|
3347
3348
|
currentToolCallForAsyncTask = {
|
|
3348
3349
|
id: tc.id,
|
|
3349
3350
|
name: tc.name,
|
|
@@ -3363,9 +3364,11 @@ export class Engine {
|
|
|
3363
3364
|
try {
|
|
3364
3365
|
yield { type: 'tool_start', id: tc.id, name: tc.name, input: tc.input, threadId: this.currentThreadId };
|
|
3365
3366
|
if (this.#toolRegistry) {
|
|
3367
|
+
toolErrorOutput = this.#toolRegistry.get(tc.name)?.errorOutput || null;
|
|
3366
3368
|
output = await this.#toolRegistry.execute(tc.name, tc.input, toolCtx);
|
|
3367
3369
|
} else {
|
|
3368
3370
|
const tool = this.#tools.get(tc.name);
|
|
3371
|
+
toolErrorOutput = tool.errorOutput || null;
|
|
3369
3372
|
// Pass the full toolCtx (cwd, workDir, signal, …) — not just
|
|
3370
3373
|
// `{ signal }`. Legacy registerTool() callers historically got
|
|
3371
3374
|
// a 1-field ctx, but that means tools like bash/file-read run
|
|
@@ -3380,7 +3383,8 @@ export class Engine {
|
|
|
3380
3383
|
if (displayImages.length > 0) {
|
|
3381
3384
|
output = stripDisplayImageData(output, displayImages);
|
|
3382
3385
|
}
|
|
3383
|
-
|
|
3386
|
+
isError = toolErrorOutput === 'json-error-envelope' && isToolErrorOutput(output);
|
|
3387
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, displayImages, isError, threadId: this.currentThreadId };
|
|
3384
3388
|
if (displayImages.some(image => image.deliveryQueued === true)) hasDisplayImageAnchor = true;
|
|
3385
3389
|
} catch (err) {
|
|
3386
3390
|
output = `Error: ${err.message}`;
|
package/yeaft/tools/bash.js
CHANGED
|
@@ -210,6 +210,7 @@ Guidelines:
|
|
|
210
210
|
},
|
|
211
211
|
required: ['command'],
|
|
212
212
|
},
|
|
213
|
+
errorOutput: null,
|
|
213
214
|
isConcurrencySafe: () => false,
|
|
214
215
|
isReadOnly: () => false,
|
|
215
216
|
isDestructive: (input) => {
|
|
@@ -223,7 +224,7 @@ Guidelines:
|
|
|
223
224
|
},
|
|
224
225
|
async execute(input, ctx) {
|
|
225
226
|
const { command, cwd: inputCwd, timeout_ms, background = false, taskTitle } = input;
|
|
226
|
-
if (!command)
|
|
227
|
+
if (!command) throw new Error('command is required');
|
|
227
228
|
|
|
228
229
|
// Resolve working directory
|
|
229
230
|
const cwd = inputCwd
|
|
@@ -231,7 +232,7 @@ Guidelines:
|
|
|
231
232
|
: (ctx?.cwd || process.cwd());
|
|
232
233
|
|
|
233
234
|
if (!existsSync(cwd)) {
|
|
234
|
-
|
|
235
|
+
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
// Clamp timeout
|
|
@@ -240,7 +241,7 @@ Guidelines:
|
|
|
240
241
|
|
|
241
242
|
if (background) {
|
|
242
243
|
if (!ctx?.taskManager) {
|
|
243
|
-
|
|
244
|
+
throw new Error('background tasks are unavailable in this runtime');
|
|
244
245
|
}
|
|
245
246
|
try {
|
|
246
247
|
const task = ctx.taskManager.startShellTask({
|
|
@@ -263,7 +264,7 @@ Guidelines:
|
|
|
263
264
|
try { ctx.registerAsyncTask?.(task.id, currentToolCall || {}); } catch { /* never block tool return on coord errors */ }
|
|
264
265
|
return `Started background task ${task.id}.\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nUse ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
|
|
265
266
|
} catch (err) {
|
|
266
|
-
|
|
267
|
+
throw new Error(err?.message || String(err));
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
|
|
@@ -287,7 +288,7 @@ Guidelines:
|
|
|
287
288
|
}
|
|
288
289
|
return output || '(no output)';
|
|
289
290
|
} catch (err) {
|
|
290
|
-
|
|
291
|
+
throw new Error(err.message);
|
|
291
292
|
}
|
|
292
293
|
},
|
|
293
294
|
});
|
package/yeaft/tools/file-read.js
CHANGED
|
@@ -34,6 +34,50 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
|
34
34
|
* round-trip this tool's prompt guidance promises to avoid). */
|
|
35
35
|
const DEFAULT_LIMIT = 3000;
|
|
36
36
|
|
|
37
|
+
/** Keep raw output close to the model-facing 32 KiB budget while leaving
|
|
38
|
+
* room for line metadata and the Registry's localized truncation marker. */
|
|
39
|
+
const DEFAULT_OUTPUT_BYTES = 30 * 1024;
|
|
40
|
+
|
|
41
|
+
function takeUtf8(text, maxBytes) {
|
|
42
|
+
const chars = [];
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
for (const char of text) {
|
|
45
|
+
const size = Buffer.byteLength(char, 'utf8');
|
|
46
|
+
if (bytes + size > maxBytes) break;
|
|
47
|
+
chars.push(char);
|
|
48
|
+
bytes += size;
|
|
49
|
+
}
|
|
50
|
+
return { text: chars.join(''), charCount: chars.length, bytes };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatLinesWithinBudget(allLines, startLine, endLine, startColumn = 0, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
let usedBytes = 0;
|
|
56
|
+
let nextLine = startLine;
|
|
57
|
+
let nextColumn = startColumn;
|
|
58
|
+
for (let i = startLine; i < endLine; i += 1) {
|
|
59
|
+
const prefix = parts.length > 0 ? '\n' : '';
|
|
60
|
+
const lineChars = Array.from(allLines[i]);
|
|
61
|
+
const column = i === startLine ? Math.min(startColumn, lineChars.length) : 0;
|
|
62
|
+
const linePrefix = `${i + 1}\t${column > 0 ? `[column ${column}] ` : ''}`;
|
|
63
|
+
const formatted = linePrefix + lineChars.slice(column).join('');
|
|
64
|
+
const remaining = maxBytes - usedBytes - Buffer.byteLength(prefix, 'utf8');
|
|
65
|
+
if (remaining <= 0) break;
|
|
66
|
+
const bounded = takeUtf8(formatted, remaining);
|
|
67
|
+
if (!bounded.text) break;
|
|
68
|
+
parts.push(prefix + bounded.text);
|
|
69
|
+
usedBytes += Buffer.byteLength(prefix, 'utf8') + bounded.bytes;
|
|
70
|
+
if (bounded.text !== formatted) {
|
|
71
|
+
nextLine = i;
|
|
72
|
+
nextColumn = column + Math.max(0, bounded.charCount - Array.from(linePrefix).length);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
nextLine = i + 1;
|
|
76
|
+
nextColumn = 0;
|
|
77
|
+
}
|
|
78
|
+
return { text: parts.join(''), nextLine, nextColumn };
|
|
79
|
+
}
|
|
80
|
+
|
|
37
81
|
export default defineTool({
|
|
38
82
|
name: 'FileRead',
|
|
39
83
|
description: {
|
|
@@ -70,14 +114,21 @@ Guidelines:
|
|
|
70
114
|
},
|
|
71
115
|
},
|
|
72
116
|
offset: {
|
|
73
|
-
type: '
|
|
117
|
+
type: 'integer',
|
|
118
|
+
minimum: 0,
|
|
74
119
|
description: {
|
|
75
120
|
en: 'Line number to start reading from (0-based, default: 0)',
|
|
76
121
|
zh: '起始行号(从 0 开始计数,默认 0)',
|
|
77
122
|
},
|
|
78
123
|
},
|
|
124
|
+
column_offset: {
|
|
125
|
+
type: 'integer',
|
|
126
|
+
minimum: 0,
|
|
127
|
+
description: { en: 'Unicode character offset within the first requested line (0-based, default: 0)', zh: '首个待读行内的 Unicode 字符偏移量(从 0 开始,默认 0)' },
|
|
128
|
+
},
|
|
79
129
|
limit: {
|
|
80
|
-
type: '
|
|
130
|
+
type: 'integer',
|
|
131
|
+
minimum: 1,
|
|
81
132
|
description: {
|
|
82
133
|
en: `Maximum number of lines to read (default: ${DEFAULT_LIMIT})`,
|
|
83
134
|
zh: `最多读取行数(默认 ${DEFAULT_LIMIT} 行)`,
|
|
@@ -89,8 +140,17 @@ Guidelines:
|
|
|
89
140
|
isConcurrencySafe: () => true,
|
|
90
141
|
isReadOnly: () => true,
|
|
91
142
|
async execute(input, ctx) {
|
|
92
|
-
const { file_path, offset = 0, limit = DEFAULT_LIMIT } = input;
|
|
143
|
+
const { file_path, offset = 0, column_offset = 0, limit = DEFAULT_LIMIT } = input;
|
|
93
144
|
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
145
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
146
|
+
return JSON.stringify({ error: 'offset must be a non-negative integer' });
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isInteger(column_offset) || column_offset < 0) {
|
|
149
|
+
return JSON.stringify({ error: 'column_offset must be a non-negative integer' });
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
152
|
+
return JSON.stringify({ error: 'limit must be a positive integer' });
|
|
153
|
+
}
|
|
94
154
|
|
|
95
155
|
const cwd = ctx?.cwd || process.cwd();
|
|
96
156
|
const absPath = resolve(cwd, file_path);
|
|
@@ -126,20 +186,28 @@ Guidelines:
|
|
|
126
186
|
const allLines = content.split('\n');
|
|
127
187
|
const totalLines = allLines.length;
|
|
128
188
|
|
|
189
|
+
if (offset > totalLines) {
|
|
190
|
+
return JSON.stringify({ error: `offset ${offset} exceeds file length (${totalLines} lines)` });
|
|
191
|
+
}
|
|
192
|
+
if (offset === totalLines) {
|
|
193
|
+
return `[Offset ${offset} is at end of file (${totalLines} lines total).]`;
|
|
194
|
+
}
|
|
195
|
+
|
|
129
196
|
// Apply offset and limit
|
|
130
|
-
const startLine =
|
|
197
|
+
const startLine = offset;
|
|
131
198
|
const endLine = Math.min(startLine + limit, totalLines);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
199
|
+
const startColumn = column_offset;
|
|
200
|
+
const { text: numbered, nextLine, nextColumn } = formatLinesWithinBudget(allLines, startLine, endLine, startColumn);
|
|
201
|
+
|
|
202
|
+
const hasMoreContent = nextColumn > 0 || nextLine < totalLines;
|
|
203
|
+
if (startLine > 0 || startColumn > 0 || hasMoreContent) {
|
|
204
|
+
const continuation = hasMoreContent
|
|
205
|
+
? nextColumn > 0
|
|
206
|
+
? ` Continue with offset=${nextLine}, column_offset=${nextColumn}.`
|
|
207
|
+
: ` Continue with offset=${nextLine}.`
|
|
208
|
+
: '';
|
|
209
|
+
const shownEnd = nextColumn > 0 ? nextLine + 1 : nextLine;
|
|
210
|
+
return `${numbered}\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${totalLines} total.${continuation}]`;
|
|
143
211
|
}
|
|
144
212
|
|
|
145
213
|
return numbered;
|
package/yeaft/tools/glob.js
CHANGED
|
@@ -10,6 +10,13 @@ import { readdir, stat } from 'fs/promises';
|
|
|
10
10
|
import { existsSync } from 'fs';
|
|
11
11
|
import { resolve, join, relative } from 'path';
|
|
12
12
|
|
|
13
|
+
const STAT_CONCURRENCY = 32;
|
|
14
|
+
const SKIP_DIRS = new Set([
|
|
15
|
+
'node_modules', '.git', '__pycache__', '.next', '.nuxt',
|
|
16
|
+
'dist', 'build', '.cache', '.venv', 'venv', '.tox',
|
|
17
|
+
'vendor', 'target', '.gradle', '.idea', '.vscode',
|
|
18
|
+
]);
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* Simple glob pattern matcher (supports * and **).
|
|
15
22
|
* @param {string} pattern
|
|
@@ -44,19 +51,13 @@ async function* walkDir(dir, baseDir, maxDepth = 10, depth = 0) {
|
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// Skip common large/irrelevant directories
|
|
48
|
-
const SKIP = new Set([
|
|
49
|
-
'node_modules', '.git', '__pycache__', '.next', '.nuxt',
|
|
50
|
-
'dist', 'build', '.cache', '.venv', 'venv', '.tox',
|
|
51
|
-
'vendor', 'target', '.gradle', '.idea', '.vscode',
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
54
|
for (const entry of entries) {
|
|
55
55
|
const fullPath = join(dir, entry.name);
|
|
56
56
|
const relPath = relative(baseDir, fullPath);
|
|
57
57
|
|
|
58
58
|
if (entry.isDirectory()) {
|
|
59
|
-
|
|
59
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
60
|
+
if (SKIP_DIRS.has(entry.name) || normalized === '.yeaft/worktrees' || normalized.startsWith('.yeaft/worktrees/')) continue;
|
|
60
61
|
yield { path: relPath, isDir: true };
|
|
61
62
|
yield* walkDir(fullPath, baseDir, maxDepth, depth + 1);
|
|
62
63
|
} else {
|
|
@@ -106,7 +107,8 @@ Guidelines:
|
|
|
106
107
|
},
|
|
107
108
|
},
|
|
108
109
|
limit: {
|
|
109
|
-
type: '
|
|
110
|
+
type: 'integer',
|
|
111
|
+
minimum: 1,
|
|
110
112
|
description: {
|
|
111
113
|
en: 'Maximum number of results (default: 500)',
|
|
112
114
|
zh: '最多返回结果数(默认 500)',
|
|
@@ -120,6 +122,9 @@ Guidelines:
|
|
|
120
122
|
async execute(input, ctx) {
|
|
121
123
|
const { pattern, path: searchPath, limit = 500 } = input;
|
|
122
124
|
if (!pattern) return JSON.stringify({ error: 'pattern is required' });
|
|
125
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
126
|
+
return JSON.stringify({ error: 'limit must be a positive integer' });
|
|
127
|
+
}
|
|
123
128
|
|
|
124
129
|
const cwd = ctx?.cwd || process.cwd();
|
|
125
130
|
const baseDir = searchPath ? resolve(cwd, searchPath) : cwd;
|
|
@@ -129,29 +134,25 @@ Guidelines:
|
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
137
|
+
const paths = [];
|
|
134
138
|
for await (const entry of walkDir(baseDir, baseDir)) {
|
|
135
|
-
if (
|
|
139
|
+
if (!entry.isDir && matchGlob(pattern, entry.path)) paths.push(entry.path);
|
|
140
|
+
}
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// Exact newest-first semantics require every matching mtime. Batch the
|
|
143
|
+
// metadata reads instead of serializing one syscall per path.
|
|
144
|
+
const matches = [];
|
|
145
|
+
for (let i = 0; i < paths.length; i += STAT_CONCURRENCY) {
|
|
146
|
+
matches.push(...await Promise.all(paths.slice(i, i + STAT_CONCURRENCY).map(async (path) => {
|
|
139
147
|
try {
|
|
140
|
-
const fileStat = await stat(join(baseDir,
|
|
141
|
-
|
|
142
|
-
path: entry.path,
|
|
143
|
-
mtime: fileStat.mtimeMs,
|
|
144
|
-
});
|
|
148
|
+
const fileStat = await stat(join(baseDir, path));
|
|
149
|
+
return { path, mtime: fileStat.mtimeMs };
|
|
145
150
|
} catch {
|
|
146
|
-
|
|
151
|
+
return { path, mtime: 0 };
|
|
147
152
|
}
|
|
148
|
-
}
|
|
153
|
+
})));
|
|
149
154
|
}
|
|
150
|
-
|
|
151
|
-
// Sort by mtime (newest first)
|
|
152
155
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
153
|
-
|
|
154
|
-
// Trim to limit
|
|
155
156
|
const trimmed = matches.slice(0, limit);
|
|
156
157
|
|
|
157
158
|
return trimmed.map(m => m.path).join('\n') || '(no matches)';
|