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.
- package/README.md +205 -0
- package/app/cli/bin/recallx-mcp.js +2 -0
- package/app/cli/bin/recallx.js +8 -0
- package/app/cli/src/cli.js +808 -0
- package/app/cli/src/format.js +242 -0
- package/app/cli/src/http.js +35 -0
- package/app/mcp/api-client.js +101 -0
- package/app/mcp/index.js +128 -0
- package/app/mcp/server.js +786 -0
- package/app/server/app.js +2263 -0
- package/app/server/config.js +27 -0
- package/app/server/db.js +399 -0
- package/app/server/errors.js +17 -0
- package/app/server/governance.js +466 -0
- package/app/server/index.js +26 -0
- package/app/server/inferred-relations.js +247 -0
- package/app/server/observability.js +495 -0
- package/app/server/project-graph.js +199 -0
- package/app/server/relation-scoring.js +59 -0
- package/app/server/repositories.js +2992 -0
- package/app/server/retrieval.js +486 -0
- package/app/server/semantic/chunker.js +85 -0
- package/app/server/semantic/provider.js +124 -0
- package/app/server/semantic/types.js +1 -0
- package/app/server/semantic/vector-store.js +169 -0
- package/app/server/utils.js +43 -0
- package/app/server/workspace-session.js +128 -0
- package/app/server/workspace.js +79 -0
- package/app/shared/contracts.js +268 -0
- package/app/shared/request-runtime.js +30 -0
- package/app/shared/types.js +1 -0
- package/app/shared/version.js +1 -0
- package/dist/renderer/assets/ProjectGraphCanvas-BMvz9DmE.js +312 -0
- package/dist/renderer/assets/index-C2-KXqBO.css +1 -0
- package/dist/renderer/assets/index-CrDu22h7.js +76 -0
- package/dist/renderer/index.html +13 -0
- package/package.json +49 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { getApiBase, getAuthToken, requestJson } from "./http.js";
|
|
7
|
+
import { RECALLX_VERSION } from "../../shared/version.js";
|
|
8
|
+
import { renderActivitySearchResults, renderBundleMarkdown, renderGovernanceIssues, renderJson, renderNode, renderRelated, renderSearchResults, renderTelemetryErrors, renderTelemetrySummary, renderText, renderWorkspaceSearchResults, renderWorkspaces, } from "./format.js";
|
|
9
|
+
const DEFAULT_SOURCE = {
|
|
10
|
+
actorType: "human",
|
|
11
|
+
actorLabel: "recallx-cli",
|
|
12
|
+
toolName: "recallx-cli",
|
|
13
|
+
toolVersion: RECALLX_VERSION,
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_MCP_LAUNCHER_PATH = path.join(os.homedir(), ".recallx", "bin", "recallx-mcp");
|
|
16
|
+
export async function runCli(argv) {
|
|
17
|
+
const { command, args, options, positionals } = parseArgv(argv.slice(2));
|
|
18
|
+
const apiBase = getApiBase(options);
|
|
19
|
+
const token = getAuthToken(options);
|
|
20
|
+
const format = options.format || "text";
|
|
21
|
+
if (!command || command === "help" || options.help) {
|
|
22
|
+
writeStdout(renderHelp());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
switch (command) {
|
|
26
|
+
case "health":
|
|
27
|
+
return runHealth(apiBase, token, format);
|
|
28
|
+
case "serve":
|
|
29
|
+
return runServe(args);
|
|
30
|
+
case "mcp":
|
|
31
|
+
return runMcp(apiBase, token, format, args, positionals);
|
|
32
|
+
case "search":
|
|
33
|
+
return runSearch(apiBase, token, format, args, positionals);
|
|
34
|
+
case "get":
|
|
35
|
+
return runGet(apiBase, token, format, args, positionals);
|
|
36
|
+
case "related":
|
|
37
|
+
case "neighborhood":
|
|
38
|
+
return runRelated(apiBase, token, format, args, positionals);
|
|
39
|
+
case "context":
|
|
40
|
+
return runContext(apiBase, token, format, args, positionals);
|
|
41
|
+
case "create":
|
|
42
|
+
return runCreate(apiBase, token, format, args, positionals);
|
|
43
|
+
case "append":
|
|
44
|
+
return runAppend(apiBase, token, format, args, positionals);
|
|
45
|
+
case "link":
|
|
46
|
+
return runLink(apiBase, token, format, args, positionals);
|
|
47
|
+
case "attach":
|
|
48
|
+
return runAttach(apiBase, token, format, args, positionals);
|
|
49
|
+
case "feedback":
|
|
50
|
+
return runFeedback(apiBase, token, format, args, positionals);
|
|
51
|
+
case "governance":
|
|
52
|
+
return runGovernance(apiBase, token, format, args, positionals);
|
|
53
|
+
case "workspace":
|
|
54
|
+
return runWorkspace(apiBase, token, format, args, positionals);
|
|
55
|
+
case "observability":
|
|
56
|
+
return runObservability(apiBase, token, format, args, positionals);
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unknown command: ${command}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function runHealth(apiBase, token, format) {
|
|
62
|
+
const data = await requestJson(apiBase, "/health", { token });
|
|
63
|
+
outputData(data, format, "health");
|
|
64
|
+
}
|
|
65
|
+
async function runServe(args) {
|
|
66
|
+
if (typeof args.port === "string" && args.port.trim()) {
|
|
67
|
+
process.env.RECALLX_PORT = args.port.trim();
|
|
68
|
+
}
|
|
69
|
+
if (typeof args.bind === "string" && args.bind.trim()) {
|
|
70
|
+
process.env.RECALLX_BIND = args.bind.trim();
|
|
71
|
+
}
|
|
72
|
+
if (typeof args["workspace-root"] === "string" && args["workspace-root"].trim()) {
|
|
73
|
+
const workspaceRoot = path.resolve(args["workspace-root"].trim());
|
|
74
|
+
process.env.RECALLX_WORKSPACE_ROOT = workspaceRoot;
|
|
75
|
+
}
|
|
76
|
+
if (typeof args["workspace-name"] === "string" && args["workspace-name"].trim()) {
|
|
77
|
+
process.env.RECALLX_WORKSPACE_NAME = args["workspace-name"].trim();
|
|
78
|
+
}
|
|
79
|
+
if (typeof args["api-token"] === "string" && args["api-token"].trim()) {
|
|
80
|
+
process.env.RECALLX_API_TOKEN = args["api-token"].trim();
|
|
81
|
+
}
|
|
82
|
+
if (typeof args["renderer-dist"] === "string" && args["renderer-dist"].trim()) {
|
|
83
|
+
const rendererDist = path.resolve(args["renderer-dist"].trim());
|
|
84
|
+
process.env.RECALLX_RENDERER_DIST_PATH = rendererDist;
|
|
85
|
+
}
|
|
86
|
+
await import(pathToFileURL(resolveServerEntryScript()).href);
|
|
87
|
+
}
|
|
88
|
+
async function runMcp(apiBase, token, format, args, positionals) {
|
|
89
|
+
const action = positionals[0] || args.action || "config";
|
|
90
|
+
const launcherPath = args.path || args.launcher || DEFAULT_MCP_LAUNCHER_PATH;
|
|
91
|
+
const commandParts = buildMcpCommandParts(apiBase, token);
|
|
92
|
+
const launcherCommandParts = buildMcpCommandParts(apiBase, null);
|
|
93
|
+
switch (action) {
|
|
94
|
+
case "path": {
|
|
95
|
+
outputData({ path: launcherPath }, format, "mcp-path");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case "command": {
|
|
99
|
+
outputData({ command: commandParts.map(quoteShellArg).join(" ") }, format, "mcp-command");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
case "config": {
|
|
103
|
+
outputData(buildMcpConfigPayload(launcherPath), format, "mcp-config");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "install": {
|
|
107
|
+
await installMcpLauncher(launcherPath, launcherCommandParts);
|
|
108
|
+
outputData({
|
|
109
|
+
path: launcherPath,
|
|
110
|
+
command: launcherCommandParts.map(quoteShellArg).join(" "),
|
|
111
|
+
config: buildMcpConfigPayload(launcherPath),
|
|
112
|
+
notes: token
|
|
113
|
+
? [
|
|
114
|
+
"Use the `recallx` server key in Codex MCP configs and set RECALLX_API_TOKEN in the MCP client environment. The launcher intentionally does not persist bearer tokens.",
|
|
115
|
+
]
|
|
116
|
+
: [
|
|
117
|
+
"Use the `recallx` server key in Codex MCP configs. If the target API uses bearer auth, set RECALLX_API_TOKEN in the MCP client environment before launching MCP.",
|
|
118
|
+
],
|
|
119
|
+
}, format, "mcp-install");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`Unknown mcp action: ${action}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function runSearch(apiBase, token, format, args, positionals) {
|
|
127
|
+
const mode = args.mode || positionals[0];
|
|
128
|
+
if (mode === "activities" || mode === "activity") {
|
|
129
|
+
return runActivitySearch(apiBase, token, format, args, positionals.slice(1));
|
|
130
|
+
}
|
|
131
|
+
if (mode === "workspace" || mode === "all") {
|
|
132
|
+
return runWorkspaceSearch(apiBase, token, format, args, positionals.slice(1));
|
|
133
|
+
}
|
|
134
|
+
const query = args.query || positionals.join(" ");
|
|
135
|
+
const filters = {};
|
|
136
|
+
if (args.type)
|
|
137
|
+
filters.types = splitList(args.type);
|
|
138
|
+
if (args.status)
|
|
139
|
+
filters.status = splitList(args.status);
|
|
140
|
+
if (args["source-label"])
|
|
141
|
+
filters.sourceLabels = splitList(args["source-label"]);
|
|
142
|
+
if (args.tag || args.tags)
|
|
143
|
+
filters.tags = splitList(args.tag || args.tags);
|
|
144
|
+
const payload = {
|
|
145
|
+
query,
|
|
146
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
147
|
+
limit: numberOption(args.limit, 10),
|
|
148
|
+
offset: numberOption(args.offset, 0),
|
|
149
|
+
sort: args.sort || "relevance",
|
|
150
|
+
};
|
|
151
|
+
const data = await requestJson(apiBase, "/nodes/search", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
token,
|
|
154
|
+
body: compactObject(payload),
|
|
155
|
+
});
|
|
156
|
+
outputData(data, format, "search");
|
|
157
|
+
}
|
|
158
|
+
async function runActivitySearch(apiBase, token, format, args, positionals) {
|
|
159
|
+
const filters = {};
|
|
160
|
+
const query = args.query || positionals.join(" ");
|
|
161
|
+
if (args["target-node-id"] || args.targetNodeId) {
|
|
162
|
+
filters.targetNodeIds = splitList(args["target-node-id"] || args.targetNodeId);
|
|
163
|
+
}
|
|
164
|
+
if (args.type || args["activity-type"] || args.activityType) {
|
|
165
|
+
filters.activityTypes = splitList(args.type || args["activity-type"] || args.activityType);
|
|
166
|
+
}
|
|
167
|
+
if (args["source-label"]) {
|
|
168
|
+
filters.sourceLabels = splitList(args["source-label"]);
|
|
169
|
+
}
|
|
170
|
+
if (args["created-after"] || args.createdAfter) {
|
|
171
|
+
filters.createdAfter = args["created-after"] || args.createdAfter;
|
|
172
|
+
}
|
|
173
|
+
if (args["created-before"] || args.createdBefore) {
|
|
174
|
+
filters.createdBefore = args["created-before"] || args.createdBefore;
|
|
175
|
+
}
|
|
176
|
+
const data = await requestJson(apiBase, "/activities/search", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
token,
|
|
179
|
+
body: compactObject({
|
|
180
|
+
query,
|
|
181
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
182
|
+
limit: numberOption(args.limit, 10),
|
|
183
|
+
offset: numberOption(args.offset, 0),
|
|
184
|
+
sort: args.sort || "relevance",
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
outputData(data, format, "search-activities");
|
|
188
|
+
}
|
|
189
|
+
async function runWorkspaceSearch(apiBase, token, format, args, positionals) {
|
|
190
|
+
const nodeFilters = {};
|
|
191
|
+
const activityFilters = {};
|
|
192
|
+
const query = args.query || positionals.join(" ");
|
|
193
|
+
const scopes = args.scopes ? splitList(args.scopes) : ["nodes", "activities"];
|
|
194
|
+
if (args["node-type"])
|
|
195
|
+
nodeFilters.types = splitList(args["node-type"]);
|
|
196
|
+
if (args.status)
|
|
197
|
+
nodeFilters.status = splitList(args.status);
|
|
198
|
+
if (args.tag || args.tags)
|
|
199
|
+
nodeFilters.tags = splitList(args.tag || args.tags);
|
|
200
|
+
if (args["node-source-label"])
|
|
201
|
+
nodeFilters.sourceLabels = splitList(args["node-source-label"]);
|
|
202
|
+
if (args["target-node-id"] || args.targetNodeId) {
|
|
203
|
+
activityFilters.targetNodeIds = splitList(args["target-node-id"] || args.targetNodeId);
|
|
204
|
+
}
|
|
205
|
+
if (args["activity-type"] || args.activityType) {
|
|
206
|
+
activityFilters.activityTypes = splitList(args["activity-type"] || args.activityType);
|
|
207
|
+
}
|
|
208
|
+
if (args["activity-source-label"]) {
|
|
209
|
+
activityFilters.sourceLabels = splitList(args["activity-source-label"]);
|
|
210
|
+
}
|
|
211
|
+
if (args["created-after"] || args.createdAfter) {
|
|
212
|
+
activityFilters.createdAfter = args["created-after"] || args.createdAfter;
|
|
213
|
+
}
|
|
214
|
+
if (args["created-before"] || args.createdBefore) {
|
|
215
|
+
activityFilters.createdBefore = args["created-before"] || args.createdBefore;
|
|
216
|
+
}
|
|
217
|
+
const data = await requestJson(apiBase, "/search", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
token,
|
|
220
|
+
body: compactObject({
|
|
221
|
+
query,
|
|
222
|
+
scopes,
|
|
223
|
+
nodeFilters: Object.keys(nodeFilters).length > 0 ? nodeFilters : undefined,
|
|
224
|
+
activityFilters: Object.keys(activityFilters).length > 0 ? activityFilters : undefined,
|
|
225
|
+
limit: numberOption(args.limit, 10),
|
|
226
|
+
offset: numberOption(args.offset, 0),
|
|
227
|
+
sort: args.sort || "relevance",
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
outputData(data, format, "search-workspace");
|
|
231
|
+
}
|
|
232
|
+
async function runGet(apiBase, token, format, args, positionals) {
|
|
233
|
+
const id = args.id || positionals[0];
|
|
234
|
+
if (!id) {
|
|
235
|
+
throw new Error("get requires a node id");
|
|
236
|
+
}
|
|
237
|
+
const data = await requestJson(apiBase, `/nodes/${encodeURIComponent(id)}`, { token });
|
|
238
|
+
outputData(data, format, "get");
|
|
239
|
+
}
|
|
240
|
+
async function runRelated(apiBase, token, format, args, positionals) {
|
|
241
|
+
const id = args.id || positionals[0];
|
|
242
|
+
if (!id) {
|
|
243
|
+
throw new Error("neighborhood requires a node id");
|
|
244
|
+
}
|
|
245
|
+
const query = new URLSearchParams();
|
|
246
|
+
if (args.depth)
|
|
247
|
+
query.set("depth", String(numberOption(args.depth, 1)));
|
|
248
|
+
if (args.type)
|
|
249
|
+
query.set("types", splitList(args.type).join(","));
|
|
250
|
+
if (args["include-inferred"] !== undefined) {
|
|
251
|
+
query.set("include_inferred", parseBooleanFlag(args["include-inferred"], true) ? "1" : "0");
|
|
252
|
+
}
|
|
253
|
+
if (args["max-inferred"] !== undefined) {
|
|
254
|
+
query.set("max_inferred", String(numberOption(args["max-inferred"], 4)));
|
|
255
|
+
}
|
|
256
|
+
const data = await requestJson(apiBase, `/nodes/${encodeURIComponent(id)}/neighborhood${query.toString() ? `?${query}` : ""}`, {
|
|
257
|
+
token,
|
|
258
|
+
});
|
|
259
|
+
outputData(data, format, "related");
|
|
260
|
+
}
|
|
261
|
+
async function runContext(apiBase, token, format, args, positionals) {
|
|
262
|
+
const targetId = args.id || args.target || positionals[0];
|
|
263
|
+
const payload = {
|
|
264
|
+
mode: args.mode || "compact",
|
|
265
|
+
preset: args.preset || "for-coding",
|
|
266
|
+
options: {
|
|
267
|
+
includeRelated: parseBooleanFlag(args["include-related"], true),
|
|
268
|
+
includeRecentActivities: parseBooleanFlag(args["include-recent-activities"], true),
|
|
269
|
+
includeDecisions: parseBooleanFlag(args["include-decisions"], true),
|
|
270
|
+
includeOpenQuestions: parseBooleanFlag(args["include-open-questions"], true),
|
|
271
|
+
maxItems: numberOption(args["max-items"], 12),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
if (targetId) {
|
|
275
|
+
payload.target = {
|
|
276
|
+
id: targetId,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const data = await requestJson(apiBase, "/context/bundles", {
|
|
280
|
+
method: "POST",
|
|
281
|
+
token,
|
|
282
|
+
body: payload,
|
|
283
|
+
});
|
|
284
|
+
const bundle = data?.data?.bundle || data?.bundle || data;
|
|
285
|
+
outputData(bundle, format, "context");
|
|
286
|
+
}
|
|
287
|
+
async function runCreate(apiBase, token, format, args, positionals) {
|
|
288
|
+
const body = await readBodyInput(args);
|
|
289
|
+
const payload = {
|
|
290
|
+
type: args.type || positionals[0],
|
|
291
|
+
title: args.title || positionals[1],
|
|
292
|
+
body: body || undefined,
|
|
293
|
+
tags: collectTags(args),
|
|
294
|
+
canonicality: args.canonicality,
|
|
295
|
+
status: args.status,
|
|
296
|
+
source: buildSource(args),
|
|
297
|
+
metadata: parseJsonOption(args.metadata),
|
|
298
|
+
};
|
|
299
|
+
validateRequired(payload.type, "create requires --type");
|
|
300
|
+
validateRequired(payload.title, "create requires --title");
|
|
301
|
+
const data = await requestJson(apiBase, "/nodes", {
|
|
302
|
+
method: "POST",
|
|
303
|
+
token,
|
|
304
|
+
body: compactObject(payload),
|
|
305
|
+
});
|
|
306
|
+
outputData(data, format, "create");
|
|
307
|
+
}
|
|
308
|
+
async function runAppend(apiBase, token, format, args, positionals) {
|
|
309
|
+
const targetNodeId = args.target || positionals[0];
|
|
310
|
+
const body = await readBodyInput(args);
|
|
311
|
+
validateRequired(targetNodeId, "append requires --target");
|
|
312
|
+
validateRequired(args.type, "append requires --type");
|
|
313
|
+
const payload = {
|
|
314
|
+
targetNodeId,
|
|
315
|
+
activityType: args.type,
|
|
316
|
+
body,
|
|
317
|
+
source: buildSource(args),
|
|
318
|
+
metadata: parseJsonOption(args.metadata),
|
|
319
|
+
};
|
|
320
|
+
const data = await requestJson(apiBase, "/activities", {
|
|
321
|
+
method: "POST",
|
|
322
|
+
token,
|
|
323
|
+
body: compactObject(payload),
|
|
324
|
+
});
|
|
325
|
+
outputData(data, format, "append");
|
|
326
|
+
}
|
|
327
|
+
async function runLink(apiBase, token, format, args, positionals) {
|
|
328
|
+
const fromNodeId = args.from || positionals[0];
|
|
329
|
+
const toNodeId = args.to || positionals[1];
|
|
330
|
+
const relationType = args["relation-type"] || args.type || positionals[2];
|
|
331
|
+
validateRequired(fromNodeId, "link requires a from node id");
|
|
332
|
+
validateRequired(toNodeId, "link requires a to node id");
|
|
333
|
+
validateRequired(relationType, "link requires a relation type");
|
|
334
|
+
const payload = {
|
|
335
|
+
fromNodeId,
|
|
336
|
+
toNodeId,
|
|
337
|
+
relationType,
|
|
338
|
+
status: args.status || "suggested",
|
|
339
|
+
source: buildSource(args),
|
|
340
|
+
metadata: parseJsonOption(args.metadata),
|
|
341
|
+
};
|
|
342
|
+
const data = await requestJson(apiBase, "/relations", {
|
|
343
|
+
method: "POST",
|
|
344
|
+
token,
|
|
345
|
+
body: compactObject(payload),
|
|
346
|
+
});
|
|
347
|
+
outputData(data, format, "link");
|
|
348
|
+
}
|
|
349
|
+
async function runAttach(apiBase, token, format, args, positionals) {
|
|
350
|
+
const nodeId = args.node || positionals[0];
|
|
351
|
+
const path = args.path || positionals[1];
|
|
352
|
+
validateRequired(nodeId, "attach requires --node");
|
|
353
|
+
validateRequired(path, "attach requires --path");
|
|
354
|
+
const payload = {
|
|
355
|
+
nodeId,
|
|
356
|
+
path,
|
|
357
|
+
mimeType: args["mime-type"] || args.mimeType,
|
|
358
|
+
checksum: args.checksum,
|
|
359
|
+
source: buildSource(args),
|
|
360
|
+
metadata: parseJsonOption(args.metadata),
|
|
361
|
+
};
|
|
362
|
+
const data = await requestJson(apiBase, "/artifacts", {
|
|
363
|
+
method: "POST",
|
|
364
|
+
token,
|
|
365
|
+
body: compactObject(payload),
|
|
366
|
+
});
|
|
367
|
+
outputData(data, format, "attach");
|
|
368
|
+
}
|
|
369
|
+
async function runFeedback(apiBase, token, format, args, positionals) {
|
|
370
|
+
const resultType = args["result-type"] || args.resultType || positionals[0];
|
|
371
|
+
const resultId = args["result-id"] || args.resultId || positionals[1];
|
|
372
|
+
const verdict = args.verdict || positionals[2];
|
|
373
|
+
validateRequired(resultType, "feedback requires --result-type");
|
|
374
|
+
validateRequired(resultId, "feedback requires --result-id");
|
|
375
|
+
validateRequired(verdict, "feedback requires --verdict");
|
|
376
|
+
return runPostCommand(apiBase, token, format, "/search-feedback-events", "feedback", {
|
|
377
|
+
resultType,
|
|
378
|
+
resultId,
|
|
379
|
+
verdict,
|
|
380
|
+
query: args.query,
|
|
381
|
+
sessionId: args["session-id"] || args.sessionId,
|
|
382
|
+
runId: args["run-id"] || args.runId,
|
|
383
|
+
confidence: numberOption(args.confidence, 1),
|
|
384
|
+
source: buildSource(args),
|
|
385
|
+
metadata: parseJsonOption(args.metadata),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async function runGovernance(apiBase, token, format, args, positionals) {
|
|
389
|
+
const action = positionals[0] || args.action || "list";
|
|
390
|
+
switch (action) {
|
|
391
|
+
case "list":
|
|
392
|
+
case "issues": {
|
|
393
|
+
const query = new URLSearchParams();
|
|
394
|
+
if (args.states)
|
|
395
|
+
query.set("states", splitList(args.states).join(","));
|
|
396
|
+
if (args.limit)
|
|
397
|
+
query.set("limit", String(numberOption(args.limit, 20)));
|
|
398
|
+
const data = await requestJson(apiBase, `/governance/issues${query.toString() ? `?${query}` : ""}`, {
|
|
399
|
+
token,
|
|
400
|
+
});
|
|
401
|
+
outputData(data, format, "governance-issues");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
case "show": {
|
|
405
|
+
const entityType = args["entity-type"] || args.entityType || positionals[1];
|
|
406
|
+
const entityId = args["entity-id"] || args.entityId || positionals[2];
|
|
407
|
+
validateRequired(entityType, "governance show requires --entity-type");
|
|
408
|
+
validateRequired(entityId, "governance show requires --entity-id");
|
|
409
|
+
const data = await requestJson(apiBase, `/governance/state/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`, {
|
|
410
|
+
token,
|
|
411
|
+
});
|
|
412
|
+
outputData(data, format, "governance-show");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
case "recompute": {
|
|
416
|
+
const data = await requestJson(apiBase, "/governance/recompute", {
|
|
417
|
+
method: "POST",
|
|
418
|
+
token,
|
|
419
|
+
body: compactObject({
|
|
420
|
+
entityType: args["entity-type"] || args.entityType,
|
|
421
|
+
entityIds: args["entity-ids"] || args.entityIds ? splitList(args["entity-ids"] || args.entityIds) : undefined,
|
|
422
|
+
limit: numberOption(args.limit, 100),
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
outputData(data, format, "governance-recompute");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
throw new Error(`Unknown governance action: ${action}`);
|
|
430
|
+
}
|
|
431
|
+
async function runWorkspace(apiBase, token, format, args, positionals) {
|
|
432
|
+
const action = positionals[0] || args.action || "current";
|
|
433
|
+
switch (action) {
|
|
434
|
+
case "current": {
|
|
435
|
+
const data = await requestJson(apiBase, "/workspace", { token });
|
|
436
|
+
outputData(data, format, "workspace-current");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
case "list": {
|
|
440
|
+
const data = await requestJson(apiBase, "/workspaces", { token });
|
|
441
|
+
outputData(data, format, "workspace-list");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
case "create":
|
|
445
|
+
return runWorkspaceMutation(apiBase, token, format, {
|
|
446
|
+
action: "create",
|
|
447
|
+
rootPath: args.root || args.path || positionals[1],
|
|
448
|
+
workspaceName: args.name || args.title,
|
|
449
|
+
});
|
|
450
|
+
case "open":
|
|
451
|
+
case "switch":
|
|
452
|
+
return runWorkspaceMutation(apiBase, token, format, {
|
|
453
|
+
action,
|
|
454
|
+
rootPath: args.root || args.path || positionals[1],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
throw new Error(`Unknown workspace action: ${action}`);
|
|
458
|
+
}
|
|
459
|
+
async function runObservability(apiBase, token, format, args, positionals) {
|
|
460
|
+
const action = positionals[0] || args.action || "summary";
|
|
461
|
+
switch (action) {
|
|
462
|
+
case "summary": {
|
|
463
|
+
const query = new URLSearchParams();
|
|
464
|
+
query.set("since", args.since || "24h");
|
|
465
|
+
const data = await requestJson(apiBase, `/observability/summary?${query.toString()}`, { token });
|
|
466
|
+
outputData(data, format, "observability-summary");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
case "errors": {
|
|
470
|
+
const query = new URLSearchParams();
|
|
471
|
+
query.set("since", args.since || "24h");
|
|
472
|
+
if (args.surface)
|
|
473
|
+
query.set("surface", args.surface);
|
|
474
|
+
if (args.limit)
|
|
475
|
+
query.set("limit", String(numberOption(args.limit, 50)));
|
|
476
|
+
const data = await requestJson(apiBase, `/observability/errors?${query.toString()}`, { token });
|
|
477
|
+
outputData(data, format, "observability-errors");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`Unknown observability action: ${action}`);
|
|
482
|
+
}
|
|
483
|
+
function buildSource(args) {
|
|
484
|
+
return {
|
|
485
|
+
actorType: args["actor-type"] || args.actorType || DEFAULT_SOURCE.actorType,
|
|
486
|
+
actorLabel: args["actor-label"] || args.actorLabel || DEFAULT_SOURCE.actorLabel,
|
|
487
|
+
toolName: args["tool-name"] || args.toolName || DEFAULT_SOURCE.toolName,
|
|
488
|
+
toolVersion: args["tool-version"] || args.toolVersion || DEFAULT_SOURCE.toolVersion,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async function runWorkspaceMutation(apiBase, token, format, { action, rootPath, workspaceName }) {
|
|
492
|
+
validateRequired(rootPath, `workspace ${action} requires --root`);
|
|
493
|
+
return runPostCommand(apiBase, token, format, action === "create" ? "/workspaces" : "/workspaces/open", action === "create" ? "workspace-create" : "workspace-open", action === "create"
|
|
494
|
+
? {
|
|
495
|
+
rootPath,
|
|
496
|
+
workspaceName,
|
|
497
|
+
}
|
|
498
|
+
: {
|
|
499
|
+
rootPath,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function runPostCommand(apiBase, token, format, path, command, body) {
|
|
503
|
+
const data = await requestJson(apiBase, path, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
token,
|
|
506
|
+
body: compactObject(body),
|
|
507
|
+
});
|
|
508
|
+
outputData(data, format, command);
|
|
509
|
+
}
|
|
510
|
+
function outputData(data, format, command) {
|
|
511
|
+
const payload = data?.data ?? data;
|
|
512
|
+
if (format === "json") {
|
|
513
|
+
writeStdout(renderJson(data));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
switch (command) {
|
|
517
|
+
case "search":
|
|
518
|
+
writeStdout(renderSearchResults(payload));
|
|
519
|
+
return;
|
|
520
|
+
case "search-activities":
|
|
521
|
+
writeStdout(renderActivitySearchResults(payload));
|
|
522
|
+
return;
|
|
523
|
+
case "search-workspace":
|
|
524
|
+
writeStdout(renderWorkspaceSearchResults(payload));
|
|
525
|
+
return;
|
|
526
|
+
case "get":
|
|
527
|
+
writeStdout(renderNode(payload.node || payload));
|
|
528
|
+
return;
|
|
529
|
+
case "related":
|
|
530
|
+
writeStdout(renderRelated(payload));
|
|
531
|
+
return;
|
|
532
|
+
case "governance-issues":
|
|
533
|
+
writeStdout(renderGovernanceIssues(payload));
|
|
534
|
+
return;
|
|
535
|
+
case "workspace-list":
|
|
536
|
+
writeStdout(renderWorkspaces(payload));
|
|
537
|
+
return;
|
|
538
|
+
case "append":
|
|
539
|
+
case "create":
|
|
540
|
+
case "link":
|
|
541
|
+
case "attach":
|
|
542
|
+
case "feedback":
|
|
543
|
+
case "governance-show":
|
|
544
|
+
case "governance-recompute":
|
|
545
|
+
case "workspace-current":
|
|
546
|
+
case "workspace-create":
|
|
547
|
+
case "workspace-open":
|
|
548
|
+
writeStdout(renderText(payload));
|
|
549
|
+
return;
|
|
550
|
+
case "observability-summary":
|
|
551
|
+
writeStdout(renderTelemetrySummary(payload));
|
|
552
|
+
return;
|
|
553
|
+
case "observability-errors":
|
|
554
|
+
writeStdout(renderTelemetryErrors(payload));
|
|
555
|
+
return;
|
|
556
|
+
case "context":
|
|
557
|
+
writeStdout(renderBundleMarkdown(payload));
|
|
558
|
+
return;
|
|
559
|
+
case "health":
|
|
560
|
+
writeStdout(renderText(payload));
|
|
561
|
+
return;
|
|
562
|
+
case "mcp-path":
|
|
563
|
+
writeStdout(`${payload.path}\n`);
|
|
564
|
+
return;
|
|
565
|
+
case "mcp-command":
|
|
566
|
+
writeStdout(`${payload.command}\n`);
|
|
567
|
+
return;
|
|
568
|
+
case "mcp-config":
|
|
569
|
+
if (format === "markdown") {
|
|
570
|
+
writeStdout(`\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
writeStdout(`${JSON.stringify(payload, null, 2)}\n`);
|
|
574
|
+
return;
|
|
575
|
+
case "mcp-install":
|
|
576
|
+
writeStdout([
|
|
577
|
+
`Installed launcher: ${payload.path}`,
|
|
578
|
+
`Direct command: ${payload.command}`,
|
|
579
|
+
...(Array.isArray(payload.notes) ? payload.notes : []),
|
|
580
|
+
"",
|
|
581
|
+
JSON.stringify(payload.config, null, 2),
|
|
582
|
+
"",
|
|
583
|
+
].join("\n"));
|
|
584
|
+
return;
|
|
585
|
+
default:
|
|
586
|
+
writeStdout(renderText(payload));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function readBodyInput(args) {
|
|
590
|
+
if (args.file) {
|
|
591
|
+
return readFile(args.file, "utf8");
|
|
592
|
+
}
|
|
593
|
+
const value = args.body ?? args.text;
|
|
594
|
+
if (!value) {
|
|
595
|
+
return "";
|
|
596
|
+
}
|
|
597
|
+
if (value === "-") {
|
|
598
|
+
return readAllStdin();
|
|
599
|
+
}
|
|
600
|
+
if (typeof value === "string" && value.startsWith("@")) {
|
|
601
|
+
return readFile(value.slice(1), "utf8");
|
|
602
|
+
}
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
function readAllStdin() {
|
|
606
|
+
return new Promise((resolve, reject) => {
|
|
607
|
+
let data = "";
|
|
608
|
+
process.stdin.setEncoding("utf8");
|
|
609
|
+
process.stdin.on("data", (chunk) => {
|
|
610
|
+
data += chunk;
|
|
611
|
+
});
|
|
612
|
+
process.stdin.on("end", () => resolve(data));
|
|
613
|
+
process.stdin.on("error", reject);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function parseArgv(argv) {
|
|
617
|
+
const options = {};
|
|
618
|
+
const positionals = [];
|
|
619
|
+
const args = {};
|
|
620
|
+
let command = "";
|
|
621
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
622
|
+
const value = argv[index];
|
|
623
|
+
if (value === "--") {
|
|
624
|
+
positionals.push(...argv.slice(index + 1));
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
if (value.startsWith("--")) {
|
|
628
|
+
const [keyPart, inlineValue] = value.slice(2).split("=", 2);
|
|
629
|
+
const key = toOptionKey(keyPart);
|
|
630
|
+
const next = argv[index + 1];
|
|
631
|
+
const optionValue = inlineValue !== undefined
|
|
632
|
+
? inlineValue
|
|
633
|
+
: next && !next.startsWith("-")
|
|
634
|
+
? (index += 1, next)
|
|
635
|
+
: true;
|
|
636
|
+
options[key] = optionValue;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (!command) {
|
|
640
|
+
command = value;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
positionals.push(value);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
command,
|
|
647
|
+
args: options,
|
|
648
|
+
options,
|
|
649
|
+
positionals,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function renderHelp() {
|
|
653
|
+
return `RecallX CLI
|
|
654
|
+
|
|
655
|
+
Usage:
|
|
656
|
+
recallx serve [--port 8787] [--bind 127.0.0.1] [--workspace-root /path/to/workspace]
|
|
657
|
+
recallx serve [--workspace-name Personal] [--api-token secret]
|
|
658
|
+
recallx serve [--renderer-dist /path/to/dist/renderer]
|
|
659
|
+
recallx health
|
|
660
|
+
recallx mcp config
|
|
661
|
+
recallx mcp install [--path ~/.recallx/bin/recallx-mcp]
|
|
662
|
+
recallx mcp path
|
|
663
|
+
recallx mcp command
|
|
664
|
+
recallx search "agent memory" [--type project] [--limit 5]
|
|
665
|
+
recallx search activities "what changed" [--activity-type agent_run_summary]
|
|
666
|
+
recallx search workspace "cleanup" [--scopes nodes,activities]
|
|
667
|
+
recallx get <node-id>
|
|
668
|
+
recallx neighborhood <node-id> [--depth 1] [--include-inferred true] [--max-inferred 4]
|
|
669
|
+
recallx related <node-id> [--depth 1]
|
|
670
|
+
recallx context <target-id> [--mode compact] [--preset for-coding]
|
|
671
|
+
recallx create --type note --title "..." [--body "..." | --file path.md]
|
|
672
|
+
recallx append --target <node-id> --type agent_run_summary --text "..."
|
|
673
|
+
recallx link <from-id> <to-id> <relation-type>
|
|
674
|
+
recallx attach --node <node-id> --path artifacts/file.md
|
|
675
|
+
recallx feedback --result-type node --result-id <id> --verdict useful [--query "..."]
|
|
676
|
+
recallx governance issues [--states contested,low_confidence]
|
|
677
|
+
recallx governance show --entity-type node --entity-id <id>
|
|
678
|
+
recallx governance recompute [--entity-type node] [--entity-ids id1,id2]
|
|
679
|
+
recallx workspace current
|
|
680
|
+
recallx workspace list
|
|
681
|
+
recallx workspace create --root /path/to/workspace [--name "Personal"]
|
|
682
|
+
recallx workspace open --root /path/to/workspace
|
|
683
|
+
recallx observability summary [--since 24h]
|
|
684
|
+
recallx observability errors [--since 24h] [--surface mcp] [--limit 50]
|
|
685
|
+
|
|
686
|
+
Global flags:
|
|
687
|
+
--api <url> Override API base URL
|
|
688
|
+
--token <token> Override bearer token
|
|
689
|
+
--format <text|json|markdown>
|
|
690
|
+
`;
|
|
691
|
+
}
|
|
692
|
+
function resolveMcpEntryScript() {
|
|
693
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../bin/recallx-mcp.js");
|
|
694
|
+
}
|
|
695
|
+
function resolveServerEntryScript() {
|
|
696
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
697
|
+
const packagedEntry = path.resolve(moduleDir, "../../server/index.js");
|
|
698
|
+
if (existsSync(packagedEntry)) {
|
|
699
|
+
return packagedEntry;
|
|
700
|
+
}
|
|
701
|
+
const builtEntry = path.resolve(moduleDir, "../../../dist/server/app/server/index.js");
|
|
702
|
+
if (existsSync(builtEntry)) {
|
|
703
|
+
return builtEntry;
|
|
704
|
+
}
|
|
705
|
+
throw new Error("Unable to locate the RecallX server entrypoint. Build the project or use an installed npm package.");
|
|
706
|
+
}
|
|
707
|
+
function buildMcpCommandParts(apiBase, token) {
|
|
708
|
+
const parts = [process.execPath, resolveMcpEntryScript(), "--api", apiBase];
|
|
709
|
+
if (token) {
|
|
710
|
+
parts.push("--token", token);
|
|
711
|
+
}
|
|
712
|
+
return parts;
|
|
713
|
+
}
|
|
714
|
+
function buildMcpConfigPayload(launcherPath) {
|
|
715
|
+
return {
|
|
716
|
+
mcpServers: {
|
|
717
|
+
recallx: {
|
|
718
|
+
command: launcherPath,
|
|
719
|
+
args: [],
|
|
720
|
+
env: {
|
|
721
|
+
RECALLX_API_TOKEN: "<set at runtime for bearer-mode services>",
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async function installMcpLauncher(launcherPath, commandParts) {
|
|
728
|
+
await mkdir(path.dirname(launcherPath), { recursive: true });
|
|
729
|
+
await writeFile(launcherPath, `#!/bin/sh\nexec ${commandParts.map(quoteShellArg).join(" ")} "$@"\n`, "utf8");
|
|
730
|
+
await chmod(launcherPath, 0o755);
|
|
731
|
+
}
|
|
732
|
+
function quoteShellArg(value) {
|
|
733
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
734
|
+
}
|
|
735
|
+
function splitList(value) {
|
|
736
|
+
if (Array.isArray(value)) {
|
|
737
|
+
return value.flatMap(splitList);
|
|
738
|
+
}
|
|
739
|
+
return String(value)
|
|
740
|
+
.split(",")
|
|
741
|
+
.map((entry) => entry.trim())
|
|
742
|
+
.filter(Boolean);
|
|
743
|
+
}
|
|
744
|
+
function compactObject(value) {
|
|
745
|
+
if (!value || typeof value !== "object") {
|
|
746
|
+
return value;
|
|
747
|
+
}
|
|
748
|
+
if (Array.isArray(value)) {
|
|
749
|
+
return value.map(compactObject).filter((entry) => entry !== undefined);
|
|
750
|
+
}
|
|
751
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null && entry !== ""));
|
|
752
|
+
}
|
|
753
|
+
function collectTags(args) {
|
|
754
|
+
const tags = [];
|
|
755
|
+
if (args.tag)
|
|
756
|
+
tags.push(...splitList(args.tag));
|
|
757
|
+
if (args.tags)
|
|
758
|
+
tags.push(...splitList(args.tags));
|
|
759
|
+
return tags.length > 0 ? tags : undefined;
|
|
760
|
+
}
|
|
761
|
+
function parseJsonOption(value) {
|
|
762
|
+
if (!value) {
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
if (typeof value !== "string") {
|
|
766
|
+
return value;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
return JSON.parse(value);
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
return value;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function validateRequired(value, message) {
|
|
776
|
+
if (!value) {
|
|
777
|
+
throw new Error(message);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function numberOption(value, fallback) {
|
|
781
|
+
if (value === undefined || value === null || value === "") {
|
|
782
|
+
return fallback;
|
|
783
|
+
}
|
|
784
|
+
const parsed = Number(value);
|
|
785
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
786
|
+
}
|
|
787
|
+
function parseBooleanFlag(value, fallback = false) {
|
|
788
|
+
if (value === undefined) {
|
|
789
|
+
return fallback;
|
|
790
|
+
}
|
|
791
|
+
if (typeof value === "boolean") {
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
const normalized = String(value).toLowerCase();
|
|
795
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
796
|
+
return true;
|
|
797
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
798
|
+
return false;
|
|
799
|
+
return fallback;
|
|
800
|
+
}
|
|
801
|
+
function toOptionKey(value) {
|
|
802
|
+
return value
|
|
803
|
+
.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
804
|
+
.replace(/^([A-Z])/, (match) => match.toLowerCase());
|
|
805
|
+
}
|
|
806
|
+
function writeStdout(value) {
|
|
807
|
+
process.stdout.write(value);
|
|
808
|
+
}
|