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/dist/index.cjs CHANGED
@@ -249,7 +249,7 @@ var init_manifest = __esm({
249
249
  import_node_path2 = require("path");
250
250
  import_node_fs2 = require("fs");
251
251
  init_writer();
252
- TEMPLATE_VERSION = 2;
252
+ TEMPLATE_VERSION = 3;
253
253
  }
254
254
  });
255
255
 
@@ -729,14 +729,177 @@ var init_sqlite = __esm({
729
729
  }
730
730
  });
731
731
 
732
+ // src/db/sqlite-deno.ts
733
+ function runtimeRequire2() {
734
+ const importMetaUrl = import_meta2.url;
735
+ return importMetaUrl ? (0, import_node_module2.createRequire)(importMetaUrl) : require;
736
+ }
737
+ function loadNodeSqlite() {
738
+ if (_ctor2) return _ctor2;
739
+ const mod = runtimeRequire2()("node:sqlite");
740
+ if (!mod.DatabaseSync) {
741
+ throw new Error(
742
+ "node:sqlite is unavailable in this runtime \u2014 cannot open the Deno SQLite adapter"
743
+ );
744
+ }
745
+ _ctor2 = mod.DatabaseSync;
746
+ return _ctor2;
747
+ }
748
+ var import_node_module2, import_meta2, _ctor2, DenoSqliteAdapter;
749
+ var init_sqlite_deno = __esm({
750
+ "src/db/sqlite-deno.ts"() {
751
+ "use strict";
752
+ import_node_module2 = require("module");
753
+ import_meta2 = {};
754
+ _ctor2 = null;
755
+ DenoSqliteAdapter = class {
756
+ dialect = "sqlite";
757
+ _db = null;
758
+ _path;
759
+ _wal;
760
+ _busyTimeout;
761
+ constructor(path3, options) {
762
+ this._path = path3;
763
+ this._wal = options?.wal ?? true;
764
+ this._busyTimeout = options?.busyTimeout ?? 5e3;
765
+ }
766
+ get db() {
767
+ if (!this._db) throw new Error("DenoSqliteAdapter: not open \u2014 call open() first");
768
+ return this._db;
769
+ }
770
+ open() {
771
+ const Ctor = loadNodeSqlite();
772
+ this._db = new Ctor(this._path);
773
+ this._db.exec(`PRAGMA busy_timeout = ${this._busyTimeout.toString()}`);
774
+ if (this._wal) {
775
+ this._db.exec("PRAGMA journal_mode = WAL");
776
+ }
777
+ }
778
+ close() {
779
+ this._db?.close();
780
+ this._db = null;
781
+ }
782
+ run(sql, params = []) {
783
+ this.db.prepare(sql).run(...params);
784
+ }
785
+ get(sql, params = []) {
786
+ return this.db.prepare(sql).get(...params);
787
+ }
788
+ all(sql, params = []) {
789
+ return this.db.prepare(sql).all(...params);
790
+ }
791
+ prepare(sql) {
792
+ const stmt = this.db.prepare(sql);
793
+ return {
794
+ run: (...params) => {
795
+ const info = stmt.run(...params);
796
+ return {
797
+ changes: Number(info.changes),
798
+ lastInsertRowid: info.lastInsertRowid
799
+ };
800
+ },
801
+ get: (...params) => stmt.get(...params),
802
+ all: (...params) => stmt.all(...params)
803
+ };
804
+ }
805
+ introspectColumns(table) {
806
+ const rows = this.all(`PRAGMA table_info("${table}")`);
807
+ return rows.map((r6) => r6.name);
808
+ }
809
+ /** Mirror of SQLiteAdapter.addColumn — SQLite ALTER quirks are binding-agnostic. */
810
+ addColumn(table, column, typeSpec) {
811
+ const upperType = typeSpec.toUpperCase();
812
+ if (upperType.includes("PRIMARY KEY")) return;
813
+ const hasNonConstantDefault = upperType.includes("CURRENT_TIMESTAMP") || /DATETIME\s*\(\s*'NOW'\s*\)/i.test(typeSpec) || upperType.includes("RANDOM()");
814
+ if (hasNonConstantDefault) {
815
+ 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();
816
+ this.run(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${safeType || "TEXT"}`);
817
+ this.run(`UPDATE "${table}" SET "${column}" = CURRENT_TIMESTAMP WHERE "${column}" IS NULL`);
818
+ } else {
819
+ this.run(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${typeSpec}`);
820
+ }
821
+ }
822
+ /**
823
+ * O(1) watch-loop change-probe — same composition as SQLiteAdapter, but
824
+ * `data_version` is read with a plain prepared statement because node:sqlite
825
+ * has no `.pragma(name, { simple: true })` scalar helper.
826
+ */
827
+ changeProbe() {
828
+ const dataVersion = this.db.prepare("PRAGMA data_version").get().data_version;
829
+ const totalChanges = this.db.prepare("SELECT total_changes() AS n").get().n;
830
+ return `${String(dataVersion)}:${String(totalChanges)}`;
831
+ }
832
+ // ── Async surface (sync under the hood; mirrors SQLiteAdapter) ──────────
833
+ // eslint-disable-next-line @typescript-eslint/require-await
834
+ async runAsync(sql, params) {
835
+ this.run(sql, params);
836
+ }
837
+ // eslint-disable-next-line @typescript-eslint/require-await
838
+ async getAsync(sql, params) {
839
+ return this.get(sql, params);
840
+ }
841
+ // eslint-disable-next-line @typescript-eslint/require-await
842
+ async allAsync(sql, params) {
843
+ return this.all(sql, params);
844
+ }
845
+ // eslint-disable-next-line @typescript-eslint/require-await
846
+ async introspectColumnsAsync(table) {
847
+ return this.introspectColumns(table);
848
+ }
849
+ // eslint-disable-next-line @typescript-eslint/require-await
850
+ async introspectAllColumns(tables) {
851
+ const map = /* @__PURE__ */ new Map();
852
+ for (const t8 of tables) {
853
+ try {
854
+ const cols = this.introspectColumns(t8);
855
+ if (cols.length > 0) map.set(t8, new Set(cols));
856
+ } catch {
857
+ }
858
+ }
859
+ return map;
860
+ }
861
+ // eslint-disable-next-line @typescript-eslint/require-await
862
+ async addColumnAsync(table, column, typeSpec) {
863
+ this.addColumn(table, column, typeSpec);
864
+ }
865
+ /** BEGIN/COMMIT around an awaited fn; ROLLBACK on throw. Mirror of SQLiteAdapter. */
866
+ async withClient(fn) {
867
+ const dbRef = this.db;
868
+ const getSync = this.get.bind(this);
869
+ const allSync = this.all.bind(this);
870
+ const tx = {
871
+ run: (sql, params) => {
872
+ const info = dbRef.prepare(sql).run(...params ?? []);
873
+ return Promise.resolve({ changes: Number(info.changes) });
874
+ },
875
+ get: (sql, params) => Promise.resolve(getSync(sql, params ?? [])),
876
+ all: (sql, params) => Promise.resolve(allSync(sql, params ?? []))
877
+ };
878
+ this.run("BEGIN");
879
+ try {
880
+ const result = await fn(tx);
881
+ this.run("COMMIT");
882
+ return result;
883
+ } catch (err) {
884
+ try {
885
+ this.run("ROLLBACK");
886
+ } catch {
887
+ }
888
+ throw err;
889
+ }
890
+ }
891
+ };
892
+ }
893
+ });
894
+
732
895
  // src/db/postgres.ts
