latticesql 4.2.3 → 4.2.4

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
@@ -138,6 +138,8 @@ npm install latticesql
138
138
 
139
139
  Requires **Node.js 18+**. The default backend is SQLite (`better-sqlite3`) — no external database process needed.
140
140
 
141
+ > **Prefer a desktop app?** Download a native, double-click build of the GUI (no terminal) for macOS or Windows from [latticesql.com/install](https://latticesql.com/install) — it runs the same GUI server. See [docs/desktop.md](docs/desktop.md).
142
+
141
143
  To use the Postgres backend (for Supabase, Neon, RDS, or any other Postgres-compatible database), install the optional dependency:
142
144
 
143
145
  ```bash
package/dist/cli.js CHANGED
@@ -1494,17 +1494,179 @@ var init_sqlite = __esm({
1494
1494
  }
1495
1495
  });
1496
1496
 
1497
+ // src/db/sqlite-deno.ts
1498
+ import { createRequire as createRequire2 } from "module";
1499
+ function runtimeRequire2() {
1500
+ const importMetaUrl = import.meta.url;
1501
+ return importMetaUrl ? createRequire2(importMetaUrl) : __require;
1502
+ }
1503
+ function loadNodeSqlite() {
1504
+ if (_ctor2) return _ctor2;
1505
+ const mod = runtimeRequire2()("node:sqlite");
1506
+ if (!mod.DatabaseSync) {
1507
+ throw new Error(
1508
+ "node:sqlite is unavailable in this runtime \u2014 cannot open the Deno SQLite adapter"
1509
+ );
1510
+ }
1511
+ _ctor2 = mod.DatabaseSync;
1512
+ return _ctor2;
1513
+ }
1514
+ var _ctor2, DenoSqliteAdapter;
1515
+ var init_sqlite_deno = __esm({
1516
+ "src/db/sqlite-deno.ts"() {
1517
+ "use strict";
1518
+ _ctor2 = null;
1519
+ DenoSqliteAdapter = class {
1520
+ dialect = "sqlite";
1521
+ _db = null;
1522
+ _path;
1523
+ _wal;
1524
+ _busyTimeout;
1525
+ constructor(path3, options) {
1526
+ this._path = path3;
1527
+ this._wal = options?.wal ?? true;
1528
+ this._busyTimeout = options?.busyTimeout ?? 5e3;
1529
+ }
1530
+ get db() {
1531
+ if (!this._db) throw new Error("DenoSqliteAdapter: not open \u2014 call open() first");
1532
+ return this._db;
1533
+ }
1534
+ open() {
1535
+ const Ctor = loadNodeSqlite();
1536
+ this._db = new Ctor(this._path);
1537
+ this._db.exec(`PRAGMA busy_timeout = ${this._busyTimeout.toString()}`);
1538
+ if (this._wal) {
1539
+ this._db.exec("PRAGMA journal_mode = WAL");
1540
+ }
1541
+ }
1542
+ close() {
1543
+ this._db?.close();
1544
+ this._db = null;
1545
+ }
1546
+ run(sql, params = []) {
1547
+ this.db.prepare(sql).run(...params);
1548
+ }
1549
+ get(sql, params = []) {
1550
+ return this.db.prepare(sql).get(...params);
1551
+ }
1552
+ all(sql, params = []) {
1553
+ return this.db.prepare(sql).all(...params);
1554
+ }
1555
+ prepare(sql) {
1556
+ const stmt = this.db.prepare(sql);
1557
+ return {
1558
+ run: (...params) => {
1559
+ const info = stmt.run(...params);
1560
+ return {
1561
+ changes: Number(info.changes),
1562
+ lastInsertRowid: info.lastInsertRowid
1563
+ };
1564
+ },
1565
+ get: (...params) => stmt.get(...params),
1566
+ all: (...params) => stmt.all(...params)
1567
+ };
1568
+ }
1569
+ introspectColumns(table) {
1570
+ const rows = this.all(`PRAGMA table_info("${table}")`);
1571
+ return rows.map((r6) => r6.name);
1572
+ }
1573
+ /** Mirror of SQLiteAdapter.addColumn — SQLite ALTER quirks are binding-agnostic. */
1574
+ addColumn(table, column, typeSpec) {
1575
+ const upperType = typeSpec.toUpperCase();
1576
+ if (upperType.includes("PRIMARY KEY")) return;
1577
+ const hasNonConstantDefault = upperType.includes("CURRENT_TIMESTAMP") || /DATETIME\s*\(\s*'NOW'\s*\)/i.test(typeSpec) || upperType.includes("RANDOM()");
1578
+ if (hasNonConstantDefault) {
1579
+ const safeType = typeSpec.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+\(?\s*CURRENT_TIMESTAMP\s*\)?/gi, "").replace(/\bDEFAULT\s+\(?\s*datetime\([^)]*\)\s*\)?/gi, "").replace(/\bDEFAULT\s+\(?\s*RANDOM\(\)\s*\)?/gi, "").replace(/\s+/g, " ").trim();
1580
+ this.run(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${safeType || "TEXT"}`);
1581
+ this.run(`UPDATE "${table}" SET "${column}" = CURRENT_TIMESTAMP WHERE "${column}" IS NULL`);
1582
+ } else {
1583
+ this.run(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${typeSpec}`);
1584
+ }
1585
+ }
1586
+ /**
1587
+ * O(1) watch-loop change-probe — same composition as SQLiteAdapter, but
1588
+ * `data_version` is read with a plain prepared statement because node:sqlite
1589
+ * has no `.pragma(name, { simple: true })` scalar helper.
1590
+ */
1591
+ changeProbe() {
1592
+ const dataVersion = this.db.prepare("PRAGMA data_version").get().data_version;
1593
+ const totalChanges = this.db.prepare("SELECT total_changes() AS n").get().n;
1594
+ return `${String(dataVersion)}:${String(totalChanges)}`;
1595
+ }
1596
+ // ── Async surface (sync under the hood; mirrors SQLiteAdapter) ──────────
1597
+ // eslint-disable-next-line @typescript-eslint/require-await
1598
+ async runAsync(sql, params) {
1599
+ this.run(sql, params);
1600
+ }
1601
+ // eslint-disable-next-line @typescript-eslint/require-await
1602
+ async getAsync(sql, params) {
1603
+ return this.get(sql, params);
1604
+ }
1605
+ // eslint-disable-next-line @typescript-eslint/require-await
1606
+ async allAsync(sql, params) {
1607
+ return this.all(sql, params);
1608
+ }
1609
+ // eslint-disable-next-line @typescript-eslint/require-await
1610
+ async introspectColumnsAsync(table) {
1611
+ return this.introspectColumns(table);
1612
+ }
1613
+ // eslint-disable-next-line @typescript-eslint/require-await
1614
+ async introspectAllColumns(tables) {
1615
+ const map = /* @__PURE__ */ new Map();
1616
+ for (const t8 of tables) {
1617
+ try {
1618
+ const cols = this.introspectColumns(t8);
1619
+ if (cols.length > 0) map.set(t8, new Set(cols));
1620
+ } catch {
1621
+ }
1622
+ }
1623
+ return map;
1624
+ }
1625
+ // eslint-disable-next-line @typescript-eslint/require-await
1626
+ async addColumnAsync(table, column, typeSpec) {
1627
+ this.addColumn(table, column, typeSpec);
1628
+ }
1629
+ /** BEGIN/COMMIT around an awaited fn; ROLLBACK on throw. Mirror of SQLiteAdapter. */
1630
+ async withClient(fn) {
1631
+ const dbRef = this.db;
1632
+ const getSync = this.get.bind(this);
1633
+ const allSync = this.all.bind(this);
1634
+ const tx = {
1635
+ run: (sql, params) => {
1636
+ const info = dbRef.prepare(sql).run(...params ?? []);
1637
+ return Promise.resolve({ changes: Number(info.changes) });
1638
+ },
1639
+ get: (sql, params) => Promise.resolve(getSync(sql, params ?? [])),
1640
+ all: (sql, params) => Promise.resolve(allSync(sql, params ?? []))
1641
+ };
1642
+ this.run("BEGIN");
1643
+ try {
1644
+ const result = await fn(tx);
1645
+ this.run("COMMIT");
1646
+ return result;
1647
+ } catch (err) {
1648
+ try {
1649
+ this.run("ROLLBACK");
1650
+ } catch {
1651
+ }
1652
+ throw err;
1653
+ }
1654
+ }
1655
+ };
1656
+ }
1657
+ });
1658
+
1497
1659
  // src/db/postgres.ts
1498
1660
  import path2 from "path";
1499
1661
  import { fileURLToPath } from "url";
1500
- import { createRequire as createRequire2 } from "module";
1662
+ import { createRequire as createRequire3 } from "module";
1501
1663
  function moduleContext() {
1502
1664
  if (_moduleContext) return _moduleContext;
1503
1665
  const importMetaUrl = import.meta.url;
1504
1666
  if (importMetaUrl) {
1505
1667
  _moduleContext = {
1506
1668
  dir: path2.dirname(fileURLToPath(importMetaUrl)),
1507
- require: createRequire2(importMetaUrl)
1669
+ require: createRequire3(importMetaUrl)
1508
1670
  };
1509
1671
  } else {
1510
1672
  _moduleContext = { dir: __dirname, require: __require };
@@ -9693,6 +9855,9 @@ function buildAdapter(dbPath, options) {
9693
9855
  const adapterOpts = {};
9694
9856
  if (options.wal !== void 0) adapterOpts.wal = options.wal;
9695
9857
  if (options.busyTimeout !== void 0) adapterOpts.busyTimeout = options.busyTimeout;
9858
+ if (typeof globalThis.Deno !== "undefined") {
9859
+ return new DenoSqliteAdapter(sqlitePath, adapterOpts);
9860
+ }
9696
9861
  return new SQLiteAdapter(sqlitePath, adapterOpts);
9697
9862
  }
9698
9863
  function _resolveTemplateName(render) {
@@ -9712,6 +9877,7 @@ var init_lattice = __esm({
9712
9877
  init_render_cursor();
9713
9878
  init_adapter();
9714
9879
  init_sqlite();
9880
+ init_sqlite_deno();
9715
9881
  init_postgres();
9716
9882
  init_pk();
9717
9883
  init_manager();
@@ -13884,9 +14050,6 @@ async function resolveClaudeAuth(db) {
13884
14050
  const apiKey = await resolveAnthropicKey(db);
13885
14051
  return apiKey ? { apiKey } : null;
13886
14052
  }
13887
- async function hasClaudeAuth(db) {
13888
- return Boolean(await readMachineCredential(db, CLAUDE_OAUTH_KIND)) || await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY");
13889
- }
13890
14053
  async function claudeAuthKind(db) {
13891
14054
  if (await readMachineCredential(db, CLAUDE_OAUTH_KIND)) return "oauth";
13892
14055
  if (await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY")) return "key";
@@ -13914,7 +14077,6 @@ async function dispatchAssistantRoute(req, res, ctx) {
13914
14077
  hasAnthropicKey,
13915
14078
  hasOpenaiKey,
13916
14079
  hasElevenlabsKey,
13917
- hasClaudeAuth: await hasClaudeAuth(db),
13918
14080
  claudeAuthKind: await claudeAuthKind(db),
13919
14081
  hasVoiceKey: voice !== null,
13920
14082
  sttProvider: voice?.provider ?? null,
@@ -14042,13 +14204,17 @@ async function dispatchAssistantRoute(req, res, ctx) {
14042
14204
  const verifier = generatePkceVerifier();
14043
14205
  const state2 = generateState();
14044
14206
  const cookieOpts = "HttpOnly; Path=/; Max-Age=600; SameSite=Lax";
14045
- res.writeHead(302, {
14046
- Location: buildAuthorizeUrl(cfg, state2, pkceChallengeFor(verifier)),
14047
- "Set-Cookie": [
14048
- `lat_oauth_verifier=${verifier}; ${cookieOpts}`,
14049
- `lat_oauth_state=${state2}; ${cookieOpts}`
14050
- ]
14051
- });
14207
+ const setCookie = [
14208
+ `lat_oauth_verifier=${verifier}; ${cookieOpts}`,
14209
+ `lat_oauth_state=${state2}; ${cookieOpts}`
14210
+ ];
14211
+ const authorizeUrl = buildAuthorizeUrl(cfg, state2, pkceChallengeFor(verifier));
14212
+ if ((req.headers.accept ?? "").includes("application/json")) {
14213
+ res.writeHead(200, { "Content-Type": "application/json", "Set-Cookie": setCookie });
14214
+ res.end(JSON.stringify({ authorizeUrl }));
14215
+ return true;
14216
+ }
14217
+ res.writeHead(302, { Location: authorizeUrl, "Set-Cookie": setCookie });
14052
14218
  res.end();
14053
14219
  return true;
14054
14220
  }
@@ -16115,7 +16281,7 @@ var init_extract = __esm({
16115
16281
  });
16116
16282
 
16117
16283
  // src/ai/llm-client.ts
16118
- import { createRequire as createRequire4 } from "module";
16284
+ import { createRequire as createRequire5 } from "module";
16119
16285
  var DEFAULT_MODEL;
16120
16286
  var init_llm_client = __esm({
16121
16287
  "src/ai/llm-client.ts"() {
@@ -16669,7 +16835,7 @@ var init_url_safety = __esm({
16669
16835
  import { JSDOM } from "jsdom";
16670
16836
  import { Readability } from "@mozilla/readability";
16671
16837
  import { basename as basename5 } from "path";
16672
- import { createRequire as createRequire5 } from "module";
16838
+ import { createRequire as createRequire6 } from "module";
16673
16839
  async function crawlUrl(rawUrl, opts = {}) {
16674
16840
  const u2 = await assertSafeUrl(rawUrl, opts.allowPrivate ?? false);
16675
16841
  const fetchImpl = opts.fetcher ?? fetch;
@@ -16916,7 +17082,7 @@ async function renderViaPlaywright(url, timeoutMs, warnIfMissing = false) {
16916
17082
  let chromium;
16917
17083
  try {
16918
17084
  const importMetaUrl = import.meta.url;
16919
- const req = importMetaUrl ? createRequire5(importMetaUrl) : __require;
17085
+ const req = importMetaUrl ? createRequire6(importMetaUrl) : __require;
16920
17086
  const pw = req("playwright");
16921
17087
  chromium = pw.chromium;
16922
17088
  } catch {
@@ -17847,7 +18013,7 @@ var init_tools = __esm({
17847
18013
  });
17848
18014
 
17849
18015
  // src/gui/ai/chat.ts
17850
- import { createRequire as createRequire6 } from "module";
18016
+ import { createRequire as createRequire7 } from "module";
17851
18017
  function capToolResult(s2) {
17852
18018
  if (s2.length <= MAX_TOOL_RESULT_CHARS) return s2;
17853
18019
  if (s2.length > MAX_TOOL_RESULT_SKIP)
@@ -18080,7 +18246,7 @@ async function* runChat(opts) {
18080
18246
  function loadSdk() {
18081
18247
  if (!_sdk) {
18082
18248
  const importMetaUrl = import.meta.url;
18083
- const req = importMetaUrl ? createRequire6(importMetaUrl) : __require;
18249
+ const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
18084
18250
  try {
18085
18251
  _sdk = req("@anthropic-ai/sdk");
18086
18252
  } catch (err) {
@@ -58435,12 +58601,12 @@ init_postgres();
58435
58601
 
58436
58602
  // src/gui/realtime.ts
58437
58603
  import { EventEmitter } from "events";
58438
- import { createRequire as createRequire3 } from "module";
58604
+ import { createRequire as createRequire4 } from "module";
58439
58605
  var _pgModule = null;
58440
58606
  function loadPg() {
58441
58607
  if (_pgModule) return _pgModule;
58442
58608
  const importMetaUrl = import.meta.url;
58443
- const requireFromHere = importMetaUrl ? createRequire3(importMetaUrl) : (
58609
+ const requireFromHere = importMetaUrl ? createRequire4(importMetaUrl) : (
58444
58610
  // CJS fallback — Node provides `require` on every CJS module scope.
58445
58611
  __require
58446
58612
  );
@@ -61799,6 +61965,20 @@ var displayConfigJs = `
61799
61965
  });
