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,242 @@
1
+ function pad(label, value) {
2
+ return `${label}: ${value ?? ""}`;
3
+ }
4
+
5
+ export function renderJson(value) {
6
+ return `${JSON.stringify(value, null, 2)}\n`;
7
+ }
8
+
9
+ export function renderText(value) {
10
+ if (value == null) {
11
+ return "";
12
+ }
13
+
14
+ if (typeof value === "string") {
15
+ return `${value}\n`;
16
+ }
17
+
18
+ if (Array.isArray(value)) {
19
+ return `${value.map((item) => `- ${renderInline(item)}`).join("\n")}\n`;
20
+ }
21
+
22
+ if (typeof value === "object") {
23
+ return `${Object.entries(value)
24
+ .map(([key, entry]) => `${key}: ${renderInline(entry)}`)
25
+ .join("\n")}\n`;
26
+ }
27
+
28
+ return `${String(value)}\n`;
29
+ }
30
+
31
+ export function renderNode(node) {
32
+ const lines = [
33
+ `${node.title || node.id}`,
34
+ pad("id", node.id),
35
+ pad("type", node.type),
36
+ pad("status", node.status),
37
+ pad("canonicality", node.canonicality),
38
+ pad("summary", node.summary),
39
+ pad("source", node.sourceLabel || node.source_label || ""),
40
+ ];
41
+
42
+ if (Array.isArray(node.tags) && node.tags.length > 0) {
43
+ lines.push(pad("tags", node.tags.join(", ")));
44
+ }
45
+
46
+ if (node.body) {
47
+ lines.push("");
48
+ lines.push(node.body);
49
+ }
50
+
51
+ return `${lines.join("\n")}\n`;
52
+ }
53
+
54
+ export function renderSearchResults(data) {
55
+ const items = data?.items || [];
56
+ if (!items.length) {
57
+ return "No results.\n";
58
+ }
59
+
60
+ return `${items
61
+ .map((item, index) => {
62
+ const summary = item.summary ? `\n ${item.summary}` : "";
63
+ return `${index + 1}. ${item.title || item.id} (${item.type || "node"})\n id: ${item.id}\n status: ${item.status || ""}${summary}`;
64
+ })
65
+ .join("\n\n")}\n`;
66
+ }
67
+
68
+ export function renderActivitySearchResults(data) {
69
+ const items = data?.items || [];
70
+ if (!items.length) {
71
+ return "No activity results.\n";
72
+ }
73
+
74
+ return `${items
75
+ .map((item, index) => {
76
+ const headline = item.targetNodeTitle || item.targetNodeId;
77
+ const body = item.body ? `\n ${item.body}` : "";
78
+ return `${index + 1}. ${headline} (${item.activityType})\n id: ${item.id}\n target: ${item.targetNodeId}\n created: ${item.createdAt}${body}`;
79
+ })
80
+ .join("\n\n")}\n`;
81
+ }
82
+
83
+ export function renderWorkspaceSearchResults(data) {
84
+ const items = data?.items || [];
85
+ if (!items.length) {
86
+ return "No workspace results.\n";
87
+ }
88
+
89
+ return `${items
90
+ .map((item, index) => {
91
+ if (item.resultType === "activity" && item.activity) {
92
+ const headline = item.activity.targetNodeTitle || item.activity.targetNodeId;
93
+ const body = item.activity.body ? `\n ${item.activity.body}` : "";
94
+ return `${index + 1}. [activity] ${headline} (${item.activity.activityType})\n id: ${item.activity.id}\n target: ${item.activity.targetNodeId}\n created: ${item.activity.createdAt}${body}`;
95
+ }
96
+
97
+ const node = item.node || {};
98
+ const summary = node.summary ? `\n ${node.summary}` : "";
99
+ return `${index + 1}. [node] ${node.title || node.id} (${node.type || "node"})\n id: ${node.id}\n status: ${node.status || ""}${summary}`;
100
+ })
101
+ .join("\n\n")}\n`;
102
+ }
103
+
104
+ export function renderRelated(data) {
105
+ const items = data?.items || data?.related || [];
106
+ if (!items.length) {
107
+ return "No related nodes.\n";
108
+ }
109
+
110
+ return `${items
111
+ .map((item, index) => {
112
+ const node = item?.node || item;
113
+ const relation = item?.relation || item;
114
+ return `${index + 1}. ${node?.title || node?.nodeId || node?.id} (${relation?.relationType || node?.type || ""})`;
115
+ })
116
+ .join("\n")}\n`;
117
+ }
118
+
119
+ export function renderGovernanceIssues(data) {
120
+ const items = data?.items || [];
121
+ if (!items.length) {
122
+ return "No governance issues.\n";
123
+ }
124
+
125
+ return `${items
126
+ .map(
127
+ (item, index) =>
128
+ `${index + 1}. ${item.title || item.entityId}\n entity: ${item.entityType || ""}:${item.entityId || ""}\n state: ${item.state || ""}\n confidence: ${item.confidence ?? ""}\n reasons: ${Array.isArray(item.reasons) ? item.reasons.join(", ") : ""}`,
129
+ )
130
+ .join("\n\n")}\n`;
131
+ }
132
+
133
+ export function renderWorkspaces(data) {
134
+ const items = data?.items || [];
135
+ if (!items.length) {
136
+ return "No workspaces.\n";
137
+ }
138
+
139
+ return `${items
140
+ .map((item, index) => {
141
+ const marker = item.isCurrent ? "*" : " ";
142
+ return `${index + 1}. ${marker} ${item.workspaceName}\n root: ${item.rootPath}\n auth: ${item.authMode || ""}`;
143
+ })
144
+ .join("\n\n")}\n`;
145
+ }
146
+
147
+ export function renderBundleMarkdown(bundle) {
148
+ const lines = [];
149
+ lines.push(`# ${bundle.target?.title || bundle.target?.id || "Context bundle"}`);
150
+ lines.push("");
151
+ lines.push(`- mode: ${bundle.mode || ""}`);
152
+ lines.push(`- preset: ${bundle.preset || ""}`);
153
+ if (bundle.summary) {
154
+ lines.push("");
155
+ lines.push(bundle.summary);
156
+ }
157
+ if (Array.isArray(bundle.items) && bundle.items.length > 0) {
158
+ lines.push("");
159
+ lines.push("## Items");
160
+ for (const item of bundle.items) {
161
+ lines.push(`- ${item.title || item.nodeId || item.id}${item.summary ? `: ${item.summary}` : ""}`);
162
+ }
163
+ }
164
+ if (Array.isArray(bundle.sources) && bundle.sources.length > 0) {
165
+ lines.push("");
166
+ lines.push("## Sources");
167
+ for (const source of bundle.sources) {
168
+ lines.push(`- ${source.nodeId || source.id}${source.sourceLabel ? ` (${source.sourceLabel})` : ""}`);
169
+ }
170
+ }
171
+ return `${lines.join("\n")}\n`;
172
+ }
173
+
174
+ export function renderTelemetrySummary(data) {
175
+ const lines = [
176
+ `since: ${data?.since || ""}`,
177
+ `logs: ${data?.logsPath || ""}`,
178
+ `events: ${data?.totalEvents ?? 0}`,
179
+ ];
180
+
181
+ const slow = Array.isArray(data?.slowOperations) ? data.slowOperations : [];
182
+ if (slow.length > 0) {
183
+ lines.push("");
184
+ lines.push("Slow operations:");
185
+ for (const item of slow.slice(0, 5)) {
186
+ lines.push(`- [${item.surface}] ${item.operation} p95=${item.p95DurationMs ?? ""}ms errors=${item.errorCount}/${item.count}`);
187
+ }
188
+ }
189
+
190
+ const mcpFailures = Array.isArray(data?.mcpToolFailures) ? data.mcpToolFailures : [];
191
+ if (mcpFailures.length > 0) {
192
+ lines.push("");
193
+ lines.push("MCP failures:");
194
+ for (const item of mcpFailures.slice(0, 5)) {
195
+ lines.push(`- ${item.operation}: ${item.count}`);
196
+ }
197
+ }
198
+
199
+ if (data?.ftsFallbackRate) {
200
+ lines.push("");
201
+ lines.push(
202
+ `fts fallback: ${data.ftsFallbackRate.fallbackCount}/${data.ftsFallbackRate.sampleCount} (${data.ftsFallbackRate.ratio ?? "n/a"})`
203
+ );
204
+ }
205
+ if (data?.semanticAugmentationRate) {
206
+ lines.push(
207
+ `semantic augmentation: ${data.semanticAugmentationRate.usedCount}/${data.semanticAugmentationRate.sampleCount} (${data.semanticAugmentationRate.ratio ?? "n/a"})`
208
+ );
209
+ }
210
+
211
+ return `${lines.join("\n")}\n`;
212
+ }
213
+
214
+ export function renderTelemetryErrors(data) {
215
+ const items = data?.items || [];
216
+ if (!items.length) {
217
+ return "No telemetry errors.\n";
218
+ }
219
+
220
+ return `${items
221
+ .map(
222
+ (item, index) =>
223
+ `${index + 1}. [${item.surface}] ${item.operation}\n ts: ${item.ts}\n trace: ${item.traceId}\n error: ${item.errorKind || ""}/${item.errorCode || ""}\n status: ${item.statusCode ?? ""}\n durationMs: ${item.durationMs ?? ""}`
224
+ )
225
+ .join("\n\n")}\n`;
226
+ }
227
+
228
+ function renderInline(value) {
229
+ if (value == null) {
230
+ return "";
231
+ }
232
+
233
+ if (Array.isArray(value)) {
234
+ return value.map(renderInline).join(", ");
235
+ }
236
+
237
+ if (typeof value === "object") {
238
+ return JSON.stringify(value);
239
+ }
240
+
241
+ return String(value);
242
+ }
@@ -0,0 +1,35 @@
1
+ import { buildApiRequestInit, buildApiUrl, parseApiJsonBody } from "../../shared/request-runtime.js";
2
+ const DEFAULT_API_BASE = "http://127.0.0.1:8787/api/v1";
3
+ export function getApiBase(argvOptions = {}, env = process.env) {
4
+ return (argvOptions.api ||
5
+ env.RECALLX_API_URL ||
6
+ DEFAULT_API_BASE);
7
+ }
8
+ export function getAuthToken(argvOptions = {}, env = process.env) {
9
+ return argvOptions.token || env.RECALLX_TOKEN || "";
10
+ }
11
+ export async function requestJson(apiBase, path, { method = "GET", token, body } = {}) {
12
+ const response = await fetch(buildApiUrl(apiBase, path), buildApiRequestInit({ method, token, body }));
13
+ let payload = null;
14
+ let parseError = null;
15
+ try {
16
+ payload = await parseApiJsonBody(response);
17
+ }
18
+ catch (error) {
19
+ parseError = error;
20
+ }
21
+ if (!response.ok) {
22
+ const error = payload?.error;
23
+ const code = error?.code || `HTTP_${response.status}`;
24
+ const message = error?.message || response.statusText || "Request failed";
25
+ throw new Error(`${code}: ${message}`);
26
+ }
27
+ if (parseError) {
28
+ throw new Error("INVALID_RESPONSE: RecallX API returned non-JSON output.");
29
+ }
30
+ if (payload && payload.ok === false) {
31
+ const error = payload.error || {};
32
+ throw new Error(`${error.code || "INTERNAL_ERROR"}: ${error.message || "Request failed"}`);
33
+ }
34
+ return payload ?? {};
35
+ }
@@ -0,0 +1,101 @@
1
+ import { buildApiRequestInit, buildApiUrl, parseApiJsonBody } from "../shared/request-runtime.js";
2
+ import { currentTelemetryContext } from "../server/observability.js";
3
+ export class RecallXApiError extends Error {
4
+ status;
5
+ code;
6
+ details;
7
+ constructor(message, options) {
8
+ super(message);
9
+ this.name = "RecallXApiError";
10
+ this.status = options?.status ?? 500;
11
+ this.code = options?.code ?? "RECALLX_API_ERROR";
12
+ this.details = options?.details;
13
+ }
14
+ }
15
+ function isApiErrorEnvelope(payload) {
16
+ return Boolean(payload &&
17
+ typeof payload === "object" &&
18
+ "ok" in payload &&
19
+ payload.ok === false &&
20
+ "error" in payload &&
21
+ payload.error &&
22
+ typeof payload.error === "object");
23
+ }
24
+ export class RecallXApiClient {
25
+ baseUrl;
26
+ apiToken;
27
+ constructor(baseUrl, apiToken) {
28
+ this.baseUrl = baseUrl;
29
+ this.apiToken = apiToken ?? null;
30
+ }
31
+ async get(path) {
32
+ return this.request("GET", path);
33
+ }
34
+ async post(path, body) {
35
+ return this.request("POST", path, body);
36
+ }
37
+ async patch(path, body) {
38
+ return this.request("PATCH", path, body);
39
+ }
40
+ async request(method, path, body) {
41
+ const headers = new Headers();
42
+ const telemetryContext = currentTelemetryContext();
43
+ if (telemetryContext?.traceId) {
44
+ headers.set("x-recallx-trace-id", telemetryContext.traceId);
45
+ }
46
+ if (telemetryContext?.toolName) {
47
+ headers.set("x-recallx-mcp-tool", telemetryContext.toolName);
48
+ }
49
+ let response;
50
+ try {
51
+ response = await fetch(buildApiUrl(this.baseUrl, path), buildApiRequestInit({
52
+ method,
53
+ token: this.apiToken,
54
+ body,
55
+ headers
56
+ }));
57
+ }
58
+ catch (error) {
59
+ throw new RecallXApiError("Failed to reach the local RecallX API.", {
60
+ status: 503,
61
+ code: "NETWORK_ERROR",
62
+ details: error
63
+ });
64
+ }
65
+ let payload;
66
+ try {
67
+ payload = await parseApiJsonBody(response);
68
+ }
69
+ catch (error) {
70
+ throw new RecallXApiError("RecallX API returned non-JSON output.", {
71
+ status: response.status,
72
+ code: "INVALID_RESPONSE",
73
+ details: error
74
+ });
75
+ }
76
+ if (!payload || typeof payload !== "object") {
77
+ throw new RecallXApiError("RecallX API returned an empty response.", {
78
+ status: response.status,
79
+ code: "EMPTY_RESPONSE"
80
+ });
81
+ }
82
+ if ("ok" in payload && payload.ok === true) {
83
+ return payload.data;
84
+ }
85
+ if (isApiErrorEnvelope(payload)) {
86
+ throw new RecallXApiError(payload.error.message, {
87
+ status: response.status,
88
+ code: payload.error.code,
89
+ details: payload.error.details
90
+ });
91
+ }
92
+ if (!response.ok) {
93
+ throw new RecallXApiError(`RecallX API request failed with status ${response.status}.`, {
94
+ status: response.status,
95
+ code: "HTTP_ERROR",
96
+ details: payload
97
+ });
98
+ }
99
+ return payload;
100
+ }
101
+ }
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { RecallXApiClient } from "./api-client.js";
4
+ import { createRecallXMcpServer } from "./server.js";
5
+ import { RECALLX_VERSION } from "../shared/version.js";
6
+ function createApiClient() {
7
+ return new RecallXApiClient(process.env.RECALLX_API_URL ?? "http://127.0.0.1:8787/api/v1", process.env.RECALLX_API_TOKEN);
8
+ }
9
+ function createObservabilityStateReader() {
10
+ let cachedState = null;
11
+ let cachedAt = 0;
12
+ let inFlight = null;
13
+ const cacheTtlMs = 5_000;
14
+ return async function readObservabilityState() {
15
+ const now = Date.now();
16
+ if (cachedState && now - cachedAt < cacheTtlMs) {
17
+ return cachedState;
18
+ }
19
+ if (inFlight) {
20
+ return inFlight;
21
+ }
22
+ inFlight = resolveObservabilityState()
23
+ .then((state) => {
24
+ cachedState = state;
25
+ cachedAt = Date.now();
26
+ return state;
27
+ })
28
+ .finally(() => {
29
+ inFlight = null;
30
+ });
31
+ return inFlight;
32
+ };
33
+ }
34
+ async function resolveObservabilityState() {
35
+ try {
36
+ const client = createApiClient();
37
+ const [workspacePayload, settingsPayload] = await Promise.all([
38
+ client.get("/workspace"),
39
+ client.get("/settings?keys=observability.enabled,observability.retentionDays,observability.slowRequestMs,observability.capturePayloadShape")
40
+ ]);
41
+ const workspace = workspacePayload ?? {};
42
+ const values = (settingsPayload.values ?? {});
43
+ return {
44
+ enabled: values["observability.enabled"] === true,
45
+ workspaceRoot: typeof workspace.rootPath === "string" ? workspace.rootPath : process.cwd(),
46
+ workspaceName: typeof workspace.workspaceName === "string" ? workspace.workspaceName : "RecallX MCP",
47
+ retentionDays: typeof values["observability.retentionDays"] === "number" ? values["observability.retentionDays"] : 14,
48
+ slowRequestMs: typeof values["observability.slowRequestMs"] === "number" ? values["observability.slowRequestMs"] : 250,
49
+ capturePayloadShape: values["observability.capturePayloadShape"] !== false
50
+ };
51
+ }
52
+ catch {
53
+ return {
54
+ enabled: false,
55
+ workspaceRoot: process.cwd(),
56
+ workspaceName: "RecallX MCP",
57
+ retentionDays: 14,
58
+ slowRequestMs: 250,
59
+ capturePayloadShape: true
60
+ };
61
+ }
62
+ }
63
+ function printHelp() {
64
+ console.error(`RecallX MCP server
65
+
66
+ Usage:
67
+ npm run mcp
68
+ npm run dev:mcp
69
+ node dist/server/app/mcp/index.js --api http://127.0.0.1:8787/api/v1
70
+ recallx-mcp --api http://127.0.0.1:8787/api/v1
71
+
72
+ Environment:
73
+ RECALLX_API_URL Local RecallX API base URL (default: http://127.0.0.1:8787/api/v1)
74
+ RECALLX_API_TOKEN Optional bearer token for auth-enabled RecallX instances
75
+ RECALLX_MCP_SOURCE_LABEL Default provenance label for writes (default: RecallX MCP)
76
+ RECALLX_MCP_TOOL_NAME Default provenance tool name (default: recallx-mcp)
77
+ `);
78
+ }
79
+ function parseArgs(argv) {
80
+ const options = {};
81
+ for (let index = 0; index < argv.length; index += 1) {
82
+ const arg = argv[index];
83
+ if (!arg.startsWith("--")) {
84
+ continue;
85
+ }
86
+ const key = arg.slice(2);
87
+ const next = argv[index + 1];
88
+ if (!next || next.startsWith("--")) {
89
+ options[key] = true;
90
+ continue;
91
+ }
92
+ options[key] = next;
93
+ index += 1;
94
+ }
95
+ return options;
96
+ }
97
+ async function main() {
98
+ const args = parseArgs(process.argv.slice(2));
99
+ if (args.help) {
100
+ printHelp();
101
+ return;
102
+ }
103
+ if (typeof args.api === "string") {
104
+ process.env.RECALLX_API_URL = args.api;
105
+ }
106
+ if (typeof args.token === "string") {
107
+ process.env.RECALLX_API_TOKEN = args.token;
108
+ }
109
+ if (typeof args["source-label"] === "string") {
110
+ process.env.RECALLX_MCP_SOURCE_LABEL = args["source-label"];
111
+ }
112
+ if (typeof args["tool-name"] === "string") {
113
+ process.env.RECALLX_MCP_TOOL_NAME = args["tool-name"];
114
+ }
115
+ const readObservabilityState = createObservabilityStateReader();
116
+ const server = createRecallXMcpServer({
117
+ serverVersion: RECALLX_VERSION,
118
+ observabilityState: await readObservabilityState(),
119
+ getObservabilityState: readObservabilityState
120
+ });
121
+ const transport = new StdioServerTransport();
122
+ await server.connect(transport);
123
+ console.error(`RecallX MCP connected over stdio -> ${process.env.RECALLX_API_URL ?? "http://127.0.0.1:8787/api/v1"}`);
124
+ }
125
+ main().catch((error) => {
126
+ console.error("RecallX MCP failed to start:", error);
127
+ process.exit(1);
128
+ });