@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 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
- const child = spawn(
255
- python,
256
- [path.join(versionDir, 'main.py'), ...pythonArgs],
257
- { cwd: versionDir, env, stdio: 'inherit' }
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.on('error', err => {
261
- console.error(`[kite] 启动 Python 失败: ${err.message}`);
262
- console.error(`[kite] 请确认 Python 3.8+ 已安装`);
263
- console.error(`[kite] 或运行: npm install 重新准备环境`);
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
- markEnvFailed('python_launch_failed');
267
- process.exit(1);
268
- });
269
-
270
- child.on('exit', (code, signal) => {
271
- // 如果是异常退出,检查是否是环境问题
272
- if (code !== 0 && code !== null) {
273
- console.error(`[kite] Python 进程异常退出,退出码: ${code}`);
274
-
275
- // 读取 Kernel 日志,检查是否是依赖问题
276
- // KITE_INSTANCE_DIR Launcher 设置,格式为 ~/.kite/workspace/Kite
277
- const workspaceDir = path.join(kiteHome, 'workspace');
278
- const instanceDir = path.join(workspaceDir, 'Kite');
279
- const kernelLog = path.join(instanceDir, 'kernel', 'log', 'latest.log');
280
-
281
- let isDependencyIssue = false;
282
-
283
- if (fs.existsSync(kernelLog)) {
284
- try {
285
- const log = fs.readFileSync(kernelLog, 'utf-8');
286
-
287
- // 检测依赖相关错误
288
- const depErrors = [
289
- 'ImportError',
290
- 'ModuleNotFoundError',
291
- 'not installed',
292
- 'cannot parse module.md',
293
- 'No module named',
294
- 'Failed to import'
295
- ];
296
-
297
- for (const errorPattern of depErrors) {
298
- if (log.includes(errorPattern)) {
299
- isDependencyIssue = true;
300
- console.error(`[kite] 检测到依赖问题: ${errorPattern}`);
301
- break;
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
- if (isDependencyIssue || code === 1 || code === 127) {
311
- console.error(`[kite] 下次启动将重新检查环境`);
312
- markEnvFailed(isDependencyIssue ? 'dependency_import_error' : 'python_runtime_error');
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
- process.exit(code ?? 0);
317
- });
370
+ process.exit(code ?? 0);
371
+ });
318
372
 
319
- for (const sig of ['SIGINT', 'SIGTERM']) {
320
- process.on(sig, () => child.kill(sig));
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 = Path.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentunion/kite",
3
- "version": "1.6.2",
3
+ "version": "1.6.5",
4
4
  "description": "Kite framework launcher — start Kite from anywhere",
5
5
  "bin": {
6
6
  "kite": "./cli.js"