@yeaft/webchat-agent 1.0.300 → 1.0.302

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.
@@ -37,6 +37,7 @@ import fileEdit from './file-edit.js';
37
37
  import globTool from './glob.js';
38
38
  import grepTool from './grep.js';
39
39
  import listDir from './list-dir.js';
40
+ import diskUsage from './disk-usage.js';
40
41
  import applyPatch from './apply-patch.js';
41
42
  import listTasks from './list-tasks.js';
42
43
  import readTaskLog from './read-task-log.js';
@@ -102,6 +103,7 @@ export const allTools = [
102
103
  globTool,
103
104
  grepTool,
104
105
  listDir,
106
+ diskUsage,
105
107
  applyPatch,
106
108
  listTasks,
107
109
  readTaskLog,
@@ -0,0 +1,211 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+
4
+ const DEFAULT_MAX_BYTES = 512 * 1024;
5
+ const DEFAULT_KILL_GRACE_MS = 250;
6
+ const DEFAULT_FORCE_SETTLE_MS = 1000;
7
+
8
+ function abortError(signal) {
9
+ if (signal?.reason instanceof Error && signal.reason.name === 'AbortError') return signal.reason;
10
+ const error = new Error(
11
+ signal?.reason instanceof Error ? signal.reason.message : 'The operation was aborted',
12
+ );
13
+ error.name = 'AbortError';
14
+ return error;
15
+ }
16
+
17
+ function killProcessTree(proc, signal, platform, spawnProcessSync) {
18
+ if (!proc.pid) return false;
19
+ if (platform === 'win32') {
20
+ try {
21
+ const result = spawnProcessSync('taskkill', ['/pid', String(proc.pid), '/t', '/f'], {
22
+ stdio: 'ignore',
23
+ windowsHide: true,
24
+ timeout: 5000,
25
+ });
26
+ if (!result.error && result.status === 0) return true;
27
+ } catch {}
28
+ try { return proc.kill(signal) !== false; } catch { return false; }
29
+ }
30
+ try {
31
+ process.kill(-proc.pid, signal);
32
+ return true;
33
+ } catch {
34
+ try { return proc.kill(signal) !== false; } catch { return false; }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Execute a binary directly without a shell and keep captured output bounded.
40
+ *
41
+ * @param {string} command
42
+ * @param {string[]} args
43
+ * @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, forceSettleMs?: number, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
44
+ */
45
+ export function runProcess(command, args, options = {}) {
46
+ if (options.signal?.aborted) return Promise.reject(abortError(options.signal));
47
+
48
+ return new Promise((resolve, reject) => {
49
+ const platform = options.platform || process.platform;
50
+ const spawnProcess = options.spawnProcess || spawn;
51
+ const spawnProcessSync = options.spawnProcessSync || spawnSync;
52
+ const maxBytes = Number.isFinite(options.maxBytes)
53
+ ? Math.max(0, options.maxBytes)
54
+ : DEFAULT_MAX_BYTES;
55
+ const killGraceMs = Number.isFinite(options.killGraceMs)
56
+ ? Math.max(0, options.killGraceMs)
57
+ : DEFAULT_KILL_GRACE_MS;
58
+ const forceSettleMs = Number.isFinite(options.forceSettleMs)
59
+ ? Math.max(1, options.forceSettleMs)
60
+ : DEFAULT_FORCE_SETTLE_MS;
61
+ const proc = spawnProcess(command, args, {
62
+ cwd: options.cwd,
63
+ env: options.env || process.env,
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ windowsHide: true,
66
+ detached: platform !== 'win32',
67
+ });
68
+ const stdout = [];
69
+ const stderr = [];
70
+ let stdoutBytes = 0;
71
+ let stderrBytes = 0;
72
+ let stdoutTruncated = false;
73
+ let stderrTruncated = false;
74
+ let truncated = false;
75
+ let settled = false;
76
+ let timedOut = false;
77
+ let aborted = false;
78
+ let stopRequested = false;
79
+ let forceRequested = false;
80
+ let timer = null;
81
+ let forceTimer = null;
82
+ let forceSettleTimer = null;
83
+
84
+ let onStdout;
85
+ let onStderr;
86
+ let onError;
87
+ let onClose;
88
+ const cleanup = () => {
89
+ if (timer) clearTimeout(timer);
90
+ if (forceTimer) clearTimeout(forceTimer);
91
+ if (forceSettleTimer) clearTimeout(forceSettleTimer);
92
+ timer = null;
93
+ forceTimer = null;
94
+ forceSettleTimer = null;
95
+ options.signal?.removeEventListener('abort', onAbort);
96
+ if (onStdout) proc.stdout?.off('data', onStdout);
97
+ if (onStderr) proc.stderr?.off('data', onStderr);
98
+ if (onError) proc.off('error', onError);
99
+ if (onClose) proc.off('close', onClose);
100
+ };
101
+ const decode = (chunks, wasTruncated, preserveCarriageReturns = false) => {
102
+ const decoder = new StringDecoder('utf8');
103
+ let value = decoder.write(Buffer.concat(chunks));
104
+ if (!wasTruncated) value += decoder.end();
105
+ return preserveCarriageReturns ? value : value.replace(/\r/g, '');
106
+ };
107
+ const finish = code => {
108
+ if (settled) return;
109
+ settled = true;
110
+ cleanup();
111
+ if (aborted) {
112
+ reject(abortError(options.signal));
113
+ return;
114
+ }
115
+ resolve({
116
+ code: timedOut ? 124 : (code ?? 1),
117
+ stdout: decode(stdout, stdoutTruncated, options.preserveCarriageReturns),
118
+ stderr: decode(stderr, stderrTruncated),
119
+ truncated,
120
+ timedOut,
121
+ });
122
+ };
123
+ const forceStop = () => {
124
+ if (settled || forceRequested) return;
125
+ forceRequested = true;
126
+ killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
127
+ forceSettleTimer = setTimeout(() => finish(null), forceSettleMs);
128
+ forceSettleTimer.unref?.();
129
+ };
130
+ const stop = () => {
131
+ if (settled || stopRequested) return;
132
+ stopRequested = true;
133
+ if (platform === 'win32') {
134
+ // taskkill must run while the parent PID still identifies the tree.
135
+ // It is already forceful, so do not wait for the direct child to exit.
136
+ forceRequested = true;
137
+ killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
138
+ if (!settled) {
139
+ forceSettleTimer = setTimeout(() => finish(null), forceSettleMs);
140
+ forceSettleTimer.unref?.();
141
+ }
142
+ return;
143
+ }
144
+ killProcessTree(proc, 'SIGTERM', platform, spawnProcessSync);
145
+ forceTimer = setTimeout(forceStop, killGraceMs);
146
+ forceTimer.unref?.();
147
+ };
148
+ const onAbort = () => {
149
+ aborted = true;
150
+ stop();
151
+ };
152
+ const capture = (target, chunk, isStdout) => {
153
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
154
+ const current = isStdout ? stdoutBytes : stderrBytes;
155
+ const remaining = maxBytes - current;
156
+ if (remaining <= 0) {
157
+ truncated = true;
158
+ if (isStdout) stdoutTruncated = true;
159
+ else stderrTruncated = true;
160
+ stop();
161
+ return;
162
+ }
163
+ const bounded = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
164
+ target.push(bounded);
165
+ if (isStdout) stdoutBytes += bounded.length;
166
+ else stderrBytes += bounded.length;
167
+ if (bounded.length !== buffer.length) {
168
+ truncated = true;
169
+ if (isStdout) stdoutTruncated = true;
170
+ else stderrTruncated = true;
171
+ stop();
172
+ }
173
+ };
174
+
175
+ onStdout = chunk => capture(stdout, chunk, true);
176
+ onStderr = chunk => capture(stderr, chunk, false);
177
+ onError = error => {
178
+ if (settled) return;
179
+ if (stopRequested) {
180
+ finish(null);
181
+ return;
182
+ }
183
+ settled = true;
184
+ cleanup();
185
+ reject(error);
186
+ };
187
+ onClose = code => {
188
+ if (stopRequested && !forceRequested) {
189
+ // The direct child is gone. Kill any process that remained in its
190
+ // detached group before releasing the tool call.
191
+ forceRequested = true;
192
+ killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
193
+ }
194
+ finish(code);
195
+ };
196
+ proc.stdout.on('data', onStdout);
197
+ proc.stderr.on('data', onStderr);
198
+ proc.on('error', onError);
199
+ proc.on('close', onClose);
200
+
201
+ if (Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
202
+ timer = setTimeout(() => {
203
+ timedOut = true;
204
+ stop();
205
+ }, options.timeoutMs);
206
+ timer.unref?.();
207
+ }
208
+ options.signal?.addEventListener('abort', onAbort, { once: true });
209
+ if (options.signal?.aborted) onAbort();
210
+ });
211
+ }
@@ -0,0 +1,108 @@
1
+ import { extname } from 'node:path';
2
+
3
+ export const SEARCH_SKIP_DIRS = new Set([
4
+ 'node_modules', '.git', '__pycache__', '.next', '.nuxt',
5
+ 'dist', 'build', '.cache', '.venv', 'venv', '.tox',
6
+ 'vendor', 'target', '.gradle', '.idea', '.vscode',
7
+ ]);
8
+
9
+ export const SEARCH_SKIP_GLOBS = Object.freeze([
10
+ ...[...SEARCH_SKIP_DIRS].flatMap(name => [`!${name}/**`, `!**/${name}/**`]),
11
+ '!.yeaft/worktrees/**',
12
+ '!**/.yeaft/worktrees/**',
13
+ ]);
14
+
15
+ const TYPE_EXTENSIONS = {
16
+ js: ['.js', '.jsx', '.mjs', '.cjs'], ts: ['.ts', '.tsx', '.mts', '.cts'],
17
+ py: ['.py'], rust: ['.rs'], go: ['.go'], java: ['.java'],
18
+ json: ['.json'], yaml: ['.yaml', '.yml'], markdown: ['.md', '.markdown'],
19
+ html: ['.html', '.htm'], css: ['.css'], shell: ['.sh', '.bash', '.zsh'],
20
+ };
21
+
22
+ export function isSkippedSearchDirectory(relativePath, name) {
23
+ const normalized = String(relativePath || '').replace(/\\/g, '/');
24
+ return SEARCH_SKIP_DIRS.has(name)
25
+ || normalized === '.yeaft/worktrees'
26
+ || normalized.endsWith('/.yeaft/worktrees');
27
+ }
28
+
29
+ function expandBraces(pattern) {
30
+ const match = pattern.match(/\{([^{}]+)\}/);
31
+ if (!match) return [pattern];
32
+ return match[1].split(',').flatMap(part => expandBraces(
33
+ pattern.slice(0, match.index) + part + pattern.slice(match.index + match[0].length),
34
+ ));
35
+ }
36
+
37
+ function globToRegExp(pattern) {
38
+ let source = '';
39
+ for (let index = 0; index < pattern.length; index += 1) {
40
+ const char = pattern[index];
41
+ if (char === '*' && pattern[index + 1] === '*') {
42
+ index += 1;
43
+ if (pattern[index + 1] === '/') {
44
+ index += 1;
45
+ source += '(?:.*/)?';
46
+ } else {
47
+ source += '.*';
48
+ }
49
+ } else if (char === '*') {
50
+ source += '[^/]*';
51
+ } else if (char === '?') {
52
+ source += '[^/]';
53
+ } else {
54
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
55
+ }
56
+ }
57
+ return new RegExp(`^${source}$`);
58
+ }
59
+
60
+ export function createSearchPathMatcher({ glob, type } = {}) {
61
+ const normalizedGlob = String(glob || '').replace(/\\/g, '/');
62
+ const globMatchers = normalizedGlob
63
+ ? expandBraces(normalizedGlob).map(globToRegExp)
64
+ : [];
65
+ const matchBase = normalizedGlob && !normalizedGlob.includes('/');
66
+ const extensions = type ? TYPE_EXTENSIONS[type] : null;
67
+
68
+ return path => {
69
+ const normalized = String(path || '').replace(/\\/g, '/');
70
+ const candidate = matchBase ? normalized.split('/').pop() : normalized;
71
+ if (globMatchers.length && !globMatchers.some(matcher => matcher.test(candidate))) return false;
72
+ if (type && !extensions?.includes(extname(normalized).toLowerCase())) return false;
73
+ return true;
74
+ };
75
+ }
76
+
77
+ export function throwIfAborted(signal) {
78
+ if (!signal?.aborted) return;
79
+ if (signal.reason instanceof Error && signal.reason.name === 'AbortError') {
80
+ throw signal.reason;
81
+ }
82
+ const error = new Error(
83
+ signal.reason instanceof Error ? signal.reason.message : 'The operation was aborted',
84
+ );
85
+ error.name = 'AbortError';
86
+ throw error;
87
+ }
88
+
89
+ export function isAbortError(error) {
90
+ return error?.name === 'AbortError';
91
+ }
92
+
93
+ export function waitForAbortable(promise, signal) {
94
+ throwIfAborted(signal);
95
+ if (!signal) return promise;
96
+ return new Promise((resolve, reject) => {
97
+ const onAbort = () => {
98
+ cleanup();
99
+ try { throwIfAborted(signal); } catch (error) { reject(error); }
100
+ };
101
+ const cleanup = () => signal.removeEventListener('abort', onAbort);
102
+ signal.addEventListener('abort', onAbort, { once: true });
103
+ Promise.resolve(promise).then(
104
+ value => { cleanup(); resolve(value); },
105
+ error => { cleanup(); reject(error); },
106
+ );
107
+ });
108
+ }
@@ -14,6 +14,7 @@
14
14
  * @typedef {Object} ToolContext
15
15
  * @property {AbortSignal} [signal] — cancellation signal
16
16
  * @property {string} [yeaftDir] — Yeaft data directory
17
+ * @property {Promise<Array> & {toolReady?: Record<string, Promise<object>>}} [managedCliReady] — resolves after optional managed CLI setup; toolReady exposes per-command readiness
17
18
  * @property {ReturnType<import('../runtime-platform.js').getRuntimePlatformInfo>} [runtimePlatform]
18
19
  * — runtime OS/shell facts for platform-aware tools
19
20
  * @property {string} [cwd] — working directory
@@ -1779,6 +1779,7 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
1779
1779
  skillManager: session.skillManager,
1780
1780
  mcpManager: session.mcpManager,
1781
1781
  yeaftDir: session.yeaftDir,
1782
+ managedCliReady: session.managedCliReady || null,
1782
1783
  // Share the session-shared ToolUsageStats so per-VP tool calls land
1783
1784
  // in the same on-disk snapshot the `yeaft_fetch_tool_stats` handler
1784
1785
  // reads. Without this, engine's record-on-tool-exec guard
@@ -4836,6 +4837,7 @@ export async function ensureSessionLoaded(opts = {}) {
4836
4837
  skipMCP: true,
4837
4838
  skipSkills: true,
4838
4839
  serverMode: true,
4840
+ managedCliReady: ctx.managedCliReady,
4839
4841
  });
4840
4842
  claimRuntimeOwnership(session);
4841
4843
 
@@ -7112,6 +7114,7 @@ export async function resetYeaftSession() {
7112
7114
  skipMCP: true,
7113
7115
  skipSkills: true,
7114
7116
  serverMode: true,
7117
+ managedCliReady: ctx.managedCliReady,
7115
7118
  });
7116
7119
  claimRuntimeOwnership(session);
7117
7120
  installYeaftRuntimeBridge(session);