sinapse-ai 1.14.0 → 1.16.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.
- package/.sinapse-ai/data/entity-registry.yaml +66 -16
- package/.sinapse-ai/development/tasks/spec-analyze.md +460 -0
- package/.sinapse-ai/development/tasks/spec-clarify.md +374 -0
- package/.sinapse-ai/development/templates/chrome-brain/rules/chrome-brain-autoload.md +4 -0
- package/.sinapse-ai/development/templates/chrome-brain/scripts/chrome-brain-log.cjs +78 -0
- package/.sinapse-ai/development/templates/chrome-brain/scripts/chrome-ensure.cjs +108 -0
- package/.sinapse-ai/development/workflows/spec-pipeline.yaml +121 -7
- package/.sinapse-ai/install-manifest.yaml +34 -14
- package/.sinapse-ai/product/templates/spec-tmpl.md +18 -0
- package/.sinapse-ai/product/templates/statusline/agent-badges.json +862 -0
- package/.sinapse-ai/product/templates/statusline/statusline-script.js +36 -2
- package/.sinapse-ai/product/templates/statusline/track-agent.cjs +119 -9
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +2 -6
- package/bin/commands/install.js +22 -44
- package/bin/lib/header.js +14 -10
- package/bin/lib/setup-statusline.js +12 -1
- package/bin/modules/chrome-brain-installer.js +124 -199
- package/package.json +1 -1
- package/scripts/gen-agent-badges.js +105 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Spec Pipeline: Clarify Specification
|
|
2
|
+
|
|
3
|
+
> **Phase:** 4b - Clarify
|
|
4
|
+
> **Owner Agent:** @project-lead
|
|
5
|
+
> **Pipeline:** spec-pipeline
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Desambiguar a especificação ANTES do QA gate e do plano. Roda DEPOIS de `write` (spec escrita)
|
|
12
|
+
e ANTES de `critique`. Varre o `spec.md` por uma taxonomia de 9 categorias de cobertura, faz no
|
|
13
|
+
máximo 5 perguntas guiadas por (Impacto × Incerteza) — uma de cada vez — e integra cada resposta
|
|
14
|
+
atomicamente no `spec.md`. Sub-especificação descoberta tarde é o retrabalho mais caro do pipeline;
|
|
15
|
+
o clarify ataca isso cedo, perguntando apenas o que muda decisão downstream.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## autoClaude
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
autoClaude:
|
|
23
|
+
version: '3.0'
|
|
24
|
+
pipelinePhase: spec-clarify
|
|
25
|
+
|
|
26
|
+
elicit: true # Interactive - asks the user clarifying questions
|
|
27
|
+
deterministic: false # Output depends on user answers
|
|
28
|
+
composable: true
|
|
29
|
+
|
|
30
|
+
selfCritique:
|
|
31
|
+
required: false
|
|
32
|
+
|
|
33
|
+
inputs:
|
|
34
|
+
- name: storyId
|
|
35
|
+
type: string
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
- name: spec
|
|
39
|
+
type: file
|
|
40
|
+
path: docs/stories/{storyId}/spec/spec.md
|
|
41
|
+
required: true
|
|
42
|
+
|
|
43
|
+
- name: requirements
|
|
44
|
+
type: file
|
|
45
|
+
path: docs/stories/{storyId}/spec/requirements.json
|
|
46
|
+
required: false
|
|
47
|
+
|
|
48
|
+
outputs:
|
|
49
|
+
- name: spec.md
|
|
50
|
+
type: file
|
|
51
|
+
path: docs/stories/{storyId}/spec/spec.md
|
|
52
|
+
mutation: append # Adds/updates the `## Clarifications` section + target sections
|
|
53
|
+
|
|
54
|
+
verification:
|
|
55
|
+
type: gate
|
|
56
|
+
blocking: false # Skippable with explicit rework-risk warning
|
|
57
|
+
completion_field: clarificationComplete
|
|
58
|
+
|
|
59
|
+
contextRequirements:
|
|
60
|
+
projectContext: true
|
|
61
|
+
filesContext: true
|
|
62
|
+
implementationPlan: false
|
|
63
|
+
spec: true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Coverage Taxonomy (9 categories)
|
|
69
|
+
|
|
70
|
+
Varra o `spec.md` e marque cada categoria como **Clear / Partial / Missing**. Perguntas só nascem
|
|
71
|
+
de categorias `Partial` ou `Missing` cujo esclarecimento muda uma decisão downstream.
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
taxonomy:
|
|
75
|
+
- id: tax-1
|
|
76
|
+
name: 'Functional Scope & Behavior'
|
|
77
|
+
looks_for: 'Core actions, in/out of scope, behavioral edges'
|
|
78
|
+
|
|
79
|
+
- id: tax-2
|
|
80
|
+
name: 'Domain & Data Model'
|
|
81
|
+
looks_for: 'Entities, attributes, relationships, identity, lifecycle/state'
|
|
82
|
+
|
|
83
|
+
- id: tax-3
|
|
84
|
+
name: 'Interaction & UX Flow'
|
|
85
|
+
looks_for: 'Steps, states, error/empty states, accessibility intent'
|
|
86
|
+
|
|
87
|
+
- id: tax-4
|
|
88
|
+
name: 'Non-Functional Quality Attributes'
|
|
89
|
+
looks_for: 'Performance, scale, latency, availability, security targets (quantified)'
|
|
90
|
+
|
|
91
|
+
- id: tax-5
|
|
92
|
+
name: 'Integration & External Dependencies'
|
|
93
|
+
looks_for: 'External services, APIs, contracts, failure modes of integrations'
|
|
94
|
+
|
|
95
|
+
- id: tax-6
|
|
96
|
+
name: 'Edge Cases & Failure Handling'
|
|
97
|
+
looks_for: 'Boundary conditions, invalid input, partial failure, recovery'
|
|
98
|
+
|
|
99
|
+
- id: tax-7
|
|
100
|
+
name: 'Constraints & Tradeoffs'
|
|
101
|
+
looks_for: 'Hard constraints, rejected alternatives, explicit tradeoffs'
|
|
102
|
+
|
|
103
|
+
- id: tax-8
|
|
104
|
+
name: 'Terminology & Consistency'
|
|
105
|
+
looks_for: 'Consistent naming, no synonym drift, defined glossary terms'
|
|
106
|
+
|
|
107
|
+
- id: tax-9
|
|
108
|
+
name: 'Completion Signals'
|
|
109
|
+
looks_for: 'Measurable done criteria, acceptance signals, Success Criteria present'
|
|
110
|
+
|
|
111
|
+
- id: tax-misc
|
|
112
|
+
name: 'Misc / Signals'
|
|
113
|
+
looks_for: 'TODO / ??? placeholders, unquantified adjectives (fast/scalable/secure), [NEEDS CLARIFICATION] markers'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Question Selection (Impact × Uncertainty)
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
selection:
|
|
122
|
+
heuristic: 'Impact × Uncertainty'
|
|
123
|
+
|
|
124
|
+
rule: |
|
|
125
|
+
Only include questions whose answers materially impact architecture, data modeling,
|
|
126
|
+
task decomposition, test design, UX behavior, operational readiness, or compliance
|
|
127
|
+
validation. Discard cosmetic questions that do not change a downstream decision.
|
|
128
|
+
|
|
129
|
+
prioritize:
|
|
130
|
+
- High impact + high uncertainty (ask first)
|
|
131
|
+
- High impact + medium uncertainty
|
|
132
|
+
discard:
|
|
133
|
+
- Low impact (any uncertainty)
|
|
134
|
+
- Already Clear in the taxonomy scan
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Question Format
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
question_format:
|
|
143
|
+
max_questions: 5
|
|
144
|
+
one_at_a_time: true # Ask, wait for answer, integrate, then next
|
|
145
|
+
|
|
146
|
+
styles:
|
|
147
|
+
- type: multiple_choice
|
|
148
|
+
options: '2 to 5 mutually exclusive options'
|
|
149
|
+
include_recommendation: true # Mark the recommended option
|
|
150
|
+
- type: short_answer
|
|
151
|
+
constraint: 'answer in <= 5 words'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Integration (atomic)
|
|
157
|
+
|
|
158
|
+
Cada resposta é integrada IMEDIATAMENTE antes da próxima pergunta — registro + aplicação na seção-alvo.
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
integration:
|
|
162
|
+
log_section:
|
|
163
|
+
create_if_missing: '## Clarifications'
|
|
164
|
+
subsection: '### Session {YYYY-MM-DD}'
|
|
165
|
+
entry_format: '- Q: {question} -> A: {answer}'
|
|
166
|
+
|
|
167
|
+
apply_change:
|
|
168
|
+
description: 'Apply the answer to the target section of the spec'
|
|
169
|
+
targets:
|
|
170
|
+
- Functional Requirements (FR-*)
|
|
171
|
+
- Key Entities / Data Model
|
|
172
|
+
- Success Criteria (SC-*)
|
|
173
|
+
- Edge Cases
|
|
174
|
+
- Constraints (CON-*)
|
|
175
|
+
rule: 'Never log an answer without applying it to the target section'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Stop Criteria
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
stop_when:
|
|
184
|
+
any_of:
|
|
185
|
+
- 'All critical ambiguities resolved'
|
|
186
|
+
- 'User says proceed / done / skip'
|
|
187
|
+
- 'Reached 5 questions'
|
|
188
|
+
- 'No critical ambiguity detected in the taxonomy scan (zero questions is valid)'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Gate
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
gate:
|
|
197
|
+
order: 'Run (and complete) clarify BEFORE the plan phase'
|
|
198
|
+
blocking: false
|
|
199
|
+
|
|
200
|
+
on_skip:
|
|
201
|
+
allowed: true
|
|
202
|
+
required_action: |
|
|
203
|
+
Warn explicitly: downstream rework risk increases when clarification is skipped.
|
|
204
|
+
message: |
|
|
205
|
+
⚠️ Skipping clarification. Ambiguities in the spec may surface during plan/implement,
|
|
206
|
+
increasing rework cost. Proceeding at user request.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Execution Flow
|
|
212
|
+
|
|
213
|
+
### Step 1: Scan Coverage
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
scan:
|
|
217
|
+
action: scan_spec_against_taxonomy
|
|
218
|
+
for_each: category in taxonomy
|
|
219
|
+
process: |
|
|
220
|
+
1. Read spec.md
|
|
221
|
+
2. Mark category Clear / Partial / Missing
|
|
222
|
+
3. Note specific gap location (section)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Step 2: Select Questions
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
select:
|
|
229
|
+
action: rank_by_impact_uncertainty
|
|
230
|
+
process: |
|
|
231
|
+
1. Build candidate questions from Partial/Missing categories
|
|
232
|
+
2. Apply the materially-impacts filter (discard cosmetic)
|
|
233
|
+
3. Rank by Impact x Uncertainty
|
|
234
|
+
4. Cap the queue at 5
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Step 3: Ask & Integrate Loop
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
ask_loop:
|
|
241
|
+
for_each: question in selected (max 5)
|
|
242
|
+
process: |
|
|
243
|
+
1. Ask ONE question (multiple-choice 2-5 opts w/ recommendation, OR short answer <=5 words)
|
|
244
|
+
2. Wait for the answer
|
|
245
|
+
3. Append `- Q: ... -> A: ...` under `## Clarifications` / `### Session {date}`
|
|
246
|
+
4. Apply the answer to the target spec section
|
|
247
|
+
5. Re-check stop criteria; break if met
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Step 4: Finalize
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
finalize:
|
|
254
|
+
action: confirm_clarification_complete
|
|
255
|
+
process: |
|
|
256
|
+
1. Ensure every asked question was both logged AND applied
|
|
257
|
+
2. Set clarificationComplete = true
|
|
258
|
+
3. Hand off to critique
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Integration
|
|
264
|
+
|
|
265
|
+
### Command Integration (@project-lead)
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
command:
|
|
269
|
+
name: '*clarify-spec'
|
|
270
|
+
syntax: '*clarify-spec {story-id}'
|
|
271
|
+
agent: pm
|
|
272
|
+
|
|
273
|
+
examples:
|
|
274
|
+
- '*clarify-spec STORY-42'
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Pipeline Integration
|
|
278
|
+
|
|
279
|
+
```yaml
|
|
280
|
+
pipeline:
|
|
281
|
+
phase: clarify
|
|
282
|
+
previous_phase: spec
|
|
283
|
+
next_phase: critique
|
|
284
|
+
|
|
285
|
+
requires:
|
|
286
|
+
- spec.md
|
|
287
|
+
|
|
288
|
+
optional:
|
|
289
|
+
- requirements.json
|
|
290
|
+
|
|
291
|
+
gate: true # Order gate: complete before plan
|
|
292
|
+
blocking: false # Skippable with rework-risk warning
|
|
293
|
+
|
|
294
|
+
pass_to_next:
|
|
295
|
+
- spec.md
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Error Handling
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
errors:
|
|
304
|
+
- id: missing-spec
|
|
305
|
+
condition: 'spec.md not found'
|
|
306
|
+
action: 'Halt - cannot clarify without a spec'
|
|
307
|
+
blocking: true
|
|
308
|
+
|
|
309
|
+
- id: no-ambiguity
|
|
310
|
+
condition: 'Taxonomy scan finds zero critical ambiguities'
|
|
311
|
+
action: 'Skip questions, set clarificationComplete=true, proceed to critique'
|
|
312
|
+
blocking: false
|
|
313
|
+
|
|
314
|
+
- id: user-defers
|
|
315
|
+
condition: 'User declines to answer / says proceed'
|
|
316
|
+
action: 'Warn of rework risk, set clarificationComplete=false, proceed'
|
|
317
|
+
blocking: false
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Examples
|
|
323
|
+
|
|
324
|
+
### Example: Clarify a vague NFR
|
|
325
|
+
|
|
326
|
+
**Input:** spec.md says "the endpoint should be fast" (tax-4 = Partial, unquantified adjective)
|
|
327
|
+
|
|
328
|
+
**Interaction:**
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
Q: What is the acceptable p95 latency for the endpoint?
|
|
332
|
+
A) < 100ms B) < 300ms (recommended) C) < 1s D) No target needed
|
|
333
|
+
A: B
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Spec mutation:**
|
|
337
|
+
|
|
338
|
+
```markdown
|
|
339
|
+
## Clarifications
|
|
340
|
+
|
|
341
|
+
### Session 2026-06-24
|
|
342
|
+
|
|
343
|
+
- Q: What is the acceptable p95 latency for the endpoint? → A: < 300ms (p95)
|
|
344
|
+
|
|
345
|
+
<!-- and NFR section updated: -->
|
|
346
|
+
| Category | Requirement | Target |
|
|
347
|
+
| ----------- | ------------------------ | ------------- |
|
|
348
|
+
| Performance | Endpoint response (p95) | < 300ms |
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Metadata
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
metadata:
|
|
357
|
+
story: '3.4b'
|
|
358
|
+
epic: 'Epic 3 - Spec Pipeline'
|
|
359
|
+
created: '2026-06-24'
|
|
360
|
+
author: '@project-lead (Axis)'
|
|
361
|
+
version: '1.0.0'
|
|
362
|
+
source: 'GitHub Spec Kit — templates/commands/clarify.md'
|
|
363
|
+
tags:
|
|
364
|
+
- spec-pipeline
|
|
365
|
+
- clarify
|
|
366
|
+
- ambiguity
|
|
367
|
+
- elicitation
|
|
368
|
+
- product-lead
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Handoff
|
|
372
|
+
next_agent: @quality-gate
|
|
373
|
+
next_command: *critique-spec {story-id}
|
|
374
|
+
condition: Clarification complete (no critical ambiguities open) OR user proceeds with rework-risk warning
|
|
@@ -21,6 +21,10 @@ When the user's prompt matches ANY of these patterns, Chrome Brain is active:
|
|
|
21
21
|
4. Track screenshot count — max 15 per session
|
|
22
22
|
5. Handoff results to domain squad when applicable
|
|
23
23
|
|
|
24
|
+
## Janela fixa & login (uma vez)
|
|
25
|
+
|
|
26
|
+
A automação roda numa **janela de Chrome dedicada e fixa** (perfil `~/.chrome-debug-profile`), separada do Chrome pessoal do usuário. Ela é lançada **só quando uma tarefa de browser precisa** e **nunca é morta enquanto saudável** (o `chrome-ensure` só relança se a porta estiver realmente fora do ar). Na primeira vez que uma tarefa acessar um site que exige login, o usuário loga **uma vez** nessa janela — o perfil persiste, então não precisa logar de novo. Para logar proativamente em todas as contas de uma vez: `sinapse chrome-brain login`.
|
|
27
|
+
|
|
24
28
|
## Auto-Learning — MANDATORY
|
|
25
29
|
|
|
26
30
|
After completing ANY browser automation task, evaluate:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Chrome Brain — chrome-brain-log (Node, cross-platform).
|
|
6
|
+
*
|
|
7
|
+
* Called by the PostToolUse hook after every browser MCP tool. Appends a usage
|
|
8
|
+
* line and warns when the per-session screenshot count gets risky (the API has
|
|
9
|
+
* a ~20MB response cap). Node version of the old bash logger so it runs hidden
|
|
10
|
+
* on Windows (no console window) and needs no date/awk/grep.
|
|
11
|
+
*
|
|
12
|
+
* FAIL-SOFT: never throws, always exits 0.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
function toolName() {
|
|
20
|
+
// Claude Code passes a JSON payload on stdin; fall back to env then 'unknown'.
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
23
|
+
if (raw) {
|
|
24
|
+
const j = JSON.parse(raw);
|
|
25
|
+
return j.tool_name || j.toolName || process.env.HOOK_TOOL_NAME || 'unknown';
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
/* no stdin / not JSON */
|
|
29
|
+
}
|
|
30
|
+
return process.env.HOOK_TOOL_NAME || 'unknown';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const dir = path.join(os.homedir(), '.chrome-brain');
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const today = now.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
|
39
|
+
const time = now.toTimeString().slice(0, 8); // HH:MM:SS
|
|
40
|
+
const tool = toolName();
|
|
41
|
+
|
|
42
|
+
fs.appendFileSync(path.join(dir, `session-${today}.log`), `${time} ${tool}\n`);
|
|
43
|
+
|
|
44
|
+
if (/take_screenshot|take_snapshot/.test(tool)) {
|
|
45
|
+
const countFile = path.join(dir, '.screenshot-count');
|
|
46
|
+
const dateFile = path.join(dir, '.screenshot-date');
|
|
47
|
+
|
|
48
|
+
let lastDate = '';
|
|
49
|
+
try {
|
|
50
|
+
lastDate = fs.readFileSync(dateFile, 'utf8').trim();
|
|
51
|
+
} catch {
|
|
52
|
+
/* first run */
|
|
53
|
+
}
|
|
54
|
+
if (lastDate !== today) {
|
|
55
|
+
fs.writeFileSync(dateFile, today);
|
|
56
|
+
fs.writeFileSync(countFile, '0');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let count = 0;
|
|
60
|
+
try {
|
|
61
|
+
count = parseInt(fs.readFileSync(countFile, 'utf8'), 10) || 0;
|
|
62
|
+
} catch {
|
|
63
|
+
/* default 0 */
|
|
64
|
+
}
|
|
65
|
+
count += 1;
|
|
66
|
+
fs.writeFileSync(countFile, String(count));
|
|
67
|
+
|
|
68
|
+
if (count === 12) {
|
|
69
|
+
process.stderr.write('WARNING: 12 screenshots this session. Consider saving state and rotating.\n');
|
|
70
|
+
} else if (count >= 15) {
|
|
71
|
+
process.stderr.write('CRITICAL: 15+ screenshots. Session at risk of the API size cap. Save state NOW.\n');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* fail-soft */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
process.exit(0);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Chrome Brain — chrome-ensure (Node, cross-platform).
|
|
6
|
+
*
|
|
7
|
+
* Called by the PreToolUse hook before any browser MCP tool. Guarantees the
|
|
8
|
+
* SINGLE fixed-profile debug Chrome is up on the CDP port — and crucially:
|
|
9
|
+
* - NEVER kills a healthy instance (the old bash script force-killed on a
|
|
10
|
+
* flaky check, which dropped the live DevTools connection).
|
|
11
|
+
* - Launches detached + windowsHide so NO console (CMD/PowerShell) window
|
|
12
|
+
* ever pops (the old `#!/bin/bash` script spawned a console on Windows).
|
|
13
|
+
* - Uses one fixed profile so the user logs in ONCE and stays logged in.
|
|
14
|
+
*
|
|
15
|
+
* Reads config from ~/.sinapse/chrome-brain.json (written at install time):
|
|
16
|
+
* { "chromePath": "...", "port": 9222, "profile": "~/.chrome-debug-profile" }
|
|
17
|
+
*
|
|
18
|
+
* FAIL-SOFT: any error -> exit 0. A browser helper is never worth blocking a
|
|
19
|
+
* tool call (the MCP itself will surface a clear error if Chrome is truly down).
|
|
20
|
+
*
|
|
21
|
+
* Pass `--visible` (or run via `chrome-brain login`) to force a launch even when
|
|
22
|
+
* an instance is already up — used to open the window for first-time login.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const http = require('http');
|
|
29
|
+
const { spawn } = require('child_process');
|
|
30
|
+
|
|
31
|
+
const CONFIG_PATH = path.join(os.homedir(), '.sinapse', 'chrome-brain.json');
|
|
32
|
+
|
|
33
|
+
function readConfig() {
|
|
34
|
+
try {
|
|
35
|
+
const c = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
36
|
+
if (!c || !c.chromePath) return null;
|
|
37
|
+
return {
|
|
38
|
+
chromePath: c.chromePath,
|
|
39
|
+
port: Number(c.port) || 9222,
|
|
40
|
+
profile: c.profile || path.join(os.homedir(), '.chrome-debug-profile'),
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// True iff a debug Chrome is already answering CDP on the port. Short timeout so
|
|
48
|
+
// the hook never stalls a tool call.
|
|
49
|
+
function isAlive(port) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const req = http.get(
|
|
52
|
+
{ host: '127.0.0.1', port, path: '/json/version', timeout: 1200 },
|
|
53
|
+
(res) => {
|
|
54
|
+
res.resume();
|
|
55
|
+
resolve(res.statusCode === 200);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
req.on('error', () => resolve(false));
|
|
59
|
+
req.on('timeout', () => {
|
|
60
|
+
req.destroy();
|
|
61
|
+
resolve(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function launch(cfg) {
|
|
67
|
+
const args = [
|
|
68
|
+
`--remote-debugging-port=${cfg.port}`,
|
|
69
|
+
`--user-data-dir=${cfg.profile}`,
|
|
70
|
+
'--no-first-run',
|
|
71
|
+
'--no-default-browser-check',
|
|
72
|
+
];
|
|
73
|
+
// detached + ignored stdio + unref: Chrome survives this short-lived hook
|
|
74
|
+
// process and keeps the fixed profile alive across the whole session.
|
|
75
|
+
// windowsHide hides any console wrapper (chrome.exe itself is a GUI app, so
|
|
76
|
+
// its own window still shows — that is intentional, the user logs in there).
|
|
77
|
+
const child = spawn(cfg.chromePath, args, {
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: 'ignore',
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
});
|
|
82
|
+
child.unref();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const force = process.argv.includes('--visible') || process.argv.includes('--force');
|
|
87
|
+
const cfg = readConfig();
|
|
88
|
+
if (!cfg) process.exit(0); // not configured -> do nothing, never block
|
|
89
|
+
|
|
90
|
+
const alive = await isAlive(cfg.port);
|
|
91
|
+
if (alive && !force) process.exit(0); // healthy -> NEVER touch it, just return
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
launch(cfg);
|
|
95
|
+
} catch {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Poll until ready (~10s max). We never kill anything; if it does not come up
|
|
100
|
+
// we exit quietly and let the MCP report the connection error.
|
|
101
|
+
for (let i = 0; i < 20; i++) {
|
|
102
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
103
|
+
if (await isAlive(cfg.port)) process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main().catch(() => process.exit(0));
|