recallx 1.0.2 → 1.0.4

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 CHANGED
@@ -35,13 +35,14 @@ RecallX is documented around three public ways to use it:
35
35
  2. npm package `recallx` for the full local runtime
36
36
  3. npm package `recallx-headless` for the headless runtime
37
37
 
38
+ RecallX does not currently ship separate OS-native installers or package formats such as `.dmg`, `.msi`, `.deb`, or `AppImage`.
39
+
38
40
  ## 1. Git Public Repo
39
41
 
40
42
  Use the public repo when you want the full source-run surface:
41
43
 
42
44
  - local API under `/api/v1`
43
45
  - source-run renderer workflow through `npm run dev`
44
- - source-run desktop workflow through `npm run dev:desktop`
45
46
  - stdio MCP bridge through `npm run mcp`
46
47
  - runtime workspace create/open switching without restarting the service
47
48
 
@@ -52,12 +53,6 @@ npm install
52
53
  npm run dev
53
54
  ```
54
55
 
55
- Desktop runtime from source:
56
-
57
- ```bash
58
- npm run dev:desktop
59
- ```
60
-
61
56
  MCP from source:
62
57
 
63
58
  ```bash
@@ -94,6 +89,7 @@ In another shell:
94
89
 
95
90
  ```bash
96
91
  recallx health
92
+ recallx update
97
93
  recallx mcp install
98
94
  recallx-mcp --help
99
95
  ```
@@ -106,10 +102,6 @@ The full npm package includes:
106
102
  - `recallx serve` and subcommands
107
103
  - `recallx-mcp`
108
104
 
109
- The full npm package does not include:
110
-
111
- - desktop release artifacts
112
-
113
105
  `recallx mcp install` writes a stable launcher to `~/.recallx/bin/recallx-mcp`, which is the recommended command path for Codex and other editor MCP configs.
114
106
 
115
107
  If the API is running in bearer mode, set `RECALLX_API_TOKEN` in the MCP client environment. The launcher does not write tokens to disk.
@@ -128,6 +120,15 @@ recallx serve --workspace-root /Users/name/Documents/RecallX
128
120
  recallx serve --api-token secret-token
129
121
  ```
130
122
 
123
+ To update an npm-installed full runtime:
124
+
125
+ ```bash
126
+ recallx update
127
+ recallx update --apply
128
+ ```
129
+
130
+ `recallx update` currently supports npm global installs of `recallx` and `recallx-headless`. Source checkouts should keep using their package manager directly.
131
+
131
132
  ## 3. npm Headless Runtime (`recallx-headless`)
132
133
 
133
134
  Use the headless npm package when you want the local API, CLI, and MCP entrypoint without shipping the renderer bundle:
@@ -141,6 +142,7 @@ In another shell:
141
142
 
142
143
  ```bash
143
144
  recallx health
145
+ recallx update
144
146
  recallx-mcp --help
