membot 0.0.1 → 0.1.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 (73) hide show
  1. package/package.json +81 -24
  2. package/patches/@huggingface%2Ftransformers@4.2.0.patch +137 -0
  3. package/scripts/apply-transformers-patch.sh +35 -0
  4. package/src/cli.ts +70 -0
  5. package/src/commands/check-update.ts +69 -0
  6. package/src/commands/mcpx.ts +112 -0
  7. package/src/commands/reindex.ts +53 -0
  8. package/src/commands/serve.ts +58 -0
  9. package/src/commands/upgrade.ts +220 -0
  10. package/src/config/loader.ts +100 -0
  11. package/src/config/schemas.ts +39 -0
  12. package/src/constants.ts +42 -0
  13. package/src/context.ts +80 -0
  14. package/src/db/blobs.ts +53 -0
  15. package/src/db/chunks.ts +176 -0
  16. package/src/db/connection.ts +173 -0
  17. package/src/db/files.ts +325 -0
  18. package/src/db/migrations/001-init.ts +63 -0
  19. package/src/db/migrations/002-fts.ts +12 -0
  20. package/src/db/migrations.ts +45 -0
  21. package/src/errors.ts +87 -0
  22. package/src/ingest/chunker.ts +117 -0
  23. package/src/ingest/converter/docx.ts +15 -0
  24. package/src/ingest/converter/html.ts +20 -0
  25. package/src/ingest/converter/image.ts +71 -0
  26. package/src/ingest/converter/index.ts +119 -0
  27. package/src/ingest/converter/llm.ts +66 -0
  28. package/src/ingest/converter/ocr.ts +51 -0
  29. package/src/ingest/converter/pdf.ts +38 -0
  30. package/src/ingest/converter/text.ts +8 -0
  31. package/src/ingest/describer.ts +72 -0
  32. package/src/ingest/embedder.ts +83 -0
  33. package/src/ingest/fetcher.ts +280 -0
  34. package/src/ingest/ingest.ts +444 -0
  35. package/src/ingest/local-reader.ts +64 -0
  36. package/src/ingest/search-text.ts +18 -0
  37. package/src/ingest/source-resolver.ts +186 -0
  38. package/src/mcp/instructions.ts +34 -0
  39. package/src/mcp/server.ts +101 -0
  40. package/src/mount/commander.ts +174 -0
  41. package/src/mount/mcp.ts +111 -0
  42. package/src/mount/zod-to-cli.ts +158 -0
  43. package/src/operations/add.ts +69 -0
  44. package/src/operations/diff.ts +105 -0
  45. package/src/operations/index.ts +38 -0
  46. package/src/operations/info.ts +95 -0
  47. package/src/operations/list.ts +87 -0
  48. package/src/operations/move.ts +83 -0
  49. package/src/operations/prune.ts +80 -0
  50. package/src/operations/read.ts +102 -0
  51. package/src/operations/refresh.ts +72 -0
  52. package/src/operations/remove.ts +35 -0
  53. package/src/operations/search.ts +72 -0
  54. package/src/operations/tree.ts +103 -0
  55. package/src/operations/types.ts +81 -0
  56. package/src/operations/versions.ts +78 -0
  57. package/src/operations/write.ts +77 -0
  58. package/src/output/formatter.ts +68 -0
  59. package/src/output/logger.ts +114 -0
  60. package/src/output/progress.ts +78 -0
  61. package/src/output/tty.ts +91 -0
  62. package/src/refresh/runner.ts +296 -0
  63. package/src/refresh/scheduler.ts +54 -0
  64. package/src/sdk.ts +27 -0
  65. package/src/search/hybrid.ts +100 -0
  66. package/src/search/keyword.ts +62 -0
  67. package/src/search/semantic.ts +56 -0
  68. package/src/update/background.ts +73 -0
  69. package/src/update/cache.ts +40 -0
  70. package/src/update/checker.ts +117 -0
  71. package/.claude/settings.local.json +0 -7
  72. package/CLAUDE.md +0 -139
  73. package/docs/plan.md +0 -905
