recallx 1.0.2 → 1.0.5

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
@@ -74,11 +69,25 @@ npm start
74
69
  Checks:
75
70
 
76
71
  ```bash
72
+ npm run branch:check
73
+ npm run version:check
77
74
  npm run check
78
75
  npm test
79
76
  npm run build
80
77
  ```
81
78
 
79
+ Start unrelated work in a separate branch/worktree instead of stacking it on the current checkout:
80
+
81
+ ```bash
82
+ npm run branch:new -- fix-short-name
83
+ ```
84
+
85
+ When you are preparing a release, bump from the highest known version instead of whatever the current branch happens to say:
86
+
87
+ ```bash
88
+ npm run version:bump -- patch
89
+ ```
90
+
82
91
  If you want an installable runtime instead of source-run workflows, use one of the npm distribution paths below.
83
92
 
84
93
  ## 2. npm Full Runtime (`recallx`)
@@ -94,6 +103,7 @@ In another shell:
94
103
 
95
104
  ```bash
96
105
  recallx health
106
+ recallx update
97
107
  recallx mcp install
98
108
  recallx-mcp --help
99
109
  ```
@@ -106,10 +116,6 @@ The full npm package includes:
106
116
  - `recallx serve` and subcommands
107
117
  - `recallx-mcp`
108
118
 
109
- The full npm package does not include:
110
-
111
- - desktop release artifacts
112
-
113
119
  `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
120
 
115
121
  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 +134,15 @@ recallx serve --workspace-root /Users/name/Documents/RecallX
128
134
  recallx serve --api-token secret-token
129
135
  ```
130
136
 
137
+ To update an npm-installed full runtime:
138
+
139
+ ```bash
140
+ recallx update
141
+ recallx update --apply
142
+ ```
143
+
144
+ `recallx update` currently supports npm global installs of `recallx` and `recallx-headless`. Source checkouts should keep using their package manager directly.
145
+
131
146
  ## 3. npm Headless Runtime (`recallx-headless`)
132
147
 
133
148
  Use the headless npm package when you want the local API, CLI, and MCP entrypoint without shipping the renderer bundle:
@@ -141,6 +156,7 @@ In another shell:
141
156
 
142
157
  ```bash
143
158
  recallx health
159
+ recallx update
144
160
  recallx-mcp --help
145
161
  ```
146
162
 
@@ -154,10 +170,16 @@ The headless npm package includes:
154
170
  The headless npm package does not include:
155
171
 
156
172
  - renderer pages
157
- - desktop release artifacts
158
173
 
159
174
  At `/`, the headless runtime returns a small runtime notice instead of the renderer.
160
175
 
176
+ To update an npm-installed headless runtime:
177
+
178
+ ```bash
179
+ recallx update
180
+ recallx update --apply
181
+ ```
182
+
161
183
  Node requirements:
162
184
 
163
185
  - 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.5";