145
147
  ```
146
148
 
@@ -154,10 +156,16 @@ The headless npm package includes:
154
156
  The headless npm package does not include:
155
157
 
156
158
  - renderer pages
157
- - desktop release artifacts
158
159
 
159
160
  At `/`, the headless runtime returns a small runtime notice instead of the renderer.
160
161
 
162
+ To update an npm-installed headless runtime:
163
+
164
+ ```bash
165
+ recallx update
166
+ recallx update --apply
167
+ ```
168
+
161
169
  Node requirements:
162
170
 
163
171
  - npm packages: Node 22.13+
@@ -5,7 +5,8 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { getApiBase, getAuthToken, requestJson } from "./http.js";
7
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";
8
+ import { applyCliUpdate, getCliUpdatePlan } from "./update.js";
9
+ import { renderActivitySearchResults, renderBundleMarkdown, renderGovernanceIssues, renderJson, renderNode, renderRelated, renderSearchResults, renderTelemetryErrors, renderTelemetrySummary, renderText, renderUpdateResult, renderWorkspaceSearchResults, renderWorkspaces, } from "./format.js";
9
10
  const DEFAULT_SOURCE = {
10
11
  actorType: "human",
11
12
  actorLabel: "recallx-cli",
@@ -27,6 +28,8 @@ export async function runCli(argv) {
27
28
  return runHealth(apiBase, token, format);
28
29
  case "serve":
29
30
  return runServe(args);
31
+ case "update":
32
+ return runUpdate(format, args);
30
33
  case "mcp":
31
34
  return runMcp(apiBase, token, format, args, positionals);
32
35
  case "search":
@@ -85,6 +88,15 @@ async function runServe(args) {
85
88
  }
86
89
  await import(pathToFileURL(resolveServerEntryScript()).href);
87
90
  }
91
+ export async function runUpdate(format, args, dependencies = {}) {
92
+ const plan = await (dependencies.getCliUpdatePlan ?? getCliUpdatePlan)();
93
+ if (parseBooleanFlag(args.apply, false)) {
94
+ const result = await (dependencies.applyCliUpdate ?? applyCliUpdate)(plan);
95
+ outputData(result, format, "update");
96
+ return;
97
+ }
98
+ outputData(plan, format, "update");
99
+ }
88
100
  async function runMcp(apiBase, token, format, args, positionals) {
89
101
  const action = positionals[0] || args.action || "config";
90
102
  const launcherPath = args.path || args.launcher || DEFAULT_MCP_LAUNCHER_PATH;
@@ -582,6 +594,9 @@ function outputData(data, format, command) {
582
594
  "",
583
595
  ].join("\n"));
584
596
  return;
597
+ case "update":
598
+ writeStdout(renderUpdateResult(payload));
599
+ return;
585
600
  default:
586
601
  writeStdout(renderText(payload));
587
602
  }
@@ -657,6 +672,7 @@ Usage:
657
672
  recallx serve [--workspace-name Personal] [--api-token secret]
658
673
  recallx serve [--renderer-dist /path/to/dist/renderer]
659
674
  recallx health
675
+ recallx update [--apply]
660
676
  recallx mcp config
661
677
  recallx mcp install [--path ~/.recallx/bin/recallx-mcp]
662
678
  recallx mcp path
@@ -144,6 +144,36 @@ export function renderWorkspaces(data) {
144
144
  .join("\n\n")}\n`;
145
145
  }
146
146
 
