@yurtsever/capsa 0.1.0-alpha.2 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +206 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10053,7 +10053,7 @@ function loadSqlite() {
|
|
|
10053
10053
|
}
|
|
10054
10054
|
var DB_DIR = ".capsa";
|
|
10055
10055
|
var DB_FILE = "index.db";
|
|
10056
|
-
var SCHEMA_VERSION =
|
|
10056
|
+
var SCHEMA_VERSION = 3;
|
|
10057
10057
|
function dbPath(projectRoot) {
|
|
10058
10058
|
return join6(projectRoot, DB_DIR, DB_FILE);
|
|
10059
10059
|
}
|
|
@@ -10075,9 +10075,20 @@ var Store = class {
|
|
|
10075
10075
|
const row = this.db.prepare("SELECT value FROM meta WHERE key = 'schema'").get();
|
|
10076
10076
|
return row?.value !== String(SCHEMA_VERSION);
|
|
10077
10077
|
}
|
|
10078
|
-
/**
|
|
10078
|
+
/**
|
|
10079
|
+
* Drop all indexed content (not the log, not decisions) and mark the schema
|
|
10080
|
+
* current. The virtual tables are dropped and recreated, not emptied:
|
|
10081
|
+
* `CREATE VIRTUAL TABLE IF NOT EXISTS` would keep an old tokenizer alive
|
|
10082
|
+
* through a rebuild — which once left FTS without stemming after v2.
|
|
10083
|
+
*/
|
|
10079
10084
|
reset() {
|
|
10080
|
-
this.db.exec(
|
|
10085
|
+
this.db.exec(`
|
|
10086
|
+
DROP TABLE IF EXISTS chunks_fts;
|
|
10087
|
+
DROP TABLE IF EXISTS chunks_vec;
|
|
10088
|
+
DELETE FROM chunks;
|
|
10089
|
+
DELETE FROM items;
|
|
10090
|
+
`);
|
|
10091
|
+
this.migrate();
|
|
10081
10092
|
this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('schema', ?)").run(String(SCHEMA_VERSION));
|
|
10082
10093
|
}
|
|
10083
10094
|
migrate() {
|
|
@@ -10216,6 +10227,10 @@ var Store = class {
|
|
|
10216
10227
|
}
|
|
10217
10228
|
}));
|
|
10218
10229
|
}
|
|
10230
|
+
/** All chunks of one item, in document order. */
|
|
10231
|
+
chunksForItem(itemId) {
|
|
10232
|
+
return this.db.prepare("SELECT id, item_id, idx, heading_path, text FROM chunks WHERE item_id = ? ORDER BY idx").all(itemId);
|
|
10233
|
+
}
|
|
10219
10234
|
listItems() {
|
|
10220
10235
|
return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
|
|
10221
10236
|
}
|
|
@@ -10555,6 +10570,40 @@ function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
|
|
|
10555
10570
|
return scores;
|
|
10556
10571
|
}
|
|
10557
10572
|
|
|
10573
|
+
// ../index/dist/pin.js
|
|
10574
|
+
var LEADING_ID = /^([A-Za-z]{1,8}-?\d{1,5}(?:-\d{1,5})?|\d{2,6})(?=[-_.\s]|$)/;
|
|
10575
|
+
function leadingId(relPath) {
|
|
10576
|
+
const base = relPath.split(/[\\/]/).pop() ?? "";
|
|
10577
|
+
return LEADING_ID.exec(base)?.[1];
|
|
10578
|
+
}
|
|
10579
|
+
function pinnedItems(query, items) {
|
|
10580
|
+
const q = query.toLowerCase();
|
|
10581
|
+
const idCount = /* @__PURE__ */ new Map();
|
|
10582
|
+
for (const item of items) {
|
|
10583
|
+
const id = leadingId(item.rel_path)?.toLowerCase();
|
|
10584
|
+
if (id)
|
|
10585
|
+
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
10586
|
+
}
|
|
10587
|
+
const out = [];
|
|
10588
|
+
for (const item of items) {
|
|
10589
|
+
const rel = item.rel_path.toLowerCase();
|
|
10590
|
+
if (q.includes(rel)) {
|
|
10591
|
+
out.push(item);
|
|
10592
|
+
continue;
|
|
10593
|
+
}
|
|
10594
|
+
const id = leadingId(item.rel_path);
|
|
10595
|
+
if (!id || (idCount.get(id.toLowerCase()) ?? 0) > 1)
|
|
10596
|
+
continue;
|
|
10597
|
+
const re = new RegExp(`(^|[^\\w-])${escape2(id)}(?=$|[^\\w-])`, "i");
|
|
10598
|
+
if (re.test(query))
|
|
10599
|
+
out.push(item);
|
|
10600
|
+
}
|
|
10601
|
+
return out;
|
|
10602
|
+
}
|
|
10603
|
+
function escape2(s) {
|
|
10604
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10605
|
+
}
|
|
10606
|
+
|
|
10558
10607
|
// ../index/dist/retrieve.js
|
|
10559
10608
|
async function retrieve(store, embedder, query, options = {}) {
|
|
10560
10609
|
const started = Date.now();
|
|
@@ -10562,31 +10611,97 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10562
10611
|
const candidates = options.candidates ?? 30;
|
|
10563
10612
|
const weights = options.weights ?? DEFAULT_WEIGHTS;
|
|
10564
10613
|
const minRelative = options.minRelativeScore ?? 0.5;
|
|
10614
|
+
const perItem = /* @__PURE__ */ new Map();
|
|
10615
|
+
const chunks = [];
|
|
10616
|
+
const debug = [];
|
|
10617
|
+
let tokens = 0;
|
|
10618
|
+
const pinned = pinnedItems(query, store.listItems());
|
|
10619
|
+
for (const item of pinned) {
|
|
10620
|
+
for (const c of store.chunksForItem(item.id)) {
|
|
10621
|
+
const t = estimateTokens(c.text);
|
|
10622
|
+
const fits = tokens + t <= maxTokens || chunks.length === 0;
|
|
10623
|
+
if (options.debug) {
|
|
10624
|
+
debug.push({
|
|
10625
|
+
chunkId: c.id,
|
|
10626
|
+
relPath: item.rel_path,
|
|
10627
|
+
headingPath: JSON.parse(c.heading_path),
|
|
10628
|
+
kind: item.kind,
|
|
10629
|
+
status: item.status,
|
|
10630
|
+
vecRank: null,
|
|
10631
|
+
ftsRank: null,
|
|
10632
|
+
fused: 0,
|
|
10633
|
+
factor: 1,
|
|
10634
|
+
score: Number.POSITIVE_INFINITY,
|
|
10635
|
+
kept: fits,
|
|
10636
|
+
why: fits ? "pinned" : "budget"
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
if (!fits)
|
|
10640
|
+
continue;
|
|
10641
|
+
perItem.set(item.id, (perItem.get(item.id) ?? 0) + 1);
|
|
10642
|
+
tokens += t;
|
|
10643
|
+
chunks.push({
|
|
10644
|
+
chunkId: c.id,
|
|
10645
|
+
score: Number.POSITIVE_INFINITY,
|
|
10646
|
+
kind: item.kind,
|
|
10647
|
+
status: item.status,
|
|
10648
|
+
updatedAt: item.updated_at,
|
|
10649
|
+
relPath: item.rel_path,
|
|
10650
|
+
headingPath: JSON.parse(c.heading_path),
|
|
10651
|
+
text: c.text,
|
|
10652
|
+
tokens: t
|
|
10653
|
+
});
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
const pinnedIds = new Set(pinned.map((i) => i.id));
|
|
10657
|
+
const neighbourCutoff = pinned.length ? Math.max(minRelative, 0.75) : minRelative;
|
|
10658
|
+
const neighbourBudget = pinned.length ? Math.min(maxTokens, tokens + Math.floor(maxTokens / 3)) : maxTokens;
|
|
10565
10659
|
const [qv] = await embedder.embed([query]);
|
|
10566
10660
|
const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
|
|
10567
10661
|
const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
|
|
10568
10662
|
const fused = rrf([vec, fts], weights.rrfK);
|
|
10569
|
-
const rows = store.chunksWithItems([...fused.keys()]);
|
|
10663
|
+
const rows = store.chunksWithItems([...fused.keys()]).filter((r) => !pinnedIds.has(r.item_id));
|
|
10570
10664
|
const now = Date.now();
|
|
10571
|
-
const scored = rows.map((r) =>
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
const chunks = [];
|
|
10577
|
-
let tokens = 0;
|
|
10665
|
+
const scored = rows.map((r) => {
|
|
10666
|
+
const fusedScore = fused.get(r.id) ?? 0;
|
|
10667
|
+
const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
|
|
10668
|
+
return { row: r, fusedScore, factor, score: fusedScore * factor };
|
|
10669
|
+
}).sort((a, b) => b.score - a.score);
|
|
10578
10670
|
const best = scored[0]?.score ?? 0;
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
if (
|
|
10671
|
+
let cut = tokens >= maxTokens;
|
|
10672
|
+
let kept = 0;
|
|
10673
|
+
for (const { row, fusedScore, factor, score } of scored) {
|
|
10674
|
+
let why = "kept";
|
|
10675
|
+
if (cut || kept > 0 && score < best * neighbourCutoff) {
|
|
10676
|
+
cut = true;
|
|
10677
|
+
why = "cutoff";
|
|
10678
|
+
} else if ((perItem.get(row.item_id) ?? 0) >= 2) {
|
|
10679
|
+
why = "per-item cap";
|
|
10680
|
+
} else if (tokens + estimateTokens(row.text) > neighbourBudget && chunks.length > 0) {
|
|
10681
|
+
why = "budget";
|
|
10682
|
+
}
|
|
10683
|
+
if (options.debug) {
|
|
10684
|
+
debug.push({
|
|
10685
|
+
chunkId: row.id,
|
|
10686
|
+
relPath: row.item.rel_path,
|
|
10687
|
+
headingPath: JSON.parse(row.heading_path),
|
|
10688
|
+
kind: row.item.kind,
|
|
10689
|
+
status: row.item.status,
|
|
10690
|
+
vecRank: rankIn(vec, row.id),
|
|
10691
|
+
ftsRank: rankIn(fts, row.id),
|
|
10692
|
+
fused: fusedScore,
|
|
10693
|
+
factor,
|
|
10694
|
+
score,
|
|
10695
|
+
kept: why === "kept",
|
|
10696
|
+
why
|
|
10697
|
+
});
|
|
10698
|
+
}
|
|
10699
|
+
if (why !== "kept")
|
|
10584
10700
|
continue;
|
|
10585
10701
|
const t = estimateTokens(row.text);
|
|
10586
|
-
|
|
10587
|
-
continue;
|
|
10588
|
-
perItem.set(row.item_id, used + 1);
|
|
10702
|
+
perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
|
|
10589
10703
|
tokens += t;
|
|
10704
|
+
kept++;
|
|
10590
10705
|
chunks.push({
|
|
10591
10706
|
chunkId: row.id,
|
|
10592
10707
|
score,
|
|
@@ -10598,8 +10713,8 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10598
10713
|
text: row.text,
|
|
10599
10714
|
tokens: t
|
|
10600
10715
|
});
|
|
10601
|
-
if (tokens >=
|
|
10602
|
-
|
|
10716
|
+
if (tokens >= neighbourBudget)
|
|
10717
|
+
cut = true;
|
|
10603
10718
|
}
|
|
10604
10719
|
const durationMs = Date.now() - started;
|
|
10605
10720
|
store.logContext({
|
|
@@ -10609,7 +10724,28 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10609
10724
|
tokensEst: tokens,
|
|
10610
10725
|
durationMs
|
|
10611
10726
|
});
|
|
10612
|
-
return { query, chunks, tokensEst: tokens, durationMs };
|
|
10727
|
+
return { query, chunks, tokensEst: tokens, durationMs, ...options.debug ? { debug } : {} };
|
|
10728
|
+
}
|
|
10729
|
+
function rankIn(list, id) {
|
|
10730
|
+
const i = list.indexOf(id);
|
|
10731
|
+
return i === -1 ? null : i + 1;
|
|
10732
|
+
}
|
|
10733
|
+
function formatDebug(result) {
|
|
10734
|
+
if (!result.debug)
|
|
10735
|
+
return "";
|
|
10736
|
+
const lines = [" vec fts factor score keep where"];
|
|
10737
|
+
for (const d of result.debug) {
|
|
10738
|
+
const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
|
|
10739
|
+
lines.push([
|
|
10740
|
+
String(d.vecRank ?? "-").padStart(5),
|
|
10741
|
+
String(d.ftsRank ?? "-").padStart(4),
|
|
10742
|
+
d.factor.toFixed(2).padStart(7),
|
|
10743
|
+
(Number.isFinite(d.score) ? d.score.toFixed(4) : "pin").padStart(8),
|
|
10744
|
+
(d.kept ? d.why === "pinned" ? "pinned" : "yes" : d.why).padEnd(12),
|
|
10745
|
+
where
|
|
10746
|
+
].join(" "));
|
|
10747
|
+
}
|
|
10748
|
+
return lines.join("\n");
|
|
10613
10749
|
}
|
|
10614
10750
|
function formatContext(result) {
|
|
10615
10751
|
if (result.chunks.length === 0)
|
|
@@ -10631,7 +10767,8 @@ function projectState(store) {
|
|
|
10631
10767
|
for (const i of items)
|
|
10632
10768
|
counts[i.kind] = (counts[i.kind] ?? 0) + 1;
|
|
10633
10769
|
const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
|
|
10634
|
-
const
|
|
10770
|
+
const rank = (i) => i.status === "in-progress" ? 0 : 1;
|
|
10771
|
+
const open2 = items.filter(live).sort((a, b) => rank(a) - rank(b) || a.rel_path.localeCompare(b.rel_path)).map((i) => ({ relPath: i.rel_path, name: i.name, status: i.status ?? "unknown", updatedAt: i.updated_at }));
|
|
10635
10772
|
const recentlyChanged = [...items].sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
|
|
10636
10773
|
const decisions = store.db.prepare("SELECT id, created_at, title FROM decisions ORDER BY id DESC LIMIT 10").all().map((d) => ({ id: d.id, createdAt: d.created_at, title: d.title }));
|
|
10637
10774
|
return { counts, open: open2, recentlyChanged, decisions };
|
|
@@ -40000,17 +40137,37 @@ async function guarded(run) {
|
|
|
40000
40137
|
process.exit(1);
|
|
40001
40138
|
}
|
|
40002
40139
|
}
|
|
40003
|
-
|
|
40004
|
-
|
|
40005
|
-
|
|
40140
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--model", "--tokens"]);
|
|
40141
|
+
function parseArgs(args) {
|
|
40142
|
+
const positional = [];
|
|
40143
|
+
const flags = /* @__PURE__ */ new Map();
|
|
40144
|
+
for (let i = 0; i < args.length; i++) {
|
|
40145
|
+
const arg = args[i];
|
|
40146
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
40147
|
+
const value = args[++i];
|
|
40148
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
40149
|
+
throw new Error(`${arg} expects a value`);
|
|
40150
|
+
}
|
|
40151
|
+
flags.set(arg, value);
|
|
40152
|
+
} else if (arg.startsWith("--")) {
|
|
40153
|
+
flags.set(arg, "true");
|
|
40154
|
+
} else {
|
|
40155
|
+
positional.push(arg);
|
|
40156
|
+
}
|
|
40157
|
+
}
|
|
40158
|
+
return { positional, flags };
|
|
40006
40159
|
}
|
|
40007
|
-
function
|
|
40008
|
-
const
|
|
40009
|
-
|
|
40160
|
+
function tokensFlag(flags) {
|
|
40161
|
+
const raw2 = flags.get("--tokens");
|
|
40162
|
+
if (raw2 === void 0) return void 0;
|
|
40163
|
+
const n = Number(raw2);
|
|
40164
|
+
if (!Number.isInteger(n) || n < 200) throw new Error(`--tokens expects an integer \u2265 200, got "${raw2}"`);
|
|
40165
|
+
return n;
|
|
40010
40166
|
}
|
|
40011
40167
|
async function runIndexCommand(args) {
|
|
40012
|
-
const
|
|
40013
|
-
const
|
|
40168
|
+
const { positional, flags } = parseArgs(args);
|
|
40169
|
+
const root = resolve3(positional[0] ?? process.cwd());
|
|
40170
|
+
const embedder = ollamaEmbedder({ model: flags.get("--model") });
|
|
40014
40171
|
console.error(`capsa: indexing ${root} with ${embedder.model} \u2026`);
|
|
40015
40172
|
const res = await indexProject(root, embedder, (p) => {
|
|
40016
40173
|
if (p.phase === "rebuild") console.error("capsa: index schema changed, rebuilding from scratch \u2026");
|
|
@@ -40023,19 +40180,25 @@ async function runIndexCommand(args) {
|
|
|
40023
40180
|
for (const i of res.issues.slice(0, 10)) console.error(` ! ${i.path}: ${i.message}`);
|
|
40024
40181
|
}
|
|
40025
40182
|
async function runSearchCommand(args) {
|
|
40026
|
-
const positional
|
|
40183
|
+
const { positional, flags } = parseArgs(args);
|
|
40027
40184
|
const query = positional[0];
|
|
40028
40185
|
if (!query) {
|
|
40029
|
-
console.error("capsa search <query> [path]");
|
|
40186
|
+
console.error("capsa search <query> [path] [--tokens n] [--model name] [--debug]");
|
|
40030
40187
|
process.exit(1);
|
|
40031
40188
|
}
|
|
40032
40189
|
const root = resolve3(positional[1] ?? process.cwd());
|
|
40033
|
-
const embedder = ollamaEmbedder({ model:
|
|
40034
|
-
const maxTokens =
|
|
40190
|
+
const embedder = ollamaEmbedder({ model: flags.get("--model") });
|
|
40191
|
+
const maxTokens = tokensFlag(flags);
|
|
40035
40192
|
const store = openStore(root, embedder);
|
|
40036
40193
|
try {
|
|
40037
|
-
const
|
|
40038
|
-
|
|
40194
|
+
const debug = flags.get("--debug") === "true";
|
|
40195
|
+
const res = await retrieve(store, embedder, query, { maxTokens, source: "cli:search", debug });
|
|
40196
|
+
if (debug) {
|
|
40197
|
+
console.log(formatDebug(res));
|
|
40198
|
+
console.log("");
|
|
40199
|
+
} else {
|
|
40200
|
+
console.log(formatContext(res));
|
|
40201
|
+
}
|
|
40039
40202
|
console.error(`
|
|
40040
40203
|
${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
|
|
40041
40204
|
} finally {
|
|
@@ -40043,7 +40206,7 @@ ${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
|
|
|
40043
40206
|
}
|
|
40044
40207
|
}
|
|
40045
40208
|
async function runStateCommand(args) {
|
|
40046
|
-
const root =
|
|
40209
|
+
const root = resolve3(parseArgs(args).positional[0] ?? process.cwd());
|
|
40047
40210
|
const store = openStore(root, ollamaEmbedder());
|
|
40048
40211
|
try {
|
|
40049
40212
|
console.log(formatState(projectState(store)));
|
|
@@ -40052,7 +40215,11 @@ async function runStateCommand(args) {
|
|
|
40052
40215
|
}
|
|
40053
40216
|
}
|
|
40054
40217
|
async function runMcpCommand(args) {
|
|
40055
|
-
|
|
40218
|
+
const { positional, flags } = parseArgs(args);
|
|
40219
|
+
await serveStdio({
|
|
40220
|
+
projectRoot: resolve3(positional[0] ?? process.cwd()),
|
|
40221
|
+
embedder: ollamaEmbedder({ model: flags.get("--model") })
|
|
40222
|
+
});
|
|
40056
40223
|
}
|
|
40057
40224
|
|
|
40058
40225
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yurtsever/capsa",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "Your project's knowledge in one capsule. Local-first project memory and cockpit for AI coding agents: indexes runbooks, tickets, plans and instruction files, serves the smallest relevant context over MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|