recallx-headless 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
@@ -5,7 +5,7 @@
5
5
  - This package is the npm-distributed headless runtime for RecallX.
6
6
  - It provides the `recallx` and `recallx-mcp` commands.
7
7
  - It can start the local RecallX API directly through `recallx serve`.
8
- - It does not include the renderer or desktop release artifacts.
8
+ - It does not include the renderer.
9
9
 
10
10
  It defers behavior to the local RecallX API contract in [`docs/api.md`](../../docs/api.md).
11
11
 
@@ -28,6 +28,7 @@ In another shell:
28
28
 
29
29
  ```bash
30
30
  recallx health
31
+ recallx update
31
32
  recallx-mcp --help
32
33
  recallx mcp install
33
34
  ```
@@ -55,7 +56,16 @@ recallx serve --workspace-name "Personal Workspace"
55
56
  recallx serve --api-token secret-token
56
57
  ```
57
58
 
58
- The headless package does not ship renderer pages or desktop release artifacts. At `/`, it returns a runtime notice instead of the renderer app.
59
+ To update an npm-installed RecallX runtime from the CLI:
60
+
61
+ ```bash
62
+ recallx update
63
+ recallx update --apply
64
+ ```
65
+
66
+ `recallx update` currently supports npm global installs of `recallx` and `recallx-headless`. Source checkouts should keep using their package manager directly.
67
+
68
+ The headless package does not ship renderer pages. At `/`, it returns a runtime notice instead of the renderer app.
59
69
 
60
70
  You can also print the direct MCP command or a config snippet:
61
71
 
@@ -71,6 +81,7 @@ Quick health and workspace checks:
71
81
 
72
82
  ```bash
73
83
  recallx health
84
+ recallx update
74
85
  recallx workspace current
75
86
  recallx workspace list
76
87
  ```
@@ -106,6 +117,7 @@ recallx mcp config
106
117
 
107
118
  ```bash
108
119
  recallx health
120
+ recallx update [--apply]
109
121
  recallx mcp config
110
122
  recallx mcp install
111
123
  recallx mcp path
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recallx-headless",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Headless RecallX runtime with API, CLI, and MCP entrypoint.",
5
5
  "type": "module",
6
6
  "bin": {