147
+ export function renderUpdateResult(data) {
148
+ const lines = [
149
+ `package: ${data?.packageName || ""}`,
150
+ `current: ${data?.currentVersion || ""}`,
151
+ `latest: ${data?.latestVersion || ""}`,
152
+ ];
153
+
154
+ if (data?.status === "updated") {
155
+ lines.push("status: updated");
156
+ } else if (data?.status === "up_to_date") {
157
+ lines.push("status: up to date");
158
+ } else {
159
+ lines.push("status: update available");
160
+ }
161
+
162
+ if (data?.installCommand) {
163
+ lines.push(`command: ${data.installCommand}`);
164
+ }
165
+
166
+ if (data?.status === "update_available" && data?.applied !== true) {
167
+ lines.push("hint: re-run with `recallx update --apply` to install the latest npm package from this shell.");
168
+ }
169
+
170
+ if (data?.packageRoot) {
171
+ lines.push(`packageRoot: ${data.packageRoot}`);
172
+ }
173
+
174
+ return `${lines.join("\n")}\n`;
175
+ }
176
+
147
177
  export function renderBundleMarkdown(bundle) {
148
178
  const lines = [];
149
179
  lines.push(`# ${bundle.target?.title || bundle.target?.id || "Context bundle"}`);
@@ -0,0 +1,118 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const SUPPORTED_NPM_PACKAGES = new Set(["recallx", "recallx-headless"]);
8
+ export async function getCliUpdatePlan(options = {}) {
9
+ const moduleUrl = options.moduleUrl ?? import.meta.url;
10
+ const packageRoot = options.packageRoot ?? resolveCliPackageRoot(moduleUrl);
11
+ const packageJson = readPackageJson(packageRoot, options.readFileSyncFn ?? readFileSync);
12
+ const packageName = options.packageName ?? packageJson.name;
13
+ const currentVersion = options.currentVersion ?? packageJson.version;
14
+ const platform = options.platform ?? process.platform;
15
+ const npmCommand = options.npmCommand ?? resolveNpmCommand(platform);
16
+ const execAsync = options.execFileAsyncFn ?? execFileAsync;
17
+ if (!SUPPORTED_NPM_PACKAGES.has(packageName)) {
18
+ throw new Error("UPDATE_UNSUPPORTED: `recallx update` currently supports npm-installed `recallx` and `recallx-headless` runtimes only.");
19
+ }
20
+ const globalRoot = (await runCommand(execAsync, npmCommand, ["root", "-g"])).trim();
21
+ if (!globalRoot || !isPathInside(packageRoot, globalRoot)) {
22
+ throw new Error("UPDATE_UNSUPPORTED: `recallx update` only works for npm global installs. For source checkouts or other install methods, update the package with your package manager directly.");
23
+ }
24
+ const latestVersionRaw = await runCommand(execAsync, npmCommand, ["view", packageName, "version", "--json"]);
25
+ const latestVersion = normalizeVersionPayload(latestVersionRaw);
26
+ const installArgs = ["install", "-g", `${packageName}@latest`];
27
+ return {
28
+ packageName,
29
+ currentVersion,
30
+ latestVersion,
31
+ packageRoot,
32
+ globalRoot,
33
+ npmCommand,
34
+ installArgs,
35
+ installCommand: [npmCommand, ...installArgs].map(quoteShellArg).join(" "),
36
+ status: currentVersion === latestVersion ? "up_to_date" : "update_available",
37
+ applied: false,
38
+ };
39
+ }
40
+ export function applyCliUpdate(plan, options = {}) {
41
+ if (!plan || typeof plan !== "object") {
42
+ throw new Error("UPDATE_INVALID_PLAN: Missing update plan.");
43
+ }
44
+ if (plan.status === "up_to_date") {
45
+ return {
46
+ ...plan,
47
+ applied: false,
48
+ };
49
+ }
50
+ const platform = options.platform ?? process.platform;
51
+ const npmCommand = options.npmCommand ?? plan.npmCommand ?? resolveNpmCommand(platform);
52
+ const execSync = options.execFileSyncFn ?? execFileSync;
53
+ execSync(npmCommand, plan.installArgs ?? ["install", "-g", `${plan.packageName}@latest`], {
54
+ stdio: "inherit",
55
+ });
56
+ return {
57
+ ...plan,
58
+ status: "updated",
59
+ applied: true,
60
+ };
61
+ }
62
+ function resolveCliPackageRoot(moduleUrl) {
63
+ const modulePath = fileURLToPath(moduleUrl);
64
+ return path.resolve(path.dirname(modulePath), "../../..");
65
+ }
66
+ function readPackageJson(packageRoot, readFileSyncFn) {
67
+ const packageJsonPath = path.join(packageRoot, "package.json");
68
+ if (!existsSync(packageJsonPath)) {
69
+ throw new Error(`UPDATE_UNSUPPORTED: Could not find package metadata at ${packageJsonPath}.`);
70
+ }
71
+ return JSON.parse(readFileSyncFn(packageJsonPath, "utf8"));
72
+ }
73
+ async function runCommand(execAsync, command, args) {
74
+ try {
75
+ const result = await execAsync(command, args, { encoding: "utf8" });
76
+ return typeof result.stdout === "string" ? result.stdout.trim() : "";
77
+ }
78
+ catch (error) {
79
+ const stderr = error && typeof error === "object" && "stderr" in error && typeof error.stderr === "string"
80
+ ? error.stderr.trim()
81
+ : "";
82
+ const detail = stderr || (error instanceof Error ? error.message : String(error));
83
+ throw new Error(`UPDATE_COMMAND_FAILED: ${detail}`);
84
+ }
85
+ }
86
+ function normalizeVersionPayload(value) {
87
+ if (!value) {
88
+ throw new Error("UPDATE_LOOKUP_FAILED: npm did not return a version.");
89
+ }
90
+ try {
91
+ const parsed = JSON.parse(value);
92
+ if (Array.isArray(parsed)) {
93
+ const last = parsed.at(-1);
94
+ if (typeof last === "string" && last.trim()) {
95
+ return last;
96
+ }
97
+ }
98
+ if (typeof parsed === "string" && parsed.trim()) {
99
+ return parsed;
100
+ }
101
+ }
102
+ catch {
103
+ if (typeof value === "string" && value.trim()) {
104
+ return value.trim();
105
+ }
106
+ }
107
+ throw new Error("UPDATE_LOOKUP_FAILED: npm returned an invalid version payload.");
108
+ }
109
+ function isPathInside(candidatePath, parentPath) {
110
+ const relative = path.relative(parentPath, candidatePath);
111
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
112
+ }
113
+ function quoteShellArg(value) {
114
+ return `"${String(value).replace(/"/g, '\\"')}"`;
115
+ }
116
+ function resolveNpmCommand(platform) {
117
+ return platform === "win32" ? "npm.cmd" : "npm";
118
+ }
package/app/server/app.js CHANGED
@@ -14,7 +14,6 @@ import { buildProjectGraph } from "./project-graph.js";
14
14
  import { createId, isPathWithinRoot } from "./utils.js";
15
15
  const relationTypeSet = new Set(relationTypes);
16
16
  const allowedLoopbackHostnames = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
17
- const isDesktopManagedApi = process.env.ELECTRON_RUN_AS_NODE === "1";
18
17
  const updateNodeRequestSchema = updateNodeSchema.extend({
19
18
  source: sourceSchema
20
19
  });
@@ -453,7 +452,7 @@ function buildServiceIndex(workspaceInfo) {
453
452
  {
454
453
  method: "GET",
455
454
  path: "/api/v1/observability/errors?since=24h&surface=mcp",
456
- purpose: "Inspect recent telemetry errors for the API, MCP bridge, or desktop shell."
455
+ purpose: "Inspect recent telemetry errors for the API or MCP bridge."
457
456
  },
458
457
  {
459
458
  method: "POST",
@@ -543,7 +542,7 @@ export function createRecallXApp(params) {
543
542
  "observability.capturePayloadShape"
544
543
  ]);
545
544
  return {
546
- enabled: isDesktopManagedApi ? parseBooleanSetting(settings["observability.enabled"], false) : true,
545
+ enabled: true,
547
546
  workspaceRoot: currentWorkspaceRoot(),
548
547
  workspaceName: currentWorkspaceInfo().workspaceName,
549
548
  retentionDays: Math.max(1, parseNumberSetting(settings["observability.retentionDays"], 14)),
@@ -1391,7 +1390,7 @@ export function createRecallXApp(params) {
1391
1390
  }));
1392
1391
  app.get("/api/v1/observability/errors", handleAsyncRoute(async (request, response) => {
1393
1392
  const surface = readRequestParam(request.query.surface);
1394
- const normalizedSurface = surface === "api" || surface === "mcp" || surface === "desktop" ? surface : "all";
1393
+ const normalizedSurface = surface === "api" || surface === "mcp" ? surface : "all";
1395
1394
  const errors = await observability.listErrors({
1396
1395
  since: readRequestParam(request.query.since),
1397
1396
  surface: normalizedSurface,
@@ -1 +1 @@
1
- export const RECALLX_VERSION = "1.0.2";
1
+ export const RECALLX_VERSION = "1.0.4";