@tekmidian/pai 0.25.2 → 0.26.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/auto-route-BWGvvpcP.mjs +86 -0
- package/dist/auto-route-BWGvvpcP.mjs.map +1 -0
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/program.mjs +2 -2
- package/dist/clusters-BdvGIoD-.mjs +201 -0
- package/dist/clusters-BdvGIoD-.mjs.map +1 -0
- package/dist/daemon/index.mjs +6 -5
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BaBTOB1P.mjs +1356 -0
- package/dist/daemon-BaBTOB1P.mjs.map +1 -0
- package/dist/detector-DExEQ5cW.mjs +74 -0
- package/dist/detector-DExEQ5cW.mjs.map +1 -0
- package/dist/factory-CAy0N1xR.mjs +142 -0
- package/dist/factory-CAy0N1xR.mjs.map +1 -0
- package/dist/indexer-backend-vOJqSE0U.mjs +299 -0
- package/dist/indexer-backend-vOJqSE0U.mjs.map +1 -0
- package/dist/latent-ideas-DuM_kgkW.mjs +191 -0
- package/dist/latent-ideas-DuM_kgkW.mjs.map +1 -0
- package/dist/neighborhood-DSkvFMAv.mjs +135 -0
- package/dist/neighborhood-DSkvFMAv.mjs.map +1 -0
- package/dist/note-context-DrcY4cWm.mjs +126 -0
- package/dist/note-context-DrcY4cWm.mjs.map +1 -0
- package/dist/pick-FrfZ_iY7.mjs +13299 -0
- package/dist/pick-FrfZ_iY7.mjs.map +1 -0
- package/dist/postgres-DTyxU4B1.mjs +891 -0
- package/dist/postgres-DTyxU4B1.mjs.map +1 -0
- package/dist/query-feedback-BX5nSyRm.mjs +76 -0
- package/dist/query-feedback-BX5nSyRm.mjs.map +1 -0
- package/dist/router-B2xR3gVP.mjs +228 -0
- package/dist/router-B2xR3gVP.mjs.map +1 -0
- package/dist/sqlite--BBAyXLH.mjs +271 -0
- package/dist/sqlite--BBAyXLH.mjs.map +1 -0
- package/dist/themes-DICajLf-.mjs +148 -0
- package/dist/themes-DICajLf-.mjs.map +1 -0
- package/dist/tools-DMAQxlOk.mjs +1939 -0
- package/dist/tools-DMAQxlOk.mjs.map +1 -0
- package/dist/trace-DfyGmMG_.mjs +137 -0
- package/dist/trace-DfyGmMG_.mjs.map +1 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs +536 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs.map +1 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs +1856 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs.map +1 -0
- package/dist/zettelkasten-C8BikWss.mjs +1063 -0
- package/dist/zettelkasten-C8BikWss.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { r as readPaiMarker } from "./pai-marker-B20KqhA8.mjs";
|
|
2
|
+
import { t as detectProject } from "./detect-Bf2z-oKB.mjs";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/session/auto-route.ts
|
|
7
|
+
/**
|
|
8
|
+
* Determine which project a session should be routed to.
|
|
9
|
+
*
|
|
10
|
+
* @param registryDb Open PAI registry database
|
|
11
|
+
* @param federation Memory storage backend (needed only for topic fallback)
|
|
12
|
+
* @param cwd Working directory to detect from (defaults to process.cwd())
|
|
13
|
+
* @param context Optional conversation text for topic-based fallback
|
|
14
|
+
* @returns Best project match, or null if nothing matched
|
|
15
|
+
*/
|
|
16
|
+
async function autoRoute(registryDb, federation, cwd, context) {
|
|
17
|
+
const target = resolve(cwd ?? process.cwd());
|
|
18
|
+
const pathMatch = detectProject(registryDb, target);
|
|
19
|
+
if (pathMatch) return {
|
|
20
|
+
slug: pathMatch.slug,
|
|
21
|
+
display_name: pathMatch.display_name,
|
|
22
|
+
root_path: pathMatch.root_path,
|
|
23
|
+
method: "path",
|
|
24
|
+
confidence: 1
|
|
25
|
+
};
|
|
26
|
+
const markerResult = findMarkerUpward(registryDb, target);
|
|
27
|
+
if (markerResult) return markerResult;
|
|
28
|
+
if (context && context.trim().length > 0) {
|
|
29
|
+
const { detectTopicShift } = await import("./detector-DExEQ5cW.mjs").then((n) => n.n);
|
|
30
|
+
const topicResult = await detectTopicShift(registryDb, federation, {
|
|
31
|
+
context,
|
|
32
|
+
threshold: .5
|
|
33
|
+
});
|
|
34
|
+
if (topicResult.suggestedProject && topicResult.confidence > 0) {
|
|
35
|
+
const projectRow = registryDb.prepare("SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'").get(topicResult.suggestedProject);
|
|
36
|
+
if (projectRow) return {
|
|
37
|
+
slug: projectRow.slug,
|
|
38
|
+
display_name: projectRow.display_name,
|
|
39
|
+
root_path: projectRow.root_path,
|
|
40
|
+
method: "topic",
|
|
41
|
+
confidence: topicResult.confidence
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Walk up the directory tree from `startDir`, checking each level for a
|
|
49
|
+
* `Notes/PAI.md` file. If found, read the slug and look up the project.
|
|
50
|
+
*
|
|
51
|
+
* Stops at the filesystem root or after 20 levels (safety guard).
|
|
52
|
+
*/
|
|
53
|
+
function findMarkerUpward(registryDb, startDir) {
|
|
54
|
+
let current = startDir;
|
|
55
|
+
let depth = 0;
|
|
56
|
+
while (depth < 20) {
|
|
57
|
+
if (existsSync(`${current}/Notes/PAI.md`)) {
|
|
58
|
+
const marker = readPaiMarker(current);
|
|
59
|
+
if (marker && marker.status !== "archived") {
|
|
60
|
+
const projectRow = registryDb.prepare("SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'").get(marker.slug);
|
|
61
|
+
if (projectRow) return {
|
|
62
|
+
slug: projectRow.slug,
|
|
63
|
+
display_name: projectRow.display_name,
|
|
64
|
+
root_path: projectRow.root_path,
|
|
65
|
+
method: "marker",
|
|
66
|
+
confidence: 1
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parent = dirname(current);
|
|
71
|
+
if (parent === current) break;
|
|
72
|
+
current = parent;
|
|
73
|
+
depth++;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Format an AutoRouteResult as JSON for machine consumption.
|
|
79
|
+
*/
|
|
80
|
+
function formatAutoRouteJson(result) {
|
|
81
|
+
return JSON.stringify(result, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { autoRoute, formatAutoRouteJson };
|
|
86
|
+
//# sourceMappingURL=auto-route-BWGvvpcP.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-route-BWGvvpcP.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -7,12 +7,12 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
7
7
|
import "../sync--BoxBBok.mjs";
|
|
8
8
|
import "../embeddings-Bn86ssxR.mjs";
|
|
9
9
|
import "../search-CpTv1I24.mjs";
|
|
10
|
-
import "../pick-
|
|
10
|
+
import "../pick-FrfZ_iY7.mjs";
|
|
11
11
|
import "../checkpoint-block-D75rhsPY.mjs";
|
|
12
12
|
import "../indexer-AEcT8wHf.mjs";
|
|
13
13
|
import "../ipc-client-aVKVERjJ.mjs";
|
|
14
14
|
import "../config-CcdkNSWa.mjs";
|
|
15
|
-
import "../factory-
|
|
15
|
+
import "../factory-CAy0N1xR.mjs";
|
|
16
16
|
import "../main-resolver-BozXCqpl.mjs";
|
|
17
17
|
import { buildProgram } from "./program.mjs";
|
|
18
18
|
|
package/dist/cli/program.mjs
CHANGED
|
@@ -6,12 +6,12 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
6
6
|
import "../sync--BoxBBok.mjs";
|
|
7
7
|
import "../embeddings-Bn86ssxR.mjs";
|
|
8
8
|
import "../search-CpTv1I24.mjs";
|
|
9
|
-
import { A as registerProjectsCommands, C as registerRestoreCommands, D as registerIdentityCommands, E as registerMcpCommands, M as resolveIdentifier, O as registerMemoryCommands, S as registerSetupCommand, T as registerDaemonCommands, _ as registerUpdateCommand, a as cmdEnd, b as registerZettelCommands, c as cmdGoto, d as registerHelpCommand, f as registerDbCommands, g as registerNotifyCommands, h as registerTaskCommands, i as cmdPauseAll, j as findMovedPath, k as registerRegistryCommands, l as cmdPause, m as registerTopicCommands, n as cmdFind, o as registerSessionCleanupCommand, p as registerKgCommands, r as cmdClearNames, s as registerSessionCommands, t as cmdPick, u as cmdList, v as registerSkillCommands, w as registerBackupCommands, x as registerObsidianCommands, y as registerObservationCommands } from "../pick-
|
|
9
|
+
import { A as registerProjectsCommands, C as registerRestoreCommands, D as registerIdentityCommands, E as registerMcpCommands, M as resolveIdentifier, O as registerMemoryCommands, S as registerSetupCommand, T as registerDaemonCommands, _ as registerUpdateCommand, a as cmdEnd, b as registerZettelCommands, c as cmdGoto, d as registerHelpCommand, f as registerDbCommands, g as registerNotifyCommands, h as registerTaskCommands, i as cmdPauseAll, j as findMovedPath, k as registerRegistryCommands, l as cmdPause, m as registerTopicCommands, n as cmdFind, o as registerSessionCleanupCommand, p as registerKgCommands, r as cmdClearNames, s as registerSessionCommands, t as cmdPick, u as cmdList, v as registerSkillCommands, w as registerBackupCommands, x as registerObsidianCommands, y as registerObservationCommands } from "../pick-FrfZ_iY7.mjs";
|
|
10
10
|
import "../checkpoint-block-D75rhsPY.mjs";
|
|
11
11
|
import "../indexer-AEcT8wHf.mjs";
|
|
12
12
|
import "../ipc-client-aVKVERjJ.mjs";
|
|
13
13
|
import "../config-CcdkNSWa.mjs";
|
|
14
|
-
import "../factory-
|
|
14
|
+
import "../factory-CAy0N1xR.mjs";
|
|
15
15
|
import { t as cmdMain } from "../main-resolver-BozXCqpl.mjs";
|
|
16
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { t as STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/graph/clusters.ts
|
|
4
|
+
/**
|
|
5
|
+
* Query pai_observations (Postgres) for observation types associated with
|
|
6
|
+
* the given file paths. Returns a map from vault_path → type counts.
|
|
7
|
+
*
|
|
8
|
+
* Falls back to an empty map when the pool is not available or the query fails.
|
|
9
|
+
*/
|
|
10
|
+
async function fetchObservationTypes(pool, filePaths, projectId) {
|
|
11
|
+
if (filePaths.length === 0) return /* @__PURE__ */ new Map();
|
|
12
|
+
try {
|
|
13
|
+
const params = [...filePaths];
|
|
14
|
+
let projectFilter = "";
|
|
15
|
+
if (projectId !== void 0) {
|
|
16
|
+
params.push(projectId);
|
|
17
|
+
projectFilter = `AND project_id = $${params.length}`;
|
|
18
|
+
}
|
|
19
|
+
const result = await pool.query(`SELECT unnested_path AS path, type, COUNT(*) AS cnt
|
|
20
|
+
FROM pai_observations,
|
|
21
|
+
LATERAL unnest(files_modified || files_read) AS unnested_path
|
|
22
|
+
WHERE unnested_path = ANY($1::text[])
|
|
23
|
+
${projectFilter}
|
|
24
|
+
GROUP BY unnested_path, type`, [filePaths, ...params.slice(filePaths.length)]);
|
|
25
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const row of result.rows) {
|
|
27
|
+
const existing = byPath.get(row.path) ?? {};
|
|
28
|
+
existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);
|
|
29
|
+
byPath.set(row.path, existing);
|
|
30
|
+
}
|
|
31
|
+
return byPath;
|
|
32
|
+
} catch {
|
|
33
|
+
return /* @__PURE__ */ new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Aggregate per-path observation type counts into cluster-level counts,
|
|
38
|
+
* then pick the dominant type.
|
|
39
|
+
*/
|
|
40
|
+
function aggregateObservationTypes(paths, byPath) {
|
|
41
|
+
const counts = {};
|
|
42
|
+
for (const path of paths) {
|
|
43
|
+
const pathCounts = byPath.get(path);
|
|
44
|
+
if (!pathCounts) continue;
|
|
45
|
+
for (const [type, n] of Object.entries(pathCounts)) counts[type] = (counts[type] ?? 0) + n;
|
|
46
|
+
}
|
|
47
|
+
let dominant = "unknown";
|
|
48
|
+
let maxCount = 0;
|
|
49
|
+
for (const [type, n] of Object.entries(counts)) if (n > maxCount) {
|
|
50
|
+
maxCount = n;
|
|
51
|
+
dominant = type;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
dominant,
|
|
55
|
+
counts
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const SKIP_PREFIXES = [
|
|
59
|
+
"Attachments/",
|
|
60
|
+
"🗓️ Daily Notes/",
|
|
61
|
+
"Copilot/copilot-conversations/",
|
|
62
|
+
"Z - Zettelkasten/Tweets/"
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Cluster vault notes by wikilink connectivity when embeddings aren't available.
|
|
66
|
+
* Uses BFS to find connected components in the link graph, then picks the
|
|
67
|
+
* largest components as clusters. Labels are derived from the most common
|
|
68
|
+
* title words in each component.
|
|
69
|
+
*/
|
|
70
|
+
async function clusterByLinks(backend, lookbackDays, minSize, maxClusters) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const from = now - lookbackDays * 864e5;
|
|
73
|
+
const recentNotes = (await backend.getRecentVaultFiles(from)).filter((f) => f.vaultPath.endsWith(".md"));
|
|
74
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const n of recentNotes) noteMap.set(n.vaultPath, {
|
|
76
|
+
title: n.title,
|
|
77
|
+
indexed_at: n.indexedAt
|
|
78
|
+
});
|
|
79
|
+
const adj = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const path of noteMap.keys()) if (!adj.has(path)) adj.set(path, /* @__PURE__ */ new Set());
|
|
81
|
+
const linkGraph = await backend.getVaultLinkGraph();
|
|
82
|
+
for (const { source_path, target_path } of linkGraph) if (noteMap.has(source_path) && noteMap.has(target_path)) {
|
|
83
|
+
adj.get(source_path).add(target_path);
|
|
84
|
+
adj.get(target_path).add(source_path);
|
|
85
|
+
}
|
|
86
|
+
const degrees = [...adj.entries()].map(([p, s]) => ({
|
|
87
|
+
path: p,
|
|
88
|
+
degree: s.size
|
|
89
|
+
}));
|
|
90
|
+
degrees.sort((a, b) => b.degree - a.degree);
|
|
91
|
+
const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * .05)]?.degree ?? 10);
|
|
92
|
+
const hubNodes = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const { path, degree } of degrees) if (degree >= hubThreshold) hubNodes.add(path);
|
|
94
|
+
else break;
|
|
95
|
+
for (const hub of hubNodes) adj.delete(hub);
|
|
96
|
+
for (const [, neighbors] of adj) for (const hub of hubNodes) neighbors.delete(hub);
|
|
97
|
+
const visited = /* @__PURE__ */ new Set();
|
|
98
|
+
const components = [];
|
|
99
|
+
for (const path of noteMap.keys()) {
|
|
100
|
+
if (visited.has(path) || hubNodes.has(path)) continue;
|
|
101
|
+
if (SKIP_PREFIXES.some((p) => path.startsWith(p))) {
|
|
102
|
+
visited.add(path);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const component = [];
|
|
106
|
+
const queue = [path];
|
|
107
|
+
visited.add(path);
|
|
108
|
+
while (queue.length > 0) {
|
|
109
|
+
const current = queue.shift();
|
|
110
|
+
component.push(current);
|
|
111
|
+
const neighbors = adj.get(current);
|
|
112
|
+
if (!neighbors) continue;
|
|
113
|
+
for (const neighbor of neighbors) if (!visited.has(neighbor) && !SKIP_PREFIXES.some((p) => neighbor.startsWith(p))) {
|
|
114
|
+
visited.add(neighbor);
|
|
115
|
+
queue.push(neighbor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (component.length >= minSize) components.push(component);
|
|
119
|
+
}
|
|
120
|
+
components.sort((a, b) => b.length - a.length);
|
|
121
|
+
const topComponents = components.slice(0, maxClusters);
|
|
122
|
+
function generateLinkLabel(paths) {
|
|
123
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const p of paths) {
|
|
125
|
+
const title = noteMap.get(p)?.title;
|
|
126
|
+
if (!title) continue;
|
|
127
|
+
const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
128
|
+
for (const word of words) wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w).join(" / ") || "Linked Notes";
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
themes: topComponents.map((component, idx) => {
|
|
134
|
+
const notes = component.map((p) => ({
|
|
135
|
+
path: p,
|
|
136
|
+
title: noteMap.get(p)?.title ?? null
|
|
137
|
+
}));
|
|
138
|
+
const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;
|
|
139
|
+
const uniqueFolders = new Set(component.map((p) => p.split("/")[0]));
|
|
140
|
+
return {
|
|
141
|
+
id: idx,
|
|
142
|
+
label: generateLinkLabel(component),
|
|
143
|
+
notes,
|
|
144
|
+
size: component.length,
|
|
145
|
+
folderDiversity: uniqueFolders.size / component.length,
|
|
146
|
+
avgRecency,
|
|
147
|
+
linkedRatio: 1,
|
|
148
|
+
suggestIndexNote: component.length >= 10
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
totalNotesAnalyzed: recentNotes.length,
|
|
152
|
+
timeWindow: {
|
|
153
|
+
from,
|
|
154
|
+
to: now
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function handleGraphClusters(pool, backend, params) {
|
|
159
|
+
const minSize = params.min_size ?? 3;
|
|
160
|
+
const maxClusters = params.max_clusters ?? 20;
|
|
161
|
+
const lookbackDays = params.lookback_days ?? 90;
|
|
162
|
+
if (!(params.project_id ?? 0)) throw new Error("graph_clusters: project_id is required (pass the vault project's numeric ID)");
|
|
163
|
+
const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);
|
|
164
|
+
const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));
|
|
165
|
+
const observationsByPath = pool !== null ? await fetchObservationTypes(pool, allPaths, params.project_id) : /* @__PURE__ */ new Map();
|
|
166
|
+
const fileRows = await backend.getVaultFilesByPaths(allPaths);
|
|
167
|
+
const indexedAtMap = new Map(fileRows.map((f) => [f.vaultPath, f.indexedAt]));
|
|
168
|
+
const clusters = themeResult.themes.map((theme) => {
|
|
169
|
+
const notePaths = theme.notes.map((n) => n.path);
|
|
170
|
+
const notesWithTimestamps = theme.notes.map((n) => ({
|
|
171
|
+
vault_path: n.path,
|
|
172
|
+
title: n.title ?? n.path.split("/").pop() ?? n.path,
|
|
173
|
+
indexed_at: indexedAtMap.get(n.path) ?? 0
|
|
174
|
+
}));
|
|
175
|
+
const avgRecency = theme.avgRecency;
|
|
176
|
+
const { dominant, counts } = aggregateObservationTypes(notePaths, observationsByPath);
|
|
177
|
+
return {
|
|
178
|
+
id: theme.id,
|
|
179
|
+
label: theme.label,
|
|
180
|
+
size: theme.size,
|
|
181
|
+
folder_diversity: theme.folderDiversity,
|
|
182
|
+
avg_recency: avgRecency,
|
|
183
|
+
linked_ratio: theme.linkedRatio,
|
|
184
|
+
dominant_observation_type: dominant,
|
|
185
|
+
observation_type_counts: counts,
|
|
186
|
+
suggest_index_note: theme.suggestIndexNote,
|
|
187
|
+
has_idea_note: false,
|
|
188
|
+
notes: notesWithTimestamps
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
clusters.sort((a, b) => b.size - a.size);
|
|
192
|
+
return {
|
|
193
|
+
clusters: clusters.slice(0, maxClusters),
|
|
194
|
+
total_notes_analyzed: themeResult.totalNotesAnalyzed,
|
|
195
|
+
time_window: themeResult.timeWindow
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { handleGraphClusters };
|
|
201
|
+
//# sourceMappingURL=clusters-BdvGIoD-.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clusters-BdvGIoD-.mjs","names":[],"sources":["../src/graph/clusters.ts"],"sourcesContent":["/**\n * clusters.ts — graph_clusters endpoint handler\n *\n * Reuses the zettelThemes() agglomerative clustering algorithm and enriches\n * each cluster with observation-type statistics, avg_recency from member\n * timestamps, and helper flags for the Obsidian knowledge plugin.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphClustersParams {\n project_id?: number;\n min_size?: number;\n max_clusters?: number;\n lookback_days?: number;\n similarity_threshold?: number;\n}\n\nexport interface ClusterNode {\n id: number;\n label: string;\n size: number;\n folder_diversity: number;\n avg_recency: number;\n linked_ratio: number;\n dominant_observation_type: string;\n observation_type_counts: Record<string, number>;\n suggest_index_note: boolean;\n has_idea_note: boolean;\n notes: Array<{ vault_path: string; title: string; indexed_at: number }>;\n}\n\nexport interface GraphClustersResult {\n clusters: ClusterNode[];\n total_notes_analyzed: number;\n time_window: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment\n// ---------------------------------------------------------------------------\n\n/**\n * Query pai_observations (Postgres) for observation types associated with\n * the given file paths. Returns a map from vault_path → type counts.\n *\n * Falls back to an empty map when the pool is not available or the query fails.\n */\nasync function fetchObservationTypes(\n pool: Pool,\n filePaths: string[],\n projectId?: number\n): Promise<Map<string, Record<string, number>>> {\n if (filePaths.length === 0) return new Map();\n\n try {\n const params: (string | number)[] = [...filePaths];\n let projectFilter = \"\";\n if (projectId !== undefined) {\n params.push(projectId);\n projectFilter = `AND project_id = $${params.length}`;\n }\n\n const result = await pool.query<{ path: string; type: string; cnt: string }>(\n `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n FROM pai_observations,\n LATERAL unnest(files_modified || files_read) AS unnested_path\n WHERE unnested_path = ANY($1::text[])\n ${projectFilter}\n GROUP BY unnested_path, type`,\n [filePaths, ...params.slice(filePaths.length)]\n );\n\n const byPath = new Map<string, Record<string, number>>();\n for (const row of result.rows) {\n const existing = byPath.get(row.path) ?? {};\n existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n byPath.set(row.path, existing);\n }\n return byPath;\n } catch {\n return new Map();\n }\n}\n\n/**\n * Aggregate per-path observation type counts into cluster-level counts,\n * then pick the dominant type.\n */\nfunction aggregateObservationTypes(\n paths: string[],\n byPath: Map<string, Record<string, number>>\n): { dominant: string; counts: Record<string, number> } {\n const counts: Record<string, number> = {};\n for (const path of paths) {\n const pathCounts = byPath.get(path);\n if (!pathCounts) continue;\n for (const [type, n] of Object.entries(pathCounts)) {\n counts[type] = (counts[type] ?? 0) + n;\n }\n }\n\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n\n return { dominant, counts };\n}\n\n// ---------------------------------------------------------------------------\n// Link-based fallback clustering (wikilink connected components)\n// ---------------------------------------------------------------------------\n\nconst SKIP_PREFIXES = [\n \"Attachments/\", \"🗓️ Daily Notes/\", \"Copilot/copilot-conversations/\",\n \"Z - Zettelkasten/Tweets/\",\n];\n\n/**\n * Cluster vault notes by wikilink connectivity when embeddings aren't available.\n * Uses BFS to find connected components in the link graph, then picks the\n * largest components as clusters. Labels are derived from the most common\n * title words in each component.\n */\nasync function clusterByLinks(\n backend: StorageBackend,\n lookbackDays: number,\n minSize: number,\n maxClusters: number,\n): Promise<{ themes: Array<{ id: number; label: string; notes: Array<{ path: string; title: string | null }>; size: number; folderDiversity: number; avgRecency: number; linkedRatio: number; suggestIndexNote: boolean }>; totalNotesAnalyzed: number; timeWindow: { from: number; to: number } }> {\n const now = Date.now();\n const from = now - lookbackDays * 86400000;\n\n // Get recent notes\n const recentFiles = await backend.getRecentVaultFiles(from);\n const recentNotes = recentFiles.filter(f => f.vaultPath.endsWith(\".md\"));\n\n const noteMap = new Map<string, { title: string | null; indexed_at: number }>();\n for (const n of recentNotes) {\n noteMap.set(n.vaultPath, { title: n.title, indexed_at: n.indexedAt });\n }\n\n // Build adjacency list from vault_links (only for recent notes)\n const adj = new Map<string, Set<string>>();\n for (const path of noteMap.keys()) {\n if (!adj.has(path)) adj.set(path, new Set());\n }\n\n const linkGraph = await backend.getVaultLinkGraph();\n\n for (const { source_path, target_path } of linkGraph) {\n if (noteMap.has(source_path) && noteMap.has(target_path)) {\n adj.get(source_path)!.add(target_path);\n adj.get(target_path)!.add(source_path);\n }\n }\n\n // Remove hub nodes before BFS\n const degrees = [...adj.entries()].map(([p, s]) => ({ path: p, degree: s.size }));\n degrees.sort((a, b) => b.degree - a.degree);\n const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * 0.05)]?.degree ?? 10);\n const hubNodes = new Set<string>();\n for (const { path, degree } of degrees) {\n if (degree >= hubThreshold) hubNodes.add(path);\n else break;\n }\n\n for (const hub of hubNodes) {\n adj.delete(hub);\n }\n for (const [, neighbors] of adj) {\n for (const hub of hubNodes) {\n neighbors.delete(hub);\n }\n }\n\n // BFS connected components\n const visited = new Set<string>();\n const components: string[][] = [];\n\n for (const path of noteMap.keys()) {\n if (visited.has(path) || hubNodes.has(path)) continue;\n if (SKIP_PREFIXES.some(p => path.startsWith(p))) { visited.add(path); continue; }\n const component: string[] = [];\n const queue = [path];\n visited.add(path);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n component.push(current);\n const neighbors = adj.get(current);\n if (!neighbors) continue;\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor) && !SKIP_PREFIXES.some(p => neighbor.startsWith(p))) {\n visited.add(neighbor);\n queue.push(neighbor);\n }\n }\n }\n\n if (component.length >= minSize) {\n components.push(component);\n }\n }\n\n components.sort((a, b) => b.length - a.length);\n const topComponents = components.slice(0, maxClusters);\n\n // STOP_WORDS imported from utils/stop-words.ts (module-level import)\n\n function generateLinkLabel(paths: string[]): string {\n const wordCounts = new Map<string, number>();\n for (const p of paths) {\n const title = noteMap.get(p)?.title;\n if (!title) continue;\n const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\\s]/g, \" \").split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted.slice(0, 3).map(([w]) => w).join(\" / \") || \"Linked Notes\";\n }\n\n const themes = topComponents.map((component, idx) => {\n const notes = component.map(p => ({\n path: p,\n title: noteMap.get(p)?.title ?? null,\n }));\n const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;\n const uniqueFolders = new Set(component.map(p => p.split(\"/\")[0]));\n\n return {\n id: idx,\n label: generateLinkLabel(component),\n notes,\n size: component.length,\n folderDiversity: uniqueFolders.size / component.length,\n avgRecency,\n linkedRatio: 1.0,\n suggestIndexNote: component.length >= 10,\n };\n });\n\n return {\n themes,\n totalNotesAnalyzed: recentNotes.length,\n timeWindow: { from, to: now },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphClusters(\n pool: Pool | null,\n backend: StorageBackend,\n params: GraphClustersParams\n): Promise<GraphClustersResult> {\n const minSize = params.min_size ?? 3;\n const maxClusters = params.max_clusters ?? 20;\n const lookbackDays = params.lookback_days ?? 90;\n\n const vaultProjectId = params.project_id ?? 0;\n\n if (!vaultProjectId) {\n throw new Error(\n \"graph_clusters: project_id is required (pass the vault project's numeric ID)\"\n );\n }\n\n const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);\n\n const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));\n\n const observationsByPath =\n pool !== null\n ? await fetchObservationTypes(pool, allPaths, params.project_id)\n : new Map<string, Record<string, number>>();\n\n // Fetch indexed_at timestamps for all notes in bulk\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const indexedAtMap = new Map<string, number>(fileRows.map(f => [f.vaultPath, f.indexedAt]));\n\n const clusters: ClusterNode[] = themeResult.themes.map((theme) => {\n const notePaths = theme.notes.map((n) => n.path);\n\n const notesWithTimestamps = theme.notes.map((n) => ({\n vault_path: n.path,\n title: n.title ?? n.path.split(\"/\").pop() ?? n.path,\n indexed_at: indexedAtMap.get(n.path) ?? 0,\n }));\n\n const avgRecency = theme.avgRecency;\n\n const { dominant, counts } = aggregateObservationTypes(\n notePaths,\n observationsByPath\n );\n\n return {\n id: theme.id,\n label: theme.label,\n size: theme.size,\n folder_diversity: theme.folderDiversity,\n avg_recency: avgRecency,\n linked_ratio: theme.linkedRatio,\n dominant_observation_type: dominant,\n observation_type_counts: counts,\n suggest_index_note: theme.suggestIndexNote,\n has_idea_note: false,\n notes: notesWithTimestamps,\n };\n });\n\n clusters.sort((a, b) => b.size - a.size);\n\n return {\n clusters: clusters.slice(0, maxClusters),\n total_notes_analyzed: themeResult.totalNotesAnalyzed,\n time_window: themeResult.timeWindow,\n };\n}\n"],"mappings":";;;;;;;;;AAsDA,eAAe,sBACb,MACA,WACA,WAC8C;AAC9C,KAAI,UAAU,WAAW,EAAG,wBAAO,IAAI,KAAK;AAE5C,KAAI;EACF,MAAM,SAA8B,CAAC,GAAG,UAAU;EAClD,IAAI,gBAAgB;AACpB,MAAI,cAAc,QAAW;AAC3B,UAAO,KAAK,UAAU;AACtB,mBAAgB,qBAAqB,OAAO;;EAG9C,MAAM,SAAS,MAAM,KAAK,MACxB;;;;WAIK,cAAc;sCAEnB,CAAC,WAAW,GAAG,OAAO,MAAM,UAAU,OAAO,CAAC,CAC/C;EAED,MAAM,yBAAS,IAAI,KAAqC;AACxD,OAAK,MAAM,OAAO,OAAO,MAAM;GAC7B,MAAM,WAAW,OAAO,IAAI,IAAI,KAAK,IAAI,EAAE;AAC3C,YAAS,IAAI,SAAS,SAAS,IAAI,SAAS,KAAK,SAAS,IAAI,KAAK,GAAG;AACtE,UAAO,IAAI,IAAI,MAAM,SAAS;;AAEhC,SAAO;SACD;AACN,yBAAO,IAAI,KAAK;;;;;;;AAQpB,SAAS,0BACP,OACA,QACsD;CACtD,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,OAAO,IAAI,KAAK;AACnC,MAAI,CAAC,WAAY;AACjB,OAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,WAAW,CAChD,QAAO,SAAS,OAAO,SAAS,KAAK;;CAIzC,IAAI,WAAW;CACf,IAAI,WAAW;AACf,MAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,OAAO,CAC5C,KAAI,IAAI,UAAU;AAChB,aAAW;AACX,aAAW;;AAIf,QAAO;EAAE;EAAU;EAAQ;;AAO7B,MAAM,gBAAgB;CACpB;CAAgB;CAAoB;CACpC;CACD;;;;;;;AAQD,eAAe,eACb,SACA,cACA,SACA,aACkS;CAClS,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,OAAO,MAAM,eAAe;CAIlC,MAAM,eADc,MAAM,QAAQ,oBAAoB,KAAK,EAC3B,QAAO,MAAK,EAAE,UAAU,SAAS,MAAM,CAAC;CAExE,MAAM,0BAAU,IAAI,KAA2D;AAC/E,MAAK,MAAM,KAAK,YACd,SAAQ,IAAI,EAAE,WAAW;EAAE,OAAO,EAAE;EAAO,YAAY,EAAE;EAAW,CAAC;CAIvE,MAAM,sBAAM,IAAI,KAA0B;AAC1C,MAAK,MAAM,QAAQ,QAAQ,MAAM,CAC/B,KAAI,CAAC,IAAI,IAAI,KAAK,CAAE,KAAI,IAAI,sBAAM,IAAI,KAAK,CAAC;CAG9C,MAAM,YAAY,MAAM,QAAQ,mBAAmB;AAEnD,MAAK,MAAM,EAAE,aAAa,iBAAiB,UACzC,KAAI,QAAQ,IAAI,YAAY,IAAI,QAAQ,IAAI,YAAY,EAAE;AACxD,MAAI,IAAI,YAAY,CAAE,IAAI,YAAY;AACtC,MAAI,IAAI,YAAY,CAAE,IAAI,YAAY;;CAK1C,MAAM,UAAU,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,QAAQ;EAAE,MAAM;EAAG,QAAQ,EAAE;EAAM,EAAE;AACjF,SAAQ,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;CAC3C,MAAM,eAAe,KAAK,IAAI,IAAI,QAAQ,KAAK,MAAM,QAAQ,SAAS,IAAK,GAAG,UAAU,GAAG;CAC3F,MAAM,2BAAW,IAAI,KAAa;AAClC,MAAK,MAAM,EAAE,MAAM,YAAY,QAC7B,KAAI,UAAU,aAAc,UAAS,IAAI,KAAK;KACzC;AAGP,MAAK,MAAM,OAAO,SAChB,KAAI,OAAO,IAAI;AAEjB,MAAK,MAAM,GAAG,cAAc,IAC1B,MAAK,MAAM,OAAO,SAChB,WAAU,OAAO,IAAI;CAKzB,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,aAAyB,EAAE;AAEjC,MAAK,MAAM,QAAQ,QAAQ,MAAM,EAAE;AACjC,MAAI,QAAQ,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,CAAE;AAC7C,MAAI,cAAc,MAAK,MAAK,KAAK,WAAW,EAAE,CAAC,EAAE;AAAE,WAAQ,IAAI,KAAK;AAAE;;EACtE,MAAM,YAAsB,EAAE;EAC9B,MAAM,QAAQ,CAAC,KAAK;AACpB,UAAQ,IAAI,KAAK;AAEjB,SAAO,MAAM,SAAS,GAAG;GACvB,MAAM,UAAU,MAAM,OAAO;AAC7B,aAAU,KAAK,QAAQ;GACvB,MAAM,YAAY,IAAI,IAAI,QAAQ;AAClC,OAAI,CAAC,UAAW;AAChB,QAAK,MAAM,YAAY,UACrB,KAAI,CAAC,QAAQ,IAAI,SAAS,IAAI,CAAC,cAAc,MAAK,MAAK,SAAS,WAAW,EAAE,CAAC,EAAE;AAC9E,YAAQ,IAAI,SAAS;AACrB,UAAM,KAAK,SAAS;;;AAK1B,MAAI,UAAU,UAAU,QACtB,YAAW,KAAK,UAAU;;AAI9B,YAAW,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;CAC9C,MAAM,gBAAgB,WAAW,MAAM,GAAG,YAAY;CAItD,SAAS,kBAAkB,OAAyB;EAClD,MAAM,6BAAa,IAAI,KAAqB;AAC5C,OAAK,MAAM,KAAK,OAAO;GACrB,MAAM,QAAQ,QAAQ,IAAI,EAAE,EAAE;AAC9B,OAAI,CAAC,MAAO;GACZ,MAAM,QAAQ,MAAM,aAAa,CAAC,QAAQ,2BAA2B,IAAI,CAAC,MAAM,MAAM,CACnF,QAAO,MAAK,EAAE,SAAS,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;AAClD,QAAK,MAAM,QAAQ,MACjB,YAAW,IAAI,OAAO,WAAW,IAAI,KAAK,IAAI,KAAK,EAAE;;AAIzD,SADe,CAAC,GAAG,WAAW,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,CACtD,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,IAAI;;AAuB3D,QAAO;EACL,QArBa,cAAc,KAAK,WAAW,QAAQ;GACnD,MAAM,QAAQ,UAAU,KAAI,OAAM;IAChC,MAAM;IACN,OAAO,QAAQ,IAAI,EAAE,EAAE,SAAS;IACjC,EAAE;GACH,MAAM,aAAa,UAAU,QAAQ,KAAK,MAAM,OAAO,QAAQ,IAAI,EAAE,EAAE,cAAc,IAAI,EAAE,GAAG,UAAU;GACxG,MAAM,gBAAgB,IAAI,IAAI,UAAU,KAAI,MAAK,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC;AAElE,UAAO;IACL,IAAI;IACJ,OAAO,kBAAkB,UAAU;IACnC;IACA,MAAM,UAAU;IAChB,iBAAiB,cAAc,OAAO,UAAU;IAChD;IACA,aAAa;IACb,kBAAkB,UAAU,UAAU;IACvC;IACD;EAIA,oBAAoB,YAAY;EAChC,YAAY;GAAE;GAAM,IAAI;GAAK;EAC9B;;AAOH,eAAsB,oBACpB,MACA,SACA,QAC8B;CAC9B,MAAM,UAAU,OAAO,YAAY;CACnC,MAAM,cAAc,OAAO,gBAAgB;CAC3C,MAAM,eAAe,OAAO,iBAAiB;AAI7C,KAAI,EAFmB,OAAO,cAAc,GAG1C,OAAM,IAAI,MACR,+EACD;CAGH,MAAM,cAAc,MAAM,eAAe,SAAS,cAAc,SAAS,YAAY;CAErF,MAAM,WAAW,YAAY,OAAO,SAAS,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC;CAE9E,MAAM,qBACJ,SAAS,OACL,MAAM,sBAAsB,MAAM,UAAU,OAAO,WAAW,mBAC9D,IAAI,KAAqC;CAG/C,MAAM,WAAW,MAAM,QAAQ,qBAAqB,SAAS;CAC7D,MAAM,eAAe,IAAI,IAAoB,SAAS,KAAI,MAAK,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;CAE3F,MAAM,WAA0B,YAAY,OAAO,KAAK,UAAU;EAChE,MAAM,YAAY,MAAM,MAAM,KAAK,MAAM,EAAE,KAAK;EAEhD,MAAM,sBAAsB,MAAM,MAAM,KAAK,OAAO;GAClD,YAAY,EAAE;GACd,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,IAAI,CAAC,KAAK,IAAI,EAAE;GAC/C,YAAY,aAAa,IAAI,EAAE,KAAK,IAAI;GACzC,EAAE;EAEH,MAAM,aAAa,MAAM;EAEzB,MAAM,EAAE,UAAU,WAAW,0BAC3B,WACA,mBACD;AAED,SAAO;GACL,IAAI,MAAM;GACV,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,kBAAkB,MAAM;GACxB,aAAa;GACb,cAAc,MAAM;GACpB,2BAA2B;GAC3B,yBAAyB;GACzB,oBAAoB,MAAM;GAC1B,eAAe;GACf,OAAO;GACR;GACD;AAEF,UAAS,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK;AAExC,QAAO;EACL,UAAU,SAAS,MAAM,GAAG,YAAY;EACxC,sBAAsB,YAAY;EAClC,aAAa,YAAY;EAC1B"}
|
package/dist/daemon/index.mjs
CHANGED
|
@@ -11,12 +11,13 @@ import "../checkpoint-block-D75rhsPY.mjs";
|
|
|
11
11
|
import "../indexer-AEcT8wHf.mjs";
|
|
12
12
|
import { t as PaiClient } from "../ipc-client-aVKVERjJ.mjs";
|
|
13
13
|
import { i as ensureConfigDir, o as loadConfig } from "../config-CcdkNSWa.mjs";
|
|
14
|
-
import "../factory-
|
|
15
|
-
import { n as serve } from "../daemon-
|
|
14
|
+
import "../factory-CAy0N1xR.mjs";
|
|
15
|
+
import { n as serve } from "../daemon-BaBTOB1P.mjs";
|
|
16
16
|
import "../state-DTvy-jRB.mjs";
|
|
17
|
-
import "../
|
|
18
|
-
import "../
|
|
19
|
-
import "../
|
|
17
|
+
import "../router-B2xR3gVP.mjs";
|
|
18
|
+
import "../tools-DMAQxlOk.mjs";
|
|
19
|
+
import "../detector-DExEQ5cW.mjs";
|
|
20
|
+
import "../work-queue-worker-Bt_UcbMy.mjs";
|
|
20
21
|
import { Command } from "commander";
|
|
21
22
|
|
|
22
23
|
//#region src/daemon/index.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/daemon/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon — Entry point\n *\n * Commands:\n * serve — Start the PAI daemon (foreground, managed by launchd in production)\n * status — Query daemon status via IPC\n * index — Trigger an immediate index run via IPC\n */\n\nimport { Command } from \"commander\";\nimport { loadConfig, ensureConfigDir } from \"./config.js\";\nimport { serve } from \"./daemon.js\";\nimport { PaiClient } from \"./ipc-client.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"pai-daemon\")\n .description(\"PAI Daemon — background service for PAI Knowledge OS\")\n .version(\"0.1.0\");\n\n// ---------------------------------------------------------------------------\n// serve\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"serve\")\n .description(\"Start the PAI daemon in the foreground\")\n .action(async () => {\n ensureConfigDir();\n const config = loadConfig();\n await serve(config);\n });\n\n// ---------------------------------------------------------------------------\n// status\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"status\")\n .description(\"Query the running daemon status\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n const status = await client.status();\n console.log(JSON.stringify(status, null, 2));\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// index\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"index\")\n .description(\"Trigger an immediate index run in the running daemon\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n await client.triggerIndex();\n console.log(\"Index triggered. Check daemon logs for progress.\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// Parse\n// ---------------------------------------------------------------------------\n\nprogram.parse(process.argv);\n\nif (process.argv.length <= 2) {\n program.help();\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/daemon/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon — Entry point\n *\n * Commands:\n * serve — Start the PAI daemon (foreground, managed by launchd in production)\n * status — Query daemon status via IPC\n * index — Trigger an immediate index run via IPC\n */\n\nimport { Command } from \"commander\";\nimport { loadConfig, ensureConfigDir } from \"./config.js\";\nimport { serve } from \"./daemon.js\";\nimport { PaiClient } from \"./ipc-client.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"pai-daemon\")\n .description(\"PAI Daemon — background service for PAI Knowledge OS\")\n .version(\"0.1.0\");\n\n// ---------------------------------------------------------------------------\n// serve\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"serve\")\n .description(\"Start the PAI daemon in the foreground\")\n .action(async () => {\n ensureConfigDir();\n const config = loadConfig();\n await serve(config);\n });\n\n// ---------------------------------------------------------------------------\n// status\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"status\")\n .description(\"Query the running daemon status\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n const status = await client.status();\n console.log(JSON.stringify(status, null, 2));\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// index\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"index\")\n .description(\"Trigger an immediate index run in the running daemon\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n await client.triggerIndex();\n console.log(\"Index triggered. Check daemon logs for progress.\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// Parse\n// ---------------------------------------------------------------------------\n\nprogram.parse(process.argv);\n\nif (process.argv.length <= 2) {\n program.help();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,MAAM,UAAU,IAAI,SAAS;AAE7B,QACG,KAAK,aAAa,CAClB,YAAY,uDAAuD,CACnE,QAAQ,QAAQ;AAMnB,QACG,QAAQ,QAAQ,CAChB,YAAY,yCAAyC,CACrD,OAAO,YAAY;AAClB,kBAAiB;AAEjB,OAAM,MADS,YAAY,CACR;EACnB;AAMJ,QACG,QAAQ,SAAS,CACjB,YAAY,kCAAkC,CAC9C,OAAO,YAAY;CAElB,MAAM,SAAS,IAAI,UADJ,YAAY,CACS,WAAW;AAE/C,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,QAAQ;AACpC,UAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;UACrC,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,UAAU,MAAM;AAC9B,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QACG,QAAQ,QAAQ,CAChB,YAAY,uDAAuD,CACnE,OAAO,YAAY;CAElB,MAAM,SAAS,IAAI,UADJ,YAAY,CACS,WAAW;AAE/C,KAAI;AACF,QAAM,OAAO,cAAc;AAC3B,UAAQ,IAAI,mDAAmD;UACxD,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,UAAU,MAAM;AAC9B,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QAAQ,MAAM,QAAQ,KAAK;AAE3B,IAAI,QAAQ,KAAK,UAAU,EACzB,SAAQ,MAAM"}
|