membot 0.0.1 → 0.1.1
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/.claude/skills/membot.md +137 -0
- package/.cursor/rules/membot.mdc +137 -0
- package/README.md +131 -0
- package/package.json +83 -24
- package/patches/@huggingface%2Ftransformers@4.2.0.patch +137 -0
- package/scripts/apply-transformers-patch.sh +35 -0
- package/src/cli.ts +72 -0
- package/src/commands/check-update.ts +69 -0
- package/src/commands/mcpx.ts +112 -0
- package/src/commands/reindex.ts +53 -0
- package/src/commands/serve.ts +58 -0
- package/src/commands/skill.ts +131 -0
- package/src/commands/upgrade.ts +220 -0
- package/src/config/loader.ts +100 -0
- package/src/config/schemas.ts +39 -0
- package/src/constants.ts +42 -0
- package/src/context.ts +80 -0
- package/src/db/blobs.ts +53 -0
- package/src/db/chunks.ts +176 -0
- package/src/db/connection.ts +173 -0
- package/src/db/files.ts +325 -0
- package/src/db/migrations/001-init.ts +63 -0
- package/src/db/migrations/002-fts.ts +12 -0
- package/src/db/migrations.ts +45 -0
- package/src/errors.ts +87 -0
- package/src/ingest/chunker.ts +117 -0
- package/src/ingest/converter/docx.ts +15 -0
- package/src/ingest/converter/html.ts +20 -0
- package/src/ingest/converter/image.ts +71 -0
- package/src/ingest/converter/index.ts +119 -0
- package/src/ingest/converter/llm.ts +66 -0
- package/src/ingest/converter/ocr.ts +51 -0
- package/src/ingest/converter/pdf.ts +38 -0
- package/src/ingest/converter/text.ts +8 -0
- package/src/ingest/describer.ts +72 -0
- package/src/ingest/embedder.ts +98 -0
- package/src/ingest/fetcher.ts +280 -0
- package/src/ingest/ingest.ts +444 -0
- package/src/ingest/local-reader.ts +64 -0
- package/src/ingest/search-text.ts +18 -0
- package/src/ingest/source-resolver.ts +186 -0
- package/src/mcp/instructions.ts +34 -0
- package/src/mcp/server.ts +101 -0
- package/src/mount/commander.ts +174 -0
- package/src/mount/mcp.ts +111 -0
- package/src/mount/zod-to-cli.ts +158 -0
- package/src/operations/add.ts +69 -0
- package/src/operations/diff.ts +105 -0
- package/src/operations/index.ts +38 -0
- package/src/operations/info.ts +95 -0
- package/src/operations/list.ts +87 -0
- package/src/operations/move.ts +83 -0
- package/src/operations/prune.ts +80 -0
- package/src/operations/read.ts +102 -0
- package/src/operations/refresh.ts +72 -0
- package/src/operations/remove.ts +35 -0
- package/src/operations/search.ts +72 -0
- package/src/operations/tree.ts +103 -0
- package/src/operations/types.ts +81 -0
- package/src/operations/versions.ts +78 -0
- package/src/operations/write.ts +77 -0
- package/src/output/formatter.ts +68 -0
- package/src/output/logger.ts +114 -0
- package/src/output/progress.ts +78 -0
- package/src/output/tty.ts +91 -0
- package/src/refresh/runner.ts +296 -0
- package/src/refresh/scheduler.ts +54 -0
- package/src/sdk.ts +27 -0
- package/src/search/hybrid.ts +100 -0
- package/src/search/keyword.ts +62 -0
- package/src/search/semantic.ts +56 -0
- package/src/types/text-modules.d.ts +9 -0
- package/src/update/background.ts +73 -0
- package/src/update/cache.ts +40 -0
- package/src/update/checker.ts +117 -0
- package/.claude/settings.local.json +0 -7
- package/CLAUDE.md +0 -139
- package/docs/plan.md +0 -905
|
@@ -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,72 @@
|
|
|
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 { registerSkillCommand } from "./commands/skill.ts";
|
|
11
|
+
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
12
|
+
import type { BuildContextOptions } from "./context.ts";
|
|
13
|
+
import { mountAsCommanderCommand } from "./mount/commander.ts";
|
|
14
|
+
import { OPERATIONS } from "./operations/index.ts";
|
|
15
|
+
import { logger } from "./output/logger.ts";
|
|
16
|
+
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name("membot")
|
|
20
|
+
.description("Versioned context store with hybrid search for AI agents. Stdio + HTTP MCP server and CLI.")
|
|
21
|
+
.version(pkg.version)
|
|
22
|
+
.option("-c, --config <path>", "membot data dir (default ~/.membot)")
|
|
23
|
+
.option("-j, --json", "force JSON output")
|
|
24
|
+
.option("-v, --verbose", "verbose / debug logging")
|
|
25
|
+
.option("--no-color", "disable ANSI colors")
|
|
26
|
+
.option("--no-interactive", "force non-interactive mode (no spinners)");
|
|
27
|
+
|
|
28
|
+
program.configureHelp({
|
|
29
|
+
styleTitle: (str) => bold(str),
|
|
30
|
+
styleCommandText: (str) => cyan(str),
|
|
31
|
+
styleSubcommandText: (str) => cyan(str),
|
|
32
|
+
styleOptionText: (str) => yellow(str),
|
|
33
|
+
styleArgumentText: (str) => green(str),
|
|
34
|
+
styleDescriptionText: (str) => dim(str),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const getContextOptions = (): BuildContextOptions => {
|
|
38
|
+
const opts = program.opts<{
|
|
39
|
+
config?: string;
|
|
40
|
+
json?: boolean;
|
|
41
|
+
verbose?: boolean;
|
|
42
|
+
color?: boolean;
|
|
43
|
+
interactive?: boolean;
|
|
44
|
+
}>();
|
|
45
|
+
return {
|
|
46
|
+
configFlag: opts.config,
|
|
47
|
+
json: opts.json,
|
|
48
|
+
verbose: opts.verbose,
|
|
49
|
+
noColor: opts.color === false,
|
|
50
|
+
noInteractive: opts.interactive === false,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const op of OPERATIONS) {
|
|
55
|
+
mountAsCommanderCommand(program, op, getContextOptions);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerServeCommand(program);
|
|
59
|
+
registerReindexCommand(program);
|
|
60
|
+
registerMcpxCommand(program);
|
|
61
|
+
registerSkillCommand(program);
|
|
62
|
+
registerCheckUpdateCommand(program);
|
|
63
|
+
registerUpgradeCommand(program);
|
|
64
|
+
|
|
65
|
+
const updateNotice = maybeCheckForUpdate();
|
|
66
|
+
|
|
67
|
+
program.parse();
|
|
68
|
+
|
|
69
|
+
process.on("beforeExit", async () => {
|
|
70
|
+
const notice = await updateNotice;
|
|
71
|
+
if (notice) logger.writeRaw(notice);
|
|
72
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import claudeSkill from "../../.claude/skills/membot.md" with { type: "text" };
|
|
6
|
+
import cursorRule from "../../.cursor/rules/membot.mdc" with { type: "text" };
|
|
7
|
+
import { HelpfulError, isHelpfulError, mapKindToExit } from "../errors.ts";
|
|
8
|
+
import { renderCliError } from "../mount/commander.ts";
|
|
9
|
+
import { logger } from "../output/logger.ts";
|
|
10
|
+
import { detectMode, setMode } from "../output/tty.ts";
|
|
11
|
+
|
|
12
|
+
interface SkillTarget {
|
|
13
|
+
agentLabel: string;
|
|
14
|
+
scopeLabel: string;
|
|
15
|
+
dir: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SkillInstallOptions {
|
|
21
|
+
claude?: boolean;
|
|
22
|
+
cursor?: boolean;
|
|
23
|
+
global?: boolean;
|
|
24
|
+
project?: boolean;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `membot skill install [--claude] [--cursor] [--global|--project] [-f]`
|
|
30
|
+
*
|
|
31
|
+
* Drop the membot agent skill into the right location for Claude Code
|
|
32
|
+
* (`.claude/skills/membot.md`) or Cursor (`.cursor/rules/membot.mdc`),
|
|
33
|
+
* either in the current project (default) or in the user's home directory
|
|
34
|
+
* (`--global`). Both flags can be combined to install for both targets at
|
|
35
|
+
* once. The skill files are bundled into the binary via Bun text imports
|
|
36
|
+
* so this works in the compiled distribution as well as in `bun run`.
|
|
37
|
+
*/
|
|
38
|
+
export function registerSkillCommand(program: Command): void {
|
|
39
|
+
const skill = program.command("skill").description("Install agent skills (Claude Code, Cursor)");
|
|
40
|
+
|
|
41
|
+
skill
|
|
42
|
+
.command("install")
|
|
43
|
+
.description(
|
|
44
|
+
"Install the membot skill into Claude Code (.claude/skills/membot.md) and/or Cursor (.cursor/rules/membot.mdc)",
|
|
45
|
+
)
|
|
46
|
+
.option("--claude", "install for Claude Code")
|
|
47
|
+
.option("--cursor", "install for Cursor")
|
|
48
|
+
.option("--global", "install to the user's home directory (default: project)")
|
|
49
|
+
.option("--project", "install to the current working directory (default)")
|
|
50
|
+
.option("-f, --force", "overwrite if the skill file already exists")
|
|
51
|
+
.action((opts: SkillInstallOptions) => {
|
|
52
|
+
const globalOpts = program.optsWithGlobals<{ json?: boolean; verbose?: boolean; color?: boolean }>();
|
|
53
|
+
setMode(
|
|
54
|
+
detectMode({
|
|
55
|
+
json: globalOpts.json,
|
|
56
|
+
verbose: globalOpts.verbose,
|
|
57
|
+
noColor: globalOpts.color === false,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
install(opts);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
renderCliError(err);
|
|
64
|
+
process.exit(isHelpfulError(err) ? mapKindToExit(err.kind) : 1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve and write every requested skill file. Throws `HelpfulError` on
|
|
71
|
+
* any input or conflict failure so the mount-style error renderer can
|
|
72
|
+
* surface a uniform JSON / colorized message.
|
|
73
|
+
*/
|
|
74
|
+
function install(opts: SkillInstallOptions): void {
|
|
75
|
+
if (!opts.claude && !opts.cursor) {
|
|
76
|
+
throw new HelpfulError({
|
|
77
|
+
kind: "input_error",
|
|
78
|
+
message: "no agent target specified",
|
|
79
|
+
hint: "Pass --claude, --cursor, or both — e.g. `membot skill install --claude`",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const targets = computeTargets(opts);
|
|
84
|
+
for (const target of targets) {
|
|
85
|
+
const dest = join(target.dir, target.filename);
|
|
86
|
+
if (existsSync(dest) && !opts.force) {
|
|
87
|
+
throw new HelpfulError({
|
|
88
|
+
kind: "conflict",
|
|
89
|
+
message: `${dest} already exists`,
|
|
90
|
+
hint: "Re-run with --force to overwrite",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
mkdirSync(target.dir, { recursive: true });
|
|
94
|
+
writeFileSync(dest, target.content, "utf-8");
|
|
95
|
+
logger.info(`installed ${target.agentLabel} skill (${target.scopeLabel}): ${dest}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Materialise the (agent × scope) cartesian product of install targets the
|
|
101
|
+
* user asked for. Default scope is project when neither --global nor
|
|
102
|
+
* --project is passed; passing both installs to both locations.
|
|
103
|
+
*/
|
|
104
|
+
function computeTargets(opts: SkillInstallOptions): SkillTarget[] {
|
|
105
|
+
const scopes: { label: string; resolveDir: (rel: string) => string }[] = [];
|
|
106
|
+
if (opts.global) scopes.push({ label: "global", resolveDir: (rel) => join(homedir(), rel) });
|
|
107
|
+
if (opts.project || !opts.global) scopes.push({ label: "project", resolveDir: (rel) => resolve(rel) });
|
|
108
|
+
|
|
109
|
+
const targets: SkillTarget[] = [];
|
|
110
|
+
for (const scope of scopes) {
|
|
111
|
+
if (opts.claude) {
|
|
112
|
+
targets.push({
|
|
113
|
+
agentLabel: "Claude Code",
|
|
114
|
+
scopeLabel: scope.label,
|
|
115
|
+
dir: scope.resolveDir(".claude/skills"),
|
|
116
|
+
filename: "membot.md",
|
|
117
|
+
content: claudeSkill,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (opts.cursor) {
|
|
121
|
+
targets.push({
|
|
122
|
+
agentLabel: "Cursor",
|
|
123
|
+
scopeLabel: scope.label,
|
|
124
|
+
dir: scope.resolveDir(".cursor/rules"),
|
|
125
|
+
filename: "membot.mdc",
|
|
126
|
+
content: cursorRule,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return targets;
|
|
131
|
+
}
|