61800
61966
  }
61801
61967
 
61968
+ // SINGLE SOURCE OF TRUTH for the assistant's Claude connection state, derived
61969
+ // from /api/assistant/config's claudeAuthKind (oauth | key | null). EVERY
61970
+ // place that shows "Connected with Claude" / opens the API-key panel / gates
61971
+ // on "the assistant has auth" MUST go through this \u2014 never re-derive from raw
61972
+ // fields, or the signals disagree (a stray "or hasAnthropicKey" once made
61973
+ // onboarding show "Connected with Claude" for an API-key-only setup while the
61974
+ // settings panel showed not-connected).
61975
+ // .oauth -> a Claude SUBSCRIPTION is connected ("Connected with Claude")
61976
+ // .any -> some working auth exists (subscription OR API key)
61977
+ function claudeAuth(cfg) {
61978
+ var kind = (cfg && cfg.claudeAuthKind) || null; // 'oauth' | 'key' | null
61979
+ return { kind: kind, oauth: kind === 'oauth', any: kind != null };
61980
+ }
61981
+
61802
61982
  // Disable a button + show an inline spinner for the duration of an
61803
61983
  // async action so a slow server round-trip can't be double-clicked.
61804
61984
  // The fn arg should return a Promise; the button is restored on settle.
@@ -62589,7 +62769,12 @@ var offlineEditQueueJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
62589
62769
  if (eventStreamClosed) return;
