Skip to content

Commit 2f6f70f

Browse files
committed
feat: add CLI help and version flags
Adds `curion --help` / `-h` and `curion --version` / `-v` flag interception to the entrypoint so the binary prints usage / version to stdout and exits 0 before any storage or MCP transport startup. The flag handlers never create or touch the project-local `.curion/` directory and never write to stderr; normal MCP stdio behaviour for unflagged invocations is unchanged. The runtime server version now syncs from `package.json` through a generated `src/version.ts`, regenerated by `scripts/sync-version.mjs` on every `prebuild` / `pretest`. The McpServer `serverInfo.version` field is no longer hardcoded to the stale `0.2.0` literal. The `--version` CLI output and the MCP `initialize` response both reflect `package.json#version` at release time. Adds `tests/cli-flags.test.ts` with 15 unit and end-to-end subprocess tests covering: the `buildHelpText` shape and content, the `VERSION` constant matching `package.json`, `buildServer` using `VERSION` and honouring the `options.version` override, and the four flag invocations plus combined-flag argv-order precedence and a no-flag regression guard. Bumps `package.json` (and `package-lock.json`) from 0.3.5 to 0.3.6 to prep the release. The generated `src/version.ts` is gitignored. Changelog: moves the existing Unreleased CLI entry into a new `[0.3.6] - 2026-06-30` section and updates the compare links.
1 parent b098bdb commit 2f6f70f

9 files changed

Lines changed: 795 additions & 5 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
dist/
33
*.tsbuildinfo
44

5+
# Generated src/version.ts (synced from package.json by
6+
# `scripts/sync-version.mjs`; wired into prebuild/pretest).
7+
src/version.ts
8+
59
# Dependencies
610
node_modules/
711

CHANGELOG.md

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
Nothing yet.
11+
12+
## [0.3.6] - 2026-06-30
13+
14+
User-facing CLI release. Adds the two metadata flags
15+
(`--help` / `-h` and `--version` / `-v`) to the `curion` binary
16+
and replaces the previously hardcoded server version with a
17+
generated, package-synced version constant. No MCP tool surface
18+
change; no storage or controller change; no release-engineering
19+
change. The npm package name (`@geanatz/curion`), the Trusted
20+
Publishing / OIDC provenance path, and the binary name (`curion`)
21+
are unchanged.
22+
23+
### Added
24+
25+
- **`curion --help` / `-h` and `curion --version` / `-v` flag
26+
interception.** Running `curion` with any of these four flags
27+
now prints to stdout and exits 0 *before* any storage or MCP
28+
transport startup. The flag handlers never create or touch the
29+
project-local `.curion/` directory and never write to stderr
30+
(logs). Normal MCP stdio behaviour for unflagged invocations is
31+
unchanged. The help text (`src/cli-help.ts`) advertises the
32+
two public tools, the env-var configuration contract, and a
33+
docs link.
34+
- **Runtime server version now syncs from `package.json`.** The
35+
McpServer `serverInfo.version` field is no longer hardcoded to
36+
`"0.2.0"`; it is sourced from a generated `src/version.ts`
37+
constant that `scripts/sync-version.mjs` regenerates from
38+
`package.json#version` on every `prebuild` / `pretest`. A
39+
caller can still override the value through `buildServer({
40+
version })` for tests. The `--version` CLI output, the MCP
41+
`serverInfo.version` field, and the published npm tarball
42+
version are now guaranteed to match `package.json#version` at
43+
every release.
44+
- **Generated version script.** `scripts/sync-version.mjs` is a
45+
pure-Node, dependency-free script that writes
46+
`src/version.ts` (gitignored) from `package.json#version`. It
47+
is idempotent: re-running it on an in-sync tree is a no-op.
48+
The `sync-version` script is wired into `prebuild` and
49+
`pretest` so the generated file is always in sync before a
50+
build or test run.
51+
52+
### Changed
53+
54+
- **`src/server.ts` no longer hardcodes `"0.2.0"`.** The previous
55+
hardcoded `version: options.version ?? "0.2.0"` fallback has
56+
been removed in favour of `options.version ?? VERSION`, where
57+
`VERSION` is the generated constant from `src/version.ts`. This
58+
is the only source-code change to the runtime server module;
59+
the McpServer / MCP transport wiring, the tool surface, the
60+
detector, and the storage layer are all unchanged.
61+
- **No behavioural change to the MCP server.** The two tools,
62+
their `text` / `structuredContent` shapes, the
63+
`clarification_needed` contract, and the project-local
64+
`.curion/` storage shape are unchanged. The only McpServer
65+
surface that changed is `serverInfo.version`.
66+
67+
### Deprecated
68+
69+
- Nothing in this release.
70+
71+
### Removed
72+
73+
- Nothing in this release.
74+
75+
### Fixed
76+
77+
- **`serverInfo.version` no longer reports the stale `0.2.0`
78+
value.** The previous hardcoded fallback meant the MCP
79+
`initialize` response advertised a server version that lagged
80+
every release tag. The generated `VERSION` constant now keeps
81+
it in lock-step with `package.json#version`.
82+
83+
### Security
84+
85+
- Nothing in this release. Provenance attestation remains enabled
86+
for every published tarball via Trusted Publishing and
87+
`publishConfig.provenance: true`, and no long-lived `NPM_TOKEN`
88+
secret is read or required.
89+
1090
## [0.3.5] - 2026-06-30
1191