733
896
  function moduleContext() {
734
897
  if (_moduleContext) return _moduleContext;
735
- const importMetaUrl = import_meta2.url;
898
+ const importMetaUrl = import_meta3.url;
736
899
  if (importMetaUrl) {
737
900
  _moduleContext = {
738
901
  dir: import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl)),
739
- require: (0, import_node_module2.createRequire)(importMetaUrl)
902
+ require: (0, import_node_module3.createRequire)(importMetaUrl)
740
903
  };
741
904
  } else {
742
905
  _moduleContext = { dir: __dirname, require };
@@ -994,14 +1157,14 @@ function rewriteParams(sql) {
994
1157
  function rewrite(sql) {
995
1158
  return rewriteParams(translateDialect(sql));
996
1159
  }
997
- var import_node_path4, import_node_url, import_node_module2, import_meta2, _moduleContext, SYNC_NOT_SUPPORTED_MSG, PostgresAdapter, POSTGRES_POLYFILLS;
1160
+ var import_node_path4, import_node_url, import_node_module3, import_meta3, _moduleContext, SYNC_NOT_SUPPORTED_MSG, PostgresAdapter, POSTGRES_POLYFILLS;
998
1161
  var init_postgres = __esm({
999
1162
  "src/db/postgres.ts"() {
1000
1163
  "use strict";
1001
1164
  import_node_path4 = __toESM(require("path"), 1);
1002
1165
  import_node_url = require("url");
1003
- import_node_module2 = require("module");
1004
- import_meta2 = {};
1166
+ import_node_module3 = require("module");
1167
+ import_meta3 = {};
1005
1168
  _moduleContext = null;
1006
1169
  SYNC_NOT_SUPPORTED_MSG = "PostgresAdapter: synchronous adapter methods (run/get/all/prepare/introspectColumns/addColumn) are no longer supported on Postgres as of latticesql 1.10.0. Use the async surface (runAsync/getAsync/allAsync/prepareAsync/introspectColumnsAsync/addColumnAsync/withClient) instead. Lattice core methods (Lattice.query, .insert, .update, .render, etc.) already route through the async surface \u2014 only consumer code that escapes into adapter.run/get/all directly needs migrating.";
1007
1170
  PostgresAdapter = class {
@@ -3446,6 +3609,64 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
3446
3609
  warnings: []
3447
3610
  };
3448
3611
  if (manifest === null) return result;
3612
+ if (options.removeOrphanedDirectories !== false) {
3613
+ for (const [table, entry] of Object.entries(manifest.entityContexts)) {
3614
+ if (entityContexts.has(table)) continue;
3615
+ const directoryRoot = entry.directoryRoot;
3616
+ const rootPath = (0, import_node_path5.join)(outputDir, directoryRoot);
3617
+ if (!(0, import_node_fs3.existsSync)(rootPath)) continue;
3618
+ const globalProtected = new Set(options.protectedFiles ?? []);
3619
+ for (const [slug, files] of Object.entries(entry.entities)) {
3620
+ const entityDir = (0, import_node_path5.join)(rootPath, slug);
3621
+ if (!(0, import_node_fs3.existsSync)(entityDir)) continue;
3622
+ for (const filename of entityFileNames(files)) {
3623
+ if (globalProtected.has(filename)) continue;
3624
+ const filePath = (0, import_node_path5.join)(entityDir, filename);
3625
+ if (!(0, import_node_fs3.existsSync)(filePath)) continue;
3626
+ if (!options.dryRun) (0, import_node_fs3.unlinkSync)(filePath);
3627
+ options.onOrphan?.(filePath, "file");
3628
+ result.filesRemoved.push(filePath);
3629
+ }
3630
+ let remaining;
3631
+ try {
3632
+ remaining = (0, import_node_fs3.existsSync)(entityDir) ? (0, import_node_fs3.readdirSync)(entityDir) : [];
3633
+ } catch {
3634
+ remaining = [];
3635
+ }
3636
+ if (remaining.length === 0) {
3637
+ if (!options.dryRun) {
3638
+ try {
3639
+ (0, import_node_fs3.rmdirSync)(entityDir);
3640
+ } catch {
3641
+ }
3642
+ }
3643
+ options.onOrphan?.(entityDir, "directory");
3644
+ result.directoriesRemoved.push(entityDir);
3645
+ } else {
3646
+ result.directoriesSkipped.push(entityDir);
3647
+ result.warnings.push(
3648
+ `${entityDir}: left in place (contains user files: ${remaining.join(", ")})`
3649
+ );
3650
+ }
3651
+ }
3652
+ let rootRemaining;
3653
+ try {
3654
+ rootRemaining = (0, import_node_fs3.existsSync)(rootPath) ? (0, import_node_fs3.readdirSync)(rootPath) : [];
3655
+ } catch {
3656
+ rootRemaining = [];
3657
+ }
3658
+ if (rootRemaining.length === 0) {
3659
+ if (!options.dryRun) {
3660
+ try {
3661
+ (0, import_node_fs3.rmdirSync)(rootPath);
3662
+ } catch {
3663
+ }
3664
+ }
3665
+ options.onOrphan?.(rootPath, "directory");
3666
+ result.directoriesRemoved.push(rootPath);
3667
+ }
3668
+ }
3669
+ }
3449
3670
  for (const [table, def] of entityContexts) {
3450
3671
  const entry = manifest.entityContexts[table];
3451
3672
  if (!entry) continue;
@@ -3945,7 +4166,8 @@ var init_engine = __esm({
3945
4166
  const currentSlugsByTable = /* @__PURE__ */ new Map();
3946
4167
  for (const [table, def] of entityContexts) {
3947
4168
  const rows = await this._schema.queryTable(this._adapter, table, this._schema.readRel);
3948
- const slugs = new Set(rows.map((row) => def.slug(row)));
4169
+ const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
4170
+ const slugs = new Set(_RenderEngine._disambiguateSlugs(rows, def.slug, entityPk));
3949
4171
  currentSlugsByTable.set(table, slugs);
3950
4172
  }
3951
4173
  return cleanupEntityContexts(
@@ -3994,6 +4216,71 @@ var init_engine = __esm({
3994
4216
  static _normKey(v2) {
3995
4217
  return String(v2);
3996
4218
  }
4219
+ /**
4220
+ * Sanitize and validate ONE base slug.
4221
+ *
4222
+ * Replaces non-ASCII whitespace (e.g. the macOS narrow no-break space U+202F
4223
+ * that shows up in screenshot filenames) with a regular space, strips control
4224
+ * characters, then rejects any slug that still contains a character outside the
4225
+ * allowed set (the path-traversal guard). Throws on an invalid slug — never
4226
+ * silently rewrites it.
4227
+ */
4228
+ static _sanitizeSlug(rawSlug) {
4229
+ const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
4230
+ if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
4231
+ throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
4232
+ }
4233
+ return slug;
4234
+ }
4235
+ /**
4236
+ * Disambiguate per-row slugs so two rows that produce the SAME base slug do not
4237
+ * write to (and clobber) the same directory.
4238
+ *
4239
+ * Returns one final slug per row, in the SAME order as `rows`. A base slug used
4240
+ * by exactly one row is returned unchanged (no churn for the common case). When
4241
+ * a base slug is shared by >1 row, EVERY colliding row gets a short, stable
4242
+ * suffix derived from its primary key (`<base>-<pk8>`), so the result is
4243
+ * order-independent: the same row gets the same slug on every render regardless
4244
+ * of row order. The suffix lengthens only if two rows' 8-char PK prefixes still
4245
+ * collide (e.g. shared prefix), guaranteeing uniqueness without changing the
4246
+ * common-case output. Slugs are sanitized + path-traversal-validated via
4247
+ * {@link _sanitizeSlug}; `def.slug` itself is never modified.
4248
+ */
4249
+ static _disambiguateSlugs(rows, slugFn, pkCol) {
4250
+ const baseSlugs = rows.map((row) => _RenderEngine._sanitizeSlug(slugFn(row)));
4251
+ const byBase = /* @__PURE__ */ new Map();
4252
+ for (let i6 = 0; i6 < baseSlugs.length; i6++) {
4253
+ const base = baseSlugs[i6];
4254
+ const bucket = byBase.get(base);
4255
+ if (bucket) bucket.push(i6);
4256
+ else byBase.set(base, [i6]);
4257
+ }
4258
+ const final = baseSlugs.map(() => "");
4259
+ const pkOf = (i6) => {
4260
+ const v2 = rows[i6]?.[pkCol];
4261
+ let s2;
4262
+ if (v2 == null) s2 = "";
4263
+ else if (typeof v2 === "object") s2 = JSON.stringify(v2);
4264
+ else s2 = String(v2);
4265
+ return _RenderEngine._sanitizeSlug(s2).replace(/[ /\\]/g, "");
4266
+ };
4267
+ for (const [base, indices] of byBase) {
4268
+ if (indices.length === 1) {
4269
+ final[indices[0]] = base;
4270
+ continue;
4271
+ }
4272
+ const pks = indices.map(pkOf);
4273
+ const maxLen = Math.max(...pks.map((p3) => p3.length), 1);
4274
+ let len = 8;
4275
+ while (len < maxLen && new Set(pks.map((p3) => p3.slice(0, len))).size !== pks.length) {
4276
+ len += 4;
4277
+ }
4278
+ for (let k6 = 0; k6 < indices.length; k6++) {
4279
+ final[indices[k6]] = `${base}-${pks[k6].slice(0, len)}`;
4280
+ }
4281
+ }
4282
+ return final;
4283
+ }
3997
4284
  /**
3998
4285
  * Prefetch the batchable belongsTo sources for one entity-context table.
3999
4286
  * For each (target+filters+softDelete) group, issue exactly ONE
@@ -4074,6 +4361,7 @@ var init_engine = __esm({
4074
4361
  const baseRows = await this._schema.queryTable(this._adapter, table, this._schema.readRel);
4075
4362
  const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
4076
4363
  const directoryRoot = def.directoryRoot ?? table;
4364
+ const finalSlugs = _RenderEngine._disambiguateSlugs(allRows, def.slug, entityPk);
4077
4365
  const belongsToBatches = await this._prefetchBelongsToBatches(
4078
4366
  def,
4079
4367
  allRows,
@@ -4115,11 +4403,7 @@ var init_engine = __esm({
4115
4403
  if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
4116
4404
  await new Promise((r6) => setImmediate(r6));
4117
4405
  }
4118
- const rawSlug = def.slug(entityRow);
4119
- const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
4120
- if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
4121
- throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
4122
- }
4406
+ const slug = finalSlugs[i6];
4123
4407
  const entityDir = def.directory ? (0, import_node_path6.join)(outputDir, def.directory(entityRow)) : (0, import_node_path6.join)(outputDir, directoryRoot, slug);
4124
4408
  const resolvedDir = (0, import_node_path6.resolve)(entityDir);
4125
4409
  const resolvedBase = (0, import_node_path6.resolve)(outputDir);
@@ -9807,6 +10091,9 @@ function buildAdapter(dbPath, options) {
9807
10091
  const adapterOpts = {};
9808
10092
  if (options.wal !== void 0) adapterOpts.wal = options.wal;
9809
10093
  if (options.busyTimeout !== void 0) adapterOpts.busyTimeout = options.busyTimeout;
10094
+ if (typeof globalThis.Deno !== "undefined") {
10095
+ return new DenoSqliteAdapter(sqlitePath, adapterOpts);
10096
+ }
9810
10097
  return new SQLiteAdapter(sqlitePath, adapterOpts);
9811
10098
  }
9812
10099
  function _resolveTemplateName(render) {
@@ -9828,6 +10115,7 @@ var init_lattice = __esm({
9828
10115
  import_node_fs12 = require("fs");
9829
10116
  init_adapter();
9830
10117
  init_sqlite();
10118
+ init_sqlite_deno();
9831
10119
  init_postgres();
9832
10120
  init_pk();
9833
10121
  init_manager();
@@ -52363,11 +52651,11 @@ var init_table_policy = __esm({
52363
52651
  });
52364
52652
 
52365
52653
  // src/ai/llm-client.ts
52366
- var import_node_module3, DEFAULT_MODEL, CHEAPEST_MODEL;
52654
+ var import_node_module4, DEFAULT_MODEL, CHEAPEST_MODEL;
52367
52655
  var init_llm_client = __esm({
52368
52656
  "src/ai/llm-client.ts"() {
52369
52657
  "use strict";
52370
- import_node_module3 = require("module");
52658
+ import_node_module4 = require("module");
52371
52659
  DEFAULT_MODEL = "claude-haiku-4-5";
52372
52660
  CHEAPEST_MODEL = "claude-haiku-4-5";
52373
52661
  }
@@ -52779,8 +53067,8 @@ async function sniffMime(body) {
52779
53067
  async function renderViaPlaywright(url, timeoutMs, warnIfMissing = false) {
52780
53068
  let chromium;
52781
53069
  try {
52782
- const importMetaUrl = import_meta3.url;
52783
- const req = importMetaUrl ? (0, import_node_module4.createRequire)(importMetaUrl) : require;
53070
+ const importMetaUrl = import_meta4.url;
53071
+ const req = importMetaUrl ? (0, import_node_module5.createRequire)(importMetaUrl) : require;
52784
53072
  const pw = req("playwright");
52785
53073
  chromium = pw.chromium;
52786
53074
  } catch {
@@ -52804,16 +53092,16 @@ async function renderViaPlaywright(url, timeoutMs, warnIfMissing = false) {
52804
53092
  if (browser) await browser.close().catch(() => void 0);
52805
53093
  }
52806
53094
  }
52807
- var import_jsdom, import_readability, import_node_path27, import_node_module4, import_meta3, DEFAULT_MAX_BYTES2, DEFAULT_TIMEOUT_MS, DEFAULT_UA, DOMAIN_EXTRACTORS, warnedPlaywrightMissing;
53095
+ var import_jsdom, import_readability, import_node_path27, import_node_module5, import_meta4, DEFAULT_MAX_BYTES2, DEFAULT_TIMEOUT_MS, DEFAULT_UA, DOMAIN_EXTRACTORS, warnedPlaywrightMissing;
52808
53096
  var init_crawl = __esm({
52809
53097
  "src/ai/crawl.ts"() {
52810
53098
  "use strict";
52811
53099
  import_jsdom = require("jsdom");
52812
53100
  import_readability = require("@mozilla/readability");
52813
53101
  import_node_path27 = require("path");
52814
- import_node_module4 = require("module");
53102
+ import_node_module5 = require("module");
52815
53103
  init_url_safety();
52816
- import_meta3 = {};
53104
+ import_meta4 = {};
52817
53105
  DEFAULT_MAX_BYTES2 = 25 * 1024 * 1024;
52818
53106
  DEFAULT_TIMEOUT_MS = 3e4;
52819
53107
  DEFAULT_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
@@ -53985,9 +54273,6 @@ async function resolveClaudeAuth(db) {
53985
54273
  const apiKey = await resolveAnthropicKey(db);
53986
54274
  return apiKey ? { apiKey } : null;
53987
54275
  }
53988
- async function hasClaudeAuth(db) {
53989
- return Boolean(await readMachineCredential(db, CLAUDE_OAUTH_KIND)) || await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY");
53990
- }
53991
54276
  async function claudeAuthKind(db) {
53992
54277
  if (await readMachineCredential(db, CLAUDE_OAUTH_KIND)) return "oauth";
53993
54278
  if (await hasCredential(db, "anthropic", "ANTHROPIC_API_KEY")) return "key";
@@ -54015,7 +54300,6 @@ async function dispatchAssistantRoute(req, res, ctx) {
54015
54300
  hasAnthropicKey,
54016
54301
  hasOpenaiKey,
54017
54302
  hasElevenlabsKey,
54018
- hasClaudeAuth: await hasClaudeAuth(db),
54019
54303
  claudeAuthKind: await claudeAuthKind(db),
54020
54304
  hasVoiceKey: voice !== null,
54021
54305
  sttProvider: voice?.provider ?? null,
@@ -54143,13 +54427,17 @@ async function dispatchAssistantRoute(req, res, ctx) {
54143
54427
  const verifier = generatePkceVerifier();
54144
54428
  const state2 = generateState();
54145
54429
  const cookieOpts = "HttpOnly; Path=/; Max-Age=600; SameSite=Lax";
54146
- res.writeHead(302, {
54147
- Location: buildAuthorizeUrl(cfg, state2, pkceChallengeFor(verifier)),
54148
- "Set-Cookie": [
54149
- `lat_oauth_verifier=${verifier}; ${cookieOpts}`,
54150
- `lat_oauth_state=${state2}; ${cookieOpts}`
54151
- ]
54152
- });
54430
+ const setCookie = [
54431
+ `lat_oauth_verifier=${verifier}; ${cookieOpts}`,
54432
+ `lat_oauth_state=${state2}; ${cookieOpts}`
54433
+ ];
54434
+ const authorizeUrl = buildAuthorizeUrl(cfg, state2, pkceChallengeFor(verifier));
54435
+ if ((req.headers.accept ?? "").includes("application/json")) {
54436
+ res.writeHead(200, { "Content-Type": "application/json", "Set-Cookie": setCookie });
54437
+ res.end(JSON.stringify({ authorizeUrl }));
54438
+ return true;
54439
+ }
54440
+ res.writeHead(302, { Location: authorizeUrl, "Set-Cookie": setCookie });
54153
54441
  res.end();
54154
54442
  return true;
54155
54443
  }
@@ -54725,7 +55013,7 @@ function findDocsDir() {
54725
55013
  if (_docsDir !== void 0) return _docsDir;
54726
55014
  let dir;
54727
55015
  try {
54728
- dir = (0, import_node_path29.dirname)((0, import_node_url2.fileURLToPath)(import_meta6.url));
55016
+ dir = (0, import_node_path29.dirname)((0, import_node_url2.fileURLToPath)(import_meta7.url));
54729
55017
  } catch {
54730
55018
  dir = process.cwd();
54731
55019
  }
@@ -54820,14 +55108,14 @@ function searchLatticeDocs(query, limit = 4) {
54820
55108
  }))
54821
55109
  };
54822
55110
  }
54823
- var import_node_fs27, import_node_path29, import_node_url2, import_meta6, _docsDir, MAX_SECTION_CHARS, _cache;
55111
+ var import_node_fs27, import_node_path29, import_node_url2, import_meta7, _docsDir, MAX_SECTION_CHARS, _cache;
54824
55112
  var init_lattice_docs = __esm({
54825
55113
  "src/gui/ai/lattice-docs.ts"() {
54826
55114
  "use strict";
54827
55115
  import_node_fs27 = require("fs");
54828
55116
  import_node_path29 = require("path");
54829
55117
  import_node_url2 = require("url");
54830
- import_meta6 = {};
55118
+ import_meta7 = {};
54831
55119
  MAX_SECTION_CHARS = 2400;
54832
55120
  _cache = /* @__PURE__ */ new Map();
54833
55121
  }
@@ -57694,8 +57982,8 @@ async function* runChat(opts) {
57694
57982
  }
57695
57983
  function loadSdk() {
57696
57984
  if (!_sdk) {
57697
- const importMetaUrl = import_meta7.url;
57698
- const req = importMetaUrl ? (0, import_node_module7.createRequire)(importMetaUrl) : require;
57985
+ const importMetaUrl = import_meta8.url;
57986
+ const req = importMetaUrl ? (0, import_node_module8.createRequire)(importMetaUrl) : require;
57699
57987
  try {
57700
57988
  _sdk = req("@anthropic-ai/sdk");
57701
57989
  } catch (err) {
@@ -57752,15 +58040,15 @@ function createAnthropicClient(auth) {
57752
58040
  }
57753
58041
  };
57754
58042
  }
57755
- var import_node_module7, import_meta7, DEFAULT_MODEL2, MAX_TOOL_LOOPS, MAX_CONSECUTIVE_TOOL_FAILURES, MAX_TOKENS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_RESULT_SKIP, MAX_TOOL_INPUT_CHARS, LIVE_TOOL_RESULT_CHARS, MAX_CONTEXT_RECOVERY_TRIMS, TRIMMED_PLACEHOLDER, BASE_SYSTEM_PROMPT, LOCAL_GUI_RECORD_RE, _sdk;
58043
+ var import_node_module8, import_meta8, DEFAULT_MODEL2, MAX_TOOL_LOOPS, MAX_CONSECUTIVE_TOOL_FAILURES, MAX_TOKENS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_RESULT_SKIP, MAX_TOOL_INPUT_CHARS, LIVE_TOOL_RESULT_CHARS, MAX_CONTEXT_RECOVERY_TRIMS, TRIMMED_PLACEHOLDER, BASE_SYSTEM_PROMPT, LOCAL_GUI_RECORD_RE, _sdk;
57756
58044
  var init_chat = __esm({
57757
58045
  "src/gui/ai/chat.ts"() {
57758
58046
  "use strict";
57759
- import_node_module7 = require("module");
58047
+ import_node_module8 = require("module");
57760
58048
  init_dispatch();
57761
58049
  init_tools();
57762
58050
  init_column_descriptions();
57763
- import_meta7 = {};
58051
+ import_meta8 = {};
57764
58052
  DEFAULT_MODEL2 = "claude-haiku-4-5";
57765
58053
  MAX_TOOL_LOOPS = 16;
57766
58054
  MAX_CONSECUTIVE_TOOL_FAILURES = 3;
@@ -57806,6 +58094,7 @@ __export(index_exports, {
57806
58094
  DEFAULT_ENTRY_TYPES: () => DEFAULT_ENTRY_TYPES,
57807
58095
  DEFAULT_MAX_NODES: () => DEFAULT_MAX_NODES,
57808
58096
  DEFAULT_TYPE_ALIASES: () => DEFAULT_TYPE_ALIASES,
58097
+ DenoSqliteAdapter: () => DenoSqliteAdapter,
57809
58098
  EMBEDDINGS_TABLE: () => EMBEDDINGS_TABLE,
57810
58099
  EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
57811
58100
  EmbeddingScanTooLargeError: () => EmbeddingScanTooLargeError,
@@ -58564,6 +58853,7 @@ async function autoUpdate(opts) {
58564
58853
 
58565
58854
  // src/index.ts
58566
58855
  init_sqlite();
58856
+ init_sqlite_deno();
58567
58857
  init_postgres();
58568
58858
  init_computed();
58569
58859
  init_governance();
@@ -59886,10 +60176,10 @@ function isBetter(next, prev) {
59886
60176
  }
59887
60177
 
59888
60178
  // src/ai/vision.ts
59889
- var import_node_module5 = require("module");
60179
+ var import_node_module6 = require("module");
59890
60180
  var import_promises7 = require("fs/promises");
59891
60181
  init_llm_client();
59892
- var import_meta4 = {};
60182
+ var import_meta5 = {};
59893
60183
  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.";
59894
60184
  var MAX_DIM = 1568;
59895
60185
  async function describeImage(auth, path3, opts = {}) {
@@ -59949,8 +60239,8 @@ function buildVisionAnthropicConfig(auth) {
59949
60239
  }
59950
60240
  function defaultSender(auth) {
59951
60241
  return async (input) => {
59952
- const importMetaUrl = import_meta4.url;
59953
- const req = importMetaUrl ? (0, import_node_module5.createRequire)(importMetaUrl) : require;
60242
+ const importMetaUrl = import_meta5.url;
60243
+ const req = importMetaUrl ? (0, import_node_module6.createRequire)(importMetaUrl) : require;
59954
60244
  const sdk = req("@anthropic-ai/sdk");
59955
60245
  const Anthropic = sdk.Anthropic ?? sdk.default;
59956
60246
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -59976,8 +60266,8 @@ function defaultSender(auth) {
59976
60266
  }
59977
60267
  function defaultPdfSender(auth) {
59978
60268
  return async (input) => {
59979
- const importMetaUrl = import_meta4.url;
59980
- const req = importMetaUrl ? (0, import_node_module5.createRequire)(importMetaUrl) : require;
60269
+ const importMetaUrl = import_meta5.url;
60270
+ const req = importMetaUrl ? (0, import_node_module6.createRequire)(importMetaUrl) : require;
59981
60271
  const sdk = req("@anthropic-ai/sdk");
59982
60272
  const Anthropic = sdk.Anthropic ?? sdk.default;
59983
60273
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
@@ -60288,13 +60578,13 @@ init_postgres();
60288
60578
 
60289
60579
  // src/gui/realtime.ts
60290
60580
  var import_node_events = require("events");
60291
- var import_node_module6 = require("module");
60292
- var import_meta5 = {};
60581
+ var import_node_module7 = require("module");
60582
+ var import_meta6 = {};
60293
60583
  var _pgModule = null;
60294
60584
  function loadPg() {
60295
60585
  if (_pgModule) return _pgModule;
60296
- const importMetaUrl = import_meta5.url;
60297
- const requireFromHere = importMetaUrl ? (0, import_node_module6.createRequire)(importMetaUrl) : (
60586
+ const importMetaUrl = import_meta6.url;
60587
+ const requireFromHere = importMetaUrl ? (0, import_node_module7.createRequire)(importMetaUrl) : (
60298
60588
  // CJS fallback — Node provides `require` on every CJS module scope.
60299
60589
  require
60300
60590
  );
@@ -63653,6 +63943,20 @@ var displayConfigJs = `
63653
63943
  });
63654
63944
  }
63655
63945
 
63946
+ // SINGLE SOURCE OF TRUTH for the assistant's Claude connection state, derived
63947
+ // from /api/assistant/config's claudeAuthKind (oauth | key | null). EVERY
63948
+ // place that shows "Connected with Claude" / opens the API-key panel / gates
63949
+ // on "the assistant has auth" MUST go through this \u2014 never re-derive from raw
63950
+ // fields, or the signals disagree (a stray "or hasAnthropicKey" once made
63951
+ // onboarding show "Connected with Claude" for an API-key-only setup while the
63952
+ // settings panel showed not-connected).
63953
+ // .oauth -> a Claude SUBSCRIPTION is connected ("Connected with Claude")
63954
+ // .any -> some working auth exists (subscription OR API key)
63955
+ function claudeAuth(cfg) {
63956
+ var kind = (cfg && cfg.claudeAuthKind) || null; // 'oauth' | 'key' | null
63957
+ return { kind: kind, oauth: kind === 'oauth', any: kind != null };
63958
+ }
63959
+
63656
63960
  // Disable a button + show an inline spinner for the duration of an
63657
63961
  // async action so a slow server round-trip can't be double-clicked.
63658
63962
  // The fn arg should return a Promise; the button is restored on settle.
@@ -64443,7 +64747,12 @@ var offlineEditQueueJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
64443
64747
  if (eventStreamClosed) return;
64444
64748
  // Unexpected drop: show the disconnect on the pill and auto-reconnect with
64445
64749
  // backoff (the server replays state + render snapshot on reconnect).
64446
- setStatusPill('cloud', 'disconnected');
64750
+ // Preserve the KNOWN mode (cloudMode is the single source of truth, set
64751
+ // from the server's realtime-state message) \u2014 never hardcode 'cloud',
64752
+ // which on a LOCAL (SQLite) workspace would flip cloudMode=true and divert
64753
+ // writes into the offline queue with a bogus "will sync when cloud
64754
+ // reconnects" toast against a workspace that has no cloud.
64755
+ setStatusPill(cloudMode ? 'cloud' : 'local', 'disconnected');
64447
64756
  scheduleEventStreamReconnect();
64448
64757
  };
64449
64758
  }
@@ -68351,7 +68660,7 @@ var rowContextJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
68351
68660
  var cfg = res[1];
68352
68661
  st.name = (id && id.display_name) || '';
68353
68662
  st.email = (id && id.email) || '';
68354
- st.connected = !!(cfg && (cfg.claudeAuthKind === 'oauth' || cfg.hasAnthropicKey));
68663
+ st.connected = claudeAuth(cfg).oauth;
68355
68664
  if (!st.wsName && st.name) st.wsName = st.name + "'s Workspace";
68356
68665
  render();
68357
68666
  });
@@ -69012,7 +69321,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
69012
69321
  '</p>' +
69013
69322
  // Connect-with-Claude is the primary path (use your subscription, no
69014
69323
  // API key). A pasted API key is demoted to an "Advanced" disclosure.
69015
- (cfg.claudeAuthKind === 'oauth'
69324
+ (claudeAuth(cfg).oauth
69016
69325
  ? '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
69017
69326
  '<span class="feed-source" style="background:var(--accent-soft);color:var(--accent)">Connected with Claude</span>' +
69018
69327
  '<button id="asst-oauth-disconnect" class="btn">Disconnect</button>' +
@@ -69034,7 +69343,7 @@ var dataModelJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
69034
69343
  '</div>' +
69035
69344
  '<div id="connect-claude-msg" style="margin-top:6px;font-size:12px;color:var(--text-muted)"></div>' +
69036
69345
  '</div>') +
69037
- '<details style="margin-bottom:12px"' + (cfg.claudeAuthKind === 'key' ? ' open' : '') + '>' +
69346
+ '<details style="margin-bottom:12px"' + (claudeAuth(cfg).kind === 'key' ? ' open' : '') + '>' +
69038
69347
  '<summary style="cursor:pointer;font-size:12px;color:var(--text-muted)">Advanced \u2014 use an API key instead</summary>' +
69039
69348
  '<div style="margin-top:8px">' +
69040
69349
  rowHtml('asst-anthropic', 'Claude API token (chat)', !!cfg.hasAnthropicKey, 'sk-ant-\u2026') +
@@ -71026,7 +71335,7 @@ var createDatabaseWizardJs = ` // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\
71026
71335
  function renderComposer() {
71027
71336
  var host = document.getElementById('rail-composer'); if (!host) return;
71028
71337
  fetchJson('/api/assistant/config').then(function (cfg) {
71029
- if (cfg && cfg.hasClaudeAuth) {
71338
+ if (claudeAuth(cfg).any) {
71030
71339
  var micHtml = cfg.hasVoiceKey
71031
71340
  ? '<button class="composer-mic" id="chat-mic" title="Record voice">\u{1F399}</button>'
71032
71341
  : '';
@@ -77370,6 +77679,7 @@ async function startGuiServer(options) {
77370
77679
  }
77371
77680
  const autoRender = options.autoRender ?? false;
77372
77681
  const guiVersion = options.version ?? "";
77682
+ const desktopOpenExternal = options.desktopOpenExternal;
77373
77683
  const sessionId = crypto.randomUUID();
77374
77684
  let updateService = null;
77375
77685
  let activeRef = bootConfigPath && bootOutputDir ? await openConfig(bootConfigPath, bootOutputDir, autoRender, options.realtimeWatchdogMs) : null;
@@ -77552,6 +77862,20 @@ async function startGuiServer(options) {
77552
77862
  sendJson(res, { version: guiVersion });
77553
77863
  return;
77554
77864
  }
77865
+ if (method === "GET" && pathname === "/api/desktop/open") {
77866
+ if (!desktopOpenExternal) {
77867
+ sendJson(res, { error: "not found" }, 404);
77868
+ return;
77869
+ }
77870
+ const target = new URL(req.url ?? "", "http://localhost").searchParams.get("url");
77871
+ if (!target || !/^https?:\/\//i.test(target)) {
77872
+ sendJson(res, { error: "url must be http(s)" }, 400);
77873
+ return;
77874
+ }
77875
+ desktopOpenExternal(target);
77876
+ sendJson(res, { ok: true });
77877
+ return;
77878
+ }
77555
77879
  if (method === "GET" && pathname === "/api/update/status") {
77556
77880
  sendJson(
77557
77881
  res,
@@ -78120,6 +78444,7 @@ var FileSourceKeyStore = class {
78120
78444
  DEFAULT_ENTRY_TYPES,
78121
78445
  DEFAULT_MAX_NODES,
78122
78446
  DEFAULT_TYPE_ALIASES,
78447
+ DenoSqliteAdapter,
78123
78448
  EMBEDDINGS_TABLE,
78124
78449
  EmbeddingDimensionMismatchError,
78125
78450
  EmbeddingScanTooLargeError,