latticesql 2.2.2 → 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,6 +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
+ /* 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);
7593
+ }
7587
7594
 
7588
7595
  /* Inline create-row at the bottom of every table */
7589
7596
  tr.create-row td { background: var(--surface-2); }
@@ -9074,6 +9081,20 @@ var appJs = `
9074
9081
 
9075
9082
  window.addEventListener('hashchange', renderRoute);
9076
9083
 
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() {
9088
+ fetchJson('/api/dbconfig').then(function (d) {
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 */ });
9095
+ }
9096
+ initCloudReconnectNotice();
9097
+
9077
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
9078
9099
  // Sidebar
9079
9100
  // \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
@@ -14198,6 +14219,7 @@ var guiAppHtml = `<!doctype html>
14198
14219
  </svg>
14199
14220
  </button>
14200
14221
  </header>
14222
+ <div class="cloud-reconnect" id="cloud-reconnect" hidden></div>
14201
14223
  <div class="layout">
14202
14224
  <nav class="sidebar">
14203
14225
  <label class="sidebar-advanced toggle" title="Advanced mode \u2014 row/table editor instead of the file workspace">
@@ -19359,7 +19381,10 @@ async function dispatchDbConfigRoute(req, res, ctx) {
19359
19381
  // without a local `__lattice_team_connections` row (which doesn't
19360
19382
  // exist when the team cloud itself is the active database).
19361
19383
  teamId: ctx.teamMembership?.teamId ?? null,
19362
- myUserId: ctx.teamMembership?.myUserId ?? null
19384
+ myUserId: ctx.teamMembership?.myUserId ?? null,
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
19363
19388
  });
19364
19389
  });
19365
19390
  return true;
@@ -21533,10 +21558,538 @@ import { tmpdir as tmpdir2 } from "os";
21533
21558
  import { basename as basename10, extname as extname2, resolve as resolve8, join as join20 } from "path";
21534
21559
 
21535
21560
  // src/gui/ai/extract.ts
21536
- import { readFile } from "fs/promises";
21561
+ import { readFile as readFile2 } from "fs/promises";
21537
21562
  import { extname, basename as basename7 } from "path";
