@agentunion/kite 1.0.0

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.
Files changed (69) hide show
  1. package/__init__.py +1 -0
  2. package/__main__.py +15 -0
  3. package/cli.js +70 -0
  4. package/core/__init__.py +0 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/event_hub/BENCHMARK.md +94 -0
  7. package/core/event_hub/__init__.py +0 -0
  8. package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
  10. package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
  11. package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
  12. package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
  13. package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
  14. package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
  15. package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
  16. package/core/event_hub/bench.py +459 -0
  17. package/core/event_hub/bench_extreme.py +308 -0
  18. package/core/event_hub/bench_perf.py +350 -0
  19. package/core/event_hub/bench_results/.gitkeep +0 -0
  20. package/core/event_hub/bench_results/2026-02-28_13-26-48.json +51 -0
  21. package/core/event_hub/bench_results/2026-02-28_13-44-45.json +51 -0
  22. package/core/event_hub/bench_results/2026-02-28_13-45-39.json +51 -0
  23. package/core/event_hub/dedup.py +31 -0
  24. package/core/event_hub/entry.py +113 -0
  25. package/core/event_hub/hub.py +263 -0
  26. package/core/event_hub/module.md +21 -0
  27. package/core/event_hub/router.py +21 -0
  28. package/core/event_hub/server.py +138 -0
  29. package/core/event_hub_bench/entry.py +371 -0
  30. package/core/event_hub_bench/module.md +25 -0
  31. package/core/launcher/__init__.py +0 -0
  32. package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
  34. package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
  35. package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
  36. package/core/launcher/data/log/lifecycle.jsonl +1045 -0
  37. package/core/launcher/data/processes_14752.json +32 -0
  38. package/core/launcher/data/token.txt +1 -0
  39. package/core/launcher/entry.py +965 -0
  40. package/core/launcher/module.md +37 -0
  41. package/core/launcher/module_scanner.py +253 -0
  42. package/core/launcher/process_manager.py +435 -0
  43. package/core/registry/__init__.py +0 -0
  44. package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  45. package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
  46. package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
  47. package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
  48. package/core/registry/data/port.txt +1 -0
  49. package/core/registry/data/port_14752.txt +1 -0
  50. package/core/registry/data/port_484.txt +1 -0
  51. package/core/registry/entry.py +73 -0
  52. package/core/registry/module.md +30 -0
  53. package/core/registry/server.py +256 -0
  54. package/core/registry/store.py +232 -0
  55. package/extensions/__init__.py +0 -0
  56. package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
  57. package/extensions/services/__init__.py +0 -0
  58. package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
  59. package/extensions/services/watchdog/__init__.py +0 -0
  60. package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
  62. package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
  63. package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
  64. package/extensions/services/watchdog/entry.py +143 -0
  65. package/extensions/services/watchdog/module.md +25 -0
  66. package/extensions/services/watchdog/monitor.py +420 -0
  67. package/extensions/services/watchdog/server.py +167 -0
  68. package/main.py +17 -0
  69. package/package.json +27 -0
