haltija 1.3.0-beta.8 → 1.3.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/README.md CHANGED
@@ -76,6 +76,26 @@ Haltija connects to the browser you're already using. The one with the bug, the
76
76
 
77
77
  ---
78
78
 
79
+ ## Haltija vs. Claude in Chrome
80
+
81
+ Both let an agent drive a browser — they're built for different jobs. Because Haltija can either **spawn its own isolated browser** *or* **attach to your real one** (via a script tag / bookmarklet on a port you choose), it covers the axes a live-session-only tool can't.
82
+
83
+ | Axis | Haltija | Claude in Chrome |
84
+ |---|---|---|
85
+ | Setup / auth | ✅ `bunx haltija` — zero auth, self-spawns | ⚠️ Extension + claude.ai login + per-site permissions |
86
+ | How the agent invokes it | ✅ Plain `hj` CLI / REST — always available in a shell | ⚠️ MCP tools; must be loaded + extension connected |
87
+ | Project integration | ✅ *Is* your test harness — JSON tests (`hj test-suite`), record→replay, CI | ❌ None |
88
+ | Determinism / cost | ✅ DOM + `eval` with stable ref IDs — cheap text, scriptable, headless-CI-able | ⚠️ Has structured reads (page text, find-by-text) *and* vision/coordinate actions; the vision path is token-heavy and coordinate clicks less deterministic |
89
+ | Isolation | ✅ Own instance on its own port — never touches your browsing | ❌ Drives tabs in your live session |
90
+ | Real-browser fidelity (WebGL/3D, pixels) | ✅ Attach to a real GPU Chrome or the desktop app *(headless GPU is the one weak spot)* | ✅ Full real Chrome GPU |
91
+ | Authenticated / external sites | ✅ Inject into your logged-in browser on a chosen port *(one-time setup step)* | ✅ Native — already your logged-in profile |
92
+ | Flakiness / contention | ✅ A private port per project = dedicated instance *(a shared instance can still flake under contention)* | ⚠️ Depends on live browser + extension state |
93
+ | Screenshots | ✅ Native (desktop) or WebRTC `getDisplayMedia` (browser) | ✅ Vision-native |
94
+
95
+ **The short version:** reach for Claude in Chrome to glance at the tab you're already looking at; reach for Haltija when you want a browser your agent *controls* — reproducibly, cheaply, in CI, and as the regression harness your project already has.
96
+
97
+ ---
98
+
79
99
  ## How It Works
80
100
 
81
101
  ```
@@ -246,18 +266,25 @@ The server rejects every REST/WebSocket request without a matching `X-Haltija-To
246
266
  ## Installation
247
267
 
248
268
  ```bash
249
- bunx haltija # Desktop app (recommended)
250
- bunx haltija --server # Server only (your browser, CI, remote)
269
+ bunx haltija # Bundled browser + embedded server (easiest start)
270
+ bunx haltija --server # Server only your browser, per-project dev/debug
251
271
  npm install -g haltija # Install globally
252
272
 
253
273
  # Server options
254
274
  haltija --https # HTTPS mode
255
275
  haltija --port 3000 # Custom port
276
+ haltija --name <proj> # Register a per-project instance (hj --name <proj>)
256
277
  haltija --token <secret> # Require X-Haltija-Token on every request
257
- haltija --headless # For CI pipelines
278
+ haltija --headless # For CI pipelines (no desktop app needed)
258
279
  haltija --setup-mcp # Configure Claude Desktop
259
280
  ```
260
281
 
282
+ **Which mode?** For day-to-day dev/debugging, run a per-project server
283
+ (`haltija --server --name <proj>`) and drive it with the `hj` CLI or your coding
284
+ agent — that's the paved path. For CI, `haltija --headless` runs a deterministic
285
+ headless Chromium with no desktop app required. A downloadable notarized desktop
286
+ app also exists but isn't required for either.
287
+
261
288
  ---
262
289
 
263
290
  ## Security
