@tekmidian/pai 0.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/ARCHITECTURE.md +567 -0
- package/FEATURE.md +108 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/auto-route-D7W6RE06.mjs +86 -0
- package/dist/auto-route-D7W6RE06.mjs.map +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +5927 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/config-DBh1bYM2.mjs +151 -0
- package/dist/config-DBh1bYM2.mjs.map +1 -0
- package/dist/daemon/index.d.mts +1 -0
- package/dist/daemon/index.mjs +56 -0
- package/dist/daemon/index.mjs.map +1 -0
- package/dist/daemon-mcp/index.d.mts +1 -0
- package/dist/daemon-mcp/index.mjs +185 -0
- package/dist/daemon-mcp/index.mjs.map +1 -0
- package/dist/daemon-v5O897D4.mjs +773 -0
- package/dist/daemon-v5O897D4.mjs.map +1 -0
- package/dist/db-4lSqLFb8.mjs +199 -0
- package/dist/db-4lSqLFb8.mjs.map +1 -0
- package/dist/db-BcDxXVBu.mjs +110 -0
- package/dist/db-BcDxXVBu.mjs.map +1 -0
- package/dist/detect-BHqYcjJ1.mjs +86 -0
- package/dist/detect-BHqYcjJ1.mjs.map +1 -0
- package/dist/detector-DKA83aTZ.mjs +74 -0
- package/dist/detector-DKA83aTZ.mjs.map +1 -0
- package/dist/embeddings-mfqv-jFu.mjs +91 -0
- package/dist/embeddings-mfqv-jFu.mjs.map +1 -0
- package/dist/factory-BDAiKtYR.mjs +42 -0
- package/dist/factory-BDAiKtYR.mjs.map +1 -0
- package/dist/index.d.mts +307 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/indexer-B20bPHL-.mjs +677 -0
- package/dist/indexer-B20bPHL-.mjs.map +1 -0
- package/dist/indexer-backend-BXaocO5r.mjs +360 -0
- package/dist/indexer-backend-BXaocO5r.mjs.map +1 -0
- package/dist/ipc-client-DPy7s3iu.mjs +156 -0
- package/dist/ipc-client-DPy7s3iu.mjs.map +1 -0
- package/dist/mcp/index.d.mts +1 -0
- package/dist/mcp/index.mjs +373 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/migrate-Bwj7qPaE.mjs +241 -0
- package/dist/migrate-Bwj7qPaE.mjs.map +1 -0
- package/dist/pai-marker-DX_mFLum.mjs +186 -0
- package/dist/pai-marker-DX_mFLum.mjs.map +1 -0
- package/dist/postgres-Ccvpc6fC.mjs +335 -0
- package/dist/postgres-Ccvpc6fC.mjs.map +1 -0
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/schemas-DjdwzIQ8.mjs +3405 -0
- package/dist/schemas-DjdwzIQ8.mjs.map +1 -0
- package/dist/search-PjftDxxs.mjs +282 -0
- package/dist/search-PjftDxxs.mjs.map +1 -0
- package/dist/sqlite-CHUrNtbI.mjs +90 -0
- package/dist/sqlite-CHUrNtbI.mjs.map +1 -0
- package/dist/tools-CLK4080-.mjs +805 -0
- package/dist/tools-CLK4080-.mjs.map +1 -0
- package/dist/utils-DEWdIFQ0.mjs +160 -0
- package/dist/utils-DEWdIFQ0.mjs.map +1 -0
- package/package.json +72 -0
- package/templates/README.md +181 -0
- package/templates/agent-prefs.example.md +362 -0
- package/templates/claude-md.template.md +733 -0
- package/templates/pai-project.template.md +13 -0
- package/templates/voices.example.json +251 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
import { n as populateSlugs, r as searchMemory } from "./search-PjftDxxs.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/topics/detector.ts
|
|
5
|
+
var detector_exports = /* @__PURE__ */ __exportAll({ detectTopicShift: () => detectTopicShift });
|
|
6
|
+
/**
|
|
7
|
+
* Detect whether the provided context text best matches a different project
|
|
8
|
+
* than the session's current routing.
|
|
9
|
+
*
|
|
10
|
+
* Works with either a raw SQLite Database or a StorageBackend.
|
|
11
|
+
* For the StorageBackend path, keyword search is used.
|
|
12
|
+
* For the raw Database path (legacy/direct), searchMemory() is called.
|
|
13
|
+
*/
|
|
14
|
+
async function detectTopicShift(registryDb, federation, params) {
|
|
15
|
+
const threshold = params.threshold ?? .6;
|
|
16
|
+
const candidates = params.candidates ?? 20;
|
|
17
|
+
const currentProject = params.currentProject?.trim() || null;
|
|
18
|
+
if (!params.context || params.context.trim().length === 0) return {
|
|
19
|
+
shifted: false,
|
|
20
|
+
currentProject,
|
|
21
|
+
suggestedProject: null,
|
|
22
|
+
confidence: 0,
|
|
23
|
+
chunkCount: 0,
|
|
24
|
+
topProjects: []
|
|
25
|
+
};
|
|
26
|
+
let results;
|
|
27
|
+
const isBackend = (x) => "backendType" in x;
|
|
28
|
+
if (isBackend(federation)) results = await federation.searchKeyword(params.context, { maxResults: candidates });
|
|
29
|
+
else results = searchMemory(federation, params.context, { maxResults: candidates });
|
|
30
|
+
if (results.length === 0) return {
|
|
31
|
+
shifted: false,
|
|
32
|
+
currentProject,
|
|
33
|
+
suggestedProject: null,
|
|
34
|
+
confidence: 0,
|
|
35
|
+
chunkCount: 0,
|
|
36
|
+
topProjects: []
|
|
37
|
+
};
|
|
38
|
+
const withSlugs = populateSlugs(results, registryDb);
|
|
39
|
+
const projectScores = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const r of withSlugs) {
|
|
41
|
+
const slug = r.projectSlug;
|
|
42
|
+
if (!slug) continue;
|
|
43
|
+
projectScores.set(slug, (projectScores.get(slug) ?? 0) + r.score);
|
|
44
|
+
}
|
|
45
|
+
if (projectScores.size === 0) return {
|
|
46
|
+
shifted: false,
|
|
47
|
+
currentProject,
|
|
48
|
+
suggestedProject: null,
|
|
49
|
+
confidence: 0,
|
|
50
|
+
chunkCount: withSlugs.length,
|
|
51
|
+
topProjects: []
|
|
52
|
+
};
|
|
53
|
+
const ranked = Array.from(projectScores.entries()).sort((a, b) => b[1] - a[1]);
|
|
54
|
+
const totalScore = ranked.reduce((sum, [, s]) => sum + s, 0);
|
|
55
|
+
const topProjects = ranked.slice(0, 3).map(([slug, score]) => ({
|
|
56
|
+
slug,
|
|
57
|
+
score: totalScore > 0 ? score / totalScore : 0
|
|
58
|
+
}));
|
|
59
|
+
const topSlug = ranked[0][0];
|
|
60
|
+
const topRawScore = ranked[0][1];
|
|
61
|
+
const confidence = totalScore > 0 ? topRawScore / totalScore : 0;
|
|
62
|
+
return {
|
|
63
|
+
shifted: currentProject !== null && topSlug !== currentProject && confidence >= threshold,
|
|
64
|
+
currentProject,
|
|
65
|
+
suggestedProject: topSlug,
|
|
66
|
+
confidence,
|
|
67
|
+
chunkCount: withSlugs.length,
|
|
68
|
+
topProjects
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { detector_exports as n, detectTopicShift as t };
|
|
74
|
+
//# sourceMappingURL=detector-DKA83aTZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector-DKA83aTZ.mjs","names":[],"sources":["../src/topics/detector.ts"],"sourcesContent":["/**\n * Topic shift detection engine.\n *\n * Accepts a context summary (recent conversation text) and determines whether\n * the conversation has drifted away from the currently-routed project.\n *\n * Algorithm:\n * 1. Run keyword memory_search against the context text (no project filter)\n * 2. Score results by project — sum of BM25 scores per project\n * 3. Compare the top-scoring project against the current project\n * 4. If a different project dominates by more than the confidence threshold,\n * report a topic shift.\n *\n * Design decisions:\n * - Keyword search only (no semantic) — fast, no embedding requirement\n * - Works with or without an active daemon (direct DB access path)\n * - Stateless: callers supply currentProject; detector has no session memory\n * - Minimal: returns a plain result object, not MCP content arrays\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { searchMemory, populateSlugs } from \"../memory/search.js\";\nimport type { SearchResult } from \"../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicCheckParams {\n /** Recent conversation context (a few sentences or tool call summaries) */\n context: string;\n /** The project slug the session is currently routed to. May be null/empty. */\n currentProject?: string;\n /**\n * Minimum confidence [0,1] to declare a shift. Default: 0.6.\n * Higher = less sensitive, fewer false positives.\n */\n threshold?: number;\n /**\n * Maximum results to draw from memory search (candidates). Default: 20.\n * More candidates = more accurate scoring, slightly slower.\n */\n candidates?: number;\n}\n\nexport interface TopicCheckResult {\n /** Whether a significant topic shift was detected. */\n shifted: boolean;\n /** The project slug the session is currently routed to (echoed from input). */\n currentProject: string | null;\n /** The project slug that best matches the context, or null if no clear match. */\n suggestedProject: string | null;\n /**\n * Confidence score for the suggested project [0,1].\n * Represents the fraction of total score mass held by the top project.\n * 1.0 = all matching chunks belong to one project.\n * 0.5 = two projects are equally matched.\n */\n confidence: number;\n /** Number of memory chunks that contributed to scoring. */\n chunkCount: number;\n /** Top-3 scoring projects with their normalised scores (for debugging). */\n topProjects: Array<{ slug: string; score: number }>;\n}\n\n// ---------------------------------------------------------------------------\n// Core algorithm\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the provided context text best matches a different project\n * than the session's current routing.\n *\n * Works with either a raw SQLite Database or a StorageBackend.\n * For the StorageBackend path, keyword search is used.\n * For the raw Database path (legacy/direct), searchMemory() is called.\n */\nexport async function detectTopicShift(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: TopicCheckParams\n): Promise<TopicCheckResult> {\n const threshold = params.threshold ?? 0.6;\n const candidates = params.candidates ?? 20;\n const currentProject = params.currentProject?.trim() || null;\n\n if (!params.context || params.context.trim().length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // -------------------------------------------------------------------------\n // Run memory search across ALL projects (no project filter)\n // -------------------------------------------------------------------------\n\n let results: SearchResult[];\n\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n results = await federation.searchKeyword(params.context, {\n maxResults: candidates,\n });\n } else {\n results = searchMemory(federation, params.context, {\n maxResults: candidates,\n });\n }\n\n if (results.length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // Populate project slugs from the registry\n const withSlugs = populateSlugs(results, registryDb);\n\n // -------------------------------------------------------------------------\n // Score projects by summing BM25 scores of matching chunks\n // -------------------------------------------------------------------------\n\n const projectScores = new Map<string, number>();\n\n for (const r of withSlugs) {\n const slug = r.projectSlug;\n if (!slug) continue;\n projectScores.set(slug, (projectScores.get(slug) ?? 0) + r.score);\n }\n\n if (projectScores.size === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: withSlugs.length,\n topProjects: [],\n };\n }\n\n // Sort by total score descending\n const ranked = Array.from(projectScores.entries())\n .sort((a, b) => b[1] - a[1]);\n\n const totalScore = ranked.reduce((sum, [, s]) => sum + s, 0);\n\n // Top-3 for reporting (normalised to [0,1] fraction of total mass)\n const topProjects = ranked.slice(0, 3).map(([slug, score]) => ({\n slug,\n score: totalScore > 0 ? score / totalScore : 0,\n }));\n\n const topSlug = ranked[0][0];\n const topRawScore = ranked[0][1];\n const confidence = totalScore > 0 ? topRawScore / totalScore : 0;\n\n // -------------------------------------------------------------------------\n // Determine if a shift occurred\n // -------------------------------------------------------------------------\n\n // A shift is detected when:\n // 1. confidence >= threshold (the top project dominates)\n // 2. The top project is different from currentProject\n // 3. There is a currentProject to compare against\n // (if no current project, we still return the best match but no \"shift\")\n\n const isDifferent =\n currentProject !== null &&\n topSlug !== currentProject;\n\n const shifted = isDifferent && confidence >= threshold;\n\n return {\n shifted,\n currentProject,\n suggestedProject: topSlug,\n confidence,\n chunkCount: withSlugs.length,\n topProjects,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA8EA,eAAsB,iBACpB,YACA,YACA,QAC2B;CAC3B,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,iBAAiB,OAAO,gBAAgB,MAAM,IAAI;AAExD,KAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,MAAM,CAAC,WAAW,EACtD,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAOH,IAAI;CAEJ,MAAM,aAAa,MACjB,iBAAiB;AAEnB,KAAI,UAAU,WAAW,CACvB,WAAU,MAAM,WAAW,cAAc,OAAO,SAAS,EACvD,YAAY,YACb,CAAC;KAEF,WAAU,aAAa,YAAY,OAAO,SAAS,EACjD,YAAY,YACb,CAAC;AAGJ,KAAI,QAAQ,WAAW,EACrB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAIH,MAAM,YAAY,cAAc,SAAS,WAAW;CAMpD,MAAM,gCAAgB,IAAI,KAAqB;AAE/C,MAAK,MAAM,KAAK,WAAW;EACzB,MAAM,OAAO,EAAE;AACf,MAAI,CAAC,KAAM;AACX,gBAAc,IAAI,OAAO,cAAc,IAAI,KAAK,IAAI,KAAK,EAAE,MAAM;;AAGnE,KAAI,cAAc,SAAS,EACzB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY,UAAU;EACtB,aAAa,EAAE;EAChB;CAIH,MAAM,SAAS,MAAM,KAAK,cAAc,SAAS,CAAC,CAC/C,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG;CAE9B,MAAM,aAAa,OAAO,QAAQ,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE;CAG5D,MAAM,cAAc,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,YAAY;EAC7D;EACA,OAAO,aAAa,IAAI,QAAQ,aAAa;EAC9C,EAAE;CAEH,MAAM,UAAU,OAAO,GAAG;CAC1B,MAAM,cAAc,OAAO,GAAG;CAC9B,MAAM,aAAa,aAAa,IAAI,cAAc,aAAa;AAkB/D,QAAO;EACL,SANA,mBAAmB,QACnB,YAAY,kBAEiB,cAAc;EAI3C;EACA,kBAAkB;EAClB;EACA,YAAY,UAAU;EACtB;EACD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/memory/embeddings.ts
|
|
4
|
+
var embeddings_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
configureEmbeddingModel: () => configureEmbeddingModel,
|
|
6
|
+
cosineSimilarity: () => cosineSimilarity,
|
|
7
|
+
deserializeEmbedding: () => deserializeEmbedding,
|
|
8
|
+
generateEmbedding: () => generateEmbedding,
|
|
9
|
+
serializeEmbedding: () => serializeEmbedding
|
|
10
|
+
});
|
|
11
|
+
const DEFAULT_EMBEDDING_MODEL = "Snowflake/snowflake-arctic-embed-m-v1.5";
|
|
12
|
+
/** Query prefix required by Snowflake Arctic Embed for retrieval tasks. */
|
|
13
|
+
const QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
|
|
14
|
+
let _embeddingPipeline = null;
|
|
15
|
+
let _currentModel = null;
|
|
16
|
+
/**
|
|
17
|
+
* Configure the embedding model to use.
|
|
18
|
+
* Must be called before the first generateEmbedding() call.
|
|
19
|
+
* If the pipeline is already loaded with a different model, it will be reloaded.
|
|
20
|
+
*
|
|
21
|
+
* @param model HuggingFace model ID (e.g. "Snowflake/snowflake-arctic-embed-m-v1.5").
|
|
22
|
+
* Pass undefined or empty string to use the default model.
|
|
23
|
+
*/
|
|
24
|
+
function configureEmbeddingModel(model) {
|
|
25
|
+
const resolved = model?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
26
|
+
if (_currentModel !== null && _currentModel !== resolved) _embeddingPipeline = null;
|
|
27
|
+
_currentModel = resolved;
|
|
28
|
+
}
|
|
29
|
+
async function getEmbedder() {
|
|
30
|
+
const model = _currentModel ?? DEFAULT_EMBEDDING_MODEL;
|
|
31
|
+
if (!_embeddingPipeline) {
|
|
32
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
33
|
+
_embeddingPipeline = await pipeline("feature-extraction", model, { dtype: "q8" });
|
|
34
|
+
}
|
|
35
|
+
return _embeddingPipeline;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a normalized 768-dim embedding for the given text.
|
|
39
|
+
*
|
|
40
|
+
* Uses CLS pooling (first token) and L2 normalization (cosine similarity ready).
|
|
41
|
+
*
|
|
42
|
+
* @param text The text to embed.
|
|
43
|
+
* @param isQuery If true, prepend the Snowflake query prefix. Use for search queries.
|
|
44
|
+
* Documents should be embedded without the prefix (default: false).
|
|
45
|
+
*/
|
|
46
|
+
async function generateEmbedding(text, isQuery = false) {
|
|
47
|
+
const input = (isQuery ? QUERY_PREFIX : "") + text;
|
|
48
|
+
const output = await (await getEmbedder())(input, {
|
|
49
|
+
pooling: "cls",
|
|
50
|
+
normalize: true
|
|
51
|
+
});
|
|
52
|
+
return new Float32Array(output.data);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Serialize a Float32Array to a Buffer for storage in a SQLite BLOB column.
|
|
56
|
+
*/
|
|
57
|
+
function serializeEmbedding(vec) {
|
|
58
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Deserialize a Buffer (from a SQLite BLOB column) back into a Float32Array.
|
|
62
|
+
*/
|
|
63
|
+
function deserializeEmbedding(blob) {
|
|
64
|
+
return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Compute cosine similarity between two normalized embedding vectors.
|
|
68
|
+
*
|
|
69
|
+
* Since both vectors are already L2-normalized by the embedding model,
|
|
70
|
+
* cosine similarity reduces to a dot product — but we compute the full
|
|
71
|
+
* formula for correctness when embeddings may not be pre-normalized.
|
|
72
|
+
*
|
|
73
|
+
* Returns a value in [-1, 1] where 1 = identical.
|
|
74
|
+
*/
|
|
75
|
+
function cosineSimilarity(a, b) {
|
|
76
|
+
let dot = 0;
|
|
77
|
+
let normA = 0;
|
|
78
|
+
let normB = 0;
|
|
79
|
+
for (let i = 0; i < a.length; i++) {
|
|
80
|
+
dot += a[i] * b[i];
|
|
81
|
+
normA += a[i] * a[i];
|
|
82
|
+
normB += b[i] * b[i];
|
|
83
|
+
}
|
|
84
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
85
|
+
if (denom === 0) return 0;
|
|
86
|
+
return dot / denom;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { embeddings_exports as i, cosineSimilarity as n, deserializeEmbedding as r, configureEmbeddingModel as t };
|
|
91
|
+
//# sourceMappingURL=embeddings-mfqv-jFu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddings-mfqv-jFu.mjs","names":[],"sources":["../src/memory/embeddings.ts"],"sourcesContent":["/**\n * Embedding generation for the PAI federation memory engine (Phase 2.5).\n *\n * Uses @huggingface/transformers with the Snowflake/snowflake-arctic-embed-m-v1.5 model\n * (768 dims, q8 quantization, MTEB strong retrieval quality).\n *\n * The model uses CLS pooling (first token) — NOT mean pooling.\n * For retrieval, queries require a prefix: \"Represent this sentence for searching relevant passages: \"\n * Documents should be embedded WITHOUT a prefix.\n *\n * The pipeline is a lazy singleton — loaded on first call, reused thereafter.\n * This avoids loading the heavy ML model on every CLI invocation.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 768;\nconst DEFAULT_EMBEDDING_MODEL = \"Snowflake/snowflake-arctic-embed-m-v1.5\";\n\n/** Query prefix required by Snowflake Arctic Embed for retrieval tasks. */\nconst QUERY_PREFIX = \"Represent this sentence for searching relevant passages: \";\n\n// ---------------------------------------------------------------------------\n// Lazy pipeline singleton\n// ---------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _embeddingPipeline: any = null;\nlet _currentModel: string | null = null;\n\n/**\n * Configure the embedding model to use.\n * Must be called before the first generateEmbedding() call.\n * If the pipeline is already loaded with a different model, it will be reloaded.\n *\n * @param model HuggingFace model ID (e.g. \"Snowflake/snowflake-arctic-embed-m-v1.5\").\n * Pass undefined or empty string to use the default model.\n */\nexport function configureEmbeddingModel(model?: string): void {\n const resolved = model?.trim() || DEFAULT_EMBEDDING_MODEL;\n if (_currentModel !== null && _currentModel !== resolved) {\n // Model changed — force reload on next call\n _embeddingPipeline = null;\n }\n _currentModel = resolved;\n}\n\nasync function getEmbedder() {\n const model = _currentModel ?? DEFAULT_EMBEDDING_MODEL;\n if (!_embeddingPipeline) {\n // Dynamic import to avoid loading the ML runtime on startup\n const { pipeline } = await import(\"@huggingface/transformers\");\n _embeddingPipeline = await pipeline(\n \"feature-extraction\",\n model,\n { dtype: \"q8\" },\n );\n }\n return _embeddingPipeline;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a normalized 768-dim embedding for the given text.\n *\n * Uses CLS pooling (first token) and L2 normalization (cosine similarity ready).\n *\n * @param text The text to embed.\n * @param isQuery If true, prepend the Snowflake query prefix. Use for search queries.\n * Documents should be embedded without the prefix (default: false).\n */\nexport async function generateEmbedding(text: string, isQuery: boolean = false): Promise<Float32Array> {\n const prefix = isQuery ? QUERY_PREFIX : \"\";\n const input = prefix + text;\n const extractor = await getEmbedder();\n // Snowflake Arctic Embed uses CLS pooling (first token), not mean pooling\n const output = await extractor(input, { pooling: \"cls\", normalize: true });\n return new Float32Array(output.data);\n}\n\n// ---------------------------------------------------------------------------\n// Serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a Float32Array to a Buffer for storage in a SQLite BLOB column.\n */\nexport function serializeEmbedding(vec: Float32Array): Buffer {\n return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);\n}\n\n/**\n * Deserialize a Buffer (from a SQLite BLOB column) back into a Float32Array.\n */\nexport function deserializeEmbedding(blob: Buffer): Float32Array {\n return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Similarity computation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute cosine similarity between two normalized embedding vectors.\n *\n * Since both vectors are already L2-normalized by the embedding model,\n * cosine similarity reduces to a dot product — but we compute the full\n * formula for correctness when embeddings may not be pre-normalized.\n *\n * Returns a value in [-1, 1] where 1 = identical.\n */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB);\n if (denom === 0) return 0;\n return dot / denom;\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,0BAA0B;;AAGhC,MAAM,eAAe;AAOrB,IAAI,qBAA0B;AAC9B,IAAI,gBAA+B;;;;;;;;;AAUnC,SAAgB,wBAAwB,OAAsB;CAC5D,MAAM,WAAW,OAAO,MAAM,IAAI;AAClC,KAAI,kBAAkB,QAAQ,kBAAkB,SAE9C,sBAAqB;AAEvB,iBAAgB;;AAGlB,eAAe,cAAc;CAC3B,MAAM,QAAQ,iBAAiB;AAC/B,KAAI,CAAC,oBAAoB;EAEvB,MAAM,EAAE,aAAa,MAAM,OAAO;AAClC,uBAAqB,MAAM,SACzB,sBACA,OACA,EAAE,OAAO,MAAM,CAChB;;AAEH,QAAO;;;;;;;;;;;AAgBT,eAAsB,kBAAkB,MAAc,UAAmB,OAA8B;CAErG,MAAM,SADS,UAAU,eAAe,MACjB;CAGvB,MAAM,SAAS,OAFG,MAAM,aAAa,EAEN,OAAO;EAAE,SAAS;EAAO,WAAW;EAAM,CAAC;AAC1E,QAAO,IAAI,aAAa,OAAO,KAAK;;;;;AAUtC,SAAgB,mBAAmB,KAA2B;AAC5D,QAAO,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,WAAW;;;;;AAMhE,SAAgB,qBAAqB,MAA4B;AAC/D,QAAO,IAAI,aAAa,KAAK,QAAQ,KAAK,YAAY,KAAK,aAAa,EAAE;;;;;;;;;;;AAgB5E,SAAgB,iBAAiB,GAAiB,GAAyB;CACzE,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAO,EAAE,KAAK,EAAE;AAChB,WAAS,EAAE,KAAK,EAAE;AAClB,WAAS,EAAE,KAAK,EAAE;;CAEpB,MAAM,QAAQ,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,MAAM;AACjD,KAAI,UAAU,EAAG,QAAO;AACxB,QAAO,MAAM"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/storage/factory.ts
|
|
4
|
+
var factory_exports = /* @__PURE__ */ __exportAll({ createStorageBackend: () => createStorageBackend });
|
|
5
|
+
/**
|
|
6
|
+
* Create and return the configured StorageBackend.
|
|
7
|
+
*
|
|
8
|
+
* Auto-fallback behaviour:
|
|
9
|
+
* - storageBackend = "sqlite" → SQLiteBackend always
|
|
10
|
+
* - storageBackend = "postgres" → PostgresBackend if reachable, else SQLiteBackend
|
|
11
|
+
*/
|
|
12
|
+
async function createStorageBackend(config) {
|
|
13
|
+
if (config.storageBackend === "postgres") return await tryPostgres(config);
|
|
14
|
+
return createSQLiteBackend();
|
|
15
|
+
}
|
|
16
|
+
async function tryPostgres(config) {
|
|
17
|
+
try {
|
|
18
|
+
const { PostgresBackend } = await import("./postgres-Ccvpc6fC.mjs");
|
|
19
|
+
const backend = new PostgresBackend(config.postgres ?? {});
|
|
20
|
+
const err = await backend.testConnection();
|
|
21
|
+
if (err) {
|
|
22
|
+
process.stderr.write(`[pai-daemon] Postgres unavailable (${err}). Falling back to SQLite.\n`);
|
|
23
|
+
await backend.close();
|
|
24
|
+
return createSQLiteBackend();
|
|
25
|
+
}
|
|
26
|
+
process.stderr.write("[pai-daemon] Connected to PostgreSQL backend.\n");
|
|
27
|
+
return backend;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
30
|
+
process.stderr.write(`[pai-daemon] Postgres init error (${msg}). Falling back to SQLite.\n`);
|
|
31
|
+
return createSQLiteBackend();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function createSQLiteBackend() {
|
|
35
|
+
const { openFederation } = await import("./db-BcDxXVBu.mjs").then((n) => n.t);
|
|
36
|
+
const { SQLiteBackend } = await import("./sqlite-CHUrNtbI.mjs");
|
|
37
|
+
return new SQLiteBackend(openFederation());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { factory_exports as n, createStorageBackend as t };
|
|
42
|
+
//# sourceMappingURL=factory-BDAiKtYR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory-BDAiKtYR.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n * If Postgres is configured but unavailable, falls back to SQLite with\n * a warning log — the daemon never crashes due to a missing Postgres.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-fallback behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend if reachable, else SQLiteBackend\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await tryPostgres(config);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\nasync function tryPostgres(config: PaiDaemonConfig): Promise<StorageBackend> {\n try {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n const backend = new PostgresBackend(pgConfig);\n\n const err = await backend.testConnection();\n if (err) {\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${err}). Falling back to SQLite.\\n`\n );\n await backend.close();\n return createSQLiteBackend();\n }\n\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n return backend;\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Postgres init error (${msg}). Falling back to SQLite.\\n`\n );\n return createSQLiteBackend();\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;;;;;;;;;;AAkBA,eAAsB,qBACpB,QACyB;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,YAAY,OAAO;AAIlC,QAAO,qBAAqB;;AAG9B,eAAe,YAAY,QAAkD;AAC3E,KAAI;EACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;EAEzC,MAAM,UAAU,IAAI,gBADH,OAAO,YAAY,EAAE,CACO;EAE7C,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,WAAQ,OAAO,MACb,sCAAsC,IAAI,8BAC3C;AACD,SAAM,QAAQ,OAAO;AACrB,UAAO,qBAAqB;;AAG9B,UAAQ,OAAO,MAAM,kDAAkD;AACvE,SAAO;UACA,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MACb,qCAAqC,IAAI,8BAC1C;AACD,SAAO,qBAAqB;;;AAIhC,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Database, Database as Database$1, Database as Database$2 } from "better-sqlite3";
|
|
2
|
+
|
|
3
|
+
//#region src/registry/schema.d.ts
|
|
4
|
+
declare const SCHEMA_VERSION = 3;
|
|
5
|
+
declare const CREATE_TABLES_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n slug TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n root_path TEXT NOT NULL UNIQUE,\n encoded_dir TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL DEFAULT 'local'\n CHECK(type IN ('local','central','obsidian-linked','external')),\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('active','archived','migrating')),\n parent_id INTEGER,\n obsidian_link TEXT,\n claude_notes_dir TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n number INTEGER NOT NULL,\n date TEXT NOT NULL,\n slug TEXT NOT NULL,\n title TEXT NOT NULL,\n filename TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open'\n CHECK(status IN ('open','completed','compacted')),\n claude_session_id TEXT,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n closed_at INTEGER,\n UNIQUE (project_id, number),\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n project_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (project_id, tag_id),\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n session_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (session_id, tag_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n alias TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n session_id INTEGER,\n trigger TEXT NOT NULL\n CHECK(trigger IN ('precompact','manual','end-session')),\n files_written TEXT NOT NULL,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);\n";
|
|
6
|
+
/**
|
|
7
|
+
* Run the full DDL against an open database connection.
|
|
8
|
+
*
|
|
9
|
+
* The function is idempotent — every statement uses IF NOT EXISTS so it is
|
|
10
|
+
* safe to call on an already-initialised database. After creating the tables
|
|
11
|
+
* it inserts the current SCHEMA_VERSION into schema_version if no row exists
|
|
12
|
+
* yet.
|
|
13
|
+
*/
|
|
14
|
+
declare function initializeSchema(db: Database): void;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/registry/db.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Open (or create) the PAI registry database.
|
|
19
|
+
*
|
|
20
|
+
* @param path Absolute path to registry.db. Defaults to ~/.pai/registry.db.
|
|
21
|
+
* @returns An open better-sqlite3 Database instance.
|
|
22
|
+
*
|
|
23
|
+
* Side effects on first call:
|
|
24
|
+
* - Creates the parent directory if it does not exist.
|
|
25
|
+
* - Enables WAL journal mode.
|
|
26
|
+
* - Runs initializeSchema() if schema_version is empty.
|
|
27
|
+
*/
|
|
28
|
+
declare function openRegistry(path?: string): Database$2;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/registry/migrate.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Reverse Claude Code's directory encoding.
|
|
33
|
+
*
|
|
34
|
+
* Claude Code's actual encoding rules:
|
|
35
|
+
* - `/` (path separator) → `-`
|
|
36
|
+
* - ` ` (space) → `--` (escaped)
|
|
37
|
+
* - `.` (dot) → `--` (escaped)
|
|
38
|
+
* - `-` (literal hyphen) → `--` (escaped)
|
|
39
|
+
*
|
|
40
|
+
* Because space, dot, and hyphen all encode to `--`, the encoding is
|
|
41
|
+
* **lossy** — you cannot unambiguously reverse it. This function therefore
|
|
42
|
+
* provides a *best-effort* heuristic decode (treating `--` as a literal `-`
|
|
43
|
+
* which gives wrong results for paths with spaces or dots).
|
|
44
|
+
*
|
|
45
|
+
* PREFER using {@link buildEncodedDirMap} to get the authoritative mapping
|
|
46
|
+
* from session-registry.json instead of calling this function directly.
|
|
47
|
+
*
|
|
48
|
+
* Examples (best-effort, may be wrong for paths with spaces/dots):
|
|
49
|
+
* `-Users-alice-dev-apps-MyProject` → `/Users/alice/dev/apps/MyProject`
|
|
50
|
+
* `-Users-alice--ssh` → `/Users/alice/-ssh` ← WRONG (actually .ssh)
|
|
51
|
+
*
|
|
52
|
+
* @param encoded The Claude-encoded directory name.
|
|
53
|
+
* @param lookupMap Optional authoritative map from {@link buildEncodedDirMap}.
|
|
54
|
+
* If provided and the key is found, that value is returned
|
|
55
|
+
* instead of the heuristic result.
|
|
56
|
+
*/
|
|
57
|
+
declare function decodeEncodedDir(encoded: string, lookupMap?: Map<string, string>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Derive a URL-safe kebab-case slug from an arbitrary string.
|
|
60
|
+
*
|
|
61
|
+
* Uses the last path component so that `/Users/alice/dev/my-app` → `my-app`.
|
|
62
|
+
*/
|
|
63
|
+
declare function slugify(value: string): string;
|
|
64
|
+
interface ParsedSession {
|
|
65
|
+
number: number;
|
|
66
|
+
date: string;
|
|
67
|
+
slug: string;
|
|
68
|
+
title: string;
|
|
69
|
+
filename: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Attempt to parse a session note filename into its structured parts.
|
|
73
|
+
*
|
|
74
|
+
* Returns `null` if the filename does not match either known format.
|
|
75
|
+
*/
|
|
76
|
+
declare function parseSessionFilename(filename: string): ParsedSession | null;
|
|
77
|
+
interface MigrationResult {
|
|
78
|
+
projectsInserted: number;
|
|
79
|
+
projectsSkipped: number;
|
|
80
|
+
sessionsInserted: number;
|
|
81
|
+
errors: string[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Migrate the existing JSON session-registry into the SQLite registry.
|
|
85
|
+
*
|
|
86
|
+
* @param db Open better-sqlite3 Database (target).
|
|
87
|
+
* @param registryPath Path to session-registry.json.
|
|
88
|
+
* Defaults to ~/.claude/session-registry.json.
|
|
89
|
+
*
|
|
90
|
+
* The migration is idempotent: projects and sessions that already exist
|
|
91
|
+
* (matched by slug / project_id+number) are silently skipped.
|
|
92
|
+
*/
|
|
93
|
+
declare function migrateFromJson(db: Database, registryPath?: string): MigrationResult;
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/registry/pai-marker.d.ts
|
|
96
|
+
/**
|
|
97
|
+
* PAI.md marker file management.
|
|
98
|
+
*
|
|
99
|
+
* Each registered project gets a `Notes/PAI.md` file with a YAML frontmatter
|
|
100
|
+
* `pai:` block that PAI manages. The rest of the file (body content, other
|
|
101
|
+
* frontmatter keys) is user-owned and never modified by PAI.
|
|
102
|
+
*
|
|
103
|
+
* YAML parsing/updating is done with simple regex — no external dependency.
|
|
104
|
+
*/
|
|
105
|
+
interface PaiMarker {
|
|
106
|
+
/** Absolute path to the PAI.md file */
|
|
107
|
+
path: string;
|
|
108
|
+
/** The `slug` value from the `pai:` frontmatter block */
|
|
109
|
+
slug: string;
|
|
110
|
+
/** Absolute path to the project root (parent of Notes/) */
|
|
111
|
+
projectRoot: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create or update `<projectRoot>/Notes/PAI.md`.
|
|
115
|
+
*
|
|
116
|
+
* - File absent: creates `Notes/` if needed, writes from template.
|
|
117
|
+
* - File present: updates only the `pai:` frontmatter block; body and all
|
|
118
|
+
* other frontmatter keys are preserved verbatim.
|
|
119
|
+
*
|
|
120
|
+
* @param projectRoot Absolute path to the project root directory.
|
|
121
|
+
* @param slug PAI slug for this project.
|
|
122
|
+
* @param displayName Human-readable name (defaults to slug if omitted).
|
|
123
|
+
*/
|
|
124
|
+
declare function ensurePaiMarker(projectRoot: string, slug: string, displayName?: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* Read PAI marker data from `<projectRoot>/Notes/PAI.md`.
|
|
127
|
+
* Returns null if the file does not exist or contains no `pai:` block.
|
|
128
|
+
*/
|
|
129
|
+
declare function readPaiMarker(projectRoot: string): {
|
|
130
|
+
slug: string;
|
|
131
|
+
registered: string;
|
|
132
|
+
status: string;
|
|
133
|
+
} | null;
|
|
134
|
+
/**
|
|
135
|
+
* Scan a list of parent directories for `<child>/Notes/PAI.md` marker files.
|
|
136
|
+
* Each directory in `searchDirs` is scanned one level deep — its immediate
|
|
137
|
+
* child directories are checked for a `Notes/PAI.md` file.
|
|
138
|
+
*
|
|
139
|
+
* Returns an array of PaiMarker objects for every valid marker found.
|
|
140
|
+
* Invalid or malformed markers are silently skipped.
|
|
141
|
+
*
|
|
142
|
+
* @param searchDirs Absolute paths to parent directories.
|
|
143
|
+
*/
|
|
144
|
+
declare function discoverPaiMarkers(searchDirs: string[]): PaiMarker[];
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/memory/schema.d.ts
|
|
147
|
+
declare const FEDERATION_SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n";
|
|
148
|
+
/**
|
|
149
|
+
* Apply the full federation schema to an open database.
|
|
150
|
+
*
|
|
151
|
+
* Idempotent — all statements use IF NOT EXISTS so calling this on an
|
|
152
|
+
* already-initialised database is safe.
|
|
153
|
+
*
|
|
154
|
+
* Also runs any necessary migrations for existing databases (e.g. adding the
|
|
155
|
+
* embedding column to an older schema that was created without it).
|
|
156
|
+
*/
|
|
157
|
+
declare function initializeFederationSchema(db: Database): void;
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/memory/db.d.ts
|
|
160
|
+
/**
|
|
161
|
+
* Open (or create) the PAI federation database.
|
|
162
|
+
*
|
|
163
|
+
* @param path Absolute path to federation.db. Defaults to ~/.pai/federation.db.
|
|
164
|
+
* @returns An open better-sqlite3 Database instance.
|
|
165
|
+
*
|
|
166
|
+
* Side effects on first call:
|
|
167
|
+
* - Creates the parent directory if it does not exist.
|
|
168
|
+
* - Enables WAL journal mode.
|
|
169
|
+
* - Runs initializeFederationSchema() to ensure tables exist.
|
|
170
|
+
*/
|
|
171
|
+
declare function openFederation(path?: string): Database$1;
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/memory/chunker.d.ts
|
|
174
|
+
/**
|
|
175
|
+
* Markdown text chunker for the PAI memory engine.
|
|
176
|
+
*
|
|
177
|
+
* Splits markdown files into overlapping text segments suitable for BM25
|
|
178
|
+
* full-text indexing. Respects heading boundaries where possible, falling
|
|
179
|
+
* back to paragraph and sentence splitting when sections are large.
|
|
180
|
+
*/
|
|
181
|
+
interface Chunk {
|
|
182
|
+
text: string;
|
|
183
|
+
startLine: number;
|
|
184
|
+
endLine: number;
|
|
185
|
+
hash: string;
|
|
186
|
+
}
|
|
187
|
+
interface ChunkOptions {
|
|
188
|
+
/** Approximate maximum tokens per chunk. Default 400. */
|
|
189
|
+
maxTokens?: number;
|
|
190
|
+
/** Overlap in tokens from the previous chunk. Default 80. */
|
|
191
|
+
overlap?: number;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Approximate token count using a words * 1.3 heuristic.
|
|
195
|
+
* Matches the OpenClaw estimate approach.
|
|
196
|
+
*/
|
|
197
|
+
declare function estimateTokens(text: string): number;
|
|
198
|
+
/**
|
|
199
|
+
* Chunk a markdown file into overlapping segments for BM25 indexing.
|
|
200
|
+
*
|
|
201
|
+
* Strategy:
|
|
202
|
+
* 1. Split by headings (##, ###) as natural boundaries.
|
|
203
|
+
* 2. If a section exceeds maxTokens, split by paragraphs.
|
|
204
|
+
* 3. If a paragraph still exceeds maxTokens, split by sentences.
|
|
205
|
+
* 4. Apply overlap: each chunk includes the last `overlap` tokens from the
|
|
206
|
+
* previous chunk.
|
|
207
|
+
*/
|
|
208
|
+
declare function chunkMarkdown(content: string, opts?: ChunkOptions): Chunk[];
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/memory/indexer.d.ts
|
|
211
|
+
interface IndexResult {
|
|
212
|
+
filesProcessed: number;
|
|
213
|
+
chunksCreated: number;
|
|
214
|
+
filesSkipped: number;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Classify a relative file path into one of the four memory tiers.
|
|
218
|
+
*
|
|
219
|
+
* Rules (in priority order):
|
|
220
|
+
* - MEMORY.md anywhere in memory/ → 'evergreen'
|
|
221
|
+
* - YYYY-MM-DD.md in memory/ → 'daily'
|
|
222
|
+
* - anything else in memory/ → 'topic'
|
|
223
|
+
* - anything in Notes/ → 'session'
|
|
224
|
+
*/
|
|
225
|
+
declare function detectTier(relativePath: string): "evergreen" | "daily" | "topic" | "session";
|
|
226
|
+
/**
|
|
227
|
+
* Index a single file into the federation database.
|
|
228
|
+
*
|
|
229
|
+
* @returns true if the file was re-indexed (changed or new), false if skipped.
|
|
230
|
+
*/
|
|
231
|
+
declare function indexFile(db: Database, projectId: number, rootPath: string, relativePath: string, source: string, tier: string): boolean;
|
|
232
|
+
declare function indexProject(db: Database, projectId: number, rootPath: string, claudeNotesDir?: string | null): Promise<IndexResult>;
|
|
233
|
+
/**
|
|
234
|
+
* Index all active projects registered in the registry DB.
|
|
235
|
+
*
|
|
236
|
+
* Async: yields to the event loop between each project so that the daemon's
|
|
237
|
+
* Unix socket server can process IPC requests (e.g. status) while indexing.
|
|
238
|
+
*/
|
|
239
|
+
declare function indexAll(db: Database, registryDb: Database): Promise<{
|
|
240
|
+
projects: number;
|
|
241
|
+
result: IndexResult;
|
|
242
|
+
}>;
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/memory/search.d.ts
|
|
245
|
+
interface SearchResult {
|
|
246
|
+
projectId: number;
|
|
247
|
+
projectSlug?: string;
|
|
248
|
+
path: string;
|
|
249
|
+
startLine: number;
|
|
250
|
+
endLine: number;
|
|
251
|
+
snippet: string;
|
|
252
|
+
score: number;
|
|
253
|
+
tier: string;
|
|
254
|
+
source: string;
|
|
255
|
+
}
|
|
256
|
+
interface SearchOptions {
|
|
257
|
+
/** Restrict search to these project IDs. */
|
|
258
|
+
projectIds?: number[];
|
|
259
|
+
/** Restrict to 'memory' or 'notes' sources. */
|
|
260
|
+
sources?: string[];
|
|
261
|
+
/** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */
|
|
262
|
+
tiers?: string[];
|
|
263
|
+
/** Maximum number of results to return. Default 10. */
|
|
264
|
+
maxResults?: number;
|
|
265
|
+
/** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */
|
|
266
|
+
minScore?: number;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Convert a free-text query into an FTS5 query string.
|
|
270
|
+
*
|
|
271
|
+
* Strategy:
|
|
272
|
+
* 1. Tokenise by whitespace and punctuation
|
|
273
|
+
* 2. Remove stop words and tokens shorter than 2 characters
|
|
274
|
+
* 3. Double-quote each remaining token (exact word form)
|
|
275
|
+
* 4. Join with OR so that any matching token returns a result
|
|
276
|
+
*
|
|
277
|
+
* Using OR instead of AND is critical for multi-word queries: the words rarely
|
|
278
|
+
* all appear in the same chunk, so AND would return zero results. FTS5 BM25
|
|
279
|
+
* scoring naturally ranks chunks where more terms match higher, so the most
|
|
280
|
+
* relevant chunks still surface at the top.
|
|
281
|
+
*
|
|
282
|
+
* Example: "Synchrotech interview follow-up Gilles"
|
|
283
|
+
* → `"synchrotech" OR "interview" OR "follow" OR "gilles"`
|
|
284
|
+
* → chunks matching any term, ranked by how many terms match
|
|
285
|
+
*/
|
|
286
|
+
declare function buildFtsQuery(query: string): string;
|
|
287
|
+
/**
|
|
288
|
+
* Search across all indexed memory using FTS5 BM25 ranking.
|
|
289
|
+
*
|
|
290
|
+
* Results are ordered by BM25 score (most relevant first).
|
|
291
|
+
* FTS5 bm25() returns negative values; closer to 0 = more relevant.
|
|
292
|
+
* We negate the score so callers get positive values where higher = better.
|
|
293
|
+
*
|
|
294
|
+
* Multilingual note: SQLite FTS5 uses the `unicode61` tokenizer by default,
|
|
295
|
+
* which handles Unicode correctly (German umlauts, French accents, etc.) without
|
|
296
|
+
* language-specific stemming. No changes needed here — it is already
|
|
297
|
+
* multilingual-safe.
|
|
298
|
+
*/
|
|
299
|
+
declare function searchMemory(db: Database, query: string, opts?: SearchOptions): SearchResult[];
|
|
300
|
+
/**
|
|
301
|
+
* Populate the projectSlug field on search results by looking up project IDs
|
|
302
|
+
* in the registry database.
|
|
303
|
+
*/
|
|
304
|
+
declare function populateSlugs(results: SearchResult[], registryDb: Database): SearchResult[];
|
|
305
|
+
//#endregion
|
|
306
|
+
export { CREATE_TABLES_SQL, type Chunk, type ChunkOptions, FEDERATION_SCHEMA_SQL, type IndexResult, type MigrationResult, type PaiMarker, SCHEMA_VERSION, type SearchOptions, type SearchResult, buildFtsQuery, chunkMarkdown, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, searchMemory, slugify };
|
|
307
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts","../src/registry/migrate.ts","../src/registry/pai-marker.ts","../src/memory/schema.ts","../src/memory/db.ts","../src/memory/chunker.ts","../src/memory/indexer.ts","../src/memory/search.ts"],"mappings":";;;cAgBa,cAAA;AAAA,cAEA,iBAAA;;;;;;ACYb;;;iBDyGgB,gBAAA,CAAiB,EAAA,EAAI,QAAA;;;AArHrC;;;;;AAqHA;;;;;;AArHA,iBCYgB,YAAA,CAAa,IAAA,YAAuC,UAAA;;;;;;;AC0HpE;;;;;AAUC;;;;;;;;;;;AAyBD;;;;;AAmCA;iBArGgB,gBAAA,CACd,OAAA,UACA,SAAA,GAAY,GAAA;;;;;;iBA6BE,OAAA,CAAQ,KAAA;AAAA,UAgBd,aAAA;EACR,MAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;EACA,QAAA;AAAA;;;;;;iBAcc,oBAAA,CACd,QAAA,WACC,aAAA;AAAA,UAiCc,eAAA;EACf,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,MAAA;AAAA;;;;;;AClDF;;;;;iBD+DgB,eAAA,CACd,EAAA,EAAI,QAAA,EACJ,YAAA,YACC,eAAA;;;;;;AFlOH;;;;;AAEA;UGMiB,SAAA;;EAEf,IAAA;EHR4B;EGU5B,IAAA;EH2G8B;EGzG9B,WAAA;AAAA;;;;;AFAF;;;;;;;iBEkJgB,eAAA,CACd,WAAA,UACA,IAAA,UACA,WAAA;AD1DF;;;;AAAA,iBC2HgB,aAAA,CACd,WAAA;EACG,IAAA;EAAc,UAAA;EAAoB,MAAA;AAAA;AD9FvC;;;;;AAUC;;;;;AAVD,iBC2HgB,kBAAA,CAAmB,UAAA,aAAuB,SAAA;;;cC7P7C,qBAAA;;AHQb;;;;;;;;iBG+CgB,0BAAA,CAA2B,EAAA,EAAI,QAAA;;;AJ3D/C;;;;;AAqHA;;;;;;AArHA,iBKYgB,cAAA,CAAe,IAAA,YAAyC,UAAA;;;;;;ALdxE;;;;UMNiB,KAAA;EACf,IAAA;EACA,SAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,UAGe,YAAA;ENsHe;EMpH9B,SAAA;ENoHmC;EMlHnC,OAAA;AAAA;;;ALSF;;iBKCgB,cAAA,CAAe,IAAA;;;;;;AJ0F/B;;;;;iBIoFgB,aAAA,CAAc,OAAA,UAAiB,IAAA,GAAO,YAAA,GAAe,KAAA;;;UCrLpD,WAAA;EACf,cAAA;EACA,aAAA;EACA,YAAA;AAAA;;;;ANGF;;;;;;iBMagB,UAAA,CACd,YAAA;;AL6EF;;;;iBKVgB,SAAA,CACd,EAAA,EAAI,QAAA,EACJ,SAAA,UACA,QAAA,UACA,YAAA,UACA,MAAA,UACA,IAAA;AAAA,iBA6UoB,YAAA,CACpB,EAAA,EAAI,QAAA,EACJ,SAAA,UACA,QAAA,UACA,cAAA,mBACC,OAAA,CAAQ,WAAA;;;;AL/SX;;;iBKuesB,QAAA,CACpB,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,QAAA,GACX,OAAA;EAAU,QAAA;EAAkB,MAAA,EAAQ,WAAA;AAAA;;;UC/mBtB,YAAA;EACf,SAAA;EACA,WAAA;EACA,IAAA;EACA,SAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,aAAA;EPDY;EOG3B,UAAA;;EAEA,OAAA;;EAEA,KAAA;ENoF8B;EMlF9B,UAAA;ENoFe;EMlFf,QAAA;AAAA;;;;AN+GF;;;;;AAUC;;;;;;;;;;iBMnFe,aAAA,CAAc,KAAA;AN4G9B;;;;;AAmCA;;;;;;;AAnCA,iBM1EgB,YAAA,CACd,EAAA,EAAI,QAAA,EACJ,KAAA,UACA,IAAA,GAAO,aAAA,GACN,YAAA;AL2DH;;;;AAAA,iBKwNgB,aAAA,CACd,OAAA,EAAS,YAAA,IACT,UAAA,EAAY,QAAA,GACX,YAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { a as initializeSchema, i as SCHEMA_VERSION, n as openRegistry, r as CREATE_TABLES_SQL } from "./db-4lSqLFb8.mjs";
|
|
2
|
+
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, r as migrateFromJson } from "./migrate-Bwj7qPaE.mjs";
|
|
3
|
+
import { n as ensurePaiMarker, r as readPaiMarker, t as discoverPaiMarkers } from "./pai-marker-DX_mFLum.mjs";
|
|
4
|
+
import { i as initializeFederationSchema, n as openFederation, r as FEDERATION_SCHEMA_SQL } from "./db-BcDxXVBu.mjs";
|
|
5
|
+
import { a as indexProject, i as indexFile, o as chunkMarkdown, r as indexAll, s as estimateTokens, t as detectTier } from "./indexer-B20bPHL-.mjs";
|
|
6
|
+
import "./embeddings-mfqv-jFu.mjs";
|
|
7
|
+
import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-PjftDxxs.mjs";
|
|
8
|
+
import "./tools-CLK4080-.mjs";
|
|
9
|
+
import "./mcp/index.mjs";
|
|
10
|
+
|
|
11
|
+
export { CREATE_TABLES_SQL, FEDERATION_SCHEMA_SQL, SCHEMA_VERSION, buildFtsQuery, chunkMarkdown, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, searchMemory, slugify };
|