package/package.json CHANGED
@@ -1,26 +1,83 @@
1
1
  {
2
- "name": "membot",
3
- "version": "0.0.1",
4
- "description": "Versioned context store with hybrid search for AI agents. Stdio + HTTP MCP server and CLI.",
5
- "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
- "context",
9
- "memory",
10
- "agent",
11
- "rag",
12
- "embeddings",
13
- "duckdb",
14
- "bun"
15
- ],
16
- "license": "MIT",
17
- "author": "Evan Tahler <evan@arcade.dev>",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/evantahler/membot.git"
21
- },
22
- "homepage": "https://github.com/evantahler/membot",
23
- "bugs": {
24
- "url": "https://github.com/evantahler/membot/issues"
25
- }
2
+ "name": "membot",
3
+ "version": "0.1.0",
4
+ "description": "Versioned context store with hybrid search for AI agents. Stdio + HTTP MCP server and CLI.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/sdk.ts",
8
+ "./cli": "./src/cli.ts"
9
+ },
10
+ "main": "./src/sdk.ts",
11
+ "types": "./src/sdk.ts",
12
+ "bin": {
13
+ "membot": "./src/cli.ts"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "patches",
18
+ "scripts",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "dev": "bun run src/cli.ts",
24
+ "test": "bun test",
25
+ "lint": "biome ci . && tsc --noEmit",
26
+ "format": "biome check --write .",
27
+ "prebuild": "bash scripts/apply-transformers-patch.sh",
28
+ "build": "bun build --compile --minify --sourcemap --external '@duckdb/*' ./src/cli.ts --outfile dist/membot"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "context",
34
+ "memory",
35
+ "agent",
36
+ "rag",
37
+ "embeddings",
38
+ "duckdb",
39
+ "bun"
40
+ ],
41
+ "license": "MIT",
42
+ "author": "Evan Tahler <evan@arcade.dev>",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/evantahler/membot.git"
46
+ },
47
+ "homepage": "https://github.com/evantahler/membot",
48
+ "bugs": {
49
+ "url": "https://github.com/evantahler/membot/issues"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "dependencies": {
55
+ "@anthropic-ai/sdk": "^0.32.0",
56
+ "@duckdb/node-api": "1.5.2-r.1",
57
+ "@evantahler/mcpx": "^0.21.4",
58
+ "@huggingface/transformers": "^4.2.0",
59
+ "@modelcontextprotocol/sdk": "^1.29.0",
60
+ "ansis": "^4.2.0",
61
+ "commander": "^14.0.3",
62
+ "gray-matter": "^4.0.3",
63
+ "mammoth": "^1.8.0",
64
+ "nanospinner": "^1.2.2",
65
+ "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
66
+ "picomatch": "^4.0.4",
67
+ "@types/picomatch": "^4.0.3",
68
+ "tesseract.js": "^5.1.0",
69
+ "turndown": "^7.2.0",
70
+ "@types/turndown": "^5.0.5",
71
+ "unpdf": "^0.12.0",
72
+ "zod": "^4.0.0",
73
+ "zod-to-json-schema": "^3.23.0"
74
+ },
75
+ "devDependencies": {
76
+ "@biomejs/biome": "^2.4.14",
77
+ "@types/bun": "latest",
78
+ "typescript": "^6"
79
+ },
80
+ "peerDependencies": {
81
+ "typescript": "^6"
82
+ }
26
83
  }