62590
62770
  // Unexpected drop: show the disconnect on the pill and auto-reconnect with
62591
62771
  // backoff (the server replays state + render snapshot on reconnect).
62592
- setStatusPill('cloud', 'disconnected');
62772
+ // Preserve the KNOWN mode (cloudMode is the single source of truth, set
62773
+ // from the server's realtime-state message) \u2014 never hardcode 'cloud',
62774
+ // which on a LOCAL (SQLite) workspace would flip cloudMode=true and divert
62775
+ // writes into the offline queue with a bogus "will sync when cloud
62776
+ // reconnects" toast against a workspace that has no cloud.
62777
+ setStatusPill(cloudMode ? 'cloud' : 'local', 'disconnected');
62593
62778
  scheduleEventStreamReconnect();
62594
62779
  };
62595
62780
  }
@@ -66497,7 +66682,7 @@ var rowContextJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
66497
66682
  var cfg = res[1];
66498
66683
  st.name = (id && id.display_name) || '';
66499
66684
  st.email = (id && id.email) || '';
66500
- st.connected = !!(cfg && (cfg.claudeAuthKind === 'oauth' || cfg.hasAnthropicKey));
66685
+ st.connected = claudeAuth(cfg).oauth;
66501
66686
  if (!st.wsName && st.name) st.wsName = st.name + "'s Workspace";
