@zhixuan92/multi-model-agent 5.0.2 → 5.0.3

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 (255) hide show
  1. package/README.md +8 -18
  2. package/dist/cli/index.d.ts +62 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +345 -0
  5. package/dist/cli/index.js.map +1 -0
  6. package/dist/cli/info.d.ts +22 -0
  7. package/dist/cli/info.d.ts.map +1 -0
  8. package/dist/cli/info.js +100 -0
  9. package/dist/cli/info.js.map +1 -0
  10. package/dist/cli/logs.d.ts +15 -0
  11. package/dist/cli/logs.d.ts.map +1 -0
  12. package/dist/cli/logs.js +102 -0
  13. package/dist/cli/logs.js.map +1 -0
  14. package/dist/cli/print-token.d.ts +18 -0
  15. package/dist/cli/print-token.d.ts.map +1 -0
  16. package/dist/cli/print-token.js +60 -0
  17. package/dist/cli/print-token.js.map +1 -0
  18. package/dist/cli/serve.d.ts +28 -0
  19. package/dist/cli/serve.d.ts.map +1 -0
  20. package/dist/cli/serve.js +405 -0
  21. package/dist/cli/serve.js.map +1 -0
  22. package/dist/cli/status.d.ts +49 -0
  23. package/dist/cli/status.d.ts.map +1 -0
  24. package/dist/cli/status.js +155 -0
  25. package/dist/cli/status.js.map +1 -0
  26. package/dist/cli/sync-skills.d.ts +58 -0
  27. package/dist/cli/sync-skills.d.ts.map +1 -0
  28. package/dist/cli/sync-skills.js +266 -0
  29. package/dist/cli/sync-skills.js.map +1 -0
  30. package/dist/cli/telemetry.d.ts +10 -0
  31. package/dist/cli/telemetry.d.ts.map +1 -0
  32. package/dist/cli/telemetry.js +161 -0
  33. package/dist/cli/telemetry.js.map +1 -0
  34. package/dist/cli/toggle.d.ts +26 -0
  35. package/dist/cli/toggle.d.ts.map +1 -0
  36. package/dist/cli/toggle.js +185 -0
  37. package/dist/cli/toggle.js.map +1 -0
  38. package/dist/http/async-dispatch.d.ts +44 -0
  39. package/dist/http/async-dispatch.d.ts.map +1 -0
  40. package/dist/http/async-dispatch.js +175 -0
  41. package/dist/http/async-dispatch.js.map +1 -0
  42. package/dist/http/auth.d.ts +20 -0
  43. package/dist/http/auth.d.ts.map +1 -0
  44. package/dist/http/auth.js +56 -0
  45. package/dist/http/auth.js.map +1 -0
  46. package/dist/http/canonicalize-file-paths.d.ts +8 -0
  47. package/dist/http/canonicalize-file-paths.d.ts.map +1 -0
  48. package/dist/http/canonicalize-file-paths.js +43 -0
  49. package/dist/http/canonicalize-file-paths.js.map +1 -0
  50. package/dist/http/cwd-validator.d.ts +11 -0
  51. package/dist/http/cwd-validator.d.ts.map +1 -0
  52. package/dist/http/cwd-validator.js +130 -0
  53. package/dist/http/cwd-validator.js.map +1 -0
  54. package/dist/http/errors.d.ts +4 -0
  55. package/dist/http/errors.d.ts.map +1 -0
  56. package/dist/http/errors.js +9 -0
  57. package/dist/http/errors.js.map +1 -0
  58. package/dist/http/execution-context.d.ts +18 -0
  59. package/dist/http/execution-context.d.ts.map +1 -0
  60. package/dist/http/execution-context.js +61 -0
  61. package/dist/http/execution-context.js.map +1 -0
  62. package/dist/http/handler-deps.d.ts +19 -0
  63. package/dist/http/handler-deps.d.ts.map +1 -0
  64. package/dist/http/handler-deps.js +2 -0
  65. package/dist/http/handler-deps.js.map +1 -0
  66. package/dist/http/handlers/control/batch-slice.d.ts +4 -0
  67. package/dist/http/handlers/control/batch-slice.d.ts.map +1 -0
  68. package/dist/http/handlers/control/batch-slice.js +40 -0
  69. package/dist/http/handlers/control/batch-slice.js.map +1 -0
  70. package/dist/http/handlers/control/batch.d.ts +23 -0
  71. package/dist/http/handlers/control/batch.d.ts.map +1 -0
  72. package/dist/http/handlers/control/batch.js +332 -0
  73. package/dist/http/handlers/control/batch.js.map +1 -0
  74. package/dist/http/handlers/control/context-blocks.d.ts +22 -0
  75. package/dist/http/handlers/control/context-blocks.d.ts.map +1 -0
  76. package/dist/http/handlers/control/context-blocks.js +111 -0
  77. package/dist/http/handlers/control/context-blocks.js.map +1 -0
  78. package/dist/http/handlers/introspection/health.d.ts +20 -0
  79. package/dist/http/handlers/introspection/health.d.ts.map +1 -0
  80. package/dist/http/handlers/introspection/health.js +18 -0
  81. package/dist/http/handlers/introspection/health.js.map +1 -0
  82. package/dist/http/handlers/introspection/status.d.ts +26 -0
  83. package/dist/http/handlers/introspection/status.d.ts.map +1 -0
  84. package/dist/http/handlers/introspection/status.js +136 -0
  85. package/dist/http/handlers/introspection/status.js.map +1 -0
  86. package/dist/http/handlers/tools/audit.d.ts +4 -0
  87. package/dist/http/handlers/tools/audit.d.ts.map +1 -0
  88. package/dist/http/handlers/tools/audit.js +43 -0
  89. package/dist/http/handlers/tools/audit.js.map +1 -0
  90. package/dist/http/handlers/tools/debug.d.ts +4 -0
  91. package/dist/http/handlers/tools/debug.d.ts.map +1 -0
  92. package/dist/http/handlers/tools/debug.js +43 -0
  93. package/dist/http/handlers/tools/debug.js.map +1 -0
  94. package/dist/http/handlers/tools/delegate.d.ts +4 -0
  95. package/dist/http/handlers/tools/delegate.d.ts.map +1 -0
  96. package/dist/http/handlers/tools/delegate.js +43 -0
  97. package/dist/http/handlers/tools/delegate.js.map +1 -0
  98. package/dist/http/handlers/tools/execute-plan.d.ts +4 -0
  99. package/dist/http/handlers/tools/execute-plan.d.ts.map +1 -0
  100. package/dist/http/handlers/tools/execute-plan.js +45 -0
  101. package/dist/http/handlers/tools/execute-plan.js.map +1 -0
  102. package/dist/http/handlers/tools/investigate.d.ts +4 -0
  103. package/dist/http/handlers/tools/investigate.d.ts.map +1 -0
  104. package/dist/http/handlers/tools/investigate.js +64 -0
  105. package/dist/http/handlers/tools/investigate.js.map +1 -0
  106. package/dist/http/handlers/tools/journal-recall.d.ts +4 -0
  107. package/dist/http/handlers/tools/journal-recall.d.ts.map +1 -0
  108. package/dist/http/handlers/tools/journal-recall.js +40 -0
  109. package/dist/http/handlers/tools/journal-recall.js.map +1 -0
  110. package/dist/http/handlers/tools/journal-record.d.ts +8 -0
  111. package/dist/http/handlers/tools/journal-record.d.ts.map +1 -0
  112. package/dist/http/handlers/tools/journal-record.js +40 -0
  113. package/dist/http/handlers/tools/journal-record.js.map +1 -0
  114. package/dist/http/handlers/tools/research.d.ts +4 -0
  115. package/dist/http/handlers/tools/research.d.ts.map +1 -0
  116. package/dist/http/handlers/tools/research.js +64 -0
  117. package/dist/http/handlers/tools/research.js.map +1 -0
  118. package/dist/http/handlers/tools/retry.d.ts +4 -0
  119. package/dist/http/handlers/tools/retry.d.ts.map +1 -0
  120. package/dist/http/handlers/tools/retry.js +73 -0
  121. package/dist/http/handlers/tools/retry.js.map +1 -0
  122. package/dist/http/handlers/tools/review.d.ts +4 -0
  123. package/dist/http/handlers/tools/review.d.ts.map +1 -0
  124. package/dist/http/handlers/tools/review.js +43 -0
  125. package/dist/http/handlers/tools/review.js.map +1 -0
  126. package/dist/http/journal-lock.d.ts +4 -0
  127. package/dist/http/journal-lock.d.ts.map +1 -0
  128. package/dist/http/journal-lock.js +34 -0
  129. package/dist/http/journal-lock.js.map +1 -0
  130. package/dist/http/middleware/body-reader.d.ts +16 -0
  131. package/dist/http/middleware/body-reader.d.ts.map +1 -0
  132. package/dist/http/middleware/body-reader.js +44 -0
  133. package/dist/http/middleware/body-reader.js.map +1 -0
  134. package/dist/http/middleware/caller-identity.d.ts +16 -0
  135. package/dist/http/middleware/caller-identity.d.ts.map +1 -0
  136. package/dist/http/middleware/caller-identity.js +16 -0
  137. package/dist/http/middleware/caller-identity.js.map +1 -0
  138. package/dist/http/middleware/decompress.d.ts +14 -0
  139. package/dist/http/middleware/decompress.d.ts.map +1 -0
  140. package/dist/http/middleware/decompress.js +51 -0
  141. package/dist/http/middleware/decompress.js.map +1 -0
  142. package/dist/http/project-registry.d.ts +54 -0
  143. package/dist/http/project-registry.d.ts.map +1 -0
  144. package/dist/http/project-registry.js +130 -0
  145. package/dist/http/project-registry.js.map +1 -0
  146. package/dist/http/request-observability.d.ts +8 -0
  147. package/dist/http/request-observability.d.ts.map +1 -0
  148. package/dist/http/request-observability.js +20 -0
  149. package/dist/http/request-observability.js.map +1 -0
  150. package/dist/http/request-pipeline.d.ts +16 -0
  151. package/dist/http/request-pipeline.d.ts.map +1 -0
  152. package/dist/http/request-pipeline.js +144 -0
  153. package/dist/http/request-pipeline.js.map +1 -0
  154. package/dist/http/server.d.ts +17 -0
  155. package/dist/http/server.d.ts.map +1 -0
  156. package/dist/http/server.js +300 -0
  157. package/dist/http/server.js.map +1 -0
  158. package/dist/http/types.d.ts +20 -0
  159. package/dist/http/types.d.ts.map +1 -0
  160. package/dist/http/types.js +2 -0
  161. package/dist/http/types.js.map +1 -0
  162. package/dist/skill-install/disabled-state.d.ts +35 -0
  163. package/dist/skill-install/disabled-state.d.ts.map +1 -0
  164. package/dist/skill-install/disabled-state.js +96 -0
  165. package/dist/skill-install/disabled-state.js.map +1 -0
  166. package/dist/skill-install/discover.d.ts +29 -0
  167. package/dist/skill-install/discover.d.ts.map +1 -0
  168. package/dist/skill-install/discover.js +104 -0
  169. package/dist/skill-install/discover.js.map +1 -0
  170. package/dist/skill-install/include-utils.d.ts +27 -0
  171. package/dist/skill-install/include-utils.d.ts.map +1 -0
  172. package/dist/skill-install/include-utils.js +90 -0
  173. package/dist/skill-install/include-utils.js.map +1 -0
  174. package/dist/skill-install/manifest.d.ts +82 -0
  175. package/dist/skill-install/manifest.d.ts.map +1 -0
  176. package/dist/skill-install/manifest.js +215 -0
  177. package/dist/skill-install/manifest.js.map +1 -0
  178. package/dist/skill-install/skill-installer-common.d.ts +26 -0
  179. package/dist/skill-install/skill-installer-common.d.ts.map +1 -0
  180. package/dist/skill-install/skill-installer-common.js +139 -0
  181. package/dist/skill-install/skill-installer-common.js.map +1 -0
  182. package/dist/skill-install/skill-installers/claude-code.d.ts +43 -0
  183. package/dist/skill-install/skill-installers/claude-code.d.ts.map +1 -0
  184. package/dist/skill-install/skill-installers/claude-code.js +65 -0
  185. package/dist/skill-install/skill-installers/claude-code.js.map +1 -0
  186. package/dist/skill-install/skill-installers/codex-cli.d.ts +27 -0
  187. package/dist/skill-install/skill-installers/codex-cli.d.ts.map +1 -0
  188. package/dist/skill-install/skill-installers/codex-cli.js +84 -0
  189. package/dist/skill-install/skill-installers/codex-cli.js.map +1 -0
  190. package/dist/skill-install/skill-installers/cursor.d.ts +72 -0
  191. package/dist/skill-install/skill-installers/cursor.d.ts.map +1 -0
  192. package/dist/skill-install/skill-installers/cursor.js +81 -0
  193. package/dist/skill-install/skill-installers/cursor.js.map +1 -0
  194. package/dist/skill-install/skill-installers/gemini-cli.d.ts +50 -0
  195. package/dist/skill-install/skill-installers/gemini-cli.d.ts.map +1 -0
  196. package/dist/skill-install/skill-installers/gemini-cli.js +72 -0
  197. package/dist/skill-install/skill-installers/gemini-cli.js.map +1 -0
  198. package/dist/skill-install/skill-manifest-sync.d.ts +11 -0
  199. package/dist/skill-install/skill-manifest-sync.d.ts.map +1 -0
  200. package/dist/skill-install/skill-manifest-sync.js +65 -0
  201. package/dist/skill-install/skill-manifest-sync.js.map +1 -0
  202. package/dist/skills/_shared/auth.md +41 -0
  203. package/dist/skills/_shared/error-handling.md +31 -0
  204. package/dist/skills/_shared/polling.md +88 -0
  205. package/dist/skills/_shared/response-shape.md +55 -0
  206. package/dist/skills/_shared/review-policy.md +15 -0
  207. package/dist/skills/mma-audit/SKILL.md +270 -0
  208. package/dist/skills/mma-context-blocks/SKILL.md +148 -0
  209. package/dist/skills/mma-debug/SKILL.md +208 -0
  210. package/dist/skills/mma-delegate/SKILL.md +216 -0
  211. package/dist/skills/mma-execute-plan/SKILL.md +214 -0
  212. package/dist/skills/mma-explore/SKILL.md +190 -0
  213. package/dist/skills/mma-investigate/SKILL.md +258 -0
  214. package/dist/skills/mma-journal-recall/SKILL.md +242 -0
  215. package/dist/skills/mma-journal-record/SKILL.md +202 -0
  216. package/dist/skills/mma-research/SKILL.md +223 -0
  217. package/dist/skills/mma-retry/SKILL.md +221 -0
  218. package/dist/skills/mma-review/SKILL.md +209 -0
  219. package/dist/skills/multi-model-agent/SKILL.md +206 -0
  220. package/dist/telemetry/consent.d.ts +4 -0
  221. package/dist/telemetry/consent.d.ts.map +1 -0
  222. package/dist/telemetry/consent.js +40 -0
  223. package/dist/telemetry/consent.js.map +1 -0
  224. package/dist/telemetry/flusher.d.ts +19 -0
  225. package/dist/telemetry/flusher.d.ts.map +1 -0
  226. package/dist/telemetry/flusher.js +277 -0
  227. package/dist/telemetry/flusher.js.map +1 -0
  228. package/dist/telemetry/generation.d.ts +9 -0
  229. package/dist/telemetry/generation.d.ts.map +1 -0
  230. package/dist/telemetry/generation.js +33 -0
  231. package/dist/telemetry/generation.js.map +1 -0
  232. package/dist/telemetry/identity.d.ts +9 -0
  233. package/dist/telemetry/identity.d.ts.map +1 -0
  234. package/dist/telemetry/identity.js +35 -0
  235. package/dist/telemetry/identity.js.map +1 -0
  236. package/dist/telemetry/install-id.d.ts +13 -0
  237. package/dist/telemetry/install-id.d.ts.map +1 -0
  238. package/dist/telemetry/install-id.js +49 -0
  239. package/dist/telemetry/install-id.js.map +1 -0
  240. package/dist/telemetry/install-meta.d.ts +10 -0
  241. package/dist/telemetry/install-meta.d.ts.map +1 -0
  242. package/dist/telemetry/install-meta.js +15 -0
  243. package/dist/telemetry/install-meta.js.map +1 -0
  244. package/dist/telemetry/queue.d.ts +35 -0
  245. package/dist/telemetry/queue.d.ts.map +1 -0
  246. package/dist/telemetry/queue.js +287 -0
  247. package/dist/telemetry/queue.js.map +1 -0
  248. package/dist/telemetry/recorder.d.ts +39 -0
  249. package/dist/telemetry/recorder.d.ts.map +1 -0
  250. package/dist/telemetry/recorder.js +173 -0
  251. package/dist/telemetry/recorder.js.map +1 -0
  252. package/package.json +43 -24
  253. package/scripts/postinstall.js +36 -0
  254. package/bin/mmagent.mjs +0 -47
  255. package/postinstall.mjs +0 -8
