@yeaft/webchat-agent 1.0.419 → 1.0.421

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.419",
3
+ "version": "1.0.421",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,115 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { basename, join, relative, resolve } from 'node:path';
3
+ import { platform } from 'node:os';
4
+ import ctx from '../context.js';
5
+ import { resolveAndValidatePath } from './utils.js';
6
+ import { sendWorkbenchResult } from './request-routing.js';
7
+
8
+ const MAX_REFERENCES = 32;
9
+ const MAX_SCANNED_ENTRIES = 5000;
10
+ const MAX_DEPTH = 10;
11
+ const SKIP_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'dist', 'build', '.cache', 'bin', 'obj']);
12
+
13
+ async function isFile(path) {
14
+ try { return (await stat(path)).isFile(); } catch { return false; }
15
+ }
16
+
17
+ function comparable(value) {
18
+ const normalized = String(value || '').replaceAll('\\', '/');
19
+ return platform() === 'win32' ? normalized.toLowerCase() : normalized;
20
+ }
21
+
22
+ async function findUniqueBasenames(workDir, requestedPaths, {
23
+ maxScannedEntries = MAX_SCANNED_ENTRIES,
24
+ maxDepth = MAX_DEPTH,
25
+ readDirectory = readdir,
26
+ } = {}) {
27
+ const targets = new Set(requestedPaths.map(path => comparable(basename(path))).filter(Boolean));
28
+ const matches = new Map([...targets].map(target => [target, []]));
29
+ if (targets.size === 0) return { matches, complete: true };
30
+ let scanned = 0;
31
+ let complete = true;
32
+
33
+ async function walk(dir, depth) {
34
+ if (depth > maxDepth) {
35
+ complete = false;
36
+ return;
37
+ }
38
+ let entries;
39
+ try {
40
+ entries = await readDirectory(dir, { withFileTypes: true });
41
+ } catch {
42
+ complete = false;
43
+ return;
44
+ }
45
+ entries.sort((left, right) => left.name.localeCompare(right.name));
46
+ for (const entry of entries) {
47
+ if (scanned >= maxScannedEntries) {
48
+ complete = false;
49
+ return;
50
+ }
51
+ scanned += 1;
52
+ if (entry.isDirectory()) {
53
+ if (!SKIP_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);
54
+ } else if (entry.isFile()) {
55
+ const target = comparable(entry.name);
56
+ const found = matches.get(target);
57
+ if (found && found.length < 2) found.push(join(dir, entry.name));
58
+ }
59
+ }
60
+ }
61
+
62
+ await walk(resolve(workDir), 0);
63
+ return { matches, complete };
64
+ }
65
+
66
+ export async function resolveFileReferences(references, workDir, scanOptions) {
67
+ const unique = [...new Set((Array.isArray(references) ? references : [])
68
+ .map(value => typeof value === 'string' ? value.trim() : '')
69
+ .filter(Boolean))].slice(0, MAX_REFERENCES);
70
+ const exactMatches = await Promise.all(unique.map(async requestedPath => {
71
+ const exactPath = resolveAndValidatePath(requestedPath, workDir);
72
+ return await isFile(exactPath) ? exactPath : null;
73
+ }));
74
+ const unresolved = unique.filter((_path, index) => !exactMatches[index]);
75
+ const basenameScan = await findUniqueBasenames(workDir, unresolved, scanOptions);
76
+ const root = resolve(workDir);
77
+
78
+ return unique.flatMap((requestedPath, index) => {
79
+ const matches = basenameScan.matches.get(comparable(basename(requestedPath))) || [];
80
+ const fallbackPath = basenameScan.complete && matches.length === 1 ? matches[0] : null;
81
+ const matchedPath = exactMatches[index] || fallbackPath;
82
+ if (!matchedPath) return [];
83
+ return [{
84
+ requestedPath,
85
+ resolvedPath: relative(root, matchedPath).replaceAll('\\', '/') || basename(matchedPath),
86
+ }];
87
+ });
88
+ }
89
+
90
+ export async function handleResolveFileReferences(msg) {
91
+ const { conversationId, requestId, _requestUserId, _requestClientId } = msg;
92
+ const conv = ctx.conversations.get(conversationId);
93
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
94
+ try {
95
+ const references = await resolveFileReferences(msg.references, workDir);
96
+ sendWorkbenchResult(ctx, msg, {
97
+ type: 'file_references_resolved',
98
+ conversationId,
99
+ requestId,
100
+ _requestUserId,
101
+ _requestClientId,
102
+ references,
103
+ });
104
+ } catch (error) {
105
+ sendWorkbenchResult(ctx, msg, {
106
+ type: 'file_references_resolved',
107
+ conversationId,
108
+ requestId,
109
+ _requestUserId,
110
+ _requestClientId,
111
+ references: [],
112
+ error: error.message,
113
+ });
114
+ }
115
+ }
package/workbench.js CHANGED
@@ -16,5 +16,6 @@ export {
16
16
  } from './workbench/git-ops.js';
17
17
 
18
18
  export { handleFileSearch } from './workbench/file-search.js';
19
+ export { handleResolveFileReferences } from './workbench/file-reference-resolver.js';
19
20
 
20
21
  export { handleTransferFiles } from './workbench/transfer.js';
