@tekmidian/pai 0.8.5 → 0.9.1
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 +121 -0
- package/FEATURE.md +5 -0
- package/README.md +54 -0
- package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
- package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
- package/dist/cli/index.mjs +345 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
- package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
- package/dist/daemon/index.mjs +6 -6
- package/dist/{daemon-BaYX-w_d.mjs → daemon-kp49BE7u.mjs} +93 -19
- package/dist/daemon-kp49BE7u.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +51 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
- package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
- package/dist/{factory-BzWfxsvK.mjs → factory-DKDPRhAN.mjs} +3 -3
- package/dist/{factory-BzWfxsvK.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs +276 -89
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs +152 -2
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
- package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
- package/dist/kg-B5ysyRLC.mjs +94 -0
- package/dist/kg-B5ysyRLC.mjs.map +1 -0
- package/dist/kg-extraction-BlGM40q7.mjs +211 -0
- package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
- package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
- package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
- package/dist/{postgres-DbUXNuy_.mjs → postgres-BGERehmX.mjs} +22 -1
- package/dist/{postgres-DbUXNuy_.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
- package/dist/skills/Reconstruct/SKILL.md +36 -0
- package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
- package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
- package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
- package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
- package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
- package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
- package/dist/{tools-BXSwlzeH.mjs → tools-8t7BQrm9.mjs} +717 -15
- package/dist/tools-8t7BQrm9.mjs.map +1 -0
- package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
- package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
- package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
- package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/session-start/load-project-context.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +203 -1
- package/dist/daemon-BaYX-w_d.mjs.map +0 -1
- package/dist/indexer-D53l5d1U.mjs +0 -1
- package/dist/tools-BXSwlzeH.mjs.map +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
import { t as STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
|
|
2
3
|
import { i as searchMemoryHybrid, n as populateSlugs } from "./search-DC1qhkKn.mjs";
|
|
3
4
|
import { r as formatDetectionJson, t as detectProject } from "./detect-CdaA48EI.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { i as kgQuery, n as kgContradictions, r as kgInvalidate, t as kgAdd } from "./kg-B5ysyRLC.mjs";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
6
9
|
|
|
7
10
|
//#region src/mcp/tools/types.ts
|
|
8
11
|
/**
|
|
@@ -122,7 +125,7 @@ async function toolMemorySearch(registryDb, federation, params, searchDefaults)
|
|
|
122
125
|
return `${header}\n${raw.length > snippetLength ? raw.slice(0, snippetLength) + "..." : raw}`;
|
|
123
126
|
}).join("\n\n---\n\n");
|
|
124
127
|
try {
|
|
125
|
-
const { saveQueryResult } = await import("./query-feedback-
|
|
128
|
+
const { saveQueryResult } = await import("./query-feedback-CQSumXDy.mjs").then((n) => n.t);
|
|
126
129
|
saveQueryResult({
|
|
127
130
|
query: params.query,
|
|
128
131
|
timestamp: Date.now(),
|
|
@@ -647,7 +650,7 @@ function toolSessionList(registryDb, params) {
|
|
|
647
650
|
*/
|
|
648
651
|
async function toolSessionRoute(registryDb, federation, params) {
|
|
649
652
|
try {
|
|
650
|
-
const { autoRoute, formatAutoRouteJson } = await import("./auto-route-
|
|
653
|
+
const { autoRoute, formatAutoRouteJson } = await import("./auto-route-CruBrTf-.mjs");
|
|
651
654
|
const result = await autoRoute(registryDb, federation, params.cwd, params.context);
|
|
652
655
|
if (!result) return { content: [{
|
|
653
656
|
type: "text",
|
|
@@ -711,7 +714,7 @@ function toolRegistrySearch(registryDb, params) {
|
|
|
711
714
|
//#region src/mcp/tools/zettel.ts
|
|
712
715
|
async function toolZettelExplore(backend, params) {
|
|
713
716
|
try {
|
|
714
|
-
const { zettelExplore } = await import("./zettelkasten-
|
|
717
|
+
const { zettelExplore } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
715
718
|
const result = await zettelExplore(backend, {
|
|
716
719
|
startNote: params.start_note,
|
|
717
720
|
depth: params.depth,
|
|
@@ -734,7 +737,7 @@ async function toolZettelExplore(backend, params) {
|
|
|
734
737
|
}
|
|
735
738
|
async function toolZettelHealth(backend, params) {
|
|
736
739
|
try {
|
|
737
|
-
const { zettelHealth } = await import("./zettelkasten-
|
|
740
|
+
const { zettelHealth } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
738
741
|
const result = await zettelHealth(backend, {
|
|
739
742
|
scope: params.scope,
|
|
740
743
|
projectPath: params.project_path,
|
|
@@ -757,7 +760,7 @@ async function toolZettelHealth(backend, params) {
|
|
|
757
760
|
}
|
|
758
761
|
async function toolZettelSurprise(backend, params) {
|
|
759
762
|
try {
|
|
760
|
-
const { zettelSurprise } = await import("./zettelkasten-
|
|
763
|
+
const { zettelSurprise } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
761
764
|
const results = await zettelSurprise(backend, {
|
|
762
765
|
referencePath: params.reference_path,
|
|
763
766
|
vaultProjectId: params.vault_project_id,
|
|
@@ -781,7 +784,7 @@ async function toolZettelSurprise(backend, params) {
|
|
|
781
784
|
}
|
|
782
785
|
async function toolZettelSuggest(backend, params) {
|
|
783
786
|
try {
|
|
784
|
-
const { zettelSuggest } = await import("./zettelkasten-
|
|
787
|
+
const { zettelSuggest } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
785
788
|
const results = await zettelSuggest(backend, {
|
|
786
789
|
notePath: params.note_path,
|
|
787
790
|
vaultProjectId: params.vault_project_id,
|
|
@@ -804,7 +807,7 @@ async function toolZettelSuggest(backend, params) {
|
|
|
804
807
|
}
|
|
805
808
|
async function toolZettelConverse(backend, params) {
|
|
806
809
|
try {
|
|
807
|
-
const { zettelConverse } = await import("./zettelkasten-
|
|
810
|
+
const { zettelConverse } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
808
811
|
const result = await zettelConverse(backend, {
|
|
809
812
|
question: params.question,
|
|
810
813
|
vaultProjectId: params.vault_project_id,
|
|
@@ -812,7 +815,7 @@ async function toolZettelConverse(backend, params) {
|
|
|
812
815
|
limit: params.limit
|
|
813
816
|
});
|
|
814
817
|
try {
|
|
815
|
-
const { saveQueryResult } = await import("./query-feedback-
|
|
818
|
+
const { saveQueryResult } = await import("./query-feedback-CQSumXDy.mjs").then((n) => n.t);
|
|
816
819
|
saveQueryResult({
|
|
817
820
|
query: params.question,
|
|
818
821
|
timestamp: Date.now(),
|
|
@@ -840,7 +843,7 @@ async function toolZettelConverse(backend, params) {
|
|
|
840
843
|
}
|
|
841
844
|
async function toolZettelThemes(backend, params) {
|
|
842
845
|
try {
|
|
843
|
-
const { zettelThemes } = await import("./zettelkasten-
|
|
846
|
+
const { zettelThemes } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
844
847
|
const result = await zettelThemes(backend, {
|
|
845
848
|
vaultProjectId: params.vault_project_id,
|
|
846
849
|
lookbackDays: params.lookback_days,
|
|
@@ -864,7 +867,7 @@ async function toolZettelThemes(backend, params) {
|
|
|
864
867
|
}
|
|
865
868
|
async function toolZettelGodNotes(backend, params) {
|
|
866
869
|
try {
|
|
867
|
-
const { zettelGodNotes } = await import("./zettelkasten-
|
|
870
|
+
const { zettelGodNotes } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
868
871
|
const result = await zettelGodNotes(backend, {
|
|
869
872
|
limit: params.limit,
|
|
870
873
|
minInbound: params.min_inbound
|
|
@@ -885,7 +888,7 @@ async function toolZettelGodNotes(backend, params) {
|
|
|
885
888
|
}
|
|
886
889
|
async function toolZettelCommunities(backend, params) {
|
|
887
890
|
try {
|
|
888
|
-
const { zettelCommunities } = await import("./zettelkasten-
|
|
891
|
+
const { zettelCommunities } = await import("./zettelkasten-BdaMzTGQ.mjs");
|
|
889
892
|
const result = await zettelCommunities(backend, {
|
|
890
893
|
minSize: params.min_size,
|
|
891
894
|
maxCommunities: params.max_communities,
|
|
@@ -906,6 +909,698 @@ async function toolZettelCommunities(backend, params) {
|
|
|
906
909
|
}
|
|
907
910
|
}
|
|
908
911
|
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/mcp/tools/kg.ts
|
|
914
|
+
async function toolKgAdd(pool, params) {
|
|
915
|
+
try {
|
|
916
|
+
if (!params.subject || !params.predicate || !params.object) return {
|
|
917
|
+
content: [{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: "kg_add error: subject, predicate, and object are required"
|
|
920
|
+
}],
|
|
921
|
+
isError: true
|
|
922
|
+
};
|
|
923
|
+
const triple = await kgAdd(pool, params);
|
|
924
|
+
return { content: [{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: JSON.stringify(triple, null, 2)
|
|
927
|
+
}] };
|
|
928
|
+
} catch (e) {
|
|
929
|
+
return {
|
|
930
|
+
content: [{
|
|
931
|
+
type: "text",
|
|
932
|
+
text: `kg_add error: ${String(e)}`
|
|
933
|
+
}],
|
|
934
|
+
isError: true
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async function toolKgQuery(pool, params) {
|
|
939
|
+
try {
|
|
940
|
+
const asOf = params.as_of ? new Date(params.as_of) : void 0;
|
|
941
|
+
if (asOf && isNaN(asOf.getTime())) return {
|
|
942
|
+
content: [{
|
|
943
|
+
type: "text",
|
|
944
|
+
text: `kg_query error: invalid as_of date: ${params.as_of}`
|
|
945
|
+
}],
|
|
946
|
+
isError: true
|
|
947
|
+
};
|
|
948
|
+
const triples = await kgQuery(pool, {
|
|
949
|
+
subject: params.subject,
|
|
950
|
+
predicate: params.predicate,
|
|
951
|
+
object: params.object,
|
|
952
|
+
project_id: params.project_id,
|
|
953
|
+
as_of: asOf,
|
|
954
|
+
include_invalidated: params.include_invalidated
|
|
955
|
+
});
|
|
956
|
+
return { content: [{
|
|
957
|
+
type: "text",
|
|
958
|
+
text: JSON.stringify(triples, null, 2)
|
|
959
|
+
}] };
|
|
960
|
+
} catch (e) {
|
|
961
|
+
return {
|
|
962
|
+
content: [{
|
|
963
|
+
type: "text",
|
|
964
|
+
text: `kg_query error: ${String(e)}`
|
|
965
|
+
}],
|
|
966
|
+
isError: true
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function toolKgInvalidate(pool, params) {
|
|
971
|
+
try {
|
|
972
|
+
if (params.triple_id === void 0 || params.triple_id === null) return {
|
|
973
|
+
content: [{
|
|
974
|
+
type: "text",
|
|
975
|
+
text: "kg_invalidate error: triple_id is required"
|
|
976
|
+
}],
|
|
977
|
+
isError: true
|
|
978
|
+
};
|
|
979
|
+
await kgInvalidate(pool, params.triple_id);
|
|
980
|
+
return { content: [{
|
|
981
|
+
type: "text",
|
|
982
|
+
text: JSON.stringify({
|
|
983
|
+
invalidated: true,
|
|
984
|
+
triple_id: params.triple_id
|
|
985
|
+
})
|
|
986
|
+
}] };
|
|
987
|
+
} catch (e) {
|
|
988
|
+
return {
|
|
989
|
+
content: [{
|
|
990
|
+
type: "text",
|
|
991
|
+
text: `kg_invalidate error: ${String(e)}`
|
|
992
|
+
}],
|
|
993
|
+
isError: true
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function toolKgContradictions(pool, params) {
|
|
998
|
+
try {
|
|
999
|
+
if (!params.subject) return {
|
|
1000
|
+
content: [{
|
|
1001
|
+
type: "text",
|
|
1002
|
+
text: "kg_contradictions error: subject is required"
|
|
1003
|
+
}],
|
|
1004
|
+
isError: true
|
|
1005
|
+
};
|
|
1006
|
+
const contradictions = await kgContradictions(pool, params.subject);
|
|
1007
|
+
return { content: [{
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: JSON.stringify(contradictions, null, 2)
|
|
1010
|
+
}] };
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
return {
|
|
1013
|
+
content: [{
|
|
1014
|
+
type: "text",
|
|
1015
|
+
text: `kg_contradictions error: ${String(e)}`
|
|
1016
|
+
}],
|
|
1017
|
+
isError: true
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
//#endregion
|
|
1023
|
+
//#region src/memory/wakeup.ts
|
|
1024
|
+
/**
|
|
1025
|
+
* Wake-up context system — progressive context loading inspired by mempalace.
|
|
1026
|
+
*
|
|
1027
|
+
* Layers:
|
|
1028
|
+
* L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.
|
|
1029
|
+
* L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.
|
|
1030
|
+
* L2 On-Demand — triggered by topic queries (handled by memory_search).
|
|
1031
|
+
* L3 Deep Search — unlimited federated memory search (memory_search tool).
|
|
1032
|
+
*/
|
|
1033
|
+
/** Maximum tokens for the L1 essential story block. Approx 4 chars/token. */
|
|
1034
|
+
const L1_TOKEN_BUDGET = 800;
|
|
1035
|
+
L1_TOKEN_BUDGET * 4;
|
|
1036
|
+
/** Maximum session notes to scan when building L1. */
|
|
1037
|
+
const L1_MAX_NOTES = 10;
|
|
1038
|
+
/** Sections to extract from session notes (in priority order). */
|
|
1039
|
+
const EXTRACT_SECTIONS = [
|
|
1040
|
+
"Work Done",
|
|
1041
|
+
"Key Decisions",
|
|
1042
|
+
"Next Steps",
|
|
1043
|
+
"Checkpoint"
|
|
1044
|
+
];
|
|
1045
|
+
/** Identity file location. */
|
|
1046
|
+
const IDENTITY_FILE = join(homedir(), ".pai", "identity.txt");
|
|
1047
|
+
/**
|
|
1048
|
+
* Load L0 identity from ~/.pai/identity.txt.
|
|
1049
|
+
* Returns the file content, or an empty string if the file does not exist.
|
|
1050
|
+
* Never throws.
|
|
1051
|
+
*/
|
|
1052
|
+
function loadL0Identity() {
|
|
1053
|
+
if (!existsSync(IDENTITY_FILE)) return "";
|
|
1054
|
+
try {
|
|
1055
|
+
return readFileSync(IDENTITY_FILE, "utf-8").trim();
|
|
1056
|
+
} catch {
|
|
1057
|
+
return "";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Find the Notes directory for a project given its root_path from the registry.
|
|
1062
|
+
* Checks local Notes/ first, then central ~/.claude/projects/... path.
|
|
1063
|
+
*/
|
|
1064
|
+
function findNotesDirForProject(rootPath) {
|
|
1065
|
+
const localCandidates = [
|
|
1066
|
+
join(rootPath, "Notes"),
|
|
1067
|
+
join(rootPath, "notes"),
|
|
1068
|
+
join(rootPath, ".claude", "Notes")
|
|
1069
|
+
];
|
|
1070
|
+
for (const p of localCandidates) if (existsSync(p)) return p;
|
|
1071
|
+
const encoded = rootPath.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
1072
|
+
const centralNotes = join(homedir(), ".claude", "projects", encoded, "Notes");
|
|
1073
|
+
if (existsSync(centralNotes)) return centralNotes;
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Recursively find all .md session note files in a Notes directory.
|
|
1078
|
+
* Handles both flat layout (Notes/*.md) and month-subdirectory layout
|
|
1079
|
+
* (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename
|
|
1080
|
+
* (note numbers are monotonically increasing, so lexicographic = newest-last,
|
|
1081
|
+
* so we reverse).
|
|
1082
|
+
*/
|
|
1083
|
+
function findSessionNotes(notesDir) {
|
|
1084
|
+
const result = [];
|
|
1085
|
+
const scanDir = (dir) => {
|
|
1086
|
+
if (!existsSync(dir)) return;
|
|
1087
|
+
let entries;
|
|
1088
|
+
try {
|
|
1089
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({
|
|
1090
|
+
name: e.name,
|
|
1091
|
+
isDir: e.isDirectory()
|
|
1092
|
+
}));
|
|
1093
|
+
} catch {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
for (const entry of entries) {
|
|
1097
|
+
const fullPath = join(dir, entry.name);
|
|
1098
|
+
if (entry.isDir) scanDir(fullPath);
|
|
1099
|
+
else if (entry.name.match(/^\d{3,4}[\s_-].*\.md$/)) result.push(fullPath);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
scanDir(notesDir);
|
|
1103
|
+
result.sort((a, b) => {
|
|
1104
|
+
const numA = parseInt(basename(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
1105
|
+
return parseInt(basename(b).match(/^(\d+)/)?.[1] ?? "0", 10) - numA;
|
|
1106
|
+
});
|
|
1107
|
+
return result;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Extract the most important lines from a session note.
|
|
1111
|
+
* Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.
|
|
1112
|
+
* Returns a condensed string under maxChars.
|
|
1113
|
+
*/
|
|
1114
|
+
function extractKeyLines(content, maxChars) {
|
|
1115
|
+
const lines = content.split("\n");
|
|
1116
|
+
const selected = [];
|
|
1117
|
+
let inTargetSection = false;
|
|
1118
|
+
let currentSection = "";
|
|
1119
|
+
let charCount = 0;
|
|
1120
|
+
for (const line of lines) {
|
|
1121
|
+
const h2Match = line.match(/^## (.+)$/);
|
|
1122
|
+
const h3Match = line.match(/^### (.+)$/);
|
|
1123
|
+
if (h2Match) {
|
|
1124
|
+
currentSection = h2Match[1];
|
|
1125
|
+
inTargetSection = EXTRACT_SECTIONS.some((s) => currentSection.toLowerCase().includes(s.toLowerCase()));
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (h3Match) {
|
|
1129
|
+
if (inTargetSection) {
|
|
1130
|
+
const label = `[${h3Match[1]}]`;
|
|
1131
|
+
if (charCount + label.length < maxChars) {
|
|
1132
|
+
selected.push(label);
|
|
1133
|
+
charCount += label.length + 1;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (!inTargetSection) continue;
|
|
1139
|
+
const trimmed = line.trim();
|
|
1140
|
+
if (!trimmed || trimmed.startsWith("<!--") || trimmed === "---") continue;
|
|
1141
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
|
|
1142
|
+
if (charCount + trimmed.length + 1 > maxChars) break;
|
|
1143
|
+
selected.push(trimmed);
|
|
1144
|
+
charCount += trimmed.length + 1;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return selected.join("\n");
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Build the L1 essential story block.
|
|
1151
|
+
*
|
|
1152
|
+
* Reads the most recent session notes for the project and extracts the key
|
|
1153
|
+
* lines (Work Done, Key Decisions, Next Steps) within the token budget.
|
|
1154
|
+
*
|
|
1155
|
+
* @param rootPath The project root path (from the registry).
|
|
1156
|
+
* @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).
|
|
1157
|
+
* @returns Formatted L1 block, or empty string if no notes found.
|
|
1158
|
+
*/
|
|
1159
|
+
function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
1160
|
+
const charBudget = tokenBudget * 4;
|
|
1161
|
+
const notesDir = findNotesDirForProject(rootPath);
|
|
1162
|
+
if (!notesDir) return "";
|
|
1163
|
+
const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);
|
|
1164
|
+
if (noteFiles.length === 0) return "";
|
|
1165
|
+
const sections = [];
|
|
1166
|
+
let remaining = charBudget;
|
|
1167
|
+
for (const noteFile of noteFiles) {
|
|
1168
|
+
if (remaining <= 50) break;
|
|
1169
|
+
let content;
|
|
1170
|
+
try {
|
|
1171
|
+
content = readFileSync(noteFile, "utf-8");
|
|
1172
|
+
} catch {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
const name = basename(noteFile);
|
|
1176
|
+
const titleMatch = name.match(/^\d+ - (\d{4}-\d{2}-\d{2}) - (.+)\.md$/);
|
|
1177
|
+
const dateLabel = titleMatch ? titleMatch[1] : "";
|
|
1178
|
+
const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
|
|
1179
|
+
const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
|
|
1180
|
+
const extracted = extractKeyLines(content, perNoteChars);
|
|
1181
|
+
if (!extracted) continue;
|
|
1182
|
+
const noteBlock = `[${dateLabel} - ${titleLabel}]\n${extracted}`;
|
|
1183
|
+
sections.push(noteBlock);
|
|
1184
|
+
remaining -= noteBlock.length + 1;
|
|
1185
|
+
}
|
|
1186
|
+
if (sections.length === 0) return "";
|
|
1187
|
+
return sections.join("\n\n");
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Build the combined wake-up context block (L0 + L1).
|
|
1191
|
+
*
|
|
1192
|
+
* Returns a formatted string suitable for injection as a system-reminder,
|
|
1193
|
+
* or an empty string if both layers are empty.
|
|
1194
|
+
*
|
|
1195
|
+
* @param rootPath Project root path for L1 note lookup. Optional.
|
|
1196
|
+
* @param tokenBudget L1 token budget. Default 800.
|
|
1197
|
+
*/
|
|
1198
|
+
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
1199
|
+
const identity = loadL0Identity();
|
|
1200
|
+
const essentialStory = rootPath ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
1201
|
+
if (!identity && !essentialStory) return "";
|
|
1202
|
+
const parts = [];
|
|
1203
|
+
if (identity) parts.push(`## L0 Identity\n\n${identity}`);
|
|
1204
|
+
if (essentialStory) parts.push(`## L1 Essential Story\n\n${essentialStory}`);
|
|
1205
|
+
return parts.join("\n\n");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region src/mcp/tools/wakeup.ts
|
|
1210
|
+
const DEFAULT_TOKEN_BUDGET = 800;
|
|
1211
|
+
function toolMemoryWakeup(registryDb, params) {
|
|
1212
|
+
try {
|
|
1213
|
+
const tokenBudget = params.token_budget ?? DEFAULT_TOKEN_BUDGET;
|
|
1214
|
+
let rootPath;
|
|
1215
|
+
if (params.project) {
|
|
1216
|
+
const bySlug = registryDb.prepare("SELECT root_path FROM projects WHERE slug = ?").get(params.project);
|
|
1217
|
+
if (bySlug) rootPath = bySlug.root_path;
|
|
1218
|
+
else {
|
|
1219
|
+
const detected = detectProjectFromPath(registryDb, params.project);
|
|
1220
|
+
if (detected) rootPath = detected.root_path;
|
|
1221
|
+
}
|
|
1222
|
+
} else {
|
|
1223
|
+
const detected = detectProjectFromPath(registryDb, process.cwd());
|
|
1224
|
+
if (detected) rootPath = detected.root_path;
|
|
1225
|
+
}
|
|
1226
|
+
const wakeupBlock = buildWakeupContext(rootPath, tokenBudget);
|
|
1227
|
+
if (!wakeupBlock) return { content: [{
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: "No wake-up context available. Create ~/.pai/identity.txt for L0 identity, or ensure session notes exist for L1 story."
|
|
1230
|
+
}] };
|
|
1231
|
+
return { content: [{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: `WAKEUP CONTEXT\n\n${wakeupBlock}`
|
|
1234
|
+
}] };
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
return {
|
|
1237
|
+
content: [{
|
|
1238
|
+
type: "text",
|
|
1239
|
+
text: `Wakeup context error: ${String(e)}`
|
|
1240
|
+
}],
|
|
1241
|
+
isError: true
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/memory/taxonomy.ts
|
|
1248
|
+
/**
|
|
1249
|
+
* Build a taxonomy of stored memory — what projects exist, how much is stored,
|
|
1250
|
+
* and what has been active recently.
|
|
1251
|
+
*
|
|
1252
|
+
* Registry queries (projects, sessions) are synchronous (better-sqlite3).
|
|
1253
|
+
* Storage backend queries (files, chunks) are async.
|
|
1254
|
+
*/
|
|
1255
|
+
async function getTaxonomy(registryDb, storage, options = {}) {
|
|
1256
|
+
const includeArchived = options.include_archived ?? false;
|
|
1257
|
+
const limit = options.limit ?? 50;
|
|
1258
|
+
const statusFilter = includeArchived ? "status IN ('active', 'archived', 'migrating')" : "status = 'active'";
|
|
1259
|
+
const projectRows = registryDb.prepare(`SELECT id, slug, display_name, status, created_at, updated_at
|
|
1260
|
+
FROM projects
|
|
1261
|
+
WHERE ${statusFilter}
|
|
1262
|
+
ORDER BY updated_at DESC
|
|
1263
|
+
LIMIT ?`).all(limit);
|
|
1264
|
+
if (projectRows.length === 0) return {
|
|
1265
|
+
projects: [],
|
|
1266
|
+
totals: {
|
|
1267
|
+
projects: 0,
|
|
1268
|
+
sessions: 0,
|
|
1269
|
+
notes: 0,
|
|
1270
|
+
chunks: 0
|
|
1271
|
+
},
|
|
1272
|
+
recent_activity: []
|
|
1273
|
+
};
|
|
1274
|
+
const projectIds = projectRows.map((p) => p.id);
|
|
1275
|
+
const sessionCountsByProject = /* @__PURE__ */ new Map();
|
|
1276
|
+
const lastSessionDateByProject = /* @__PURE__ */ new Map();
|
|
1277
|
+
for (const projectId of projectIds) {
|
|
1278
|
+
const countRow = registryDb.prepare("SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?").get(projectId);
|
|
1279
|
+
sessionCountsByProject.set(projectId, countRow.n);
|
|
1280
|
+
const lastRow = registryDb.prepare("SELECT date FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1").get(projectId);
|
|
1281
|
+
lastSessionDateByProject.set(projectId, lastRow?.date ?? null);
|
|
1282
|
+
}
|
|
1283
|
+
const tagsByProject = /* @__PURE__ */ new Map();
|
|
1284
|
+
for (const projectId of projectIds) {
|
|
1285
|
+
const tags = registryDb.prepare(`SELECT t.name
|
|
1286
|
+
FROM tags t
|
|
1287
|
+
JOIN project_tags pt ON pt.tag_id = t.id
|
|
1288
|
+
WHERE pt.project_id = ?
|
|
1289
|
+
ORDER BY t.name`).all(projectId);
|
|
1290
|
+
tagsByProject.set(projectId, tags.map((t) => t.name));
|
|
1291
|
+
}
|
|
1292
|
+
const noteCountsByProject = /* @__PURE__ */ new Map();
|
|
1293
|
+
const chunkCountsByProject = /* @__PURE__ */ new Map();
|
|
1294
|
+
const isBackend = (x) => x.backendType === "sqlite";
|
|
1295
|
+
if (isBackend(storage)) {
|
|
1296
|
+
const rawDb = storage.getRawDb?.();
|
|
1297
|
+
if (rawDb) for (const projectId of projectIds) {
|
|
1298
|
+
const noteRow = rawDb.prepare("SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?").get(projectId);
|
|
1299
|
+
noteCountsByProject.set(projectId, noteRow.n);
|
|
1300
|
+
const chunkRow = rawDb.prepare("SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?").get(projectId);
|
|
1301
|
+
chunkCountsByProject.set(projectId, chunkRow.n);
|
|
1302
|
+
}
|
|
1303
|
+
} else for (const projectId of projectIds) {
|
|
1304
|
+
noteCountsByProject.set(projectId, 0);
|
|
1305
|
+
chunkCountsByProject.set(projectId, 0);
|
|
1306
|
+
}
|
|
1307
|
+
const stats = await storage.getStats();
|
|
1308
|
+
const totalProjects = registryDb.prepare(`SELECT COUNT(*) AS n FROM projects WHERE ${statusFilter}`).get().n;
|
|
1309
|
+
const totalSessions = registryDb.prepare("SELECT COUNT(*) AS n FROM sessions").get().n;
|
|
1310
|
+
const recentActivity = registryDb.prepare(`SELECT s.date, s.title, p.slug
|
|
1311
|
+
FROM sessions s
|
|
1312
|
+
JOIN projects p ON p.id = s.project_id
|
|
1313
|
+
WHERE p.${statusFilter.replace("status", "p.status")}
|
|
1314
|
+
ORDER BY s.created_at DESC
|
|
1315
|
+
LIMIT 10`).all().map((row) => ({
|
|
1316
|
+
project_slug: row.slug,
|
|
1317
|
+
action: `session: ${row.title || "(untitled)"}`,
|
|
1318
|
+
timestamp: row.date
|
|
1319
|
+
}));
|
|
1320
|
+
return {
|
|
1321
|
+
projects: projectRows.map((row) => ({
|
|
1322
|
+
slug: row.slug,
|
|
1323
|
+
display_name: row.display_name,
|
|
1324
|
+
session_count: sessionCountsByProject.get(row.id) ?? 0,
|
|
1325
|
+
note_count: noteCountsByProject.get(row.id) ?? 0,
|
|
1326
|
+
last_activity: lastSessionDateByProject.get(row.id) ?? null,
|
|
1327
|
+
top_tags: tagsByProject.get(row.id) ?? []
|
|
1328
|
+
})),
|
|
1329
|
+
totals: {
|
|
1330
|
+
projects: totalProjects,
|
|
1331
|
+
sessions: totalSessions,
|
|
1332
|
+
notes: stats.files,
|
|
1333
|
+
chunks: stats.chunks
|
|
1334
|
+
},
|
|
1335
|
+
recent_activity: recentActivity
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/mcp/tools/taxonomy.ts
|
|
1341
|
+
async function toolMemoryTaxonomy(registryDb, storage, params = {}) {
|
|
1342
|
+
try {
|
|
1343
|
+
const result = await getTaxonomy(registryDb, storage, {
|
|
1344
|
+
include_archived: params.include_archived,
|
|
1345
|
+
limit: params.limit
|
|
1346
|
+
});
|
|
1347
|
+
const lines = [];
|
|
1348
|
+
lines.push(`PAI Memory Taxonomy — ${result.totals.projects} project(s), ${result.totals.sessions} session(s), ${result.totals.notes} indexed file(s), ${result.totals.chunks} chunk(s)`);
|
|
1349
|
+
lines.push("");
|
|
1350
|
+
if (result.projects.length === 0) lines.push("No active projects found.");
|
|
1351
|
+
else {
|
|
1352
|
+
lines.push("Projects:");
|
|
1353
|
+
for (const p of result.projects) {
|
|
1354
|
+
const tagStr = p.top_tags.length > 0 ? ` [${p.top_tags.join(", ")}]` : "";
|
|
1355
|
+
const activityStr = p.last_activity ? ` last: ${p.last_activity}` : "";
|
|
1356
|
+
lines.push(` ${p.slug} — ${p.display_name} sessions=${p.session_count}` + (p.note_count > 0 ? ` files=${p.note_count}` : "") + activityStr + tagStr);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (result.recent_activity.length > 0) {
|
|
1360
|
+
lines.push("");
|
|
1361
|
+
lines.push("Recent activity (last 10 sessions across all projects):");
|
|
1362
|
+
for (const a of result.recent_activity) lines.push(` ${a.timestamp} ${a.project_slug} ${a.action}`);
|
|
1363
|
+
}
|
|
1364
|
+
return { content: [{
|
|
1365
|
+
type: "text",
|
|
1366
|
+
text: lines.join("\n")
|
|
1367
|
+
}] };
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
return {
|
|
1370
|
+
content: [{
|
|
1371
|
+
type: "text",
|
|
1372
|
+
text: `memory_taxonomy error: ${String(e)}`
|
|
1373
|
+
}],
|
|
1374
|
+
isError: true
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/memory/tunnels.ts
|
|
1381
|
+
/**
|
|
1382
|
+
* tunnels.ts — cross-project concept detection ("palace graph / tunnel detection")
|
|
1383
|
+
*
|
|
1384
|
+
* A "tunnel" is a concept (word or short phrase) that appears in chunks from
|
|
1385
|
+
* at least two distinct projects. These serendipitous cross-project connections
|
|
1386
|
+
* are surfaced so the user can discover unexpected relationships between their
|
|
1387
|
+
* work streams.
|
|
1388
|
+
*
|
|
1389
|
+
* Algorithm:
|
|
1390
|
+
* 1. Pull the top-N most frequent significant terms from memory_chunks via BM25 FTS.
|
|
1391
|
+
* We use the FTS5 vocab table (if available) or fall back to term frequency
|
|
1392
|
+
* aggregation over the raw text via a trigram approach.
|
|
1393
|
+
* 2. For each candidate term, count how many distinct projects have at least one
|
|
1394
|
+
* chunk containing it and aggregate occurrence stats.
|
|
1395
|
+
* 3. Filter by min_projects and min_occurrences, sort by project breadth then
|
|
1396
|
+
* frequency, return top limit results.
|
|
1397
|
+
*
|
|
1398
|
+
* Backend support:
|
|
1399
|
+
* - SQLite — uses `memory_fts` MATCH to count per-project occurrences.
|
|
1400
|
+
* - Postgres — uses `memory_chunks` tsvector + ts_stat for term extraction and
|
|
1401
|
+
* per-project term frequency counting via plainto_tsquery.
|
|
1402
|
+
*/
|
|
1403
|
+
/**
|
|
1404
|
+
* Extract candidate terms from the SQLite FTS5 index using the vocabulary
|
|
1405
|
+
* approach: iterate the fts5vocab table (if it exists) for the most common
|
|
1406
|
+
* terms, then per-term count distinct projects.
|
|
1407
|
+
*/
|
|
1408
|
+
async function findTunnelsSqlite(db, slugMap, opts) {
|
|
1409
|
+
const projectIds = Object.keys(slugMap).map(Number);
|
|
1410
|
+
if (projectIds.length < 2) return {
|
|
1411
|
+
tunnels: [],
|
|
1412
|
+
projects_analyzed: projectIds.length,
|
|
1413
|
+
total_concepts_evaluated: 0
|
|
1414
|
+
};
|
|
1415
|
+
let candidateTerms = [];
|
|
1416
|
+
try {
|
|
1417
|
+
candidateTerms = db.prepare(`SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt
|
|
1418
|
+
FROM memory_fts_v
|
|
1419
|
+
GROUP BY term
|
|
1420
|
+
HAVING SUM(cnt) >= ?
|
|
1421
|
+
ORDER BY SUM(doc) DESC
|
|
1422
|
+
LIMIT 500`).all(opts.min_occurrences).map((r) => r.term).filter((t) => t.length >= 3 && !STOP_WORDS.has(t));
|
|
1423
|
+
} catch {
|
|
1424
|
+
const sampleRows = db.prepare(`SELECT LOWER(text) AS text FROM memory_chunks
|
|
1425
|
+
WHERE LENGTH(text) > 20
|
|
1426
|
+
ORDER BY RANDOM()
|
|
1427
|
+
LIMIT 2000`).all();
|
|
1428
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1429
|
+
for (const { text } of sampleRows) {
|
|
1430
|
+
const tokens = text.split(/[\s\p{P}]+/u).filter(Boolean).filter((t) => t.length >= 3 && !STOP_WORDS.has(t));
|
|
1431
|
+
for (const t of tokens) freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
1432
|
+
}
|
|
1433
|
+
candidateTerms = [...freq.entries()].filter(([, n]) => n >= opts.min_occurrences).sort((a, b) => b[1] - a[1]).slice(0, 200).map(([t]) => t);
|
|
1434
|
+
}
|
|
1435
|
+
if (candidateTerms.length === 0) return {
|
|
1436
|
+
tunnels: [],
|
|
1437
|
+
projects_analyzed: projectIds.length,
|
|
1438
|
+
total_concepts_evaluated: 0
|
|
1439
|
+
};
|
|
1440
|
+
const tunnels = [];
|
|
1441
|
+
for (const term of candidateTerms) try {
|
|
1442
|
+
const rows = db.prepare(`SELECT c.project_id, COUNT(*) AS cnt,
|
|
1443
|
+
MIN(c.updated_at) AS first_seen,
|
|
1444
|
+
MAX(c.updated_at) AS last_seen
|
|
1445
|
+
FROM memory_fts f
|
|
1446
|
+
JOIN memory_chunks c ON c.id = f.id
|
|
1447
|
+
WHERE memory_fts MATCH ?
|
|
1448
|
+
AND c.project_id IN (${projectIds.map(() => "?").join(", ")})
|
|
1449
|
+
GROUP BY c.project_id`).all(`"${term.replace(/"/g, "\"\"")}"`, ...projectIds);
|
|
1450
|
+
if (rows.length < opts.min_projects) continue;
|
|
1451
|
+
const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);
|
|
1452
|
+
if (totalOccurrences < opts.min_occurrences) continue;
|
|
1453
|
+
const projects = rows.map((r) => slugMap[r.project_id] ?? String(r.project_id)).filter(Boolean);
|
|
1454
|
+
const firstSeen = Math.min(...rows.map((r) => r.first_seen));
|
|
1455
|
+
const lastSeen = Math.max(...rows.map((r) => r.last_seen));
|
|
1456
|
+
tunnels.push({
|
|
1457
|
+
concept: term,
|
|
1458
|
+
projects,
|
|
1459
|
+
occurrences: totalOccurrences,
|
|
1460
|
+
first_seen: firstSeen,
|
|
1461
|
+
last_seen: lastSeen
|
|
1462
|
+
});
|
|
1463
|
+
} catch {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
tunnels.sort((a, b) => {
|
|
1467
|
+
const byProjects = b.projects.length - a.projects.length;
|
|
1468
|
+
if (byProjects !== 0) return byProjects;
|
|
1469
|
+
return b.occurrences - a.occurrences;
|
|
1470
|
+
});
|
|
1471
|
+
return {
|
|
1472
|
+
tunnels: tunnels.slice(0, opts.limit),
|
|
1473
|
+
projects_analyzed: projectIds.length,
|
|
1474
|
+
total_concepts_evaluated: candidateTerms.length
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Use Postgres ts_stat() + plainto_tsquery to efficiently find terms that
|
|
1479
|
+
* appear across multiple projects.
|
|
1480
|
+
*/
|
|
1481
|
+
async function findTunnelsPostgres(pool, slugMap, opts) {
|
|
1482
|
+
const projectIds = Object.keys(slugMap).map(Number);
|
|
1483
|
+
if (projectIds.length < 2) return {
|
|
1484
|
+
tunnels: [],
|
|
1485
|
+
projects_analyzed: projectIds.length,
|
|
1486
|
+
total_concepts_evaluated: 0
|
|
1487
|
+
};
|
|
1488
|
+
let candidateTerms = (await pool.query(`SELECT word, ndoc, nentry
|
|
1489
|
+
FROM ts_stat(
|
|
1490
|
+
'SELECT to_tsvector(''simple'', text) FROM memory_chunks WHERE project_id = ANY($1)'
|
|
1491
|
+
)
|
|
1492
|
+
WHERE length(word) >= 3
|
|
1493
|
+
AND nentry >= $2
|
|
1494
|
+
ORDER BY ndoc DESC
|
|
1495
|
+
LIMIT 500`, [projectIds, opts.min_occurrences])).rows.map((r) => r.word).filter((t) => !STOP_WORDS.has(t));
|
|
1496
|
+
if (candidateTerms.length === 0) return {
|
|
1497
|
+
tunnels: [],
|
|
1498
|
+
projects_analyzed: projectIds.length,
|
|
1499
|
+
total_concepts_evaluated: 0
|
|
1500
|
+
};
|
|
1501
|
+
candidateTerms = candidateTerms.slice(0, 200);
|
|
1502
|
+
const valuesClause = candidateTerms.map((t, i) => `($${i + 2}::text)`).join(", ");
|
|
1503
|
+
const batchResult = await pool.query(`SELECT v.concept, c.project_id::text, COUNT(*) AS cnt,
|
|
1504
|
+
MIN(c.updated_at) AS first_seen,
|
|
1505
|
+
MAX(c.updated_at) AS last_seen
|
|
1506
|
+
FROM (VALUES ${valuesClause}) AS v(concept)
|
|
1507
|
+
JOIN memory_chunks c
|
|
1508
|
+
ON to_tsvector('simple', c.text) @@ plainto_tsquery('simple', v.concept)
|
|
1509
|
+
AND c.project_id = ANY($1)
|
|
1510
|
+
GROUP BY v.concept, c.project_id`, [projectIds, ...candidateTerms]);
|
|
1511
|
+
const byConceptMap = /* @__PURE__ */ new Map();
|
|
1512
|
+
for (const row of batchResult.rows) {
|
|
1513
|
+
const existing = byConceptMap.get(row.concept) ?? {
|
|
1514
|
+
projects: /* @__PURE__ */ new Set(),
|
|
1515
|
+
occurrences: 0,
|
|
1516
|
+
firstSeen: Infinity,
|
|
1517
|
+
lastSeen: -Infinity
|
|
1518
|
+
};
|
|
1519
|
+
existing.projects.add(parseInt(row.project_id, 10));
|
|
1520
|
+
existing.occurrences += parseInt(row.cnt, 10);
|
|
1521
|
+
const fs = parseInt(row.first_seen, 10);
|
|
1522
|
+
const ls = parseInt(row.last_seen, 10);
|
|
1523
|
+
if (fs < existing.firstSeen) existing.firstSeen = fs;
|
|
1524
|
+
if (ls > existing.lastSeen) existing.lastSeen = ls;
|
|
1525
|
+
byConceptMap.set(row.concept, existing);
|
|
1526
|
+
}
|
|
1527
|
+
const tunnels = [];
|
|
1528
|
+
for (const [concept, data] of byConceptMap) {
|
|
1529
|
+
if (data.projects.size < opts.min_projects) continue;
|
|
1530
|
+
if (data.occurrences < opts.min_occurrences) continue;
|
|
1531
|
+
const projects = [...data.projects].map((id) => slugMap[id] ?? String(id)).filter(Boolean);
|
|
1532
|
+
tunnels.push({
|
|
1533
|
+
concept,
|
|
1534
|
+
projects,
|
|
1535
|
+
occurrences: data.occurrences,
|
|
1536
|
+
first_seen: data.firstSeen === Infinity ? 0 : data.firstSeen,
|
|
1537
|
+
last_seen: data.lastSeen === -Infinity ? 0 : data.lastSeen
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
tunnels.sort((a, b) => {
|
|
1541
|
+
const byProjects = b.projects.length - a.projects.length;
|
|
1542
|
+
if (byProjects !== 0) return byProjects;
|
|
1543
|
+
return b.occurrences - a.occurrences;
|
|
1544
|
+
});
|
|
1545
|
+
return {
|
|
1546
|
+
tunnels: tunnels.slice(0, opts.limit),
|
|
1547
|
+
projects_analyzed: projectIds.length,
|
|
1548
|
+
total_concepts_evaluated: candidateTerms.length
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Find cross-project concept tunnels.
|
|
1553
|
+
*
|
|
1554
|
+
* Works with both SQLite and Postgres storage backends.
|
|
1555
|
+
* Requires the `registryDb` (better-sqlite3) for project slug resolution.
|
|
1556
|
+
*
|
|
1557
|
+
* @param backend Active PAI storage backend.
|
|
1558
|
+
* @param registryDb Registry database for project slug resolution.
|
|
1559
|
+
* @param options Filter and limit options.
|
|
1560
|
+
*/
|
|
1561
|
+
async function findTunnels(backend, registryDb, options) {
|
|
1562
|
+
const opts = {
|
|
1563
|
+
min_projects: options?.min_projects ?? 2,
|
|
1564
|
+
min_occurrences: options?.min_occurrences ?? 3,
|
|
1565
|
+
limit: options?.limit ?? 20
|
|
1566
|
+
};
|
|
1567
|
+
const projectRows = registryDb.prepare("SELECT id, slug FROM projects WHERE status != 'archived'").all();
|
|
1568
|
+
const slugMap = {};
|
|
1569
|
+
for (const { id, slug } of projectRows) slugMap[id] = slug;
|
|
1570
|
+
if (backend.backendType === "postgres") {
|
|
1571
|
+
const pool = backend.getPool?.();
|
|
1572
|
+
if (!pool) throw new Error("findTunnels: Postgres backend does not expose getPool()");
|
|
1573
|
+
return findTunnelsPostgres(pool, slugMap, opts);
|
|
1574
|
+
}
|
|
1575
|
+
const rawDb = backend.getRawDb?.();
|
|
1576
|
+
if (!rawDb) throw new Error("findTunnels: SQLite backend does not expose getRawDb()");
|
|
1577
|
+
return findTunnelsSqlite(rawDb, slugMap, opts);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/mcp/tools/tunnels.ts
|
|
1582
|
+
async function toolMemoryTunnels(registryDb, backend, params) {
|
|
1583
|
+
try {
|
|
1584
|
+
const result = await findTunnels(backend, registryDb, {
|
|
1585
|
+
min_projects: params.min_projects,
|
|
1586
|
+
min_occurrences: params.min_occurrences,
|
|
1587
|
+
limit: params.limit
|
|
1588
|
+
});
|
|
1589
|
+
return { content: [{
|
|
1590
|
+
type: "text",
|
|
1591
|
+
text: JSON.stringify(result, null, 2)
|
|
1592
|
+
}] };
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
return {
|
|
1595
|
+
content: [{
|
|
1596
|
+
type: "text",
|
|
1597
|
+
text: `memory_tunnels error: ${String(e)}`
|
|
1598
|
+
}],
|
|
1599
|
+
isError: true
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
909
1604
|
//#endregion
|
|
910
1605
|
//#region src/mcp/tools.ts
|
|
911
1606
|
var tools_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -913,8 +1608,15 @@ var tools_exports = /* @__PURE__ */ __exportAll({
|
|
|
913
1608
|
detectProjectFromPath: () => detectProjectFromPath,
|
|
914
1609
|
formatProject: () => formatProject,
|
|
915
1610
|
lookupProjectId: () => lookupProjectId,
|
|
1611
|
+
toolKgAdd: () => toolKgAdd,
|
|
1612
|
+
toolKgContradictions: () => toolKgContradictions,
|
|
1613
|
+
toolKgInvalidate: () => toolKgInvalidate,
|
|
1614
|
+
toolKgQuery: () => toolKgQuery,
|
|
916
1615
|
toolMemoryGet: () => toolMemoryGet,
|
|
917
1616
|
toolMemorySearch: () => toolMemorySearch,
|
|
1617
|
+
toolMemoryTaxonomy: () => toolMemoryTaxonomy,
|
|
1618
|
+
toolMemoryTunnels: () => toolMemoryTunnels,
|
|
1619
|
+
toolMemoryWakeup: () => toolMemoryWakeup,
|
|
918
1620
|
toolProjectDetect: () => toolProjectDetect,
|
|
919
1621
|
toolProjectHealth: () => toolProjectHealth,
|
|
920
1622
|
toolProjectInfo: () => toolProjectInfo,
|
|
@@ -934,5 +1636,5 @@ var tools_exports = /* @__PURE__ */ __exportAll({
|
|
|
934
1636
|
});
|
|
935
1637
|
|
|
936
1638
|
//#endregion
|
|
937
|
-
export {
|
|
938
|
-
//# sourceMappingURL=tools-
|
|
1639
|
+
export { toolSessionList as a, toolProjectHealth as c, toolProjectTodo as d, toolMemoryGet as f, toolRegistrySearch as i, toolProjectInfo as l, toolMemoryTaxonomy as n, toolSessionRoute as o, toolMemorySearch as p, toolMemoryWakeup as r, toolProjectDetect as s, tools_exports as t, toolProjectList as u };
|
|
1640
|
+
//# sourceMappingURL=tools-8t7BQrm9.mjs.map
|