@agentunion/kite 1.6.2 → 1.6.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/cli.js +114 -59
- package/kite_cli/commands/install_skill.py +3 -1
- package/launcher/entry.py +48 -1
- package/main.py +44 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -55,6 +55,8 @@ const args = process.argv.slice(2);
|
|
|
55
55
|
const pythonArgs = [];
|
|
56
56
|
let cwdOverride = null;
|
|
57
57
|
let useVersion = version;
|
|
58
|
+
let isDaemon = false;
|
|
59
|
+
let isStop = false;
|
|
58
60
|
|
|
59
61
|
for (let i = 0; i < args.length; i++) {
|
|
60
62
|
if (args[i] === '--cwd' && args[i + 1]) {
|
|
@@ -62,6 +64,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
62
64
|
} else if (args[i] === '--use' && args[i + 1]) {
|
|
63
65
|
useVersion = args[++i];
|
|
64
66
|
} else {
|
|
67
|
+
if (args[i] === '--daemon' || args[i] === '-d') isDaemon = true;
|
|
68
|
+
if (args[i] === '--stop') isStop = true;
|
|
65
69
|
pythonArgs.push(args[i]);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -136,7 +140,8 @@ if (args[0] === 'start') {
|
|
|
136
140
|
...process.env,
|
|
137
141
|
KITE_PROJECT: cliWorkDir,
|
|
138
142
|
KITE_MODULES: modulesDir,
|
|
139
|
-
KITE_DATA: dataDir
|
|
143
|
+
KITE_DATA: dataDir,
|
|
144
|
+
KITE_CALLER_DIR: process.cwd()
|
|
140
145
|
}
|
|
141
146
|
});
|
|
142
147
|
result.on('exit', code => process.exit(code ?? 0));
|
|
@@ -251,71 +256,121 @@ if (!fs.existsSync(python)) {
|
|
|
251
256
|
|
|
252
257
|
console.log(`[kite] Python: ${python}`);
|
|
253
258
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
259
|
+
// --stop: 同步执行,等待结果后退出
|
|
260
|
+
if (isStop) {
|
|
261
|
+
const child = spawn(
|
|
262
|
+
python,
|
|
263
|
+
[path.join(versionDir, 'main.py'), ...pythonArgs],
|
|
264
|
+
{ cwd: versionDir, env, stdio: 'inherit' }
|
|
265
|
+
);
|
|
266
|
+
child.on('error', err => {
|
|
267
|
+
console.error(`[kite] 执行失败: ${err.message}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
child.on('exit', code => process.exit(code ?? 0));
|
|
271
|
+
|
|
272
|
+
// --daemon / -d: 后台启动,Node 立即退出
|
|
273
|
+
} else if (isDaemon) {
|
|
274
|
+
// 日志输出到实例目录
|
|
275
|
+
const basename = path.basename(kiteCwd);
|
|
276
|
+
const instanceDir = path.join(kiteHome, 'workspace', basename || 'default');
|
|
277
|
+
const logDir = path.join(instanceDir, 'launcher', 'log');
|
|
278
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
279
|
+
const daemonLog = path.join(logDir, 'daemon.log');
|
|
280
|
+
const logFd = fs.openSync(daemonLog, 'w');
|
|
281
|
+
|
|
282
|
+
const spawnOpts = {
|
|
283
|
+
cwd: versionDir,
|
|
284
|
+
env,
|
|
285
|
+
stdio: ['ignore', logFd, logFd],
|
|
286
|
+
detached: true,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Windows: 额外设置无窗口标志
|
|
290
|
+
if (process.platform === 'win32') {
|
|
291
|
+
spawnOpts.windowsHide = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 透传给 Python 时去掉 --daemon / -d,由 Node 层处理脱离
|
|
295
|
+
const childArgs = pythonArgs.filter(a => a !== '--daemon' && a !== '-d');
|
|
296
|
+
const child = spawn(
|
|
297
|
+
python,
|
|
298
|
+
[path.join(versionDir, 'main.py'), ...childArgs],
|
|
299
|
+
spawnOpts
|
|
300
|
+
);
|
|
259
301
|
|
|
260
|
-
child.
|
|
261
|
-
console.
|
|
262
|
-
console.
|
|
263
|
-
console.
|
|
302
|
+
child.unref();
|
|
303
|
+
console.log(`[kite] 后台启动成功 (PID: ${child.pid})`);
|
|
304
|
+
console.log(`[kite] 日志: ${daemonLog}`);
|
|
305
|
+
console.log(`[kite] 停止: kite --stop`);
|
|
306
|
+
fs.closeSync(logFd);
|
|
307
|
+
process.exit(0);
|
|
264
308
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
309
|
+
// 正常前台启动
|
|
310
|
+
} else {
|
|
311
|
+
const child = spawn(
|
|
312
|
+
python,
|
|
313
|
+
[path.join(versionDir, 'main.py'), ...pythonArgs],
|
|
314
|
+
{ cwd: versionDir, env, stdio: 'inherit' }
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
child.on('error', err => {
|
|
318
|
+
console.error(`[kite] 启动 Python 失败: ${err.message}`);
|
|
319
|
+
console.error(`[kite] 请确认 Python 3.8+ 已安装`);
|
|
320
|
+
console.error(`[kite] 或运行: npm install 重新准备环境`);
|
|
321
|
+
|
|
322
|
+
// 标记环境失败
|
|
323
|
+
markEnvFailed('python_launch_failed');
|
|
324
|
+
process.exit(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
child.on('exit', (code, signal) => {
|
|
328
|
+
// 如果是异常退出,检查是否是环境问题
|
|
329
|
+
if (code !== 0 && code !== null) {
|
|
330
|
+
console.error(`[kite] Python 进程异常退出,退出码: ${code}`);
|
|
331
|
+
|
|
332
|
+
// 读取 Kernel 日志,检查是否是依赖问题
|
|
333
|
+
const workspaceDir = path.join(kiteHome, 'workspace');
|
|
334
|
+
const instanceDir = path.join(workspaceDir, 'Kite');
|
|
335
|
+
const kernelLog = path.join(instanceDir, 'kernel', 'log', 'latest.log');
|
|
336
|
+
|
|
337
|
+
let isDependencyIssue = false;
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(kernelLog)) {
|
|
340
|
+
try {
|
|
341
|
+
const log = fs.readFileSync(kernelLog, 'utf-8');
|
|
342
|
+
|
|
343
|
+
const depErrors = [
|
|
344
|
+
'ImportError',
|
|
345
|
+
'ModuleNotFoundError',
|
|
346
|
+
'not installed',
|
|
347
|
+
'cannot parse module.md',
|
|
348
|
+
'No module named',
|
|
349
|
+
'Failed to import'
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
for (const errorPattern of depErrors) {
|
|
353
|
+
if (log.includes(errorPattern)) {
|
|
354
|
+
isDependencyIssue = true;
|
|
355
|
+
console.error(`[kite] 检测到依赖问题: ${errorPattern}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
302
358
|
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
// 读取日志失败,忽略
|
|
303
361
|
}
|
|
304
|
-
} catch (e) {
|
|
305
|
-
// 读取日志失败,忽略
|
|
306
362
|
}
|
|
307
|
-
}
|
|
308
363
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
if (isDependencyIssue || code === 1 || code === 127) {
|
|
365
|
+
console.error(`[kite] 下次启动将重新检查环境`);
|
|
366
|
+
markEnvFailed(isDependencyIssue ? 'dependency_import_error' : 'python_runtime_error');
|
|
367
|
+
}
|
|
313
368
|
}
|
|
314
|
-
}
|
|
315
369
|
|
|
316
|
-
|
|
317
|
-
});
|
|
370
|
+
process.exit(code ?? 0);
|
|
371
|
+
});
|
|
318
372
|
|
|
319
|
-
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
320
|
-
|
|
373
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
374
|
+
process.on(sig, () => child.kill(sig));
|
|
375
|
+
}
|
|
321
376
|
}
|
|
@@ -32,7 +32,9 @@ def get_user_skill_dir():
|
|
|
32
32
|
|
|
33
33
|
def get_project_skill_dir():
|
|
34
34
|
"""获取项目级 skill 目录"""
|
|
35
|
-
cwd
|
|
35
|
+
# cli.js 会把 cwd 改成包目录,用 KITE_CALLER_DIR 获取用户的实际目录
|
|
36
|
+
caller_dir = os.environ.get("KITE_CALLER_DIR", "")
|
|
37
|
+
cwd = Path(caller_dir) if caller_dir else Path.cwd()
|
|
36
38
|
return cwd / ".claude" / "skills" / "kite"
|
|
37
39
|
|
|
38
40
|
|
package/launcher/entry.py
CHANGED
|
@@ -356,12 +356,59 @@ class Launcher:
|
|
|
356
356
|
threading.Thread(target=_force, daemon=True).start()
|
|
357
357
|
|
|
358
358
|
def _setup_unix_signals(self):
|
|
359
|
-
"""Register SIGTERM/SIGINT handlers on Linux/macOS."""
|
|
359
|
+
"""Register SIGTERM/SIGINT handlers + 'q' key listener on Linux/macOS."""
|
|
360
360
|
def _handler(signum, frame):
|
|
361
361
|
self._request_shutdown(f"收到信号 {signum},正在关闭...")
|
|
362
362
|
signal.signal(signal.SIGTERM, _handler)
|
|
363
363
|
signal.signal(signal.SIGINT, _handler)
|
|
364
364
|
|
|
365
|
+
# 'q' key listener: 使用 termios 将 stdin 设为 raw 模式
|
|
366
|
+
def _listen_unix():
|
|
367
|
+
try:
|
|
368
|
+
import tty
|
|
369
|
+
import termios
|
|
370
|
+
import select
|
|
371
|
+
except ImportError:
|
|
372
|
+
return # 无 tty 支持(如 daemon 模式),跳过
|
|
373
|
+
|
|
374
|
+
fd = sys.stdin.fileno()
|
|
375
|
+
try:
|
|
376
|
+
old_settings = termios.tcgetattr(fd)
|
|
377
|
+
except termios.error:
|
|
378
|
+
return # stdin 不是终端(如重定向、daemon),跳过
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
tty.setcbreak(fd) # cbreak 模式:单字符读取,但保留 Ctrl+C 信号
|
|
382
|
+
while not self._thread_shutdown.is_set():
|
|
383
|
+
# select 超时 0.2s,避免忙等
|
|
384
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0.2)
|
|
385
|
+
if rlist:
|
|
386
|
+
ch = sys.stdin.read(1)
|
|
387
|
+
if ch == '\x1b': # ESC
|
|
388
|
+
print("[launcher] ESC 强制退出")
|
|
389
|
+
try:
|
|
390
|
+
if self._ws and self._loop:
|
|
391
|
+
import concurrent.futures
|
|
392
|
+
fut = asyncio.run_coroutine_threadsafe(
|
|
393
|
+
self._publish_event("module.exiting", {
|
|
394
|
+
"module_id": "launcher",
|
|
395
|
+
"reason": "ESC exit",
|
|
396
|
+
"action": "none",
|
|
397
|
+
}),
|
|
398
|
+
self._loop,
|
|
399
|
+
)
|
|
400
|
+
fut.result(timeout=1)
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
os._exit(0)
|
|
404
|
+
elif ch in ('q', 'Q'):
|
|
405
|
+
self._request_shutdown("收到退出请求,正在关闭...")
|
|
406
|
+
return
|
|
407
|
+
finally:
|
|
408
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
409
|
+
|
|
410
|
+
threading.Thread(target=_listen_unix, daemon=True).start()
|
|
411
|
+
|
|
365
412
|
def _setup_windows_exit(self):
|
|
366
413
|
"""SetConsoleCtrlHandler for Ctrl+C + daemon thread for 'q' key.
|
|
367
414
|
|
package/main.py
CHANGED
|
@@ -19,6 +19,50 @@ if "--stop" in sys.argv:
|
|
|
19
19
|
stop_instances()
|
|
20
20
|
sys.exit(0)
|
|
21
21
|
|
|
22
|
+
# --daemon / -d: 后台运行模式,脱离终端,关闭终端不影响进程
|
|
23
|
+
if "--daemon" in sys.argv or "-d" in sys.argv:
|
|
24
|
+
import os
|
|
25
|
+
import subprocess
|
|
26
|
+
|
|
27
|
+
# 构建子进程参数:去掉 --daemon / -d,其余保留
|
|
28
|
+
child_args = [sys.executable, os.path.abspath(__file__)]
|
|
29
|
+
for arg in sys.argv[1:]:
|
|
30
|
+
if arg not in ("--daemon", "-d"):
|
|
31
|
+
child_args.append(arg)
|
|
32
|
+
|
|
33
|
+
# 日志输出到实例目录(复用 start_launcher 的路径逻辑)
|
|
34
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
35
|
+
workspace = os.environ.get("KITE_WORKSPACE") or os.path.join(home, ".kite", "workspace")
|
|
36
|
+
basename = os.path.basename(os.getcwd().rstrip(os.sep)) or "default"
|
|
37
|
+
instance_dir = os.path.join(workspace, basename)
|
|
38
|
+
log_dir = os.path.join(instance_dir, "launcher", "log")
|
|
39
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
40
|
+
daemon_log = os.path.join(log_dir, "daemon.log")
|
|
41
|
+
|
|
42
|
+
log_fd = open(daemon_log, "w", encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
kwargs = {
|
|
45
|
+
"stdout": log_fd,
|
|
46
|
+
"stderr": log_fd,
|
|
47
|
+
"stdin": subprocess.DEVNULL,
|
|
48
|
+
"cwd": os.getcwd(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if sys.platform == "win32":
|
|
52
|
+
# Windows: 脱离控制台,创建新进程组
|
|
53
|
+
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
54
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
55
|
+
kwargs["creationflags"] = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
|
56
|
+
else:
|
|
57
|
+
# Linux / macOS: 新会话,脱离终端
|
|
58
|
+
kwargs["start_new_session"] = True
|
|
59
|
+
|
|
60
|
+
proc = subprocess.Popen(child_args, **kwargs)
|
|
61
|
+
print(f"[Kite] 后台启动成功 (PID: {proc.pid})")
|
|
62
|
+
print(f"[Kite] 日志: {daemon_log}")
|
|
63
|
+
print(f"[Kite] 停止: python main.py --stop")
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
22
66
|
# --no-tls: 禁用 TLS 要求(本地开发用)
|
|
23
67
|
if "--no-tls" in sys.argv:
|
|
24
68
|
import os
|