haltija 1.1.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/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/build-bookmarklet.ts +107 -0
- package/bin/cli-subcommand.mjs +537 -0
- package/bin/format-events.mjs +125 -0
- package/bin/format-test.mjs +183 -0
- package/bin/format-tree.mjs +165 -0
- package/bin/hj.mjs +59 -0
- package/bin/mcp-setup.mjs +288 -0
- package/bin/server.ts +9 -0
- package/bin/tosijs-dev.mjs +591 -0
- package/bin/tosijs-dev.ts +74 -0
- package/dist/client.js +387 -0
- package/dist/component.js +6685 -0
- package/dist/index.js +10201 -0
- package/dist/server.js +9847 -0
- package/docs/CI-INTEGRATION.md +230 -0
- package/docs/EXECUTIVE-SUMMARY.md +213 -0
- package/docs/README.md +67 -0
- package/docs/REST-API.md +123 -0
- package/docs/ROADMAP.md +591 -0
- package/docs/UX-CRIMES.md +599 -0
- package/docs/agent-prompt.md +139 -0
- package/docs/getting-started/app.md +96 -0
- package/docs/getting-started/playground.md +75 -0
- package/docs/getting-started/service.md +96 -0
- package/docs/recipes.md +245 -0
- package/haltija-icon.svg +79 -0
- package/package.json +68 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text formatter for Haltija test results
|
|
3
|
+
*
|
|
4
|
+
* Flat step list, agent-scannable and human-readable.
|
|
5
|
+
*
|
|
6
|
+
* ok Login flow 2340ms 7/7
|
|
7
|
+
* 1 ok navigate /login 120ms
|
|
8
|
+
* 2 ok type #email "user@example.com" 85ms
|
|
9
|
+
* 3 ok click button[type=submit] 200ms
|
|
10
|
+
* 4 FAIL wait .dashboard 5000ms timeout
|
|
11
|
+
* > element not found, page shows: [.error-message]
|
|
12
|
+
* 5 skip assert url /dashboard
|
|
13
|
+
* patience 4/5 remaining streak=1 timeout=7000ms
|
|
14
|
+
* ---
|
|
15
|
+
* hj test-run --json
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a single test result
|
|
20
|
+
* @param {object} result - TestRunResult from POST /test/run
|
|
21
|
+
* @returns {string} formatted text output with footer
|
|
22
|
+
*/
|
|
23
|
+
export function formatTestResult(result) {
|
|
24
|
+
if (!result) return '(no result)\n---\nhj test-run --json'
|
|
25
|
+
|
|
26
|
+
const lines = []
|
|
27
|
+
const status = result.passed ? 'ok' : 'FAIL'
|
|
28
|
+
const name = result.test || 'unnamed'
|
|
29
|
+
const duration = result.duration ? `${result.duration}ms` : ''
|
|
30
|
+
const counts = result.summary
|
|
31
|
+
? `${result.summary.passed}/${result.summary.total}`
|
|
32
|
+
: ''
|
|
33
|
+
|
|
34
|
+
// Header
|
|
35
|
+
lines.push([status, name, duration, counts].filter(Boolean).join(' '))
|
|
36
|
+
|
|
37
|
+
// Steps
|
|
38
|
+
if (result.steps) {
|
|
39
|
+
for (const step of result.steps) {
|
|
40
|
+
const stepStatus = step.passed ? 'ok' : (step.error === 'skipped' ? 'skip' : 'FAIL')
|
|
41
|
+
const desc = formatStepDescription(step)
|
|
42
|
+
const dur = step.duration ? `${step.duration}ms` : ''
|
|
43
|
+
const err = (!step.passed && step.error && step.error !== 'skipped')
|
|
44
|
+
? step.error
|
|
45
|
+
: ''
|
|
46
|
+
|
|
47
|
+
lines.push(` ${step.index + 1} ${[stepStatus, desc, dur, err].filter(Boolean).join(' ')}`)
|
|
48
|
+
|
|
49
|
+
// Failure context detail
|
|
50
|
+
if (!step.passed && step.context) {
|
|
51
|
+
const detail = formatFailureContext(step.context)
|
|
52
|
+
if (detail) lines.push(` > ${detail}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Patience stats
|
|
58
|
+
if (result.patience) {
|
|
59
|
+
const p = result.patience
|
|
60
|
+
lines.push(` patience ${p.remaining}/${p.allowed} remaining streak=${p.consecutiveFailures}/${p.streak} timeout=${p.finalTimeoutMs}ms`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Footer
|
|
64
|
+
lines.push('---')
|
|
65
|
+
lines.push('hj test-run --json')
|
|
66
|
+
|
|
67
|
+
return lines.join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format a suite result
|
|
72
|
+
* @param {object} result - SuiteRunResult from POST /test/suite
|
|
73
|
+
* @returns {string} formatted text output with footer
|
|
74
|
+
*/
|
|
75
|
+
export function formatSuiteResult(result) {
|
|
76
|
+
if (!result) return '(no result)\n---\nhj test-run --json'
|
|
77
|
+
|
|
78
|
+
const lines = []
|
|
79
|
+
const status = result.summary?.failed === 0 ? 'ok' : 'FAIL'
|
|
80
|
+
const duration = result.duration ? `${result.duration}ms` : ''
|
|
81
|
+
const counts = result.summary
|
|
82
|
+
? `${result.summary.passed}/${result.summary.total} tests`
|
|
83
|
+
: ''
|
|
84
|
+
|
|
85
|
+
// Header
|
|
86
|
+
lines.push([status, 'suite', duration, counts].filter(Boolean).join(' '))
|
|
87
|
+
|
|
88
|
+
// Per-test summary lines
|
|
89
|
+
if (result.results) {
|
|
90
|
+
for (const testResult of result.results) {
|
|
91
|
+
const tStatus = testResult.passed ? 'ok' : 'FAIL'
|
|
92
|
+
const name = testResult.test || 'unnamed'
|
|
93
|
+
const dur = testResult.duration ? `${testResult.duration}ms` : ''
|
|
94
|
+
const tCounts = testResult.summary
|
|
95
|
+
? `${testResult.summary.passed}/${testResult.summary.total}`
|
|
96
|
+
: ''
|
|
97
|
+
lines.push(` ${[tStatus, name, dur, tCounts].filter(Boolean).join(' ')}`)
|
|
98
|
+
|
|
99
|
+
// Show first failure for failed tests
|
|
100
|
+
if (!testResult.passed && testResult.steps) {
|
|
101
|
+
const failed = testResult.steps.find(s => !s.passed)
|
|
102
|
+
if (failed) {
|
|
103
|
+
const desc = formatStepDescription(failed)
|
|
104
|
+
const err = failed.error || ''
|
|
105
|
+
lines.push(` step ${failed.index + 1}: ${[desc, err].filter(Boolean).join(' ')}`)
|
|
106
|
+
if (failed.context) {
|
|
107
|
+
const detail = formatFailureContext(failed.context)
|
|
108
|
+
if (detail) lines.push(` > ${detail}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Footer
|
|
116
|
+
lines.push('---')
|
|
117
|
+
lines.push('hj test-run --json')
|
|
118
|
+
|
|
119
|
+
return lines.join('\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract action + key identifier from a step
|
|
124
|
+
*/
|
|
125
|
+
function formatStepDescription(step) {
|
|
126
|
+
const s = step.step || step
|
|
127
|
+
const action = s.action || step.description || ''
|
|
128
|
+
|
|
129
|
+
switch (action) {
|
|
130
|
+
case 'navigate':
|
|
131
|
+
return `navigate ${s.url || ''}`
|
|
132
|
+
case 'click':
|
|
133
|
+
return `click ${s.selector || s.ref || ''}`
|
|
134
|
+
case 'type':
|
|
135
|
+
return `type ${s.selector || s.ref || ''} "${truncate(s.text || '', 30)}"`
|
|
136
|
+
case 'key':
|
|
137
|
+
return `key ${s.key || ''}`
|
|
138
|
+
case 'wait':
|
|
139
|
+
return `wait ${s.selector || s.duration + 'ms' || ''}`
|
|
140
|
+
case 'assert': {
|
|
141
|
+
const a = s.assertion || {}
|
|
142
|
+
const sel = a.selector || ''
|
|
143
|
+
const val = a.text || a.value || a.pattern || ''
|
|
144
|
+
return `assert ${a.type || ''} ${sel} ${val ? '"' + truncate(val, 30) + '"' : ''}`.trim()
|
|
145
|
+
}
|
|
146
|
+
case 'eval':
|
|
147
|
+
return `eval ${truncate(s.code || '', 40)}`
|
|
148
|
+
case 'verify':
|
|
149
|
+
return `verify ${truncate(s.eval || '', 40)}`
|
|
150
|
+
default:
|
|
151
|
+
return step.description || action || 'unknown'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format failure context into a single line
|
|
157
|
+
*/
|
|
158
|
+
function formatFailureContext(context) {
|
|
159
|
+
const parts = []
|
|
160
|
+
|
|
161
|
+
if (context.reason) {
|
|
162
|
+
parts.push(context.reason)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (context.buttonsOnPage?.length) {
|
|
166
|
+
parts.push(`page shows: [${context.buttonsOnPage.join(', ')}]`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (context.actual !== undefined && context.expected !== undefined) {
|
|
170
|
+
parts.push(`expected "${context.expected}" got "${context.actual}"`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (context.suggestion) {
|
|
174
|
+
parts.push(context.suggestion)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return parts.join(', ')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function truncate(str, max) {
|
|
181
|
+
if (!str || str.length <= max) return str
|
|
182
|
+
return str.slice(0, max - 1) + '…'
|
|
183
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text formatter for Haltija tree output
|
|
3
|
+
*
|
|
4
|
+
* Push/pop paren encoding: hierarchy for agents, indentation for humans.
|
|
5
|
+
*
|
|
6
|
+
* 1 body
|
|
7
|
+
* ( 2 div.header
|
|
8
|
+
* 3 button#submit "Submit" interactive
|
|
9
|
+
* 4 input#email interactive value="user@example.com"
|
|
10
|
+
* )
|
|
11
|
+
* ( 5 div.content
|
|
12
|
+
* ( 6 ul.nav
|
|
13
|
+
* 7 li "Home"
|
|
14
|
+
* 8 li "About"
|
|
15
|
+
* )
|
|
16
|
+
* 9 p "Welcome back"
|
|
17
|
+
* )
|
|
18
|
+
* ---
|
|
19
|
+
* hj tree --json
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MAX_TEXT_LEN = 80
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format a DomTreeNode tree as agent+human readable text
|
|
26
|
+
* @param {object} node - DomTreeNode from server response
|
|
27
|
+
* @param {number} indent - current indentation level (internal)
|
|
28
|
+
* @returns {string} formatted text output with footer
|
|
29
|
+
*/
|
|
30
|
+
export function formatTree(node, indent = 0) {
|
|
31
|
+
if (!node) return ''
|
|
32
|
+
|
|
33
|
+
const lines = []
|
|
34
|
+
formatNode(node, indent, lines)
|
|
35
|
+
|
|
36
|
+
// Footer: JSON escape hatch
|
|
37
|
+
lines.push('---')
|
|
38
|
+
lines.push('hj tree --json')
|
|
39
|
+
|
|
40
|
+
return lines.join('\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recursively format a node and its children
|
|
45
|
+
*/
|
|
46
|
+
function formatNode(node, indent, lines) {
|
|
47
|
+
if (!node) return
|
|
48
|
+
|
|
49
|
+
// Skip the haltija widget entirely
|
|
50
|
+
if (node.tag === 'haltija-dev') return
|
|
51
|
+
|
|
52
|
+
const prefix = ' '.repeat(indent)
|
|
53
|
+
const hasChildren = (node.children && node.children.length > 0) ||
|
|
54
|
+
(node.shadowChildren && node.shadowChildren.length > 0)
|
|
55
|
+
|
|
56
|
+
const line = buildLine(node)
|
|
57
|
+
|
|
58
|
+
if (hasChildren) {
|
|
59
|
+
// Push: ( before children
|
|
60
|
+
lines.push(`${prefix}( ${line}`)
|
|
61
|
+
|
|
62
|
+
// Recurse children
|
|
63
|
+
if (node.children) {
|
|
64
|
+
for (const child of node.children) {
|
|
65
|
+
formatNode(child, indent + 2, lines)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (node.shadowChildren) {
|
|
69
|
+
for (const child of node.shadowChildren) {
|
|
70
|
+
if (child.classes && child.classes.includes('widget')) continue
|
|
71
|
+
formatNode(child, indent + 2, lines)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pop: )
|
|
76
|
+
lines.push(`${prefix})`)
|
|
77
|
+
} else {
|
|
78
|
+
// Leaf node: just the line
|
|
79
|
+
lines.push(`${prefix}${line}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a single node's description line (without prefix/indent)
|
|
85
|
+
*/
|
|
86
|
+
function buildLine(node) {
|
|
87
|
+
const parts = []
|
|
88
|
+
|
|
89
|
+
// Ref number (bare, no colon)
|
|
90
|
+
parts.push(node.ref || '?')
|
|
91
|
+
|
|
92
|
+
// Tag with id and classes
|
|
93
|
+
let tagPart = node.tag || '?'
|
|
94
|
+
if (node.id) tagPart += `#${node.id}`
|
|
95
|
+
if (node.classes && node.classes.length) {
|
|
96
|
+
tagPart += '.' + node.classes.join('.')
|
|
97
|
+
}
|
|
98
|
+
parts.push(tagPart)
|
|
99
|
+
|
|
100
|
+
// Attributes (key=value, quote values with spaces)
|
|
101
|
+
if (node.attrs) {
|
|
102
|
+
for (const [key, val] of Object.entries(node.attrs)) {
|
|
103
|
+
if (val === '' || val === 'true') {
|
|
104
|
+
parts.push(key)
|
|
105
|
+
} else if (/\s/.test(val) || val.length > 40) {
|
|
106
|
+
parts.push(`${key}="${truncate(val, 40)}"`)
|
|
107
|
+
} else {
|
|
108
|
+
parts.push(`${key}=${val}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Form value (for inputs)
|
|
114
|
+
if (node.value !== undefined && node.value !== '') {
|
|
115
|
+
parts.push(`value="${truncate(node.value, 30)}"`)
|
|
116
|
+
}
|
|
117
|
+
if (node.checked !== undefined) {
|
|
118
|
+
parts.push(node.checked ? 'checked' : 'unchecked')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Flags (bare words, no brackets)
|
|
122
|
+
const flags = formatFlags(node.flags)
|
|
123
|
+
if (flags) parts.push(flags)
|
|
124
|
+
|
|
125
|
+
// Text content (quoted, at end)
|
|
126
|
+
if (node.text) {
|
|
127
|
+
parts.push(`"${truncate(node.text, MAX_TEXT_LEN)}"`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Truncation indicator
|
|
131
|
+
if (node.truncated && node.childCount) {
|
|
132
|
+
parts.push(`(${node.childCount} children)`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.join(' ')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Format flags as bare space-separated words */
|
|
139
|
+
function formatFlags(flags) {
|
|
140
|
+
if (!flags) return ''
|
|
141
|
+
const parts = []
|
|
142
|
+
|
|
143
|
+
if (flags.interactive) parts.push('interactive')
|
|
144
|
+
if (flags.disabled) parts.push('disabled')
|
|
145
|
+
if (flags.required) parts.push('required')
|
|
146
|
+
if (flags.readOnly) parts.push('readonly')
|
|
147
|
+
if (flags.focused) parts.push('focused')
|
|
148
|
+
if (flags.hidden && flags.hiddenReason) {
|
|
149
|
+
parts.push(`hidden:${flags.hiddenReason}`)
|
|
150
|
+
} else if (flags.hidden) {
|
|
151
|
+
parts.push('hidden')
|
|
152
|
+
}
|
|
153
|
+
if (flags.offScreen && !flags.hidden) parts.push('offscreen')
|
|
154
|
+
if (flags.customElement) parts.push('custom')
|
|
155
|
+
if (flags.hasAria) parts.push('aria')
|
|
156
|
+
|
|
157
|
+
return parts.join(' ')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Truncate a string with ellipsis */
|
|
161
|
+
function truncate(str, max) {
|
|
162
|
+
if (!str) return ''
|
|
163
|
+
if (str.length <= max) return str
|
|
164
|
+
return str.slice(0, max - 1) + '…'
|
|
165
|
+
}
|
package/bin/hj.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hj - Short alias for haltija CLI subcommands
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* hj tree # DOM tree
|
|
7
|
+
* hj click @42 # Click by ref
|
|
8
|
+
* hj type @10 hello # Type text
|
|
9
|
+
* hj eval 1+1 # Eval JS
|
|
10
|
+
* hj status # Server status
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { runSubcommand, isSubcommand, getSuggestion, listSubcommands } from './cli-subcommand.mjs'
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2)
|
|
16
|
+
|
|
17
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
18
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
19
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
20
|
+
console.log(`
|
|
21
|
+
${bold('hj')} - Haltija command-line interface
|
|
22
|
+
|
|
23
|
+
Usage: hj <command> [args...]
|
|
24
|
+
${listSubcommands()}
|
|
25
|
+
Run ${dim('hj --help')} for this help.
|
|
26
|
+
Run ${dim('haltija --help')} for server/app options.
|
|
27
|
+
`)
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse --port option
|
|
32
|
+
let port = process.env.DEV_CHANNEL_PORT || '8700'
|
|
33
|
+
const portIdx = args.indexOf('--port')
|
|
34
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
35
|
+
port = args[portIdx + 1]
|
|
36
|
+
args.splice(portIdx, 2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const subcommand = args[0]
|
|
40
|
+
const subArgs = args.slice(1).filter(a => a !== '--window' || true) // keep all args
|
|
41
|
+
|
|
42
|
+
if (!isSubcommand(subcommand)) {
|
|
43
|
+
const suggestion = getSuggestion(subcommand)
|
|
44
|
+
if (suggestion === '--help') {
|
|
45
|
+
console.log(listSubcommands())
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let msg = `Unknown command: '${subcommand}'`
|
|
50
|
+
if (suggestion) {
|
|
51
|
+
msg += ` — did you mean '${suggestion}'?`
|
|
52
|
+
}
|
|
53
|
+
console.error(msg)
|
|
54
|
+
console.error(`\nExamples: hj tree, hj navigate <url>, hj click @42`)
|
|
55
|
+
console.error(`Run 'hj' for docs.`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
} else {
|
|
58
|
+
runSubcommand(subcommand, subArgs, port)
|
|
59
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* haltija-mcp-setup - Configure Claude Code to use Haltija
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx haltija-mcp-setup # Create .mcp.json in current directory
|
|
7
|
+
* npx haltija-mcp-setup --global # Add to user's Claude Code config
|
|
8
|
+
* npx haltija-mcp-setup --check # Check if Haltija MCP is configured
|
|
9
|
+
* npx haltija-mcp-setup --help # Show help
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
|
|
13
|
+
import { homedir } from 'os'
|
|
14
|
+
import { join, dirname } from 'path'
|
|
15
|
+
import { fileURLToPath } from 'url'
|
|
16
|
+
import { execSync, spawnSync } from 'child_process'
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const args = process.argv.slice(2)
|
|
20
|
+
|
|
21
|
+
// Colors for terminal output
|
|
22
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
23
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
24
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`
|
|
25
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
26
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
27
|
+
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(`
|
|
30
|
+
${bold('haltija-mcp-setup')} - Configure Claude Code to use Haltija
|
|
31
|
+
|
|
32
|
+
${bold('Usage:')}
|
|
33
|
+
npx haltija-mcp-setup Create .mcp.json in current directory (recommended)
|
|
34
|
+
npx haltija-mcp-setup --global Add to user's global Claude Code config
|
|
35
|
+
npx haltija-mcp-setup --check Check current MCP configuration status
|
|
36
|
+
npx haltija-mcp-setup --remove Remove Haltija from MCP configuration
|
|
37
|
+
npx haltija-mcp-setup --help Show this help
|
|
38
|
+
|
|
39
|
+
${bold('What this does:')}
|
|
40
|
+
Configures Claude Code to connect to the Haltija server, giving Claude
|
|
41
|
+
native tools for browser control: click, type, query DOM, take screenshots, etc.
|
|
42
|
+
|
|
43
|
+
${bold('Prerequisites:')}
|
|
44
|
+
1. Haltija server running: ${dim('bunx haltija')}
|
|
45
|
+
2. Claude Code installed
|
|
46
|
+
|
|
47
|
+
${bold('After setup:')}
|
|
48
|
+
Restart Claude Code to load the new MCP server.
|
|
49
|
+
`)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find the MCP server path
|
|
54
|
+
function findMcpServerPath() {
|
|
55
|
+
// Check if we're in the haltija package
|
|
56
|
+
const localPath = join(__dirname, '../apps/mcp/build/index.js')
|
|
57
|
+
if (existsSync(localPath)) {
|
|
58
|
+
return localPath
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for globally installed package
|
|
62
|
+
try {
|
|
63
|
+
const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' })
|
|
64
|
+
if (result.status === 0) {
|
|
65
|
+
const globalPath = join(result.stdout.trim(), 'tosijs-dev/apps/mcp/build/index.js')
|
|
66
|
+
if (existsSync(globalPath)) {
|
|
67
|
+
return globalPath
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
// Check node_modules in current directory
|
|
73
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules/tosijs-dev/apps/mcp/build/index.js')
|
|
74
|
+
if (existsSync(nodeModulesPath)) {
|
|
75
|
+
return nodeModulesPath
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if Claude Code is installed
|
|
82
|
+
function isClaudeCodeInstalled() {
|
|
83
|
+
const claudeJsonPath = join(homedir(), '.claude.json')
|
|
84
|
+
return existsSync(claudeJsonPath)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Get project-level .mcp.json path
|
|
88
|
+
function getProjectMcpPath() {
|
|
89
|
+
return join(process.cwd(), '.mcp.json')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get user-level claude.json path
|
|
93
|
+
function getClaudeJsonPath() {
|
|
94
|
+
return join(homedir(), '.claude.json')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check current configuration
|
|
98
|
+
function checkConfig() {
|
|
99
|
+
console.log(bold('\nHaltija MCP Configuration Status\n'))
|
|
100
|
+
|
|
101
|
+
// Check Claude Code installation
|
|
102
|
+
if (isClaudeCodeInstalled()) {
|
|
103
|
+
console.log(green('✓') + ' Claude Code is installed')
|
|
104
|
+
} else {
|
|
105
|
+
console.log(red('✗') + ' Claude Code not detected (~/.claude.json missing)')
|
|
106
|
+
console.log(dim(' Install Claude Code from: https://claude.ai/code'))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check project-level config
|
|
110
|
+
const projectMcpPath = getProjectMcpPath()
|
|
111
|
+
if (existsSync(projectMcpPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const config = JSON.parse(readFileSync(projectMcpPath, 'utf8'))
|
|
114
|
+
if (config.mcpServers?.haltija) {
|
|
115
|
+
console.log(green('✓') + ` Project .mcp.json has Haltija configured`)
|
|
116
|
+
console.log(dim(` Path: ${projectMcpPath}`))
|
|
117
|
+
} else {
|
|
118
|
+
console.log(yellow('○') + ' Project .mcp.json exists but Haltija not configured')
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
console.log(red('✗') + ' Project .mcp.json exists but is invalid JSON')
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
console.log(dim('○') + ' No project-level .mcp.json')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check MCP server build
|
|
128
|
+
const mcpPath = findMcpServerPath()
|
|
129
|
+
if (mcpPath) {
|
|
130
|
+
console.log(green('✓') + ' MCP server found')
|
|
131
|
+
console.log(dim(` Path: ${mcpPath}`))
|
|
132
|
+
} else {
|
|
133
|
+
console.log(red('✗') + ' MCP server not found')
|
|
134
|
+
console.log(dim(' Run from haltija directory or install globally'))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if Haltija server is running
|
|
138
|
+
try {
|
|
139
|
+
execSync('curl -s http://localhost:8700/status', { encoding: 'utf8', timeout: 2000 })
|
|
140
|
+
console.log(green('✓') + ' Haltija server is running on port 8700')
|
|
141
|
+
} catch {
|
|
142
|
+
console.log(yellow('○') + ' Haltija server not running')
|
|
143
|
+
console.log(dim(' Start with: bunx haltija'))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create or update .mcp.json
|
|
150
|
+
function setupProjectConfig() {
|
|
151
|
+
const mcpPath = findMcpServerPath()
|
|
152
|
+
if (!mcpPath) {
|
|
153
|
+
console.log(red('Error:') + ' Could not find Haltija MCP server.')
|
|
154
|
+
console.log('Make sure you run this from the haltija directory or have it installed.')
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const projectMcpPath = getProjectMcpPath()
|
|
159
|
+
let config = { mcpServers: {} }
|
|
160
|
+
|
|
161
|
+
// Read existing config if present
|
|
162
|
+
if (existsSync(projectMcpPath)) {
|
|
163
|
+
try {
|
|
164
|
+
config = JSON.parse(readFileSync(projectMcpPath, 'utf8'))
|
|
165
|
+
if (!config.mcpServers) {
|
|
166
|
+
config.mcpServers = {}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
console.log(yellow('Warning:') + ' Existing .mcp.json is invalid, creating new one')
|
|
170
|
+
config = { mcpServers: {} }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if already configured
|
|
175
|
+
if (config.mcpServers.haltija) {
|
|
176
|
+
console.log(yellow('Haltija is already configured in .mcp.json'))
|
|
177
|
+
console.log(dim('Use --remove to remove it, or delete .mcp.json manually'))
|
|
178
|
+
process.exit(0)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add Haltija configuration
|
|
182
|
+
config.mcpServers.haltija = {
|
|
183
|
+
command: 'node',
|
|
184
|
+
args: [mcpPath]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
writeFileSync(projectMcpPath, JSON.stringify(config, null, 2) + '\n')
|
|
188
|
+
|
|
189
|
+
console.log(green('✓') + ' Created .mcp.json with Haltija configuration')
|
|
190
|
+
console.log(dim(` Path: ${projectMcpPath}`))
|
|
191
|
+
console.log('')
|
|
192
|
+
console.log(bold('Next steps:'))
|
|
193
|
+
console.log(' 1. Restart Claude Code to load the MCP server')
|
|
194
|
+
console.log(' 2. Start Haltija if not running: ' + dim('bunx haltija'))
|
|
195
|
+
console.log(' 3. Haltija tools (click, type, query, etc.) will be available in Claude')
|
|
196
|
+
console.log('')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add to global Claude Code config
|
|
200
|
+
function setupGlobalConfig() {
|
|
201
|
+
const mcpPath = findMcpServerPath()
|
|
202
|
+
if (!mcpPath) {
|
|
203
|
+
console.log(red('Error:') + ' Could not find Haltija MCP server.')
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Try using claude CLI first
|
|
208
|
+
try {
|
|
209
|
+
execSync(`claude mcp add haltija node "${mcpPath}" --scope user`, {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
stdio: 'inherit'
|
|
212
|
+
})
|
|
213
|
+
console.log('')
|
|
214
|
+
console.log(green('✓') + ' Added Haltija to global Claude Code configuration')
|
|
215
|
+
console.log('')
|
|
216
|
+
console.log(bold('Next steps:'))
|
|
217
|
+
console.log(' 1. Restart Claude Code to load the MCP server')
|
|
218
|
+
console.log(' 2. Start Haltija if not running: ' + dim('bunx haltija'))
|
|
219
|
+
console.log('')
|
|
220
|
+
return
|
|
221
|
+
} catch {
|
|
222
|
+
// Fall back to manual config
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Manual fallback
|
|
226
|
+
const claudeJsonPath = getClaudeJsonPath()
|
|
227
|
+
if (!existsSync(claudeJsonPath)) {
|
|
228
|
+
console.log(red('Error:') + ' Claude Code config not found at ~/.claude.json')
|
|
229
|
+
console.log('Make sure Claude Code is installed.')
|
|
230
|
+
process.exit(1)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(yellow('Note:') + ' Could not use claude CLI, please add manually:')
|
|
234
|
+
console.log('')
|
|
235
|
+
console.log('Run: ' + bold(`claude mcp add haltija node "${mcpPath}" --scope user`))
|
|
236
|
+
console.log('')
|
|
237
|
+
console.log('Or add to ~/.claude.json under your project\'s mcpServers:')
|
|
238
|
+
console.log(dim(JSON.stringify({ haltija: { command: 'node', args: [mcpPath] } }, null, 2)))
|
|
239
|
+
console.log('')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Remove Haltija configuration
|
|
243
|
+
function removeConfig() {
|
|
244
|
+
const projectMcpPath = getProjectMcpPath()
|
|
245
|
+
|
|
246
|
+
if (!existsSync(projectMcpPath)) {
|
|
247
|
+
console.log('No .mcp.json found in current directory')
|
|
248
|
+
process.exit(0)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const config = JSON.parse(readFileSync(projectMcpPath, 'utf8'))
|
|
253
|
+
if (config.mcpServers?.haltija) {
|
|
254
|
+
delete config.mcpServers.haltija
|
|
255
|
+
|
|
256
|
+
// Remove file if no other servers, otherwise update
|
|
257
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
258
|
+
unlinkSync(projectMcpPath)
|
|
259
|
+
console.log(green('✓') + ' Removed .mcp.json (no other MCP servers configured)')
|
|
260
|
+
} else {
|
|
261
|
+
writeFileSync(projectMcpPath, JSON.stringify(config, null, 2) + '\n')
|
|
262
|
+
console.log(green('✓') + ' Removed Haltija from .mcp.json')
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
console.log('Haltija is not configured in .mcp.json')
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.log(red('Error:') + ' Failed to parse .mcp.json')
|
|
269
|
+
process.exit(1)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Main
|
|
274
|
+
if (args.includes('--check')) {
|
|
275
|
+
checkConfig()
|
|
276
|
+
} else if (args.includes('--global')) {
|
|
277
|
+
setupGlobalConfig()
|
|
278
|
+
} else if (args.includes('--remove')) {
|
|
279
|
+
removeConfig()
|
|
280
|
+
} else {
|
|
281
|
+
// Default: project-level setup
|
|
282
|
+
if (!isClaudeCodeInstalled()) {
|
|
283
|
+
console.log(yellow('Warning:') + ' Claude Code not detected (~/.claude.json missing)')
|
|
284
|
+
console.log('This setup configures Haltija for Claude Code.')
|
|
285
|
+
console.log('')
|
|
286
|
+
}
|
|
287
|
+
setupProjectConfig()
|
|
288
|
+
}
|