21538
- import { spawn as spawn2 } from "child_process";
21563
+
21564
+ // src/gui/ai/doc-extractors.ts
21565
+ import { readFile } from "fs/promises";
21539
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;
21540
22093
  var CODE_LANGS = {
21541
22094
  ".ts": "typescript",
21542
22095
  ".tsx": "typescript",
@@ -21584,81 +22137,25 @@ var TEXT_EXT = /* @__PURE__ */ new Set([
21584
22137
  ".htm"
21585
22138
  ]);
21586
22139
  var TEXT_MIME = /^(text\/|application\/(json|xml|xhtml\+xml|x-yaml|yaml|toml))/;
21587
- var MARKITDOWN_EXT = /* @__PURE__ */ new Set([
21588
- ".pdf",
21589
- ".docx",
21590
- ".doc",
21591
- ".pptx",
21592
- ".ppt",
21593
- ".xlsx",
21594
- ".xls",
21595
- ".epub",
21596
- ".rtf",
21597
- ".odt",
21598
- ".ods",
21599
- ".odp"
21600
- ]);
21601
- var MARKITDOWN_TIMEOUT_MS = 12e4;
21602
- var MARKITDOWN_MAX_BYTES = 5e7;
21603
- function runMarkitdown(path2) {
21604
- return new Promise((resolve12) => {
21605
- const bin = process.env.MARKITDOWN_BIN ?? "markitdown";
21606
- let child;
21607
- try {
21608
- child = spawn2(bin, [path2], { stdio: ["ignore", "pipe", "ignore"] });
21609
- } catch {
21610
- resolve12(null);
21611
- return;
21612
- }
21613
- let out = "";
21614
- let bytes = 0;
21615
- let settled = false;
21616
- const finish = (v) => {
21617
- if (settled) return;
21618
- settled = true;
21619
- clearTimeout(timer);
21620
- resolve12(v);
21621
- };
21622
- const timer = setTimeout(() => {
21623
- child.kill();
21624
- finish(null);
21625
- }, MARKITDOWN_TIMEOUT_MS);
21626
- child.stdout.on("data", (c) => {
21627
- bytes += c.length;
21628
- if (bytes > MARKITDOWN_MAX_BYTES) {
21629
- child.kill();
21630
- finish(null);
21631
- } else {
21632
- out += c.toString("utf8");
21633
- }
21634
- });
21635
- child.on("error", () => {
21636
- finish(null);
21637
- });
21638
- child.on("close", (code) => {
21639
- finish(code === 0 && out.trim() ? out.trim() : null);
21640
- });
21641
- });
21642
- }
21643
22140
  function languageOf(name) {
21644
22141
  return CODE_LANGS[extname(name).toLowerCase()] ?? null;
21645
22142
  }
21646
22143
  function truncate(s) {
21647
- return s.length > MAX_TEXT ? s.slice(0, MAX_TEXT) : s;
22144
+ return s.length > MAX_TEXT2 ? s.slice(0, MAX_TEXT2) : s;
21648
22145
  }
21649
22146
  async function parseFile(path2, mimeHint, originalName) {
21650
22147
  const name = originalName ?? basename7(path2);
21651
22148
  const ext = extname(name).toLowerCase();
21652
22149
  const lang = languageOf(name);
21653
22150
  if (lang) {
21654
- return { text: truncate(await readFile(path2, "utf8")), language: lang };
22151
+ return { text: truncate(await readFile2(path2, "utf8")), language: lang };
21655
22152
  }
21656
22153
  if (mimeHint && TEXT_MIME.test(mimeHint) || TEXT_EXT.has(ext)) {
21657
- return { text: truncate(await readFile(path2, "utf8")) };
22154
+ return { text: truncate(await readFile2(path2, "utf8")) };
21658
22155
  }
21659
- if (MARKITDOWN_EXT.has(ext)) {
21660
- const md = await runMarkitdown(path2);
21661
- if (md) return { text: truncate(md) };
22156
+ const doc = await extractDocument(path2, ext);
22157
+ if (doc != null) {
22158
+ return { text: truncate(doc) };
21662
22159
  }
21663
22160
  return { text: "", skip: true };
21664
22161
  }
@@ -21672,7 +22169,7 @@ function describe(text, mime, name) {
21672
22169
 
21673
22170
  // src/ai/vision.ts
21674
22171
  import { createRequire as createRequire5 } from "module";
21675
- import { readFile as readFile2 } from "fs/promises";
22172
+ import { readFile as readFile3 } from "fs/promises";
21676
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.";
21677
22174
  var MAX_DIM = 1568;
21678
22175
  async function describeImage(auth, path2, opts = {}) {
@@ -21688,7 +22185,7 @@ async function describeImage(auth, path2, opts = {}) {
21688
22185
  }
21689
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.";
21690
22187
  async function describePdf(auth, path2, opts = {}) {
21691
- const buf = await readFile2(path2);
22188
+ const buf = await readFile3(path2);
21692
22189
  const maxBytes = opts.maxBytes ?? 3e7;
21693
22190
  if (buf.length > maxBytes) {
21694
22191
  throw new Error(
@@ -22037,6 +22534,10 @@ function fileSlug(name, id) {
22037
22534
  const base = slugify(name.replace(/\.[^./\\]+$/, "")) || "file";
22038
22535
  return `${base}-${id.slice(0, 8)}`;
22039
22536
  }
22537
+ function fileIdentity(displayName, id) {
22538
+ const label = displayName.trim() || "file";
22539
+ return { slug: fileSlug(displayName, id), name: label, title: label };
22540
+ }
22040
22541
  var MIME_BY_EXT = {
22041
22542
  ".pdf": "application/pdf",
22042
22543
  ".png": "image/png",
@@ -22341,7 +22842,8 @@ function looksLikeUrl(s) {
22341
22842
  const t = s.trim();
22342
22843
  return /^https?:\/\/\S+$/i.test(t) && !/\s/.test(t);
22343
22844
  }
22344
- function readBuffer2(req, maxBytes = 5e7) {
22845
+ var MAX_INGEST_BYTES = 5e7;
22846
+ function readBuffer2(req, maxBytes = MAX_INGEST_BYTES) {
22345
22847
  return new Promise((resolve_, reject) => {
22346
22848
  const chunks = [];
22347
22849
  let size = 0;
@@ -22407,7 +22909,7 @@ async function dispatchIngestRoute(req, res, ctx) {
22407
22909
  const fileId = crypto.randomUUID();
22408
22910
  const { id: id2 } = await createRow(mctx, "files", {
22409
22911
  id: fileId,
22410
- slug: fileSlug(name2, fileId),
22912
+ ...fileIdentity(name2, fileId),
22411
22913
  original_name: name2,
22412
22914
  mime: mime2,
22413
22915
  size_bytes: buf.length,
@@ -22461,7 +22963,7 @@ async function dispatchIngestRoute(req, res, ctx) {
22461
22963
  const textFileId = crypto.randomUUID();
22462
22964
  const { id: id2 } = await createRow(mctx, "files", {
22463
22965
  id: textFileId,
22464
- slug: fileSlug(title, textFileId),
22966
+ ...fileIdentity(title, textFileId),
22465
22967
  original_name: title,
22466
22968
  mime: mime2,
22467
22969
  size_bytes: Buffer.byteLength(content, "utf8"),
@@ -22493,12 +22995,16 @@ async function dispatchIngestRoute(req, res, ctx) {
22493
22995
  sendJson5(res, { error: `file not found: ${abs}` }, 400);
22494
22996
  return true;
22495
22997
  }
22998
+ if (size > MAX_INGEST_BYTES) {
22999
+ sendJson5(res, { error: "file too large" }, 413);
23000
+ return true;
23001
+ }
22496
23002
  const name = basename10(abs);
22497
23003
  const mime = mimeFor(name);
22498
23004
  const localFileId = crypto.randomUUID();
22499
23005
  const { id } = await createRow(mctx, "files", {
22500
23006
  id: localFileId,
22501
- slug: fileSlug(name, localFileId),
23007
+ ...fileIdentity(name, localFileId),
22502
23008
  path: abs,
22503
23009
  original_name: name,
22504
23010
  mime,
@@ -22608,7 +23114,7 @@ function sendText(res, body, status = 200, contentType = "text/plain; charset=ut
22608
23114
  function openUrl(url) {
22609
23115
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
22610
23116
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
22611
- const child = spawn3(command, args, { stdio: "ignore", detached: true });
23117
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
22612
23118
  child.unref();
22613
23119
  }
22614
23120
  function listen(server, port, host) {
@@ -22858,7 +23364,7 @@ function resolveOutputDirForConfig(configPath) {
22858
23364
  }
22859
23365
  return resolve9(base, "context");
22860
23366
  }
22861
- async function openConfig(configPath, outputDir, autoRender = false) {
23367
+ async function openConfig(configPath, outputDir, autoRender = false, teamCloud = false) {
22862
23368
  const parsed = parseConfigFile(configPath);
22863
23369
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
22864
23370
  mkdirSync10(dirname11(parsed.dbPath), { recursive: true });
@@ -22987,6 +23493,7 @@ async function openConfig(configPath, outputDir, autoRender = false) {
22987
23493
  }
22988
23494
  }
22989
23495
  let teamContext = null;
23496
+ let cloudReconnectRequired = false;
22990
23497
  if (db.getDialect() === "postgres") {
22991
23498
  let teamEnabled = false;
22992
23499
  try {
@@ -22994,7 +23501,14 @@ async function openConfig(configPath, outputDir, autoRender = false) {
22994
23501
  } catch {
22995
23502
  teamEnabled = false;
22996
23503
  }
22997
- 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) {
22998
23512
  try {
22999
23513
  const rawDb = parseDocument3(readFileSync15(configPath, "utf8")).get("db");
23000
23514
  const dbLine = typeof rawDb === "string" ? rawDb.trim() : "";
@@ -23036,7 +23550,7 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23036
23550
  }
23037
23551
  }
23038
23552
  let realtime = null;
23039
- if (db.getDialect() === "postgres") {
23553
+ if (db.getDialect() === "postgres" && !cloudReconnectRequired) {
23040
23554
  try {
23041
23555
  realtime = new RealtimeBroker(parsed.dbPath);
23042
23556
  await realtime.start();
@@ -23075,6 +23589,8 @@ async function openConfig(configPath, outputDir, autoRender = false) {
23075
23589
  teamsClient,
23076
23590
  validTables,
23077
23591
  teamContext,
23592
+ teamCloud,
23593
+ cloudReconnectRequired,
23078
23594
  junctionTables,
23079
23595
  entityContextByTable,
23080
23596
  manifest,
@@ -23169,7 +23685,7 @@ async function disposeActive(active) {
23169
23685
  async function reopenSameConfig(active, autoRender) {
23170
23686
  const feed = active.feed;
23171
23687
  await disposeActive(active);
23172
- const next = await openConfig(active.configPath, active.outputDir, autoRender);
23688
+ const next = await openConfig(active.configPath, active.outputDir, autoRender, active.teamCloud);
23173
23689
  next.feed = feed;
23174
23690
  return next;
23175
23691
  }
@@ -23304,7 +23820,7 @@ async function applySchemaConfig(active, entry, direction, autoRender) {
23304
23820
  for (const sql of ddl) await execSql(active.db, sql);
23305
23821
  saveConfigDoc(active.configPath, doc);
23306
23822
  await disposeActive(active);
23307
- return openConfig(active.configPath, active.outputDir, autoRender);
23823
+ return openConfig(active.configPath, active.outputDir, autoRender, active.teamCloud);
23308
23824
  }
23309
23825
  function schemaReverseSummary(verb, entry) {
23310
23826
  const what = entry.operation.replace("schema.", "").replace(/_/g, " ");
@@ -23318,7 +23834,7 @@ async function startGuiServer(options) {
23318
23834
  const teamCloud = options.teamCloud ?? false;
23319
23835
  const autoRender = options.autoRender ?? false;
23320
23836
  const sessionId = crypto.randomUUID();
23321
- let active = await openConfig(configPath, outputDir, autoRender);
23837
+ let active = await openConfig(configPath, outputDir, autoRender, teamCloud);
23322
23838
  const latticeRoot = findLatticeRoot(dirname11(configPath));
23323
23839
  let currentWorkspaceId = null;
23324
23840
  if (latticeRoot) {
@@ -24508,7 +25024,7 @@ data: ${JSON.stringify(data)}
24508
25024
  const paths = resolveWorkspacePaths(latticeRoot, ws);
24509
25025
  let next;
24510
25026
  try {
24511
- next = await openConfig(paths.configPath, paths.contextDir, autoRender);
25027
+ next = await openConfig(paths.configPath, paths.contextDir, autoRender, teamCloud);
24512
25028
  } catch (e) {
24513
25029
  const err = e;
24514
25030
  sendJson(
@@ -24550,7 +25066,12 @@ data: ${JSON.stringify(data)}
24550
25066
  const newPaths = resolveWorkspacePaths(latticeRoot, created);
24551
25067
  let newActive;
24552
25068
  try {
24553
- newActive = await openConfig(newPaths.configPath, newPaths.contextDir, autoRender);
25069
+ newActive = await openConfig(
25070
+ newPaths.configPath,
25071
+ newPaths.contextDir,
25072
+ autoRender,
25073
+ teamCloud
25074
+ );
24554
25075
  } catch (e) {
24555
25076
  sendJson(
24556
25077
  res,
@@ -24609,7 +25130,12 @@ data: ${JSON.stringify(data)}
24609
25130
  const fbPaths = resolveWorkspacePaths(latticeRoot, fallback);
24610
25131
  let next;
24611
25132
  try {
24612
- next = await openConfig(fbPaths.configPath, fbPaths.contextDir, autoRender);
25133
+ next = await openConfig(
25134
+ fbPaths.configPath,
25135
+ fbPaths.contextDir,
25136
+ autoRender,
25137
+ teamCloud
25138
+ );
24613
25139
  } catch (e) {
24614
25140
  const err = e;
24615
25141
  const codePrefix = err.code ? `[${err.code}] ` : "";
@@ -24701,7 +25227,12 @@ data: ${JSON.stringify(data)}
24701
25227
  }
24702
25228
  let next;
24703
25229
  try {
24704
- next = await openConfig(newPath, resolveOutputDirForConfig(newPath), autoRender);
25230
+ next = await openConfig(
25231
+ newPath,
25232
+ resolveOutputDirForConfig(newPath),
25233
+ autoRender,
25234
+ teamCloud
25235
+ );
24705
25236
  } catch (e) {
24706
25237
  const err = e;
24707
25238
  console.error(`[dbconfig.switch] openConfig(${newPath}) failed:`, err);
@@ -24728,7 +25259,8 @@ data: ${JSON.stringify(data)}
24728
25259
  const next = await openConfig(
24729
25260
  newConfigPath,
24730
25261
  resolveOutputDirForConfig(newConfigPath),
24731
- autoRender
25262
+ autoRender,
25263
+ teamCloud
24732
25264
  );
24733
25265
  await disposeActive(active);
24734
25266
  active = next;
@@ -24770,7 +25302,8 @@ data: ${JSON.stringify(data)}
24770
25302
  next = await openConfig(
24771
25303
  fallback.path,
24772
25304
  resolveOutputDirForConfig(fallback.path),
24773
- autoRender
25305
+ autoRender,
25306
+ teamCloud
24774
25307
  );
24775
25308
  } catch (e) {
24776
25309
  const err = e;
@@ -25170,8 +25703,14 @@ data: ${JSON.stringify(data)}
25170
25703
  teamId: active.teamContext.teamId,
25171
25704
  myUserId: active.teamContext.myUserId
25172
25705
  } : null,
25706
+ cloudReconnectRequired: active.cloudReconnectRequired,
25173
25707
  swap: async () => {
25174
- 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
+ );
25175
25714
  await disposeActive(active);
25176
25715
  active = next;
25177
25716
  }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "2.2.2",
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",