recallx 1.0.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.
Files changed (37) hide show
  1. package/README.md +205 -0
  2. package/app/cli/bin/recallx-mcp.js +2 -0
  3. package/app/cli/bin/recallx.js +8 -0
  4. package/app/cli/src/cli.js +808 -0
  5. package/app/cli/src/format.js +242 -0
  6. package/app/cli/src/http.js +35 -0
  7. package/app/mcp/api-client.js +101 -0
  8. package/app/mcp/index.js +128 -0
  9. package/app/mcp/server.js +786 -0
  10. package/app/server/app.js +2263 -0
  11. package/app/server/config.js +27 -0
  12. package/app/server/db.js +399 -0
  13. package/app/server/errors.js +17 -0
  14. package/app/server/governance.js +466 -0
  15. package/app/server/index.js +26 -0
  16. package/app/server/inferred-relations.js +247 -0
  17. package/app/server/observability.js +495 -0
  18. package/app/server/project-graph.js +199 -0
  19. package/app/server/relation-scoring.js +59 -0
  20. package/app/server/repositories.js +2992 -0
  21. package/app/server/retrieval.js +486 -0
  22. package/app/server/semantic/chunker.js +85 -0
  23. package/app/server/semantic/provider.js +124 -0
  24. package/app/server/semantic/types.js +1 -0
  25. package/app/server/semantic/vector-store.js +169 -0
  26. package/app/server/utils.js +43 -0
  27. package/app/server/workspace-session.js +128 -0
  28. package/app/server/workspace.js +79 -0
  29. package/app/shared/contracts.js +268 -0
  30. package/app/shared/request-runtime.js +30 -0
  31. package/app/shared/types.js +1 -0
  32. package/app/shared/version.js +1 -0
  33. package/dist/renderer/assets/ProjectGraphCanvas-BMvz9DmE.js +312 -0
  34. package/dist/renderer/assets/index-C2-KXqBO.css +1 -0
  35. package/dist/renderer/assets/index-CrDu22h7.js +76 -0
  36. package/dist/renderer/index.html +13 -0
  37. package/package.json +49 -0