@@ -1387,6 +1387,13 @@ if (!gotTheLock) {
1387
1387
  // App lifecycle
1388
1388
  app.whenReady().then(async () => {
1389
1389
  console.log('[Haltija Desktop] App ready, starting initialization...')
1390
+
1391
+ // Clear the "manually quit" marker — user is explicitly starting Haltija,
1392
+ // so hj should resume auto-launching when needed.
1393
+ try {
1394
+ const quitMarker = path.join(os.homedir(), '.haltija', 'last-quit')
1395
+ if (fs.existsSync(quitMarker)) fs.rmSync(quitMarker, { force: true })
1396
+ } catch {}
1390
1397
 
1391
1398
  try {
1392
1399
  // Start or connect to server first
@@ -1444,6 +1451,13 @@ if (!gotTheLock) {
1444
1451
  }
1445
1452
  embeddedServers.length = 0
1446
1453
  }
1454
+ // Drop a marker so hj's auto-launch knows the user explicitly quit.
1455
+ // Cleared next time the user manually starts Haltija.
1456
+ try {
1457
+ const dir = path.join(os.homedir(), '.haltija')
1458
+ fs.mkdirSync(dir, { recursive: true })
1459
+ fs.writeFileSync(path.join(dir, 'last-quit'), String(Date.now()))
1460
+ } catch {}
1447
1461
  })
1448
1462
 
1449
1463
  // Handle certificate errors (for self-signed certs in dev)
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.3.0-beta.8",
3
+ "version": "1.3.0",
4
+ "private": true,
4
5
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
5
6
  "homepage": "https://github.com/tonioloewald/haltija",
6
7
  "author": {
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.3.0-beta.8";
49
+ var VERSION = "1.3.0";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -98,6 +98,47 @@
98
98
  return lower.includes(parsed.searchText);
99
99
  }
100
100
 
101
+ // src/key-codes.ts
102
+ function keyToCode(key) {
103
+ const specialKeys = {
104
+ Enter: "Enter",
105
+ Escape: "Escape",
106
+ Tab: "Tab",
107
+ Backspace: "Backspace",
108
+ Delete: "Delete",
109
+ ArrowUp: "ArrowUp",
110
+ ArrowDown: "ArrowDown",
111
+ ArrowLeft: "ArrowLeft",
112
+ ArrowRight: "ArrowRight",
113
+ Home: "Home",
114
+ End: "End",
115
+ PageUp: "PageUp",
116
+ PageDown: "PageDown",
117
+ " ": "Space",
118
+ Space: "Space",
119
+ ".": "Period",
120
+ ",": "Comma",
121
+ "/": "Slash",
122
+ ";": "Semicolon",
123
+ "'": "Quote",
124
+ "[": "BracketLeft",
125
+ "]": "BracketRight",
126
+ "\\": "Backslash",
127
+ "-": "Minus",
128
+ "=": "Equal",
129
+ "`": "Backquote"
130
+ };
131
+ if (specialKeys[key])
132
+ return specialKeys[key];
133
+ if (/^F\d{1,2}$/.test(key))
134
+ return key;
135
+ if (/^[a-zA-Z]$/.test(key))
136
+ return `Key${key.toUpperCase()}`;
137
+ if (/^[0-9]$/.test(key))
138
+ return `Digit${key}`;
139
+ return key;
140
+ }
141
+
101
142
  // src/component.ts
102
143
  var VERSION2 = VERSION;
103
144
  var PRODUCT_NAME = "Haltija";
@@ -1435,7 +1476,7 @@
1435
1476
  if (htmlEl.disabled) {
1436
1477
  flags.disabled = true;
1437
1478
  }
1438
- if (htmlEl.readOnly) {
1479
+ if ("readOnly" in htmlEl && htmlEl.readOnly) {
1439
1480
  flags.readOnly = true;
1440
1481
  }
1441
1482
  if (!htmlEl.validity.valid) {
@@ -1717,6 +1758,8 @@
1717
1758
  isElectron = false;
1718
1759
  browserId = uid();
1719
1760
  killed = false;
1761
+ displayStream = null;
1762
+ displayVideo = null;
1720
1763
  isActive = true;
1721
1764
  homeLeft = 0;
1722
1765
  homeBottom = 16;
@@ -1918,6 +1961,7 @@
1918
1961
  this.restoreDialogs();
1919
1962
  this.clearEventWatchers();
1920
1963
  this.stopMutationWatch();
1964
+ this.stopScreenCapture();
1921
1965
  }
1922
1966
  attributeChangedCallback(name, _old, value) {
1923
1967
  if (name === "server") {
@@ -2085,6 +2129,10 @@
2085
2129
  color: white;
2086
2130
  animation: pulse 1s infinite;
2087
2131
  }
2132
+ .btn[data-action="screen"].sharing {
2133
+ background: #3b82f6;
2134
+ color: white;
2135
+ }
2088
2136
  .btn[data-action="logs"] {
2089
2137
  font-size: 10px;
2090
2138
  font-weight: 500;
@@ -2485,6 +2533,7 @@
2485
2533
  <div class="controls">
2486
2534
  <button class="btn" data-action="select" title="Click or drag to select elements" aria-label="Select elements">\uD83D\uDC46</button>
2487
2535
  <button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
2536
+ <button class="btn" data-action="screen" title="Share screen so the agent can take screenshots (browser only; not needed in Haltija desktop app)" aria-label="Share screen for screenshots">\uD83D\uDDA5</button>
2488
2537
  <button class="btn" data-action="logs" title="Show event log panel" aria-label="Toggle event log">LOG</button>
2489
2538
  <button class="btn info-btn" data-action="stats" title="Copy stats to clipboard" aria-label="Copy stats">i</button>
2490
2539
  <button class="btn" data-action="minimize" title="Minimize widget (⌥Tab)" aria-label="Minimize">─</button>
@@ -2566,6 +2615,8 @@
2566
2615
  this.toggleRecording();
2567
2616
  if (action2 === "select")
2568
2617
  this.startSelection();
2618
+ if (action2 === "screen")
2619
+ this.toggleScreenCapture();
2569
2620
  if (action2 === "stats")
2570
2621
  this.copyStatsToClipboard();
2571
2622
  if (action2 === "close-modal")
@@ -3016,7 +3067,7 @@
3016
3067
  if (options.addAssertions && inputValue) {
3017
3068
  steps.push({
3018
3069
  action: "assert",
3019
- assertion: { type: "value", selector, expected: inputValue },
3070
+ assertion: { type: "value", selector, value: inputValue },
3020
3071
  description: `Verify ${inputLabel} is "${inputValue}"`
3021
3072
  });
3022
3073
  }
@@ -3845,6 +3896,55 @@ ${elementSummary}${moreText}`;
3845
3896
  this.hide();
3846
3897
  }
3847
3898
  }
3899
+ async toggleScreenCapture() {
3900
+ if (this.displayStream) {
3901
+ this.stopScreenCapture();
3902
+ return;
3903
+ }
3904
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
3905
+ console.warn(`${LOG_PREFIX} Screen capture not supported in this browser.`);
3906
+ return;
3907
+ }
3908
+ try {
3909
+ const stream = await navigator.mediaDevices.getDisplayMedia({
3910
+ video: { frameRate: { ideal: 5 } },
3911
+ audio: false
3912
+ });
3913
+ this.displayStream = stream;
3914
+ const video = document.createElement("video");
3915
+ video.srcObject = stream;
3916
+ video.muted = true;
3917
+ video.playsInline = true;
3918
+ await video.play().catch(() => {});
3919
+ this.displayVideo = video;
3920
+ stream.getVideoTracks()[0]?.addEventListener("ended", () => {
3921
+ this.stopScreenCapture();
3922
+ });
3923
+ const btn = this.shadowRoot?.querySelector('.btn[data-action="screen"]');
3924
+ btn?.classList.add("sharing");
3925
+ } catch (err) {
3926
+ console.log(`${LOG_PREFIX} Screen capture not started: ${err.message}`);
3927
+ }
3928
+ }
3929
+ stopScreenCapture() {
3930
+ if (this.displayStream) {
3931
+ for (const track of this.displayStream.getTracks()) {
3932
+ try {
3933
+ track.stop();
3934
+ } catch {}
3935
+ }
3936
+ this.displayStream = null;
3937
+ }
3938
+ if (this.displayVideo) {
3939
+ try {
3940
+ this.displayVideo.pause();
3941
+ } catch {}
3942
+ this.displayVideo.srcObject = null;
3943
+ this.displayVideo = null;
3944
+ }
3945
+ const btn = this.shadowRoot?.querySelector('.btn[data-action="screen"]');
3946
+ btn?.classList.remove("sharing");
3947
+ }
3848
3948
  toggleMinimize() {
3849
3949
  if (this.headless)
3850
3950
  return;
@@ -5336,7 +5436,8 @@ ${elementSummary}${moreText}`;
5336
5436
  scroll: 0,
5337
5437
  mutation: 0,
5338
5438
  console: 0,
5339
- focus: 0
5439
+ focus: 0,
5440
+ recording: 0
5340
5441
  };
5341
5442
  this.statsStartTime = Date.now();
5342
5443
  this.startSemanticEvents();
@@ -5781,7 +5882,37 @@ ${elementSummary}${moreText}`;
5781
5882
  return;
5782
5883
  }
5783
5884
  }
5784
- this.respond(msg2.id, false, null, "Screenshots require the Haltija Desktop app. Run: npx haltija@latest -f");
5885
+ if (this.displayVideo && this.displayStream) {
5886
+ try {
5887
+ const video = this.displayVideo;
5888
+ const w = video.videoWidth;
5889
+ const h = video.videoHeight;
5890
+ if (!w || !h) {
5891
+ this.respond(msg2.id, false, null, "Screen capture stream has no frame data yet — try again in a moment.");
5892
+ return;
5893
+ }
5894
+ const canvas = document.createElement("canvas");
5895
+ canvas.width = w;
5896
+ canvas.height = h;
5897
+ const ctx = canvas.getContext("2d");
5898
+ ctx.drawImage(video, 0, 0, w, h);
5899
+ const dataUrl = canvas.toDataURL("image/png");
5900
+ const converted = await convertFormat(dataUrl);
5901
+ this.respond(msg2.id, true, {
5902
+ image: converted.image,
5903
+ viewport,
5904
+ format,
5905
+ width: converted.width,
5906
+ height: converted.height,
5907
+ source: "getDisplayMedia"
5908
+ });
5909
+ return;
5910
+ } catch (err) {
5911
+ this.respond(msg2.id, false, null, `Screen capture frame failed: ${err.message}`);
5912
+ return;
5913
+ }
5914
+ }
5915
+ this.respond(msg2.id, false, null, "No screenshot capture available. Either run the Haltija Desktop app (npx haltija@latest -f) or click the \uD83D\uDDA5 button in the Haltija widget to share your screen.");
5785
5916
  } catch (err) {
5786
5917
  this.respond(msg2.id, false, null, err.message);
5787
5918
  }
@@ -6113,7 +6244,7 @@ ${elementSummary}${moreText}`;
6113
6244
  }
6114
6245
  } else if (isContentEditable) {
6115
6246
  document.execCommand("insertText", false, char);
6116
- } else {}
6247
+ }
6117
6248
  el.dispatchEvent(new InputEvent("input", {
6118
6249
  bubbles: true,
6119
6250
  inputType: "insertText",
@@ -6193,31 +6324,6 @@ ${elementSummary}${moreText}`;
6193
6324
  el.value = value;
6194
6325
  }
6195
6326
  }
6196
- getKeyCode(char) {
6197
- const upper = char.toUpperCase();
6198
- if (upper >= "A" && upper <= "Z")
6199
- return `Key${upper}`;
6200
- if (char >= "0" && char <= "9")
6201
- return `Digit${char}`;
6202
- const specialKeys = {
6203
- " ": "Space",
6204
- ".": "Period",
6205
- ",": "Comma",
6206
- "/": "Slash",
6207
- ";": "Semicolon",
6208
- "'": "Quote",
6209
- "[": "BracketLeft",
6210
- "]": "BracketRight",
6211
- "\\": "Backslash",
6212
- "-": "Minus",
6213
- "=": "Equal",
6214
- "`": "Backquote",
6215
- Enter: "Enter",
6216
- Tab: "Tab",
6217
- Backspace: "Backspace"
6218
- };
6219
- return specialKeys[char] || `Key${upper}`;
6220
- }
6221
6327
  getAdjacentKeys() {
6222
6328
  return {
6223
6329
  a: ["s", "q", "w", "z"],
@@ -6464,32 +6570,7 @@ ${elementSummary}${moreText}`;
6464
6570
  }
6465
6571
  }
6466
6572
  getKeyCode(key) {
6467
- const specialKeys = {
6468
- Enter: "Enter",
6469
- Escape: "Escape",
6470
- Tab: "Tab",
6471
- Backspace: "Backspace",
6472
- Delete: "Delete",
6473
- ArrowUp: "ArrowUp",
6474
- ArrowDown: "ArrowDown",
6475
- ArrowLeft: "ArrowLeft",
6476
- ArrowRight: "ArrowRight",
6477
- Home: "Home",
6478
- End: "End",
6479
- PageUp: "PageUp",
6480
- PageDown: "PageDown",
6481
- " ": "Space",
6482
- Space: "Space"
6483
- };
6484
- if (specialKeys[key])
6485
- return specialKeys[key];
6486
- if (/^F\d{1,2}$/.test(key))
6487
- return key;
6488
- if (/^[a-zA-Z]$/.test(key))
6489
- return `Key${key.toUpperCase()}`;
6490
- if (/^[0-9]$/.test(key))
6491
- return `Digit${key}`;
6492
- return key;
6573
+ return keyToCode(key);
6493
6574
  }
6494
6575
  getElementLabel(el) {
6495
6576
  const text = el.textContent?.trim().slice(0, 30);