haltija 1.2.4 → 1.2.7

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.
@@ -36,6 +36,10 @@ const HALTIJA_SERVER = `http://localhost:${HALTIJA_PORT}`
36
36
  // Combined with webContents.id to create globally unique tab identifiers
37
37
  const APP_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
38
38
 
39
+ // Pending session: when an agent opens a tab with a session, store it here
40
+ // so injectWidget can include it in the widget config for the next new tab
41
+ let pendingTabSession = null
42
+
39
43
  // ============================================
40
44
  // Preferences
41
45
  // ============================================
@@ -741,8 +745,16 @@ async function injectWidget(webContents) {
741
745
  // This is stable across navigations (even cross-origin) enabling cross-page recording
742
746
  const wsUrl = HALTIJA_SERVER.replace('http:', 'ws:') + '/ws/browser'
743
747
  const windowId = `hj-${APP_INSTANCE_ID}-${webContents.id}`
748
+ const configObj = { serverUrl: wsUrl, windowId }
749
+ // Session priority: pending session from agent's tabs/open > env var
750
+ if (pendingTabSession) {
751
+ configObj.session = pendingTabSession
752
+ pendingTabSession = null // Consume — only applies to the next tab
753
+ } else if (process.env.HALTIJA_SESSION) {
754
+ configObj.session = process.env.HALTIJA_SESSION
755
+ }
744
756
  await webContents.executeJavaScript(
745
- `window.__haltija_config__ = { serverUrl: '${wsUrl}', windowId: '${windowId}' };`,
757
+ `window.__haltija_config__ = ${JSON.stringify(configObj)};`,
746
758
  )
747
759
  await webContents.executeJavaScript(componentCode)
748
760
 
@@ -882,9 +894,10 @@ function setupScreenCapture() {
882
894
  })
883
895
 
884
896
  // Tab management — forwarded to renderer
