@yurtsever/capsa 0.1.0-alpha.2 → 0.1.0-alpha.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/dist/index.js +120 -34
- 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() {
|
|
@@ -10568,24 +10579,47 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10568
10579
|
const fused = rrf([vec, fts], weights.rrfK);
|
|
10569
10580
|
const rows = store.chunksWithItems([...fused.keys()]);
|
|
10570
10581
|
const now = Date.now();
|
|
10571
|
-
const scored = rows.map((r) =>
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10582
|
+
const scored = rows.map((r) => {
|
|
10583
|
+
const fusedScore = fused.get(r.id) ?? 0;
|
|
10584
|
+
const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
|
|
10585
|
+
return { row: r, fusedScore, factor, score: fusedScore * factor };
|
|
10586
|
+
}).sort((a, b) => b.score - a.score);
|
|
10575
10587
|
const perItem = /* @__PURE__ */ new Map();
|
|
10576
10588
|
const chunks = [];
|
|
10589
|
+
const debug = [];
|
|
10577
10590
|
let tokens = 0;
|
|
10578
10591
|
const best = scored[0]?.score ?? 0;
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10592
|
+
let cut = false;
|
|
10593
|
+
for (const { row, fusedScore, factor, score } of scored) {
|
|
10594
|
+
let why = "kept";
|
|
10595
|
+
if (cut || chunks.length > 0 && score < best * minRelative) {
|
|
10596
|
+
cut = true;
|
|
10597
|
+
why = "cutoff";
|
|
10598
|
+
} else if ((perItem.get(row.item_id) ?? 0) >= 2) {
|
|
10599
|
+
why = "per-item cap";
|
|
10600
|
+
} else if (tokens + estimateTokens(row.text) > maxTokens && chunks.length > 0) {
|
|
10601
|
+
why = "budget";
|
|
10602
|
+
}
|
|
10603
|
+
if (options.debug) {
|
|
10604
|
+
debug.push({
|
|
10605
|
+
chunkId: row.id,
|
|
10606
|
+
relPath: row.item.rel_path,
|
|
10607
|
+
headingPath: JSON.parse(row.heading_path),
|
|
10608
|
+
kind: row.item.kind,
|
|
10609
|
+
status: row.item.status,
|
|
10610
|
+
vecRank: rankIn(vec, row.id),
|
|
10611
|
+
ftsRank: rankIn(fts, row.id),
|
|
10612
|
+
fused: fusedScore,
|
|
10613
|
+
factor,
|
|
10614
|
+
score,
|
|
10615
|
+
kept: why === "kept",
|
|
10616
|
+
why
|
|
10617
|
+
});
|
|
10618
|
+
}
|
|
10619
|
+
if (why !== "kept")
|
|
10584
10620
|
continue;
|
|
10585
10621
|
const t = estimateTokens(row.text);
|
|
10586
|
-
|
|
10587
|
-
continue;
|
|
10588
|
-
perItem.set(row.item_id, used + 1);
|
|
10622
|
+
perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
|
|
10589
10623
|
tokens += t;
|
|
10590
10624
|
chunks.push({
|
|
10591
10625
|
chunkId: row.id,
|
|
@@ -10599,7 +10633,7 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10599
10633
|
tokens: t
|
|
10600
10634
|
});
|
|
10601
10635
|
if (tokens >= maxTokens)
|
|
10602
|
-
|
|
10636
|
+
cut = true;
|
|
10603
10637
|
}
|
|
10604
10638
|
const durationMs = Date.now() - started;
|
|
10605
10639
|
store.logContext({
|
|
@@ -10609,7 +10643,28 @@ async function retrieve(store, embedder, query, options = {}) {
|
|
|
10609
10643
|
tokensEst: tokens,
|
|
10610
10644
|
durationMs
|
|
10611
10645
|
});
|
|
10612
|
-
return { query, chunks, tokensEst: tokens, durationMs };
|
|
10646
|
+
return { query, chunks, tokensEst: tokens, durationMs, ...options.debug ? { debug } : {} };
|
|
10647
|
+
}
|
|
10648
|
+
function rankIn(list, id) {
|
|
10649
|
+
const i = list.indexOf(id);
|
|
10650
|
+
return i === -1 ? null : i + 1;
|
|
10651
|
+
}
|
|
10652
|
+
function formatDebug(result) {
|
|
10653
|
+
if (!result.debug)
|
|
10654
|
+
return "";
|
|
10655
|
+
const lines = [" vec fts factor score keep where"];
|
|
10656
|
+
for (const d of result.debug) {
|
|
10657
|
+
const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
|
|
10658
|
+
lines.push([
|
|
10659
|
+
String(d.vecRank ?? "-").padStart(5),
|
|
10660
|
+
String(d.ftsRank ?? "-").padStart(4),
|
|
10661
|
+
d.factor.toFixed(2).padStart(7),
|
|
10662
|
+
d.score.toFixed(4).padStart(8),
|
|
10663
|
+
(d.kept ? "yes" : d.why).padEnd(12),
|
|
10664
|
+
where
|
|
10665
|
+
].join(" "));
|
|
10666
|
+
}
|
|
10667
|
+
return lines.join("\n");
|
|
10613
10668
|
}
|
|
10614
10669
|
function formatContext(result) {
|
|
10615
10670
|
if (result.chunks.length === 0)
|
|
@@ -10631,7 +10686,8 @@ function projectState(store) {
|
|
|
10631
10686
|
for (const i of items)
|
|
10632
10687
|
counts[i.kind] = (counts[i.kind] ?? 0) + 1;
|
|
10633
10688
|
const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
|
|
10634
|
-
const
|
|
10689
|
+
const rank = (i) => i.status === "in-progress" ? 0 : 1;
|
|
10690
|
+
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
10691
|
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
10692
|
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
10693
|
return { counts, open: open2, recentlyChanged, decisions };
|
|
@@ -40000,17 +40056,37 @@ async function guarded(run) {
|
|
|
40000
40056
|
process.exit(1);
|
|
40001
40057
|
}
|
|
40002
40058
|
}
|
|
40003
|
-
|
|
40004
|
-
|
|
40005
|
-
|
|
40059
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--model", "--tokens"]);
|
|
40060
|
+
function parseArgs(args) {
|
|
40061
|
+
const positional = [];
|
|
40062
|
+
const flags = /* @__PURE__ */ new Map();
|
|
40063
|
+
for (let i = 0; i < args.length; i++) {
|
|
40064
|
+
const arg = args[i];
|
|
40065
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
40066
|
+
const value = args[++i];
|
|
40067
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
40068
|
+
throw new Error(`${arg} expects a value`);
|
|
40069
|
+
}
|
|
40070
|
+
flags.set(arg, value);
|
|
40071
|
+
} else if (arg.startsWith("--")) {
|
|
40072
|
+
flags.set(arg, "true");
|
|
40073
|
+
} else {
|
|
40074
|
+
positional.push(arg);
|
|
40075
|
+
}
|
|
40076
|
+
}
|
|
40077
|
+
return { positional, flags };
|
|
40006
40078
|
}
|
|
40007
|
-
function
|
|
40008
|
-
const
|
|
40009
|
-
|
|
40079
|
+
function tokensFlag(flags) {
|
|
40080
|
+
const raw2 = flags.get("--tokens");
|
|
40081
|
+
if (raw2 === void 0) return void 0;
|
|
40082
|
+
const n = Number(raw2);
|
|
40083
|
+
if (!Number.isInteger(n) || n < 200) throw new Error(`--tokens expects an integer \u2265 200, got "${raw2}"`);
|
|
40084
|
+
return n;
|
|
40010
40085
|
}
|
|
40011
40086
|
async function runIndexCommand(args) {
|
|
40012
|
-
const
|
|
40013
|
-
const
|
|
40087
|
+
const { positional, flags } = parseArgs(args);
|
|
40088
|
+
const root = resolve3(positional[0] ?? process.cwd());
|
|
40089
|
+
const embedder = ollamaEmbedder({ model: flags.get("--model") });
|
|
40014
40090
|
console.error(`capsa: indexing ${root} with ${embedder.model} \u2026`);
|
|
40015
40091
|
const res = await indexProject(root, embedder, (p) => {
|
|
40016
40092
|
if (p.phase === "rebuild") console.error("capsa: index schema changed, rebuilding from scratch \u2026");
|
|
@@ -40023,19 +40099,25 @@ async function runIndexCommand(args) {
|
|
|
40023
40099
|
for (const i of res.issues.slice(0, 10)) console.error(` ! ${i.path}: ${i.message}`);
|
|
40024
40100
|
}
|
|
40025
40101
|
async function runSearchCommand(args) {
|
|
40026
|
-
const positional
|
|
40102
|
+
const { positional, flags } = parseArgs(args);
|
|
40027
40103
|
const query = positional[0];
|
|
40028
40104
|
if (!query) {
|
|
40029
|
-
console.error("capsa search <query> [path]");
|
|
40105
|
+
console.error("capsa search <query> [path] [--tokens n] [--model name] [--debug]");
|
|
40030
40106
|
process.exit(1);
|
|
40031
40107
|
}
|
|
40032
40108
|
const root = resolve3(positional[1] ?? process.cwd());
|
|
40033
|
-
const embedder = ollamaEmbedder({ model:
|
|
40034
|
-
const maxTokens =
|
|
40109
|
+
const embedder = ollamaEmbedder({ model: flags.get("--model") });
|
|
40110
|
+
const maxTokens = tokensFlag(flags);
|
|
40035
40111
|
const store = openStore(root, embedder);
|
|
40036
40112
|
try {
|
|
40037
|
-
const
|
|
40038
|
-
|
|
40113
|
+
const debug = flags.get("--debug") === "true";
|
|
40114
|
+
const res = await retrieve(store, embedder, query, { maxTokens, source: "cli:search", debug });
|
|
40115
|
+
if (debug) {
|
|
40116
|
+
console.log(formatDebug(res));
|
|
40117
|
+
console.log("");
|
|
40118
|
+
} else {
|
|
40119
|
+
console.log(formatContext(res));
|
|
40120
|
+
}
|
|
40039
40121
|
console.error(`
|
|
40040
40122
|
${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
|
|
40041
40123
|
} finally {
|
|
@@ -40043,7 +40125,7 @@ ${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
|
|
|
40043
40125
|
}
|
|
40044
40126
|
}
|
|
40045
40127
|
async function runStateCommand(args) {
|
|
40046
|
-
const root =
|
|
40128
|
+
const root = resolve3(parseArgs(args).positional[0] ?? process.cwd());
|
|
40047
40129
|
const store = openStore(root, ollamaEmbedder());
|
|
40048
40130
|
try {
|
|
40049
40131
|
console.log(formatState(projectState(store)));
|
|
@@ -40052,7 +40134,11 @@ async function runStateCommand(args) {
|
|
|
40052
40134
|
}
|
|
40053
40135
|
}
|
|
40054
40136
|
async function runMcpCommand(args) {
|
|
40055
|
-
|
|
40137
|
+
const { positional, flags } = parseArgs(args);
|
|
40138
|
+
await serveStdio({
|
|
40139
|
+
projectRoot: resolve3(positional[0] ?? process.cwd()),
|
|
40140
|
+
embedder: ollamaEmbedder({ model: flags.get("--model") })
|
|
40141
|
+
});
|
|
40056
40142
|
}
|
|
40057
40143
|
|
|
40058
40144
|
// 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.3",
|
|
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",
|