@diegoaltoworks/chatter 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/chunking.d.ts +31 -0
- package/dist/core/chunking.d.ts.map +1 -0
- package/dist/core/retrieval.d.ts +39 -0
- package/dist/core/retrieval.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +211 -25
- package/dist/index.mjs +205 -20
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +201 -19
- package/dist/mcp-server.mjs +200 -18
- package/dist/personas/index.js +4 -2
- package/dist/personas/index.mjs +4 -2
- package/dist/personas/timeContext.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +207 -25
- package/dist/server.mjs +202 -20
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -10
package/dist/mcp-server.mjs
CHANGED
|
@@ -92,6 +92,126 @@ var init_buildLock = __esm({
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
// src/core/chunking.ts
|
|
96
|
+
function headingOf(line) {
|
|
97
|
+
const match = HEADING.exec(line);
|
|
98
|
+
if (!match) return void 0;
|
|
99
|
+
const title = (match[2] ?? "").replace(/[ \t]+#+$/, "").replace(/^#+$/, "").trim();
|
|
100
|
+
return { level: match[1].length, title };
|
|
101
|
+
}
|
|
102
|
+
function startsTable(lines, i) {
|
|
103
|
+
return lines[i].includes("|") && i + 1 < lines.length && TABLE_DELIMITER.test(lines[i + 1]);
|
|
104
|
+
}
|
|
105
|
+
function fenceEnd(lines, start) {
|
|
106
|
+
const opener = FENCE.exec(lines[start]);
|
|
107
|
+
if (!opener) return start + 1;
|
|
108
|
+
const marker = opener[1];
|
|
109
|
+
const closer = new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}[ \\t]*$`);
|
|
110
|
+
for (let i = start + 1; i < lines.length; i++) if (closer.test(lines[i])) return i + 1;
|
|
111
|
+
return lines.length;
|
|
112
|
+
}
|
|
113
|
+
function listEnd(lines, start) {
|
|
114
|
+
let end = start + 1;
|
|
115
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
116
|
+
const line = lines[i];
|
|
117
|
+
if (isBlank(line)) {
|
|
118
|
+
let next = i + 1;
|
|
119
|
+
while (next < lines.length && isBlank(lines[next])) next++;
|
|
120
|
+
if (next < lines.length && (LIST_ITEM.test(lines[next]) || isIndented(lines[next]))) {
|
|
121
|
+
i = next - 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (headingOf(line) && !isIndented(line)) break;
|
|
127
|
+
if (FENCE.test(line) && !isIndented(line)) break;
|
|
128
|
+
end = i + 1;
|
|
129
|
+
}
|
|
130
|
+
return end;
|
|
131
|
+
}
|
|
132
|
+
function tableEnd(lines, start) {
|
|
133
|
+
let end = start + 2;
|
|
134
|
+
while (end < lines.length && !isBlank(lines[end]) && lines[end].includes("|")) end++;
|
|
135
|
+
return end;
|
|
136
|
+
}
|
|
137
|
+
function paragraphEnd(lines, start) {
|
|
138
|
+
let end = start + 1;
|
|
139
|
+
while (end < lines.length && !isBlank(lines[end]) && !headingOf(lines[end]) && !FENCE.test(lines[end]) && !LIST_ITEM.test(lines[end])) {
|
|
140
|
+
end++;
|
|
141
|
+
}
|
|
142
|
+
return end;
|
|
143
|
+
}
|
|
144
|
+
function scan(text) {
|
|
145
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
146
|
+
const sections = [{ trail: [], blocks: [] }];
|
|
147
|
+
const stack = [];
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < lines.length) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
if (isBlank(line)) {
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const heading = FENCE.test(line) ? void 0 : headingOf(line);
|
|
156
|
+
if (heading) {
|
|
157
|
+
while (stack.length && stack[stack.length - 1].level >= heading.level) stack.pop();
|
|
158
|
+
stack.push(heading);
|
|
159
|
+
sections.push({ trail: stack.map((h) => h.title), heading: line, blocks: [] });
|
|
160
|
+
i++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let end;
|
|
164
|
+
if (FENCE.test(line)) end = fenceEnd(lines, i);
|
|
165
|
+
else if (LIST_ITEM.test(line)) end = listEnd(lines, i);
|
|
166
|
+
else if (startsTable(lines, i)) end = tableEnd(lines, i);
|
|
167
|
+
else end = paragraphEnd(lines, i);
|
|
168
|
+
sections[sections.length - 1].blocks.push({ lines: lines.slice(i, end) });
|
|
169
|
+
i = end;
|
|
170
|
+
}
|
|
171
|
+
return sections;
|
|
172
|
+
}
|
|
173
|
+
function chunkSections(text, max = 900) {
|
|
174
|
+
const cap = Math.max(1, max);
|
|
175
|
+
const out = [];
|
|
176
|
+
const emit = (parts, section) => {
|
|
177
|
+
const body = parts.join("\n\n");
|
|
178
|
+
if (body.trim() === "") return;
|
|
179
|
+
out.push({ text: body, section: [...section], position: out.length });
|
|
180
|
+
};
|
|
181
|
+
for (const section of scan(text)) {
|
|
182
|
+
let parts = section.heading ? [section.heading.trim()] : [];
|
|
183
|
+
let size = parts.join("\n\n").length;
|
|
184
|
+
let hasBlock = false;
|
|
185
|
+
for (const block of section.blocks) {
|
|
186
|
+
const piece = block.lines.join("\n");
|
|
187
|
+
const added = piece.length + (parts.length ? 2 : 0);
|
|
188
|
+
if (hasBlock && size + added > cap) {
|
|
189
|
+
emit(parts, section.trail);
|
|
190
|
+
parts = [];
|
|
191
|
+
size = 0;
|
|
192
|
+
hasBlock = false;
|
|
193
|
+
}
|
|
194
|
+
size += piece.length + (parts.length ? 2 : 0);
|
|
195
|
+
parts.push(piece);
|
|
196
|
+
hasBlock = true;
|
|
197
|
+
}
|
|
198
|
+
emit(parts, section.trail);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
var FENCE, HEADING, LIST_ITEM, TABLE_DELIMITER, isBlank, isIndented;
|
|
203
|
+
var init_chunking = __esm({
|
|
204
|
+
"src/core/chunking.ts"() {
|
|
205
|
+
"use strict";
|
|
206
|
+
FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
207
|
+
HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
|
|
208
|
+
LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+\S/;
|
|
209
|
+
TABLE_DELIMITER = /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
|
210
|
+
isBlank = (line) => line.trim() === "";
|
|
211
|
+
isIndented = (line) => /^(?: {2,}|\t)/.test(line);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
95
215
|
// src/core/loaders.ts
|
|
96
216
|
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
97
217
|
import { join as join2 } from "node:path";
|
|
@@ -134,6 +254,7 @@ __export(retrieval_exports, {
|
|
|
134
254
|
openLibsqlClient: () => openLibsqlClient,
|
|
135
255
|
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
136
256
|
});
|
|
257
|
+
import { relative, sep } from "node:path";
|
|
137
258
|
function createOpenAIEmbedder(client) {
|
|
138
259
|
return async (input) => {
|
|
139
260
|
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
@@ -176,6 +297,7 @@ var init_retrieval = __esm({
|
|
|
176
297
|
"src/core/retrieval.ts"() {
|
|
177
298
|
"use strict";
|
|
178
299
|
init_buildLock();
|
|
300
|
+
init_chunking();
|
|
179
301
|
init_loaders();
|
|
180
302
|
init_logger();
|
|
181
303
|
EMB_MODEL = "text-embedding-3-large";
|
|
@@ -186,6 +308,7 @@ var init_retrieval = __esm({
|
|
|
186
308
|
this.embed = embed;
|
|
187
309
|
this.db = options.databaseClient;
|
|
188
310
|
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
311
|
+
this.chunking = options.chunking ?? "lines";
|
|
189
312
|
this.logger = options.logger ?? createConsoleLogger();
|
|
190
313
|
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
191
314
|
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
@@ -199,6 +322,7 @@ var init_retrieval = __esm({
|
|
|
199
322
|
*/
|
|
200
323
|
db;
|
|
201
324
|
knowledgeDir;
|
|
325
|
+
chunking;
|
|
202
326
|
logger;
|
|
203
327
|
buildLock;
|
|
204
328
|
instanceId;
|
|
@@ -259,11 +383,41 @@ var init_retrieval = __esm({
|
|
|
259
383
|
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
260
384
|
return;
|
|
261
385
|
}
|
|
386
|
+
this.logger.info(`Chunking mode: ${this.chunking}`);
|
|
387
|
+
if (this.chunking === "sections") await this.ensureSectionColumns();
|
|
262
388
|
const rows = [];
|
|
263
389
|
for (const d of docs) {
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
390
|
+
if (this.chunking === "sections") {
|
|
391
|
+
const source = relative(this.knowledgeDir, d.source).split(sep).join("/");
|
|
392
|
+
for (const c of chunkSections(d.text)) {
|
|
393
|
+
const section = c.section.join(" > ");
|
|
394
|
+
const id = await sha256(`${d.bucket}|${source}|${section}|${c.text}`);
|
|
395
|
+
const context = section ? `${source} > ${section}` : source;
|
|
396
|
+
rows.push({
|
|
397
|
+
id,
|
|
398
|
+
bucket: d.bucket,
|
|
399
|
+
source,
|
|
400
|
+
text: c.text,
|
|
401
|
+
embedText: `${context}
|
|
402
|
+
|
|
403
|
+
${c.text}`,
|
|
404
|
+
section,
|
|
405
|
+
position: c.position
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
for (const part of chunk(d.text)) {
|
|
410
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
411
|
+
rows.push({
|
|
412
|
+
id,
|
|
413
|
+
bucket: d.bucket,
|
|
414
|
+
source: d.source,
|
|
415
|
+
text: part,
|
|
416
|
+
embedText: part,
|
|
417
|
+
section: null,
|
|
418
|
+
position: null
|
|
419
|
+
});
|
|
420
|
+
}
|
|
267
421
|
}
|
|
268
422
|
}
|
|
269
423
|
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
@@ -272,11 +426,17 @@ var init_retrieval = __esm({
|
|
|
272
426
|
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
273
427
|
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
274
428
|
await this.db.batch(
|
|
275
|
-
batch.map(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
429
|
+
batch.map(
|
|
430
|
+
(r) => this.chunking === "sections" ? {
|
|
431
|
+
sql: `INSERT INTO chunks(id,bucket,source,text,section,position) VALUES(?,?,?,?,?,?)
|
|
432
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
433
|
+
args: [r.id, r.bucket, r.source, r.text, r.section, r.position]
|
|
434
|
+
} : {
|
|
435
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
436
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
437
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
438
|
+
}
|
|
439
|
+
),
|
|
280
440
|
"write"
|
|
281
441
|
);
|
|
282
442
|
}
|
|
@@ -297,7 +457,7 @@ var init_retrieval = __esm({
|
|
|
297
457
|
return;
|
|
298
458
|
}
|
|
299
459
|
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
300
|
-
const textById = new Map(rows.map((r) => [r.id, r.
|
|
460
|
+
const textById = new Map(rows.map((r) => [r.id, r.embedText]));
|
|
301
461
|
const BATCH = 96;
|
|
302
462
|
for (let i = 0; i < missing.length; i += BATCH) {
|
|
303
463
|
const batchIds = missing.slice(i, i + BATCH);
|
|
@@ -318,6 +478,18 @@ var init_retrieval = __esm({
|
|
|
318
478
|
}
|
|
319
479
|
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
320
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Adds the nullable `section`/`position` columns to a `chunks` table that
|
|
483
|
+
* every released version created without them. Checked via table_info so it
|
|
484
|
+
* is a no-op once present; only `'sections'` mode ever calls it.
|
|
485
|
+
*/
|
|
486
|
+
async ensureSectionColumns() {
|
|
487
|
+
const info = await this.db.execute("PRAGMA table_info(chunks)");
|
|
488
|
+
const have = new Set(info.rows.map((r) => String(r.name)));
|
|
489
|
+
if (!have.has("section")) await this.db.execute("ALTER TABLE chunks ADD COLUMN section TEXT");
|
|
490
|
+
if (!have.has("position"))
|
|
491
|
+
await this.db.execute("ALTER TABLE chunks ADD COLUMN position INTEGER");
|
|
492
|
+
}
|
|
321
493
|
// Remove chunks from database that no longer exist in markdown files
|
|
322
494
|
async cleanupStaleChunks(currentIds) {
|
|
323
495
|
const result = await this.db.execute("SELECT id FROM chunks");
|
|
@@ -364,25 +536,34 @@ var init_retrieval = __esm({
|
|
|
364
536
|
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
365
537
|
* the embedding call.
|
|
366
538
|
*/
|
|
367
|
-
async
|
|
539
|
+
async queryChunks(q, k = 6, allowed = ["base"]) {
|
|
368
540
|
if (allowed.length === 0) return [];
|
|
369
541
|
const [qv] = await this.embed([q]);
|
|
370
542
|
const placeholders = allowed.map(() => "?").join(",");
|
|
371
543
|
const res = await this.db.execute({
|
|
372
|
-
sql: `SELECT c
|
|
544
|
+
sql: `SELECT c.*, e.embedding
|
|
373
545
|
FROM chunks c
|
|
374
546
|
JOIN embeddings e ON e.id = c.id
|
|
375
547
|
WHERE c.bucket IN (${placeholders})`,
|
|
376
548
|
args: allowed
|
|
377
549
|
});
|
|
378
|
-
const scored =
|
|
379
|
-
for (const row of res.rows) {
|
|
550
|
+
const scored = res.rows.map((row) => {
|
|
380
551
|
const emb = JSON.parse(String(row.embedding));
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
552
|
+
const trail = row.section == null ? "" : String(row.section);
|
|
553
|
+
return {
|
|
554
|
+
text: String(row.text),
|
|
555
|
+
bucket: String(row.bucket ?? ""),
|
|
556
|
+
source: String(row.source ?? ""),
|
|
557
|
+
section: trail ? trail.split(" > ") : [],
|
|
558
|
+
score: _VectorStore.cosine(qv, emb)
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
scored.sort((a, b) => b.score - a.score);
|
|
562
|
+
return scored.slice(0, k);
|
|
563
|
+
}
|
|
564
|
+
/** Same results as {@link queryChunks}, as bare text. */
|
|
565
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
566
|
+
return (await this.queryChunks(q, k, allowed)).map((c) => c.text);
|
|
386
567
|
}
|
|
387
568
|
};
|
|
388
569
|
}
|
|
@@ -1004,6 +1185,7 @@ async function createMCPServer(config) {
|
|
|
1004
1185
|
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1005
1186
|
databaseClient: db,
|
|
1006
1187
|
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
1188
|
+
chunking: config.chunking,
|
|
1007
1189
|
logger: log
|
|
1008
1190
|
});
|
|
1009
1191
|
}
|
package/dist/personas/index.js
CHANGED
|
@@ -211,7 +211,7 @@ function timeContext(zones, now = Date.now()) {
|
|
|
211
211
|
const at = new Date(now);
|
|
212
212
|
const parts = zones.flatMap((zone) => {
|
|
213
213
|
try {
|
|
214
|
-
const
|
|
214
|
+
const fields = new Intl.DateTimeFormat("en-GB", {
|
|
215
215
|
timeZone: zone,
|
|
216
216
|
weekday: "long",
|
|
217
217
|
day: "numeric",
|
|
@@ -220,7 +220,9 @@ function timeContext(zones, now = Date.now()) {
|
|
|
220
220
|
hour: "2-digit",
|
|
221
221
|
minute: "2-digit",
|
|
222
222
|
hour12: false
|
|
223
|
-
}).
|
|
223
|
+
}).formatToParts(at);
|
|
224
|
+
const part = (type) => fields.find((field) => field.type === type)?.value ?? "";
|
|
225
|
+
const formatted = `${part("weekday")} ${part("day")} ${part("month")} ${part("year")} at ${part("hour")}:${part("minute")}`;
|
|
224
226
|
return [`${formatted} (${zone})`];
|
|
225
227
|
} catch {
|
|
226
228
|
return [];
|
package/dist/personas/index.mjs
CHANGED
|
@@ -183,7 +183,7 @@ function timeContext(zones, now = Date.now()) {
|
|
|
183
183
|
const at = new Date(now);
|
|
184
184
|
const parts = zones.flatMap((zone) => {
|
|
185
185
|
try {
|
|
186
|
-
const
|
|
186
|
+
const fields = new Intl.DateTimeFormat("en-GB", {
|
|
187
187
|
timeZone: zone,
|
|
188
188
|
weekday: "long",
|
|
189
189
|
day: "numeric",
|
|
@@ -192,7 +192,9 @@ function timeContext(zones, now = Date.now()) {
|
|
|
192
192
|
hour: "2-digit",
|
|
193
193
|
minute: "2-digit",
|
|
194
194
|
hour12: false
|
|
195
|
-
}).
|
|
195
|
+
}).formatToParts(at);
|
|
196
|
+
const part = (type) => fields.find((field) => field.type === type)?.value ?? "";
|
|
197
|
+
const formatted = `${part("weekday")} ${part("day")} ${part("month")} ${part("year")} at ${part("hour")}:${part("minute")}`;
|
|
196
198
|
return [`${formatted} (${zone})`];
|
|
197
199
|
} catch {
|
|
198
200
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeContext.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"timeContext.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CA6B7E"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAiB5B,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG;IAAE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEtE;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAiB5B,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG;IAAE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEtE;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAsQ7E"}
|