@@ -0,0 +1,15 @@
1
+ import type { MultiModelConfig } from '@zhixuan92/multi-model-agent-core';
2
+ export interface LogsDeps {
3
+ config: MultiModelConfig;
4
+ homeDir?: string;
5
+ follow?: boolean;
6
+ batchId?: string;
7
+ /** Polling interval when --follow; defaults to 300ms. */
8
+ pollMs?: number;
9
+ /** Max time to wait for the log file to appear under --follow; defaults to 30s. */
10
+ waitForLogMs?: number;
11
+ stdout?: (s: string) => boolean;
12
+ stderr?: (s: string) => boolean;
13
+ }
14
+ export declare function runLogs(deps: LogsDeps): Promise<number>;
15
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../../src/cli/logs.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAE1E,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CACjC;AAeD,wBAAsB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CA0E7D"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * logs.ts — `mmagent logs` subcommand.
3
+ *
4
+ * Tails the diagnostic log file for today (mmagent-YYYY-MM-DD.jsonl). Supports
5
+ * --follow for tail-F semantics and --batch=<id> to filter to a single batch.
6
+ *
7
+ * Exit codes:
8
+ * 0 — success (including "no log file found" cases so scripts don't break)
9
+ * 1 — reserved for future use
10
+ */
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import * as fs from 'node:fs';
14
+ function todayUtc() {
15
+ return new Date().toISOString().slice(0, 10);
16
+ }
17
+ function resolveLogPath(config, homeDir) {
18
+ const dir = config.diagnostics?.logDir ?? path.join(homeDir, '.multi-model', 'logs');
19
+ return path.join(dir, `mmagent-${todayUtc()}.jsonl`);
20
+ }
21
+ function matchesBatch(line, batchId) {
22
+ return line.includes(`"batchId":"${batchId}"`);
23
+ }
24
+ export async function runLogs(deps) {
25
+ const stdout = deps.stdout ?? process.stdout.write.bind(process.stdout);
26
+ const stderr = deps.stderr ?? process.stderr.write.bind(process.stderr);
27
+ const homeDir = deps.homeDir ?? os.homedir();
28
+ const follow = deps.follow ?? false;
29
+ const pollMs = deps.pollMs ?? 300;
30
+ const waitForLogMs = deps.waitForLogMs ?? 30_000;
31
+ if (!deps.config.diagnostics?.log) {
32
+ stderr(`mmagent logs: diagnostics.log is false in config; set it to true to capture new events.\n`);
33
+ }
34
+ const logPath = resolveLogPath(deps.config, homeDir);
35
+ if (!fs.existsSync(logPath)) {
36
+ if (!follow) {
37
+ stderr(`mmagent logs: no log file at ${logPath}. Start the server with diagnostics.log: true and try again.\n`);
38
+ return 0;
39
+ }
40
+ const deadline = Date.now() + waitForLogMs;
41
+ while (!fs.existsSync(logPath) && Date.now() < deadline) {
42
+ await new Promise((r) => setTimeout(r, pollMs));
43
+ }
44
+ if (!fs.existsSync(logPath)) {
45
+ stderr(`mmagent logs: no log file appeared within ${Math.floor(waitForLogMs / 1000)}s at ${logPath}.\n`);
46
+ return 0;
47
+ }
48
+ }
49
+ // Emit existing content (optionally batch-filtered).
50
+ let offset = 0;
51
+ try {
52
+ const existing = fs.readFileSync(logPath, 'utf8');
53
+ for (const line of existing.split('\n')) {
54
+ if (line.length === 0)
55
+ continue;
56
+ if (deps.batchId && !matchesBatch(line, deps.batchId))
57
+ continue;
58
+ stdout(line + '\n');
59
+ }
60
+ offset = existing.length;
61
+ }
62
+ catch (err) {
63
+ stderr(`mmagent logs: cannot read ${logPath}: ${err instanceof Error ? err.message : String(err)}\n`);
64
+ return 0;
65
+ }
66
+ if (!follow)
67
+ return 0;
68
+ // Tail — poll for new content appended after `offset`.
69
+ let buf = '';
70
+ while (true) {
71
+ await new Promise((r) => setTimeout(r, pollMs));
72
+ let stat;
73
+ try {
74
+ stat = fs.statSync(logPath);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (stat.size <= offset)
80
+ continue;
81
+ const fd = fs.openSync(logPath, 'r');
82
+ try {
83
+ const chunk = Buffer.alloc(stat.size - offset);
84
+ fs.readSync(fd, chunk, 0, chunk.length, offset);
85
+ buf += chunk.toString('utf8');
86
+ offset = stat.size;
87
+ }
88
+ finally {
89
+ fs.closeSync(fd);
90
+ }
91
+ const lines = buf.split('\n');
92
+ buf = lines.pop() ?? ''; // keep any trailing partial line for the next iteration
93
+ for (const line of lines) {
94
+ if (line.length === 0)
95
+ continue;
96
+ if (deps.batchId && !matchesBatch(line, deps.batchId))
97
+ continue;
98
+ stdout(line + '\n');
99
+ }
100
+ }
101
+ }
102
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.js","sourceRoot":"","sources":["../../src/cli/logs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface PrintTokenDeps {
2
+ /** Home directory (defaults to os.homedir()). */
3
+ homeDir?: string;
4
+ /** Token file path (already expanded). Overrides config discovery. */
5
+ tokenFile?: string;
6
+ /** Environment variable accessor. Defaults to process.env. */
7
+ env?: Record<string, string | undefined>;
8
+ /** Write to stdout. Defaults to process.stdout.write. */
9
+ stdout?: (s: string) => boolean;
10
+ /** Write to stderr. Defaults to process.stderr.write. */
11
+ stderr?: (s: string) => boolean;
12
+ }
13
+ /**
14
+ * Read the bearer token and print it to stdout.
15
+ * Returns 0 on success, 1 on error.
16
+ */
17
+ export declare function printToken(deps?: PrintTokenDeps): number;
18
+ //# sourceMappingURL=print-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"print-token.d.ts","sourceRoot":"","sources":["../../src/cli/print-token.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,yDAAyD;IACzD,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,yDAAyD;IACzD,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,CAsC5D"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * print-token.ts — `mmagent print-token` subcommand.
3
+ *
4
+ * Reads the bearer auth token and prints it to stdout.
5
+ * Env override (MMAGENT_AUTH_TOKEN) wins over any file.
6
+ * Missing file → prints an error message to stderr and exits 1.
7
+ *
8
+ * Usage:
9
+ * mmagent print-token [--config <path>]
10
+ */
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import * as fs from 'node:fs';
14
+ /** Expand a leading '~/' to the home directory. */
15
+ function expandHome(p, homeDir) {
16
+ if (p.startsWith('~/'))
17
+ return path.join(homeDir, p.slice(2));
18
+ return p;
19
+ }
20
+ /**
21
+ * Read the bearer token and print it to stdout.
22
+ * Returns 0 on success, 1 on error.
23
+ */
24
+ export function printToken(deps = {}) {
25
+ const homeDir = deps.homeDir ?? os.homedir();
26
+ const env = deps.env ?? process.env;
27
+ const stdout = deps.stdout ?? process.stdout.write.bind(process.stdout);
28
+ const stderr = deps.stderr ?? process.stderr.write.bind(process.stderr);
29
+ // Env override wins
30
+ const envToken = (env['MMAGENT_AUTH_TOKEN'] ?? '').trim();
31
+ if (envToken.length > 0) {
32
+ stdout(envToken + '\n');
33
+ return 0;
34
+ }
35
+ // Fall back to token file
36
+ const rawTokenFile = deps.tokenFile ?? path.join(homeDir, '.multi-model', 'auth-token');
37
+ const tokenFile = expandHome(rawTokenFile, homeDir);
38
+ try {
39
+ const token = fs.readFileSync(tokenFile, 'utf-8').trim();
40
+ if (token.length === 0) {
41
+ stderr(`mmagent: token file is empty: ${tokenFile}\n`);
42
+ return 1;
43
+ }
44
+ stdout(token + '\n');
45
+ return 0;
46
+ }
47
+ catch (err) {
48
+ const code = err.code;
49
+ if (code === 'ENOENT') {
50
+ stderr(`mmagent: token file not found: ${tokenFile}\n` +
51
+ `Run 'mmagent serve' once to generate a token, or set MMAGENT_AUTH_TOKEN.\n`);
52
+ }
53
+ else {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ stderr(`mmagent: cannot read token file ${tokenFile}: ${msg}\n`);
56
+ }
57
+ return 1;
58
+ }
59
+ }
60
+ //# sourceMappingURL=print-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"print-token.js","sourceRoot":"","sources":["../../src/cli/print-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,mDAAmD;AACnD,SAAS,UAAU,CAAC,CAAS,EAAE,OAAe;IAC5C,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,OAAO,CAAC,CAAC;AACX,CAAC;AAeD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,OAAuB,EAAE;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAExE,oBAAoB;IACpB,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;QACxB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,0BAA0B;IAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;IACxF,MAAM,SAAS,GAAG,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEpD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACzD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,iCAAiC,SAAS,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;QACjD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtB,MAAM,CACJ,kCAAkC,SAAS,IAAI;gBAC/C,4EAA4E,CAC7E,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,MAAM,CAAC,mCAAmC,SAAS,KAAK,GAAG,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { MultiModelConfig } from '@zhixuan92/multi-model-agent-core';
2
+ export declare function maybeAutoUpdateSkills(config: MultiModelConfig, stderr: (s: string) => boolean): Promise<void>;
3
+ /** A running server handle returned by startServe(). */
4
+ export interface ServeHandle {
5
+ /** The port the server is listening on (useful when port=0 for ephemeral). */
6
+ port: number;
7
+ /**
8
+ * Gracefully shut down the server.
9
+ * Removes any registered SIGTERM/SIGINT handlers to prevent leaks.
10
+ * After this resolves, the process is no longer listening and may exit safely.
11
+ */
12
+ stop(): Promise<void>;
13
+ }
14
+ /**
15
+ * Start the HTTP server with the given config.
16
+ *
17
+ * Registers SIGTERM and SIGINT handlers that drain in-flight requests and
18
+ * exit the process cleanly. If config includes `server.limits.shutdownDrainMs`,
19
+ * the server will wait up to that duration for in-flight requests to finish.
20
+ *
21
+ * @param config Full MultiModelConfig (includes agents.*, defaults, diagnostics,
22
+ * and server block). startServer() inspects the agents.* field
23
+ * and enables real tool handlers when present.
24
+ * @param exit Process exit function — defaults to process.exit.
25
+ * Exposed so tests can suppress actual exits.
26
+ */
27
+ export declare function startServe(config: MultiModelConfig, exit?: (code: number) => never): Promise<ServeHandle>;
28
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AA2B1E,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,GAC7B,OAAO,CAAC,IAAI,CAAC,CAsCf;AAiBD,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAkC,GACzD,OAAO,CAAC,WAAW,CAAC,CA2QtB"}
@@ -0,0 +1,405 @@
1
+ /**
2
+ * serve.ts — starts the HTTP server and manages its signal lifecycle.
3
+ *
4
+ * This module owns the complete serve lifecycle: starting the HTTP server,
5
+ * registering SIGTERM/SIGINT handlers, draining in-flight requests, and
6
+ * cleanly exiting the process. The CLI entry point (cli/index.ts) delegates
7
+ * to this module and does not manage signals directly.
8
+ *
9
+ * Usage (library):
10
+ * const handle = await startServe(config);
11
+ * // server is running on handle.port
12
+ * await handle.stop(); // graceful shutdown; no process.exit
13
+ *
14
+ * Usage (CLI):
15
+ * mmagent serve [--config <path>]
16
+ * // this module owns signal handling and process.exit
17
+ */
18
+ import { createHash, randomUUID } from 'node:crypto';
19
+ import * as path from 'node:path';
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { collectInlineApiKeyOffenders, loadAuthToken } from '@zhixuan92/multi-model-agent-core';
24
+ import { startServer } from '../http/server.js';
25
+ import { setDraining } from '../http/request-pipeline.js';
26
+ import { createRecorder } from '../telemetry/recorder.js';
27
+ import { Flusher } from '../telemetry/flusher.js';
28
+ import { Queue } from '../telemetry/queue.js';
29
+ import { runSyncSkills } from './sync-skills.js';
30
+ import { listEntries, FutureManifestError } from '../skill-install/manifest.js';
31
+ import { readSkillContent, SUPPORTED_SKILLS } from '../skill-install/discover.js';
32
+ import { findMissingSkills } from '../skill-install/skill-installer-common.js';
33
+ import matter from 'gray-matter';
34
+ function isSkillBehind(entryName, entrySkillVersion) {
35
+ const src = readSkillContent(entryName);
36
+ if (src === null)
37
+ return false; // skill removed from bundle — sync-skills will drop it
38
+ try {
39
+ const parsed = matter(src);
40
+ const v = parsed.data['version'];
41
+ return typeof v === 'string' && v !== entrySkillVersion;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export async function maybeAutoUpdateSkills(config, stderr) {
48
+ let entries;
49
+ try {
50
+ entries = listEntries();
51
+ }
52
+ catch (err) {
53
+ if (err instanceof FutureManifestError) {
54
+ stderr(`[mmagent] warning: ${err.message}; skipping skill auto-sync\n`);
55
+ return;
56
+ }
57
+ return; // best-effort — never let manifest IO issues block serve
58
+ }
59
+ const behind = entries.filter((e) => isSkillBehind(e.name, e.skillVersion));
60
+ const missing = findMissingSkills(entries, SUPPORTED_SKILLS);
61
+ if (behind.length === 0 && missing.length === 0)
62
+ return;
63
+ if (!config.server.autoUpdateSkills) {
64
+ const drift = [];
65
+ if (behind.length > 0)
66
+ drift.push(`${behind.length} out of date (${behind.map((e) => e.name).join(', ')})`);
67
+ if (missing.length > 0)
68
+ drift.push(`${missing.length} new (${missing.map((m) => m.name).join(', ')})`);
69
+ stderr(`[mmagent] skill drift: ${drift.join('; ')}. ` +
70
+ `Run 'mmagent sync-skills' to reconcile (or set server.autoUpdateSkills=true in config).\n`);
71
+ return;
72
+ }
73
+ const deadlineMs = 5000;
74
+ try {
75
+ await Promise.race([
76
+ runSyncSkills({ silent: true, bestEffort: true, ifExists: true }),
77
+ new Promise((resolve) => setTimeout(() => resolve(), deadlineMs)),
78
+ ]);
79
+ if (behind.length > 0)
80
+ process.stdout.write(`[mmagent] auto-synced ${behind.length} updated skill(s)\n`);
81
+ if (missing.length > 0)
82
+ process.stdout.write(`[mmagent] auto-synced ${missing.length} new skill(s): ${missing.map((m) => m.name).join(', ')}\n`);
83
+ }
84
+ catch {
85
+ // bestEffort swallows inside; extra safety here.
86
+ }
87
+ }
88
+ function readServerVersion() {
89
+ try {
90
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
91
+ const pkgPath = path.join(thisDir, '..', '..', 'package.json');
92
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
93
+ return pkg.version ?? '0.0.0';
94
+ }
95
+ catch {
96
+ return '0.0.0';
97
+ }
98
+ }
99
+ function envVarHint(agentName) {
100
+ return `${agentName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_API_KEY`;
101
+ }
102
+ /**
103
+ * Shared signal-state used to deduplicate shutdown if two signals arrive
104
+ * before stop() resolves.
105
+ */
106
+ let stopInFlight = false;
107
+ // Stored so they can be removed when stop() is called programmatically
108
+ let onSigterm;
109
+ let onSigint;
110
+ let onStdoutErrorRef;
111
+ let onStderrErrorRef;
112
+ let onUncaughtRef;
113
+ let onUnhandledRejectionRef;
114
+ /**
115
+ * Start the HTTP server with the given config.
116
+ *
117
+ * Registers SIGTERM and SIGINT handlers that drain in-flight requests and
118
+ * exit the process cleanly. If config includes `server.limits.shutdownDrainMs`,
119
+ * the server will wait up to that duration for in-flight requests to finish.
120
+ *
121
+ * @param config Full MultiModelConfig (includes agents.*, defaults, diagnostics,
122
+ * and server block). startServer() inspects the agents.* field
123
+ * and enables real tool handlers when present.
124
+ * @param exit Process exit function — defaults to process.exit.
125
+ * Exposed so tests can suppress actual exits.
126
+ */
127
+ export async function startServe(config, exit = process.exit.bind(process)) {
128
+ const stderr = process.stderr.write.bind(process.stderr);
129
+ // Auto-update installed skills before bind (bounded 5s; never blocks indefinitely).
130
+ await maybeAutoUpdateSkills(config, stderr);
131
+ // Drift check — warn if installed skills don't match the canonical manifest.
132
+ try {
133
+ const { makeSkillManifestSync } = await import('../skill-install/skill-manifest-sync.js');
134
+ const { discoverPerClientInstallDirs } = await import('../skill-install/discover.js');
135
+ const sync = makeSkillManifestSync(discoverPerClientInstallDirs());
136
+ const drift = sync.driftReport();
137
+ if (drift.length > 0) {
138
+ const summary = drift.map(d => `${d.client}/${d.skill}=${d.issue}`).join(', ');
139
+ stderr(`[mmagent] WARN: skill manifest drift detected: ${summary}. Re-run 'mmagent sync-skills' to reconcile.\n`);
140
+ }
141
+ }
142
+ catch {
143
+ // best-effort — never let drift check block serve
144
+ }
145
+ // Create the telemetry recorder BEFORE startServer. The server's bus
146
+ // subscriber (TelemetryUploader) calls getRecorder() during startServer →
147
+ // if recorder is null at that moment, the uploader is wired with
148
+ // recorder=null and silently drops every event for the daemon's lifetime.
149
+ const homeDir = path.join(os.homedir(), '.multi-model');
150
+ const mmagentVersion = readServerVersion();
151
+ createRecorder({ homeDir, mmagentVersion });
152
+ // Pass the full MultiModelConfig (not just the server block) so
153
+ // registerToolHandlers sees `agents` and registers real tool endpoints.
154
+ // Stripping to { server } here caused a 3.1.0 regression where tool
155
+ // endpoints returned 503 'no_agent_config' even when agents were set.
156
+ const running = await startServer(config);
157
+ // ── stdout/stderr error + uncaught/unhandled rejection guards ────────
158
+ const logShutdown = (_cause) => {
159
+ // Option A: no diagnostics surface today. Cause name routed via stderr only.
160
+ // Option B (follow-up): wire running.diagnostics?.shutdown(_cause) here.
161
+ };
162
+ const onStdoutError = (err) => {
163
+ if (err.code === 'EPIPE') {
164
+ logShutdown('stdout_epipe');
165
+ exit(0);
166
+ }
167
+ logShutdown('stdout_other_error');
168
+ try {
169
+ process.stderr.write(`[mmagent] stdout error: ${err.message}\n`);
170
+ }
171
+ catch { /* stderr may also be dead */ }
172
+ exit(1);
173
+ };
174
+ const onStderrError = (err) => {
175
+ if (err.code === 'EPIPE') {
176
+ logShutdown('stdout_epipe');
177
+ exit(0);
178
+ }
179
+ logShutdown('stdout_other_error');
180
+ exit(1);
181
+ };
182
+ const onUncaught = (err) => {
183
+ const errno = err?.code;
184
+ if (errno === 'EPIPE') {
185
+ logShutdown('stdout_epipe');
186
+ exit(0);
187
+ }
188
+ logShutdown('uncaughtException');
189
+ try {
190
+ const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
191
+ process.stderr.write(`[mmagent] uncaught exception: ${msg}\n`);
192
+ }
193
+ catch { /* best-effort */ }
194
+ exit(1);
195
+ };
196
+ const onUnhandledRejection = (reason) => {
197
+ const errno = reason?.code;
198
+ if (errno === 'EPIPE') {
199
+ logShutdown('stdout_epipe');
200
+ exit(0);
201
+ }
202
+ logShutdown('unhandledRejection');
203
+ try {
204
+ const msg = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);
205
+ process.stderr.write(`[mmagent] unhandled rejection: ${msg}\n`);
206
+ }
207
+ catch { /* best-effort */ }
208
+ exit(1);
209
+ };
210
+ process.stdout.on('error', onStdoutError);
211
+ process.stderr.on('error', onStderrError);
212
+ process.on('uncaughtException', onUncaught);
213
+ process.on('unhandledRejection', onUnhandledRejection);
214
+ onStdoutErrorRef = onStdoutError;
215
+ onStderrErrorRef = onStderrError;
216
+ onUncaughtRef = onUncaught;
217
+ onUnhandledRejectionRef = onUnhandledRejection;
218
+ // Recorder was created above (BEFORE startServer). homeDir + mmagentVersion
219
+ // are computed there and reused here for the version-pin file + Flusher.
220
+ const lastVersionPath = path.join(homeDir, 'last-version');
221
+ let lastVersion = null;
222
+ try {
223
+ lastVersion = fs.readFileSync(lastVersionPath, 'utf8').trim();
224
+ }
225
+ catch {
226
+ // first run — no last-version file yet
227
+ }
228
+ if (lastVersion !== mmagentVersion) {
229
+ try {
230
+ fs.mkdirSync(homeDir, { recursive: true });
231
+ fs.writeFileSync(lastVersionPath, mmagentVersion + '\n', { mode: 0o600 });
232
+ }
233
+ catch (err) {
234
+ stderr(`[mmagent] warning: failed to write last-version at ${lastVersionPath}: ${err instanceof Error ? err.message : String(err)}\n`);
235
+ }
236
+ }
237
+ // Telemetry uploader. Default endpoint ships to the project's hosted
238
+ // dashboard. MMAGENT_TELEMETRY_ENDPOINT overrides for self-hosted backends;
239
+ // setting it to an empty string disables shipping entirely (events stay in
240
+ // ~/.multi-model/telemetry-queue.ndjson). The real off-switch for telemetry
241
+ // is the consent flag (MMAGENT_TELEMETRY=0 / config.telemetry.enabled =
242
+ // false) — when consent is off the recorder enqueues nothing, so the
243
+ // flusher's tick is a no-op even with the default endpoint set.
244
+ const DEFAULT_TELEMETRY_ENDPOINT = 'https://mma-telemetry-frontend.x1.lucazhang.work/v1/events';
245
+ const envEndpoint = process.env.MMAGENT_TELEMETRY_ENDPOINT;
246
+ const telemetryEndpoint = envEndpoint === undefined
247
+ ? DEFAULT_TELEMETRY_ENDPOINT
248
+ : envEndpoint.trim();
249
+ let flusher = null;
250
+ if (telemetryEndpoint) {
251
+ flusher = new Flusher({
252
+ queue: new Queue(homeDir),
253
+ dir: homeDir,
254
+ endpoint: telemetryEndpoint,
255
+ });
256
+ flusher.start();
257
+ }
258
+ // Fire once at serve startup. Lives here (not in loadConfigFromFile) so
259
+ // print-token / info / status don't re-emit the same warning repeatedly.
260
+ const inlineOffenders = collectInlineApiKeyOffenders(config);
261
+ if (inlineOffenders.length > 0) {
262
+ const firstHint = envVarHint(inlineOffenders[0]);
263
+ stderr(`[mmagent] WARNING: inline apiKey in config for agent(s): ${inlineOffenders.join(', ')}.\n` +
264
+ ` Fix:\n` +
265
+ ` export ${firstHint}='<your-key>'\n` +
266
+ ` # then in config.json, replace\n` +
267
+ ` # "apiKey": "..."\n` +
268
+ ` # with\n` +
269
+ ` # "apiKeyEnv": "${firstHint}"\n`);
270
+ }
271
+ const cleanupSignal = (sig) => {
272
+ if (stopInFlight)
273
+ return;
274
+ stopInFlight = true;
275
+ stderr(`[mmagent] received ${sig}, shutting down gracefully\u2026\n`);
276
+ // 1) Refuse new dispatches immediately so they don't compound the drain.
277
+ setDraining(true);
278
+ // 2) Walk BatchRegistry: close every in-flight task's sessions in parallel
279
+ // with a 5-second wall clock. After the grace window, SIGKILL any
280
+ // subprocess still alive whose session exposes a pid. Without this,
281
+ // SIGTERM-killed daemons leaked codex children as orphans (see 2026-05-16
282
+ // OOM post-mortem).
283
+ const SHUTDOWN_GRACE_MS = 5000;
284
+ const inflight = running.batchRegistry?.allInFlight?.() ?? [];
285
+ const closeAll = Promise.allSettled(inflight.flatMap((entry) => {
286
+ const ctxs = entry.executionContexts ? Array.from(entry.executionContexts.values()) : [];
287
+ return ctxs.map(async (ec) => {
288
+ try {
289
+ await ec.closeSessions();
290
+ }
291
+ catch { /* swallow */ }
292
+ });
293
+ }));
294
+ const drainSessions = Promise.race([
295
+ closeAll,
296
+ new Promise((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_MS).unref()),
297
+ ]).then(() => {
298
+ // Belt-and-suspenders: SIGKILL anything still alive whose pid we know.
299
+ for (const entry of inflight) {
300
+ const ctxs = entry.executionContexts ? Array.from(entry.executionContexts.values()) : [];
301
+ for (const ec of ctxs) {
302
+ for (const pid of ec.getActivePids?.() ?? []) {
303
+ try {
304
+ process.kill(pid, 'SIGKILL');
305
+ }
306
+ catch { /* already dead */ }
307
+ }
308
+ }
309
+ }
310
+ });
311
+ const drainTelemetry = flusher ? flusher.drain() : Promise.resolve();
312
+ drainSessions
313
+ .catch(() => { })
314
+ .then(() => drainTelemetry)
315
+ .catch(() => { })
316
+ .then(() => running.stop())
317
+ .then(() => exit(0))
318
+ .catch((err) => {
319
+ const msg = err instanceof Error ? err.message : String(err);
320
+ stderr(`[mmagent] shutdown failed: ${msg}\n`);
321
+ exit(1);
322
+ });
323
+ };
324
+ // Register handlers using named references so they can be removed correctly.
325
+ // Using anonymous wrappers (e.g. `process.once('SIGTERM', () => fn(sig))`)
326
+ // would make process.off(sig, fn) unable to find and remove the listener.
327
+ onSigterm = () => cleanupSignal('SIGTERM');
328
+ onSigint = () => cleanupSignal('SIGINT');
329
+ process.once('SIGTERM', onSigterm);
330
+ process.once('SIGINT', onSigint);
331
+ // Print the actual bound address so operators see what the kernel assigned
332
+ // (useful when port=0 selects an ephemeral port).
333
+ const host = running.serverAddress ?? config.server.bind;
334
+ // Emit a single structured startup line before the "listening" line.
335
+ // Fingerprint the auth token (first 8 hex of sha256) so operators can verify
336
+ // the running instance matches what their clients are using, without ever
337
+ // revealing the token. bootId discriminates successive startups from the same pid.
338
+ try {
339
+ const token = loadAuthToken({ tokenFile: config.server.auth.tokenFile });
340
+ const fp = createHash('sha256').update(token).digest('hex').slice(0, 8);
341
+ const bootId = randomUUID();
342
+ const version = readServerVersion();
343
+ process.stdout.write(`[mmagent] started | version=${version} | bind=${host}:${running.port} | pid=${process.pid} | token=${fp} | boot=${bootId}\n`);
344
+ }
345
+ catch {
346
+ // Token load shouldn't fail here (startServer already validated it), but
347
+ // if it does, skip the startup line rather than crash the server.
348
+ }
349
+ // Per-tier model lines so operators can see which provider is wired to
350
+ // each agent slot. The complex tier handles read-only sub-workers + most
351
+ // implementer work; the standard tier handles annotator/reviewer + the
352
+ // explore route's internal half. When a tier is unconfigured, log it as
353
+ // "(not configured)" so a misconfigured slot is visible at boot time.
354
+ const fmtTier = (slot) => {
355
+ const cfg = config.agents[slot];
356
+ if (!cfg || !cfg.model)
357
+ return '(not configured)';
358
+ return `${cfg.model} [${cfg.type ?? 'unknown'}]`;
359
+ };
360
+ process.stdout.write(`[mmagent] tiers | complex=${fmtTier('complex')} | standard=${fmtTier('standard')}\n`);
361
+ // A4a.4 (4.2.2+): warn when stale Claude Code project siblings exist
362
+ // under /tmp/claude/G--*. These come from prior Claude Code test runs
363
+ // and confuse worker cwd resolution if a caller passes one as ?cwd=.
364
+ // The validator already rejects them at request time (A4a.1); this
365
+ // startup scan surfaces the contamination so operators clean it up.
366
+ // Pure log behavior — does NOT block startup.
367
+ for (const root of ['/tmp/claude', '/private/tmp/claude']) {
368
+ try {
369
+ if (!fs.existsSync(root))
370
+ continue;
371
+ const stale = fs.readdirSync(root).filter(e => e.startsWith('G--'));
372
+ if (stale.length > 0) {
373
+ process.stdout.write(`[mmagent] WARNING: ${stale.length} stale Claude Code project sibling(s) under ${root}/G--*. ` +
374
+ `These can confuse cwd resolution; clean up with: rm -rf ${root}/G--*\n`);
375
+ }
376
+ }
377
+ catch { /* swallow — log-only */ }
378
+ }
379
+ process.stdout.write(`[mmagent] listening on ${host}:${running.port}\n`);
380
+ return {
381
+ port: running.port,
382
+ stop: async () => {
383
+ // Clean up signal listeners to prevent leaks when stop() is called
384
+ // programmatically (i.e. not via a signal).
385
+ if (onSigterm)
386
+ process.off('SIGTERM', onSigterm);
387
+ if (onSigint)
388
+ process.off('SIGINT', onSigint);
389
+ if (onStdoutErrorRef)
390
+ process.stdout.off('error', onStdoutErrorRef);
391
+ if (onStderrErrorRef)
392
+ process.stderr.off('error', onStderrErrorRef);
393
+ if (onUncaughtRef)
394
+ process.off('uncaughtException', onUncaughtRef);
395
+ if (onUnhandledRejectionRef)
396
+ process.off('unhandledRejection', onUnhandledRejectionRef);
397
+ onStdoutErrorRef = onStderrErrorRef = onUncaughtRef = onUnhandledRejectionRef = undefined;
398
+ if (flusher) {
399
+ await flusher.drain().catch(() => { });
400
+ }
401
+ await running.stop();
402
+ },
403
+ };
404
+ }
405
+ //# sourceMappingURL=serve.js.map