885
- ipcMain.handle('open-tab', async (event, url) => {
897
+ ipcMain.handle('open-tab', async (event, url, session) => {
886
898
  if (!mainWindow) return false
887
- mainWindow.webContents.send('open-tab', { url })
899
+ if (session) pendingTabSession = session
900
+ mainWindow.webContents.send('open-tab', { url, session })
888
901
  return true
889
902
  })
890
903
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.2.4",
3
+ "version": "1.2.7",
4
4
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
5
5
  "homepage": "https://github.com/tonioloewald/haltija",
6
6
  "author": {
@@ -130,7 +130,7 @@ document.addEventListener('keydown', (e) => {
130
130
  // ============================================
131
131
 
132
132
  // Tab management from widget (via main process IPC)
133
- window.haltija?.onOpenTab?.((data) => createTab(data.url))
133
+ window.haltija?.onOpenTab?.((data) => createTab(data.url, { session: data.session }))
134
134
  window.haltija?.onCloseTab?.((data) => {
135
135
  const tab = findTabByWindowId(data.windowId)
136
136
  if (tab) closeTab(tab.id)
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.2.4";
49
+ var VERSION = "1.2.7";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1310,9 +1310,22 @@
1310
1310
  }
1311
1311
  return summary;
1312
1312
  }
1313
+ function pruneNonInteractive(node) {
1314
+ const isInteractive = node.flags?.interactive || node.flags?.hasEvents;
1315
+ const children = node.children?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1316
+ const shadowChildren = node.shadowChildren?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1317
+ const hasInteractiveDescendant = children && children.length > 0 || shadowChildren && shadowChildren.length > 0;
1318
+ if (!isInteractive && !hasInteractiveDescendant)
1319
+ return null;
1320
+ return {
1321
+ ...node,
1322
+ children: children?.length ? children : undefined,
1323
+ shadowChildren: shadowChildren?.length ? shadowChildren : undefined
1324
+ };
1325
+ }
1313
1326
  function buildDomTree(el, options, currentDepth = 0) {
1314
1327
  const {
1315
- depth = 3,
1328
+ depth = -1,
1316
1329
  includeText = true,
1317
1330
  allAttributes = false,
1318
1331
  includeStyles = false,
@@ -1703,6 +1716,7 @@
1703
1716
  windowId;
1704
1717
  isElectron = false;
1705
1718
  browserId = uid();
1719
+ sessionToken = "";
1706
1720
  killed = false;
1707
1721
  isActive = true;
1708
1722
  homeLeft = 0;
@@ -1775,7 +1789,7 @@
1775
1789
  selectionBox = null;
1776
1790
  highlightedElements = [];
1777
1791
  static get observedAttributes() {
1778
- return ["server", "hidden"];
1792
+ return ["server", "hidden", "session"];
1779
1793
  }
1780
1794
  static async runTests() {
1781
1795
  const el = document.querySelector(TAG_NAME);
@@ -1871,10 +1885,16 @@
1871
1885
  }
1872
1886
  this.windowId = storedWindowId;
1873
1887
  }
1888
+ if (config?.session) {
1889
+ this.sessionToken = config.session;
1890
+ } else {
1891
+ this.sessionToken = uid();
1892
+ }
1874
1893
  }
1875
1894
  connectedCallback() {
1876
1895
  this.killed = false;
1877
1896
  this.serverUrl = this.getAttribute("server") || this.serverUrl;
1897
+ this.sessionToken = this.getAttribute("session") || this.sessionToken;
1878
1898
  this.render();
1879
1899
  const rect = this.getBoundingClientRect();
1880
1900
  this.homeLeft = window.innerWidth - rect.width - 16;
@@ -1902,6 +1922,11 @@
1902
1922
  this.connect();
1903
1923
  }
1904
1924
  }
1925
+ if (name === "session") {
1926
+ this.sessionToken = value;
1927
+ this.disconnect();
1928
+ this.connect();
1929
+ }
1905
1930
  }
1906
1931
  render() {
1907
1932
  if (this.shadowRoot.querySelector(".widget")) {
@@ -2087,6 +2112,25 @@
2087
2112
  .btn.info-btn:hover {
2088
2113
  background: #2563eb;
2089
2114
  }
2115
+ .session-badge {
2116
+ display: flex;
2117
+ align-items: center;
2118
+ gap: 2px;
2119
+ font-size: 9px;
2120
+ color: #666;
2121
+ background: rgba(255,255,255,0.05);
2122
+ padding: 2px 4px;
2123
+ border-radius: 3px;
2124
+ cursor: pointer;
2125
+ user-select: none;
2126
+ }
2127
+ .session-badge:hover {
2128
+ background: rgba(255,255,255,0.1);
2129
+ color: #aaa;
2130
+ }
2131
+ .session-badge.copied {
2132
+ color: #22c55e;
2133
+ }
2090
2134
  @keyframes pulse {
2091
2135
  0%, 100% { opacity: 1; }
2092
2136
  50% { opacity: 0.5; }
@@ -2449,6 +2493,8 @@
2449
2493
  <span class="logo">\uD83E\uDDDD</span>
2450
2494
  </div>
2451
2495
  <div class="title">${PRODUCT_NAME}</div>
2496
+ <span class="session-badge" data-action="copy-session" title="Session: ${this.sessionToken}
2497
+ Click to copy session command">${this.sessionToken.slice(0, 8)}</span>
2452
2498
  <div class="controls">
2453
2499
  <button class="btn" data-action="select" title="Select elements (drag to select area)" aria-label="Select elements">\uD83D\uDC46</button>
2454
2500
  <button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
@@ -2535,6 +2581,8 @@
2535
2581
  this.startSelection();
2536
2582
  if (action2 === "stats")
2537
2583
  this.copyStatsToClipboard();
2584
+ if (action2 === "copy-session")
2585
+ this.copySessionToken(e.currentTarget);
2538
2586
  if (action2 === "close-modal")
2539
2587
  this.closeTestModal();
2540
2588
  if (action2 === "copy-test")
@@ -3160,6 +3208,28 @@
3160
3208
  });
3161
3209
  }
3162
3210
  }
3211
+ async copySessionToken(badge) {
3212
+ const cmd = `export HALTIJA_SESSION=${this.sessionToken}`;
3213
+ try {
3214
+ await navigator.clipboard.writeText(cmd);
3215
+ badge.textContent = "copied!";
3216
+ badge.classList.add("copied");
3217
+ setTimeout(() => {
3218
+ badge.textContent = this.sessionToken.slice(0, 8);
3219
+ badge.classList.remove("copied");
3220
+ }, 1500);
3221
+ } catch {
3222
+ try {
3223
+ await navigator.clipboard.writeText(this.sessionToken);
3224
+ badge.textContent = "copied!";
3225
+ badge.classList.add("copied");
3226
+ setTimeout(() => {
3227
+ badge.textContent = this.sessionToken.slice(0, 8);
3228
+ badge.classList.remove("copied");
3229
+ }, 1500);
3230
+ } catch {}
3231
+ }
3232
+ }
3163
3233
  async copyStatsToClipboard() {
3164
3234
  const refStats = refRegistry.getStats();
3165
3235
  stats.refsStale = refStats.stale;
@@ -4926,6 +4996,7 @@ ${elementSummary}${moreText}`;
4926
4996
  this.send("system", "connected", {
4927
4997
  windowId: this.windowId,
4928
4998
  browserId: this.browserId,
4999
+ session: this.sessionToken,
4929
5000
  version: VERSION2,
4930
5001
  serverSessionId: SERVER_SESSION_ID,
4931
5002
  url: location.href,
@@ -5351,7 +5422,7 @@ ${elementSummary}${moreText}`;
5351
5422
  const haltija = window.haltija;
5352
5423
  if (action2 === "open") {
5353
5424
  if (haltija?.openTab) {
5354
- haltija.openTab(payload2.url).then((opened) => {
5425
+ haltija.openTab(payload2.url, payload2.session).then((opened) => {
5355
5426
  this.respond(msg2.id, true, { opened });
5356
5427
  }).catch((err) => {
5357
5428
  this.respond(msg2.id, false, null, err.message);
@@ -5471,7 +5542,10 @@ ${elementSummary}${moreText}`;
5471
5542
  const summary = buildActionableSummary(el);
5472
5543
  this.respond(msg2.id, true, summary);
5473
5544
  } else {
5474
- const tree = buildDomTree(el, request);
5545
+ let tree = buildDomTree(el, request);
5546
+ if (request.interactiveOnly && tree) {
5547
+ tree = pruneNonInteractive(tree);
5548
+ }
5475
5549
  if (request.ancestors && tree) {
5476
5550
  const ancestors = [];
5477
5551
  let parent = el.parentElement;
@@ -7207,7 +7281,7 @@ ${elementSummary}${moreText}`;
7207
7281
  }
7208
7282
  registerDevChannel();
7209
7283
  var WIDGET_ID = "haltija-widget";
7210
- function inject(serverUrl2 = "wss://localhost:8700/ws/browser") {
7284
+ function inject(serverUrl2 = "wss://localhost:8700/ws/browser", options) {
7211
7285
  const existing = document.getElementById(WIDGET_ID);
7212
7286
  if (existing) {
7213
7287
  console.log(`${LOG_PREFIX} Already injected`);
@@ -7222,6 +7296,8 @@ ${elementSummary}${moreText}`;
7222
7296
  const el = DevChannel.elementCreator()();
7223
7297
  el.id = WIDGET_ID;
7224
7298
  el.setAttribute("server", serverUrl2);
7299
+ if (options?.session)
7300
+ el.setAttribute("session", options.session);
7225
7301
  el.setAttribute("data-version", VERSION2);
7226
7302
  document.body.appendChild(el);
7227
7303
  console.log(`${LOG_PREFIX} Injected`);
@@ -7233,7 +7309,7 @@ ${elementSummary}${moreText}`;
7233
7309
  const config = window.__haltija_config__;
7234
7310
  if (config?.autoInject !== false) {
7235
7311
  if (config) {
7236
- inject(config.serverUrl || config.wsUrl);
7312
+ inject(config.serverUrl || config.wsUrl, { session: config.session });
7237
7313
  return;
7238
7314
  }
7239
7315
  }
@@ -25,7 +25,7 @@ contextBridge.exposeInMainWorld('haltija', {
25
25
  return ipcRenderer.invoke('navigate-url', url)
26
26
  },
27
27
  // Tab management — routed through main process to renderer
28
- openTab: (url) => ipcRenderer.invoke('open-tab', url),
28
+ openTab: (url, session) => ipcRenderer.invoke('open-tab', url, session),
29
29
  closeTab: (windowId) => ipcRenderer.invoke('close-tab', windowId),
30
30
  focusTab: (windowId) => ipcRenderer.invoke('focus-tab', windowId),
31
31
  openAgentTab: () => {
@@ -204,7 +204,8 @@ export function parseTreeArgs(args) {
204
204
  if (a === '--depth' || a === '-d') { body.depth = num(args[++i]); continue }
205
205
  if (a === '--selector' || a === '-s') { body.selector = args[++i]; continue }
206
206
  if (a === '--compact' || a === '-c') { body.compact = true; continue }
207
- if (a === '--visible') { body.visibleOnly = true; continue }
207
+ if (a === '--interactive' || a === '-i') { body.interactiveOnly = true; continue }
208
+ if (a === '--visible' || a === '-v') { body.visibleOnly = true; continue }
208
209
  if (a === '--text') { body.includeText = true; continue }
209
210
  if (a === '--no-text') { body.includeText = false; continue }
210
211
  if (a === '--shadow') { body.pierceShadow = true; continue }
@@ -432,6 +433,18 @@ export function clean(obj) {
432
433
  return Object.keys(result).length ? result : undefined
433
434
  }
434
435
 
436
+ // ============================================
437
+ // Session ID for multi-agent isolation
438
+ // ============================================
439
+
440
+ export function getSessionId() {
441
+ if (process.env.HALTIJA_SESSION) return process.env.HALTIJA_SESSION
442
+ // Auto-generate a session ID for this shell — persisted in env for subsequent calls
443
+ const id = `hj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
444
+ process.env.HALTIJA_SESSION = id
445
+ return id
446
+ }
447
+
435
448
  // ============================================
436
449
  // Server auto-start
437
450
  // ============================================
@@ -509,14 +522,92 @@ async function startServerInBackground(port) {
509
522
  return false
510
523
  }
511
524
 
525
+ // ============================================
526
+ // Auto-launch Electron app when no browser windows connected
527
+ // ============================================
528
+
529
+ async function launchElectronApp() {
530
+ const { execSync, spawn: spawnChild } = await import('child_process')
531
+
532
+ if (process.platform === 'darwin') {
533
+ // Check common locations for Haltija.app
534
+ const appPaths = [
535
+ '/Applications/Haltija.app',
536
+ `${process.env.HOME}/Applications/Haltija.app`,
537
+ ]
538
+ for (const p of appPaths) {
539
+ if (existsSync(p)) {
540
+ spawnChild('open', ['-a', p], { stdio: 'ignore', detached: true }).unref()
541
+ return true
542
+ }
543
+ }
544
+ // Try spotlight search as fallback
545
+ try {
546
+ const result = execSync('mdfind "kMDItemCFBundleIdentifier == com.electron.haltija" | head -1', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
547
+ if (result) {
548
+ spawnChild('open', ['-a', result], { stdio: 'ignore', detached: true }).unref()
549
+ return true
550
+ }
551
+ } catch {}
552
+ return false
553
+ }
554
+
555
+ // Linux/Windows: not yet supported
556
+ return false
557
+ }
558
+
559
+ async function ensureBrowserConnected(port) {
560
+ try {
561
+ const resp = await fetch(`http://localhost:${port}/status`, {
562
+ signal: AbortSignal.timeout(2000)
563
+ })
564
+ const status = await resp.json()
565
+ // Use status.ok (global, not session-filtered) to check if any browser is connected
566
+ if (status.ok) return true
567
+ } catch { return false }
568
+
569
+ // No windows connected — try to launch Electron app (macOS only)
570
+ if (process.platform !== 'darwin') return false
571
+
572
+ process.stderr.write('\x1b[2mLaunching Haltija browser...\x1b[0m')
573
+ const launched = await launchElectronApp()
574
+ if (!launched) {
575
+ process.stderr.write('\x1b[2m not found\x1b[0m\n')
576
+ return false
577
+ }
578
+
579
+ // Wait for a window to connect (up to 10s)
580
+ const maxWait = 10000
581
+ const start = Date.now()
582
+ while (Date.now() - start < maxWait) {
583
+ try {
584
+ const resp = await fetch(`http://localhost:${port}/status`, {
585
+ signal: AbortSignal.timeout(1000)
586
+ })
587
+ const status = await resp.json()
588
+ if (status.ok) {
589
+ process.stderr.write('\x1b[2m ready\x1b[0m\n')
590
+ return true
591
+ }
592
+ } catch {}
593
+ await new Promise(r => setTimeout(r, 500))
594
+ }
595
+ process.stderr.write('\x1b[2m timeout\x1b[0m\n')
596
+ return false
597
+ }
598
+
599
+ // Commands that don't need a browser window to be connected
600
+ const INFO_COMMANDS = new Set(['status', 'windows', 'version', 'help'])
601
+
512
602
  // ============================================
513
603
  // Main subcommand execution
514
604
  // ============================================
515
605
 
516
- export async function runSubcommand(subcommand, subArgs, port = '8700') {
606
+ export async function runSubcommand(subcommand, subArgs, port = '8700', options = {}) {
517
607
  const baseUrl = `http://localhost:${port}`
518
608
  const jsonOutput = subArgs.includes('--json')
519
- // Remove --json and extract --window before processing
609
+ const noLaunch = options.noLaunch || false
610
+ // Remove --json and extract --window/--session before processing
520
611
  let filteredArgs = subArgs.filter(a => a !== '--json')
521
612
  let targetWindowId = undefined
522
613
  const windowIdx = filteredArgs.indexOf('--window')
@@ -524,6 +615,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
524
615
  targetWindowId = filteredArgs[windowIdx + 1]
525
616
  filteredArgs = [...filteredArgs.slice(0, windowIdx), ...filteredArgs.slice(windowIdx + 2)]
526
617
  }
618
+ const sessionIdx = filteredArgs.indexOf('--session')
619
+ if (sessionIdx !== -1) {
620
+ process.env.HALTIJA_SESSION = filteredArgs[sessionIdx + 1]
621
+ filteredArgs = [...filteredArgs.slice(0, sessionIdx), ...filteredArgs.slice(sessionIdx + 2)]
622
+ }
527
623
 
528
624
  // Check if server is running, auto-start if not
529
625
  if (!(await isServerRunning(port))) {
@@ -538,6 +634,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
538
634
  }
539
635
  }
540
636
 
637
+ // Auto-launch browser if no windows connected (skip for info commands and --no-launch)
638
+ if (!noLaunch && !INFO_COMMANDS.has(subcommand)) {
639
+ await ensureBrowserConnected(port)
640
+ }
641
+
541
642
  // Special handling for 'send' command - route to appropriate endpoint
542
643
  // hj send selection [agent] → /send/selection
543
644
  // hj send recording [agent] → /send/recording
@@ -596,9 +697,10 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
596
697
  async function doRequest(url, method, body, context = {}) {
597
698
  const { subcommand, jsonOutput } = context
598
699
  try {
599
- const opts = { method }
700
+ const sessionId = getSessionId()
701
+ const opts = { method, headers: { 'X-Haltija-Session': sessionId } }
600
702
  if (body) {
601
- opts.headers = { 'Content-Type': 'application/json' }
703
+ opts.headers['Content-Type'] = 'application/json'
602
704
  opts.body = JSON.stringify(body)
603
705
  }
604
706
 
@@ -610,7 +712,7 @@ async function doRequest(url, method, body, context = {}) {
610
712
 
611
713
  // Text format for supported subcommands (unless --json)
612
714
  if (!jsonOutput && subcommand === 'tree' && json.success && json.data) {
613
- console.log(formatTree(json.data))
715
+ console.log(formatTree(json.data, 0, { depth: body?.depth }))
614
716
  } else if (!jsonOutput && subcommand === 'events' && (json.events || Array.isArray(json))) {
615
717
  console.log(formatEvents(json))
616
718
  } else if (!jsonOutput && subcommand === 'test-run' && json.test) {
@@ -730,7 +832,7 @@ export function listSubcommands() {
730
832
  return `
731
833
  Subcommands (replace curl with simple commands):
732
834
  ${bold('Inspect')}
733
- tree [selector] [-d depth] DOM tree with ref IDs
835
+ tree [selector] [-d N] [-i] [-v] DOM tree (full depth, -i=interactive, -v=visible)
734
836
  query <selector> Find elements matching selector
735
837
  inspect <@ref|selector> Detailed element info
736
838
  inspectAll <selector> Deep inspect all matches
@@ -27,15 +27,17 @@ const MAX_TEXT_LEN = 80
27
27
  * @param {number} indent - current indentation level (internal)
28
28
  * @returns {string} formatted text output with footer
29
29
  */
30
- export function formatTree(node, indent = 0) {
30
+ export function formatTree(node, indent = 0, { depth } = {}) {
31
31
  if (!node) return ''
32
32
 
33
33
  const lines = []
34
34
  formatNode(node, indent, lines)
35
35
 
36
- // Footer: JSON escape hatch
36
+ // Footer: options hint
37
+ const d = depth ?? -1
38
+ const depthLabel = d === -1 ? 'unlimited' : String(d)
37
39
  lines.push('---')
38
- lines.push('hj tree --json')
40
+ lines.push(`depth=${depthLabel} | -d N | -i (interactive) | --visible | --json`)
39
41
 
40
42
  return lines.join('\n')
41
43
  }
package/bin/hints.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "tree": "-d 5 (deeper), --compact, \"#selector\" | see: inspect, query, click",
2
+ "tree": "-d 3 (shallow), -i (interactive only), --visible, --compact | see: inspect, query, click",
3
3
  "query": "@ref or \"selector\", --all | see: tree, inspect",
4
4
  "inspect": "@ref or \"selector\", --styles, --rules, --ancestors | see: tree, query",
5
5
  "click": "@ref or \"selector\", :text(Button), --diff | see: tree, wait, type",
package/bin/hj.mjs CHANGED
@@ -36,6 +36,21 @@ if (portIdx !== -1 && args[portIdx + 1]) {
36
36
  args.splice(portIdx, 2)
37
37
  }
38
38
 
39
+ // Parse --session option (set session token)
40
+ const sessionIdx = args.indexOf('--session')
41
+ if (sessionIdx !== -1 && args[sessionIdx + 1]) {
42
+ process.env.HALTIJA_SESSION = args[sessionIdx + 1]
43
+ args.splice(sessionIdx, 2)
44
+ }
45
+
46
+ // Parse --no-launch option (skip auto-launching Electron app)
47
+ let noLaunch = false
48
+ const noLaunchIdx = args.indexOf('--no-launch')
49
+ if (noLaunchIdx !== -1) {
50
+ noLaunch = true
51
+ args.splice(noLaunchIdx, 1)
52
+ }
53
+
39
54
  const subcommand = args[0]
40
55
  const subArgs = args.slice(1).filter(a => a !== '--window' || true) // keep all args
41
56
 
@@ -61,7 +76,7 @@ if (!isSubcommand(subcommand)) {
61
76
  console.error(`Run 'hj' for docs.`)
62
77
  process.exit(1)
63
78
  } else {
64
- runSubcommand(subcommand, subArgs, port)
79
+ runSubcommand(subcommand, subArgs, port, { noLaunch })
65
80
  }
66
81
 
67
82
  function filterHelp(topic) {
@@ -362,6 +362,8 @@ if (docsDirIdx !== -1 && args[docsDirIdx + 1]) {
362
362
  // ============================================
363
363
 
364
364
  const ciMode = args.includes('--ci')
365
+ const secureMode = args.includes('--secure')
366
+ if (secureMode) process.env.HALTIJA_SECURE = '1'
365
367
  const headlessMode = args.includes('--headless') // Playwright headless (separate from CI)
366
368
  const headlessUrlIdx = args.indexOf('--headless-url')
367
369
  const headlessUrl = headlessUrlIdx !== -1 ? args[headlessUrlIdx + 1] : null