evolclaw 2.0.4 → 2.0.5
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 +12 -12
- package/dist/channels/feishu.js +68 -10
- package/dist/channels/wechat.js +238 -5
- package/dist/cli.js +107 -82
- package/dist/core/agent-runner.js +3 -2
- package/dist/core/command-handler.js +3 -3
- package/dist/core/message-processor.js +6 -2
- package/dist/core/session-manager.js +4 -3
- package/dist/index.js +21 -6
- package/dist/paths.js +3 -1
- package/dist/utils/init.js +66 -50
- package/dist/utils/markdown-to-feishu.js +58 -2
- package/dist/utils/permission.js +7 -0
- package/dist/utils/platform.js +175 -0
- package/package.json +2 -2
package/dist/utils/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { execFileSync } from 'child_process';
|
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { execFile } from 'child_process';
|
|
9
9
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from '../paths.js';
|
|
10
|
+
import { isWindows, commandExists } from './platform.js';
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
// ==================== Helpers ====================
|
|
12
13
|
function ask(rl, question) {
|
|
@@ -18,6 +19,9 @@ async function npmInstallGlobal(pkg) {
|
|
|
18
19
|
}
|
|
19
20
|
catch (e) {
|
|
20
21
|
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
|
|
22
|
+
if (isWindows) {
|
|
23
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
24
|
+
}
|
|
21
25
|
await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 120000 });
|
|
22
26
|
}
|
|
23
27
|
else {
|
|
@@ -38,6 +42,9 @@ async function sudoExec(cmd, args) {
|
|
|
38
42
|
}
|
|
39
43
|
catch (e) {
|
|
40
44
|
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES') || e.code === 'EACCES') {
|
|
45
|
+
if (isWindows) {
|
|
46
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
47
|
+
}
|
|
41
48
|
await execFileAsync('sudo', [cmd, ...args], { timeout: 120000, env });
|
|
42
49
|
}
|
|
43
50
|
else {
|
|
@@ -57,15 +64,15 @@ async function checkEnvironment(rl) {
|
|
|
57
64
|
console.log(` ✗ Node.js v${process.versions.node} — 需要 >= 22(node:sqlite 依赖)`);
|
|
58
65
|
// 检测 nvm
|
|
59
66
|
// 检测 bash 是否存在(nvm 和 n 都依赖 bash)
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
execFileSync('which', ['bash'], { encoding: 'utf-8' });
|
|
63
|
-
hasBash = true;
|
|
64
|
-
}
|
|
65
|
-
catch { }
|
|
67
|
+
const hasBash = commandExists('bash');
|
|
66
68
|
if (!hasBash) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
if (isWindows) {
|
|
70
|
+
console.log(' ⚠ Windows 环境,请从 https://nodejs.org 下载安装 Node.js 22+');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(' ⚠ 当前环境没有 bash(Alpine 容器?),无法自动升级 Node.js');
|
|
74
|
+
console.log(' → 请手动升级: apk add nodejs-current 或重建容器使用 node:22-alpine');
|
|
75
|
+
}
|
|
69
76
|
return false;
|
|
70
77
|
}
|
|
71
78
|
const hasNvm = !!process.env.NVM_DIR && fs.existsSync(process.env.NVM_DIR);
|
|
@@ -91,12 +98,7 @@ async function checkEnvironment(rl) {
|
|
|
91
98
|
}
|
|
92
99
|
else {
|
|
93
100
|
// 检测 n
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
execFileSync('which', ['n'], { encoding: 'utf-8' });
|
|
97
|
-
hasN = true;
|
|
98
|
-
}
|
|
99
|
-
catch { }
|
|
101
|
+
const hasN = commandExists('n');
|
|
100
102
|
if (hasN) {
|
|
101
103
|
const answer = (await ask(rl, ' → 是否通过 n 升级到 Node.js 22?[Y/n] ')).trim().toLowerCase();
|
|
102
104
|
if (answer === 'n' || answer === 'no') {
|
|
@@ -138,48 +140,49 @@ async function checkEnvironment(rl) {
|
|
|
138
140
|
}
|
|
139
141
|
// claude CLI >= 2.1.32
|
|
140
142
|
const MIN_CLAUDE_VER = [2, 1, 32];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log(` ✓ claude CLI v${verMatch[1]}`);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
console.log(` ✗ claude CLI v${verMatch[1]} — 需要 >= ${MIN_CLAUDE_VER.join('.')}`);
|
|
157
|
-
const answer = (await ask(rl, ' → 是否升级 claude CLI?[Y/n] ')).trim().toLowerCase();
|
|
158
|
-
if (answer === 'n' || answer === 'no') {
|
|
159
|
-
console.log(' 已取消');
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
console.log(' 正在升级 claude CLI...');
|
|
163
|
-
try {
|
|
164
|
-
await npmInstallGlobal('@anthropic-ai/claude-code@latest');
|
|
165
|
-
console.log(' ✓ claude CLI 升级完成');
|
|
143
|
+
const claudeInstalled = commandExists('claude');
|
|
144
|
+
if (claudeInstalled) {
|
|
145
|
+
try {
|
|
146
|
+
const verOutput = execFileSync('claude', ['--version'], { encoding: 'utf-8' }).trim();
|
|
147
|
+
const verMatch = verOutput.match(/^(\d+\.\d+\.\d+)/);
|
|
148
|
+
if (verMatch) {
|
|
149
|
+
const parts = verMatch[1].split('.').map(Number);
|
|
150
|
+
const isOk = parts[0] > MIN_CLAUDE_VER[0]
|
|
151
|
+
|| (parts[0] === MIN_CLAUDE_VER[0] && parts[1] > MIN_CLAUDE_VER[1])
|
|
152
|
+
|| (parts[0] === MIN_CLAUDE_VER[0] && parts[1] === MIN_CLAUDE_VER[1] && parts[2] >= MIN_CLAUDE_VER[2]);
|
|
153
|
+
if (isOk) {
|
|
154
|
+
console.log(` ✓ claude CLI v${verMatch[1]}`);
|
|
166
155
|
}
|
|
167
|
-
|
|
168
|
-
console.log(` ✗
|
|
169
|
-
|
|
156
|
+
else {
|
|
157
|
+
console.log(` ✗ claude CLI v${verMatch[1]} — 需要 >= ${MIN_CLAUDE_VER.join('.')}`);
|
|
158
|
+
const answer = (await ask(rl, ' → 是否升级 claude CLI?[Y/n] ')).trim().toLowerCase();
|
|
159
|
+
if (answer === 'n' || answer === 'no') {
|
|
160
|
+
console.log(' 已取消');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
console.log(' 正在升级 claude CLI...');
|
|
164
|
+
try {
|
|
165
|
+
await npmInstallGlobal('@anthropic-ai/claude-code@latest');
|
|
166
|
+
console.log(' ✓ claude CLI 升级完成');
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.log(` ✗ 升级失败: ${e.message?.slice(0, 200) || e}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
173
|
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(` ✓ claude CLI (${verOutput})`);
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
catch {
|
|
179
|
+
// claude command exists but --version failed
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
182
|
+
else {
|
|
183
|
+
console.log(' ✗ claude CLI 未找到');
|
|
184
|
+
console.log(' → 请先安装: npm install -g @anthropic-ai/claude-code');
|
|
185
|
+
return false;
|
|
183
186
|
}
|
|
184
187
|
// @anthropic-ai/claude-agent-sdk >= 0.2.75
|
|
185
188
|
let sdkAction = 'ok';
|
|
@@ -226,6 +229,19 @@ async function checkEnvironment(rl) {
|
|
|
226
229
|
}
|
|
227
230
|
// ==================== Shell Profile ====================
|
|
228
231
|
function setupEnvVar(home) {
|
|
232
|
+
if (isWindows) {
|
|
233
|
+
// Windows: use setx to set user environment variable
|
|
234
|
+
try {
|
|
235
|
+
execFileSync('setx', ['EVOLCLAW_HOME', home], { encoding: 'utf-8', stdio: 'pipe' });
|
|
236
|
+
console.log(` ✓ 已设置用户环境变量: EVOLCLAW_HOME=${home}`);
|
|
237
|
+
console.log(' ⚠ 请重新打开终端使其生效');
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
console.log(` ⚠ 设置环境变量失败: ${e.message?.slice(0, 100) || e}`);
|
|
241
|
+
console.log(` → 请手动设置环境变量 EVOLCLAW_HOME=${home}`);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
229
245
|
const exportLine = `export EVOLCLAW_HOME="${home}"`;
|
|
230
246
|
const candidates = [
|
|
231
247
|
path.join(os.homedir(), '.zshrc'),
|
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
* Markdown 到飞书富文本格式转换工具
|
|
3
3
|
* 使用飞书 post 格式的 md tag 原生渲染 Markdown
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* 计算字符串的显示宽度(CJK 字符按 2 宽度计算)
|
|
7
|
+
*/
|
|
8
|
+
function displayWidth(str) {
|
|
9
|
+
let width = 0;
|
|
10
|
+
for (const ch of str) {
|
|
11
|
+
const code = ch.codePointAt(0);
|
|
12
|
+
// CJK Unified Ideographs, CJK Compatibility, Fullwidth Forms, etc.
|
|
13
|
+
if ((code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified
|
|
14
|
+
(code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A
|
|
15
|
+
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility
|
|
16
|
+
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
|
|
17
|
+
(code >= 0x3000 && code <= 0x303F) // CJK Symbols
|
|
18
|
+
) {
|
|
19
|
+
width += 2;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
width += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return width;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 用空格填充字符串到指定显示宽度
|
|
29
|
+
*/
|
|
30
|
+
function padToWidth(str, targetWidth) {
|
|
31
|
+
const current = displayWidth(str);
|
|
32
|
+
const padding = Math.max(0, targetWidth - current);
|
|
33
|
+
return str + ' '.repeat(padding);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 将 Markdown 表格转换为代码块内的对齐文本
|
|
37
|
+
* 飞书 post md tag 不支持标准 markdown 表格,会静默丢弃内容
|
|
38
|
+
* 用代码块 + 等宽对齐保留二维结构
|
|
39
|
+
*/
|
|
40
|
+
function convertTablesToText(text) {
|
|
41
|
+
const tableRegex = /^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/gm;
|
|
42
|
+
return text.replace(tableRegex, (_match, headerLine, _sep, bodyBlock) => {
|
|
43
|
+
const parseRow = (line) => line.split('|').slice(1, -1).map((c) => c.trim());
|
|
44
|
+
const headers = parseRow(headerLine);
|
|
45
|
+
const rows = bodyBlock.trim().split('\n').map(parseRow);
|
|
46
|
+
// 计算每列最大显示宽度
|
|
47
|
+
const colWidths = headers.map((h, i) => {
|
|
48
|
+
const cellWidths = rows.map(r => displayWidth(r[i] || ''));
|
|
49
|
+
return Math.max(displayWidth(h), ...cellWidths);
|
|
50
|
+
});
|
|
51
|
+
// 构建对齐的表格文本
|
|
52
|
+
const headerStr = headers.map((h, i) => padToWidth(h, colWidths[i])).join(' ');
|
|
53
|
+
const sepStr = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
54
|
+
const rowStrs = rows.map(r => headers.map((_, i) => padToWidth(r[i] || '', colWidths[i])).join(' '));
|
|
55
|
+
return '```\n' + [headerStr, sepStr, ...rowStrs].join('\n') + '\n```';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
5
58
|
/**
|
|
6
59
|
* 将 Markdown 文本转换为飞书 post 消息格式
|
|
7
60
|
* 利用 md tag 让飞书原生渲染,支持代码高亮、嵌套列表、引用等全部语法
|
|
@@ -9,7 +62,9 @@
|
|
|
9
62
|
export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
10
63
|
const match = markdown.match(/^# (.+)$/m);
|
|
11
64
|
const title = match?.[1] ?? defaultTitle ?? '';
|
|
12
|
-
|
|
65
|
+
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
66
|
+
// 转换飞书不支持的 markdown 表格
|
|
67
|
+
body = convertTablesToText(body);
|
|
13
68
|
return {
|
|
14
69
|
zh_cn: {
|
|
15
70
|
title,
|
|
@@ -32,7 +87,8 @@ export function hasMarkdownSyntax(text) {
|
|
|
32
87
|
/```[\s\S]*?```/, // 代码块
|
|
33
88
|
/\[.*?\]\(.*?\)/, // 链接
|
|
34
89
|
/^[\s]*[-*+]\s/m, // 无序列表
|
|
35
|
-
/^[\s]*\d+\.\s/m // 有序列表
|
|
90
|
+
/^[\s]*\d+\.\s/m, // 有序列表
|
|
91
|
+
/^\|.+\|$/m // 表格
|
|
36
92
|
];
|
|
37
93
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
38
94
|
}
|
package/dist/utils/permission.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 危险命令黑名单(正则表达式)
|
|
2
2
|
const DANGEROUS_PATTERNS = [
|
|
3
|
+
// Unix
|
|
3
4
|
/\brm\s+-\w*r\w*f/, // rm -rf
|
|
4
5
|
/\bsudo\b/, // sudo
|
|
5
6
|
/\bmkfs\b/, // mkfs (格式化文件系统)
|
|
@@ -8,6 +9,12 @@ const DANGEROUS_PATTERNS = [
|
|
|
8
9
|
/>\s*\/dev\//, // 重定向到设备文件
|
|
9
10
|
/\bshutdown\b/, // 关机
|
|
10
11
|
/\breboot\b/, // 重启
|
|
12
|
+
// Windows
|
|
13
|
+
/\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
|
|
14
|
+
/\brd\s+\/s/i, // rd /s (递归删除目录)
|
|
15
|
+
/\bdel\s+\/[sfq]/i, // del /f, /s, /q (强制删除)
|
|
16
|
+
/\breg\s+delete/i, // reg delete (删除注册表)
|
|
17
|
+
/\bnet\s+stop/i, // net stop (停止服务)
|
|
11
18
|
];
|
|
12
19
|
/**
|
|
13
20
|
* 权限检查回调函数
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { execFileSync, execFile, spawn } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export const isWindows = process.platform === 'win32';
|
|
8
|
+
/**
|
|
9
|
+
* Encode project path as directory name (Claude SDK convention).
|
|
10
|
+
* Replace all path separators with '-'.
|
|
11
|
+
* e.g. /home/user/project -> -home-user-project
|
|
12
|
+
* C:\Users\project -> C-Users-project
|
|
13
|
+
*/
|
|
14
|
+
export function encodePath(projectPath) {
|
|
15
|
+
return projectPath.replace(/[/\\]/g, '-');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cross-platform process liveness check.
|
|
19
|
+
*/
|
|
20
|
+
export function isProcessRunning(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// ESRCH = process not found; EPERM = exists but no permission
|
|
27
|
+
return e.code === 'EPERM';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cross-platform process termination.
|
|
32
|
+
*/
|
|
33
|
+
export function killProcess(pid, force = false) {
|
|
34
|
+
if (isWindows && force) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync('taskkill', ['/PID', String(pid), '/F']);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cross-platform process search by command line pattern.
|
|
49
|
+
* Returns list of matching PIDs.
|
|
50
|
+
*/
|
|
51
|
+
export function findProcesses(pattern) {
|
|
52
|
+
try {
|
|
53
|
+
if (isWindows) {
|
|
54
|
+
const output = execFileSync('wmic', ['process', 'where', `CommandLine like '%${pattern}%'`, 'get', 'ProcessId'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
55
|
+
return output.split('\n')
|
|
56
|
+
.map(line => parseInt(line.trim(), 10))
|
|
57
|
+
.filter(pid => !isNaN(pid) && pid !== process.pid);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const output = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' }).trim();
|
|
61
|
+
return output ? output.split('\n').map(Number).filter(pid => pid !== process.pid) : [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function getProcessInfo(pid) {
|
|
69
|
+
try {
|
|
70
|
+
if (isWindows) {
|
|
71
|
+
// Use wmic on Windows
|
|
72
|
+
const output = execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize,CreationDate'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
73
|
+
const lines = output.trim().split('\n').filter(l => l.trim());
|
|
74
|
+
if (lines.length >= 2) {
|
|
75
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
76
|
+
const memKB = parts[1] ? Math.round(parseInt(parts[1], 10) / 1024) : undefined;
|
|
77
|
+
return { memory: memKB ? `${memKB}` : undefined };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const uptime = execFileSync('ps', ['-p', String(pid), '-o', 'etime='], { encoding: 'utf-8' }).trim();
|
|
82
|
+
const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
|
|
83
|
+
const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
|
|
84
|
+
return { uptime, cpu, memory: mem };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Cross-platform command existence check.
|
|
92
|
+
*/
|
|
93
|
+
export function commandExists(cmd) {
|
|
94
|
+
try {
|
|
95
|
+
if (isWindows) {
|
|
96
|
+
execFileSync('where', [cmd], { encoding: 'utf-8', stdio: 'pipe' });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
execFileSync('which', [cmd], { encoding: 'utf-8', stdio: 'pipe' });
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Cross-platform live log tailing (replaces tail -f).
|
|
109
|
+
* Returns an abort function.
|
|
110
|
+
*/
|
|
111
|
+
export function tailFile(filePath) {
|
|
112
|
+
if (!isWindows) {
|
|
113
|
+
// Unix: use tail -f (more efficient)
|
|
114
|
+
const child = spawn('tail', ['-f', filePath], { stdio: 'inherit' });
|
|
115
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
116
|
+
return { abort: () => child.kill() };
|
|
117
|
+
}
|
|
118
|
+
// Windows: Node.js-based implementation
|
|
119
|
+
// Output last 20 lines of existing content
|
|
120
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
const lines = content.split('\n');
|
|
122
|
+
const lastLines = lines.slice(-20);
|
|
123
|
+
process.stdout.write(lastLines.join('\n'));
|
|
124
|
+
let position = fs.statSync(filePath).size;
|
|
125
|
+
const watcher = fs.watch(filePath, () => {
|
|
126
|
+
const stat = fs.statSync(filePath);
|
|
127
|
+
if (stat.size > position) {
|
|
128
|
+
const fd = fs.openSync(filePath, 'r');
|
|
129
|
+
const buffer = Buffer.alloc(stat.size - position);
|
|
130
|
+
fs.readSync(fd, buffer, 0, buffer.length, position);
|
|
131
|
+
fs.closeSync(fd);
|
|
132
|
+
process.stdout.write(buffer.toString('utf-8'));
|
|
133
|
+
position = stat.size;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return { abort: () => watcher.close() };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve file path from import.meta.url (cross-platform safe).
|
|
140
|
+
* Replaces unsafe `new URL('.', import.meta.url).pathname` usage.
|
|
141
|
+
*/
|
|
142
|
+
export function dirFromImportMeta(importMetaUrl) {
|
|
143
|
+
return path.dirname(fileURLToPath(importMetaUrl));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if current file is the main entry script (cross-platform safe).
|
|
147
|
+
* Replaces unsafe `import.meta.url === \`file://\${process.argv[1]}\`` check.
|
|
148
|
+
*/
|
|
149
|
+
export function isMainScript(importMetaUrl) {
|
|
150
|
+
const argv1 = process.argv[1];
|
|
151
|
+
if (!argv1)
|
|
152
|
+
return false;
|
|
153
|
+
try {
|
|
154
|
+
const selfPath = fileURLToPath(importMetaUrl);
|
|
155
|
+
const argvPath = fs.realpathSync(argv1);
|
|
156
|
+
return selfPath === argvPath || fs.realpathSync(selfPath) === argvPath;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Register graceful shutdown signal handlers (cross-platform safe).
|
|
164
|
+
*/
|
|
165
|
+
export function onShutdown(callback) {
|
|
166
|
+
process.on('SIGINT', callback);
|
|
167
|
+
// SIGTERM is not fully supported on Windows, but Node.js can still emit it
|
|
168
|
+
// in some scenarios (e.g., process managers), so register it anyway
|
|
169
|
+
process.on('SIGTERM', callback);
|
|
170
|
+
if (isWindows) {
|
|
171
|
+
// On Windows, also handle SIGHUP for graceful shutdown
|
|
172
|
+
// when the process is terminated via Task Manager or similar
|
|
173
|
+
process.on('SIGHUP', callback);
|
|
174
|
+
}
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolclaw",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "tsx watch src/index.ts",
|
|
17
|
-
"build": "tsc && node -e \"const f='dist/cli.js',c=require('fs').readFileSync(f,'utf8');if(!c.startsWith('#!'))require('fs').writeFileSync(f,'#!/usr/bin/env node\\n'+c)\" && chmod
|
|
17
|
+
"build": "tsc && node -e \"const f='dist/cli.js',c=require('fs').readFileSync(f,'utf8');if(!c.startsWith('#!'))require('fs').writeFileSync(f,'#!/usr/bin/env node\\n'+c)\" && node -e \"try{require('child_process').execFileSync('chmod',['+x','dist/cli.js'])}catch{}\"",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|