@geml/geml
Advanced tools
+1
-0
@@ -74,1 +74,2 @@ #!/usr/bin/env node | ||
| export declare function blockSpans(source: string): Map<string, Span>; | ||
| export declare const PARSER_VERSION: string; |
+3
-3
| #!/usr/bin/env node | ||
| export interface McpOptions { | ||
| workspace: string; | ||
| root: string; | ||
| history: boolean; | ||
@@ -8,3 +8,3 @@ } | ||
| export declare function configure(o: Partial<McpOptions>): McpOptions; | ||
| export declare function resolveInWorkspace(file: string): string; | ||
| export declare function resolveInRoot(file: string): string; | ||
| export interface Tool { | ||
@@ -18,3 +18,3 @@ name: string; | ||
| export declare function handleLine(line: string, write?: (s: string) => void): void; | ||
| export declare const MCP_USAGE = "usage: geml mcp --workspace <dir> [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).\n\n --workspace <dir> REQUIRED. Root directory holding the .geml documents.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --no-history Do not auto-commit a .gemlhistory revision before each\n write. Default is to commit, so geml_revert_block always\n has a revision to undo to.\n\n Register with a client:\n claude mcp add geml-docs -- geml mcp --workspace /abs/path/to/docs"; | ||
| export declare const MCP_USAGE = "usage: geml mcp --root <dir> [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).\n\n --root <dir> REQUIRED. Root directory holding the .geml documents.\n Relative paths resolve against the server process's CWD,\n which the CLIENT chooses \u2014 pass an absolute path.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --no-history Do not auto-commit a .gemlhistory revision before each\n write. Default is to commit, so geml_revert_block always\n has a revision to undo to.\n\n Register with a client:\n claude mcp add geml -- geml mcp --root /abs/path/to/docs"; | ||
| export declare function parseArgs(args: string[]): McpOptions; |
+60
-45
| #!/usr/bin/env node | ||
| // `geml mcp` — MCP server for GEML document CRUD. | ||
| // | ||
| // Nine tools over a confined workspace of `.geml` documents: four read-only, | ||
| // Nine tools over a confined root directory of `.geml` documents: four read-only, | ||
| // five that write. It is the document-editing counterpart to the read-only | ||
@@ -10,3 +10,3 @@ // code-graph server in `codemap/mcp-server.mjs`, and deliberately mirrors its | ||
| // | ||
| // claude mcp add geml-docs -- geml mcp --workspace /abs/path/to/docs | ||
| // claude mcp add geml -- geml mcp --root /abs/path/to/docs | ||
| // | ||
@@ -24,3 +24,3 @@ // Three invariants make this worth more than letting a model `str_replace` the | ||
| // tool in the set would have nothing to revert to. | ||
| // 3. EVERY PATH IS CONFINED to a server-side `--workspace` root the client | ||
| // 3. EVERY PATH IS CONFINED to a server-side `--root` directory the client | ||
| // cannot override or widen. | ||
@@ -37,6 +37,10 @@ // | ||
| import { createInterface } from "node:readline"; | ||
| import { parse } from "./geml.js"; | ||
| import { parse, PARSER_VERSION } from "./geml.js"; | ||
| import { commit, listRevisions, isCurrent } from "./history.js"; | ||
| const SERVER_VERSION = "0.1.0"; | ||
| let OPTS = { workspace: process.cwd(), history: true }; | ||
| // One version for the whole package: `geml --version` and the MCP handshake | ||
| // must not disagree. This used to be its own literal and had drifted to 0.1.0 | ||
| // against a 1.4.x package — invisible to everyone except the user reading their | ||
| // client's server list. | ||
| const SERVER_VERSION = PARSER_VERSION; | ||
| let OPTS = { root: process.cwd(), history: true }; | ||
| /** Configure the server. Exported so the suite can point it at a temp dir. */ | ||
@@ -51,3 +55,3 @@ export function configure(o) { | ||
| // `file` is client-supplied, so `../../../etc/passwd` — or a symlink planted | ||
| // inside the workspace that points out of it — must not resolve. Canonicalize | ||
| // inside the root that points out of it — must not resolve. Canonicalize | ||
| // BOTH sides with realpathSync (which follows every link component) and require | ||
@@ -58,6 +62,6 @@ // the real target to sit at or under the real root. Unlike the code-graph | ||
| // name its own root could write anywhere. | ||
| export function resolveInWorkspace(file) { | ||
| export function resolveInRoot(file) { | ||
| if (typeof file !== "string" || file === "") | ||
| throw new Error("`file` is required"); | ||
| const root = realpathSync(OPTS.workspace); | ||
| const root = realpathSync(OPTS.root); | ||
| const target = resolve(root, file); | ||
@@ -69,6 +73,6 @@ let real; | ||
| catch { | ||
| throw new Error(`no such file in the workspace: ${file}`); | ||
| throw new Error(`no such file under the server root: ${file}`); | ||
| } | ||
| if (real !== root && !real.startsWith(root + sep)) { | ||
| throw new Error(`path escapes the workspace: ${file}`); | ||
| throw new Error(`path escapes the server root: ${file}`); | ||
| } | ||
@@ -79,9 +83,9 @@ if (!statSync(real).isFile()) | ||
| } | ||
| // Cross-document references resolve against the workspace root, never against | ||
| // Cross-document references resolve against the SERVER root, never against | ||
| // a client-named directory: `root` may only NARROW to a directory inside it. | ||
| function resolveRoot(root) { | ||
| const ws = realpathSync(OPTS.workspace); | ||
| const serverRoot = realpathSync(OPTS.root); | ||
| if (root === undefined || root === "") | ||
| return ws; | ||
| const target = resolve(ws, root); | ||
| return serverRoot; | ||
| const target = resolve(serverRoot, root); | ||
| let real; | ||
@@ -92,6 +96,6 @@ try { | ||
| catch { | ||
| throw new Error(`no such directory in the workspace: ${root}`); | ||
| throw new Error(`no such directory under the server root: ${root}`); | ||
| } | ||
| if (real !== ws && !real.startsWith(ws + sep)) | ||
| throw new Error(`root escapes the workspace: ${root}`); | ||
| if (real !== serverRoot && !real.startsWith(serverRoot + sep)) | ||
| throw new Error(`root escapes the server root: ${root}`); | ||
| return real; | ||
@@ -141,5 +145,5 @@ } | ||
| function applyWrite(spec) { | ||
| const real = resolveInWorkspace(spec.file); | ||
| const real = resolveInRoot(spec.file); | ||
| const before = readFileSync(real, "utf8"); | ||
| const root = realpathSync(OPTS.workspace); | ||
| const root = realpathSync(OPTS.root); | ||
| const errorKey = (d) => `${d.code}:${d.message}`; | ||
@@ -218,3 +222,3 @@ const preexisting = new Set(parse(before, { resolveDoc: docResolver(root) }).diagnostics | ||
| const hashId = (id) => (id.startsWith("#") ? id : `#${id}`); | ||
| const FILE_ARG = { type: "string", description: "Document path relative to the server's --workspace root, e.g. notes/spec.geml" }; | ||
| const FILE_ARG = { type: "string", description: "Document path relative to the server's --root directory, e.g. notes/spec.geml" }; | ||
| export const TOOLS = [ | ||
@@ -227,3 +231,3 @@ // ----- read ----- | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const run = runCli(["get", real, "--json"]); | ||
@@ -247,3 +251,3 @@ if (!run.ok) | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const run = runCli(["get", real, hashId(args.id)]); | ||
@@ -262,3 +266,3 @@ if (!run.ok) | ||
| file: FILE_ARG, | ||
| root: { type: "string", description: "Directory (inside the workspace) against which cross-document references resolve. Defaults to the workspace root." }, | ||
| root: { type: "string", description: "Directory (inside the server root) against which cross-document references resolve. Defaults to the server root itself. This is a REFERENCE root and is distinct from the server's own --root sandbox, which it can only narrow." }, | ||
| }, | ||
@@ -268,3 +272,3 @@ required: ["file"], | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const root = resolveRoot(args.root); | ||
@@ -287,3 +291,3 @@ const doc = parse(readFileSync(real, "utf8"), { resolveDoc: docResolver(root) }); | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const historyPath = real.replace(/\.geml$/, "") + ".gemlhistory"; | ||
@@ -310,3 +314,3 @@ if (!existsSync(historyPath)) | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const part = args.part ?? "whole"; | ||
@@ -338,3 +342,3 @@ if (!["whole", "head", "body"].includes(part)) | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| let where; | ||
@@ -370,3 +374,3 @@ if (args.position === "append") | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| const ids = Array.isArray(args.ids) ? args.ids : [args.ids]; | ||
@@ -396,3 +400,3 @@ if (!ids.length) | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| return applyWrite({ | ||
@@ -418,3 +422,3 @@ file: args.file, | ||
| run: (args) => { | ||
| const real = resolveInWorkspace(args.file); | ||
| const real = resolveInRoot(args.file); | ||
| // Default to `--rev changed`, NOT the tip (`0`) or the CLI's own `-1`. Each | ||
@@ -457,3 +461,3 @@ // write commits the PRE-write state, so the tip undoes the block only when | ||
| capabilities: { tools: {} }, | ||
| serverInfo: { name: "geml-docs", version: SERVER_VERSION }, | ||
| serverInfo: { name: "geml", version: SERVER_VERSION }, | ||
| }); | ||
@@ -499,7 +503,9 @@ } | ||
| // --------------------------------------------------------------------------- | ||
| export const MCP_USAGE = `usage: geml mcp --workspace <dir> [--no-history] | ||
| export const MCP_USAGE = `usage: geml mcp --root <dir> [--no-history] | ||
| Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0). | ||
| --workspace <dir> REQUIRED. Root directory holding the .geml documents. | ||
| --root <dir> REQUIRED. Root directory holding the .geml documents. | ||
| Relative paths resolve against the server process's CWD, | ||
| which the CLIENT chooses — pass an absolute path. | ||
| Every path a client names is confined to this directory; | ||
@@ -512,23 +518,32 @@ a client cannot widen or override it. | ||
| Register with a client: | ||
| claude mcp add geml-docs -- geml mcp --workspace /abs/path/to/docs`; | ||
| claude mcp add geml -- geml mcp --root /abs/path/to/docs`; | ||
| export function parseArgs(args) { | ||
| let workspace; | ||
| let root; | ||
| let history = true; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const a = args[i]; | ||
| if (a === "--workspace" || a === "-w") | ||
| workspace = args[++i]; | ||
| else if (a.startsWith("--workspace=")) | ||
| workspace = a.slice("--workspace=".length); | ||
| if (a === "--root" || a === "-r") | ||
| root = args[++i]; | ||
| else if (a.startsWith("--root=")) | ||
| root = a.slice("--root=".length); | ||
| else if (a === "--no-history") | ||
| history = false; | ||
| // The flag used to be --workspace/-w. Name the replacement instead of | ||
| // failing with a bare `unknown option`: this runs inside a client's server | ||
| // config, where the only thing the user sees is that the server did not | ||
| // start, and guessing from `unknown option '--workspace'` is a bad evening. | ||
| else if (a === "--workspace" || a === "-w" || a.startsWith("--workspace=")) { | ||
| throw new Error("--workspace is now --root (same meaning: the one directory the server may read and write)"); | ||
| } | ||
| else | ||
| throw new Error(`unknown option '${a}'`); | ||
| } | ||
| if (!workspace) | ||
| throw new Error("--workspace <dir> is required (the root the server may read and write)"); | ||
| const abs = resolve(workspace); | ||
| if (!root) | ||
| throw new Error("--root <dir> is required (the one directory the server may read and write)"); | ||
| // Relative paths resolve against THIS process's cwd, which an MCP client | ||
| // picks — so they work from a shell and are a coin flip from a client config. | ||
| const abs = resolve(root); | ||
| if (!existsSync(abs) || !statSync(abs).isDirectory()) | ||
| throw new Error(`--workspace is not a directory: ${workspace}`); | ||
| return { workspace: realpathSync(abs), history }; | ||
| throw new Error(`--root is not a directory: ${root}`); | ||
| return { root: realpathSync(abs), history }; | ||
| } | ||
@@ -535,0 +550,0 @@ // Auto-run only as a MAIN module: the CLI dispatcher spawns this file as a |
+2
-1
| { | ||
| "name": "@geml/geml", | ||
| "version": "1.4.3", | ||
| "version": "1.4.4", | ||
| "mcpName": "io.github.geml-spec/geml", | ||
| "publishConfig": { | ||
@@ -5,0 +6,0 @@ "access": "public" |
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
679649
0.6%13304
0.53%