@@ -0,0 +1,137 @@
1
+ diff --git a/dist/transformers.node.mjs b/dist/transformers.node.mjs
2
+ index bacb354fe1b898d4c535a39f5ef1ba5c6a463d75..0ab58f60460236259f9eba88447313f8661cc0ca 100644
3
+ --- a/dist/transformers.node.mjs
4
+ +++ b/dist/transformers.node.mjs
5
+ @@ -7542,7 +7542,10 @@ var uint16_to_float32 = /* @__PURE__ */ (function() {
6
+ })();
7
+
8
+ // src/backends/onnx.js
9
+ -import * as ONNX_NODE from "onnxruntime-node";
10
+ +// PATCHED (mcpx): static `import 'onnxruntime-node'` makes Bun --compile bundle
11
+ +// the native binding which then fails to dlopen libonnxruntime at runtime.
12
+ +// We never want the native bindings — onnxruntime-web (WASM) runs fine.
13
+ +var ONNX_NODE = void 0;
14
+
15
+ // ../../node_modules/.pnpm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/node_modules/onnxruntime-web/dist/ort.webgpu.bundle.min.mjs
16
+ var ort_webgpu_bundle_min_exports = {};
17
+ @@ -11551,23 +11554,12 @@ var ORT_SYMBOL = /* @__PURE__ */ Symbol.for("onnxruntime");
18
+ if (ORT_SYMBOL in globalThis) {
19
+ ONNX = globalThis[ORT_SYMBOL];
20
+ } else if (apis.IS_NODE_ENV) {
21
+ - ONNX = ONNX_NODE;
22
+ - switch (process.platform) {
23
+ - case "win32":
24
+ - supportedDevices.push("dml");
25
+ - break;
26
+ - case "linux":
27
+ - if (process.arch === "x64") {
28
+ - supportedDevices.push("cuda");
29
+ - }
30
+ - break;
31
+ - case "darwin":
32
+ - supportedDevices.push("coreml");
33
+ - break;
34
+ - }
35
+ - supportedDevices.push("webgpu");
36
+ - supportedDevices.push("cpu");
37
+ - defaultDevices = ["cpu"];
38
+ + // PATCHED (mcpx): force the WASM backend in node-like envs so onnxruntime-node
39
+ + // native bindings are never loaded (they can't be bundled into the Bun
40
+ + // --compile single binary).
41
+ + ONNX = ort_webgpu_bundle_min_exports;
42
+ + supportedDevices.push("wasm");
43
+ + defaultDevices = ["wasm"];
44
+ } else {
45
+ ONNX = ort_webgpu_bundle_min_exports;
46
+ if (apis.IS_WEBNN_AVAILABLE) {
47
+ @@ -17738,7 +17730,14 @@ var CohereAsrProcessor = class extends Processor {
48
+ };
49
+
50
+ // src/utils/image.js
51
+ -import sharp from "sharp";
52
+ +// PATCHED (mcpx): sharp has native bindings that can't be bundled into a
53
+ +// Bun --compile binary. We don't need image processing for text embeddings;
54
+ +// stub sharp with a function that throws lazily if image processing is ever
55
+ +// actually invoked. Truthy so the `else if (sharp)` branch below initializes
56
+ +// loadImageFunction normally.
57
+ +var sharp = function sharpStub() {
58
+ + throw new Error("Image processing (sharp) is not available in this build.");
59
+ +};
60
+ var createCanvasFunction;
61
+ var ImageDataClass;
62
+ var loadImageFunction;
63
+ @@ -22328,11 +22327,16 @@ function getExternalDataChunkNames(fullName, numChunks) {
64
+ async function getCoreModelFile(pretrained_model_name_or_path, fileName, options, suffix) {
65
+ const baseName = `${fileName}${suffix}.onnx`;
66
+ const fullPath = `${options.subfolder ?? ""}/${baseName}`;
67
+ - return await getModelFile(pretrained_model_name_or_path, fullPath, true, options, apis.IS_NODE_ENV);
68
+ + // PATCHED (mcpx): always return the model bytes (buffer) instead of a path.
69
+ + // Our patched onnxruntime backend is the bundled webgpu/web build, which
70
+ + // can't read paths via node:fs and would try to `fetch()` a bare path,
71
+ + // failing with `ERR_INVALID_URL`. Returning the buffer skips the fetch.
72
+ + return await getModelFile(pretrained_model_name_or_path, fullPath, true, options, false);
73
+ }
74
+ async function getModelDataFiles(pretrained_model_name_or_path, fileName, suffix, options, use_external_data_format, session_options = {}) {
75
+ const baseName = `${fileName}${suffix}.onnx`;
76
+ - const return_path = apis.IS_NODE_ENV;
77
+ + // PATCHED (mcpx): see getCoreModelFile patch above — return buffers, not paths.
78
+ + const return_path = false;
79
+ let externalDataPromises = [];
80
+ const num_chunks = resolveExternalDataFormat(use_external_data_format, baseName, fileName);
81
+ if (num_chunks > 0) {
82
+ diff --git a/src/backends/onnx.js b/src/backends/onnx.js
83
+ index 13b1a748272d03aa062950ff585c1a2277ba96c9..9863c46653618091593b6428a8c16622186318d6 100644
84
+ --- a/src/backends/onnx.js
85
+ +++ b/src/backends/onnx.js
86
+ @@ -20,7 +20,10 @@ import { env, apis, LogLevel } from '../env.js';
87
+
88
+ // NOTE: Import order matters here. We need to import `onnxruntime-node` before `onnxruntime-web`.
89
+ // In either case, we select the default export if it exists, otherwise we use the named export.
90
+ -import * as ONNX_NODE from 'onnxruntime-node';
91
+ +// PATCHED (mcpx): the static `import 'onnxruntime-node'` segfaults under Bun
92
+ +// (oven-sh/bun#26081). We never want the native bindings — `onnxruntime-web` (WASM) runs
93
+ +// fine on Bun + Node + browsers. The IS_NODE_ENV branch below is rerouted to ONNX_WEB.
94
+ +const ONNX_NODE = undefined;
95
+ import * as ONNX_WEB from 'onnxruntime-web/webgpu';
96
+ import { loadWasmBinary, loadWasmFactory } from './utils/cacheWasm.js';
97
+ import { isBlobURL, toAbsoluteURL } from '../utils/hub/utils.js';
98
+ @@ -106,34 +109,11 @@ if (ORT_SYMBOL in globalThis) {
99
+ // If the JS runtime exposes their own ONNX runtime, use it
100
+ ONNX = globalThis[ORT_SYMBOL];
101
+ } else if (apis.IS_NODE_ENV) {
102
+ - ONNX = ONNX_NODE;
103
+ -
104
+ - // Updated as of ONNX Runtime 1.23.0-dev.20250612-70f14d7670
105
+ - // The following table lists the supported versions of ONNX Runtime Node.js binding provided with pre-built binaries.
106
+ - // | EPs/Platforms | Windows x64 | Windows arm64 | Linux x64 | Linux arm64 | MacOS x64 | MacOS arm64 |
107
+ - // | --------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
108
+ - // | CPU | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
109
+ - // | WebGPU (experimental) | ✔️ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
110
+ - // | DirectML | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ |
111
+ - // | CUDA | ❌ | ❌ | ✔️ (CUDA v12) | ❌ | ❌ | ❌ |
112
+ - // | CoreML | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ |
113
+ - switch (process.platform) {
114
+ - case 'win32': // Windows x64 and Windows arm64
115
+ - supportedDevices.push('dml');
116
+ - break;
117
+ - case 'linux': // Linux x64 and Linux arm64
118
+ - if (process.arch === 'x64') {
119
+ - supportedDevices.push('cuda');
120
+ - }
121
+ - break;
122
+ - case 'darwin': // MacOS x64 and MacOS arm64
123
+ - supportedDevices.push('coreml');
124
+ - break;
125
+ - }
126
+ -
127
+ - supportedDevices.push('webgpu');
128
+ - supportedDevices.push('cpu');
129
+ - defaultDevices = ['cpu'];
130
+ + // PATCHED (mcpx): force the WASM backend in node-like envs to avoid
131
+ + // loading onnxruntime-node native bindings (segfaults under Bun).
132
+ + ONNX = ONNX_WEB;
133
+ + supportedDevices.push('wasm');
134
+ + defaultDevices = ['wasm'];
135
+ } else {
136
+ ONNX = ONNX_WEB;
137
+
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Apply the @huggingface/transformers patch to node_modules so that
5
+ # `bun build --compile` produces a binary using the WASM backend
6
+ # (onnxruntime-web) instead of onnxruntime-node, whose native bindings
7
+ # can't be bundled into a single-binary distribution.
8
+ #
9
+ # We apply the patch imperatively (rather than via package.json
10
+ # `patchedDependencies`) because that field, when present in a
11
+ # published package, breaks `bun install` from a tarball.
12
+
13
+ PATCH="patches/@huggingface%2Ftransformers@4.2.0.patch"
14
+ TARGET="node_modules/@huggingface/transformers"
15
+ MARKER="$TARGET/.membot-transformers-patch-applied"
16
+
17
+ if [ ! -d "$TARGET" ]; then
18
+ echo "error: $TARGET not found — run \`bun install\` first" >&2
19
+ exit 1
20
+ fi
21
+
22
+ if [ ! -f "$PATCH" ]; then
23
+ echo "error: $PATCH not found" >&2
24
+ exit 1
25
+ fi
26
+
27
+ if [ -f "$MARKER" ]; then
28
+ echo "transformers patch already applied — skipping"
29
+ exit 0
30
+ fi
31
+
32
+ echo "Applying transformers patch ($PATCH) to $TARGET..."
33
+ git apply --directory="$TARGET" "$PATCH"
34
+ touch "$MARKER"
35
+ echo "Patch applied."
package/src/cli.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { bold, cyan, dim, green, yellow } from "ansis";
4
+ import { program } from "commander";
5
+ import pkg from "../package.json" with { type: "json" };
6
+ import { registerCheckUpdateCommand } from "./commands/check-update.ts";
7
+ import { registerMcpxCommand } from "./commands/mcpx.ts";
8
+ import { registerReindexCommand } from "./commands/reindex.ts";
9
+ import { registerServeCommand } from "./commands/serve.ts";
10
+ import { registerUpgradeCommand } from "./commands/upgrade.ts";
11
+ import type { BuildContextOptions } from "./context.ts";
12
+ import { mountAsCommanderCommand } from "./mount/commander.ts";
13
+ import { OPERATIONS } from "./operations/index.ts";
14
+ import { logger } from "./output/logger.ts";
15
+ import { maybeCheckForUpdate } from "./update/background.ts";
16
+
17
+ program
18
+ .name("membot")
19
+ .description("Versioned context store with hybrid search for AI agents. Stdio + HTTP MCP server and CLI.")
20
+ .version(pkg.version)
21
+ .option("-c, --config <path>", "membot data dir (default ~/.membot)")
22
+ .option("-j, --json", "force JSON output")
23
+ .option("-v, --verbose", "verbose / debug logging")
24
+ .option("--no-color", "disable ANSI colors")
25
+ .option("--no-interactive", "force non-interactive mode (no spinners)");
26
+
27
+ program.configureHelp({
28
+ styleTitle: (str) => bold(str),
29
+ styleCommandText: (str) => cyan(str),
30
+ styleSubcommandText: (str) => cyan(str),
31
+ styleOptionText: (str) => yellow(str),
32
+ styleArgumentText: (str) => green(str),
33
+ styleDescriptionText: (str) => dim(str),
34
+ });
35
+
36
+ const getContextOptions = (): BuildContextOptions => {
37
+ const opts = program.opts<{
38
+ config?: string;
39
+ json?: boolean;
40
+ verbose?: boolean;
41
+ color?: boolean;
42
+ interactive?: boolean;
43
+ }>();
44
+ return {
45
+ configFlag: opts.config,
46
+ json: opts.json,
47
+ verbose: opts.verbose,
48
+ noColor: opts.color === false,
49
+ noInteractive: opts.interactive === false,
50
+ };
51
+ };
52
+
53
+ for (const op of OPERATIONS) {
54
+ mountAsCommanderCommand(program, op, getContextOptions);
55
+ }
56
+
57
+ registerServeCommand(program);
58
+ registerReindexCommand(program);
59
+ registerMcpxCommand(program);
60
+ registerCheckUpdateCommand(program);
61
+ registerUpgradeCommand(program);
62
+
63
+ const updateNotice = maybeCheckForUpdate();
64
+
65
+ program.parse();
66
+
67
+ process.on("beforeExit", async () => {
68
+ const notice = await updateNotice;
69
+ if (notice) logger.writeRaw(notice);
70
+ });
@@ -0,0 +1,69 @@
1
+ import { cyan, dim, green, yellow } from "ansis";
2
+ import type { Command } from "commander";
3
+ import { createSpinner } from "nanospinner";
4
+ import pkg from "../../package.json" with { type: "json" };
5
+ import { saveUpdateCache } from "../update/cache.ts";
6
+ import { checkForUpdate, type UpdateCache } from "../update/checker.ts";
7
+
8
+ /**
9
+ * Register `membot check-update`. Performs a non-destructive npm-registry check,
10
+ * caches the result, and prints the current/latest version (plus changelog when an
11
+ * update is available). Emits raw `UpdateInfo` JSON when `--json` is set.
12
+ */
13
+ export function registerCheckUpdateCommand(program: Command) {
14
+ program
15
+ .command("check-update")
16
+ .description("Check for a newer version of membot")
17
+ .action(async () => {
18
+ const opts = program.opts();
19
+ const json = !!(opts.json as boolean | undefined);
20
+ const isTTY = process.stderr.isTTY ?? false;
21
+
22
+ const spinner =
23
+ !json && isTTY ? createSpinner("Checking for updates...", { stream: process.stderr }).start() : null;
24
+
25
+ try {
26
+ const info = await checkForUpdate(pkg.version);
27
+
28
+ const cache: UpdateCache = {
29
+ lastCheckAt: new Date().toISOString(),
30
+ latestVersion: info.latestVersion,
31
+ hasUpdate: info.hasUpdate,
32
+ changelog: info.changelog,
33
+ };
34
+ await saveUpdateCache(cache);
35
+
36
+ spinner?.stop();
37
+
38
+ if (json) {
39
+ console.log(JSON.stringify(info, null, 2));
40
+ return;
41
+ }
42
+
43
+ if (!info.hasUpdate) {
44
+ if (info.aheadOfLatest) {
45
+ console.log(
46
+ yellow(`membot v${info.currentVersion} is ahead of latest published release (v${info.latestVersion})`),
47
+ );
48
+ } else {
49
+ console.log(green(`membot is up to date (v${info.currentVersion})`));
50
+ }
51
+ return;
52
+ }
53
+
54
+ console.log(yellow(`Update available: ${info.currentVersion} → ${info.latestVersion}`));
55
+
56
+ if (info.changelog) {
57
+ console.log("");
58
+ console.log(dim(info.changelog));
59
+ }
60
+
61
+ console.log("");
62
+ console.log(cyan("Run `membot upgrade` to update"));
63
+ } catch (err) {
64
+ spinner?.error({ text: "Failed to check for updates" });
65
+ console.error(String(err));
66
+ process.exit(1);
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,112 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { Command } from "commander";
4
+ import { logger } from "../output/logger.ts";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ /**
9
+ * Resolve the path to the bundled mcpx CLI entrypoint. We spawn it as a
10
+ * child process rather than calling its functions directly so that the
11
+ * upstream's argv parsing, output formatting, and config conventions stay
12
+ * authoritative — `membot mcpx <subcmd>` behaves identically to the user
13
+ * running `mcpx <subcmd>` themselves, just with our project's `--config`
14
+ * resolution layered on top when applicable.
15
+ */
16
+ const MCPX_CLI = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
17
+
18
+ /**
19
+ * Forward an argv slice to the bundled mcpx CLI. Inherits stdio so prompts
20
+ * and pretty output flow through as if mcpx were called directly. Exits
21
+ * the parent process with the child's status code on failure.
22
+ */
23
+ export async function runMcpx(args: string[]): Promise<void> {
24
+ const proc = Bun.spawn(["bun", MCPX_CLI, ...args], {
25
+ stdout: "inherit",
26
+ stderr: "inherit",
27
+ stdin: "inherit",
28
+ });
29
+ const code = await proc.exited;
30
+ if (code !== 0) process.exit(code);
31
+ }
32
+
33
+ /**
34
+ * Pull the verbatim argv tokens that follow `mcpx` in `process.argv`. We
35
+ * forward them unmodified so flags (`--help`, `-c`, etc.) reach the
36
+ * upstream CLI exactly as the user typed them.
37
+ */
38
+ function getRawMcpxArgs(): string[] {
39
+ const idx = process.argv.indexOf("mcpx");
40
+ return idx === -1 ? [] : process.argv.slice(idx + 1);
41
+ }
42
+
43
+ const PASSTHROUGH_SUBCOMMANDS: ReadonlyArray<[name: string, desc: string]> = [
44
+ ["servers", "List configured MCP server names"],
45
+ ["info", "Show server overview or schema for a specific tool"],
46
+ ["search", "Search tools by keyword and/or semantic similarity"],
47
+ ["exec", "Execute a tool call"],
48
+ ["add", "Add an MCP server"],
49
+ ["remove", "Remove an MCP server"],
50
+ ["ping", "Check connectivity to MCP servers"],
51
+ ["auth", "Authenticate with an HTTP MCP server"],
52
+ ["deauth", "Remove stored authentication for a server"],
53
+ ["resource", "List resources for a server, or read a specific resource"],
54
+ ["prompt", "List prompts for a server, or get a specific prompt"],
55
+ ["task", "Manage async tool tasks (list, get, result, cancel)"],
56
+ ["index", "Build the search index from all configured servers"],
57
+ ];
58
+
59
+ /**
60
+ * Register `membot mcpx <subcommand>` for every passthrough subcommand on
61
+ * the upstream CLI. `--help` and unknown options are forwarded so users
62
+ * always get authoritative mcpx help text.
63
+ */
64
+ export function registerMcpxCommand(program: Command): void {
65
+ const mcpx = program.command("mcpx").description("Forward to the bundled mcpx CLI for managing MCP servers");
66
+
67
+ const verifyVersion = (() => {
68
+ try {
69
+ const ourPkg = require("../../package.json") as { dependencies: Record<string, string> };
70
+ const mcpxPkg = require("@evantahler/mcpx/package.json") as { version: string };
71
+ const declared = ourPkg.dependencies["@evantahler/mcpx"];
72
+ if (!declared) return true;
73
+ return (
74
+ mcpxPkg.version === declared ||
75
+ declared.startsWith(mcpxPkg.version) ||
76
+ mcpxPkg.version.startsWith(declared.replace(/^[\^~]/, ""))
77
+ );
78
+ } catch {
79
+ return true;
80
+ }
81
+ })();
82
+ if (!verifyVersion) {
83
+ logger.warn("@evantahler/mcpx version mismatch — `membot mcpx` may behave unexpectedly.");
84
+ }
85
+
86
+ for (const [name, description] of PASSTHROUGH_SUBCOMMANDS) {
87
+ mcpx
88
+ .command(name)
89
+ .description(description)
90
+ .allowUnknownOption(true)
91
+ .helpOption(false)
92
+ .argument("[args...]", "arguments forwarded to mcpx")
93
+ .action(async () => {
94
+ await runMcpx(getRawMcpxArgs());
95
+ });
96
+ }
97
+
98
+ // Upstream mcpx's "list" is the default action when invoked with no
99
+ // subcommand — not a registered subcommand — so strip the "list" token
100
+ // before forwarding.
101
+ mcpx
102
+ .command("list")
103
+ .description("List all tools, resources, and prompts across all configured servers")
104
+ .allowUnknownOption(true)
105
+ .helpOption(false)
106
+ .argument("[args...]", "arguments forwarded to mcpx")
107
+ .action(async () => {
108
+ const raw = getRawMcpxArgs();
109
+ const args = raw[0] === "list" ? raw.slice(1) : raw;
110
+ await runMcpx(args);
111
+ });
112
+ }
@@ -0,0 +1,53 @@
1
+ import type { Command } from "commander";
2
+ import { buildContext, closeContext } from "../context.ts";
3
+ import { rebuildFts } from "../db/chunks.ts";
4
+ import { logger } from "../output/logger.ts";
5
+
6
+ /**
7
+ * `membot reindex`
8
+ *
9
+ * Rebuild the FTS index over `current_chunks`. Useful after manually
10
+ * editing the DB or upgrading after a schema change. Does NOT re-embed —
11
+ * embeddings are durable and are managed by the ingest/refresh pipelines.
12
+ */
13
+ export function registerReindexCommand(program: Command): void {
14
+ program
15
+ .command("reindex")
16
+ .description("Rebuild the FTS keyword index over current chunks")
17
+ .action(async () => {
18
+ const ctx = await buildContext({});
19
+ try {
20
+ const result = await rebuildFts(ctx.db);
21
+ switch (result.kind) {
22
+ case "rebuilt":
23
+ logger.info(`reindex: FTS index rebuilt over ${result.chunk_count} chunks`);
24
+ console.log(JSON.stringify({ ok: true, chunk_count: result.chunk_count }));
25
+ break;
26
+ case "no_chunks":
27
+ logger.info("reindex: no chunks to index — run `membot add <path>` to ingest content first");
28
+ console.log(JSON.stringify({ ok: true, chunk_count: 0 }));
29
+ break;
30
+ case "extension_unavailable":
31
+ logger.warn(
32
+ `reindex: FTS extension unavailable — search will degrade to semantic-only${
33
+ result.cause ? ` (${result.cause})` : ""
34
+ }`,
35
+ );
36
+ console.log(
37
+ JSON.stringify({
38
+ ok: false,
39
+ reason: "fts_extension_unavailable",
40
+ cause: result.cause,
41
+ }),
42
+ );
43
+ break;
44
+ case "rebuild_failed":
45
+ logger.warn(`reindex: FTS rebuild failed${result.cause ? ` (${result.cause})` : ""}`);
46
+ console.log(JSON.stringify({ ok: false, reason: "rebuild_failed", cause: result.cause }));
47
+ break;
48
+ }
49
+ } finally {
50
+ await closeContext(ctx);
51
+ }
52
+ });
53
+ }
@@ -0,0 +1,58 @@
1
+ import type { Command } from "commander";
2
+ import { buildContext, closeContext } from "../context.ts";
3
+ import { startStdioServer } from "../mcp/server.ts";
4
+ import { logger } from "../output/logger.ts";
5
+ import { startDaemon } from "../refresh/scheduler.ts";
6
+
7
+ /**
8
+ * `membot serve [--http <port>] [--watch] [--tick <sec>]`
9
+ *
10
+ * Start the MCP server in stdio mode (default) or HTTP streamable mode
11
+ * (when `--http <port>` is given). Optionally also start the refresh
12
+ * daemon (`--watch`) which ticks every `--tick` seconds and refreshes
13
+ * any rows whose `refresh_frequency_sec` has elapsed.
14
+ */
15
+ export function registerServeCommand(program: Command): void {
16
+ program
17
+ .command("serve")
18
+ .description("Run the MCP server (stdio default, --http for streamable HTTP) and optionally the refresh daemon")
19
+ .option("--http <port>", "expose MCP over HTTP on this port instead of stdio")
20
+ .option("--watch", "also run the refresh daemon (auto-refresh due rows)")
21
+ .option("--tick <sec>", "daemon tick interval in seconds (default 60)")
22
+ .action(async (options: { http?: string; watch?: boolean; tick?: string }) => {
23
+ const httpPort = options.http ? Number(options.http) : null;
24
+ let stopServer: (() => Promise<void>) | null = null;
25
+ let stopDaemon: (() => void) | null = null;
26
+
27
+ const onShutdown = async () => {
28
+ if (stopDaemon) stopDaemon();
29
+ if (stopServer) await stopServer();
30
+ };
31
+ process.on("SIGINT", () => {
32
+ logger.info("shutting down...");
33
+ onShutdown().finally(() => process.exit(0));
34
+ });
35
+ process.on("SIGTERM", () => {
36
+ onShutdown().finally(() => process.exit(0));
37
+ });
38
+
39
+ if (httpPort && !Number.isFinite(httpPort)) {
40
+ logger.error(`invalid --http port: ${options.http}`);
41
+ process.exit(2);
42
+ }
43
+
44
+ if (options.watch) {
45
+ const ctx = await buildContext({});
46
+ const tickSec = options.tick ? Number(options.tick) : ctx.config.daemon.tick_interval_sec;
47
+ stopDaemon = startDaemon(ctx, Number.isFinite(tickSec) && tickSec > 0 ? tickSec : 60);
48
+ process.on("beforeExit", () => closeContext(ctx));
49
+ }
50
+
51
+ if (httpPort) {
52
+ const { startHttpServer } = await import("../mcp/server.ts");
53
+ stopServer = await startHttpServer(httpPort);
54
+ } else {
55
+ stopServer = await startStdioServer();
56
+ }
57
+ });
58
+ }