@@ -0,0 +1,435 @@
1
+ """
2
+ Manage child process lifecycle: start, stop, monitor, cleanup leftovers.
3
+ Cross-platform support for Windows, Linux, and macOS.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ from dataclasses import dataclass, asdict
13
+
14
+
15
+ IS_WINDOWS = sys.platform == "win32"
16
+ IS_MACOS = sys.platform == "darwin"
17
+
18
+
19
+ @dataclass
20
+ class ProcessRecord:
21
+ name: str
22
+ pid: int
23
+ cmd: list
24
+ module_dir: str
25
+ started_at: float
26
+
27
+
28
+ class ProcessManager:
29
+ """Manage child processes for all Kite modules."""
30
+
31
+ def __init__(self, project_root: str, kite_token: str, instance_id: str = ""):
32
+ self.project_root = project_root
33
+ self.kite_token = kite_token
34
+ self.instance_id = instance_id or str(os.getpid())
35
+ self.data_dir = os.path.join(project_root, "core", "launcher", "data")
36
+ self.records_path = os.path.join(self.data_dir, f"processes_{self.instance_id}.json")
37
+ self._processes: dict[str, subprocess.Popen] = {} # name -> Popen
38
+ self._records: dict[str, ProcessRecord] = {} # name -> record
39
+ os.makedirs(self.data_dir, exist_ok=True)
40
+
41
+ # ── Leftover cleanup ──
42
+
43
+ def cleanup_leftovers(self):
44
+ """Scan all processes_*.json files, kill leftovers from dead instances, preserve live ones."""
45
+ import glob
46
+
47
+ # Also clean legacy processes.json (pre-multi-instance)
48
+ legacy = os.path.join(self.data_dir, "processes.json")
49
+ patterns = [
50
+ os.path.join(self.data_dir, "processes_*.json"),
51
+ ]
52
+ files = []
53
+ for p in patterns:
54
+ files.extend(glob.glob(p))
55
+ if os.path.isfile(legacy):
56
+ files.append(legacy)
57
+
58
+ registry_data_dir = os.path.join(self.project_root, "core", "registry", "data")
59
+
60
+ for path in files:
61
+ if path == self.records_path:
62
+ continue # skip our own instance
63
+
64
+ # If this is an instance file, check if that Launcher is still alive
65
+ basename = os.path.basename(path)
66
+ if basename.startswith("processes_") and basename.endswith(".json"):
67
+ inst_id = basename[len("processes_"):-len(".json")]
68
+ try:
69
+ if self._is_pid_alive(int(inst_id)):
70
+ continue # that Launcher is still running, skip
71
+ except (ValueError, TypeError):
72
+ pass
73
+
74
+ try:
75
+ with open(path, "r", encoding="utf-8") as f:
76
+ saved = json.load(f)
77
+ except Exception:
78
+ os.remove(path)
79
+ continue
80
+
81
+ for entry in saved:
82
+ pid = entry.get("pid", 0)
83
+ cmd = entry.get("cmd", [])
84
+ name = entry.get("name", "?")
85
+ if not pid:
86
+ continue
87
+ if self._is_pid_alive(pid) and self._cmd_matches(pid, cmd):
88
+ print(f"[launcher] Killing leftover process: {name} (PID {pid})")
89
+ self._force_kill(pid)
90
+ else:
91
+ print(f"[launcher] Leftover {name} (PID {pid}) already gone")
92
+
93
+ os.remove(path)
94
+
95
+ # Remove corresponding port file
96
+ basename = os.path.basename(path)
97
+ if basename.startswith("processes_") and basename.endswith(".json"):
98
+ inst_id = basename[len("processes_"):-len(".json")]
99
+ port_file = os.path.join(registry_data_dir, f"port_{inst_id}.txt")
100
+ if os.path.isfile(port_file):
101
+ os.remove(port_file)
102
+
103
+ # Also clean legacy port.txt
104
+ legacy_port = os.path.join(registry_data_dir, "port.txt")
105
+ if os.path.isfile(legacy_port):
106
+ os.remove(legacy_port)
107
+
108
+ def _is_pid_alive(self, pid: int) -> bool:
109
+ """Check if a process with given PID exists."""
110
+ if IS_WINDOWS:
111
+ try:
112
+ result = subprocess.run(
113
+ ["tasklist", "/FI", f"PID eq {pid}", "/NH"],
114
+ capture_output=True, text=True, timeout=5,
115
+ )
116
+ return str(pid) in result.stdout
117
+ except Exception:
118
+ return False
119
+ else:
120
+ try:
121
+ os.kill(pid, 0)
122
+ return True
123
+ except OSError:
124
+ return False
125
+
126
+ def _cmd_matches(self, pid: int, expected_cmd: list) -> bool:
127
+ """Verify the process command line matches what we expect (prevent killing recycled PIDs).
128
+ Uses multi-strategy fallback per platform for robustness.
129
+ """
130
+ if not expected_cmd:
131
+ return False
132
+ cmdline = self._get_process_cmdline(pid)
133
+ if cmdline:
134
+ return expected_cmd[-1] in cmdline
135
+ # Degraded fallback: match executable image name only
136
+ return self._exe_name_matches(pid, expected_cmd)
137
+
138
+ def _get_process_cmdline(self, pid: int) -> str:
139
+ """Get full command line of a process. Returns empty string on failure."""
140
+ if IS_WINDOWS:
141
+ for method in (self._win_powershell_cmdline, self._win_wmic_cmdline):
142
+ result = method(pid)
143
+ if result:
144
+ return result
145
+ return ""
146
+ elif IS_MACOS:
147
+ try:
148
+ r = subprocess.run(["ps", "-p", str(pid), "-o", "command="],
149
+ capture_output=True, text=True, timeout=5)
150
+ return r.stdout.strip()
151
+ except Exception:
152
+ return ""
153
+ else:
154
+ # Linux: /proc first, ps fallback
155
+ try:
156
+ with open(f"/proc/{pid}/cmdline", "r") as f:
157
+ return f.read()
158
+ except Exception:
159
+ pass
160
+ try:
161
+ r = subprocess.run(["ps", "-p", str(pid), "-o", "args="],
162
+ capture_output=True, text=True, timeout=5)
163
+ return r.stdout.strip()
164
+ except Exception:
165
+ return ""
166
+
167
+ def _win_powershell_cmdline(self, pid: int) -> str:
168
+ """Windows: Get cmdline via PowerShell Get-CimInstance (modern, replaces wmic)."""
169
+ try:
170
+ r = subprocess.run(
171
+ ["powershell", "-NoProfile", "-Command",
172
+ f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}').CommandLine"],
173
+ capture_output=True, text=True, timeout=8,
174
+ )
175
+ return r.stdout.strip()
176
+ except Exception:
177
+ return ""
178
+
179
+ def _win_wmic_cmdline(self, pid: int) -> str:
180
+ """Windows: Get cmdline via wmic (legacy fallback, deprecated but widely available)."""
181
+ try:
182
+ r = subprocess.run(
183
+ ["wmic", "process", "where", f"ProcessId={pid}",
184
+ "get", "CommandLine", "/value"],
185
+ capture_output=True, text=True, timeout=5,
186
+ )
187
+ return r.stdout.strip()
188
+ except Exception:
189
+ return ""
190
+
191
+ def _exe_name_matches(self, pid: int, expected_cmd: list) -> bool:
192
+ """Last resort: check if process image name matches expected executable."""
193
+ exe = os.path.basename(expected_cmd[0]).lower()
194
+ if exe.endswith(".exe"):
195
+ exe = exe[:-4]
196
+ if IS_WINDOWS:
197
+ try:
198
+ r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"],
199
+ capture_output=True, text=True, timeout=5)
200
+ return exe in r.stdout.lower()
201
+ except Exception:
202
+ return False
203
+ else:
204
+ try:
205
+ r = subprocess.run(["ps", "-p", str(pid), "-o", "comm="],
206
+ capture_output=True, text=True, timeout=5)
207
+ return exe in r.stdout.lower()
208
+ except Exception:
209
+ return False
210
+
211
+ def _force_kill(self, pid: int):
212
+ """Force kill a process by PID."""
213
+ try:
214
+ if IS_WINDOWS:
215
+ subprocess.run(["taskkill", "/F", "/PID", str(pid)],
216
+ capture_output=True, timeout=5)
217
+ else:
218
+ import signal
219
+ os.kill(pid, signal.SIGKILL)
220
+ except Exception as e:
221
+ print(f"[launcher] WARNING: failed to kill PID {pid}: {e}")
222
+
223
+ # ── Start module ──
224
+
225
+ def start_module(self, info, boot_info: dict = None) -> bool:
226
+ """Start a module as a subprocess. Returns True on success.
227
+ If boot_info is provided, it is written as JSON to the child's stdin pipe.
228
+ """
229
+ from .module_scanner import ModuleInfo
230
+ info: ModuleInfo
231
+
232
+ if info.name in self._processes:
233
+ proc = self._processes[info.name]
234
+ if proc.poll() is None:
235
+ print(f"[launcher] {info.name} already running (PID {proc.pid})")
236
+ return True
237
+
238
+ cmd = self._build_cmd(info)
239
+ if not cmd:
240
+ print(f"[launcher] ERROR: cannot build command for {info.name}")
241
+ return False
242
+
243
+ env = os.environ.copy()
244
+ env["PYTHONUTF8"] = "1" # Force UTF-8 in child Python processes
245
+ env["PYTHONUNBUFFERED"] = "1" # Disable output buffering for real-time logs
246
+ if info.launch.env:
247
+ env.update(info.launch.env)
248
+
249
+ creation_flags = 0
250
+ if IS_WINDOWS:
251
+ creation_flags = subprocess.CREATE_NO_WINDOW
252
+
253
+ use_stdin = boot_info is not None and info.launch.boot_stdin
254
+
255
+ cwd = info.module_dir
256
+ if info.launch.cwd:
257
+ cwd = os.path.normpath(os.path.join(info.module_dir, info.launch.cwd))
258
+
259
+ try:
260
+ proc = subprocess.Popen(
261
+ cmd,
262
+ cwd=cwd,
263
+ env=env,
264
+ stdin=subprocess.PIPE if use_stdin else None,
265
+ stdout=subprocess.PIPE,
266
+ stderr=subprocess.STDOUT,
267
+ creationflags=creation_flags,
268
+ )
269
+ except Exception as e:
270
+ print(f"[launcher] ERROR: failed to start {info.name}: {e}")
271
+ return False
272
+
273
+ # Write boot_info to stdin then close
274
+ if use_stdin and proc.stdin:
275
+ try:
276
+ proc.stdin.write(json.dumps(boot_info).encode() + b"\n")
277
+ proc.stdin.flush()
278
+ proc.stdin.close()
279
+ except Exception as e:
280
+ print(f"[launcher] WARNING: failed to write boot_info to {info.name}: {e}")
281
+
282
+ self._processes[info.name] = proc
283
+ self._records[info.name] = ProcessRecord(
284
+ name=info.name,
285
+ pid=proc.pid,
286
+ cmd=cmd,
287
+ module_dir=info.module_dir,
288
+ started_at=time.time(),
289
+ )
290
+
291
+ # Daemon thread to read stdout and prefix with module name
292
+ t = threading.Thread(
293
+ target=self._read_stdout, args=(info.name, proc),
294
+ daemon=True,
295
+ )
296
+ t.start()
297
+
298
+ print(f"[launcher] Started {info.name} (PID {proc.pid})")
299
+ return True
300
+
301
+ def _build_cmd(self, info) -> list:
302
+ """Build the command list based on module runtime.
303
+
304
+ For python/node: if entry starts with '-m ', use package mode (e.g. python -m pkg).
305
+ Otherwise use script file mode (e.g. python entry.py).
306
+ """
307
+ if info.launch.cmd:
308
+ return list(info.launch.cmd)
309
+
310
+ if info.runtime == "python":
311
+ if info.entry.startswith("-m "):
312
+ return [sys.executable] + info.entry.split()
313
+ return [sys.executable, os.path.join(info.module_dir, info.entry)]
314
+ elif info.runtime == "node":
315
+ if info.entry.startswith("-e ") or info.entry.startswith("--eval "):
316
+ return ["node"] + info.entry.split()
317
+ return ["node", os.path.join(info.module_dir, info.entry)]
318
+ elif info.runtime == "binary":
319
+ return [os.path.join(info.module_dir, info.entry)]
320
+ else:
321
+ print(f"[launcher] WARNING: unknown runtime '{info.runtime}' for {info.name}")
322
+ return []
323
+
324
+ @staticmethod
325
+ def _read_stdout(name: str, proc: subprocess.Popen):
326
+ """Read subprocess stdout line by line, prefix with module name."""
327
+ try:
328
+ for line in iter(proc.stdout.readline, b""):
329
+ text = line.decode("utf-8", errors="replace").rstrip()
330
+ if text:
331
+ print(f"[{name}] {text}")
332
+ except Exception:
333
+ pass
334
+
335
+ # ── Stop module ──
336
+
337
+ def stop_module(self, name: str, timeout: float = 5) -> bool:
338
+ """Stop a module by name. Returns True if stopped successfully."""
339
+ proc = self._processes.get(name)
340
+ if not proc or proc.poll() is not None:
341
+ self._processes.pop(name, None)
342
+ self._records.pop(name, None)
343
+ return True
344
+
345
+ print(f"[launcher] Stopping {name} (PID {proc.pid})...")
346
+ proc.terminate()
347
+ try:
348
+ proc.wait(timeout=timeout)
349
+ except subprocess.TimeoutExpired:
350
+ print(f"[launcher] {name} did not exit in {timeout}s, force killing")
351
+ proc.kill()
352
+ try:
353
+ proc.wait(timeout=3)
354
+ except subprocess.TimeoutExpired:
355
+ self._force_kill(proc.pid)
356
+
357
+ self._processes.pop(name, None)
358
+ self._records.pop(name, None)
359
+ print(f"[launcher] Stopped {name}")
360
+ return True
361
+
362
+ def stop_all(self, timeout: float = 10):
363
+ """Stop all managed processes. Terminate first, force kill after timeout."""
364
+ if not self._processes:
365
+ return
366
+
367
+ names = list(self._processes.keys())
368
+ print(f"[launcher] Stopping all modules: {', '.join(names)}")
369
+
370
+ # Phase 1: send terminate to all
371
+ for name in names:
372
+ proc = self._processes.get(name)
373
+ if proc and proc.poll() is None:
374
+ proc.terminate()
375
+
376
+ # Phase 2: wait for all to exit
377
+ deadline = time.time() + timeout
378
+ for name in names:
379
+ proc = self._processes.get(name)
380
+ if not proc:
381
+ continue
382
+ remaining = max(0.1, deadline - time.time())
383
+ try:
384
+ proc.wait(timeout=remaining)
385
+ except subprocess.TimeoutExpired:
386
+ pass
387
+
388
+ # Phase 3: force kill survivors
389
+ for name in names:
390
+ proc = self._processes.get(name)
391
+ if proc and proc.poll() is None:
392
+ print(f"[launcher] Force killing {name} (PID {proc.pid})")
393
+ proc.kill()
394
+ try:
395
+ proc.wait(timeout=3)
396
+ except subprocess.TimeoutExpired:
397
+ self._force_kill(proc.pid)
398
+
399
+ self._processes.clear()
400
+ self._records.clear()
401
+ print("[launcher] All modules stopped")
402
+
403
+ # ── Persistence & monitoring ──
404
+
405
+ def persist_records(self):
406
+ """Write current process records to processes.json."""
407
+ data = [asdict(r) for r in self._records.values()]
408
+ self._write_records_file(data)
409
+
410
+ def check_exited(self) -> list[tuple[str, int]]:
411
+ """Non-blocking check for exited child processes. Returns [(name, returncode)]."""
412
+ exited = []
413
+ for name in list(self._processes.keys()):
414
+ proc = self._processes[name]
415
+ rc = proc.poll()
416
+ if rc is not None:
417
+ exited.append((name, rc))
418
+ self._processes.pop(name, None)
419
+ self._records.pop(name, None)
420
+ return exited
421
+
422
+ def get_record(self, name: str) -> ProcessRecord | None:
423
+ return self._records.get(name)
424
+
425
+ def is_running(self, name: str) -> bool:
426
+ proc = self._processes.get(name)
427
+ return proc is not None and proc.poll() is None
428
+
429
+ def _write_records_file(self, data: list):
430
+ """Write data list to processes.json."""
431
+ try:
432
+ with open(self.records_path, "w", encoding="utf-8") as f:
433
+ json.dump(data, f, indent=2)
434
+ except Exception as e:
435
+ print(f"[launcher] WARNING: failed to write processes.json: {e}")
File without changes
@@ -0,0 +1 @@
1
+ 51279
@@ -0,0 +1 @@
1
+ 63756
@@ -0,0 +1 @@
1
+ 61190
@@ -0,0 +1,73 @@
1
+ """
2
+ Registry entry point.
3
+ Reads launcher_token, starts HTTP server on dynamic port, writes port.txt.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import socket
9
+
10
+ import uvicorn
11
+
12
+ # Ensure project root is on sys.path
13
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
14
+ _project_root = os.path.dirname(os.path.dirname(_this_dir))
15
+ if _project_root not in sys.path:
16
+ sys.path.insert(0, _project_root)
17
+
18
+ from core.registry.store import RegistryStore
19
+ from core.registry.server import RegistryServer
20
+
21
+
22
+ def _get_free_port() -> int:
23
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24
+ s.bind(("127.0.0.1", 0))
25
+ return s.getsockname()[1]
26
+
27
+
28
+ def _write_port_file(port: int, instance_id: str = ""):
29
+ data_dir = os.path.join(_this_dir, "data")
30
+ os.makedirs(data_dir, exist_ok=True)
31
+ filename = f"port_{instance_id}.txt" if instance_id else "port.txt"
32
+ port_file = os.path.join(data_dir, filename)
33
+ with open(port_file, "w") as f:
34
+ f.write(str(port))
35
+ print(f"[registry] Port written to {port_file}")
36
+
37
+
38
+ def main():
39
+ # Read launcher_token and config from stdin (boot_info JSON pipe)
40
+ launcher_token = ""
41
+ bind_host = "127.0.0.1"
42
+ instance_id = ""
43
+ try:
44
+ import json
45
+ line = sys.stdin.readline().strip()
46
+ if line:
47
+ boot_info = json.loads(line)
48
+ launcher_token = boot_info.get("token", "")
49
+ bind_host = boot_info.get("bind", "127.0.0.1")
50
+ instance_id = boot_info.get("instance_id", "")
51
+ except Exception:
52
+ pass
53
+
54
+ if not launcher_token:
55
+ print("[registry] ERROR: No launcher token provided via stdin boot_info")
56
+ sys.exit(1)
57
+
58
+ print(f"[registry] Launcher token received ({len(launcher_token)} chars)")
59
+
60
+ # Create store and server
61
+ store = RegistryStore(launcher_token)
62
+ server = RegistryServer(store, launcher_token=launcher_token)
63
+
64
+ # Bind to dynamic port
65
+ port = _get_free_port()
66
+ _write_port_file(port, instance_id)
67
+
68
+ print(f"[registry] Starting on {bind_host}:{port}")
69
+ uvicorn.run(server.app, host=bind_host, port=port, log_level="warning")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: registry
3
+ display_name: Registry
4
+ type: infrastructure
5
+ state: enabled
6
+ runtime: python
7
+ entry: entry.py
8
+ ---
9
+
10
+ # Registry
11
+
12
+ Kite 系统注册中心,第一个启动的核心模块。
13
+
14
+ ## 职责
15
+
16
+ - 服务注册与发现(所有模块的"电话簿")
17
+ - 能力索引(工具、Hook、事件的聚合查询)
18
+ - 通用字段查询(glob 模式匹配)
19
+ - 心跳 TTL 管理
20
+
21
+ ## API
22
+
23
+ | 方法 | 路径 | 说明 |
24
+ |------|------|------|
25
+ | POST | `/modules` | 模块生命周期(register / deregister / heartbeat) |
26
+ | GET | `/lookup` | 通用搜索(三参数均支持 glob) |
27
+ | GET | `/get/{path}` | 按路径精确读取注册内容 |
28
+ | POST | `/tokens` | 注册 token 映射(仅 Launcher) |
29
+ | POST | `/query` | LLM 自然语言查询(待实现) |
30
+ | GET | `/health` | 健康检查(无需 token) |