Add standalone executable packaging with esbuild + pkg

- Add npm scripts for building Windows/Linux/macOS executables
- Replace uuid package with crypto.randomUUID() for ESM compatibility
- Use esbuild to pre-bundle code before pkg (fixes MCP SDK subpath exports)
- Update CLI name from 'memory' to 'cortex'
- Update USAGE.md with build instructions and standalone setup
- Add bundle/ and build/ to .gitignore
This commit is contained in:
2026-02-03 09:14:26 +01:00
parent 661325a235
commit 3d5a979a1b
8 changed files with 1735 additions and 80 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules/ node_modules/
dist/ dist/
bundle/
build/
.memory/ .memory/

192
USAGE.md
View File

@@ -8,11 +8,65 @@ Data is stored in `.memory/cortex.db` (SQLite) in the current working directory.
--- ---
## Installation & Building
### Development Mode
```bash
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run CLI (development)
node dist/cli/index.js <command>
# Run MCP server (development)
node dist/mcp/index.js
```
### Standalone Executables
Build self-contained executables that don't require Node.js:
```bash
# Build for Windows (creates cortex.exe and cortex-mcp.exe)
npm run package:win
# Build for Linux
npm run package:linux
# Build for macOS
npm run package:mac
# Build all (CLI + MCP for current platform)
npm run package
```
**Output files** (in `build/` directory):
- `cortex.exe` / `cortex` — CLI tool
- `cortex-mcp.exe` / `cortex-mcp` — MCP server for Claude Code integration
### Running Standalone
```bash
# CLI
./build/cortex.exe list
./build/cortex.exe query "authentication"
./build/cortex.exe serve --port 3100
# MCP server (for Claude Code)
./build/cortex-mcp.exe
```
---
## CLI Reference ## CLI Reference
All commands are invoked as `memory <command>`. All commands are invoked as `cortex <command>` (standalone) or `node dist/cli/index.js <command>` (dev).
### `memory add <kind>` ### `cortex add <kind>`
Add a node to the knowledge graph. Add a node to the knowledge graph.
@@ -26,14 +80,14 @@ Add a node to the knowledge graph.
| `--section <section>` | no | Structured section as `"Label: body"` (repeatable) | | `--section <section>` | no | Structured section as `"Label: body"` (repeatable) |
```bash ```bash
memory add memory -t "Auth flow design" -c "OAuth2 PKCE flow for SPA" --tags auth,security --status active cortex add memory -t "Auth flow design" -c "OAuth2 PKCE flow for SPA" --tags auth,security --status active
memory add memory -t "add command" --section "Arguments: <kind> — memory, component, task, decision" --section "Options: -t title, -c content, --tags, --status" cortex add memory -t "add command" --section "Arguments: <kind> — memory, component, task, decision" --section "Options: -t title, -c content, --tags, --status"
memory add task -t "Implement login page" --status todo cortex add task -t "Implement login page" --status todo
memory add decision -t "Use PostgreSQL" -c "Chose Postgres over MySQL for JSON support" cortex add decision -t "Use PostgreSQL" -c "Chose Postgres over MySQL for JSON support"
memory add component -t "UserService" -c "Handles user CRUD operations" --tags backend cortex add component -t "UserService" -c "Handles user CRUD operations" --tags backend
``` ```
### `memory query <text>` ### `cortex query <text>`
Search the knowledge graph using natural language. Uses hybrid BM25 + vector + freshness scoring. Search the knowledge graph using natural language. Uses hybrid BM25 + vector + freshness scoring.
@@ -45,12 +99,12 @@ Search the knowledge graph using natural language. Uses hybrid BM25 + vector + f
| `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) | | `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) |
```bash ```bash
memory query "authentication" cortex query "authentication"
memory query "database decisions" --kind decision cortex query "database decisions" --kind decision
memory query "user service" --limit 5 --format json cortex query "user service" --limit 5 --format json
``` ```
### `memory show <id>` ### `cortex show <id>`
Show a node's full details, structured sections, inline children, and connections. The `<id>` can be a full UUID or a unique prefix. Show a node's full details, structured sections, inline children, and connections. The `<id>` can be a full UUID or a unique prefix.
@@ -64,11 +118,11 @@ If the node has `metadata.sections`, they render as labeled blocks. If the node
| `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) | | `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) |
```bash ```bash
memory show abc123 cortex show abc123
memory show abc123 --format json cortex show abc123 --format json
``` ```
### `memory list` ### `cortex list`
List nodes in the knowledge graph. List nodes in the knowledge graph.
@@ -82,14 +136,14 @@ List nodes in the knowledge graph.
| `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) | | `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) |
```bash ```bash
memory list cortex list
memory list --kind task --status todo cortex list --kind task --status todo
memory list --tags auth,security cortex list --tags auth,security
memory list --stale cortex list --stale
memory list --format json cortex list --format json
``` ```
### `memory update <id>` ### `cortex update <id>`
Update an existing node's fields. Update an existing node's fields.
@@ -104,13 +158,13 @@ Update an existing node's fields.
| `--section <section>` | no | Structured section as `"Label: body"` (repeatable, replaces by label) | | `--section <section>` | no | Structured section as `"Label: body"` (repeatable, replaces by label) |
```bash ```bash
memory update abc123 --status done cortex update abc123 --status done
memory update abc123 -t "Updated title" -c "New content" cortex update abc123 -t "Updated title" -c "New content"
memory update abc123 --tags newtag1,newtag2 cortex update abc123 --tags newtag1,newtag2
memory update abc123 --section "Notes: Updated implementation notes" cortex update abc123 --section "Notes: Updated implementation notes"
``` ```
### `memory remove <id>` ### `cortex remove <id>`
Remove a node. Default is soft delete (marks as stale). Use `--hard` for permanent deletion. Remove a node. Default is soft delete (marks as stale). Use `--hard` for permanent deletion.
@@ -120,11 +174,11 @@ Remove a node. Default is soft delete (marks as stale). Use `--hard` for permane
| `--hard` | no | Permanently delete instead of marking stale | | `--hard` | no | Permanently delete instead of marking stale |
```bash ```bash
memory remove abc123 cortex remove abc123
memory remove abc123 --hard cortex remove abc123 --hard
``` ```
### `memory link <fromId> <toId>` ### `cortex link <fromId> <toId>`
Create a directed edge between two nodes. Create a directed edge between two nodes.
@@ -135,11 +189,11 @@ Create a directed edge between two nodes.
| `--type <type>` | yes | Edge type: `depends_on`, `contains`, `implements`, `blocked_by`, `subtask_of`, `relates_to`, `supersedes`, `about` | | `--type <type>` | yes | Edge type: `depends_on`, `contains`, `implements`, `blocked_by`, `subtask_of`, `relates_to`, `supersedes`, `about` |
```bash ```bash
memory link abc123 def456 --type depends_on cortex link abc123 def456 --type depends_on
memory link abc123 def456 --type contains cortex link abc123 def456 --type contains
``` ```
### `memory graph [id]` ### `cortex graph [id]`
Visualize the knowledge graph as an ASCII tree. Optionally root at a specific node. Visualize the knowledge graph as an ASCII tree. Optionally root at a specific node.
@@ -148,11 +202,11 @@ Visualize the knowledge graph as an ASCII tree. Optionally root at a specific no
| `[id]` | no | Root node ID or prefix. Omit for full graph. | | `[id]` | no | Root node ID or prefix. Omit for full graph. |
```bash ```bash
memory graph cortex graph
memory graph abc123 cortex graph abc123
``` ```
### `memory children <id>` ### `cortex children <id>`
List child nodes connected via outgoing `contains` edges from the given node. List child nodes connected via outgoing `contains` edges from the given node.
@@ -163,12 +217,12 @@ List child nodes connected via outgoing `contains` edges from the given node.
| `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) | | `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) |
```bash ```bash
memory children abc123 cortex children abc123
memory children abc123 --kind task cortex children abc123 --kind task
memory children abc123 --format json cortex children abc123 --format json
``` ```
### `memory decay` ### `cortex decay`
Run auto-decay to mark old untouched nodes as stale. Nodes whose `lastAccessedAt` exceeds the threshold are marked stale and hidden from default listings and search. Run auto-decay to mark old untouched nodes as stale. Nodes whose `lastAccessedAt` exceeds the threshold are marked stale and hidden from default listings and search.
@@ -177,12 +231,12 @@ Run auto-decay to mark old untouched nodes as stale. Nodes whose `lastAccessedAt
| `--days <number>` | no | Max age in days before decay (default: 180) | | `--days <number>` | no | Max age in days before decay (default: 180) |
```bash ```bash
memory decay cortex decay
memory decay --days 90 cortex decay --days 90
memory decay --days 0 # decay all nodes not accessed today cortex decay --days 0 # decay all nodes not accessed today
``` ```
### `memory serve` ### `cortex serve`
Start the Cortex Portal web server. Auto-decay runs on startup and every 24 hours. Start the Cortex Portal web server. Auto-decay runs on startup and every 24 hours.
@@ -191,8 +245,8 @@ Start the Cortex Portal web server. Auto-decay runs on startup and every 24 hour
| `-p, --port <number>` | no | Port number (default: 3100) | | `-p, --port <number>` | no | Port number (default: 3100) |
```bash ```bash
memory serve cortex serve
memory serve --port 8080 cortex serve --port 8080
``` ```
--- ---
@@ -223,7 +277,7 @@ memory serve --port 8080
Nodes can have structured content via `metadata.sections`. Each section has a `label` and `body`. Use the `--section "Label: body"` flag on `add` or `update` to create sections. Nodes can have structured content via `metadata.sections`. Each section has a `label` and `body`. Use the `--section "Label: body"` flag on `add` or `update` to create sections.
When displayed with `memory show`, sections render as: When displayed with `cortex show`, sections render as:
``` ```
── Arguments ── ── Arguments ──
@@ -233,7 +287,7 @@ When displayed with `memory show`, sections render as:
-t title, -c content, --tags, --status -t title, -c content, --tags, --status
``` ```
The `show` command also displays inline children (nodes linked via `contains` edges). Use `memory children <id>` to list only children. The `show` command also displays inline children (nodes linked via `contains` edges). Use `cortex children <id>` to list only children.
## Search Scoring ## Search Scoring
@@ -267,7 +321,7 @@ The web server exposes these endpoints under `/api`:
Cortex includes an MCP (Model Context Protocol) server that exposes memory tools directly to Claude Code. Cortex includes an MCP (Model Context Protocol) server that exposes memory tools directly to Claude Code.
### Setup ### Setup (Development)
1. Build the project: `npm run build` 1. Build the project: `npm run build`
2. Ensure `.mcp.json` exists in the project root: 2. Ensure `.mcp.json` exists in the project root:
@@ -285,6 +339,35 @@ Cortex includes an MCP (Model Context Protocol) server that exposes memory tools
3. Restart Claude Code — the memory tools will appear automatically. 3. Restart Claude Code — the memory tools will appear automatically.
### Setup (Standalone Executable)
1. Build the MCP server: `npm run package:win` (or `package:linux`/`package:mac`)
2. Configure `.mcp.json` to use the standalone executable:
```json
{
"mcpServers": {
"memory": {
"command": "./build/cortex-mcp.exe"
}
}
}
```
Or with an absolute path for use from any directory:
```json
{
"mcpServers": {
"memory": {
"command": "C:/path/to/cortex-mcp.exe"
}
}
}
```
3. Restart Claude Code — the memory tools will appear automatically.
### MCP Tools ### MCP Tools
| Tool | Description | Parameters | | Tool | Description | Parameters |
@@ -295,9 +378,22 @@ Cortex includes an MCP (Model Context Protocol) server that exposes memory tools
| `memory_children` | List children of a node | `id`, `kind?` | | `memory_children` | List children of a node | `id`, `kind?` |
| `memory_add` | Add a new node | `kind`, `title`, `content?`, `tags?`, `status?`, `sections?` | | `memory_add` | Add a new node | `kind`, `title`, `content?`, `tags?`, `status?`, `sections?` |
| `memory_link` | Create an edge between nodes | `fromId`, `toId`, `type` | | `memory_link` | Create an edge between nodes | `fromId`, `toId`, `type` |
| `memory_split` | Break a large node into smaller children | `id`, `pieces`, `summary?` |
| `memory_merge` | Merge multiple nodes into one | `nodeIds`, `title`, `content`, `kind?` |
| `memory_dedupe` | Find similar/duplicate nodes | `threshold?`, `kind?`, `limit?` |
| `memory_prune` | Clean up stale nodes or orphans | `mode`, `maxAgeDays?` |
| `memory_reorganize` | Move a node under a new parent | `nodeId`, `newParentId` |
| `memory_bulk_tag` | Add/remove tags on multiple nodes | `action`, `tags`, `nodeIds?`, `filter?` |
| `memory_stats` | Get graph statistics | — |
| `memory_summary` | Get hierarchical summary of the graph | `refresh?` |
| `memory_prompt` | Execute natural language instruction | `prompt` |
### Manual Testing ### Manual Testing
```bash ```bash
# Development
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | node dist/mcp/index.js echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | node dist/mcp/index.js
# Standalone
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | ./build/cortex-mcp.exe
``` ```

1548
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,37 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"dev": "tsc --watch", "dev": "tsc --watch",
"serve": "node dist/server/index.js" "serve": "node dist/server/index.js",
"bundle": "npm run build && npm run bundle:cli && npm run bundle:mcp",
"bundle:cli": "esbuild dist/cli/index.js --bundle --platform=node --outfile=bundle/cli.js --external:better-sqlite3",
"bundle:mcp": "esbuild dist/mcp/index.js --bundle --platform=node --outfile=bundle/mcp.js --external:better-sqlite3",
"package": "npm run bundle && npm run package:cli && npm run package:mcp",
"package:cli": "pkg ./bundle/cli.js --targets node22-win-x64 --output build/cortex.exe --config package.json",
"package:mcp": "pkg ./bundle/mcp.js --targets node22-win-x64 --output build/cortex-mcp.exe --config package.json",
"package:win": "npm run bundle && npm run package:cli && npm run package:mcp",
"package:linux": "npm run bundle && pkg ./bundle/cli.js --targets node22-linux-x64 --output build/cortex --config package.json && pkg ./bundle/mcp.js --targets node22-linux-x64 --output build/cortex-mcp --config package.json",
"package:mac": "npm run bundle && pkg ./bundle/cli.js --targets node22-macos-x64 --output build/cortex --config package.json && pkg ./bundle/mcp.js --targets node22-macos-x64 --output build/cortex-mcp --config package.json"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "AI project memory - knowledge graph backed by SQLite + Ollama embeddings", "description": "AI project memory - knowledge graph backed by SQLite + Ollama embeddings",
"bin": { "bin": {
"memory": "./dist/cli/index.js", "cortex": "./dist/cli/index.js",
"memory-mcp": "./dist/mcp/index.js" "cortex-mcp": "./dist/mcp/index.js"
},
"pkg": {
"targets": [
"node22-win-x64"
],
"outputPath": "build",
"assets": [
"node_modules/better-sqlite3/build/Release/*.node",
"node_modules/better-sqlite3/prebuilds/**/*"
],
"scripts": [
"dist/**/*.js"
]
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.25.3", "@modelcontextprotocol/sdk": "^1.25.3",
@@ -22,7 +44,6 @@
"commander": "^14.0.3", "commander": "^14.0.3",
"cors": "^2.8.6", "cors": "^2.8.6",
"express": "^5.2.1", "express": "^5.2.1",
"uuid": "^13.0.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -30,7 +51,8 @@
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^25.2.0", "@types/node": "^25.2.0",
"@types/uuid": "^10.0.0", "@yao-pkg/pkg": "^6.3.0",
"esbuild": "^0.27.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

View File

@@ -16,8 +16,8 @@ import { closeDb } from '../core/db';
const program = new Command(); const program = new Command();
program program
.name('memory') .name('cortex')
.description('Cortex — AI project memory & knowledge graph') .description('Cortex — AI project memory & knowledge graph\n\nStore, link, and search project knowledge as a graph of typed nodes.')
.version('1.0.0'); .version('1.0.0');
program.addCommand(addCommand); program.addCommand(addCommand);

View File

@@ -1,4 +1,4 @@
import { v4 as uuid } from 'uuid'; import { randomUUID as uuid } from 'crypto';
import { getDb } from './db'; import { getDb } from './db';
import { Node, Edge, AddNodeInput, UpdateNodeInput, ListOptions, QueryOptions, SearchResult, EdgeType } from '../types'; import { Node, Edge, AddNodeInput, UpdateNodeInput, ListOptions, QueryOptions, SearchResult, EdgeType } from '../types';
import { hybridSearch, deserializeEmbedding } from './search/index'; import { hybridSearch, deserializeEmbedding } from './search/index';

View File

@@ -367,6 +367,24 @@ server.tool(
} }
); );
// --- memory_summary ---
import { getCachedSummary, generateSummary } from '../core/summary';
server.tool(
'memory_summary',
'Get a pre-computed hierarchical summary of the memory graph. Use this instead of memory_list to reduce context usage.',
{
refresh: z.boolean().optional().describe('Force regenerate summary (default: use cached)'),
},
async ({ refresh }) => {
let summary = refresh ? null : getCachedSummary();
if (!summary) {
summary = await generateSummary();
}
return { content: [{ type: 'text' as const, text: serialize(summary) }] };
}
);
// --- memory_prompt --- // --- memory_prompt ---
import { interpretAndExecute } from '../core/prompt/interpreter'; import { interpretAndExecute } from '../core/prompt/interpreter';

View File

@@ -2,6 +2,7 @@ import { getDb } from '../core/db';
import { deserializeEmbedding } from '../core/search/index'; import { deserializeEmbedding } from '../core/search/index';
import { cosineSimilarity } from '../core/search/vector'; import { cosineSimilarity } from '../core/search/vector';
import { isGenAvailable, generate } from '../core/search/ollamaGen'; import { isGenAvailable, generate } from '../core/search/ollamaGen';
import { generateSummary } from '../core/summary';
let dirty = false; let dirty = false;
@@ -194,7 +195,7 @@ Existing tags: ${tagVocab.join(', ')}`;
if (orphans.length > 0 && nonOrphans.length > 0) { if (orphans.length > 0 && nonOrphans.length > 0) {
const insertEdge = db.prepare('INSERT INTO edges (id, from_id, to_id, type, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)'); const insertEdge = db.prepare('INSERT INTO edges (id, from_id, to_id, type, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)');
const { v4: uuidv4 } = require('uuid'); const { randomUUID: uuidv4 } = require('crypto');
if (aiAvailable) { if (aiAvailable) {
for (const orphan of orphans) { for (const orphan of orphans) {
@@ -265,7 +266,7 @@ Content: ${node.content.slice(0, 2000)}`;
// Auto-split: content > 2000 chars // Auto-split: content > 2000 chars
const hugeNodes = nodes.filter(n => n.content.length > 2000 && !staleIds.has(n.id)); const hugeNodes = nodes.filter(n => n.content.length > 2000 && !staleIds.has(n.id));
const { v4: uuidv4 } = require('uuid'); const { randomUUID: uuidv4Split } = require('crypto');
const insertNode = db.prepare(`INSERT INTO nodes (id, kind, title, content, status, tags, metadata, embedding, created_at, updated_at, last_accessed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); const insertNode = db.prepare(`INSERT INTO nodes (id, kind, title, content, status, tags, metadata, embedding, created_at, updated_at, last_accessed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insertEdge2 = db.prepare('INSERT INTO edges (id, from_id, to_id, type, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)'); const insertEdge2 = db.prepare('INSERT INTO edges (id, from_id, to_id, type, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)');
@@ -289,10 +290,10 @@ Content: ${node.content.slice(0, 3000)}`;
const titleMatch = section.match(/Title:\s*(.+)/); const titleMatch = section.match(/Title:\s*(.+)/);
const contentMatch = section.match(/Content:\s*([\s\S]+)/); const contentMatch = section.match(/Content:\s*([\s\S]+)/);
if (titleMatch && contentMatch) { if (titleMatch && contentMatch) {
const childId = uuidv4(); const childId = uuidv4Split();
insertNode.run(childId, node.kind, titleMatch[1].trim(), contentMatch[1].trim(), insertNode.run(childId, node.kind, titleMatch[1].trim(), contentMatch[1].trim(),
null, JSON.stringify(node.tags), JSON.stringify({}), null, now, now, now); null, JSON.stringify(node.tags), JSON.stringify({}), null, now, now, now);
insertEdge2.run(uuidv4(), node.id, childId, 'contains', '{}', now); insertEdge2.run(uuidv4Split(), node.id, childId, 'contains', '{}', now);
childIds.push(childId); childIds.push(childId);
} }
} }
@@ -334,6 +335,14 @@ Content: ${node.content.slice(0, 300)}`;
const pruneResult = db.prepare('DELETE FROM nodes WHERE is_stale = 1 AND updated_at < ?').run(thirtyDaysAgo); const pruneResult = db.prepare('DELETE FROM nodes WHERE is_stale = 1 AND updated_at < ?').run(thirtyDaysAgo);
pruned = pruneResult.changes; pruned = pruneResult.changes;
// Regenerate summary cache
try {
await generateSummary();
console.log('[Heartbeat] Summary cache regenerated');
} catch (err) {
console.error('[Heartbeat] Failed to regenerate summary:', err);
}
const report: HeartbeatReport = { const report: HeartbeatReport = {
ranAt: now, deduped, autoTagged, autoOrganized, pruned, ranAt: now, deduped, autoTagged, autoOrganized, pruned,
summarized, merged, split: splitCount, archived, summarized, merged, split: splitCount, archived,