1292
Documentation-only patch release. No source-code, runtime, or
@@ -683,7 +763,8 @@ no semantic enrichment, no multi-project awareness. Lexical-only
683763
match scoring on the controller. Provider adapter for a single
684764
OpenAI-compatible endpoint.
685765

686-
[Unreleased]: https://github.com/geanatz/curion/compare/v0.3.5...HEAD
766+
[Unreleased]: https://github.com/geanatz/curion/compare/v0.3.6...HEAD
767+
[0.3.6]: https://github.com/geanatz/curion/compare/v0.3.5...v0.3.6
687768
[0.3.5]: https://github.com/geanatz/curion/compare/v0.3.4...v0.3.5
688769
[0.3.4]: https://github.com/geanatz/curion/compare/v0.3.3...v0.3.4
689770
[0.3.3]: https://github.com/geanatz/curion/compare/v0.3.2...v0.3.3

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@geanatz/curion",
3-
"version": "0.3.5",
3+
"version": "0.3.6",
44
"description": "Curion — project-local memory layer for AI agents",
55
"type": "module",
66
"license": "Apache-2.0",
@@ -38,7 +38,10 @@
3838
"access": "public"
3939
},
4040
"scripts": {
41+
"sync-version": "node scripts/sync-version.mjs",
42+
"prebuild": "npm run sync-version",
4143
"build": "tsc -p tsconfig.json",
44+
"pretest": "npm run sync-version",
4245
"prepublishOnly": "npm run build && npm test",
4346
"test": "node --import tsx --test tests/*.test.ts --test 'tests/diagnostics/*.test.ts'",
4447
"test:contracts": "node --import tsx --test tests/contracts.test.ts",

scripts/sync-version.mjs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Regenerate `src/version.ts` from `package.json`.
4+
*
5+
* The package version is the single source of truth — this script
6+
* reads `package.json#version` and writes a small generated
7+
* TypeScript module that exports a `VERSION` constant. The file is
8+
* intentionally short and dependency-free so it can be required
9+
* from both the runtime entrypoint (`src/index.ts`) and the
10+
* server-wiring module (`src/server.ts`).
11+
*
12+
* Wired into `prebuild` and `pretest` in `package.json` so the
13+
* generated file is always in sync before a build or test run.
14+
*
15+
* Idempotent: if the existing `src/version.ts` already matches the
16+
* generated content, the file is left untouched. This keeps
17+
* incremental builds and re-runs cheap and avoids touching mtime
18+
* unnecessarily.
19+
*/
20+
import fs from "node:fs";
21+
import path from "node:path";
22+
import { fileURLToPath } from "node:url";
23+
24+
const __filename = fileURLToPath(import.meta.url);
25+
const __dirname = path.dirname(__filename);
26+
const REPO_ROOT = path.resolve(__dirname, "..");
27+
const pkgPath = path.join(REPO_ROOT, "package.json");
28+
const outPath = path.join(REPO_ROOT, "src", "version.ts");
29+
30+
const pkgRaw = fs.readFileSync(pkgPath, "utf8");
31+
let pkg;
32+
try {
33+
pkg = JSON.parse(pkgRaw);
34+
} catch (err) {
35+
const msg = err instanceof Error ? err.message : String(err);
36+
// Build-time tool — stderr is fine here; this is not the curion
37+
// server runtime.
38+
process.stderr.write(`[curion] sync-version: failed to parse package.json: ${msg}\n`);
39+
process.exit(1);
40+
}
41+
const version = pkg.version;
42+
if (typeof version !== "string" || version.length === 0) {
43+
process.stderr.write(
44+
"[curion] sync-version: package.json is missing a non-empty `version` field\n"
45+
);
46+
process.exit(1);
47+
}
48+
49+
const header = [
50+
"/**",
51+
" * Generated from package.json. Do not edit by hand.",
52+
" *",
53+
" * Regenerated by `scripts/sync-version.mjs`, wired into",
54+
" * `prebuild` and `pretest` in `package.json`.",
55+
" * The source of truth is `package.json#version`.",
56+
" */",
57+
"",
58+
].join("\n");
59+
60+
// JSON.stringify produces a safely-escaped, quoted string literal
61+
// that TypeScript accepts verbatim.
62+
const body = `export const VERSION = ${JSON.stringify(version)};\n`;
63+
const newContent = header + body;
64+
65+
let existing = null;
66+
try {
67+
existing = fs.readFileSync(outPath, "utf8");
68+
} catch {
69+
// file doesn't exist yet — that's fine, we'll create it
70+
}
71+
72+
if (existing !== newContent) {
73+
fs.writeFileSync(outPath, newContent);
74+
process.stdout.write(`[curion] sync-version: wrote src/version.ts (${version})\n`);
75+
} else {
76+
process.stdout.write(`[curion] sync-version: src/version.ts already up to date (${version})\n`);
77+
}

src/cli-help.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* CLI help text for the `curion` binary.
3+
*
4+
* The `curion` binary is primarily an MCP stdio server. A user
5+
* running `curion --help` from a shell sees this help. A user
6+
* running `curion --version` sees just the version on its own
7+
* line (so it can be embedded in scripts / CI).
8+
*
9+
* This module is the single source of truth for the help text.
10+
* The flag interception in `src/index.ts` calls
11+
* `buildHelpText(VERSION)` and writes the result to stdout.
12+
*
13+
* Output conventions:
14+
* - Single trailing newline.
15+
* - Pure: no I/O. The caller decides where to write the text.
16+
* - Self-contained: a user can understand what `curion` is
17+
* from this text alone, including the env-var contract and
18+
* where to find more docs.
19+
*
20+
* The `version` argument is injected so the function stays pure
21+
* and testable without importing `./version.js`. The
22+
* entrypoint passes the runtime `VERSION` constant; tests pass
23+
* a stable fixture.
24+
*/
25+
26+
const DOCS_URL = "https://github.com/geanatz/curion#readme";
27+
28+
/**
29+
* Build the help text for `curion --help`. Pure function.
30+
*/
31+
export function buildHelpText(version: string): string {
32+
return [
33+
"curion — project-local memory layer for AI agents",
34+
"",
35+
"Usage:",
36+
" curion Start the MCP stdio server (default). Speaks",
37+
" JSON-RPC over stdin/stdout; logs travel on",
38+
" stderr. Always launch through an MCP client",
39+
" that manages the stdio transport.",
40+
"",
41+
"Options:",
42+
" -h, --help Show this help and exit.",
43+
" -v, --version Print the version and exit.",
44+
"",
45+
"Tools (over MCP):",
46+
" remember(text) Store a piece of project memory.",
47+
" recall(text) Retrieve the relevant pieces.",
48+
"",
49+
"Configuration:",
50+
" Curion is configured through environment variables passed by the",
51+
" parent process. It does not load .env files. The minimum is a",
52+
" primary provider:",
53+
" CURION_PRIMARY_API_KEY, CURION_PRIMARY_BASE_URL,",
54+
" CURION_PRIMARY_MODEL",
55+
" Optional: a fallback provider, semantic retrieval",
56+
" (CURION_SEMANTIC_ENABLED), and log level (CURION_LOG_LEVEL).",
57+
"",
58+
`Docs: ${DOCS_URL}`,
59+
"",
60+
].join("\n");
61+
}

src/index.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,76 @@
66
* travel on stderr; this file is the single place where stdio is
77
* wired up. Anything written to stdout will corrupt the MCP protocol
88
* stream, so we use the stderr-only logger throughout.
9+
*
10+
* CLI flag interception:
11+
* - `--help` / `-h` print the help text (see `src/cli-help.ts`)
12+
* to stdout and exit 0.
13+
* - `--version` / `-v` print the package version (see
14+
* `src/version.js`) to stdout and exit 0.
15+
* - Both flag paths short-circuit BEFORE `initStorage()` so the
16+
* project-local `.curion/` directory is NEVER created for a
17+
* help/version invocation. The MCP stdio transport is also
18+
* never started for these invocations, so the JSON-RPC framing
19+
* on stdout is never contaminated by help/version output.
20+
* - The flag handlers do not call the logger (stderr stays
21+
* empty) so the help/version output is the only thing the
22+
* shell sees on either stream.
923
*/
1024

1125
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26+
import { buildHelpText } from "./cli-help.js";
1227
import { logger } from "./logging/logger.js";
1328
import { PUBLIC_TOOL_NAMES, buildServer } from "./server.js";
1429
import { closeStorage, initStorage } from "./storage/storage.js";
1530
import { setStorageProvider as setRecallStorageProvider } from "./tools/recall.js";
1631
import { setStorageProvider } from "./tools/remember.js";
32+
import { VERSION } from "./version.js";
33+
34+
/**
35+
* Recognized CLI flags. Each entry is a token that, when present
36+
* in `argv`, triggers the matching handler. The order of this
37+
* table defines the "first match wins" precedence when multiple
38+
* recognized flags are passed in the same invocation.
39+
*/
40+
const HELP_FLAGS = new Set(["--help", "-h"]);
41+
const VERSION_FLAGS = new Set(["--version", "-v"]);
42+
43+
/**
44+
* Apply the help/version short-circuit. Returns `true` if a flag
45+
* was handled (and `process.exit(0)` was called); `false` if the
46+
* caller should continue with normal startup. Centralizing the
47+
* short-circuit here keeps the entrypoint flow readable and lets
48+
* the unit tests assert the helper in isolation from the rest of
49+
* `main()`.
50+
*
51+
* The function intentionally calls `process.exit(0)` directly so
52+
* no side effect from `main()` runs after a flag is matched.
53+
*/
54+
function maybeHandleCliFlags(argv: readonly string[]): boolean {
55+
for (const arg of argv) {
56+
if (HELP_FLAGS.has(arg)) {
57+
process.stdout.write(buildHelpText(VERSION));
58+
process.exit(0);
59+
}
60+
if (VERSION_FLAGS.has(arg)) {
61+
// Plain version on a single line so scripts / CI can parse it.
62+
process.stdout.write(`${VERSION}\n`);
63+
process.exit(0);
64+
}
65+
}
66+
return false;
67+
}
1768

1869
async function main(): Promise<void> {
70+
// CLI flag interception: --help / -h / --version / -v must
71+
// short-circuit BEFORE any storage side effect (`.curion/`
72+
// creation) or stdio transport setup. `maybeHandleCliFlags`
73+
// calls `process.exit(0)` directly when a recognized flag is
74+
// present, so no further code in `main()` runs for those
75+
// invocations. Normal MCP stdio behavior is unchanged when no
76+
// flag is passed.
77+
maybeHandleCliFlags(process.argv.slice(2));
78+
1979
// Initialize project-local storage. Side effect: creates .curion/ if
2080
// missing. Failures here are fatal — the server cannot run without
2181
// its data directory.

src/server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {
7171
buildRememberStructuredContent,
7272
} from "./tools/remember-projection.js";
7373
import { logger } from "./logging/logger.js";
74+
import { VERSION } from "./version.js";
7475

7576
export const PUBLIC_TOOL_NAMES = ["remember", "recall"] as const;
7677
export type PublicToolName = (typeof PUBLIC_TOOL_NAMES)[number];
@@ -98,7 +99,12 @@ export function buildServer(options: BuildServerOptions = {}): McpServer {
9899
const server = new McpServer(
99100
{
100101
name: options.name ?? "curion",
101-
version: options.version ?? "0.2.0",
102+
// The package version is the single source of truth. `VERSION`
103+
// is generated from `package.json#version` by
104+
// `scripts/sync-version.mjs`, wired into `prebuild`/`pretest`
105+
// so this value can never drift from a release tag. A caller
106+
// may still override it via `options.version` for tests.
107+
version: options.version ?? VERSION,
102108
},
103109
{
104110
capabilities: {

0 commit comments

Comments
 (0)