latticesql 2.2.1 → 2.2.3

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
@@ -2137,8 +2137,9 @@ Optional extras, each enabled by its own key/binary:
2137
2137
  - **Voice** — set an OpenAI (Whisper) or ElevenLabs key to dictate into the composer.
2138
2138
  - **File ingest** — reference a local file or paste text; it becomes a row in the
2139
2139
  native `files` entity with extracted text + (with a Claude key) an
2140
- LLM-written description and links to related records. PDFs/office docs use the
2141
- optional [`markitdown`](https://github.com/microsoft/markitdown) CLI when installed.
2140
+ LLM-written description and links to related records. Documents (PDF, Word,
2141
+ PowerPoint, Excel, OpenDocument, EPUB, RTF) are parsed natively in-process —
2142
+ no external CLI.
2142
2143
 
2143
2144
  Chat threads, files, and secrets are all stored as native Lattice entities.
2144
2145
 
@@ -2295,8 +2296,9 @@ the library API is unchanged and fully backwards-compatible.
2295
2296
  entity. A subscription **Connect** link (PKCE) appears when the `ANTHROPIC_OAUTH_*`
2296
2297
  values are set (see [`.env.example`](.env.example)).
2297
2298
  - **Drop files / paste text / images / URLs.** Sources become native `files` rows
2298
- (referenced, not copied) and are extracted — text via the optional `markitdown`
2299
- CLI, **images via Claude vision**, a pasted **URL crawled** for readable text —
2299
+ (referenced, not copied) and are extracted — documents (PDF / Office /
2300
+ OpenDocument / EPUB / RTF) parsed **natively in-process**, **images via Claude
2301
+ vision**, a pasted **URL crawled** for readable text —
2300
2302
  then summarized with **Claude Haiku** and classified against your records, and
2301
2303
  **added, enriched, and linked** automatically, **auto-creating the junction table
2302
2304
  when none exists** (and a new object when a source fits nothing). All audited and
package/dist/cli.js CHANGED
@@ -6279,7 +6279,7 @@ async function checkForUpdate(pkgName, currentVersion) {
6279
6279
 
6280
6280
  // src/gui/server.ts
6281
6281
  import { createServer } from "http";
6282
- import { spawn as spawn3 } from "child_process";
6282
+ import { spawn as spawn2 } from "child_process";
6283
6283
  import {
6284
6284
  existsSync as existsSync21,
6285
6285
  mkdirSync as mkdirSync10,
@@ -7584,20 +7584,13 @@ var css = `
7584
7584
  .grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
7585
7585
  .grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
7586
7586
  .grants-panel .grants-row input { accent-color: var(--accent); }
7587
- /* Deprecation banner: shown when the workspace holds a grandfathered
7588
- direct database cloud connection (no row-level security). Amber so
7589
- it reads as a warning, not an error. */
7590
- .deprecation-banner {
7591
- display: flex; align-items: center; gap: 12px;
7592
- padding: 8px 16px; font-size: 13px;
7593
- background: rgba(234, 179, 8, 0.12); color: var(--text);
7594
- border-bottom: 1px solid rgba(234, 179, 8, 0.45);
7595
- }
7596
- .deprecation-banner button {
7597
- margin-left: auto; background: transparent; border: none; cursor: pointer;
7598
- color: var(--text-muted); font-size: 13px; padding: 2px 6px; border-radius: 4px;
7587
+ /* Reconnect-required notice: a cloud opened via an unsupported direct
7588
+ database connection serves no data until reconnected through a server. */
7589
+ .cloud-reconnect {
7590
+ padding: 10px 16px; font-size: 13px; line-height: 1.4;
7591
+ background: rgba(239, 68, 68, 0.12); color: var(--text);
7592
+ border-bottom: 1px solid rgba(239, 68, 68, 0.5);
7599
7593
  }
7600
- .deprecation-banner button:hover { background: rgba(234, 179, 8, 0.18); }
7601
7594
 
7602
7595
  /* Inline create-row at the bottom of every table */
7603
7596
  tr.create-row td { background: var(--surface-2); }
@@ -9088,26 +9081,19 @@ var appJs = `
9088
9081
 
9089
9082
  window.addEventListener('hashchange', renderRoute);
9090
9083
 
9091
- // Deprecation banner: a grandfathered direct database cloud connection
9092
- // bypasses the hosted server's row security entirely \u2014 say so up front.
9093
- // Dismiss hides it for this browser session only.
9094
- function initDeprecationBanner() {
9095
- if (sessionStorage.getItem('lattice-direct-banner-dismissed')) return;
9084
+ // 2.2.3: a cloud reached via a raw postgres:// connection is refused (it
9085
+ // can't enforce per-user access). The server serves no cloud data; tell the
9086
+ // operator to reconnect through a user-authenticated server.
9087
+ function initCloudReconnectNotice() {
9096
9088
  fetchJson('/api/dbconfig').then(function (d) {
9097
- if (!d || !d.directCloud) return;
9098
- var banner = document.getElementById('deprecation-banner');
9099
- var text = document.getElementById('deprecation-banner-text');
9100
- if (!banner || !text) return;
9101
- text.textContent = "Direct database cloud connections are deprecated and don't support row-level security. Migrate to a hosted workspace.";
9102
- banner.hidden = false;
9103
- var dismiss = document.getElementById('deprecation-banner-dismiss');
9104
- if (dismiss) dismiss.addEventListener('click', function () {
9105
- banner.hidden = true;
9106
- sessionStorage.setItem('lattice-direct-banner-dismissed', '1');
9107
- });
9108
- }).catch(function () { /* dbconfig unavailable (e.g. team-cloud server mode) \u2014 no banner */ });
9089
+ if (!d || !d.cloudReconnectRequired) return;
9090
+ var bar = document.getElementById('cloud-reconnect');
9091
+ if (!bar) return;
9092
+ bar.textContent = 'This cloud is connected with a direct database connection, which is no longer supported. Reconnect through a server (sign in as a user) to access it securely.';
9093
+ bar.hidden = false;
9094
+ }).catch(function () { /* dbconfig unavailable (server mode) \u2014 nothing to show */ });
9109
9095
  }
9110
- initDeprecationBanner();
9096
+ initCloudReconnectNotice();
9111
9097
 
9112
9098
  // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
9113
9099
  // Sidebar
@@ -11509,9 +11495,11 @@ var appJs = `
11509
11495
  headers: { 'content-type': 'application/json' },
11510
11496
  body: JSON.stringify({ share: !isShared }),
11511
11497
  }).then(function () {
11512
- // The server updated team visibility in place (no DB re-open),
11513
- // so a light in-place refresh reflects it without a full reload.
11514
- return dmRefreshPanel(tableName, false);
11498
+ // Rebuild the graph (not just the panel) so the node's share-status
11499
+ // colour (gnode-shared/gnode-private) recolours immediately from the
11500
+ // refreshed entities \u2014 otherwise the swatch stayed stale until a
11501
+ // manual reload. The editor re-shows for the same table.
11502
+ return dmRefreshPanel(tableName, true);
11515
11503
  }).then(function () {
11516
11504
  showToast(isShared ? 'Unshared "' + tableName + '" from workspace' : 'Shared "' + tableName + '" with workspace', {});
11517
11505
  }).catch(function (e) { showToast('Share update failed: ' + e.message, {}); });
@@ -14231,10 +14219,7 @@ var guiAppHtml = `<!doctype html>
14231
14219
  </svg>
14232
14220
  </button>
14233
14221
  </header>
14234
- <div class="deprecation-banner" id="deprecation-banner" hidden>
14235
- <span id="deprecation-banner-text"></span>
14236
- <button id="deprecation-banner-dismiss" title="Dismiss for this session" aria-label="Dismiss">\u2715</button>
14237
- </div>
14222
+ <div class="cloud-reconnect" id="cloud-reconnect" hidden></div>
14238
14223
  <div class="layout">
14239
14224
  <nav class="sidebar">
14240
14225
  <label class="sidebar-advanced toggle" title="Advanced mode \u2014 row/table editor instead of the file workspace">
@@ -15406,19 +15391,12 @@ async function destroyTeamDirect(db) {
15406
15391
  }
15407
15392
  await db.delete("__lattice_team_identity", "singleton");
15408
15393
  }
15409
- var directDeprecationWarned = false;
15410
15394
  async function openCloud(cloudUrl) {
15411
15395
  if (!isPostgresUrl(cloudUrl)) {
15412
15396
  throw new Error(
15413
15397
  `direct-ops: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
15414
15398
  );
15415
15399
  }
15416
- if (!directDeprecationWarned) {
15417
- directDeprecationWarned = true;
15418
- console.warn(
15419
- "[teams] Direct postgres:// team-cloud connection is deprecated and does NOT enforce 2.2 row-level security. Migrate to a hosted Lattice Teams server."
15420
- );
15421
- }
15422
15400
  const db = new Lattice(cloudUrl);
15423
15401
  await db.init();
15424
15402
  for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
@@ -15583,7 +15561,7 @@ async function resolveUserIdByEmail(db, email) {
15583
15561
  function isVisibleInTeam(tableName, ctx) {
15584
15562
  if (ctx.shared.has(tableName)) return true;
15585
15563
  const owner = ctx.owners.get(tableName);
15586
- if (owner === void 0) return true;
15564
+ if (owner === void 0) return ctx.isCreator;
15587
15565
  return owner === ctx.myUserId;
15588
15566
  }
15589
15567
  async function listTeamUsers(db) {
@@ -19404,9 +19382,9 @@ async function dispatchDbConfigRoute(req, res, ctx) {
19404
19382
  // exist when the team cloud itself is the active database).
19405
19383
  teamId: ctx.teamMembership?.teamId ?? null,
19406
19384
  myUserId: ctx.teamMembership?.myUserId ?? null,
19407
- // Deprecated direct postgres:// team connection present the SPA
19408
- // shows the migrate-to-hosted deprecation banner.
19409
- directCloud: ctx.directCloud
19385
+ // 2.2.3: a direct postgres:// cloud connection is refused the SPA
19386
+ // shows a "reconnect through a server" prompt instead of cloud data.
19387
+ cloudReconnectRequired: ctx.cloudReconnectRequired
19410
19388
  });
19411
19389
  });
19412
19390
  return true;
@@ -20865,8 +20843,12 @@ async function buildSchemaContext(d) {
20865
20843
  }
20866
20844
  return lines.join("\n");
20867
20845
  }
20868
- function buildSystemPrompt(schema) {
20869
- return `${BASE_SYSTEM_PROMPT}
20846
+ function buildSystemPrompt(schema, operatorName) {
20847
+ const who = operatorName && operatorName.trim().length > 0 ? `
20848
+
20849
+ # Who you are assisting
20850
+ You are assisting ${operatorName.trim()}. When the user says "me" / "my", they mean ${operatorName.trim()}; never ask the user for their own name.` : "";
20851
+ return `${BASE_SYSTEM_PROMPT}${who}
20870
20852
 
20871
20853
  # Current database
20872
20854
  ${schema}`;
@@ -20881,7 +20863,7 @@ async function* runChat(opts) {
20881
20863
  ...opts.history ?? [],
20882
20864
  { role: "user", content: opts.userMessage }
20883
20865
  ];
20884
- const system = buildSystemPrompt(await buildSchemaContext(opts.dispatch));
20866
+ const system = buildSystemPrompt(await buildSchemaContext(opts.dispatch), opts.operatorName);
20885
20867
  let loop = 0;
20886
20868
  try {
20887
20869
  for (; loop < MAX_TOOL_LOOPS; loop++) {
@@ -21492,6 +21474,9 @@ async function dispatchChatRoute(req, res, ctx) {
21492
21474
  history,
21493
21475
  userMessage: message,
21494
21476
  temperature,
21477
+ // Give the assistant the operator's name so it addresses them and
21478
+ // resolves "me"/"my" without asking for a name it already has.
21479
+ operatorName: readIdentity().display_name,
21495
21480
  // Capture each executed tool call (capped) for cross-turn replay memory.
21496
21481
  onToolRecord: (rec) => {
21497
21482
  turns[turns.length - 1]?.toolCalls.push(rec);
@@ -21573,10 +21558,538 @@ import { tmpdir as tmpdir2 } from "os";
21573
21558
  import { basename as basename10, extname as extname2, resolve as resolve8, join as join20 } from "path";
21574
21559
 
21575
21560
  // src/gui/ai/extract.ts
21576
- import { readFile } from "fs/promises";
21561
+ import { readFile as readFile2 } from "fs/promises";
21577
21562
  import { extname, basename as basename7 } from "path";
21578
- import { spawn as spawn2 } from "child_process";
21563
+
21564
+ // src/gui/ai/doc-extractors.ts
21565
+ import { readFile } from "fs/promises";
21579
21566
  var MAX_TEXT = 2e5;
21567
+ var MAX_ENTRY_BYTES = 64 * 1024 * 1024;
21568
+ var MAX_TOTAL_BYTES = 256 * 1024 * 1024;
21569
+ var PDF_TIMEOUT_MS = 3e4;
21570
+ var textDecoder = new TextDecoder("utf-8");
21571
+ function decodeUtf8(bytes) {
21572
+ return textDecoder.decode(bytes);
21573
+ }
21574
+ async function loadOptional(specifier) {
21575
+ try {
21576
+ return await import(specifier);
21577
+ } catch {
21578
+ return null;
21579
+ }
21580
+ }
21581
+ function nullIfEmpty(s) {
21582
+ const t = s.trim();
21583
+ return t ? t : null;
21584
+ }
21585
+ function withTimeout(p, ms, label) {
21586
+ let timer;
21587
+ const timeout = new Promise((_, reject) => {
21588
+ timer = setTimeout(() => {
21589
+ reject(new Error(label));
21590
+ }, ms);
21591
+ timer.unref?.();
21592
+ });
21593
+ return Promise.race([
21594
+ p.finally(() => {
21595
+ clearTimeout(timer);
21596
+ }),
21597
+ timeout
21598
+ ]);
21599
+ }
21600
+ function decodeXmlEntities(s) {
21601
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#x([0-9a-fA-F]+);/g, (_, h) => safeCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => safeCodePoint(parseInt(d, 10))).replace(/&amp;/g, "&");
21602
+ }
21603
+ function safeCodePoint(n) {
21604
+ if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
21605
+ try {
21606
+ return String.fromCodePoint(n);
21607
+ } catch {
21608
+ return "";
21609
+ }
21610
+ }
21611
+ function stripTags(s) {
21612
+ let out = "";
21613
+ let i = 0;
21614
+ while (i < s.length) {
21615
+ const lt = s.indexOf("<", i);
21616
+ if (lt < 0) {
21617
+ out += s.slice(i);
21618
+ break;
21619
+ }
21620
+ out += s.slice(i, lt);
21621
+ const gt = s.indexOf(">", lt + 1);
21622
+ if (gt < 0) break;
21623
+ i = gt + 1;
21624
+ }
21625
+ return out;
21626
+ }
21627
+ function isNameBoundary(code) {
21628
+ return code === 32 || // space
21629
+ code === 9 || // tab
21630
+ code === 10 || // \n
21631
+ code === 13 || // \r
21632
+ code === 62 || // >
21633
+ code === 47 || // /
21634
+ Number.isNaN(code);
21635
+ }
21636
+ function eachElement(xml, tag, cb) {
21637
+ const open = "<" + tag;
21638
+ const close = "</" + tag + ">";
21639
+ let i = 0;
21640
+ while (i < xml.length) {
21641
+ const s = xml.indexOf(open, i);
21642
+ if (s < 0) break;
21643
+ const ne = s + open.length;
21644
+ if (!isNameBoundary(xml.charCodeAt(ne))) {
21645
+ i = ne;
21646
+ continue;
21647
+ }
21648
+ const gt = xml.indexOf(">", ne);
21649
+ if (gt < 0) break;
21650
+ const selfClose = xml.charCodeAt(gt - 1) === 47;
21651
+ const attrs = xml.slice(ne, selfClose ? gt - 1 : gt);
21652
+ if (selfClose) {
21653
+ cb(attrs, "", s);
21654
+ i = gt + 1;
21655
+ continue;
21656
+ }
21657
+ const e = xml.indexOf(close, gt + 1);
21658
+ if (e < 0) break;
21659
+ cb(attrs, xml.slice(gt + 1, e), s);
21660
+ i = e + close.length;
21661
+ }
21662
+ }
21663
+ function stripElement(xml, tag) {
21664
+ const open = "<" + tag;
21665
+ const close = "</" + tag + ">";
21666
+ let out = "";
21667
+ let i = 0;
21668
+ while (i < xml.length) {
21669
+ const s = xml.indexOf(open, i);
21670
+ if (s < 0) {
21671
+ out += xml.slice(i);
21672
+ break;
21673
+ }
21674
+ const ne = s + open.length;
21675
+ if (!isNameBoundary(xml.charCodeAt(ne))) {
21676
+ out += xml.slice(i, ne);
21677
+ i = ne;
21678
+ continue;
21679
+ }
21680
+ const gt = xml.indexOf(">", ne);
21681
+ if (gt < 0) {
21682
+ out += xml.slice(i);
21683
+ break;
21684
+ }
21685
+ out += xml.slice(i, s);
21686
+ if (xml.charCodeAt(gt - 1) === 47) {
21687
+ i = gt + 1;
21688
+ continue;
21689
+ }
21690
+ const e = xml.indexOf(close, gt + 1);
21691
+ if (e < 0) break;
21692
+ i = e + close.length;
21693
+ }
21694
+ return out;
21695
+ }
21696
+ function concatTagText(xml, tag) {
21697
+ let out = "";
21698
+ eachElement(xml, tag, (_, inner) => {
21699
+ out += decodeXmlEntities(stripTags(inner));
21700
+ });
21701
+ return out;
21702
+ }
21703
+ function firstTagText(xml, tag) {
21704
+ let found = "";
21705
+ let done = false;
21706
+ eachElement(xml, tag, (_, inner) => {
21707
+ if (done) return;
21708
+ found = inner;
21709
+ done = true;
21710
+ });
21711
+ return found;
21712
+ }
21713
+ function stripHtml(html) {
21714
+ const noScript = stripElement(stripElement(html, "script"), "style");
21715
+ const text = decodeXmlEntities(stripTags(noScript));
21716
+ return text.replace(/[ \t\f\r]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim();
21717
+ }
21718
+ async function unzip(path2) {
21719
+ const fflate = await loadOptional("fflate");
21720
+ if (!fflate || typeof fflate.unzipSync !== "function") return null;
21721
+ try {
21722
+ const buf = await readFile(path2);
21723
+ let total = 0;
21724
+ return fflate.unzipSync(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), {
21725
+ filter: (file) => {
21726
+ const size = file.originalSize || 0;
21727
+ if (size > MAX_ENTRY_BYTES) throw new Error("zip entry exceeds size cap");
21728
+ total += size;
21729
+ if (total > MAX_TOTAL_BYTES) throw new Error("zip total exceeds size cap");
21730
+ return true;
21731
+ }
21732
+ });
21733
+ } catch {
21734
+ return null;
21735
+ }
21736
+ }
21737
+ async function extractDocx(path2) {
21738
+ const mod = await loadOptional("mammoth");
21739
+ const lib = mod?.default ?? mod;
21740
+ if (!lib || typeof lib.extractRawText !== "function") return null;
21741
+ try {
21742
+ const { value } = await lib.extractRawText({ path: path2 });
21743
+ return nullIfEmpty(value);
21744
+ } catch {
21745
+ return null;
21746
+ }
21747
+ }
21748
+ async function extractDoc(path2) {
21749
+ const mod = await loadOptional(
21750
+ "word-extractor"
21751
+ );
21752
+ const Ctor = mod && "default" in mod ? mod.default : mod;
21753
+ if (typeof Ctor !== "function") return null;
21754
+ try {
21755
+ const doc = await new Ctor().extract(path2);
21756
+ return nullIfEmpty(doc.getBody());
21757
+ } catch {
21758
+ return null;
21759
+ }
21760
+ }
21761
+ async function extractPdf(path2) {
21762
+ const unpdf = await loadOptional("unpdf");
21763
+ if (!unpdf || typeof unpdf.getDocumentProxy !== "function") return null;
21764
+ try {
21765
+ const buf = await readFile(path2);
21766
+ const data = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
21767
+ const text = await withTimeout(
21768
+ (async () => {
21769
+ const pdf = await unpdf.getDocumentProxy(data);
21770
+ const out = await unpdf.extractText(pdf, { mergePages: true });
21771
+ return out.text;
21772
+ })(),
21773
+ PDF_TIMEOUT_MS,
21774
+ "pdf extract timeout"
21775
+ );
21776
+ return nullIfEmpty(text);
21777
+ } catch {
21778
+ return null;
21779
+ }
21780
+ }
21781
+ function partNumber(name) {
21782
+ const m = /(\d+)\.xml$/.exec(name);
21783
+ return m?.[1] ? parseInt(m[1], 10) : 0;
21784
+ }
21785
+ function slideText(xml) {
21786
+ const paras = [];
21787
+ eachElement(xml, "a:p", (_, inner) => {
21788
+ const runs = concatTagText(inner, "a:t");
21789
+ if (runs.trim()) paras.push(runs);
21790
+ });
21791
+ if (paras.length === 0) {
21792
+ const runs = concatTagText(xml, "a:t");
21793
+ if (runs.trim()) paras.push(runs);
21794
+ }
21795
+ return paras.join("\n");
21796
+ }
21797
+ async function extractPptx(path2) {
21798
+ const entries = await unzip(path2);
21799
+ if (!entries) return null;
21800
+ const slides = Object.keys(entries).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).sort((a, b) => partNumber(a) - partNumber(b));
21801
+ if (slides.length === 0) return null;
21802
+ const parts = [];
21803
+ let total = 0;
21804
+ for (const n of slides) {
21805
+ if (total >= MAX_TEXT) break;
21806
+ const bytes = entries[n];
21807
+ if (!bytes) continue;
21808
+ const text = slideText(decodeUtf8(bytes)).replace(/[ \t]+/g, " ").trim();
21809
+ if (text) {
21810
+ parts.push(text);
21811
+ total += text.length + 2;
21812
+ }
21813
+ }
21814
+ return nullIfEmpty(parts.join("\n\n"));
21815
+ }
21816
+ async function extractXlsx(path2) {
21817
+ const entries = await unzip(path2);
21818
+ if (!entries) return null;
21819
+ const shared = [];
21820
+ const ssBytes = entries["xl/sharedStrings.xml"];
21821
+ if (ssBytes) {
21822
+ eachElement(decodeUtf8(ssBytes), "si", (_, inner) => {
21823
+ shared.push(concatTagText(stripElement(inner, "rPh"), "t"));
21824
+ });
21825
+ }
21826
+ const sheetNames = Object.keys(entries).filter((n) => /^xl\/worksheets\/sheet\d+\.xml$/.test(n)).sort((a, b) => partNumber(a) - partNumber(b));
21827
+ const rowsOut = [];
21828
+ let total = 0;
21829
+ for (const n of sheetNames) {
21830
+ if (total >= MAX_TEXT) break;
21831
+ const bytes = entries[n];
21832
+ if (!bytes) continue;
21833
+ eachElement(decodeUtf8(bytes), "row", (_, rowInner) => {
21834
+ if (total >= MAX_TEXT) return;
21835
+ const cells = [];
21836
+ eachElement(rowInner, "c", (attrs, body) => {
21837
+ const type = /\bt="([^"]+)"/.exec(attrs)?.[1];
21838
+ let val = "";
21839
+ if (type === "s") {
21840
+ const idx = parseInt(firstTagText(body, "v"), 10);
21841
+ val = Number.isInteger(idx) ? shared[idx] ?? "" : "";
21842
+ } else if (type === "inlineStr") {
21843
+ val = concatTagText(body, "t");
21844
+ } else {
21845
+ val = decodeXmlEntities(firstTagText(body, "v"));
21846
+ }
21847
+ if (val) cells.push(val);
21848
+ });
21849
+ if (cells.length) {
21850
+ const line = cells.join(" ");
21851
+ rowsOut.push(line);
21852
+ total += line.length + 1;
21853
+ }
21854
+ });
21855
+ }
21856
+ return nullIfEmpty(rowsOut.join("\n"));
21857
+ }
21858
+ function odfWhitespace(s) {
21859
+ return s.replace(/<text:tab\b[^>]{0,400}\/?>/g, " ").replace(/<text:line-break\b[^>]{0,400}\/?>/g, "\n").replace(
21860
+ /<text:s\b[^>]{0,400}\btext:c="(\d+)"[^>]{0,400}\/?>/g,
21861
+ (_, c) => " ".repeat(Math.min(parseInt(c, 10) || 1, 100))
21862
+ ).replace(/<text:s\b[^>]{0,400}\/?>/g, " ");
21863
+ }
21864
+ function odfParagraph(inner) {
21865
+ return decodeXmlEntities(stripTags(odfWhitespace(inner))).trim();
21866
+ }
21867
+ async function extractOdfText(path2) {
21868
+ const entries = await unzip(path2);
21869
+ if (!entries) return null;
21870
+ const contentBytes = entries["content.xml"];
21871
+ if (!contentBytes) return null;
21872
+ const xml = decodeUtf8(contentBytes);
21873
+ const items = [];
21874
+ const collect = (_, inner, start) => {
21875
+ const line = odfParagraph(inner);
21876
+ if (line) items.push([start, line]);
21877
+ };
21878
+ eachElement(xml, "text:p", collect);
21879
+ eachElement(xml, "text:h", collect);
21880
+ items.sort((a, b) => a[0] - b[0]);
21881
+ const lines = [];
21882
+ let total = 0;
21883
+ for (const [, line] of items) {
21884
+ if (total >= MAX_TEXT) break;
21885
+ lines.push(line);
21886
+ total += line.length + 1;
21887
+ }
21888
+ return nullIfEmpty(lines.join("\n"));
21889
+ }
21890
+ async function extractOds(path2) {
21891
+ const entries = await unzip(path2);
21892
+ if (!entries) return null;
21893
+ const contentBytes = entries["content.xml"];
21894
+ if (!contentBytes) return null;
21895
+ const xml = decodeUtf8(contentBytes);
21896
+ const rows = [];
21897
+ let total = 0;
21898
+ eachElement(xml, "table:table-row", (_, rowInner) => {
21899
+ if (total >= MAX_TEXT) return;
21900
+ const cells = [];
21901
+ eachElement(rowInner, "table:table-cell", (attrs, body) => {
21902
+ const parts = [];
21903
+ eachElement(body, "text:p", (__, p) => {
21904
+ const t = odfParagraph(p);
21905
+ if (t) parts.push(t);
21906
+ });
21907
+ let val = parts.join(" ").trim();
21908
+ if (!val) {
21909
+ const ov = /\boffice:(?:value|date-value|time-value|string-value|boolean-value)="([^"]*)"/.exec(
21910
+ attrs
21911
+ )?.[1];
21912
+ if (ov) val = decodeXmlEntities(ov);
21913
+ }
21914
+ if (val) cells.push(val);
21915
+ });
21916
+ if (cells.length) {
21917
+ const line = cells.join(" ");
21918
+ rows.push(line);
21919
+ total += line.length + 1;
21920
+ }
21921
+ });
21922
+ return nullIfEmpty(rows.join("\n"));
21923
+ }
21924
+ function normalizeZipPath(p) {
21925
+ const parts = [];
21926
+ for (const seg of p.split("/")) {
21927
+ if (seg === "" || seg === ".") continue;
21928
+ if (seg === "..") parts.pop();
21929
+ else parts.push(seg);
21930
+ }
21931
+ return parts.join("/");
21932
+ }
21933
+ function resolveHref(baseDir, href) {
21934
+ let h = href.split("#")[0]?.split("?")[0] ?? "";
21935
+ try {
21936
+ h = decodeURIComponent(h);
21937
+ } catch {
21938
+ }
21939
+ return normalizeZipPath(baseDir + h);
21940
+ }
21941
+ async function extractEpub(path2) {
21942
+ const entries = await unzip(path2);
21943
+ if (!entries) return null;
21944
+ let order = [];
21945
+ const container = entries["META-INF/container.xml"];
21946
+ const opfPath = container ? /full-path="([^"]+)"/.exec(decodeUtf8(container))?.[1] : void 0;
21947
+ if (opfPath && entries[opfPath]) {
21948
+ const opf = decodeUtf8(entries[opfPath]);
21949
+ const manifest = {};
21950
+ eachElement(opf, "item", (attrs) => {
21951
+ const id = /\bid="([^"]+)"/.exec(attrs)?.[1];
21952
+ const href = /\bhref="([^"]+)"/.exec(attrs)?.[1];
21953
+ if (id && href) manifest[id] = href;
21954
+ });
21955
+ const baseDir = opfPath.includes("/") ? opfPath.slice(0, opfPath.lastIndexOf("/") + 1) : "";
21956
+ eachElement(opf, "itemref", (attrs) => {
21957
+ const idref = /\bidref="([^"]+)"/.exec(attrs)?.[1];
21958
+ const href = idref ? manifest[idref] : void 0;
21959
+ if (href) order.push(resolveHref(baseDir, href));
21960
+ });
21961
+ }
21962
+ if (order.length === 0) {
21963
+ order = Object.keys(entries).filter((n) => /\.x?html?$/i.test(n)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
21964
+ }
21965
+ const parts = [];
21966
+ let total = 0;
21967
+ for (const n of order) {
21968
+ if (total >= MAX_TEXT) break;
21969
+ const bytes = entries[n];
21970
+ if (!bytes) continue;
21971
+ const body = stripHtml(decodeUtf8(bytes));
21972
+ if (body) {
21973
+ parts.push(body);
21974
+ total += body.length + 2;
21975
+ }
21976
+ }
21977
+ return nullIfEmpty(parts.join("\n\n"));
21978
+ }
21979
+ var RTF_IGNORED_DESTINATIONS = /^\\(?:\*|(?:fonttbl|colortbl|stylesheet|info|pict|themedata|colorschememapping|latentstyles|datastore|listtable|listoverridetable|rsidtbl|generator|operator|xmlnstbl|wgrffmtfilter|mmathPr)(?![a-zA-Z]))/;
21980
+ function stripRtfDestinations(s) {
21981
+ const kept = [];
21982
+ let keepFrom = 0;
21983
+ let i = 0;
21984
+ while (i < s.length) {
21985
+ if (s[i] === "{" && RTF_IGNORED_DESTINATIONS.test(s.slice(i + 1, i + 40))) {
21986
+ const pre = s.slice(keepFrom, i);
21987
+ kept.push(pre);
21988
+ if (/\\[a-zA-Z]+$/.test(pre.slice(-40))) kept.push(" ");
21989
+ let depth = 1;
21990
+ let j = i + 1;
21991
+ for (; j < s.length && depth > 0; j++) {
21992
+ const ch = s[j];
21993
+ if (ch === "\\")
21994
+ j++;
21995
+ else if (ch === "{") depth++;
21996
+ else if (ch === "}") depth--;
21997
+ }
21998
+ i = j;
21999
+ keepFrom = i;
22000
+ } else {
22001
+ i++;
22002
+ }
22003
+ }
22004
+ kept.push(s.slice(keepFrom));
22005
+ return kept.join("");
22006
+ }
22007
+ var CP1252_HIGH = {
22008
+ 128: 8364,
22009
+ 130: 8218,
22010
+ 131: 402,
22011
+ 132: 8222,
22012
+ 133: 8230,
22013
+ 134: 8224,
22014
+ 135: 8225,
22015
+ 136: 710,
22016
+ 137: 8240,
22017
+ 138: 352,
22018
+ 139: 8249,
22019
+ 140: 338,
22020
+ 142: 381,
22021
+ 145: 8216,
22022
+ 146: 8217,
22023
+ 147: 8220,
22024
+ 148: 8221,
22025
+ 149: 8226,
22026
+ 150: 8211,
22027
+ 151: 8212,
22028
+ 152: 732,
22029
+ 153: 8482,
22030
+ 154: 353,
22031
+ 155: 8250,
22032
+ 156: 339,
22033
+ 158: 382,
22034
+ 159: 376
22035
+ };
22036
+ function cp1252Char(byte) {
22037
+ if (byte >= 128 && byte <= 159) {
22038
+ const cp = CP1252_HIGH[byte];
22039
+ return cp ? safeCodePoint(cp) : "";
22040
+ }
22041
+ return safeCodePoint(byte);
22042
+ }
22043
+ function rtfToText(rtf) {
22044
+ let s = stripRtfDestinations(rtf);
22045
+ s = s.replace(/\\'([0-9a-fA-F]{2})/g, (_, h) => cp1252Char(parseInt(h, 16)));
22046
+ s = s.replace(/\\u(-?\d+)\s?\??/g, (_, d) => {
22047
+ let n = parseInt(d, 10);
22048
+ if (n < 0) n += 65536;
22049
+ return safeCodePoint(n);
22050
+ });
22051
+ s = s.replace(/\\par[d]?\b/g, "\n").replace(/\\line\b/g, "\n").replace(/\\sect\b/g, "\n").replace(/\\page\b/g, "\n").replace(/\\tab\b/g, " ");
22052
+ s = s.replace(/\\[a-zA-Z]+-?\d*\s?/g, "").replace(/\\[^a-zA-Z]/g, "");
22053
+ s = s.replace(/[{}]/g, "");
22054
+ return s.replace(/[ \t]+/g, (m) => m.includes(" ") ? " " : " ").replace(/[ \t]\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
22055
+ }
22056
+ async function extractRtf(path2) {
22057
+ try {
22058
+ const raw = await readFile(path2, "latin1");
22059
+ if (!raw.startsWith("{\\rtf")) return null;
22060
+ return nullIfEmpty(rtfToText(raw));
22061
+ } catch {
22062
+ return null;
22063
+ }
22064
+ }
22065
+ async function extractDocument(path2, ext) {
22066
+ switch (ext) {
22067
+ case ".docx":
22068
+ return extractDocx(path2);
22069
+ case ".doc":
22070
+ return extractDoc(path2);
22071
+ case ".pdf":
22072
+ return extractPdf(path2);
22073
+ case ".pptx":
22074
+ return extractPptx(path2);
22075
+ case ".xlsx":
22076
+ return extractXlsx(path2);
22077
+ case ".odt":
22078
+ case ".odp":
22079
+ return extractOdfText(path2);
22080
+ case ".ods":
22081
+ return extractOds(path2);
22082
+ case ".epub":
22083
+ return extractEpub(path2);
22084
+ case ".rtf":
22085
+ return extractRtf(path2);
22086
+ default:
22087
+ return null;
22088
+ }
22089
+ }
22090
+
22091
+ // src/gui/ai/extract.ts
22092
+ var MAX_TEXT2 = 2e5;
21580
22093
  var CODE_LANGS = {
21581
22094
  ".ts": "typescript",
21582
22095
  ".tsx": "typescript",
@@ -21624,81 +22137,25 @@ var TEXT_EXT = /* @__PURE__ */ new Set([
21624
22137
  ".htm"
21625
22138
  ]);
21626
22139
  var TEXT_MIME = /^(text\/|application\/(json|xml|xhtml\+xml|x-yaml|yaml|toml))/;
21627
- var MARKITDOWN_EXT = /* @__PURE__ */ new Set([
21628
- ".pdf",
21629
- ".docx",
21630
- ".doc",
21631
- ".pptx",
21632
- ".ppt",
21633
- ".xlsx",
21634
- ".xls",
21635
- ".epub",
21636
- ".rtf",
21637
- ".odt",
21638
- ".ods",
21639
- ".odp"
21640
- ]);
21641
- var MARKITDOWN_TIMEOUT_MS = 12e4;
21642
- var MARKITDOWN_MAX_BYTES = 5e7;
21643
- function runMarkitdown(path2) {
21644
- return new Promise((resolve12) => {
21645
- const bin = process.env.MARKITDOWN_BIN ?? "markitdown";
21646
- let child;
21647
- try {
21648
- child = spawn2(bin, [path2], { stdio: ["ignore", "pipe", "ignore"] });
21649
- } catch {
21650
- resolve12(null);
21651
- return;
21652
- }
21653
- let out = "";
21654
- let bytes = 0;
21655
- let settled = false;
21656
- const finish = (v) => {
21657
- if (settled) return;
21658
- settled = true;
21659
- clearTimeout(timer);
21660
- resolve12(v);
21661
- };
21662
- const timer = setTimeout(() => {
21663
- child.kill();
21664
- finish(null);
21665
- }, MARKITDOWN_TIMEOUT_MS);
21666
- child.stdout.on("data", (c) => {
21667
- bytes += c.length;
21668
- if (bytes > MARKITDOWN_MAX_BYTES) {
21669
- child.kill();
21670
- finish(null);
21671
- } else {
21672
- out += c.toString("utf8");
21673
- }
21674
- });
21675
- child.on("error", () => {
21676
- finish(null);
21677
- });
21678
- child.on("close", (code) => {
21679
- finish(code === 0 && out.trim() ? out.trim() : null);
21680
- });
21681
- });
21682
- }
21683
22140
  function languageOf(name) {
21684
22141
  return CODE_LANGS[extname(name).toLowerCase()] ?? null;
21685
22142
  }
21686
22143
  function truncate(s) {
21687
- return s.length > MAX_TEXT ? s.slice(0, MAX_TEXT) : s;
22144
+ return s.length > MAX_TEXT2 ? s.slice(0, MAX_TEXT2) : s;
21688
22145
  }
21689
22146
  async function parseFile(path2, mimeHint, originalName) {
21690
22147
  const name = originalName ?? basename7(path2);
21691
22148
  const ext = extname(name).toLowerCase();
21692
22149
  const lang = languageOf(name);
21693
22150
  if (lang) {
21694
- return { text: truncate(await readFile(path2, "utf8")), language: lang };
22151
+ return { text: truncate(await readFile2(path2, "utf8")), language: lang };
21695
22152
  }
21696
22153
  if (mimeHint && TEXT_MIME.test(mimeHint) || TEXT_EXT.has(ext)) {
21697
- return { text: truncate(await readFile(path2, "utf8")) };
22154
+ return { text: truncate(await readFile2(path2, "utf8")) };
21698
22155
  }
21699
- if (MARKITDOWN_EXT.has(ext)) {
21700
- const md = await runMarkitdown(path2);
21701
- if (md) return { text: truncate(md) };
22156
+ const doc = await extractDocument(path2, ext);
22157
+ if (doc != null) {
22158
+ return { text: truncate(doc) };
21702
22159
  }
21703
22160
  return { text: "", skip: true };
21704
22161
  }
@@ -21712,7 +22169,7 @@ function describe(text, mime, name) {
21712
22169
 
21713
22170
  // src/ai/vision.ts
21714
22171
  import { createRequire as createRequire5 } from "module";
21715
- import { readFile as readFile2 } from "fs/promises";
22172
+ import { readFile as readFile3 } from "fs/promises";
21716
22173
  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.";
21717
22174
  var MAX_DIM = 1568;
21718
22175
  async function describeImage(auth, path2, opts = {}) {
@@ -21728,7 +22185,7 @@ async function describeImage(auth, path2, opts = {}) {
21728
22185
  }
21729
22186
  var DEFAULT_PDF_PROMPT = "Read this document for a knowledge base. First transcribe its readable text, then add a 2-4 sentence factual summary of what it is and its key details. It may be a scanned/image-only PDF \u2014 read the text from the page images. No preamble.";
21730
22187
  async function describePdf(auth, path2, opts = {}) {
21731
- const buf = await readFile2(path2);
22188
+ const buf = await readFile3(path2);
21732
22189
  const maxBytes = opts.maxBytes ?? 3e7;
21733
22190
  if (buf.length > maxBytes) {
21734
22191
  throw new Error(
@@ -22073,6 +22530,14 @@ async function attachBlob(srcPath, latticeRoot) {
22073
22530
  }
22074
22531
 
22075
22532
  // src/gui/ingest-routes.ts
22533
+ function fileSlug(name, id) {
22534
+ const base = slugify(name.replace(/\.[^./\\]+$/, "")) || "file";
22535
+ return `${base}-${id.slice(0, 8)}`;
22536
+ }
22537
+ function fileIdentity(displayName, id) {
22538
+ const label = displayName.trim() || "file";
22539
+ return { slug: fileSlug(displayName, id), name: label, title: label };
22540
+ }
22076
22541
  var MIME_BY_EXT = {
22077
22542
  ".pdf": "application/pdf",
22078
22543
  ".png": "image/png",
@@ -22377,7 +22842,8 @@ function looksLikeUrl(s) {
22377
22842
  const t = s.trim();
22378
22843
  return /^https?:\/\/\S+$/i.test(t) && !/\s/.test(t);
22379
22844
  }
22380
- function readBuffer2(req, maxBytes = 5e7) {
22845
+ var MAX_INGEST_BYTES = 5e7;
22846
+ function readBuffer2(req, maxBytes = MAX_INGEST_BYTES) {
22381
22847
  return new Promise((resolve_, reject) => {
22382
22848
  const chunks = [];
22383
22849
  let size = 0;
@@ -22440,8 +22906,10 @@ async function dispatchIngestRoute(req, res, ctx) {
22440
22906
  } finally {
22441
22907
  await rm(tmp, { force: true }).catch(() => void 0);
22442
22908
  }
22909
+ const fileId = crypto.randomUUID();
22443
22910
  const { id: id2 } = await createRow(mctx, "files", {
22444
- id: crypto.randomUUID(),
22911
+ id: fileId,
22912
+ ...fileIdentity(name2, fileId),
22445
22913
  original_name: name2,
22446
22914
  mime: mime2,
22447
22915
  size_bytes: buf.length,
@@ -22492,8 +22960,10 @@ async function dispatchIngestRoute(req, res, ctx) {
22492
22960
  console.warn("[ingest] url crawl failed:", e.message);
22493
22961
  }
22494
22962
  }
22963
+ const textFileId = crypto.randomUUID();
22495
22964
  const { id: id2 } = await createRow(mctx, "files", {
22496
- id: crypto.randomUUID(),
22965
+ id: textFileId,
22966
+ ...fileIdentity(title, textFileId),
22497
22967
  original_name: title,
22498
22968
  mime: mime2,
22499
22969
  size_bytes: Buffer.byteLength(content, "utf8"),
@@ -22525,10 +22995,16 @@ async function dispatchIngestRoute(req, res, ctx) {
22525
22995
  sendJson5(res, { error: `file not found: ${abs}` }, 400);
22526
22996
  return true;
22527
22997
  }
22998
+ if (size > MAX_INGEST_BYTES) {
22999
+ sendJson5(res, { error: "file too large" }, 413);
23000
+ return true;
23001
+ }
22528
23002
  const name = basename10(abs);
22529
23003
  const mime = mimeFor(name);
23004
+ const localFileId = crypto.randomUUID();
22530
23005
  const { id } = await createRow(mctx, "files", {
22531
- id: crypto.randomUUID(),
23006
+ id: localFileId,
23007
+ ...fileIdentity(name, localFileId),
22532
23008
  path: abs,
22533
23009
  original_name: name,
22534
23010
  mime,
@@ -22638,7 +23114,7 @@ function sendText(res, body, status = 200, contentType = "text/plain; charset=ut
22638
23114
  function openUrl(url) {
22639
23115
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
22640
23116
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
22641
- const child = spawn3(command, args, { stdio: "ignore", detached: true });
23117
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
22642
23118
  child.unref();
22643
23119
  }
22644
23120
  function listen(server, port, host) {
@@ -22888,7 +23364,7 @@ function resolveOutputDirForConfig(configPath) {
22888
23364
  }
22889
23365
  return resolve9(base, "context");
22890
23366
  }
22891
- async function openConfig(configPath, outputDir, autoRender = false) {
23367
+ async function openConfig(configPath, outputDir, autoRender = false, teamCloud = false) {
22892
23368
  const parsed = parseConfigFile(configPath);
22893
23369
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
22894
23370
  mkdirSync10(dirname11(parsed.dbPath), { recursive: true });
@@ -23017,6 +23493,7 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23017
23493
  }
23018
23494
  }
23019
23495
  let teamContext = null;
23496
+ let cloudReconnectRequired = false;
23020
23497
  if (db.getDialect() === "postgres") {
23021
23498
  let teamEnabled = false;
23022
23499
  try {
@@ -23024,7 +23501,14 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23024
23501
  } catch {
23025
23502
  teamEnabled = false;
23026
23503
  }
23027
- if (!teamEnabled) {
23504
+ const directGuiPostgres = !teamCloud && isPostgresUrl(parsed.dbPath);
23505
+ if (directGuiPostgres) {
23506
+ if (teamEnabled) {
23507
+ cloudReconnectRequired = true;
23508
+ teamEnabled = false;
23509
+ validTables.clear();
23510
+ }
23511
+ } else if (!teamEnabled) {
23028
23512
  try {
23029
23513
  const rawDb = parseDocument3(readFileSync15(configPath, "utf8")).get("db");
23030
23514
  const dbLine = typeof rawDb === "string" ? rawDb.trim() : "";
@@ -23065,15 +23549,8 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23065
23549
  if (!isVisibleInTeam(name, teamContext)) validTables.delete(name);
23066
23550
  }
23067
23551
  }
23068
- let directTeamConnection = false;
23069
- try {
23070
- const conns = await teamsClient.listConnections();
23071
- directTeamConnection = conns.some((c) => isPostgresUrl(c.cloud_url));
23072
- } catch (e) {
23073
- console.warn("[openConfig] could not check for direct team connections:", e.message);
23074
- }
23075
23552
  let realtime = null;
23076
- if (db.getDialect() === "postgres") {
23553
+ if (db.getDialect() === "postgres" && !cloudReconnectRequired) {
23077
23554
  try {
23078
23555
  realtime = new RealtimeBroker(parsed.dbPath);
23079
23556
  await realtime.start();
@@ -23112,7 +23589,8 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23112
23589
  teamsClient,
23113
23590
  validTables,
23114
23591
  teamContext,
23115
- directTeamConnection,
23592
+ teamCloud,
23593
+ cloudReconnectRequired,
23116
23594
  junctionTables,
23117
23595
  entityContextByTable,
23118
23596
  manifest,
@@ -23207,7 +23685,7 @@ async function disposeActive(active) {
23207
23685
  async function reopenSameConfig(active, autoRender) {
23208
23686
  const feed = active.feed;
23209
23687
  await disposeActive(active);
23210
- const next = await openConfig(active.configPath, active.outputDir, autoRender);
23688
+ const next = await openConfig(active.configPath, active.outputDir, autoRender, active.teamCloud);
23211
23689
  next.feed = feed;
23212
23690
  return next;
23213
23691
  }
@@ -23342,7 +23820,7 @@ async function applySchemaConfig(active, entry, direction, autoRender) {
23342
23820
  for (const sql of ddl) await execSql(active.db, sql);
23343
23821
  saveConfigDoc(active.configPath, doc);
23344
23822
  await disposeActive(active);
23345
- return openConfig(active.configPath, active.outputDir, autoRender);
23823
+ return openConfig(active.configPath, active.outputDir, autoRender, active.teamCloud);
23346
23824
  }
23347
23825
  function schemaReverseSummary(verb, entry) {
23348
23826
  const what = entry.operation.replace("schema.", "").replace(/_/g, " ");
@@ -23356,7 +23834,7 @@ async function startGuiServer(options) {
23356
23834
  const teamCloud = options.teamCloud ?? false;
23357
23835
  const autoRender = options.autoRender ?? false;
23358
23836
  const sessionId = crypto.randomUUID();
23359
- let active = await openConfig(configPath, outputDir, autoRender);
23837
+ let active = await openConfig(configPath, outputDir, autoRender, teamCloud);
23360
23838
  const latticeRoot = findLatticeRoot(dirname11(configPath));
23361
23839
  let currentWorkspaceId = null;
23362
23840
  if (latticeRoot) {
@@ -24546,7 +25024,7 @@ data: ${JSON.stringify(data)}
24546
25024
  const paths = resolveWorkspacePaths(latticeRoot, ws);
24547
25025
  let next;
24548
25026
  try {
24549
- next = await openConfig(paths.configPath, paths.contextDir, autoRender);
25027
+ next = await openConfig(paths.configPath, paths.contextDir, autoRender, teamCloud);
24550
25028
  } catch (e) {
24551
25029
  const err = e;
24552
25030
  sendJson(
@@ -24588,7 +25066,12 @@ data: ${JSON.stringify(data)}
24588
25066
  const newPaths = resolveWorkspacePaths(latticeRoot, created);
24589
25067
  let newActive;
24590
25068
  try {
24591
- newActive = await openConfig(newPaths.configPath, newPaths.contextDir, autoRender);
25069
+ newActive = await openConfig(
25070
+ newPaths.configPath,
25071
+ newPaths.contextDir,
25072
+ autoRender,
25073
+ teamCloud
25074
+ );
24592
25075
  } catch (e) {
24593
25076
  sendJson(
24594
25077
  res,
@@ -24647,7 +25130,12 @@ data: ${JSON.stringify(data)}
24647
25130
  const fbPaths = resolveWorkspacePaths(latticeRoot, fallback);
24648
25131
  let next;
24649
25132
  try {
24650
- next = await openConfig(fbPaths.configPath, fbPaths.contextDir, autoRender);
25133
+ next = await openConfig(
25134
+ fbPaths.configPath,
25135
+ fbPaths.contextDir,
25136
+ autoRender,
25137
+ teamCloud
25138
+ );
24651
25139
  } catch (e) {
24652
25140
  const err = e;
24653
25141
  const codePrefix = err.code ? `[${err.code}] ` : "";
@@ -24739,7 +25227,12 @@ data: ${JSON.stringify(data)}
24739
25227
  }
24740
25228
  let next;
24741
25229
  try {
24742
- next = await openConfig(newPath, resolveOutputDirForConfig(newPath), autoRender);
25230
+ next = await openConfig(
25231
+ newPath,
25232
+ resolveOutputDirForConfig(newPath),
25233
+ autoRender,
25234
+ teamCloud
25235
+ );
24743
25236
  } catch (e) {
24744
25237
  const err = e;
24745
25238
  console.error(`[dbconfig.switch] openConfig(${newPath}) failed:`, err);
@@ -24766,7 +25259,8 @@ data: ${JSON.stringify(data)}
24766
25259
  const next = await openConfig(
24767
25260
  newConfigPath,
24768
25261
  resolveOutputDirForConfig(newConfigPath),
24769
- autoRender
25262
+ autoRender,
25263
+ teamCloud
24770
25264
  );
24771
25265
  await disposeActive(active);
24772
25266
  active = next;
@@ -24808,7 +25302,8 @@ data: ${JSON.stringify(data)}
24808
25302
  next = await openConfig(
24809
25303
  fallback.path,
24810
25304
  resolveOutputDirForConfig(fallback.path),
24811
- autoRender
25305
+ autoRender,
25306
+ teamCloud
24812
25307
  );
24813
25308
  } catch (e) {
24814
25309
  const err = e;
@@ -25208,9 +25703,14 @@ data: ${JSON.stringify(data)}
25208
25703
  teamId: active.teamContext.teamId,
25209
25704
  myUserId: active.teamContext.myUserId
25210
25705
  } : null,
25211
- directCloud: active.directTeamConnection,
25706
+ cloudReconnectRequired: active.cloudReconnectRequired,
25212
25707
  swap: async () => {
25213
- const next = await openConfig(active.configPath, active.outputDir, autoRender);
25708
+ const next = await openConfig(
25709
+ active.configPath,
25710
+ active.outputDir,
25711
+ autoRender,
25712
+ active.teamCloud
25713
+ );
25214
25714
  await disposeActive(active);
25215
25715
  active = next;
25216
25716
  }
package/dist/index.cjs CHANGED
@@ -8096,19 +8096,12 @@ async function destroyTeamDirect(db) {
8096
8096
  }
8097
8097
  await db.delete("__lattice_team_identity", "singleton");
8098
8098
  }
8099
- var directDeprecationWarned = false;
8100
8099
  async function openCloud(cloudUrl) {
8101
8100
  if (!isPostgresUrl(cloudUrl)) {
8102
8101
  throw new Error(
8103
8102
  `direct-ops: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
8104
8103
  );
8105
8104
  }
8106
- if (!directDeprecationWarned) {
8107
- directDeprecationWarned = true;
8108
- console.warn(
8109
- "[teams] Direct postgres:// team-cloud connection is deprecated and does NOT enforce 2.2 row-level security. Migrate to a hosted Lattice Teams server."
8110
- );
8111
- }
8112
8105
  const db = new Lattice(cloudUrl);
8113
8106
  await db.init();
8114
8107
  for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
package/dist/index.d.cts CHANGED
@@ -4509,8 +4509,8 @@ interface PdfOptions {
4509
4509
  }
4510
4510
  /**
4511
4511
  * Read a PDF with Claude's native document support — works on text PDFs AND
4512
- * scanned/image-only PDFs (no text layer), which `markitdown` cannot extract.
4513
- * AI-gated; the model call is injectable for tests.
4512
+ * scanned/image-only PDFs (no text layer), where in-process text extraction
4513
+ * finds nothing. AI-gated; the model call is injectable for tests.
4514
4514
  */
4515
4515
  declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
4516
4516
 
package/dist/index.d.ts CHANGED
@@ -4509,8 +4509,8 @@ interface PdfOptions {
4509
4509
  }
4510
4510
  /**
4511
4511
  * Read a PDF with Claude's native document support — works on text PDFs AND
4512
- * scanned/image-only PDFs (no text layer), which `markitdown` cannot extract.
4513
- * AI-gated; the model call is injectable for tests.
4512
+ * scanned/image-only PDFs (no text layer), where in-process text extraction
4513
+ * finds nothing. AI-gated; the model call is injectable for tests.
4514
4514
  */
4515
4515
  declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
4516
4516
 
package/dist/index.js CHANGED
@@ -7962,19 +7962,12 @@ async function destroyTeamDirect(db) {
7962
7962
  }
7963
7963
  await db.delete("__lattice_team_identity", "singleton");
7964
7964
  }
7965
- var directDeprecationWarned = false;
7966
7965
  async function openCloud(cloudUrl) {
7967
7966
  if (!isPostgresUrl(cloudUrl)) {
7968
7967
  throw new Error(
7969
7968
  `direct-ops: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
7970
7969
  );
7971
7970
  }
7972
- if (!directDeprecationWarned) {
7973
- directDeprecationWarned = true;
7974
- console.warn(
7975
- "[teams] Direct postgres:// team-cloud connection is deprecated and does NOT enforce 2.2 row-level security. Migrate to a hosted Lattice Teams server."
7976
- );
7977
- }
7978
7971
  const db = new Lattice(cloudUrl);
7979
7972
  await db.init();
7980
7973
  for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,10 +57,14 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "@anthropic-ai/sdk": "^0.71.2",
60
+ "fflate": "^0.8.3",
60
61
  "file-type": "^19.6.0",
62
+ "mammoth": "^1.12.0",
61
63
  "pg": "^8.11.0",
62
64
  "playwright": "^1.48.0",
63
- "sharp": "^0.33.5"
65
+ "sharp": "^0.33.5",
66
+ "unpdf": "^1.6.2",
67
+ "word-extractor": "^1.0.4"
64
68
  },
65
69
  "devDependencies": {
66
70
  "@anthropic-ai/sdk": "^0.71.0",