latticesql 4.2.2 → 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
@@ -1015,7 +1015,7 @@ var init_manifest = __esm({
1015
1015
  "src/lifecycle/manifest.ts"() {
1016
1016
  "use strict";
1017
1017
  init_writer();
1018
- TEMPLATE_VERSION = 2;
1018
+ TEMPLATE_VERSION = 3;
1019
1019
  }
1020
1020
  });
1021
1021
 
@@ -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 };
@@ -4208,6 +4370,64 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
4208
4370
  warnings: []
4209
4371
  };
4210
4372
  if (manifest === null) return result;
4373
+ if (options.removeOrphanedDirectories !== false) {
4374
+ for (const [table, entry] of Object.entries(manifest.entityContexts)) {
4375
+ if (entityContexts.has(table)) continue;
4376
+ const directoryRoot = entry.directoryRoot;
4377
+ const rootPath = join6(outputDir, directoryRoot);
4378
+ if (!existsSync6(rootPath)) continue;
4379
+ const globalProtected = new Set(options.protectedFiles ?? []);
4380
+ for (const [slug, files] of Object.entries(entry.entities)) {
4381
+ const entityDir = join6(rootPath, slug);
4382
+ if (!existsSync6(entityDir)) continue;
4383
+ for (const filename of entityFileNames(files)) {
4384
+ if (globalProtected.has(filename)) continue;
4385
+ const filePath = join6(entityDir, filename);
4386
+ if (!existsSync6(filePath)) continue;
4387
+ if (!options.dryRun) unlinkSync3(filePath);
4388
+ options.onOrphan?.(filePath, "file");
4389
+ result.filesRemoved.push(filePath);
4390
+ }
4391
+ let remaining;
4392
+ try {
4393
+ remaining = existsSync6(entityDir) ? readdirSync2(entityDir) : [];
4394
+ } catch {
4395
+ remaining = [];
4396
+ }
4397
+ if (remaining.length === 0) {
4398
+ if (!options.dryRun) {
4399
+ try {
4400
+ rmdirSync(entityDir);
4401
+ } catch {
4402
+ }
4403
+ }
4404
+ options.onOrphan?.(entityDir, "directory");
4405
+ result.directoriesRemoved.push(entityDir);
4406
+ } else {
4407
+ result.directoriesSkipped.push(entityDir);
4408
+ result.warnings.push(
4409
+ `${entityDir}: left in place (contains user files: ${remaining.join(", ")})`
4410
+ );
4411
+ }
4412
+ }
4413
+ let rootRemaining;
4414
+ try {
4415
+ rootRemaining = existsSync6(rootPath) ? readdirSync2(rootPath) : [];
4416
+ } catch {
4417
+ rootRemaining = [];
4418
+ }
4419
+ if (rootRemaining.length === 0) {
4420
+ if (!options.dryRun) {
4421
+ try {
4422
+ rmdirSync(rootPath);
4423
+ } catch {
4424
+ }
4425
+ }
4426
+ options.onOrphan?.(rootPath, "directory");
4427
+ result.directoriesRemoved.push(rootPath);
4428
+ }
4429
+ }
4430
+ }
4211
4431
  for (const [table, def] of entityContexts) {
4212
4432
  const entry = manifest.entityContexts[table];
4213
4433
  if (!entry) continue;
@@ -4704,7 +4924,8 @@ var init_engine = __esm({
4704
4924
  const currentSlugsByTable = /* @__PURE__ */ new Map();
4705
4925
  for (const [table, def] of entityContexts) {
4706
4926
  const rows = await this._schema.queryTable(this._adapter, table, this._schema.readRel);
4707
- const slugs = new Set(rows.map((row) => def.slug(row)));
4927
+ const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
4928
+ const slugs = new Set(_RenderEngine._disambiguateSlugs(rows, def.slug, entityPk));
4708
4929
  currentSlugsByTable.set(table, slugs);
4709
4930
  }
4710
4931
  return cleanupEntityContexts(
@@ -4753,6 +4974,71 @@ var init_engine = __esm({
4753
4974
  static _normKey(v2) {
4754
4975
  return String(v2);
4755
4976
  }
4977
+ /**
4978
+ * Sanitize and validate ONE base slug.
4979
+ *
4980
+ * Replaces non-ASCII whitespace (e.g. the macOS narrow no-break space U+202F
4981
+ * that shows up in screenshot filenames) with a regular space, strips control
4982
+ * characters, then rejects any slug that still contains a character outside the
4983
+ * allowed set (the path-traversal guard). Throws on an invalid slug — never
4984
+ * silently rewrites it.
4985
+ */
4986
+ static _sanitizeSlug(rawSlug) {
4987
+ const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
4988
+ if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
4989
+ throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
4990
+ }
4991
+ return slug;
4992
+ }
4993
+ /**
4994
+ * Disambiguate per-row slugs so two rows that produce the SAME base slug do not
4995
+ * write to (and clobber) the same directory.
4996
+ *
4997
+ * Returns one final slug per row, in the SAME order as `rows`. A base slug used
4998
+ * by exactly one row is returned unchanged (no churn for the common case). When
4999
+ * a base slug is shared by >1 row, EVERY colliding row gets a short, stable
5000
+ * suffix derived from its primary key (`<base>-<pk8>`), so the result is
5001
+ * order-independent: the same row gets the same slug on every render regardless
5002
+ * of row order. The suffix lengthens only if two rows' 8-char PK prefixes still
5003
+ * collide (e.g. shared prefix), guaranteeing uniqueness without changing the
5004
+ * common-case output. Slugs are sanitized + path-traversal-validated via
5005
+ * {@link _sanitizeSlug}; `def.slug` itself is never modified.
5006
+ */
5007
+ static _disambiguateSlugs(rows, slugFn, pkCol) {
5008
+ const baseSlugs = rows.map((row) => _RenderEngine._sanitizeSlug(slugFn(row)));
5009
+ const byBase = /* @__PURE__ */ new Map();
5010
+ for (let i6 = 0; i6 < baseSlugs.length; i6++) {
5011
+ const base = baseSlugs[i6];
5012
+ const bucket = byBase.get(base);
5013
+ if (bucket) bucket.push(i6);
5014
+ else byBase.set(base, [i6]);
5015
+ }
5016
+ const final = baseSlugs.map(() => "");
5017
+ const pkOf = (i6) => {
5018
+ const v2 = rows[i6]?.[pkCol];
5019
+ let s2;
5020
+ if (v2 == null) s2 = "";
5021
+ else if (typeof v2 === "object") s2 = JSON.stringify(v2);
5022
+ else s2 = String(v2);
5023
+ return _RenderEngine._sanitizeSlug(s2).replace(/[ /\\]/g, "");
5024
+ };
5025
+ for (const [base, indices] of byBase) {
5026
+ if (indices.length === 1) {
5027
+ final[indices[0]] = base;
5028
+ continue;
5029
+ }
5030
+ const pks = indices.map(pkOf);
5031
+ const maxLen = Math.max(...pks.map((p3) => p3.length), 1);
5032
+ let len = 8;
5033
+ while (len < maxLen && new Set(pks.map((p3) => p3.slice(0, len))).size !== pks.length) {
5034
+ len += 4;
5035
+ }
5036
+ for (let k6 = 0; k6 < indices.length; k6++) {
5037
+ final[indices[k6]] = `${base}-${pks[k6].slice(0, len)}`;
5038
+ }
5039
+ }
5040
+ return final;
5041
+ }
4756
5042
  /**
4757
5043
  * Prefetch the batchable belongsTo sources for one entity-context table.
4758
5044
  * For each (target+filters+softDelete) group, issue exactly ONE
@@ -4833,6 +5119,7 @@ var init_engine = __esm({
4833
5119
  const baseRows = await this._schema.queryTable(this._adapter, table, this._schema.readRel);
4834
5120
  const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
4835
5121
  const directoryRoot = def.directoryRoot ?? table;
5122
+ const finalSlugs = _RenderEngine._disambiguateSlugs(allRows, def.slug, entityPk);
4836
5123
  const belongsToBatches = await this._prefetchBelongsToBatches(
4837
5124
  def,
4838
5125
  allRows,
@@ -4874,11 +5161,7 @@ var init_engine = __esm({
4874
5161
  if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
4875
5162
  await new Promise((r6) => setImmediate(r6));
4876
5163
  }
4877
- const rawSlug = def.slug(entityRow);
4878
- const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
4879
- if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
4880
- throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
4881
- }
5164
+ const slug = finalSlugs[i6];
4882
5165
  const entityDir = def.directory ? join7(outputDir, def.directory(entityRow)) : join7(outputDir, directoryRoot, slug);
4883
5166
  const resolvedDir = resolve3(entityDir);
4884
5167
  const resolvedBase = resolve3(outputDir);
@@ -9572,6 +9855,9 @@ function buildAdapter(dbPath, options) {
9572
9855
  const adapterOpts = {};
9573
9856
  if (options.wal !== void 0) adapterOpts.wal = options.wal;
9574
9857
  if (options.busyTimeout !== void 0) adapterOpts.busyTimeout = options.busyTimeout;
9858
+ if (typeof globalThis.Deno !== "undefined") {
9859
+ return new DenoSqliteAdapter(sqlitePath, adapterOpts);
9860
+ }
9575
9861
  return new SQLiteAdapter(sqlitePath, adapterOpts);
9576
9862
  }
9577
9863
  function _resolveTemplateName(render) {
@@ -9591,6 +9877,7 @@ var init_lattice = __esm({
9591
9877
  init_render_cursor();
9592
9878
  init_adapter();
9593
9879
  init_sqlite();
9880
+ init_sqlite_deno();
9594
9881
  init_postgres();
9595
9882
  init_pk();
9596
9883
  init_manager();
@@ -13763,9 +14050,6 @@ async function resolveClaudeAuth(db) {
13763
14050
  const apiKey = await resolveAnthropicKey(db);
13764
14051
  return apiKey ? { apiKey } : null;
13765
14052
  }
13766
- async function hasClaudeAuth(db) {
13767
- return Boolean(await readMachineCredential(db, CLAUDE_OAUTH_KIND)) || await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY");
13768
- }
13769
14053
  async function claudeAuthKind(db) {
13770
14054
  if (await readMachineCredential(db, CLAUDE_OAUTH_KIND)) return "oauth";
13771
14055
  if (await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY")) return "key";
@@ -13793,7 +14077,6 @@ async function dispatchAssistantRoute(req, res, ctx) {
13793
14077
  hasAnthropicKey,
13794
14078
  hasOpenaiKey,
13795
14079
  hasElevenlabsKey,
13796
- hasClaudeAuth: await hasClaudeAuth(db),
13797
14080
  claudeAuthKind: await claudeAuthKind(db),
13798
14081
  hasVoiceKey: voice !== null,
13799
14082
  sttProvider: voice?.provider ?? null,
@@ -13921,13 +14204,17 @@ async function dispatchAssistantRoute(req, res, ctx) {
13921
14204
  const verifier = generatePkceVerifier();
13922
14205
  const state2 = generateState();
13923
14206
  const cookieOpts = "HttpOnly; Path=/; Max-Age=600; SameSite=Lax";
13924
- res.writeHead(302, {
13925
- Location: buildAuthorizeUrl(cfg, state2, pkceChallengeFor(verifier)),
13926
- "Set-Cookie": [
13927
- `lat_oauth_verifier=${verifier}; ${cookieOpts}`,
13928
- `lat_oauth_state=${state2}; ${cookieOpts}`
13929
- ]
13930
- });
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 });
13931
14218
  res.end();
13932
14219
  return true;
13933
14220
  }
@@ -15994,7 +16281,7 @@ var init_extract = __esm({
15994
16281
  });
15995
16282
 
15996
16283
  // src/ai/llm-client.ts
15997
- import { createRequire as createRequire4 } from "module";
16284
+ import { createRequire as createRequire5 } from "module";
15998
16285
  var DEFAULT_MODEL;
15999
16286
  var init_llm_client = __esm({
16000
16287
  "src/ai/llm-client.ts"() {
@@ -16548,7 +16835,7 @@ var init_url_safety = __esm({
16548
16835
  import { JSDOM } from "jsdom";
16549
16836
  import { Readability } from "@mozilla/readability";
16550
16837
  import { basename as basename5 } from "path";
16551
- import { createRequire as createRequire5 } from "module";
16838
+ import { createRequire as createRequire6 } from "module";
16552
16839
  async function crawlUrl(rawUrl, opts = {}) {
16553
16840
  const u2 = await assertSafeUrl(rawUrl, opts.allowPrivate ?? false);
16554
16841
  const fetchImpl = opts.fetcher ?? fetch;
@@ -16795,7 +17082,7 @@ async function renderViaPlaywright(url, timeoutMs, warnIfMissing = false) {
16795
17082
  let chromium;
16796
17083
  try {
16797
17084
  const importMetaUrl = import.meta.url;
16798
- const req = importMetaUrl ? createRequire5(importMetaUrl) : __require;
17085
+ const req = importMetaUrl ? createRequire6(importMetaUrl) : __require;
16799
17086
  const pw = req("playwright");
16800
17087
  chromium = pw.chromium;
16801
17088
  } catch {
@@ -17726,7 +18013,7 @@ var init_tools = __esm({
17726
18013
  });
17727
18014
 
17728
18015
  // src/gui/ai/chat.ts
17729
- import { createRequire as createRequire6 } from "module";
18016
+ import { createRequire as createRequire7 } from "module";
17730
18017
  function capToolResult(s2) {
17731
18018
  if (s2.length <= MAX_TOOL_RESULT_CHARS) return s2;
17732
18019
  if (s2.length > MAX_TOOL_RESULT_SKIP)
@@ -17959,7 +18246,7 @@ async function* runChat(opts) {
17959
18246
  function loadSdk() {
17960
18247
  if (!_sdk) {
17961
18248
  const importMetaUrl = import.meta.url;
17962
- const req = importMetaUrl ? createRequire6(importMetaUrl) : __require;
18249
+ const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
17963
18250
  try {
17964
18251
  _sdk = req("@anthropic-ai/sdk");
17965
18252
  } catch (err) {
@@ -58314,12 +58601,12 @@ init_postgres();
58314
58601
 
58315
58602
  // src/gui/realtime.ts
58316
58603
  import { EventEmitter } from "events";
58317
- import { createRequire as createRequire3 } from "module";
58604
+ import { createRequire as createRequire4 } from "module";
58318
58605
  var _pgModule = null;
58319
58606
  function loadPg() {
58320
58607
  if (_pgModule) return _pgModule;
58321
58608
  const importMetaUrl = import.meta.url;
58322
- const requireFromHere = importMetaUrl ? createRequire3(importMetaUrl) : (
58609
+ const requireFromHere = importMetaUrl ? createRequire4(importMetaUrl) : (
58323
58610
  // CJS fallback — Node provides `require` on every CJS module scope.
58324
58611
  __require
58325
58612
  );
@@ -61678,6 +61965,20 @@ var displayConfigJs = `
61678
61965
  });
61679
61966
  }
61680
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
+
61681
61982
  // Disable a button + show an inline spinner for the duration of an
61682
61983
  // async action so a slow server round-trip can't be double-clicked.
61683
61984
  // The fn arg should return a Promise; the button is restored on settle.
@@ -62468,7 +62769,12 @@ var offlineEditQueueJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
62468
62769
  if (eventStreamClosed) return;
62469
62770
  // Unexpected drop: show the disconnect on the pill and auto-reconnect with
62470
62771
  // backoff (the server replays state + render snapshot on reconnect).
62471
- 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');
62472
62778
  scheduleEventStreamReconnect();
62473
62779
  };
62474
62780
  }
@@ -66376,7 +66682,7 @@ var rowContextJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
66376
66682
  var cfg = res[1];
66377
66683
  st.name = (id && id.display_name) || '';
66378
66684
  st.email = (id && id.email) || '';
66379
- st.connected = !!(cfg && (cfg.claudeAuthKind === 'oauth' || cfg.hasAnthropicKey));
66685
+ st.connected = claudeAuth(cfg).oauth;
66380
66686
  if (!st.wsName && st.name) st.wsName = st.name + "'s Workspace";
66381
66687
  render();
66382
66688
  });
@@ -67037,7 +67343,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
67037
67343
  '</p>' +
67038
67344
  // Connect-with-Claude is the primary path (use your subscription, no
67039
67345
  // API key). A pasted API key is demoted to an "Advanced" disclosure.
67040
- (cfg.claudeAuthKind === 'oauth'
67346
+ (claudeAuth(cfg).oauth
67041
67347
  ? '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
67042
67348
  '<span class="feed-source" style="background:var(--accent-soft);color:var(--accent)">Connected with Claude</span>' +
67043
67349
  '<button id="asst-oauth-disconnect" class="btn">Disconnect</button>' +
@@ -67059,7 +67365,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
67059
67365
  '</div>' +
67060
67366
  '<div id="connect-claude-msg" style="margin-top:6px;font-size:12px;color:var(--text-muted)"></div>' +
67061
67367
  '</div>') +
67062
- '<details style="margin-bottom:12px"' + (cfg.claudeAuthKind === 'key' ? ' open' : '') + '>' +
67368
+ '<details style="margin-bottom:12px"' + (claudeAuth(cfg).kind === 'key' ? ' open' : '') + '>' +
67063
67369
  '<summary style="cursor:pointer;font-size:12px;color:var(--text-muted)">Advanced \u2014 use an API key instead</summary>' +
67064
67370
  '<div style="margin-top:8px">' +
67065
67371
  rowHtml('asst-anthropic', 'Claude API token (chat)', !!cfg.hasAnthropicKey, 'sk-ant-\u2026') +
@@ -69051,7 +69357,7 @@ var createDatabaseWizardJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\
69051
69357
  function renderComposer() {
69052
69358
  var host = document.getElementById('rail-composer'); if (!host) return;
69053
69359
  fetchJson('/api/assistant/config').then(function (cfg) {
69054
- if (cfg && cfg.hasClaudeAuth) {
69360
+ if (claudeAuth(cfg).any) {
69055
69361
  var micHtml = cfg.hasVoiceKey
69056
69362
  ? '<button class="composer-mic" id="chat-mic" title="Record voice">\u{1F399}</button>'
69057
69363
  : '';
@@ -71976,7 +72282,7 @@ import { basename as basename11, extname as extname2, resolve as resolve11, join
71976
72282
 
71977
72283
  // src/ai/vision.ts
71978
72284
  init_llm_client();
71979
- import { createRequire as createRequire7 } from "module";
72285
+ import { createRequire as createRequire8 } from "module";
71980
72286
  import { readFile as readFile8 } from "fs/promises";
71981
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.";
71982
72288
  var MAX_DIM = 1568;
@@ -72038,7 +72344,7 @@ function buildVisionAnthropicConfig(auth) {
72038
72344
  function defaultSender(auth) {
72039
72345
  return async (input) => {
72040
72346
  const importMetaUrl = import.meta.url;
72041
- const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
72347
+ const req = importMetaUrl ? createRequire8(importMetaUrl) : __require;
72042
72348
  const sdk = req("@anthropic-ai/sdk");
72043
72349
  const Anthropic = sdk.Anthropic ?? sdk.default;
72044
72350
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -72065,7 +72371,7 @@ function defaultSender(auth) {
72065
72371
  function defaultPdfSender(auth) {
72066
72372
  return async (input) => {
72067
72373
  const importMetaUrl = import.meta.url;
72068
- const req = importMetaUrl ? createRequire7(importMetaUrl) : __require;
72374
+ const req = importMetaUrl ? createRequire8(importMetaUrl) : __require;
72069
72375
  const sdk = req("@anthropic-ai/sdk");
72070
72376
  const Anthropic = sdk.Anthropic ?? sdk.default;
72071
72377
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -75750,6 +76056,7 @@ async function startGuiServer(options) {
75750
76056
  }
75751
76057
  const autoRender = options.autoRender ?? false;
75752
76058
  const guiVersion = options.version ?? "";
76059
+ const desktopOpenExternal = options.desktopOpenExternal;
75753
76060
  const sessionId = crypto.randomUUID();
75754
76061
  let updateService = null;
75755
76062
  let activeRef = bootConfigPath && bootOutputDir ? await openConfig(bootConfigPath, bootOutputDir, autoRender, options.realtimeWatchdogMs) : null;
@@ -75932,6 +76239,20 @@ async function startGuiServer(options) {
75932
76239
  sendJson(res, { version: guiVersion });
75933
76240
  return;
75934
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
+ }
75935
76256
  if (method === "GET" && pathname === "/api/update/status") {
75936
76257
  sendJson(
75937
76258
  res,
@@ -76617,7 +76938,7 @@ function printHelp() {
76617
76938
  );
76618
76939
  }
76619
76940
  function getVersion() {
76620
- if (true) return "4.2.2";
76941
+ if (true) return "4.2.4";
76621
76942
  try {
76622
76943
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
76623
76944
  const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));