66502
66687
  render();
66503
66688
  });
@@ -67158,7 +67343,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
67158
67343
  '</p>' +
67159
67344
  // Connect-with-Claude is the primary path (use your subscription, no
67160
67345
  // API key). A pasted API key is demoted to an "Advanced" disclosure.
67161
- (cfg.claudeAuthKind === 'oauth'
67346
+ (claudeAuth(cfg).oauth
67162
67347
  ? '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
67163
67348
  '<span class="feed-source" style="background:var(--accent-soft);color:var(--accent)">Connected with Claude</span>' +
67164
67349
  '<button id="asst-oauth-disconnect" class="btn">Disconnect</button>' +
@@ -67180,7 +67365,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
67180
67365
  '</div>' +
67181
67366
  '<div id="connect-claude-msg" style="margin-top:6px;font-size:12px;color:var(--text-muted)"></div>' +
67182
67367
  '</div>') +
67183
- '<details style="margin-bottom:12px"' + (cfg.claudeAuthKind === 'key' ? ' open' : '') + '>' +
67368
+ '<details style="margin-bottom:12px"' + (claudeAuth(cfg).kind === 'key' ? ' open' : '') + '>' +
67184
67369
  '<summary style="cursor:pointer;font-size:12px;color:var(--text-muted)">Advanced \u2014 use an API key instead</summary>' +
67185
67370
  '<div style="margin-top:8px">' +
67186
67371
  rowHtml('asst-anthropic', 'Claude API token (chat)', !!cfg.hasAnthropicKey, 'sk-ant-\u2026') +
@@ -69172,7 +69357,7 @@ var createDatabaseWizardJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\
69172
69357
  function renderComposer() {
69173
69358
  var host = document.getElementById('rail-composer'); if (!host) return;
69174
69359
  fetchJson('/api/assistant/config').then(function (cfg) {
69175
- if (cfg && cfg.hasClaudeAuth) {
69360
+ if (claudeAuth(cfg).any) {
69176
69361
  var micHtml = cfg.hasVoiceKey
69177
69362
  ? '<button class="composer-mic" id="chat-mic" title="Record voice">\u{1F399}</button>'
69178
69363
  : '';
@@ -72097,7 +72282,7 @@ import { basename as basename11, extname as extname2, resolve as resolve11, join
72097
72282
 
72098
72283
  // src/ai/vision.ts
72099
72284
  init_llm_client();
72100
- import { createRequire as createRequire7 } from "module";
72285
+ import { createRequire as createRequire8 } from "module";
72101
72286
  import { readFile as readFile8 } from "fs/promises";
72102
72287
  var DEFAULT_PROMPT = "Describe this image for a knowledge base in 2-4 factual sentences: what it shows, any visible text, and notable details. No preamble.";
72103
72288
  var MAX_DIM = 1568;
@@ -72159,7 +72344,7 @@ function buildVisionAnthropicConfig(auth) {
72159
72344
  function defaultSender(auth) {
72160
72345
  return async (input) => {
72161
72346
  const importMetaUrl = import.meta.url;
72162
- const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
72347
+ const req = importMetaUrl ? createRequire8(importMetaUrl) : __require;
72163
72348
  const sdk = req("@anthropic-ai/sdk");
72164
72349
  const Anthropic = sdk.Anthropic ?? sdk.default;
72165
72350
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -72186,7 +72371,7 @@ function defaultSender(auth) {
72186
72371
  function defaultPdfSender(auth) {
72187
72372
  return async (input) => {
72188
72373
  const importMetaUrl = import.meta.url;
72189
- const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
72374
+ const req = importMetaUrl ? createRequire8(importMetaUrl) : __require;
72190
72375
  const sdk = req("@anthropic-ai/sdk");
72191
72376
  const Anthropic = sdk.Anthropic ?? sdk.default;
72192
72377
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -75871,6 +76056,7 @@ async function startGuiServer(options) {
75871
76056
  }
75872
76057
  const autoRender = options.autoRender ?? false;
75873
76058
  const guiVersion = options.version ?? "";
76059
+ const desktopOpenExternal = options.desktopOpenExternal;
75874
76060
  const sessionId = crypto.randomUUID();
75875
76061
  let updateService = null;
75876
76062
  let activeRef = bootConfigPath && bootOutputDir ? await openConfig(bootConfigPath, bootOutputDir, autoRender, options.realtimeWatchdogMs) : null;
@@ -76053,6 +76239,20 @@ async function startGuiServer(options) {
76053
76239
  sendJson(res, { version: guiVersion });
76054
76240
  return;
76055
76241
  }
76242
+ if (method === "GET" && pathname === "/api/desktop/open") {
76243
+ if (!desktopOpenExternal) {
76244
+ sendJson(res, { error: "not found" }, 404);
76245
+ return;
76246
+ }
76247
+ const target = new URL(req.url ?? "", "http://localhost").searchParams.get("url");
76248
+ if (!target || !/^https?:\/\//i.test(target)) {
76249
+ sendJson(res, { error: "url must be http(s)" }, 400);
76250
+ return;
76251
+ }
76252
+ desktopOpenExternal(target);
76253
+ sendJson(res, { ok: true });
76254
+ return;
76255
+ }
76056
76256
  if (method === "GET" && pathname === "/api/update/status") {
76057
76257
  sendJson(
76058
76258
  res,
@@ -76738,7 +76938,7 @@ function printHelp() {
76738
76938
  );
76739
76939
  }
76740
76940
  function getVersion() {
76741
- if (true) return "4.2.3";
76941
+ if (true) return "4.2.4";
76742
76942
  try {
76743
76943
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
76744
76944
  const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));