@microi.net/cli 5.5.9 → 5.6.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 (166) hide show
  1. package/.codebuddy-plugin/marketplace.json +2 -2
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.mcp.json +1 -1
  5. package/.workbuddy-plugin/marketplace.json +2 -2
  6. package/.workbuddy-plugin/plugin.json +1 -1
  7. package/README.md +71 -71
  8. package/assets/build-meta.json +5 -5
  9. package/assets/feature-matrix.json +129 -129
  10. package/cordis.patch.yml +1 -1
  11. package/package.json +1 -1
  12. package/scripts/codex-marketplace.json +20 -20
  13. package/scripts/mcp-codex-stdio-adapter.js +189 -189
  14. package/scripts/mcp-trae-windows-launcher.cmd +21 -21
  15. package/scripts/microi-cli-mcp.js +7 -7
  16. package/scripts/microi-codex-broker.js +447 -447
  17. package/scripts/microi-codex-router.js +582 -582
  18. package/scripts/microi-skills.meta.json +336 -336
  19. package/skills/.microi-skills-version.json +2 -2
  20. package/skills/.progressive-disclosure-manifest.json +3566 -3566
  21. package/skills/README.md +3 -3
  22. package/skills/ai-engine/SKILL.md +45 -45
  23. package/skills/ai-platform-governance/SKILL.md +177 -177
  24. package/skills/ai-platform-governance/references/progressive-01-/345/212/237/350/203/275/345/274/200/345/205/263.md +190 -190
  25. package/skills/app-store/SKILL.md +177 -177
  26. package/skills/business-blueprint/SKILL.md +85 -85
  27. package/skills/datasource-engine/SKILL.md +91 -91
  28. package/skills/dos-orm/SKILL.md +81 -81
  29. package/skills/dos-orm/references/api-reference.md +229 -229
  30. package/skills/email-engine/SKILL.md +80 -80
  31. package/skills/email-engine/references/v8-email.md +34 -34
  32. package/skills/job-engine/SKILL.md +154 -154
  33. package/skills/message-notification/SKILL.md +126 -126
  34. package/skills/message-notification/agents/openai.yaml +5 -5
  35. package/skills/message-notification/references/contracts.md +102 -102
  36. package/skills/microi-ai-app-auth.js +652 -652
  37. package/skills/microi-ai-application/SKILL.md +102 -102
  38. package/skills/microi-ai-application/agents/openai.yaml +4 -4
  39. package/skills/microi-ai-application/references/frontend-baseline.md +164 -164
  40. package/skills/microi-client-frontend/SKILL.md +43 -43
  41. package/skills/microi-client-frontend/references/progressive-01-3-/345/212/250/346/200/201/346/214/211/351/222/256/347/263/273/347/273/237.md +2 -2
  42. package/skills/microi-client-frontend/references/progressive-02-8-/350/277/220/350/241/214/346/227/266/351/253/230/351/242/221/345/235/221/345/244/215/347/233/230.md +28 -28
  43. package/skills/microi-client-frontend/references/progressive-03-vue3-/345/211/215/347/253/257/345/276/256/346/234/215/345/212/241/345/256/277/344/270/273/350/247/204/345/210/231.md +56 -56
  44. package/skills/microi-codex/SKILL.md +46 -46
  45. package/skills/microi-codex-installer/SKILL.md +216 -216
  46. package/skills/microi-codex-installer/agents/openai.yaml +7 -7
  47. package/skills/microi-datasource-mapping/SKILL.md +67 -67
  48. package/skills/microi-db-schema/SKILL.md +6 -6
  49. package/skills/microi-db-schema/references/core-tables.md +18 -18
  50. package/skills/microi-db-schema/references/form-component-options.md +256 -256
  51. package/skills/microi-db-schema/references/schema-overview.md +11 -11
  52. package/skills/microi-db-schema/references/schema.md +46 -46
  53. package/skills/microi-db-schema/references/table-catalog.md +57 -57
  54. package/skills/microi-deployment/SKILL.md +18 -1
  55. package/skills/microi-deployment/references/deployment-matrix.md +101 -101
  56. package/skills/microi-docs-coverage/SKILL.md +123 -123
  57. package/skills/microi-docs-coverage/references/capability-map.md +1 -0
  58. package/skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +894 -894
  59. package/skills/microi-form-engine/SKILL.md +50 -50
  60. package/skills/microi-form-engine/references/component-catalog.md +218 -218
  61. package/skills/microi-form-engine/references/data-source-events.md +124 -124
  62. package/skills/microi-form-layout/SKILL.md +13 -13
  63. package/skills/microi-form-layout/references/progressive-01-3-/344/270/211/347/247/215/345/210/206/347/273/204/347/232/204/345/255/230/345/202/250/344/270/216/351/205/215/347/275/256.md +235 -235
  64. package/skills/microi-frontend-sdk/SKILL.md +4 -4
  65. package/skills/microi-frontend-sdk/references/progressive-01-token-/345/275/223/345/211/215/347/231/273/345/275/225/347/224/250/346/210/267/344/270/216/345/275/223/345/211/215/347/273/210/347/253/257/347/231/273/345/275/225/345/215/217/350/256/256.md +175 -175
  66. package/skills/microi-left-right-layout/SKILL.md +141 -141
  67. package/skills/microi-microservice/SKILL.md +15 -15
  68. package/skills/microi-microservice/references/runtime-delivery.md +278 -278
  69. package/skills/microi-mobile-app-quality/references/progressive-01-4-/351/207/215/350/246/201/346/214/211/351/222/256/345/277/205/351/241/273/345/270/246/345/233/276/346/240/207.md +213 -213
  70. package/skills/microi-solution-quotation/SKILL.md +78 -78
  71. package/skills/microi-solution-quotation/agents/openai.yaml +4 -4
  72. package/skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -296
  73. package/skills/microi-sso/SKILL.md +85 -85
  74. package/skills/microi-sso/references/acceptance.md +49 -49
  75. package/skills/microi-sso/references/configuration-and-security.md +53 -53
  76. package/skills/microi-sso/references/inbound.md +53 -53
  77. package/skills/microi-sso/references/outbound.md +39 -39
  78. package/skills/microi-system-delivery/references/progressive-02-/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/345/277/205/351/241/273/350/246/206/347/233/226/347/232/204/345/235/221.md +20 -20
  79. package/skills/microi-ui/SKILL.md +7 -2
  80. package/skills/microi-uniapp-frontend/SKILL.md +134 -134
  81. package/skills/microi-uniapp-frontend/references/progressive-01-/347/247/273/345/212/250/347/253/257/345/210/206/347/261/273-/345/217/214/346/240/217/345/210/227/350/241/250/347/213/254/347/253/213/346/273/232/345/212/250.md +136 -136
  82. package/skills/microi-uniapp-frontend/references/progressive-02-/345/205/263/351/224/256/344/270/232/345/212/241/350/265/204/344/272/247/344/270/215/345/276/227/351/273/230/350/256/244/351/200/211/344/270/255.md +124 -124
  83. package/skills/module-engine/SKILL.md +16 -16
  84. package/skills/ocr-engine/SKILL.md +2 -2
  85. package/skills/ocr-engine/agents/openai.yaml +1 -1
  86. package/skills/page-engine/SKILL.md +28 -28
  87. package/skills/page-engine/references/progressive-01-/346/211/200/346/234/211/347/273/204/344/273/266/347/261/273/345/236/213.md +126 -126
  88. package/skills/page-engine/references/progressive-02-/347/211/210/346/234/254/345/216/206/345/217/262-/345/271/266/345/217/221/344/277/235/345/255/230/344/270/216/345/233/236/346/273/232.md +42 -42
  89. package/skills/performance-testing/SKILL.md +120 -120
  90. package/skills/playwright-e2e/SKILL.md +84 -84
  91. package/skills/playwright-e2e/references/progressive-01-/345/205/250/350/207/252/345/212/250/347/231/273/345/275/225-/345/205/215/351/252/214/350/257/201/347/240/201-/344/275/206/344/270/215/345/205/215/345/257/206/347/240/201-/345/277/205/350/257/273.md +68 -68
  92. package/skills/playwright-e2e/references/progressive-02-/346/226/207/345/255/227/345/257/271/346/257/224/345/272/246/344/270/216/345/217/257/350/257/273/346/200/247/350/207/252/345/212/250/345/214/226/346/243/200/346/237/245-/345/277/205/345/201/232.md +64 -64
  93. package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md +20 -20
  94. package/skills/playwright-e2e/references/progressive-04-ci-/345/273/272/350/256/256.md +69 -69
  95. package/skills/print-engine/SKILL.md +36 -36
  96. package/skills/production-readonly-audit/SKILL.md +41 -41
  97. package/skills/report-engine/SKILL.md +71 -71
  98. package/skills/scripts/optimize-progressive-disclosure.mjs +204 -204
  99. package/skills/scripts/refresh-progressive-disclosure.mjs +64 -64
  100. package/skills/scripts/validate-progressive-disclosure.mjs +52 -52
  101. package/skills/search-engine/SKILL.md +75 -75
  102. package/skills/spider-engine/SKILL.md +190 -190
  103. package/skills/system-observability/SKILL.md +162 -162
  104. package/skills/system-observability/references/memory-incident-triage.md +73 -73
  105. package/skills/translate-engine/SKILL.md +1 -1
  106. package/skills/ui-design/SKILL.md +3 -1
  107. package/skills/uniapp-mall-assets/SKILL.md +170 -170
  108. package/skills/unity-integration/SKILL.md +155 -155
  109. package/skills/unity-integration/agents/openai.yaml +4 -4
  110. package/skills/unity-integration/references/ai-app-delivery.md +103 -103
  111. package/skills/unity-integration/references/sdk-api.md +82 -82
  112. package/skills/unity-integration/references/toolbox-migration.md +66 -66
  113. package/skills/unity-integration/references/webgl-hosting.md +57 -57
  114. package/skills/v8-api-config/SKILL.md +378 -378
  115. package/skills/v8-cache-pattern/SKILL.md +306 -306
  116. package/skills/v8-crud-api/SKILL.md +29 -29
  117. package/skills/v8-crud-api/references/progressive-01-/346/237/245/350/257/242/345/210/227/350/241/250-/345/210/206/351/241/265.md +56 -56
  118. package/skills/v8-debugging/SKILL.md +143 -143
  119. package/skills/v8-explorer-tree/SKILL.md +64 -64
  120. package/skills/v8-export-import/references/progressive-01-excellayout-/351/253/230/347/272/247/350/207/252/347/224/261/345/270/203/345/261/200.md +201 -201
  121. package/skills/v8-export-import/references/progressive-02-powerpoint-/345/257/274/345/207/272.md +118 -118
  122. package/skills/v8-export-import/references/progressive-03-/345/256/211/345/205/250-/346/200/247/350/203/275/346/263/250/346/204/217.md +32 -32
  123. package/skills/v8-file-upload/SKILL.md +154 -154
  124. package/skills/v8-file-upload/references/progressive-01-/345/205/254/346/234/211/346/241/266-vs-/347/247/201/346/234/211/346/241/266.md +133 -133
  125. package/skills/v8-file-upload/references/progressive-02-office-/346/226/207/344/273/266/345/234/250/347/272/277/347/274/226/350/276/221/347/211/210/346/234/254/345/217/267/350/247/204/345/210/231.md +138 -138
  126. package/skills/v8-formengine-http/SKILL.md +107 -107
  127. package/skills/v8-frontend-events/SKILL.md +22 -22
  128. package/skills/v8-frontend-events/references/bluetooth-print-api.md +135 -135
  129. package/skills/v8-frontend-events/references/bluetooth-print.md +247 -247
  130. package/skills/v8-frontend-events/references/progressive-01-/345/210/227/350/241/250/344/272/213/344/273/266.md +50 -50
  131. package/skills/v8-http-integration/SKILL.md +106 -106
  132. package/skills/v8-http-integration/references/progressive-01-get-/350/257/267/346/261/202.md +31 -31
  133. package/skills/v8-http-integration/references/progressive-02-/351/224/231/350/257/257/345/244/204/347/220/206/346/250/241/345/274/217.md +5 -5
  134. package/skills/v8-image-processing/SKILL.md +190 -190
  135. package/skills/v8-image-processing/agents/openai.yaml +4 -4
  136. package/skills/v8-image-processing/references/api-reference.md +623 -623
  137. package/skills/v8-menu-buttons/SKILL.md +103 -103
  138. package/skills/v8-menu-buttons/references/progressive-01-2-/346/214/211/351/222/256/345/257/271/350/261/241-schema.md +145 -145
  139. package/skills/v8-menu-buttons/references/progressive-02-8-/346/250/241/345/274/217-f-/345/220/216/345/217/260/344/273/273/345/212/241/346/214/211/351/222/256-/351/225/277/344/273/273/345/212/241.md +228 -228
  140. package/skills/v8-menu-buttons/references/progressive-03-10-/345/217/215/346/250/241/345/274/217-/351/201/277/345/205/215.md +43 -43
  141. package/skills/v8-mongodb/SKILL.md +65 -65
  142. package/skills/v8-mq-mqtt/SKILL.md +175 -175
  143. package/skills/v8-mq-mqtt/references/mqtt-production.md +342 -342
  144. package/skills/v8-mq-mqtt/references/progressive-01-v8-mqtt-iot-/347/211/251/350/201/224/347/275/221.md +137 -137
  145. package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +203 -203
  146. package/skills/v8-saas-multi-tenant/SKILL.md +223 -223
  147. package/skills/v8-security/SKILL.md +25 -25
  148. package/skills/v8-security/references/progressive-01-2-/346/235/203/351/231/220/346/240/241/351/252/214.md +122 -122
  149. package/skills/v8-sql-query/SKILL.md +127 -127
  150. package/skills/v8-table-event/SKILL.md +20 -20
  151. package/skills/v8-table-event/references/progressive-01-informv8-js-/350/241/250/345/215/225/346/211/223/345/274/200/344/272/213/344/273/266.md +18 -18
  152. package/skills/v8-table-event/references/progressive-02-/345/211/215/347/253/257/344/272/213/344/273/266/345/220/215-v8-eventname-/345/217/257/350/203/275/347/232/204/345/200/274.md +15 -15
  153. package/skills/v8-tcp-integration/SKILL.md +1 -1
  154. package/skills/v8-tcp-integration/agents/openai.yaml +4 -4
  155. package/skills/v8-template-engine/SKILL.md +59 -59
  156. package/skills/v8-utilities/SKILL.md +92 -92
  157. package/skills/v8-utilities/references/client-api-index.md +143 -143
  158. package/skills/v8-utilities/references/platform-http-routes.md +83 -83
  159. package/skills/v8-utilities/references/server-api-index.md +74 -74
  160. package/skills/v8-workflow/SKILL.md +44 -44
  161. package/skills/vision-engine/SKILL.md +19 -19
  162. package/skills/vision-engine/references/architecture-and-acceptance.md +34 -34
  163. package/skills/workspace-conventions/SKILL.md +236 -236
  164. package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +208 -208
  165. package/skills/workspace-conventions/references/progressive-02-microi-net-api-/346/234/254/345/234/260/345/220/257/345/212/250/347/272/246/345/256/232.md +179 -179
  166. package/skills/workspace-conventions/references/progressive-03-cli-/344/270/216-ide-/346/217/222/344/273/266/351/224/231/347/211/210/345/205/261/345/255/230/347/272/246/345/256/232.md +21 -21