@@ -0,0 +1,247 @@
1
+ const AUTO_INFERRED_GENERATORS = [
2
+ "deterministic-tag-overlap",
3
+ "deterministic-body-reference",
4
+ "deterministic-activity-reference",
5
+ "deterministic-project-membership",
6
+ "deterministic-shared-artifact"
7
+ ];
8
+ const MAX_CANDIDATES = 200;
9
+ const MAX_INFERRED_PER_NODE = 12;
10
+ const MAX_ACTIVITY_BODIES = 8;
11
+ function normalizeText(value) {
12
+ return (value ?? "").toLowerCase().replace(/\s+/g, " ").trim();
13
+ }
14
+ function normalizeTags(tags) {
15
+ return Array.from(new Set(tags.map((tag) => normalizeText(tag)).filter(Boolean)));
16
+ }
17
+ function escapeRegExp(value) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+ function titlePattern(title) {
21
+ const normalized = normalizeText(title);
22
+ if (normalized.length < 5) {
23
+ return null;
24
+ }
25
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(normalized)}([^a-z0-9]|$)`, "i");
26
+ }
27
+ function mentionsNode(haystacks, candidate) {
28
+ const normalizedHaystacks = haystacks.map(normalizeText).filter(Boolean);
29
+ const idMention = normalizedHaystacks.some((haystack) => haystack.includes(candidate.id.toLowerCase()));
30
+ const candidateTitlePattern = candidate.title ? titlePattern(candidate.title) : null;
31
+ const titleMention = candidateTitlePattern
32
+ ? normalizedHaystacks.some((haystack) => candidateTitlePattern.test(haystack))
33
+ : false;
34
+ return { idMention, titleMention };
35
+ }
36
+ function sortPair(left, right) {
37
+ return left.localeCompare(right) <= 0 ? [left, right] : [right, left];
38
+ }
39
+ function buildTagOverlapCandidate(target, candidate, targetTags) {
40
+ const candidateTags = new Set(normalizeTags(candidate.tags));
41
+ const sharedTags = targetTags.filter((tag) => candidateTags.has(tag));
42
+ if (!sharedTags.length) {
43
+ return null;
44
+ }
45
+ const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
46
+ return {
47
+ fromNodeId,
48
+ toNodeId,
49
+ relationType: "related_to",
50
+ generator: "deterministic-tag-overlap",
51
+ baseScore: Math.min(0.62, 0.36 + sharedTags.length * 0.1),
52
+ evidence: {
53
+ sharedTags
54
+ }
55
+ };
56
+ }
57
+ function buildBodyReferenceCandidate(target, candidate) {
58
+ const targetMentionsCandidate = mentionsNode([target.title ?? "", target.body ?? "", target.summary ?? ""], candidate);
59
+ const candidateMentionsTarget = mentionsNode([candidate.title ?? "", candidate.body ?? "", candidate.summary ?? ""], target);
60
+ if (!targetMentionsCandidate.idMention && !targetMentionsCandidate.titleMention && !candidateMentionsTarget.idMention && !candidateMentionsTarget.titleMention) {
61
+ return null;
62
+ }
63
+ const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
64
+ const idMentionCount = Number(targetMentionsCandidate.idMention) + Number(candidateMentionsTarget.idMention);
65
+ const titleMentionCount = Number(targetMentionsCandidate.titleMention) + Number(candidateMentionsTarget.titleMention);
66
+ return {
67
+ fromNodeId,
68
+ toNodeId,
69
+ relationType: "relevant_to",
70
+ generator: "deterministic-body-reference",
71
+ baseScore: Math.min(0.82, 0.52 + idMentionCount * 0.16 + titleMentionCount * 0.1),
72
+ evidence: {
73
+ targetMentionsCandidate,
74
+ candidateMentionsTarget
75
+ }
76
+ };
77
+ }
78
+ function buildActivityReferenceCandidate(target, candidate, activityBodies) {
79
+ const activityMentionsCandidate = mentionsNode(activityBodies, candidate);
80
+ const mentionCount = Number(activityMentionsCandidate.idMention) * 2 + Number(activityMentionsCandidate.titleMention);
81
+ if (!mentionCount) {
82
+ return null;
83
+ }
84
+ const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
85
+ return {
86
+ fromNodeId,
87
+ toNodeId,
88
+ relationType: "relevant_to",
89
+ generator: "deterministic-activity-reference",
90
+ baseScore: Math.min(0.74, 0.45 + mentionCount * 0.11),
91
+ evidence: {
92
+ targetNodeId: target.id,
93
+ activityMentionsCandidate
94
+ }
95
+ };
96
+ }
97
+ function buildProjectMembershipCandidate(target, candidate, candidateProjectIds, targetProjects) {
98
+ const sharedProjectIds = candidateProjectIds.filter((projectId) => targetProjects.has(projectId));
99
+ if (!sharedProjectIds.length) {
100
+ return null;
101
+ }
102
+ const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
103
+ return {
104
+ fromNodeId,
105
+ toNodeId,
106
+ relationType: "relevant_to",
107
+ generator: "deterministic-project-membership",
108
+ baseScore: Math.min(0.8, 0.58 + sharedProjectIds.length * 0.08),
109
+ evidence: {
110
+ sharedProjectIds
111
+ }
112
+ };
113
+ }
114
+ function buildSharedArtifactCandidate(target, candidate, candidateArtifacts, targetArtifacts) {
115
+ const candidateExact = new Set(candidateArtifacts.exactPaths);
116
+ const candidateBase = new Set(candidateArtifacts.baseNames);
117
+ const sharedExactPaths = targetArtifacts.exactPaths.filter((artifactPath) => candidateExact.has(artifactPath));
118
+ const sharedBaseNames = targetArtifacts.baseNames.filter((baseName) => candidateBase.has(baseName));
119
+ if (!sharedExactPaths.length && !sharedBaseNames.length) {
120
+ return null;
121
+ }
122
+ const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
123
+ return {
124
+ fromNodeId,
125
+ toNodeId,
126
+ relationType: "related_to",
127
+ generator: "deterministic-shared-artifact",
128
+ baseScore: Math.min(0.76, 0.48 + sharedExactPaths.length * 0.16 + sharedBaseNames.length * 0.06),
129
+ evidence: {
130
+ sharedExactPaths,
131
+ sharedBaseNames
132
+ }
133
+ };
134
+ }
135
+ function collectGeneratedCandidates(repository, target, trigger) {
136
+ const candidateMap = new Map();
137
+ for (const candidate of repository.listInferenceCandidateNodes(target.id, MAX_CANDIDATES)) {
138
+ candidateMap.set(candidate.id, candidate);
139
+ }
140
+ const extraCandidateIds = [
141
+ ...repository.listSharedProjectMemberNodeIds(target.id, MAX_CANDIDATES),
142
+ ...repository.listNodesSharingArtifactPaths(target.id, MAX_CANDIDATES)
143
+ ].filter((candidateId) => !candidateMap.has(candidateId));
144
+ for (const candidate of repository.getNodesByIds(extraCandidateIds).values()) {
145
+ if (candidate.status === "active" || candidate.status === "contested") {
146
+ candidateMap.set(candidate.id, candidate);
147
+ }
148
+ }
149
+ const candidates = Array.from(candidateMap.values());
150
+ const projectMembershipsByNodeId = repository.listProjectMembershipIdsByNodeIds([target.id, ...candidates.map((candidate) => candidate.id)]);
151
+ const artifactKeysByNodeId = repository.listArtifactKeysByNodeIds([target.id, ...candidates.map((candidate) => candidate.id)]);
152
+ const targetContext = {
153
+ normalizedTags: normalizeTags(target.tags),
154
+ projectIds: new Set(projectMembershipsByNodeId.get(target.id) ?? []),
155
+ artifactKeys: artifactKeysByNodeId.get(target.id) ?? { exactPaths: [], baseNames: [] }
156
+ };
157
+ const activityBodies = trigger === "activity-append" || trigger === "reindex"
158
+ ? repository
159
+ .listNodeActivities(target.id, MAX_ACTIVITY_BODIES)
160
+ .map((activity) => activity.body ?? "")
161
+ .filter(Boolean)
162
+ : [];
163
+ return candidates.flatMap((candidate) => {
164
+ const generated = [];
165
+ const tagOverlapCandidate = buildTagOverlapCandidate(target, candidate, targetContext.normalizedTags);
166
+ if (tagOverlapCandidate) {
167
+ generated.push(tagOverlapCandidate);
168
+ }
169
+ const bodyReferenceCandidate = buildBodyReferenceCandidate(target, candidate);
170
+ if (bodyReferenceCandidate) {
171
+ generated.push(bodyReferenceCandidate);
172
+ }
173
+ if (activityBodies.length) {
174
+ const activityReferenceCandidate = buildActivityReferenceCandidate(target, candidate, activityBodies);
175
+ if (activityReferenceCandidate) {
176
+ generated.push(activityReferenceCandidate);
177
+ }
178
+ }
179
+ const projectMembershipCandidate = buildProjectMembershipCandidate(target, candidate, projectMembershipsByNodeId.get(candidate.id) ?? [], targetContext.projectIds);
180
+ if (projectMembershipCandidate) {
181
+ generated.push(projectMembershipCandidate);
182
+ }
183
+ const sharedArtifactCandidate = buildSharedArtifactCandidate(target, candidate, artifactKeysByNodeId.get(candidate.id) ?? { exactPaths: [], baseNames: [] }, targetContext.artifactKeys);
184
+ if (sharedArtifactCandidate) {
185
+ generated.push(sharedArtifactCandidate);
186
+ }
187
+ return generated;
188
+ });
189
+ }
190
+ export function refreshAutomaticInferredRelationsForNode(repository, nodeId, trigger) {
191
+ const target = repository.getNode(nodeId);
192
+ if (target.status === "archived") {
193
+ const expiredCount = repository.expireAutoInferredRelationsForNode(nodeId, [...AUTO_INFERRED_GENERATORS]);
194
+ return { upsertedCount: 0, expiredCount, relationIds: [] };
195
+ }
196
+ const deduped = new Map();
197
+ for (const candidate of collectGeneratedCandidates(repository, target, trigger)) {
198
+ const key = [candidate.fromNodeId, candidate.toNodeId, candidate.relationType, candidate.generator].join(":");
199
+ const existing = deduped.get(key);
200
+ if (!existing || candidate.baseScore > existing.baseScore) {
201
+ deduped.set(key, candidate);
202
+ }
203
+ }
204
+ const relationIds = Array.from(deduped.values())
205
+ .sort((left, right) => right.baseScore - left.baseScore)
206
+ .slice(0, MAX_INFERRED_PER_NODE)
207
+ .map((candidate) => repository.upsertInferredRelation({
208
+ fromNodeId: candidate.fromNodeId,
209
+ toNodeId: candidate.toNodeId,
210
+ relationType: candidate.relationType,
211
+ baseScore: candidate.baseScore,
212
+ usageScore: 0,
213
+ finalScore: candidate.baseScore,
214
+ status: "active",
215
+ generator: candidate.generator,
216
+ evidence: candidate.evidence,
217
+ metadata: {
218
+ trigger
219
+ }
220
+ }).id);
221
+ const expiredCount = repository.expireAutoInferredRelationsForNode(nodeId, [...AUTO_INFERRED_GENERATORS], relationIds);
222
+ return {
223
+ upsertedCount: relationIds.length,
224
+ expiredCount,
225
+ relationIds
226
+ };
227
+ }
228
+ export function reindexAutomaticInferredRelations(repository, input) {
229
+ const targetNodeIds = repository.listInferenceTargetNodeIds(input?.limit ?? 250);
230
+ let upsertedCount = 0;
231
+ let expiredCount = 0;
232
+ const relationIds = new Set();
233
+ for (const nodeId of targetNodeIds) {
234
+ const result = refreshAutomaticInferredRelationsForNode(repository, nodeId, "reindex");
235
+ upsertedCount += result.upsertedCount;
236
+ expiredCount += result.expiredCount;
237
+ for (const relationId of result.relationIds) {
238
+ relationIds.add(relationId);
239
+ }
240
+ }
241
+ return {
242
+ processedNodes: targetNodeIds.length,
243
+ upsertedCount,
244
+ expiredCount,
245
+ relationIds: Array.from(relationIds)
246
+ };
247
+ }