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.
@@ -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
+ }
package/bin/server.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Dev Channel Server CLI
4
+ *
5
+ * Run with: bun run packages/tosijs-dev/bin/server.ts
6
+ * Or after install: tosijs-dev
7
+ */
8
+
9
+ import '../src/server'