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 +2 -0
- package/dist/cli.js +357 -36
- package/dist/desktop-entry.js +76390 -0
- package/dist/index.cjs +378 -53
- package/dist/index.d.cts +76 -1
- package/dist/index.d.ts +76 -1
- package/dist/index.js +358 -35
- package/docs/desktop.md +75 -0
- package/package.json +6 -1
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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.
|
|
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"));
|