haltija 1.13.0-beta.5 → 1.13.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/CHANGELOG.md +79 -0
- package/README.md +5 -5
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +1 -1
- package/bin/launcher-args.mjs +35 -0
- package/bin/mcp-locate.mjs +17 -0
- package/bin/mcp-setup.mjs +26 -16
- package/bin/server-list.mjs +6 -1
- package/bin/tosijs-dev.mjs +21 -3
- package/bin/version.mjs +1 -1
- package/dist/api-schema.d.ts +3 -3
- package/dist/client-auth.d.ts +11 -0
- package/dist/client.js +35 -24
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +1 -1
- package/dist/component.js +1 -1
- package/dist/drag.d.ts +4 -3
- package/dist/floorplan-records.d.ts +2 -0
- package/dist/hj.js +6 -3
- package/dist/index.js +117 -50
- package/dist/mcp.js +5 -2
- package/dist/pointer-kit.d.ts +12 -0
- package/dist/remote-gate.d.ts +10 -0
- package/dist/server.js +81 -25
- package/dist/test-in-browser.d.ts +1 -1
- package/dist/test.js +68 -55
- package/dist/transports.d.ts +13 -0
- package/dist/version.d.ts +1 -1
- package/docs/CI-INTEGRATION.md +9 -0
- package/docs/README.md +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.0 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
The 1.13 betas, released. Read **Security** and **Upgrading from 1.12** before moving off 1.12.x.
|
|
6
|
+
The beta sections below have the detail and the reasoning; this section is the list. The changes
|
|
7
|
+
made after beta.5 are at the end.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **Through 1.12.9, anything on your network could drive your browser.** The server listens on
|
|
12
|
+
every interface and, without `--token`, answered any machine, `/eval` included. Affected: every
|
|
13
|
+
release through **1.12.9** (the previous `latest`) and **1.13.0-beta.1 to beta.4**. In 1.13.0 a
|
|
14
|
+
connection from another machine is refused (403) unless the server has a token (virta #815,
|
|
15
|
+
GitHub #44; first shipped in beta.5). If you stay on 1.12.x, start the server with `--token
|
|
16
|
+
<secret>` on any network you don't trust.
|
|
17
|
+
- **Pages on this machine can still call the API, and that is by design.** beta.1 listed this as a
|
|
18
|
+
known issue with "an origin gate planned before 1.13.0". That gate is not coming: the widget is
|
|
19
|
+
injected into arbitrary pages, so the REST surface must answer any origin, and an origin check
|
|
20
|
+
would break exactly the pages haltija exists for. `--token` closes it, as it closes a tunnel or
|
|
21
|
+
reverse proxy running on this machine (which connects from localhost). The README's Security
|
|
22
|
+
section says what is and isn't covered.
|
|
23
|
+
|
|
24
|
+
### Upgrading from 1.12
|
|
25
|
+
|
|
26
|
+
Each of these changes something that worked before. Before → after:
|
|
27
|
+
|
|
28
|
+
- **Other machines need a token.** Phones, headsets, other computers, and browsers in a container,
|
|
29
|
+
VM or WSL2 that reach haltija through a forwarded port: they connected freely, and now get a 403
|
|
30
|
+
until the server has `--token <secret>` and the client sends it (beta.5).
|
|
31
|
+
- **Both transports by default.** `DEV_CHANNEL_MODE` was `http`; it is `both`, so HTTPS 8701 comes
|
|
32
|
+
up too. `--http` opts out, and says what that costs other projects (beta.1).
|
|
33
|
+
- **The certificate lives in `~/.haltija/certs`.** It was per-install; an existing one is copied
|
|
34
|
+
forward, so trust you already granted survives (beta.1).
|
|
35
|
+
- **The desktop app's internal server moved from 8701 to 8710.** `HALTIJA_PORT=8701 hj tree`
|
|
36
|
+
(inspecting the app's own UI) is now `HALTIJA_PORT=8710 hj tree` (beta.1).
|
|
37
|
+
- **Only the server on 8700 takes HTTPS 8701.** A `--port` / `--name` server's HTTPS is now
|
|
38
|
+
ephemeral, shown in its banner and `/status`, unless you pass `--https-port` (beta.1).
|
|
39
|
+
- **`server` from `haltija/server` is the HTTP listener**, and `server`, `httpServer` and
|
|
40
|
+
`httpsServer` are typed `ListeningServer` (`port`, `hostname`, `url`, `stop()`) instead of
|
|
41
|
+
Bun's server type. Code that used other Bun server methods on them needs a cast (beta.1).
|
|
42
|
+
- **Navigate and refresh wait for the page's widget.** They returned at once; now they block until
|
|
43
|
+
the widget reconnects, up to `--timeout` (default 10 s). A page that doesn't load the widget
|
|
44
|
+
itself (a bookmarklet tab) waits that long every time: pass `--no-wait` there. `navigate()` and
|
|
45
|
+
`refresh()` in `DevChannelClient` / `haltija/test` now return `{ reconnected, warning? }` (beta.2).
|
|
46
|
+
- **The widget writes `window.name` while navigating**, and restores the page's own value after.
|
|
47
|
+
An app that keeps state in `window.name` can see the marker briefly (beta.2).
|
|
48
|
+
- **`press` acts at the focused element.** In `haltija/test` and `DevChannelClient.press` it was one
|
|
49
|
+
`keydown` at `body`; now it is the full key sequence at the focused element, like `hj key`
|
|
50
|
+
(beta.4).
|
|
51
|
+
- **Schematics are drawn by tosijs-floorplan.** Same response fields; the picture looks different
|
|
52
|
+
(beta.5).
|
|
53
|
+
|
|
54
|
+
### Fixed after beta.5
|
|
55
|
+
|
|
56
|
+
From the 1.13.0 release review (`reviews/1.13.0-*.md`):
|
|
57
|
+
|
|
58
|
+
- **A refused connection from another machine is now logged on the server**, once per address, with
|
|
59
|
+
the `--token` remedy. A browser cannot read why a WebSocket handshake failed, so a widget in a
|
|
60
|
+
container or VM retried forever while the server said nothing.
|
|
61
|
+
- **`DevChannelClient` and `haltija/test` send `HALTIJA_TOKEN`.** `hj` and the MCP bridge always
|
|
62
|
+
did; these two never did, so a test suite could not reach a `--token` server at all, which since
|
|
63
|
+
beta.5 is the setup for any runner on another machine.
|
|
64
|
+
- **`haltija --token=<secret>` sets a token.** The launcher read only `--token <secret>`, so the `=`
|
|
65
|
+
form, like a misspelled flag, started a server with no token and no complaint. `--flag=value` now
|
|
66
|
+
works for every launcher flag that takes a value, an unknown flag is reported, and the banner has
|
|
67
|
+
an `AUTH:` line saying whether a token is in force.
|
|
68
|
+
- **`/drag`, `hj drag` and the `drag` step move pointer-driven controls.** They sent bare mouse
|
|
69
|
+
events (some of them `CustomEvent`s with no coordinates), so a slider, resize handle or orbit
|
|
70
|
+
camera that listens for pointer events didn't move while the drag reported success. They now use
|
|
71
|
+
the same pointer-and-mouse sequence as `dragFrom`, with `buttons` set while pressed.
|
|
72
|
+
- **MCP setup replaces a stale entry.** Before 1.13 setup wrote `node …/apps/mcp/build/index.js`,
|
|
73
|
+
which no longer exists. `--check` reported such an entry as configured, and re-running setup said
|
|
74
|
+
"already configured". Both now call it stale, and setup rewrites it. The help named
|
|
75
|
+
`npx haltija-mcp-setup`, a package that does not exist; it is `npx -p haltija haltija-mcp-setup`.
|
|
76
|
+
- **The `test:integration` lane runs its tests.** It needed a server it was never given, so every
|
|
77
|
+
1.13 beta attested "0 pass, 11 skip" as a pass. It now starts its own private headless instance,
|
|
78
|
+
and a lane that only skipped fails.
|
|
79
|
+
- Docs: the README no longer shows a demo image that doesn't exist, and links the API reference and
|
|
80
|
+
the browser-test guide (`docs/TEST-IN-BROWSER.md`).
|
|
81
|
+
|
|
3
82
|
## 1.13.0-beta.5 (2026-10-18)
|
|
4
83
|
|
|
5
84
|
### Changed
|
package/README.md
CHANGED
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
Unlike screenshot-based tools, Haltija works with the actual DOM. Unlike Playwright, it connects to your real browser — already logged in, same cookies, same bug you're looking at.
|
|
10
10
|
|
|
11
|
-
<!-- TODO: Replace with actual GIF showing: 1) bunx haltija launches app, 2) agent queries tree, 3) agent clicks something, 4) highlight shows result -->
|
|
12
|
-

|
|
13
|
-
|
|
14
11
|
> *Haltija* (Finnish): a guardian spirit that watches over a place. In this case, your DOM.
|
|
15
12
|
|
|
16
13
|
---
|
|
@@ -337,7 +334,9 @@ app also exists but isn't required for either.
|
|
|
337
334
|
8701 by default), but without a token it refuses every connection that doesn't come from this
|
|
338
335
|
machine. To use haltija from another device (a phone, a headset, a second computer), start it
|
|
339
336
|
with `--token <secret>` (or `HALTIJA_TOKEN`) and send the same value as `X-Haltija-Token`,
|
|
340
|
-
`?token=`, or the widget's `token` option.
|
|
337
|
+
`?token=`, or the widget's `token` option. A browser in a container, VM or WSL2 that reaches
|
|
338
|
+
haltija through a forwarded port also counts as another machine. The server's terminal names
|
|
339
|
+
each refused address once.
|
|
341
340
|
- **On this machine, any page you visit can call it.** The widget is injected into arbitrary
|
|
342
341
|
pages, so the REST API answers any origin, `/eval` in your connected tabs included. That is by
|
|
343
342
|
design for a local dev tool, and `--token` closes it too. A tunnel or reverse proxy on this
|
|
@@ -411,10 +410,11 @@ hj api # Full API reference (markdown)
|
|
|
411
410
|
hj --help # CLI subcommand reference
|
|
412
411
|
```
|
|
413
412
|
|
|
414
|
-
- [Full API Reference](
|
|
413
|
+
- [Full API Reference](https://github.com/tonioloewald/haltija/blob/main/API.md) (auto-generated from schema)
|
|
415
414
|
- [Agent Prompt](docs/agent-prompt.md) — Copy-paste prompt for any AI agent
|
|
416
415
|
- [Recipes](docs/recipes.md) — Common workflows
|
|
417
416
|
- [CI Integration](docs/CI-INTEGRATION.md) — E2E testing with Haltija in GitHub Actions
|
|
417
|
+
- [Browser tests from your test runner](docs/TEST-IN-BROWSER.md) — `createBrowserPage` from `haltija/test`: assertions on the host, probes in a real browser
|
|
418
418
|
- [Roadmap](docs/ROADMAP.md)
|
|
419
419
|
|
|
420
420
|
---
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launcher's own flags, and `--flag=value` support for the ones that take a value.
|
|
3
|
+
*
|
|
4
|
+
* The launcher read flags with `args.indexOf('--token')`, so `--token=secret` and a typo like
|
|
5
|
+
* `--tokn secret` both started a server with NO token, silently (1.13.0 review). With the #815 gate
|
|
6
|
+
* that mostly fails closed for other machines, but a tunnel on this machine connects from loopback,
|
|
7
|
+
* and the README sends tunnel users to `--token`. So: accept the `=` form, and say when a flag is not
|
|
8
|
+
* one the launcher knows.
|
|
9
|
+
*/
|
|
10
|
+
export const VALUE_FLAGS = [
|
|
11
|
+
'--port', '--https-port', '--token', '--name', '--port-file',
|
|
12
|
+
'--headless-url', '--snapshots-dir', '--docs-dir',
|
|
13
|
+
]
|
|
14
|
+
export const BOOLEAN_FLAGS = [
|
|
15
|
+
'--app', '--server', '--headless', '--ci', '--http', '--https', '--both', '--force', '-f',
|
|
16
|
+
'--wait-ready', '--private', '--setup-mcp', '--setup-mcp-check', '--setup-mcp-remove',
|
|
17
|
+
'--help', '-h',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/** `--token=x` becomes `--token x`, for the value flags only. Returns the unknown flags it saw. */
|
|
21
|
+
export function normalizeLauncherArgs(args) {
|
|
22
|
+
const out = []
|
|
23
|
+
const unknown = []
|
|
24
|
+
for (const a of args) {
|
|
25
|
+
const eq = a.indexOf('=')
|
|
26
|
+
const flag = a.startsWith('-') && eq > 0 ? a.slice(0, eq) : a
|
|
27
|
+
if (flag !== a && VALUE_FLAGS.includes(flag)) {
|
|
28
|
+
out.push(flag, a.slice(eq + 1))
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if (a.startsWith('-') && !VALUE_FLAGS.includes(a) && !BOOLEAN_FLAGS.includes(a)) unknown.push(a)
|
|
32
|
+
out.push(a)
|
|
33
|
+
}
|
|
34
|
+
return { args: out, unknown }
|
|
35
|
+
}
|
package/bin/mcp-locate.mjs
CHANGED
|
@@ -31,3 +31,20 @@ export function mcpServerCommand(serverPath, root = PACKAGE_ROOT) {
|
|
|
31
31
|
}
|
|
32
32
|
return { command: 'node', args: [serverPath] }
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Why an existing `haltija` MCP entry can no longer start, or null when it looks runnable.
|
|
37
|
+
*
|
|
38
|
+
* Before 1.13.0 setup wrote `node …/apps/mcp/build/index.js`, a path the package no longer has, and
|
|
39
|
+
* `--check` reported such an entry as configured while re-running setup said "already configured",
|
|
40
|
+
* so nothing short of hand-editing the config could fix it. An entry that names a server file which
|
|
41
|
+
* is not there is stale, whatever else it says; one that runs a command (`npx …`) is left alone.
|
|
42
|
+
*/
|
|
43
|
+
export function staleMcpEntry(entry) {
|
|
44
|
+
const args = Array.isArray(entry?.args) ? entry.args : []
|
|
45
|
+
const script = args.find((a) => typeof a === 'string' && /\.(m?js|ts)$/.test(a))
|
|
46
|
+
if (!script) return null
|
|
47
|
+
if (/[\\/]apps[\\/]mcp[\\/]build[\\/]/.test(script)) return `it runs ${script}, the pre-1.13 server location`
|
|
48
|
+
if (!existsSync(script)) return `it runs ${script}, which does not exist`
|
|
49
|
+
return null
|
|
50
|
+
}
|
package/bin/mcp-setup.mjs
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
* haltija-mcp-setup - Configure Claude Code to use Haltija
|
|
4
4
|
*
|
|
5
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
|
|
6
|
+
* npx -p haltija haltija-mcp-setup # Create .mcp.json in current directory
|
|
7
|
+
* npx -p haltija haltija-mcp-setup --global # Add to user's Claude Code config
|
|
8
|
+
* npx -p haltija haltija-mcp-setup --check # Check if Haltija MCP is configured
|
|
9
|
+
* npx -p haltija haltija-mcp-setup --help # Show help
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
|
|
13
13
|
import { homedir } from 'os'
|
|
14
14
|
import { join, dirname } from 'path'
|
|
15
15
|
import { fileURLToPath } from 'url'
|
|
16
|
-
import {
|
|
17
|
-
import { mcpServerPath, mcpServerCommand } from './mcp-locate.mjs'
|
|
16
|
+
import { execFileSync } from 'child_process'
|
|
17
|
+
import { mcpServerPath, mcpServerCommand, staleMcpEntry } from './mcp-locate.mjs'
|
|
18
18
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
20
|
const args = process.argv.slice(2)
|
|
@@ -31,11 +31,11 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
31
31
|
${bold('haltija-mcp-setup')} - Configure Claude Code to use Haltija
|
|
32
32
|
|
|
33
33
|
${bold('Usage:')}
|
|
34
|
-
npx haltija-mcp-setup Create .mcp.json in current directory (recommended)
|
|
35
|
-
npx haltija-mcp-setup --global Add to user's global Claude Code config
|
|
36
|
-
npx haltija-mcp-setup --check Check current MCP configuration status
|
|
37
|
-
npx haltija-mcp-setup --remove Remove Haltija from MCP configuration
|
|
38
|
-
npx haltija-mcp-setup --help Show this help
|
|
34
|
+
npx -p haltija haltija-mcp-setup Create .mcp.json in current directory (recommended)
|
|
35
|
+
npx -p haltija haltija-mcp-setup --global Add to user's global Claude Code config
|
|
36
|
+
npx -p haltija haltija-mcp-setup --check Check current MCP configuration status
|
|
37
|
+
npx -p haltija haltija-mcp-setup --remove Remove Haltija from MCP configuration
|
|
38
|
+
npx -p haltija haltija-mcp-setup --help Show this help
|
|
39
39
|
|
|
40
40
|
${bold('What this does:')}
|
|
41
41
|
Configures Claude Code to connect to the Haltija server, giving Claude
|
|
@@ -84,7 +84,11 @@ function checkConfig() {
|
|
|
84
84
|
if (existsSync(projectMcpPath)) {
|
|
85
85
|
try {
|
|
86
86
|
const config = JSON.parse(readFileSync(projectMcpPath, 'utf8'))
|
|
87
|
-
|
|
87
|
+
const stale = config.mcpServers?.haltija && staleMcpEntry(config.mcpServers.haltija)
|
|
88
|
+
if (stale) {
|
|
89
|
+
console.log(red('✗') + ` Project .mcp.json has a stale Haltija entry: ${stale}`)
|
|
90
|
+
console.log(dim(' Run setup again to replace it: npx -p haltija haltija-mcp-setup'))
|
|
91
|
+
} else if (config.mcpServers?.haltija) {
|
|
88
92
|
console.log(green('✓') + ` Project .mcp.json has Haltija configured`)
|
|
89
93
|
console.log(dim(` Path: ${projectMcpPath}`))
|
|
90
94
|
} else {
|
|
@@ -108,9 +112,10 @@ function checkConfig() {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
// Check if Haltija server is running
|
|
115
|
+
const port = process.env.HALTIJA_PORT || '8700'
|
|
111
116
|
try {
|
|
112
|
-
|
|
113
|
-
console.log(green('✓') +
|
|
117
|
+
execFileSync('curl', ['-sf', `http://localhost:${port}/status`], { encoding: 'utf8', timeout: 2000 })
|
|
118
|
+
console.log(green('✓') + ` Haltija server is running on port ${port}`)
|
|
114
119
|
} catch {
|
|
115
120
|
console.log(yellow('○') + ' Haltija server not running')
|
|
116
121
|
console.log(dim(' Start with: bunx haltija'))
|
|
@@ -145,7 +150,10 @@ function setupProjectConfig() {
|
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
// Check if already configured
|
|
148
|
-
|
|
153
|
+
const stale = config.mcpServers.haltija && staleMcpEntry(config.mcpServers.haltija)
|
|
154
|
+
if (stale) {
|
|
155
|
+
console.log(yellow('Replacing a stale Haltija entry in .mcp.json:') + ` ${stale}`)
|
|
156
|
+
} else if (config.mcpServers.haltija) {
|
|
149
157
|
console.log(yellow('Haltija is already configured in .mcp.json'))
|
|
150
158
|
console.log(dim('Use --remove to remove it, or delete .mcp.json manually'))
|
|
151
159
|
process.exit(0)
|
|
@@ -178,7 +186,9 @@ function setupGlobalConfig() {
|
|
|
178
186
|
const addCommand = `claude mcp add --scope user haltija -- ${[command, ...commandArgs].map((a) => JSON.stringify(a)).join(' ')}`
|
|
179
187
|
// Try using claude CLI first
|
|
180
188
|
try {
|
|
181
|
-
|
|
189
|
+
// An argv, not a shell string: the install path is in the args, and a shell would run any
|
|
190
|
+
// `$()` or backtick in it.
|
|
191
|
+
execFileSync('claude', ['mcp', 'add', '--scope', 'user', 'haltija', '--', command, ...commandArgs], {
|
|
182
192
|
encoding: 'utf8',
|
|
183
193
|
stdio: 'inherit'
|
|
184
194
|
})
|
package/bin/server-list.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** ⚠️ AUTO-GENERATED FROM src/server-list.ts — DO NOT EDIT. Run: bun run build */
|
|
2
|
+
// src/ports.ts
|
|
3
|
+
var DEFAULT_HTTP_PORT = 8700;
|
|
4
|
+
var DEFAULT_HTTPS_PORT = 8701;
|
|
5
|
+
var PUBLIC_PORTS = [DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT];
|
|
6
|
+
|
|
2
7
|
// src/server-list.ts
|
|
3
|
-
function collectCandidates(instances, resolvedPort, defaults = [
|
|
8
|
+
function collectCandidates(instances, resolvedPort, defaults = [...PUBLIC_PORTS]) {
|
|
4
9
|
const byPort = new Map;
|
|
5
10
|
for (const e of instances) {
|
|
6
11
|
byPort.set(String(e.port), { port: String(e.port), name: e.name, cwd: e.cwd ?? null });
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { homedir, platform, tmpdir } from 'os'
|
|
|
18
18
|
import { fileURLToPath } from 'url'
|
|
19
19
|
import { dirname, join } from 'path'
|
|
20
20
|
import { stalePrivateEntries, pidIsAlive } from './private-state.mjs'
|
|
21
|
-
import { mcpServerPath, mcpServerCommand } from './mcp-locate.mjs'
|
|
21
|
+
import { mcpServerPath, mcpServerCommand, staleMcpEntry } from './mcp-locate.mjs'
|
|
22
22
|
|
|
23
23
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
24
24
|
const serverPath = join(__dirname, '../dist/server.js')
|
|
@@ -171,7 +171,11 @@ function checkMcpConfig() {
|
|
|
171
171
|
if (existsSync(configPath)) {
|
|
172
172
|
try {
|
|
173
173
|
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
174
|
-
|
|
174
|
+
const stale = config.mcpServers?.haltija && staleMcpEntry(config.mcpServers.haltija)
|
|
175
|
+
if (stale) {
|
|
176
|
+
console.log(red('✗') + ` Haltija's Claude Desktop entry is stale: ${stale}`)
|
|
177
|
+
console.log(dim(` Run: haltija --setup-mcp (it replaces the entry)`))
|
|
178
|
+
} else if (config.mcpServers?.haltija) {
|
|
175
179
|
console.log(green('✓') + ' Haltija is configured in Claude Desktop')
|
|
176
180
|
console.log(dim(` Command: ${config.mcpServers.haltija.command} ${config.mcpServers.haltija.args?.join(' ') || ''}`))
|
|
177
181
|
} else {
|
|
@@ -248,7 +252,10 @@ function setupMcp() {
|
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
// Check if already configured
|
|
251
|
-
|
|
255
|
+
const stale = config.mcpServers.haltija && staleMcpEntry(config.mcpServers.haltija)
|
|
256
|
+
if (stale) {
|
|
257
|
+
console.log(yellow('Replacing a stale Haltija entry:') + ` ${stale}`)
|
|
258
|
+
} else if (config.mcpServers.haltija) {
|
|
252
259
|
console.log(green('✓') + ' Haltija is already configured in Claude Desktop')
|
|
253
260
|
console.log(dim(` Config: ${configPath}`))
|
|
254
261
|
console.log('')
|
|
@@ -310,6 +317,7 @@ function removeMcp() {
|
|
|
310
317
|
// ============================================
|
|
311
318
|
|
|
312
319
|
import { isSubcommand, runSubcommand } from './cli-subcommand.mjs'
|
|
320
|
+
import { normalizeLauncherArgs } from './launcher-args.mjs'
|
|
313
321
|
|
|
314
322
|
// Check if first positional arg is a subcommand (not a flag, not a port number)
|
|
315
323
|
const firstNonFlag = args.find(a => !a.startsWith('-'))
|
|
@@ -349,6 +357,16 @@ if (args.includes('--setup-mcp-remove')) {
|
|
|
349
357
|
process.exit(0)
|
|
350
358
|
}
|
|
351
359
|
|
|
360
|
+
// Launcher mode from here on (subcommands took their own flags above). `--flag=value` is split
|
|
361
|
+
// in place, so every `args.indexOf('--flag')` below sees it; an unknown flag is said out loud.
|
|
362
|
+
{
|
|
363
|
+
const { args: normalized, unknown } = normalizeLauncherArgs(args)
|
|
364
|
+
args.splice(0, args.length, ...normalized)
|
|
365
|
+
for (const flag of unknown) {
|
|
366
|
+
console.error(yellow(`[haltija] unknown option ${flag} is ignored (see haltija --help)`))
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
352
370
|
// Set up environment from args
|
|
353
371
|
const env = { ...process.env }
|
|
354
372
|
|
package/bin/version.mjs
CHANGED
package/dist/api-schema.d.ts
CHANGED
|
@@ -297,8 +297,8 @@ export declare const recordingStart: EndpointDef<{
|
|
|
297
297
|
export declare const recordingStop: EndpointDef<{}>;
|
|
298
298
|
export declare const recordingGenerate: EndpointDef<{
|
|
299
299
|
url?: string | undefined;
|
|
300
|
-
events?: any[] | undefined;
|
|
301
300
|
minDelay?: number | undefined;
|
|
301
|
+
events?: any[] | undefined;
|
|
302
302
|
name?: string | undefined;
|
|
303
303
|
since?: number | undefined;
|
|
304
304
|
description?: string | undefined;
|
|
@@ -654,8 +654,8 @@ export declare const endpoints: {
|
|
|
654
654
|
readonly recordingStop: EndpointDef<{}>;
|
|
655
655
|
readonly recordingGenerate: EndpointDef<{
|
|
656
656
|
url?: string | undefined;
|
|
657
|
-
events?: any[] | undefined;
|
|
658
657
|
minDelay?: number | undefined;
|
|
658
|
+
events?: any[] | undefined;
|
|
659
659
|
name?: string | undefined;
|
|
660
660
|
since?: number | undefined;
|
|
661
661
|
description?: string | undefined;
|
|
@@ -931,8 +931,8 @@ export declare const ALL_ENDPOINTS: (EndpointDef<{
|
|
|
931
931
|
name?: string | undefined;
|
|
932
932
|
}> | EndpointDef<{}> | EndpointDef<{
|
|
933
933
|
url?: string | undefined;
|
|
934
|
-
events?: any[] | undefined;
|
|
935
934
|
minDelay?: number | undefined;
|
|
935
|
+
events?: any[] | undefined;
|
|
936
936
|
name?: string | undefined;
|
|
937
937
|
since?: number | undefined;
|
|
938
938
|
description?: string | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetch`, plus `X-Haltija-Token` when `HALTIJA_TOKEN` is set: what `DevChannelClient` and
|
|
3
|
+
* `haltija/test` send every request with.
|
|
4
|
+
*
|
|
5
|
+
* `hj` and the MCP bridge have always sent the token; these two never did, so a test suite could
|
|
6
|
+
* not reach a server started with `--token` at all. Since 1.13.0 a caller on another machine (or in
|
|
7
|
+
* a container reaching the host through a forwarded port) needs that token (#815), so the gap went
|
|
8
|
+
* from rare to the documented setup (1.13.0 review). Imported as `fetch` so no call site changes,
|
|
9
|
+
* and a header the caller set explicitly wins.
|
|
10
|
+
*/
|
|
11
|
+
export declare function authedFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// src/client-auth.ts
|
|
3
|
+
function authedFetch(input, init = {}) {
|
|
4
|
+
const token = typeof process !== "undefined" ? process.env.HALTIJA_TOKEN : undefined;
|
|
5
|
+
if (!token)
|
|
6
|
+
return fetch(input, init);
|
|
7
|
+
const headers = new Headers(init.headers);
|
|
8
|
+
if (!headers.has("X-Haltija-Token"))
|
|
9
|
+
headers.set("X-Haltija-Token", token);
|
|
10
|
+
return fetch(input, { ...init, headers });
|
|
11
|
+
}
|
|
12
|
+
|
|
2
13
|
// src/client.ts
|
|
3
14
|
class DevChannelClient {
|
|
4
15
|
baseUrl;
|
|
@@ -6,11 +17,11 @@ class DevChannelClient {
|
|
|
6
17
|
this.baseUrl = serverUrl;
|
|
7
18
|
}
|
|
8
19
|
async status() {
|
|
9
|
-
const res = await
|
|
20
|
+
const res = await authedFetch(`${this.baseUrl}/status`);
|
|
10
21
|
return res.json();
|
|
11
22
|
}
|
|
12
23
|
async send(channel, action, payload) {
|
|
13
|
-
const res = await
|
|
24
|
+
const res = await authedFetch(`${this.baseUrl}/send`, {
|
|
14
25
|
method: "POST",
|
|
15
26
|
headers: { "Content-Type": "application/json" },
|
|
16
27
|
body: JSON.stringify({ channel, action, payload })
|
|
@@ -18,7 +29,7 @@ class DevChannelClient {
|
|
|
18
29
|
return res.json();
|
|
19
30
|
}
|
|
20
31
|
async request(channel, action, payload, timeout = 5000) {
|
|
21
|
-
const res = await
|
|
32
|
+
const res = await authedFetch(`${this.baseUrl}/request`, {
|
|
22
33
|
method: "POST",
|
|
23
34
|
headers: { "Content-Type": "application/json" },
|
|
24
35
|
body: JSON.stringify({ channel, action, payload, timeout })
|
|
@@ -26,7 +37,7 @@ class DevChannelClient {
|
|
|
26
37
|
return res.json();
|
|
27
38
|
}
|
|
28
39
|
async query(selector) {
|
|
29
|
-
const res = await
|
|
40
|
+
const res = await authedFetch(`${this.baseUrl}/query`, {
|
|
30
41
|
method: "POST",
|
|
31
42
|
headers: { "Content-Type": "application/json" },
|
|
32
43
|
body: JSON.stringify({ selector, all: false })
|
|
@@ -35,7 +46,7 @@ class DevChannelClient {
|
|
|
35
46
|
return response.success ? response.data : null;
|
|
36
47
|
}
|
|
37
48
|
async queryAll(selector) {
|
|
38
|
-
const res = await
|
|
49
|
+
const res = await authedFetch(`${this.baseUrl}/query`, {
|
|
39
50
|
method: "POST",
|
|
40
51
|
headers: { "Content-Type": "application/json" },
|
|
41
52
|
body: JSON.stringify({ selector, all: true })
|
|
@@ -44,7 +55,7 @@ class DevChannelClient {
|
|
|
44
55
|
return response.success ? response.data : [];
|
|
45
56
|
}
|
|
46
57
|
async getConsole(since = 0) {
|
|
47
|
-
const res = await
|
|
58
|
+
const res = await authedFetch(`${this.baseUrl}/console?since=${since}`);
|
|
48
59
|
const response = await res.json();
|
|
49
60
|
return response.success ? response.data : [];
|
|
50
61
|
}
|
|
@@ -52,7 +63,7 @@ class DevChannelClient {
|
|
|
52
63
|
await this.request("console", "clear", {});
|
|
53
64
|
}
|
|
54
65
|
async eval(code) {
|
|
55
|
-
const res = await
|
|
66
|
+
const res = await authedFetch(`${this.baseUrl}/eval`, {
|
|
56
67
|
method: "POST",
|
|
57
68
|
headers: { "Content-Type": "application/json" },
|
|
58
69
|
body: JSON.stringify({ code })
|
|
@@ -64,7 +75,7 @@ class DevChannelClient {
|
|
|
64
75
|
return response.data;
|
|
65
76
|
}
|
|
66
77
|
async click(selector, options) {
|
|
67
|
-
const res = await
|
|
78
|
+
const res = await authedFetch(`${this.baseUrl}/click`, {
|
|
68
79
|
method: "POST",
|
|
69
80
|
headers: { "Content-Type": "application/json" },
|
|
70
81
|
body: JSON.stringify({ selector, options })
|
|
@@ -75,7 +86,7 @@ class DevChannelClient {
|
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
async type(selector, text) {
|
|
78
|
-
const res = await
|
|
89
|
+
const res = await authedFetch(`${this.baseUrl}/type`, {
|
|
79
90
|
method: "POST",
|
|
80
91
|
headers: { "Content-Type": "application/json" },
|
|
81
92
|
body: JSON.stringify({ selector, text })
|
|
@@ -98,7 +109,7 @@ class DevChannelClient {
|
|
|
98
109
|
await this.dispatch({ selector, event: "blur" });
|
|
99
110
|
}
|
|
100
111
|
async press(key, modifiers) {
|
|
101
|
-
const res = await
|
|
112
|
+
const res = await authedFetch(`${this.baseUrl}/key`, {
|
|
102
113
|
method: "POST",
|
|
103
114
|
headers: { "Content-Type": "application/json" },
|
|
104
115
|
body: JSON.stringify({
|
|
@@ -126,7 +137,7 @@ class DevChannelClient {
|
|
|
126
137
|
return outcome;
|
|
127
138
|
}
|
|
128
139
|
async refresh(hard = true) {
|
|
129
|
-
const res = await
|
|
140
|
+
const res = await authedFetch(`${this.baseUrl}/refresh`, {
|
|
130
141
|
method: "POST",
|
|
131
142
|
headers: { "Content-Type": "application/json" },
|
|
132
143
|
body: JSON.stringify({ soft: !hard })
|
|
@@ -138,7 +149,7 @@ class DevChannelClient {
|
|
|
138
149
|
return this.navigationOutcome(response);
|
|
139
150
|
}
|
|
140
151
|
async navigate(url) {
|
|
141
|
-
const res = await
|
|
152
|
+
const res = await authedFetch(`${this.baseUrl}/navigate`, {
|
|
142
153
|
method: "POST",
|
|
143
154
|
headers: { "Content-Type": "application/json" },
|
|
144
155
|
body: JSON.stringify({ url })
|
|
@@ -150,7 +161,7 @@ class DevChannelClient {
|
|
|
150
161
|
return this.navigationOutcome(response);
|
|
151
162
|
}
|
|
152
163
|
async getLocation() {
|
|
153
|
-
const res = await
|
|
164
|
+
const res = await authedFetch(`${this.baseUrl}/location`);
|
|
154
165
|
const response = await res.json();
|
|
155
166
|
if (!response.success) {
|
|
156
167
|
throw new Error(response.error || "Get location failed");
|
|
@@ -158,7 +169,7 @@ class DevChannelClient {
|
|
|
158
169
|
return response.data;
|
|
159
170
|
}
|
|
160
171
|
async startRecording(name) {
|
|
161
|
-
const res = await
|
|
172
|
+
const res = await authedFetch(`${this.baseUrl}/recording/start`, {
|
|
162
173
|
method: "POST",
|
|
163
174
|
headers: { "Content-Type": "application/json" },
|
|
164
175
|
body: JSON.stringify({ name })
|
|
@@ -170,7 +181,7 @@ class DevChannelClient {
|
|
|
170
181
|
return response.data.sessionId;
|
|
171
182
|
}
|
|
172
183
|
async stopRecording() {
|
|
173
|
-
const res = await
|
|
184
|
+
const res = await authedFetch(`${this.baseUrl}/recording/stop`, {
|
|
174
185
|
method: "POST"
|
|
175
186
|
});
|
|
176
187
|
const response = await res.json();
|
|
@@ -186,7 +197,7 @@ class DevChannelClient {
|
|
|
186
197
|
}
|
|
187
198
|
}
|
|
188
199
|
async publishBuild(event) {
|
|
189
|
-
await
|
|
200
|
+
await authedFetch(`${this.baseUrl}/build`, {
|
|
190
201
|
method: "POST",
|
|
191
202
|
headers: { "Content-Type": "application/json" },
|
|
192
203
|
body: JSON.stringify(event)
|
|
@@ -206,7 +217,7 @@ class DevChannelClient {
|
|
|
206
217
|
}
|
|
207
218
|
}
|
|
208
219
|
async watchMutations(options) {
|
|
209
|
-
const res = await
|
|
220
|
+
const res = await authedFetch(`${this.baseUrl}/mutations/watch`, {
|
|
210
221
|
method: "POST",
|
|
211
222
|
headers: { "Content-Type": "application/json" },
|
|
212
223
|
body: JSON.stringify(options || {})
|
|
@@ -217,7 +228,7 @@ class DevChannelClient {
|
|
|
217
228
|
}
|
|
218
229
|
}
|
|
219
230
|
async unwatchMutations() {
|
|
220
|
-
const res = await
|
|
231
|
+
const res = await authedFetch(`${this.baseUrl}/mutations/unwatch`, {
|
|
221
232
|
method: "POST"
|
|
222
233
|
});
|
|
223
234
|
const response = await res.json();
|
|
@@ -226,7 +237,7 @@ class DevChannelClient {
|
|
|
226
237
|
}
|
|
227
238
|
}
|
|
228
239
|
async getMutationStatus() {
|
|
229
|
-
const res = await
|
|
240
|
+
const res = await authedFetch(`${this.baseUrl}/mutations/status`);
|
|
230
241
|
const response = await res.json();
|
|
231
242
|
if (!response.success) {
|
|
232
243
|
throw new Error(response.error || "Get mutation status failed");
|
|
@@ -234,7 +245,7 @@ class DevChannelClient {
|
|
|
234
245
|
return response.data;
|
|
235
246
|
}
|
|
236
247
|
async getMessages(since = 0) {
|
|
237
|
-
const res = await
|
|
248
|
+
const res = await authedFetch(`${this.baseUrl}/messages?since=${since}`);
|
|
238
249
|
return res.json();
|
|
239
250
|
}
|
|
240
251
|
async getInteractiveElements() {
|
|
@@ -306,7 +317,7 @@ class DevChannelClient {
|
|
|
306
317
|
};
|
|
307
318
|
}
|
|
308
319
|
async validateTest(test) {
|
|
309
|
-
const res = await
|
|
320
|
+
const res = await authedFetch(`${this.baseUrl}/test/validate`, {
|
|
310
321
|
method: "POST",
|
|
311
322
|
headers: { "Content-Type": "application/json" },
|
|
312
323
|
body: JSON.stringify(test)
|
|
@@ -314,7 +325,7 @@ class DevChannelClient {
|
|
|
314
325
|
return res.json();
|
|
315
326
|
}
|
|
316
327
|
async runTest(test, options) {
|
|
317
|
-
const res = await
|
|
328
|
+
const res = await authedFetch(`${this.baseUrl}/test/run`, {
|
|
318
329
|
method: "POST",
|
|
319
330
|
headers: { "Content-Type": "application/json" },
|
|
320
331
|
body: JSON.stringify({ test, ...options })
|
|
@@ -330,7 +341,7 @@ class DevChannelClient {
|
|
|
330
341
|
};
|
|
331
342
|
}
|
|
332
343
|
async runTestSuite(tests, options) {
|
|
333
|
-
const res = await
|
|
344
|
+
const res = await authedFetch(`${this.baseUrl}/test/suite`, {
|
|
334
345
|
method: "POST",
|
|
335
346
|
headers: { "Content-Type": "application/json" },
|
|
336
347
|
body: JSON.stringify({ tests, ...options })
|
|
@@ -339,7 +350,7 @@ class DevChannelClient {
|
|
|
339
350
|
}
|
|
340
351
|
async loadTest(source) {
|
|
341
352
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
342
|
-
const res = await
|
|
353
|
+
const res = await authedFetch(source);
|
|
343
354
|
return res.json();
|
|
344
355
|
}
|
|
345
356
|
return JSON.parse(source);
|
package/dist/component.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - Option+Tab toggles visibility (but active state always shows briefly)
|
|
21
21
|
* - Localhost only by default
|
|
22
22
|
*/
|
|
23
|
-
export declare const VERSION = "1.13.0
|
|
23
|
+
export declare const VERSION = "1.13.0";
|
|
24
24
|
export declare class DevChannel extends HTMLElement {
|
|
25
25
|
static get tagName(): string;
|
|
26
26
|
static elementCreator(): () => DevChannel;
|
package/dist/component.esm.js
CHANGED