@@ -1,447 +1,447 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Shared Microi Codex MCP broker.
4
- *
5
- * Every Codex task still owns a tiny stdio router, but all routers for the same
6
- * workspace and tool version share this broker. The broker owns at most one
7
- * real MCP child per connection profile, uses an exclusive lease to survive
8
- * concurrent starts, and exits after the last router has been idle.
9
- */
10
-
11
- 'use strict';
12
-
13
- const childProcess = require('node:child_process');
14
- const crypto = require('node:crypto');
15
- const fs = require('node:fs');
16
- const net = require('node:net');
17
- const os = require('node:os');
18
- const path = require('node:path');
19
-
20
- const PROTOCOL_VERSION = '2024-11-05';
21
- const endpoint = String(process.env.MICROI_CODEX_BROKER_ENDPOINT || '');
22
- const leasePath = path.resolve(String(process.env.MICROI_CODEX_BROKER_LEASE || ''));
23
- const workspaceRoot = path.resolve(String(process.env.MICROI_WORKSPACE_ROOT || process.cwd()));
24
- const mcpServerPath = path.resolve(String(process.env.MICROI_MCP_SERVER_PATH || path.join(__dirname, 'mcp-server.js')));
25
- const toolVersion = String(process.env.MICROI_TOOL_VERSION || '0.0.0');
26
- const idleMilliseconds = Math.max(250, Number(process.env.MICROI_CODEX_BROKER_IDLE_MS || 30000));
27
- const heartbeatMilliseconds = Math.max(500, Number(process.env.MICROI_CODEX_BROKER_HEARTBEAT_MS || 5000));
28
- const staleMilliseconds = Math.max(heartbeatMilliseconds * 3, Number(process.env.MICROI_CODEX_BROKER_STALE_MS || 20000));
29
-
30
- if (!endpoint || !process.env.MICROI_CODEX_BROKER_LEASE) {
31
- process.stderr.write('[microi-codex-broker] Missing endpoint or lease path.\n');
32
- process.exit(1);
33
- }
34
-
35
- const ownerToken = crypto.randomBytes(24).toString('hex');
36
- const children = new Map();
37
- const clients = new Set();
38
- let requestSequence = 0;
39
- let leaseOwned = false;
40
- let shuttingDown = false;
41
- let idleTimer = null;
42
- let heartbeatTimer = null;
43
- let server = null;
44
-
45
- function processExists(pid) {
46
- if (!Number.isInteger(pid) || pid <= 1) return false;
47
- try {
48
- process.kill(pid, 0);
49
- return true;
50
- } catch (error) {
51
- return error?.code === 'EPERM';
52
- }
53
- }
54
-
55
- function readLease() {
56
- try {
57
- const parsed = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
58
- return parsed && typeof parsed === 'object' ? parsed : null;
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- function leaseRecord() {
65
- return {
66
- pid: process.pid,
67
- token: ownerToken,
68
- endpoint,
69
- workspaceRoot,
70
- toolVersion,
71
- startedAt: startedAtIso,
72
- heartbeatAt: new Date().toISOString(),
73
- };
74
- }
75
-
76
- function writeLeaseRecord(record, exclusive = false) {
77
- fs.mkdirSync(path.dirname(leasePath), { recursive: true });
78
- const text = `${JSON.stringify(record, null, 2)}\n`;
79
- if (exclusive) {
80
- // Publish a complete lease atomically. Creating leasePath first and then
81
- // writing its JSON leaves a short empty-file window; concurrent brokers can
82
- // mistake that window for a stale lease, remove it, and leave connected
83
- // routers without an authentication token. A same-directory hard link is
84
- // both exclusive (EEXIST wins the race) and immediately complete.
85
- const staged = `${leasePath}.${process.pid}.${Date.now()}.${ownerToken.slice(0, 8)}.new`;
86
- try {
87
- fs.writeFileSync(staged, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
88
- fs.linkSync(staged, leasePath);
89
- } finally {
90
- try { if (fs.existsSync(staged)) fs.rmSync(staged, { force: true }); } catch { /* harmless */ }
91
- }
92
- return;
93
- }
94
- const current = readLease();
95
- if (!current || current.token !== ownerToken) throw new Error('Microi Codex broker lease ownership changed');
96
- const staged = `${leasePath}.${process.pid}.${Date.now()}.tmp`;
97
- try {
98
- fs.writeFileSync(staged, text, { encoding: 'utf8', mode: 0o600 });
99
- fs.renameSync(staged, leasePath);
100
- } finally {
101
- try { if (fs.existsSync(staged)) fs.rmSync(staged, { force: true }); } catch { /* best effort */ }
102
- }
103
- }
104
-
105
- function acquireLease() {
106
- fs.mkdirSync(path.dirname(leasePath), { recursive: true });
107
- for (let attempt = 0; attempt < 4; attempt += 1) {
108
- try {
109
- writeLeaseRecord(leaseRecord(), true);
110
- leaseOwned = true;
111
- return true;
112
- } catch (error) {
113
- if (error?.code !== 'EEXIST') throw error;
114
- const existing = readLease();
115
- const heartbeat = Date.parse(String(existing?.heartbeatAt || ''));
116
- const fresh = Number.isFinite(heartbeat) && Date.now() - heartbeat < staleMilliseconds;
117
- if (existing && processExists(Number(existing.pid)) && fresh) return false;
118
- const stalePath = `${leasePath}.stale.${process.pid}.${Date.now()}`;
119
- try {
120
- fs.renameSync(leasePath, stalePath);
121
- try { fs.rmSync(stalePath, { force: true }); } catch { /* harmless */ }
122
- } catch (renameError) {
123
- if (!['ENOENT', 'EACCES', 'EPERM'].includes(renameError?.code)) throw renameError;
124
- }
125
- }
126
- }
127
- return false;
128
- }
129
-
130
- function releaseLease() {
131
- if (!leaseOwned) return;
132
- const current = readLease();
133
- if (current?.token === ownerToken) {
134
- try { fs.rmSync(leasePath, { force: true }); } catch { /* next owner can reclaim stale lease */ }
135
- }
136
- leaseOwned = false;
137
- }
138
-
139
- function safeText(value) {
140
- return String(value == null ? '' : value);
141
- }
142
-
143
- function profileIdentity(profile) {
144
- return safeText(profile?.identity) || [
145
- safeText(profile?.apiBaseUrl).replace(/\/+$/, ''),
146
- safeText(profile?.osClient).trim(),
147
- safeText(profile?.osClientType).trim(),
148
- safeText(profile?.osClientNetwork).trim(),
149
- ].join('|');
150
- }
151
-
152
- function workspaceSecretKey(baseKey, profileWorkspace) {
153
- const normalized = path.resolve(profileWorkspace).toLowerCase();
154
- const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
155
- return `${baseKey}.workspace.${hash}`;
156
- }
157
-
158
- function credentialSecretKey(baseKey, profile, profileWorkspace) {
159
- const apiBaseUrl = safeText(profile.apiBaseUrl).trim().replace(/\/+$/, '');
160
- const osClient = safeText(profile.osClient).trim();
161
- return workspaceSecretKey(`${baseKey}.${apiBaseUrl}|${osClient}`, profileWorkspace);
162
- }
163
-
164
- function validateProfile(raw) {
165
- const profile = raw && typeof raw === 'object' ? raw : {};
166
- const profileWorkspace = path.resolve(safeText(profile.workspaceRoot || workspaceRoot));
167
- if (process.platform === 'win32') {
168
- if (profileWorkspace.toLowerCase() !== workspaceRoot.toLowerCase()) throw new Error('Broker workspace mismatch');
169
- } else if (profileWorkspace !== workspaceRoot) {
170
- throw new Error('Broker workspace mismatch');
171
- }
172
- if (!/^https?:\/\//i.test(safeText(profile.apiBaseUrl))) throw new Error('Invalid Microi API URL');
173
- const syncRoot = path.resolve(safeText(profile.syncRoot || path.join(profileWorkspace, 'Microi-V8-Engine')));
174
- return {
175
- identity: profileIdentity(profile),
176
- name: safeText(profile.name || profile.osClient || 'microi'),
177
- label: safeText(profile.label),
178
- apiBaseUrl: safeText(profile.apiBaseUrl).replace(/\/+$/, ''),
179
- osClient: safeText(profile.osClient).trim(),
180
- osClientType: safeText(profile.osClientType).trim(),
181
- osClientNetwork: safeText(profile.osClientNetwork).trim(),
182
- workspaceRoot: profileWorkspace,
183
- syncRoot,
184
- tokenPath: path.resolve(safeText(profile.tokenPath || path.join(profileWorkspace, 'Microi-V8-Engine', '.microi-mcp-tokens.json'))),
185
- applicationsDir: path.resolve(safeText(profile.applicationsDir || path.join(profileWorkspace, 'Microi-V8-Engine'))),
186
- workspaceCredentialFilePath: path.join(syncRoot, '.microi-workspace-secrets.dpapi.json'),
187
- workspaceCredentialUsernameKey: credentialSecretKey('microi.username', profile, profileWorkspace),
188
- workspaceCredentialPasswordKey: credentialSecretKey('microi.password', profile, profileWorkspace),
189
- };
190
- }
191
-
192
- function deviceId() {
193
- const raw = `MCP:${os.hostname() || 'Unknown'}`;
194
- if (/^[\x20-\x7E]{1,128}$/.test(raw)) return raw;
195
- return `MCP-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
196
- }
197
-
198
- class ChildMcp {
199
- constructor(profile) {
200
- this.profile = profile;
201
- this.pending = new Map();
202
- this.buffer = '';
203
- this.stopping = false;
204
- this.ready = this.start();
205
- }
206
-
207
- async start() {
208
- if (!fs.existsSync(mcpServerPath)) throw new Error(`Microi MCP bundle does not exist: ${mcpServerPath}`);
209
- this.child = childProcess.spawn(process.execPath, [mcpServerPath], {
210
- cwd: this.profile.workspaceRoot,
211
- windowsHide: true,
212
- stdio: ['pipe', 'pipe', 'pipe'],
213
- env: {
214
- ...process.env,
215
- MICROI_API_URL: this.profile.apiBaseUrl,
216
- MICROI_TOKEN_FILE: this.profile.tokenPath,
217
- MICROI_AUTH_RECOVERY_DIR: `${this.profile.tokenPath}.recovery`,
218
- MICROI_WORKSPACE_CREDENTIAL_FILE: this.profile.workspaceCredentialFilePath,
219
- MICROI_WORKSPACE_USERNAME_KEY: this.profile.workspaceCredentialUsernameKey,
220
- MICROI_WORKSPACE_PASSWORD_KEY: this.profile.workspaceCredentialPasswordKey,
221
- MICROI_OS_CLIENT: this.profile.osClient,
222
- MICROI_OS_CLIENT_TYPE: this.profile.osClientType,
223
- MICROI_OS_CLIENT_NETWORK: this.profile.osClientNetwork,
224
- MICROI_WORKSPACE_ROOT: this.profile.workspaceRoot,
225
- MICROI_SYNC_ROOT: this.profile.syncRoot,
226
- MICROI_AI_APPLICATIONS_DIR: this.profile.applicationsDir,
227
- MICROI_LABEL_BASE64: Buffer.from(this.profile.label, 'utf8').toString('base64'),
228
- MICROI_MCP_DID: deviceId(),
229
- MICROI_TOOL_SOURCE: 'codex-broker',
230
- MICROI_TOOL_VERSION: toolVersion,
231
- MICROI_CODEX_MODE: '1',
232
- },
233
- });
234
- this.child.stdout.on('data', chunk => this.onData(chunk));
235
- this.child.stderr.on('data', chunk => process.stderr.write(`[${this.profile.name}] ${chunk}`));
236
- this.child.on('error', error => this.rejectAll(error));
237
- this.child.on('exit', code => {
238
- this.rejectAll(new Error(`Microi MCP ${this.profile.name} exited (${code ?? 'unknown'})`));
239
- if (!this.stopping && children.get(this.profile.identity) === this) children.delete(this.profile.identity);
240
- });
241
- await this.request('initialize', {
242
- protocolVersion: PROTOCOL_VERSION,
243
- capabilities: {},
244
- clientInfo: { name: 'microi-codex-shared-broker', version: toolVersion },
245
- }, true);
246
- this.notify('notifications/initialized', {});
247
- }
248
-
249
- onData(chunk) {
250
- this.buffer += chunk.toString('utf8');
251
- let newline;
252
- while ((newline = this.buffer.indexOf('\n')) >= 0) {
253
- const line = this.buffer.slice(0, newline).trim();
254
- this.buffer = this.buffer.slice(newline + 1);
255
- if (!line) continue;
256
- let message;
257
- try { message = JSON.parse(line); } catch { continue; }
258
- if (message.id == null) continue;
259
- const pending = this.pending.get(String(message.id));
260
- if (!pending) continue;
261
- this.pending.delete(String(message.id));
262
- if (message.error) pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
263
- else pending.resolve(message.result);
264
- }
265
- }
266
-
267
- rejectAll(error) {
268
- for (const pending of this.pending.values()) pending.reject(error);
269
- this.pending.clear();
270
- }
271
-
272
- write(message) {
273
- if (!this.child || this.child.stdin.destroyed) throw new Error(`Microi MCP ${this.profile.name} is unavailable`);
274
- this.child.stdin.write(`${JSON.stringify(message)}\n`);
275
- }
276
-
277
- request(method, params, skipReady = false) {
278
- return (async () => {
279
- if (!skipReady) await this.ready;
280
- const id = `broker-${process.pid}-${++requestSequence}`;
281
- const response = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
282
- this.write({ jsonrpc: '2.0', id, method, params });
283
- return response;
284
- })();
285
- }
286
-
287
- notify(method, params) {
288
- this.write({ jsonrpc: '2.0', method, params });
289
- }
290
-
291
- stop() {
292
- this.stopping = true;
293
- try { this.child?.stdin.end(); } catch { /* best effort */ }
294
- try { this.child?.kill('SIGTERM'); } catch { /* best effort */ }
295
- const child = this.child;
296
- const forceTimer = setTimeout(() => {
297
- if (child && child.exitCode === null) {
298
- try { child.kill('SIGKILL'); } catch { /* best effort */ }
299
- }
300
- }, 2000);
301
- forceTimer.unref();
302
- }
303
- }
304
-
305
- function childFor(rawProfile) {
306
- const profile = validateProfile(rawProfile);
307
- const existing = children.get(profile.identity);
308
- if (existing) return existing;
309
- const child = new ChildMcp(profile);
310
- children.set(profile.identity, child);
311
- return child;
312
- }
313
-
314
- function send(socket, message) {
315
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}\n`);
316
- }
317
-
318
- async function handleMessage(socket, message) {
319
- const id = message?.id;
320
- if (id == null) return;
321
- if (message.auth !== ownerToken) {
322
- send(socket, { id, error: 'Unauthorized local broker client' });
323
- return;
324
- }
325
- try {
326
- if (message.type === 'status') {
327
- send(socket, {
328
- id,
329
- result: {
330
- pid: process.pid,
331
- workspaceRoot,
332
- toolVersion,
333
- clients: clients.size,
334
- childCount: children.size,
335
- childPids: [...children.values()].map(item => item.child?.pid).filter(Boolean),
336
- },
337
- });
338
- return;
339
- }
340
- if (message.type !== 'request') throw new Error('Unsupported broker request');
341
- const result = await childFor(message.profile).request(safeText(message.method), message.params || {});
342
- send(socket, { id, result });
343
- } catch (error) {
344
- send(socket, { id, error: error instanceof Error ? error.message : String(error) });
345
- }
346
- }
347
-
348
- function cancelIdleShutdown() {
349
- if (idleTimer) clearTimeout(idleTimer);
350
- idleTimer = null;
351
- }
352
-
353
- function scheduleIdleShutdown() {
354
- cancelIdleShutdown();
355
- if (clients.size > 0 || shuttingDown) return;
356
- idleTimer = setTimeout(() => shutdown(0), idleMilliseconds);
357
- }
358
-
359
- function acceptClient(socket) {
360
- cancelIdleShutdown();
361
- clients.add(socket);
362
- socket.setEncoding('utf8');
363
- let buffer = '';
364
- socket.on('data', chunk => {
365
- buffer += chunk;
366
- let newline;
367
- while ((newline = buffer.indexOf('\n')) >= 0) {
368
- const line = buffer.slice(0, newline).trim();
369
- buffer = buffer.slice(newline + 1);
370
- if (!line) continue;
371
- let message;
372
- try { message = JSON.parse(line); }
373
- catch { send(socket, { id: null, error: 'Invalid broker JSON' }); continue; }
374
- Promise.resolve(handleMessage(socket, message)).catch(() => { /* handled above */ });
375
- }
376
- });
377
- const close = () => {
378
- clients.delete(socket);
379
- scheduleIdleShutdown();
380
- };
381
- socket.once('close', close);
382
- socket.once('error', close);
383
- }
384
-
385
- function removeUnixSocket() {
386
- if (process.platform === 'win32') return;
387
- try { if (fs.existsSync(endpoint)) fs.rmSync(endpoint, { force: true }); } catch { /* stale socket is reclaimable */ }
388
- }
389
-
390
- function shutdown(exitCode = 0) {
391
- if (shuttingDown) return;
392
- shuttingDown = true;
393
- cancelIdleShutdown();
394
- if (heartbeatTimer) clearInterval(heartbeatTimer);
395
- for (const socket of clients) {
396
- try { socket.end(); } catch { /* best effort */ }
397
- }
398
- clients.clear();
399
- for (const child of children.values()) child.stop();
400
- children.clear();
401
- const finish = () => {
402
- releaseLease();
403
- removeUnixSocket();
404
- process.exit(exitCode);
405
- };
406
- if (server) {
407
- try { server.close(finish); } catch { finish(); }
408
- const forceTimer = setTimeout(finish, 2200);
409
- forceTimer.unref();
410
- } else {
411
- finish();
412
- }
413
- }
414
-
415
- const startedAtIso = new Date().toISOString();
416
- if (!acquireLease()) process.exit(0);
417
-
418
- removeUnixSocket();
419
- server = net.createServer(acceptClient);
420
- server.on('error', error => {
421
- process.stderr.write(`[microi-codex-broker] ${error.message}\n`);
422
- shutdown(1);
423
- });
424
- server.listen(endpoint, () => {
425
- if (process.platform !== 'win32') {
426
- try { fs.chmodSync(endpoint, 0o600); } catch { /* best effort */ }
427
- }
428
- heartbeatTimer = setInterval(() => {
429
- try { writeLeaseRecord(leaseRecord()); } catch { shutdown(1); }
430
- }, heartbeatMilliseconds);
431
- scheduleIdleShutdown();
432
- });
433
-
434
- for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => shutdown(0));
435
- process.on('uncaughtException', error => {
436
- process.stderr.write(`[microi-codex-broker] ${error?.stack || error}\n`);
437
- shutdown(1);
438
- });
439
- process.on('exit', () => {
440
- for (const child of children.values()) {
441
- try { if (child.child?.exitCode === null) child.child.kill('SIGKILL'); } catch { /* best effort */ }
442
- }
443
- if (leaseOwned) {
444
- releaseLease();
445
- removeUnixSocket();
446
- }
447
- });
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Shared Microi Codex MCP broker.
4
+ *
5
+ * Every Codex task still owns a tiny stdio router, but all routers for the same
6
+ * workspace and tool version share this broker. The broker owns at most one
7
+ * real MCP child per connection profile, uses an exclusive lease to survive
8
+ * concurrent starts, and exits after the last router has been idle.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const childProcess = require('node:child_process');
14
+ const crypto = require('node:crypto');
15
+ const fs = require('node:fs');
16
+ const net = require('node:net');
17
+ const os = require('node:os');
18
+ const path = require('node:path');
19
+
20
+ const PROTOCOL_VERSION = '2024-11-05';
21
+ const endpoint = String(process.env.MICROI_CODEX_BROKER_ENDPOINT || '');
22
+ const leasePath = path.resolve(String(process.env.MICROI_CODEX_BROKER_LEASE || ''));
23
+ const workspaceRoot = path.resolve(String(process.env.MICROI_WORKSPACE_ROOT || process.cwd()));
24
+ const mcpServerPath = path.resolve(String(process.env.MICROI_MCP_SERVER_PATH || path.join(__dirname, 'mcp-server.js')));
25
+ const toolVersion = String(process.env.MICROI_TOOL_VERSION || '0.0.0');
26
+ const idleMilliseconds = Math.max(250, Number(process.env.MICROI_CODEX_BROKER_IDLE_MS || 30000));
27
+ const heartbeatMilliseconds = Math.max(500, Number(process.env.MICROI_CODEX_BROKER_HEARTBEAT_MS || 5000));
28
+ const staleMilliseconds = Math.max(heartbeatMilliseconds * 3, Number(process.env.MICROI_CODEX_BROKER_STALE_MS || 20000));
29
+
30
+ if (!endpoint || !process.env.MICROI_CODEX_BROKER_LEASE) {
31
+ process.stderr.write('[microi-codex-broker] Missing endpoint or lease path.\n');
32
+ process.exit(1);
33
+ }
34
+
35
+ const ownerToken = crypto.randomBytes(24).toString('hex');
36
+ const children = new Map();
37
+ const clients = new Set();
38
+ let requestSequence = 0;
39
+ let leaseOwned = false;
40
+ let shuttingDown = false;
41
+ let idleTimer = null;
42
+ let heartbeatTimer = null;
43
+ let server = null;
44
+
45
+ function processExists(pid) {
46
+ if (!Number.isInteger(pid) || pid <= 1) return false;
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ } catch (error) {
51
+ return error?.code === 'EPERM';
52
+ }
53
+ }
54
+
55
+ function readLease() {
56
+ try {
57
+ const parsed = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
58
+ return parsed && typeof parsed === 'object' ? parsed : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function leaseRecord() {
65
+ return {
66
+ pid: process.pid,
67
+ token: ownerToken,
68
+ endpoint,
69
+ workspaceRoot,
70
+ toolVersion,
71
+ startedAt: startedAtIso,
72
+ heartbeatAt: new Date().toISOString(),
73
+ };
74
+ }
75
+
76
+ function writeLeaseRecord(record, exclusive = false) {
77
+ fs.mkdirSync(path.dirname(leasePath), { recursive: true });
78
+ const text = `${JSON.stringify(record, null, 2)}\n`;
79
+ if (exclusive) {
80
+ // Publish a complete lease atomically. Creating leasePath first and then
81
+ // writing its JSON leaves a short empty-file window; concurrent brokers can
82
+ // mistake that window for a stale lease, remove it, and leave connected
83
+ // routers without an authentication token. A same-directory hard link is
84
+ // both exclusive (EEXIST wins the race) and immediately complete.
85
+ const staged = `${leasePath}.${process.pid}.${Date.now()}.${ownerToken.slice(0, 8)}.new`;
86
+ try {
87
+ fs.writeFileSync(staged, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
88
+ fs.linkSync(staged, leasePath);
89
+ } finally {
90
+ try { if (fs.existsSync(staged)) fs.rmSync(staged, { force: true }); } catch { /* harmless */ }
91
+ }
92
+ return;
93
+ }
94
+ const current = readLease();
95
+ if (!current || current.token !== ownerToken) throw new Error('Microi Codex broker lease ownership changed');
96
+ const staged = `${leasePath}.${process.pid}.${Date.now()}.tmp`;
97
+ try {
98
+ fs.writeFileSync(staged, text, { encoding: 'utf8', mode: 0o600 });
99
+ fs.renameSync(staged, leasePath);
100
+ } finally {
101
+ try { if (fs.existsSync(staged)) fs.rmSync(staged, { force: true }); } catch { /* best effort */ }
102
+ }
103
+ }
104
+
105
+ function acquireLease() {
106
+ fs.mkdirSync(path.dirname(leasePath), { recursive: true });
107
+ for (let attempt = 0; attempt < 4; attempt += 1) {
108
+ try {
109
+ writeLeaseRecord(leaseRecord(), true);
110
+ leaseOwned = true;
111
+ return true;
112
+ } catch (error) {
113
+ if (error?.code !== 'EEXIST') throw error;
114
+ const existing = readLease();
115
+ const heartbeat = Date.parse(String(existing?.heartbeatAt || ''));
116
+ const fresh = Number.isFinite(heartbeat) && Date.now() - heartbeat < staleMilliseconds;
117
+ if (existing && processExists(Number(existing.pid)) && fresh) return false;
118
+ const stalePath = `${leasePath}.stale.${process.pid}.${Date.now()}`;
119
+ try {
120
+ fs.renameSync(leasePath, stalePath);
121
+ try { fs.rmSync(stalePath, { force: true }); } catch { /* harmless */ }
122
+ } catch (renameError) {
123
+ if (!['ENOENT', 'EACCES', 'EPERM'].includes(renameError?.code)) throw renameError;
124
+ }
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ function releaseLease() {
131
+ if (!leaseOwned) return;
132
+ const current = readLease();
133
+ if (current?.token === ownerToken) {
134
+ try { fs.rmSync(leasePath, { force: true }); } catch { /* next owner can reclaim stale lease */ }
135
+ }
136
+ leaseOwned = false;
137
+ }
138
+
139
+ function safeText(value) {
140
+ return String(value == null ? '' : value);
141
+ }
142
+
143
+ function profileIdentity(profile) {
144
+ return safeText(profile?.identity) || [
145
+ safeText(profile?.apiBaseUrl).replace(/\/+$/, ''),
146
+ safeText(profile?.osClient).trim(),
147
+ safeText(profile?.osClientType).trim(),
148
+ safeText(profile?.osClientNetwork).trim(),
149
+ ].join('|');
150
+ }
151
+
152
+ function workspaceSecretKey(baseKey, profileWorkspace) {
153
+ const normalized = path.resolve(profileWorkspace).toLowerCase();
154
+ const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
155
+ return `${baseKey}.workspace.${hash}`;
156
+ }
157
+
158
+ function credentialSecretKey(baseKey, profile, profileWorkspace) {
159
+ const apiBaseUrl = safeText(profile.apiBaseUrl).trim().replace(/\/+$/, '');
160
+ const osClient = safeText(profile.osClient).trim();
161
+ return workspaceSecretKey(`${baseKey}.${apiBaseUrl}|${osClient}`, profileWorkspace);
162
+ }
163
+
164
+ function validateProfile(raw) {
165
+ const profile = raw && typeof raw === 'object' ? raw : {};
166
+ const profileWorkspace = path.resolve(safeText(profile.workspaceRoot || workspaceRoot));
167
+ if (process.platform === 'win32') {
168
+ if (profileWorkspace.toLowerCase() !== workspaceRoot.toLowerCase()) throw new Error('Broker workspace mismatch');
169
+ } else if (profileWorkspace !== workspaceRoot) {
170
+ throw new Error('Broker workspace mismatch');
171
+ }
172
+ if (!/^https?:\/\//i.test(safeText(profile.apiBaseUrl))) throw new Error('Invalid Microi API URL');
173
+ const syncRoot = path.resolve(safeText(profile.syncRoot || path.join(profileWorkspace, 'Microi-V8-Engine')));
174
+ return {
175
+ identity: profileIdentity(profile),
176
+ name: safeText(profile.name || profile.osClient || 'microi'),
177
+ label: safeText(profile.label),
178
+ apiBaseUrl: safeText(profile.apiBaseUrl).replace(/\/+$/, ''),
179
+ osClient: safeText(profile.osClient).trim(),
180
+ osClientType: safeText(profile.osClientType).trim(),
181
+ osClientNetwork: safeText(profile.osClientNetwork).trim(),
182
+ workspaceRoot: profileWorkspace,
183
+ syncRoot,
184
+ tokenPath: path.resolve(safeText(profile.tokenPath || path.join(profileWorkspace, 'Microi-V8-Engine', '.microi-mcp-tokens.json'))),
185
+ applicationsDir: path.resolve(safeText(profile.applicationsDir || path.join(profileWorkspace, 'Microi-V8-Engine'))),
186
+ workspaceCredentialFilePath: path.join(syncRoot, '.microi-workspace-secrets.dpapi.json'),
187
+ workspaceCredentialUsernameKey: credentialSecretKey('microi.username', profile, profileWorkspace),
188
+ workspaceCredentialPasswordKey: credentialSecretKey('microi.password', profile, profileWorkspace),
189
+ };
190
+ }
191
+
192
+ function deviceId() {
193
+ const raw = `MCP:${os.hostname() || 'Unknown'}`;
194
+ if (/^[\x20-\x7E]{1,128}$/.test(raw)) return raw;
195
+ return `MCP-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
196
+ }
197
+
198
+ class ChildMcp {
199
+ constructor(profile) {
200
+ this.profile = profile;
201
+ this.pending = new Map();
202
+ this.buffer = '';
203
+ this.stopping = false;
204
+ this.ready = this.start();
205
+ }
206
+
207
+ async start() {
208
+ if (!fs.existsSync(mcpServerPath)) throw new Error(`Microi MCP bundle does not exist: ${mcpServerPath}`);
209
+ this.child = childProcess.spawn(process.execPath, [mcpServerPath], {
210
+ cwd: this.profile.workspaceRoot,
211
+ windowsHide: true,
212
+ stdio: ['pipe', 'pipe', 'pipe'],
213
+ env: {
214
+ ...process.env,
215
+ MICROI_API_URL: this.profile.apiBaseUrl,
216
+ MICROI_TOKEN_FILE: this.profile.tokenPath,
217
+ MICROI_AUTH_RECOVERY_DIR: `${this.profile.tokenPath}.recovery`,
218
+ MICROI_WORKSPACE_CREDENTIAL_FILE: this.profile.workspaceCredentialFilePath,
219
+ MICROI_WORKSPACE_USERNAME_KEY: this.profile.workspaceCredentialUsernameKey,
220
+ MICROI_WORKSPACE_PASSWORD_KEY: this.profile.workspaceCredentialPasswordKey,
221
+ MICROI_OS_CLIENT: this.profile.osClient,
222
+ MICROI_OS_CLIENT_TYPE: this.profile.osClientType,
223
+ MICROI_OS_CLIENT_NETWORK: this.profile.osClientNetwork,
224
+ MICROI_WORKSPACE_ROOT: this.profile.workspaceRoot,
225
+ MICROI_SYNC_ROOT: this.profile.syncRoot,
226
+ MICROI_AI_APPLICATIONS_DIR: this.profile.applicationsDir,
227
+ MICROI_LABEL_BASE64: Buffer.from(this.profile.label, 'utf8').toString('base64'),
228
+ MICROI_MCP_DID: deviceId(),
229
+ MICROI_TOOL_SOURCE: 'codex-broker',
230
+ MICROI_TOOL_VERSION: toolVersion,
231
+ MICROI_CODEX_MODE: '1',
232
+ },
233
+ });
234
+ this.child.stdout.on('data', chunk => this.onData(chunk));
235
+ this.child.stderr.on('data', chunk => process.stderr.write(`[${this.profile.name}] ${chunk}`));
236
+ this.child.on('error', error => this.rejectAll(error));
237
+ this.child.on('exit', code => {
238
+ this.rejectAll(new Error(`Microi MCP ${this.profile.name} exited (${code ?? 'unknown'})`));
239
+ if (!this.stopping && children.get(this.profile.identity) === this) children.delete(this.profile.identity);
240
+ });
241
+ await this.request('initialize', {
242
+ protocolVersion: PROTOCOL_VERSION,
243
+ capabilities: {},
244
+ clientInfo: { name: 'microi-codex-shared-broker', version: toolVersion },
245
+ }, true);
246
+ this.notify('notifications/initialized', {});
247
+ }
248
+
249
+ onData(chunk) {
250
+ this.buffer += chunk.toString('utf8');
251
+ let newline;
252
+ while ((newline = this.buffer.indexOf('\n')) >= 0) {
253
+ const line = this.buffer.slice(0, newline).trim();
254
+ this.buffer = this.buffer.slice(newline + 1);
255
+ if (!line) continue;
256
+ let message;
257
+ try { message = JSON.parse(line); } catch { continue; }
258
+ if (message.id == null) continue;
259
+ const pending = this.pending.get(String(message.id));
260
+ if (!pending) continue;
261
+ this.pending.delete(String(message.id));
262
+ if (message.error) pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
263
+ else pending.resolve(message.result);
264
+ }
265
+ }
266
+
267
+ rejectAll(error) {
268
+ for (const pending of this.pending.values()) pending.reject(error);
269
+ this.pending.clear();
270
+ }
271
+
272
+ write(message) {
273
+ if (!this.child || this.child.stdin.destroyed) throw new Error(`Microi MCP ${this.profile.name} is unavailable`);
274
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
275
+ }
276
+
277
+ request(method, params, skipReady = false) {
278
+ return (async () => {
279
+ if (!skipReady) await this.ready;
280
+ const id = `broker-${process.pid}-${++requestSequence}`;
281
+ const response = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
282
+ this.write({ jsonrpc: '2.0', id, method, params });
283
+ return response;
284
+ })();
285
+ }
286
+
287
+ notify(method, params) {
288
+ this.write({ jsonrpc: '2.0', method, params });
289
+ }
290
+
291
+ stop() {
292
+ this.stopping = true;
293
+ try { this.child?.stdin.end(); } catch { /* best effort */ }
294
+ try { this.child?.kill('SIGTERM'); } catch { /* best effort */ }
295
+ const child = this.child;
296
+ const forceTimer = setTimeout(() => {
297
+ if (child && child.exitCode === null) {
298
+ try { child.kill('SIGKILL'); } catch { /* best effort */ }
299
+ }
300
+ }, 2000);
301
+ forceTimer.unref();
302
+ }
303
+ }
304
+
305
+ function childFor(rawProfile) {
306
+ const profile = validateProfile(rawProfile);
307
+ const existing = children.get(profile.identity);
308
+ if (existing) return existing;
309
+ const child = new ChildMcp(profile);
310
+ children.set(profile.identity, child);
311
+ return child;
312
+ }
313
+
314
+ function send(socket, message) {
315
+ if (!socket.destroyed) socket.write(`${JSON.stringify(message)}\n`);
316
+ }
317
+
318
+ async function handleMessage(socket, message) {
319
+ const id = message?.id;
320
+ if (id == null) return;
321
+ if (message.auth !== ownerToken) {
322
+ send(socket, { id, error: 'Unauthorized local broker client' });
323
+ return;
324
+ }
325
+ try {
326
+ if (message.type === 'status') {
327
+ send(socket, {
328
+ id,
329
+ result: {
330
+ pid: process.pid,
331
+ workspaceRoot,
332
+ toolVersion,
333
+ clients: clients.size,
334
+ childCount: children.size,
335
+ childPids: [...children.values()].map(item => item.child?.pid).filter(Boolean),
336
+ },
337
+ });
338
+ return;
339
+ }
340
+ if (message.type !== 'request') throw new Error('Unsupported broker request');
341
+ const result = await childFor(message.profile).request(safeText(message.method), message.params || {});
342
+ send(socket, { id, result });
343
+ } catch (error) {
344
+ send(socket, { id, error: error instanceof Error ? error.message : String(error) });
345
+ }
346
+ }
347
+
348
+ function cancelIdleShutdown() {
349
+ if (idleTimer) clearTimeout(idleTimer);
350
+ idleTimer = null;
351
+ }
352
+
353
+ function scheduleIdleShutdown() {
354
+ cancelIdleShutdown();
355
+ if (clients.size > 0 || shuttingDown) return;
356
+ idleTimer = setTimeout(() => shutdown(0), idleMilliseconds);
357
+ }
358
+
359
+ function acceptClient(socket) {
360
+ cancelIdleShutdown();
361
+ clients.add(socket);
362
+ socket.setEncoding('utf8');
363
+ let buffer = '';
364
+ socket.on('data', chunk => {
365
+ buffer += chunk;
366
+ let newline;
367
+ while ((newline = buffer.indexOf('\n')) >= 0) {
368
+ const line = buffer.slice(0, newline).trim();
369
+ buffer = buffer.slice(newline + 1);
370
+ if (!line) continue;
371
+ let message;
372
+ try { message = JSON.parse(line); }
373
+ catch { send(socket, { id: null, error: 'Invalid broker JSON' }); continue; }
374
+ Promise.resolve(handleMessage(socket, message)).catch(() => { /* handled above */ });
375
+ }
376
+ });
377
+ const close = () => {
378
+ clients.delete(socket);
379
+ scheduleIdleShutdown();
380
+ };
381
+ socket.once('close', close);
382
+ socket.once('error', close);
383
+ }
384
+
385
+ function removeUnixSocket() {
386
+ if (process.platform === 'win32') return;
387
+ try { if (fs.existsSync(endpoint)) fs.rmSync(endpoint, { force: true }); } catch { /* stale socket is reclaimable */ }
388
+ }
389
+
390
+ function shutdown(exitCode = 0) {
391
+ if (shuttingDown) return;
392
+ shuttingDown = true;
393
+ cancelIdleShutdown();
394
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
395
+ for (const socket of clients) {
396
+ try { socket.end(); } catch { /* best effort */ }
397
+ }
398
+ clients.clear();
399
+ for (const child of children.values()) child.stop();
400
+ children.clear();
401
+ const finish = () => {
402
+ releaseLease();
403
+ removeUnixSocket();
404
+ process.exit(exitCode);
405
+ };
406
+ if (server) {
407
+ try { server.close(finish); } catch { finish(); }
408
+ const forceTimer = setTimeout(finish, 2200);
409
+ forceTimer.unref();
410
+ } else {
411
+ finish();
412
+ }
413
+ }
414
+
415
+ const startedAtIso = new Date().toISOString();
416
+ if (!acquireLease()) process.exit(0);
417
+
418
+ removeUnixSocket();
419
+ server = net.createServer(acceptClient);
420
+ server.on('error', error => {
421
+ process.stderr.write(`[microi-codex-broker] ${error.message}\n`);
422
+ shutdown(1);
423
+ });
424
+ server.listen(endpoint, () => {
425
+ if (process.platform !== 'win32') {
426
+ try { fs.chmodSync(endpoint, 0o600); } catch { /* best effort */ }
427
+ }
428
+ heartbeatTimer = setInterval(() => {
429
+ try { writeLeaseRecord(leaseRecord()); } catch { shutdown(1); }
430
+ }, heartbeatMilliseconds);
431
+ scheduleIdleShutdown();
432
+ });
433
+
434
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => shutdown(0));
435
+ process.on('uncaughtException', error => {
436
+ process.stderr.write(`[microi-codex-broker] ${error?.stack || error}\n`);
437
+ shutdown(1);
438
+ });
439
+ process.on('exit', () => {
440
+ for (const child of children.values()) {
441
+ try { if (child.child?.exitCode === null) child.child.kill('SIGKILL'); } catch { /* best effort */ }
442
+ }
443
+ if (leaseOwned) {
444
+ releaseLease();
445
+ removeUnixSocket();
446
+ }
447
+ });