@luanpdd/kit-mcp 1.34.0 → 1.36.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 (118) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2 -2
  3. package/bin/mcp.js +6 -6
  4. package/bin/ui.js +74 -74
  5. package/gates/ai-prompt-stability.md +120 -120
  6. package/gates/budget-description.md +68 -68
  7. package/gates/confidence.md +29 -29
  8. package/gates/dependency-check.md +33 -33
  9. package/gates/dept-cycle-prevention.md +179 -179
  10. package/gates/golden-signals-coverage.md +133 -133
  11. package/gates/legacy-refactor-safety.md +178 -178
  12. package/gates/multi-tenant-rls-coverage.md +102 -102
  13. package/gates/no-personal-uuid.md +72 -72
  14. package/gates/obs-agents-mcp-supabase.md +86 -86
  15. package/gates/obs-skills-frontmatter.md +76 -76
  16. package/gates/observability-coverage.md +151 -151
  17. package/gates/omm-no-regression.md +83 -83
  18. package/gates/postmortem-template-required.md +127 -127
  19. package/gates/prr-checklist-coverage.md +128 -128
  20. package/gates/regression.md +32 -32
  21. package/gates/release-pipeline-policy.md +132 -132
  22. package/gates/secrets-scan.md +33 -33
  23. package/gates/service-role-not-in-user-facing.md +113 -113
  24. package/gates/skill-must-include.md +71 -71
  25. package/gates/sync-idempotent.md +62 -62
  26. package/gates/verify-phase-goal.md +34 -34
  27. package/kit/agents/designer-ui.md +216 -216
  28. package/kit/agents/workflow-generator.md +537 -0
  29. package/kit/commands/adicionar-backlog.md +1 -1
  30. package/kit/commands/adicionar-fase.md +1 -1
  31. package/kit/commands/adicionar-tarefa.md +1 -1
  32. package/kit/commands/auditar-observabilidade.md +103 -103
  33. package/kit/commands/auditar-toil.md +129 -129
  34. package/kit/commands/caracterizar-prompt.md +195 -195
  35. package/kit/commands/criar-workflow.md +158 -0
  36. package/kit/commands/definir-perfil.md +1 -1
  37. package/kit/commands/definir-slo.md +108 -108
  38. package/kit/commands/fio.md +1 -1
  39. package/kit/commands/golden-signals.md +142 -142
  40. package/kit/commands/instrumentar-fase.md +200 -200
  41. package/kit/commands/investigar-producao.md +162 -162
  42. package/kit/commands/observabilidade.md +118 -118
  43. package/kit/commands/postmortem.md +179 -179
  44. package/kit/commands/prr.md +205 -205
  45. package/kit/commands/publicar-rapido.md +207 -207
  46. package/kit/commands/risk-budget.md +220 -220
  47. package/kit/commands/sre.md +230 -230
  48. package/kit/file-manifest.json +5 -2
  49. package/kit/framework/references/output-style.md +22 -22
  50. package/kit/hooks/post-apply-migration.js +199 -199
  51. package/kit/hooks/sidecar-tool-publisher.js +210 -210
  52. package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -224
  53. package/kit/skills/_shared-legacy/glossary.md +389 -389
  54. package/kit/skills/_shared-multi-tenant/glossary.md +186 -186
  55. package/kit/skills/_shared-observability/glossary.md +396 -396
  56. package/kit/skills/_shared-sre/glossary.md +712 -712
  57. package/kit/skills/_shared-supabase/glossary.md +234 -234
  58. package/kit/skills/blameless-postmortems/SKILL.md +340 -340
  59. package/kit/skills/burn-rate-alerting/SKILL.md +258 -258
  60. package/kit/skills/cascading-failures/SKILL.md +311 -311
  61. package/kit/skills/core-analysis-loop/SKILL.md +352 -352
  62. package/kit/skills/distributed-tracing/SKILL.md +362 -362
  63. package/kit/skills/dynamic-workflow-authoring/SKILL.md +327 -0
  64. package/kit/skills/eliminating-toil/SKILL.md +243 -243
  65. package/kit/skills/event-based-slos/SKILL.md +296 -296
  66. package/kit/skills/four-golden-signals/SKILL.md +314 -314
  67. package/kit/skills/hermetic-builds/SKILL.md +323 -323
  68. package/kit/skills/legacy-monster-methods/SKILL.md +444 -444
  69. package/kit/skills/llm-as-dependency/SKILL.md +436 -436
  70. package/kit/skills/load-shedding-graceful-degradation/SKILL.md +396 -396
  71. package/kit/skills/observability-driven-development/SKILL.md +315 -315
  72. package/kit/skills/observability-maturity-model/SKILL.md +222 -222
  73. package/kit/skills/opentelemetry-standard/SKILL.md +351 -351
  74. package/kit/skills/production-readiness-review/SKILL.md +305 -305
  75. package/kit/skills/release-engineering/SKILL.md +367 -367
  76. package/kit/skills/retry-strategies/SKILL.md +372 -372
  77. package/kit/skills/sre-risk-management/SKILL.md +221 -221
  78. package/kit/skills/structured-events/SKILL.md +265 -265
  79. package/kit/skills/supabase-cron-queues/SKILL.md +275 -275
  80. package/kit/skills/supabase-database-functions/SKILL.md +332 -332
  81. package/kit/skills/supabase-declarative-schema/SKILL.md +183 -183
  82. package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -253
  83. package/kit/skills/supabase-postgres-style/SKILL.md +138 -138
  84. package/kit/skills/supabase-storage/SKILL.md +234 -234
  85. package/kit/skills/telemetry-pipelines/SKILL.md +259 -259
  86. package/kit/skills/telemetry-sampling/SKILL.md +256 -256
  87. package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -261
  88. package/kit/skills/ui-contexto-produto/SKILL.md +248 -248
  89. package/kit/skills/ui-cor-estrategia/SKILL.md +213 -213
  90. package/kit/skills/ui-critica-auditoria/SKILL.md +260 -260
  91. package/kit/skills/ui-motion-funcional/SKILL.md +264 -264
  92. package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -259
  93. package/kit/skills/ui-tipografia/SKILL.md +211 -211
  94. package/package.json +1 -1
  95. package/src/cli/index.js +1114 -1114
  96. package/src/cli/render.js +194 -194
  97. package/src/cli/upgrade-check.js +135 -135
  98. package/src/core/error-redaction.js +76 -76
  99. package/src/core/failures.js +153 -153
  100. package/src/core/gate-runner.js +205 -205
  101. package/src/core/gates.js +82 -82
  102. package/src/core/logger.js +170 -170
  103. package/src/core/manifest-verify.js +174 -174
  104. package/src/core/metrics.js +268 -268
  105. package/src/core/notify.js +60 -60
  106. package/src/core/path-safety.js +141 -141
  107. package/src/core/replays.js +120 -120
  108. package/src/core/ui.js +185 -185
  109. package/src/mcp-server/install.js +149 -149
  110. package/src/mcp-server/roots.js +124 -124
  111. package/src/ui/auto-spawn.js +113 -113
  112. package/src/ui/browser.js +78 -78
  113. package/src/ui/client.js +130 -130
  114. package/src/ui/events.js +65 -65
  115. package/src/ui/lockfile.js +191 -191
  116. package/src/ui/port.js +67 -67
  117. package/src/ui/server.js +547 -547
  118. package/src/ui/wrapper.js +129 -129