package/yeaft/skills.js CHANGED
@@ -110,13 +110,35 @@ export function matchesPlatform(platforms) {
110
110
  * @param {string} [filename] — source filename (for name fallback)
111
111
  * @returns {Skill|null}
112
112
  */
113
+ function skillFrontmatterStart(raw) {
114
+ if (!raw) return -1;
115
+
116
+ let cursor = raw.charCodeAt(0) === 0xFEFF ? 1 : 0;
117
+ if (raw.startsWith('---', cursor)) return cursor;
118
+
119
+ let sawComment = false;
120
+ while (cursor < raw.length) {
121
+ while (cursor < raw.length && /\s/.test(raw[cursor])) cursor++;
122
+ if (!raw.startsWith('<!--', cursor)) break;
123
+ const commentEnd = raw.indexOf('-->', cursor + 4);
124
+ if (commentEnd === -1) return -1;
125
+ sawComment = true;
126
+ cursor = commentEnd + 3;
127
+ }
128
+
129
+ if (!sawComment) return -1;
130
+ while (cursor < raw.length && /\s/.test(raw[cursor])) cursor++;
131
+ return raw.startsWith('---', cursor) ? cursor : -1;
132
+ }
133
+
113
134
  export function parseSkill(raw, filename = '') {
114
- if (!raw || !raw.startsWith('---')) return null;
135
+ const startIdx = skillFrontmatterStart(raw);
136
+ if (startIdx === -1) return null;
115
137
 
116
- const endIdx = raw.indexOf('\n---', 3);
138
+ const endIdx = raw.indexOf('\n---', startIdx + 3);
117
139
  if (endIdx === -1) return null;
118
140
 
119
- const frontmatter = raw.slice(4, endIdx).trim();
141
+ const frontmatter = raw.slice(startIdx + 4, endIdx).trim();
120
142
  const body = raw.slice(endIdx + 4).trim();
121
143
 
122
144
  const skill = {
@@ -229,6 +251,10 @@ function shouldIgnorePath(candidatePath, ignorePaths) {
229
251
  return ignorePaths.some(ignorePath => pathIsInside(candidatePath, ignorePath));
230
252
  }
231
253
 
254
+ function displaySkillPath(relativePath) {
255
+ return String(relativePath || '').replaceAll('\\', '/');
256
+ }
257
+
232
258
  function discoverSkills(rootDir, subPath = '', opts = {}) {
233
259
  const dir = subPath ? join(rootDir, subPath) : rootDir;
234
260
  const ignorePaths = Array.isArray(opts.ignorePaths) ? opts.ignorePaths : [];
@@ -264,11 +290,11 @@ function discoverSkills(rootDir, subPath = '', opts = {}) {
264
290
  skill.category = skill.category || subPath.split(sep).join('/');
265
291
  }
266
292
  skills.push(skill);
267
- } else {
268
- errors.push(`Failed to parse skill: ${relPath}`);
293
+ } else if (skillFrontmatterStart(raw) !== -1) {
294
+ errors.push(`Failed to parse skill: ${displaySkillPath(relPath)}`);
269
295
  }
270
296
  } catch (err) {
271
- errors.push(`Error loading ${relPath}: ${err.message}`);
297
+ errors.push(`Error loading ${displaySkillPath(relPath)}: ${err.message}`);
272
298
  }
273
299
  } else if (entry.isDirectory()) {
274
300
  // Check for SKILL.md inside this directory
@@ -290,10 +316,10 @@ function discoverSkills(rootDir, subPath = '', opts = {}) {
290
316
  skill._templates = listSubdirFiles(join(entryPath, 'templates'));
291
317
  skills.push(skill);
292
318
  } else {
293
- errors.push(`Failed to parse skill: ${relPath}/SKILL.md`);
319
+ errors.push(`Failed to parse skill: ${displaySkillPath(relPath)}/SKILL.md`);
294
320
  }
295
321
  } catch (err) {
296
- errors.push(`Error loading ${relPath}/SKILL.md: ${err.message}`);
322
+ errors.push(`Error loading ${displaySkillPath(relPath)}/SKILL.md: ${err.message}`);
297
323
  }
298
324
  } else {
299
325
  // No SKILL.md — treat as category directory, recurse
@@ -329,7 +355,9 @@ function discoverWorkspaceSkills(workspaceRoot, skillsRelativeRoot, subPath = ''
329
355
  if (!read || read.truncated) continue;
330
356
  const skill = parseSkill(read.buffer.toString('utf8'), entry.name);
331
357
  if (!skill?.name) {
332
- errors.push(`Failed to parse skill: ${relPath}`);
358
+ if (skillFrontmatterStart(read.buffer.toString('utf8')) !== -1) {
359
+ errors.push(`Failed to parse skill: ${displaySkillPath(relPath)}`);
360
+ }
333
361
  continue;
334
362
  }
335
363
  skill._source = 'file';
@@ -352,7 +380,7 @@ function discoverWorkspaceSkills(workspaceRoot, skillsRelativeRoot, subPath = ''
352
380
  if (!read || read.truncated) continue;
353
381
  const skill = parseSkill(read.buffer.toString('utf8'), entry.name);
354
382
  if (!skill?.name) {
355
- errors.push(`Failed to parse skill: ${relPath}/SKILL.md`);
383
+ errors.push(`Failed to parse skill: ${displaySkillPath(relPath)}/SKILL.md`);
356
384
  continue;
357
385
  }
358
386
  skill._source = 'directory';