@@ -1,210 +1,210 @@
1
- #!/usr/bin/env node
2
- // hook-version: 1.14.0
3
- // kit-mcp · Sidecar Tool Publisher (PostToolUse)
4
- //
5
- // Publishes every Claude Code tool invocation to the kit-mcp sidecar so the
6
- // localhost UI shows real-time activity from this IDE — including edits,
7
- // reads, bash, agent spawns, MCP calls. Closes the gap where the sidecar
8
- // previously only saw `kit sync`/`reverse-sync`/`gates` operations.
9
- //
10
- // Pipeline: PostToolUse hook → reads stdin envelope → discovers sidecar
11
- // lockfile (per project_root) → POST /publish → fire-and-forget.
12
- //
13
- // SOFT failure: any error logs to stderr and exits 0. Never blocks the user.
14
- //
15
- // Module format: ESM (package.json "type": "module"). Stays compatible whether
16
- // run from inside the kit-mcp repo or from a user project.
17
- //
18
- // Enable via `~/.claude/settings.json` (or per-project `.claude/settings.json`):
19
- // {
20
- // "hooks": {
21
- // "PostToolUse": [{
22
- // "matcher": "*",
23
- // "hooks": [{
24
- // "type": "command",
25
- // "command": "node /abs/path/to/sidecar-tool-publisher.js"
26
- // }]
27
- // }]
28
- // }
29
- // }
30
-
31
- import fs from 'node:fs';
32
- import os from 'node:os';
33
- import path from 'node:path';
34
- import http from 'node:http';
35
- import crypto from 'node:crypto';
36
- import process from 'node:process';
37
-
38
- let input = '';
39
- const stdinTimeout = setTimeout(() => process.exit(0), 1500);
40
- process.stdin.setEncoding('utf8');
41
- process.stdin.on('data', (chunk) => { input += chunk; });
42
- process.stdin.on('end', () => {
43
- clearTimeout(stdinTimeout);
44
- try {
45
- const data = input ? JSON.parse(input) : {};
46
- const toolName = data.tool_name || data.toolName || 'unknown';
47
- const projectRoot = data.project_root || data.cwd || process.cwd();
48
-
49
- debugLog({ phase: 'received', toolName, projectRoot, cwd: process.cwd(), keys: Object.keys(data) });
50
-
51
- // Try requested projectRoot first; if no lockfile found, scan all
52
- // kit-mcp-ui-*.lock files in tmpdir and pick one that healthz-responds.
53
- // This makes the hook resilient to projectRoot mismatch (case, separators,
54
- // trailing slash, parent-of-project edits, etc).
55
- let sidecar = readSidecarLock(projectRoot);
56
- if (!sidecar) sidecar = scanAnyRunningSidecar();
57
- if (!sidecar) {
58
- debugLog({ phase: 'no_sidecar', projectRoot });
59
- process.exit(0);
60
- }
61
- const { port, token } = sidecar;
62
-
63
- const payload = {
64
- tool: toolName,
65
- sessionId: data.session_id || data.sessionId || null,
66
- durationMs: typeof data.duration_ms === 'number' ? data.duration_ms : null,
67
- argsSummary: summarizeArgs(data.tool_input),
68
- source: detectSource(),
69
- };
70
-
71
- const event = {
72
- type: 'tool_invocation',
73
- ts: Date.now(),
74
- runId: null,
75
- payload,
76
- };
77
-
78
- publish(port, token, event).then(() => process.exit(0));
79
- } catch (err) {
80
- process.stderr.write(`[sidecar-tool-publisher] ${err.message}\n`);
81
- process.exit(0);
82
- }
83
- });
84
-
85
- function readSidecarLock(projectRoot) {
86
- // Mirror src/ui/lockfile.js#lockPathFor (sha1(projectRoot).slice(0,16))
87
- try {
88
- const hash = crypto.createHash('sha1').update(projectRoot).digest('hex').slice(0, 16);
89
- const lockPath = path.join(os.tmpdir(), `kit-mcp-ui-${hash}.lock`);
90
- const raw = fs.readFileSync(lockPath, 'utf8');
91
- const lock = JSON.parse(raw);
92
- if (typeof lock.port !== 'number') return null;
93
- return {
94
- port: lock.port,
95
- // SEC-14-02 (kit-mcp v1.14+): null for sidecars from v1.13 and earlier.
96
- token: typeof lock.token === 'string' && /^[0-9a-f]{64}$/.test(lock.token) ? lock.token : null,
97
- };
98
- } catch {
99
- return null;
100
- }
101
- }
102
-
103
- // Scan os.tmpdir() for any kit-mcp-ui-*.lock and return the first { port, token }
104
- // of a live sidecar. Used as a fallback when projectRoot doesn't match any
105
- // known lockfile (case variants, separator differences, parent-dir edits, etc).
106
- function scanAnyRunningSidecar() {
107
- try {
108
- const dir = os.tmpdir();
109
- const entries = fs.readdirSync(dir);
110
- for (const name of entries) {
111
- if (!/^kit-mcp-ui-[0-9a-f]{16}\.lock$/.test(name)) continue;
112
- try {
113
- const raw = fs.readFileSync(path.join(dir, name), 'utf8');
114
- const lock = JSON.parse(raw);
115
- if (typeof lock.port === 'number' && typeof lock.pid === 'number') {
116
- try {
117
- process.kill(lock.pid, 0);
118
- // SEC-14-02: return token from same lockfile so cross-project
119
- // publishing can authenticate. If token missing (older sidecar),
120
- // returns null → publish degrades to 401 silent-fail.
121
- return {
122
- port: lock.port,
123
- token: typeof lock.token === 'string' && /^[0-9a-f]{64}$/.test(lock.token) ? lock.token : null,
124
- };
125
- } catch { /* dead */ }
126
- }
127
- } catch { /* skip unreadable */ }
128
- }
129
- } catch { /* tmpdir unreadable */ }
130
- return null;
131
- }
132
-
133
- function debugLog(obj) {
134
- if (process.env.KIT_MCP_HOOK_DEBUG !== '1') return;
135
- try {
136
- const line = JSON.stringify({ ts: Date.now(), ...obj }) + '\n';
137
- fs.appendFileSync(path.join(os.tmpdir(), 'kit-mcp-hook.log'), line);
138
- } catch { /* noop */ }
139
- }
140
-
141
- function summarizeArgs(args) {
142
- if (!args || typeof args !== 'object') return null;
143
- const out = {};
144
- if (typeof args.command === 'string') out.command = truncate(args.command, 120);
145
- if (typeof args.file_path === 'string') out.file_path = truncate(args.file_path, 200);
146
- if (typeof args.pattern === 'string') out.pattern = truncate(args.pattern, 80);
147
- if (typeof args.url === 'string') out.url = truncate(args.url, 120);
148
- if (typeof args.description === 'string') out.description = truncate(args.description, 80);
149
- if (Array.isArray(args.actions)) out.action_count = args.actions.length;
150
- return Object.keys(out).length ? out : null;
151
- }
152
-
153
- function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + '…' : s; }
154
-
155
- function detectSource() {
156
- const ide = detectIde();
157
- const pid = process.ppid || process.pid;
158
- const id = `${ide}:${pid}`;
159
- return {
160
- id,
161
- ide,
162
- pid,
163
- hostname: os.hostname(),
164
- };
165
- }
166
-
167
- function detectIde() {
168
- if (process.env.CLAUDE_CODE_VERSION || process.env.CLAUDECODE) return 'claude-code';
169
- if (process.env.CURSOR_TRACE_ID) return 'cursor';
170
- if (process.env.TERM_PROGRAM === 'vscode') return 'vscode';
171
- if (process.env.JETBRAINS_IDE) return 'jetbrains';
172
- return 'unknown';
173
- }
174
-
175
- function publish(port, token, event) {
176
- return new Promise((resolve) => {
177
- const body = JSON.stringify(event);
178
- const req = http.request({
179
- method: 'POST',
180
- host: '127.0.0.1',
181
- port,
182
- path: '/publish',
183
- agent: false,
184
- headers: {
185
- host: `127.0.0.1:${port}`,
186
- 'content-type': 'application/json',
187
- 'content-length': Buffer.byteLength(body, 'utf8'),
188
- origin: `http://127.0.0.1:${port}`,
189
- connection: 'close',
190
- // SEC-14-02: token is null for sidecars from v1.13 and earlier; in that
191
- // case we omit the header and the server returns 401, which the hook
192
- // silent-fails on (matching pre-existing soft-fail discipline). A
193
- // shipped hook v1.14 talking to a still-running sidecar v1.13 just
194
- // loses the event — acceptable trade-off.
195
- ...(token ? { authorization: `Bearer ${token}` } : {}),
196
- },
197
- }, (res) => {
198
- // Drain response body to ensure server has fully processed before resolve.
199
- // v1.12.1 fix: await BOTH 'end' and 'close' to avoid premature exit before
200
- // sidecar publishes via SSE. Preserve that pattern here.
201
- res.resume();
202
- res.on('end', resolve);
203
- res.on('close', resolve);
204
- });
205
- req.on('error', () => resolve());
206
- req.setTimeout(800, () => { try { req.destroy(); } catch (_) { /* noop */ } resolve(); });
207
- req.write(body);
208
- req.end();
209
- });
210
- }
1
+ #!/usr/bin/env node
2
+ // hook-version: 1.14.0
3
+ // kit-mcp · Sidecar Tool Publisher (PostToolUse)
4
+ //
5
+ // Publishes every Claude Code tool invocation to the kit-mcp sidecar so the
6
+ // localhost UI shows real-time activity from this IDE — including edits,
7
+ // reads, bash, agent spawns, MCP calls. Closes the gap where the sidecar
8
+ // previously only saw `kit sync`/`reverse-sync`/`gates` operations.
9
+ //
10
+ // Pipeline: PostToolUse hook → reads stdin envelope → discovers sidecar
11
+ // lockfile (per project_root) → POST /publish → fire-and-forget.
12
+ //
13
+ // SOFT failure: any error logs to stderr and exits 0. Never blocks the user.
14
+ //
15
+ // Module format: ESM (package.json "type": "module"). Stays compatible whether
16
+ // run from inside the kit-mcp repo or from a user project.
17
+ //
18
+ // Enable via `~/.claude/settings.json` (or per-project `.claude/settings.json`):
19
+ // {
20
+ // "hooks": {
21
+ // "PostToolUse": [{
22
+ // "matcher": "*",
23
+ // "hooks": [{
24
+ // "type": "command",
25
+ // "command": "node /abs/path/to/sidecar-tool-publisher.js"
26
+ // }]
27
+ // }]
28
+ // }
29
+ // }
30
+
31
+ import fs from 'node:fs';
32
+ import os from 'node:os';
33
+ import path from 'node:path';
34
+ import http from 'node:http';
35
+ import crypto from 'node:crypto';
36
+ import process from 'node:process';
37
+
38
+ let input = '';
39
+ const stdinTimeout = setTimeout(() => process.exit(0), 1500);
40
+ process.stdin.setEncoding('utf8');
41
+ process.stdin.on('data', (chunk) => { input += chunk; });
42
+ process.stdin.on('end', () => {
43
+ clearTimeout(stdinTimeout);
44
+ try {
45
+ const data = input ? JSON.parse(input) : {};
46
+ const toolName = data.tool_name || data.toolName || 'unknown';
47
+ const projectRoot = data.project_root || data.cwd || process.cwd();
48
+
49
+ debugLog({ phase: 'received', toolName, projectRoot, cwd: process.cwd(), keys: Object.keys(data) });
50
+
51
+ // Try requested projectRoot first; if no lockfile found, scan all
52
+ // kit-mcp-ui-*.lock files in tmpdir and pick one that healthz-responds.
53
+ // This makes the hook resilient to projectRoot mismatch (case, separators,
54
+ // trailing slash, parent-of-project edits, etc).
55
+ let sidecar = readSidecarLock(projectRoot);
56
+ if (!sidecar) sidecar = scanAnyRunningSidecar();
57
+ if (!sidecar) {
58
+ debugLog({ phase: 'no_sidecar', projectRoot });
59
+ process.exit(0);
60
+ }
61
+ const { port, token } = sidecar;
62
+
63
+ const payload = {
64
+ tool: toolName,
65
+ sessionId: data.session_id || data.sessionId || null,
66
+ durationMs: typeof data.duration_ms === 'number' ? data.duration_ms : null,
67
+ argsSummary: summarizeArgs(data.tool_input),
68
+ source: detectSource(),
69
+ };
70
+
71
+ const event = {
72
+ type: 'tool_invocation',
73
+ ts: Date.now(),
74
+ runId: null,
75
+ payload,
76
+ };
77
+
78
+ publish(port, token, event).then(() => process.exit(0));
79
+ } catch (err) {
80
+ process.stderr.write(`[sidecar-tool-publisher] ${err.message}\n`);
81
+ process.exit(0);
82
+ }
83
+ });
84
+
85
+ function readSidecarLock(projectRoot) {
86
+ // Mirror src/ui/lockfile.js#lockPathFor (sha1(projectRoot).slice(0,16))
87
+ try {
88
+ const hash = crypto.createHash('sha1').update(projectRoot).digest('hex').slice(0, 16);
89
+ const lockPath = path.join(os.tmpdir(), `kit-mcp-ui-${hash}.lock`);
90
+ const raw = fs.readFileSync(lockPath, 'utf8');
91
+ const lock = JSON.parse(raw);
92
+ if (typeof lock.port !== 'number') return null;
93
+ return {
94
+ port: lock.port,
95
+ // SEC-14-02 (kit-mcp v1.14+): null for sidecars from v1.13 and earlier.
96
+ token: typeof lock.token === 'string' && /^[0-9a-f]{64}$/.test(lock.token) ? lock.token : null,
97
+ };
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ // Scan os.tmpdir() for any kit-mcp-ui-*.lock and return the first { port, token }
104
+ // of a live sidecar. Used as a fallback when projectRoot doesn't match any
105
+ // known lockfile (case variants, separator differences, parent-dir edits, etc).
106
+ function scanAnyRunningSidecar() {
107
+ try {
108
+ const dir = os.tmpdir();
109
+ const entries = fs.readdirSync(dir);
110
+ for (const name of entries) {
111
+ if (!/^kit-mcp-ui-[0-9a-f]{16}\.lock$/.test(name)) continue;
112
+ try {
113
+ const raw = fs.readFileSync(path.join(dir, name), 'utf8');
114
+ const lock = JSON.parse(raw);
115
+ if (typeof lock.port === 'number' && typeof lock.pid === 'number') {
116
+ try {
117
+ process.kill(lock.pid, 0);
118
+ // SEC-14-02: return token from same lockfile so cross-project
119
+ // publishing can authenticate. If token missing (older sidecar),
120
+ // returns null → publish degrades to 401 silent-fail.
121
+ return {
122
+ port: lock.port,
123
+ token: typeof lock.token === 'string' && /^[0-9a-f]{64}$/.test(lock.token) ? lock.token : null,
124
+ };
125
+ } catch { /* dead */ }
126
+ }
127
+ } catch { /* skip unreadable */ }
128
+ }
129
+ } catch { /* tmpdir unreadable */ }
130
+ return null;
131
+ }
132
+
133
+ function debugLog(obj) {
134
+ if (process.env.KIT_MCP_HOOK_DEBUG !== '1') return;
135
+ try {
136
+ const line = JSON.stringify({ ts: Date.now(), ...obj }) + '\n';
137
+ fs.appendFileSync(path.join(os.tmpdir(), 'kit-mcp-hook.log'), line);
138
+ } catch { /* noop */ }
139
+ }
140
+
141
+ function summarizeArgs(args) {
142
+ if (!args || typeof args !== 'object') return null;
143
+ const out = {};
144
+ if (typeof args.command === 'string') out.command = truncate(args.command, 120);
145
+ if (typeof args.file_path === 'string') out.file_path = truncate(args.file_path, 200);
146
+ if (typeof args.pattern === 'string') out.pattern = truncate(args.pattern, 80);
147
+ if (typeof args.url === 'string') out.url = truncate(args.url, 120);
148
+ if (typeof args.description === 'string') out.description = truncate(args.description, 80);
149
+ if (Array.isArray(args.actions)) out.action_count = args.actions.length;
150
+ return Object.keys(out).length ? out : null;
151
+ }
152
+
153
+ function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + '…' : s; }
154
+
155
+ function detectSource() {
156
+ const ide = detectIde();
157
+ const pid = process.ppid || process.pid;
158
+ const id = `${ide}:${pid}`;
159
+ return {
160
+ id,
161
+ ide,
162
+ pid,
163
+ hostname: os.hostname(),
164
+ };
165
+ }
166
+
167
+ function detectIde() {
168
+ if (process.env.CLAUDE_CODE_VERSION || process.env.CLAUDECODE) return 'claude-code';
169
+ if (process.env.CURSOR_TRACE_ID) return 'cursor';
170
+ if (process.env.TERM_PROGRAM === 'vscode') return 'vscode';
171
+ if (process.env.JETBRAINS_IDE) return 'jetbrains';
172
+ return 'unknown';
173
+ }
174
+
175
+ function publish(port, token, event) {
176
+ return new Promise((resolve) => {
177
+ const body = JSON.stringify(event);
178
+ const req = http.request({
179
+ method: 'POST',
180
+ host: '127.0.0.1',
181
+ port,
182
+ path: '/publish',
183
+ agent: false,
184
+ headers: {
185
+ host: `127.0.0.1:${port}`,
186
+ 'content-type': 'application/json',
187
+ 'content-length': Buffer.byteLength(body, 'utf8'),
188
+ origin: `http://127.0.0.1:${port}`,
189
+ connection: 'close',
190
+ // SEC-14-02: token is null for sidecars from v1.13 and earlier; in that
191
+ // case we omit the header and the server returns 401, which the hook
192
+ // silent-fails on (matching pre-existing soft-fail discipline). A
193
+ // shipped hook v1.14 talking to a still-running sidecar v1.13 just
194
+ // loses the event — acceptable trade-off.
195
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
196
+ },
197
+ }, (res) => {
198
+ // Drain response body to ensure server has fully processed before resolve.
199
+ // v1.12.1 fix: await BOTH 'end' and 'close' to avoid premature exit before
200
+ // sidecar publishes via SSE. Preserve that pattern here.
201
+ res.resume();
202
+ res.on('end', resolve);
203
+ res.on('close', resolve);
204
+ });
205
+ req.on('error', () => resolve());
206
+ req.setTimeout(800, () => { try { req.destroy(); } catch (_) { /* noop */ } resolve(); });
207
+ req.write(body);
208
+ req.end();
209
+ });
210
+ }