Sign In

mindswap

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mindswap - npm Package Compare versions

Comparing version
3.2.1
to
3.2.2
+21
server.json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.shiporbleed/mindswap",
"title": "mindswap",
"description": "Your AI's black box recorder. Auto-track project state so any AI tool picks up where the last one stopped.",
"repository": {
"url": "https://github.com/ShipOrBleed/mindswap.git",
"source": "github"
},
"version": "3.2.1",
"packages": [
{
"registryType": "npm",
"identifier": "mindswap",
"version": "3.2.1",
"transport": {
"type": "stdio"
}
}
]
}
const fs = require('fs');
const path = require('path');
const SERVER_FILE = 'server.json';
function getServerJsonPath(projectRoot) {
return path.join(projectRoot, SERVER_FILE);
}
function getRegistryName(packageJson) {
return packageJson.mcpName || null;
}
function buildRegistryManifest(packageJson, options = {}) {
const name = getRegistryName(packageJson);
if (!name) {
throw new Error('package.json is missing mcpName');
}
const version = packageJson.version;
const repositoryUrl = typeof packageJson.repository === 'object'
? packageJson.repository.url
: packageJson.repository;
const manifest = {
$schema: 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json',
name,
title: options.title || humanizeName(packageJson.name),
description: options.description || packageJson.description || '',
repository: repositoryUrl ? {
url: normalizeRepositoryUrl(repositoryUrl),
source: 'github',
} : undefined,
version,
packages: [{
registryType: 'npm',
identifier: packageJson.name,
version,
transport: {
type: 'stdio',
},
}],
};
if (options.remoteUrl) {
manifest.remotes = [{
type: 'streamable-http',
url: options.remoteUrl,
}];
}
return stripUndefined(manifest);
}
function writeRegistryManifest(projectRoot, packageJson, options = {}) {
const manifest = buildRegistryManifest(packageJson, options);
const filePath = getServerJsonPath(projectRoot);
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
return { filePath, manifest };
}
function readRegistryManifest(projectRoot) {
const filePath = getServerJsonPath(projectRoot);
if (!fs.existsSync(filePath)) return null;
try {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch {
return null;
}
}
function validateRegistryMetadata(packageJson, manifest, options = {}) {
const issues = [];
if (!packageJson.mcpName) {
issues.push('package.json is missing mcpName');
}
if (!manifest) {
issues.push('server.json is missing');
return issues;
}
if (manifest.name !== packageJson.mcpName) {
issues.push('server.json name does not match package.json mcpName');
}
if (manifest.version !== packageJson.version) {
issues.push('server.json version does not match package.json version');
}
const npmPackage = manifest.packages?.find(item => item.registryType === 'npm');
if (!npmPackage) {
issues.push('server.json is missing an npm package entry');
} else {
if (npmPackage.identifier !== packageJson.name) {
issues.push('server.json npm identifier does not match package.json name');
}
if (npmPackage.version !== packageJson.version) {
issues.push('server.json npm version does not match package.json version');
}
}
if (options.requireRemote && !manifest.remotes?.length) {
issues.push('server.json is missing a remote transport entry');
}
return issues;
}
function buildRegistryReport(packageJson, manifest, options = {}) {
const issues = validateRegistryMetadata(packageJson, manifest, options);
const ready = issues.length === 0;
return {
ready,
issues,
manifest,
package: {
name: packageJson.name,
version: packageJson.version,
mcpName: packageJson.mcpName || null,
},
checklist: ready ? [
'package.json mcpName matches server.json',
'server.json version matches package version',
'npm package is published publicly',
'mcp-publisher login github completed',
'mcp-publisher publish can run',
] : [],
};
}
function humanizeName(name) {
return String(name || 'mindswap')
.replace(/^@[^/]+\//, '')
.replace(/[-_]+/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
}
function normalizeRepositoryUrl(url) {
return String(url || '').replace(/^git\+/, '');
}
function stripUndefined(obj) {
if (Array.isArray(obj)) {
return obj.map(stripUndefined);
}
if (!obj || typeof obj !== 'object') return obj;
return Object.fromEntries(Object.entries(obj)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => [key, stripUndefined(value)]));
}
module.exports = {
SERVER_FILE,
getServerJsonPath,
buildRegistryManifest,
writeRegistryManifest,
readRegistryManifest,
validateRegistryMetadata,
buildRegistryReport,
};
+122
-1

@@ -20,6 +20,7 @@ #!/usr/bin/env node

const { sync } = require('../src/sync');
const { manageMemory, startMCPServer, startMCPHttpServer } = require('../src/mcp-server');
const { save } = require('../src/save');
const { pr } = require('../src/pr');
const { startMCPServer } = require('../src/mcp-server');
const { doctor } = require('../src/doctor');
const { buildRegistryReport, readRegistryManifest, writeRegistryManifest } = require('../src/registry');

@@ -108,2 +109,50 @@ const program = new Command();

// ─── memory ───
program
.command('memory <action> [id] [message...]')
.description('Manage structured memory items. Actions: list, get, add, update, resolve, archive, delete')
.option('-t, --type <type>', 'Memory type')
.option('--tag <tag>', 'Tag filter or value')
.option('--status <status>', 'Status filter or value')
.option('--author <author>', 'Author filter or value')
.option('--source <source>', 'Source filter or value')
.option('--limit <limit>', 'Limit results', '20')
.option('--after <iso>', 'Created after timestamp')
.option('--before <iso>', 'Created before timestamp')
.option('--hard', 'Permanently delete instead of archiving')
.option('--json', 'Output as JSON')
.action(async (action, id, messageParts, opts) => {
try {
const message = action === 'add'
? [id, ...(Array.isArray(messageParts) ? messageParts : [messageParts])].filter(Boolean).join(' ').trim()
: (Array.isArray(messageParts) ? messageParts.join(' ').trim() : String(messageParts || '').trim());
const result = manageMemory(process.cwd(), {
action,
id: action === 'add' ? undefined : id,
message,
type: opts.type,
tag: opts.tag,
status: opts.status,
author: opts.author,
source: opts.source,
limit: opts.limit,
created_after: opts.after,
created_before: opts.before,
hard: opts.hard,
json: opts.json,
});
if (result?.content?.length) {
for (const item of result.content) {
if (item?.type === 'text' && item.text) {
process.stdout.write(`${item.text}\n`);
}
}
}
if (result?.exitCode) process.exitCode = result.exitCode;
} catch (err) {
console.error(chalk.red('Error:'), err.message);
process.exit(1);
}
});
// ─── status ───

@@ -339,2 +388,74 @@ program

// ─── mcp-http ───
program
.command('mcp-http')
.description('Start mindswap as a remote MCP server over Streamable HTTP.')
.option('--host <host>', 'Host to bind to', '127.0.0.1')
.option('--port <port>', 'Port to listen on', '3000')
.option('--path <path>', 'MCP endpoint path', '/mcp')
.option('--token <token>', 'Bearer token required for requests')
.option('--origin <origin>', 'CORS allow-origin header', '*')
.action(async (opts) => {
try {
const httpServer = await startMCPHttpServer({
host: opts.host,
port: opts.port,
path: opts.path,
token: opts.token,
origin: opts.origin,
});
process.stdout.write(`mindswap MCP HTTP listening on ${httpServer.url}\n`);
} catch (err) {
process.stderr.write(`mindswap MCP error: ${err.message}\n`);
process.exit(1);
}
});
// ─── registry ───
program
.command('registry')
.description('Validate and generate MCP Registry metadata for this package.')
.option('--write', 'Write or update server.json from package metadata')
.option('--remote-url <url>', 'Include a remote Streamable HTTP endpoint in server.json')
.option('--json', 'Output as JSON')
.action(async (opts) => {
try {
const packageJson = pkg;
const projectRoot = process.cwd();
let manifest = readRegistryManifest(projectRoot);
if (opts.write || !manifest) {
const written = writeRegistryManifest(projectRoot, packageJson, {
remoteUrl: opts.remoteUrl,
});
manifest = written.manifest;
}
const report = buildRegistryReport(packageJson, manifest, {
requireRemote: Boolean(opts.remoteUrl),
});
if (opts.json) {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return;
}
process.stdout.write(`\nMCP Registry\n`);
process.stdout.write(` package: ${report.package.name}@${report.package.version}\n`);
process.stdout.write(` mcpName: ${report.package.mcpName || 'missing'}\n`);
process.stdout.write(` server.json: ${report.ready ? 'ready' : 'needs attention'}\n`);
if (report.issues.length) {
process.stdout.write(` issues:\n`);
for (const issue of report.issues) {
process.stdout.write(` - ${issue}\n`);
}
} else {
process.stdout.write(` checklist:\n`);
for (const item of report.checklist) {
process.stdout.write(` - ${item}\n`);
}
}
process.stdout.write(`\nNext: mcp-publisher login github && mcp-publisher publish\n`);
} catch (err) {
console.error(chalk.red('Error:'), err.message);
process.exit(1);
}
});
// ─── mcp install ───

@@ -341,0 +462,0 @@ program

+3
-1
{
"name": "mindswap",
"version": "3.2.1",
"version": "3.2.2",
"mcpName": "io.github.shiporbleed/mindswap",
"description": "Your AI's black box recorder. Auto-track project state so any AI tool picks up where the last one stopped.",

@@ -13,2 +14,3 @@ "main": "src/index.js",

"assets/",
"server.json",
"README.md",

@@ -15,0 +17,0 @@ "LICENSE"

+38
-198

@@ -6,226 +6,66 @@ # mindswap

**Your AI's black box recorder.** CLI + MCP server.
Keep project context in the repo so AI tools can continue work without re-explaining the same codebase.
One command captures your entire project state. Switch between Claude Code, Cursor, Copilot, Codex — the next AI picks up instantly. Zero re-explaining.
## Why it exists
<p align="center">
<img src="assets/demo.svg" alt="mindswap demo" width="680">
</p>
AI sessions reset too often. mindswap saves the current task, decisions, blockers, and handoff context so the next tool can pick up cleanly.
## Install
```bash
npm install mindswap --save-dev
npx mindswap init # once — auto-detects everything
npx mindswap ask "Why did we choose JWT?" # answer from project memory with cited matches
npx mindswap # save state when switching tools
npx mindswap doctor # diagnose setup, context freshness, and gaps
npx mindswap mcp-install # enable MCP for Claude Code / Cursor
```
## The problem
## Quick start
You're mid-feature in Codex. Tokens run out. You switch to Claude Code. It has **zero context** — doesn't know your architecture, your decisions, or that you're halfway through implementing auth middleware.
You spend 20 minutes re-explaining. Every. Single. Time.
## The solution
Just run `mindswap`. That's it.
```bash
$ npx mindswap
⚡ Saving project state...
Task: user auth (auto-detected from branch)
Branch: feat/user-auth
Changed: 4 files
Tests: ✓ 12 passed, 0 failed
Decisions: 2 auto-logged from deps
✓ State saved — ready to switch tools
```
It auto-detects your task from the branch name, captures git state, logs dependency changes as decisions, and generates context files for **every AI tool**:
| AI Tool | Generated File | Behavior |
|---------|---------------|----------|
| Universal | `HANDOFF.md` | Full overwrite |
| Claude Code | `CLAUDE.md` | Safe merge |
| Cursor | `.cursor/rules/mindswap-context.mdc` | Own file |
| GitHub Copilot | `.github/copilot-instructions.md` | Safe merge |
| Codex | `CODEX.md` | Safe merge |
| Gemini CLI | `GEMINI.md` | Safe merge |
| Windsurf | `.windsurfrules` | Own file |
| Cline | `.cline/mindswap-context.md` | Own file |
| Roo Code | `.roo/rules/mindswap-context.md` | Own file |
| Aider | `CONVENTIONS.md` | Safe merge |
| Amp | `.amp/mindswap-context.md` | Own file |
| AGENTS.md | `AGENTS.md` | Safe merge |
## The entire flow
```bash
npx mindswap init # once per project
npx mindswap resume # get an action-oriented start-of-session briefing
npx mindswap doctor # sanity-check setup and context health
npx mindswap # when switching tools
npx mindswap done # when feature is complete
```
Everything else is automatic — git hooks track commits, dependencies are auto-logged, branch state is auto-managed.
## 15 commands
| Command | Alias | What it does |
|---------|-------|-------------|
| `mindswap` | `save` | **THE one command.** Auto-detects task, deps, state — generates all context files |
| `mindswap init` | — | Initialize. Auto-detects 30+ frameworks, imports existing AI context files |
| `mindswap switch <tool>` | `sw` | One-command tool switch — save + generate + open (cursor/claude/copilot/codex/windsurf) |
| `mindswap done [msg]` | `d` | Mark task complete, archive to history, reset to idle |
| `mindswap log <msg>` | `l` | Log a memory item. Decisions warn on conflicts; use `--type` for blockers, assumptions, questions, and resolutions |
| `mindswap status` | `s` | Current state — task, branch, build/test, conflicts. `--stats` for charts |
| `mindswap doctor` | — | Diagnose setup, hook health, stale context files, conflicts, and missing continuity signals. `--json` for automation |
| `mindswap resume` | — | Action-oriented briefing — state, blockers, and the next best move. `--compact` / `--json` |
| `mindswap ask <question>` | — | Semantic question answering from project memory and history. `--json` for machine use |
| `mindswap contracts` | — | Emit machine-readable interface contracts for the current workstream |
| `mindswap sync` | — | Push, pull, or inspect shared hub state. `--push`, `--pull`, `--force`, `--hub` |
| `mindswap summary` | `sum` | Full session narrative — task, commits, decisions, conflicts. `--json` for scripts |
| `mindswap gen --all` | `gen` | Generate context files for all AI tools. Safe merge — never overwrites |
| `mindswap watch` | `w` | Background watcher — auto-updates HANDOFF.md, or all context files with `--all`; `--save` runs a full save cycle |
| `mindswap reset` | `r` | Clear task state. Decisions preserved. `--full` to clear everything |
## Key features
### Auto-everything
- **Task detection** — from branch name (`feat/user-auth` → "user auth") + recent commits
- **Dependency tracking** — added Stripe? Auto-logged. Removed Redis? Logged too. Works across JS/TS, Python, Go, Rust, and Ruby manifests.
- **Git hooks** — auto-saves state on every commit
### Branch-aware state
Each git branch has its own state. Switch to `feat/payments` — it loads that branch's task and decisions. Switch back to `main` — your main state is restored.
### Team mode
Set `MINDSWAP_TEAM=1` to make history author-aware and surface a team handoff section in generated context files.
### Native session normalization
mindswap reads recent Claude Code and Codex session files, normalizes them into a structured model, and surfaces the last session's findings, blockers, and edited files in `HANDOFF.md` and MCP context.
### Decision conflict detection
Log "NOT using Redis" then later "using Redis"? mindswap warns you. Also catches reversed choices and package.json contradictions.
### Architectural guardrails
When your diff touches code that contradicts a recorded decision, mindswap warns you in `save`, `doctor`, generated handoff files, and MCP context. It is a proactive drift check, not just a static conflict log.
### Interface contracts
`mindswap contracts` emits a machine-readable JSON contract for the active workstream, including boundaries, blockers, assumptions, and recent history so another agent can resume without re-reading the whole repo.
### Shared sync
`mindswap sync` can push or pull a local JSON hub file so teams can share continuity state across machines. Conflicts are explicit, and `doctor` surfaces the sync health.
### Structured memory
Not everything is a decision. mindswap now keeps structured memory for blockers, assumptions, open questions, and resolutions in `.mindswap/memory.json`, while keeping `decisions.log` for compatibility and conflict checks.
```bash
npx mindswap log "Need prod webhook secret" --type blocker
npx mindswap log "Assume single-region rollout for MVP" --type assumption
npx mindswap log "Should we rotate refresh tokens?" --type question
npx mindswap log "Moved to JWT after auth review" --type resolution
```
Generated context files surface unresolved blockers and questions separately so the next AI does not have to infer them from free-form notes.
### Continuity diagnostics
```bash
npx mindswap init
npx mindswap
npx mindswap doctor
```
Checks whether mindswap is initialized correctly, whether generated handoff files are stale, whether git hooks are installed, whether AI-tool-specific context files are missing, and whether conflicts or weak continuity signals need attention.
### Semantic ask
```bash
npx mindswap resume
npx mindswap ask "Why did we choose JWT?"
```
Answers natural-language questions against decisions, history, memory, and recent session context, then cites the strongest matching project records.
### Safe merge
Already have a CLAUDE.md? mindswap appends its section inside `<!-- mindswap:start/end -->` markers. Your content is never touched.
## What it gives you
### Build/test tracking
```bash
npx mindswap --check # runs tests, captures results
# Tests: ✓ 47 passed, 0 failed
```
The next AI knows "tests were passing" or exactly what's broken.
- `init` to set up a repo and import existing AI context
- `save` to capture the current task, git state, and decisions
- `doctor` to check setup health and stale context
- `resume` to start with a clean briefing
- `ask` to search project memory with citations
- `memory` to manage blockers, assumptions, questions, and resolutions
- `sync` to share continuity state across machines
- `mcp` and `mcp-http` to expose the same context to AI clients
### Auto-sync
`mindswap switch` records session start/end hooks when configured, and `mindswap watch --save --all` can run a full save + context refresh loop for deeper IDE/tool integration.
## MCP and AI tools
### 30+ frameworks detected
Next.js, Remix, Astro, SolidJS, Angular, NestJS, Express, Fastify, Hono, Django, FastAPI, Flask, Gin, Echo, GoFr, Fiber, Actix, Axum, Rails, Spring Boot, and more. Plus databases, monorepo tools, CI/CD, and infrastructure.
mindswap generates context for tools like Claude Code, Cursor, Copilot, Codex, Windsurf, Cline, Roo, Aider, Amp, Gemini CLI, and AGENTS.md-based workflows.
## What lives in `.mindswap/`
```
.mindswap/
├── HANDOFF.md ← any AI reads this
├── state.json ← machine-readable state
├── decisions.log ← decision log (kept for compatibility + conflicts)
├── memory.json ← structured memory: blockers, assumptions, questions, resolutions
├── config.json ← your preferences
├── branches/ ← per-branch state (auto)
└── history/ ← checkpoint timeline
```
**Commit these** (handoff context): `state.json`, `decisions.log`, `memory.json`, `config.json`, `HANDOFF.md`
**Don't commit** (auto-added to .gitignore): `history/`, `branches/`
## MCP Server
AI tools can query mindswap natively via [Model Context Protocol](https://modelcontextprotocol.io/) instead of reading static files. 3 tools, stdio transport.
```bash
npx mindswap mcp-install # auto-configures Claude Code, Cursor, VS Code
npx mindswap mcp-install
npx mindswap mcp-http
```
| MCP Tool | When AI calls it | What it returns |
|----------|-----------------|-----------------|
| `mindswap_get_context` | Session start — "What do I need to know?" | Synthesized briefing: task, decisions, conflicts, tests, recent work, native session findings |
| `mindswap_save_context` | Session end — "Here's what I did" | Persists summary, decisions, next steps, blockers |
| `mindswap_search` | Mid-session — "What did we decide about auth?" | Searches decisions + history + state |
## Project state
Only 3 tools by design. [Research shows](https://dev.to/aws-heroes/mcp-tool-design-why-your-ai-agent-is-failing-and-how-to-fix-it-40fc) AI accuracy drops from 82% to 73% past 20 tools. We chose quality over quantity.
The main repo data lives in `.mindswap/`:
## Security
All generated context files are scanned for secrets before writing:
- 25+ patterns: AWS keys, GitHub tokens, Stripe keys, OpenAI keys, DB URLs, private keys, JWT secrets, passwords
- Auto-redacted — secrets never reach your HANDOFF.md
- Placeholder-aware — skips `YOUR_KEY_HERE` patterns
## PR Integration
```bash
npx mindswap pr # adds context summary to your GitHub PR
```text
.mindswap/
├── HANDOFF.md
├── state.json
├── decisions.log
├── memory.json
├── config.json
├── branches/
└── history/
```
Auto-injects task, decisions, test status into the PR description with safe markers.
## npm package
## FAQ
- npm: https://www.npmjs.com/package/mindswap
- GitHub: https://github.com/ShipOrBleed/mindswap
**Will it overwrite my existing CLAUDE.md?**
No. Uses `<!-- mindswap:start/end -->` markers. Your content is preserved.
## Use it
**Does it work with my AI tool?**
If it reads markdown files (all of them do), yes.
**Does it slow things down?**
No. Only runs when you call it. Git hook runs silently on commit.
**Multiple branches?**
State is auto per-branch. Switch branches, state switches too.
**What languages?**
JS/TS, Python, Go, Rust, Ruby, Java/Kotlin. Auto-detects from project files.
## License
MIT
If mindswap helps your workflow, star the repo and keep improving the handoff loop.

@@ -28,3 +28,5 @@ const { init } = require('./init');

const { pr } = require('./pr');
const { readMemory, appendMemoryItem, getMemoryItems } = require('./memory');
const { createMCPServer, startMCPServer, startMCPHttpServer, manageMemory } = require('./mcp-server');
const { buildRegistryManifest, writeRegistryManifest, readRegistryManifest, validateRegistryMetadata, buildRegistryReport } = require('./registry');
const { readMemory, appendMemoryItem, getMemoryItems, getMemoryItemById, updateMemoryItem, resolveMemoryItem, archiveMemoryItem, deleteMemoryItem, listMemoryItems } = require('./memory');
const { doctor } = require('./doctor');

@@ -72,6 +74,21 @@

pr,
createMCPServer,
startMCPServer,
startMCPHttpServer,
manageMemory,
buildRegistryManifest,
writeRegistryManifest,
readRegistryManifest,
validateRegistryMetadata,
buildRegistryReport,
readMemory,
appendMemoryItem,
getMemoryItems,
getMemoryItemById,
updateMemoryItem,
resolveMemoryItem,
archiveMemoryItem,
deleteMemoryItem,
listMemoryItems,
doctor,
};
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const http = require('http');
const { randomUUID } = require('crypto');
const { z } = require('zod');

@@ -14,13 +17,25 @@ const fs = require('fs');

const { importSessions } = require('./session-import');
const { appendMemoryItem, getOpenMemoryItems, getRecentMemoryItems } = require('./memory');
const {
MEMORY_TYPES,
MEMORY_STATUSES,
appendMemoryItem,
getMemoryItemById,
getOpenMemoryItems,
getRecentMemoryItems,
listMemoryItems,
readMemory,
updateMemoryItem,
resolveMemoryItem,
archiveMemoryItem,
deleteMemoryItem,
} = require('./memory');
const { parseNativeSessions, getSessionSummary } = require('./session-parser');
const { analyzeGuardrails, buildGuardrailSection } = require('./guardrails');
const { buildResumeBriefing, gatherResumeData } = require('./resume');
/**
* Start the mindswap MCP server.
* 3 tools. That's it.
* Core tools for context, saving, search, and structured memory.
*/
async function startMCPServer() {
const projectRoot = process.cwd();
function createMCPServer(projectRoot) {
const server = new McpServer({

@@ -99,3 +114,186 @@ name: 'mindswap',

// Start the server
// ═══════════════════════════════════════════════════
// TOOL 4: mindswap_memory
// Structured memory CRUD for blockers, questions, assumptions, resolutions.
// ═══════════════════════════════════════════════════
server.tool(
'mindswap_memory',
`Manage structured memory items. Use this to list, add, update, resolve, archive, or delete blockers, assumptions, questions, and resolutions.`,
{
action: z.enum(['list', 'get', 'add', 'update', 'resolve', 'archive', 'delete'])
.describe('Operation to perform on memory'),
id: z.string().optional()
.describe('Memory item id for get/update/resolve/archive/delete'),
type: z.enum([...MEMORY_TYPES]).optional()
.describe('Memory type for add or filtering'),
message: z.string().optional()
.describe('Message for add/update'),
tag: z.string().optional()
.describe('Tag for add/update or filtering'),
status: z.enum([...MEMORY_STATUSES]).optional()
.describe('Status for add/update or filtering'),
author: z.string().optional()
.describe('Author for add/update or filtering'),
source: z.string().optional()
.describe('Source for add/update or filtering'),
limit: z.number().int().positive().max(200).optional()
.describe('Max number of items to return when listing'),
after: z.string().optional()
.describe('Only include items created after this timestamp'),
before: z.string().optional()
.describe('Only include items created before this timestamp'),
hard: z.boolean().default(false)
.describe('Hard delete instead of archiving'),
json: z.boolean().default(false)
.describe('Return JSON instead of formatted text'),
},
async (args) => {
return manageMemory(projectRoot, args);
}
);
// ═══════════════════════════════════════════════════
// RESOURCES: stable read-only artifacts for clients
// ═══════════════════════════════════════════════════
server.registerResource(
'mindswap_context_current',
'mindswap://context/current',
{
title: 'Current Context',
description: 'The current synthesized project context in text form.',
},
async () => readStableResource(projectRoot, 'context')
);
server.registerResource(
'mindswap_state_current',
'mindswap://state/current',
{
title: 'Current State',
description: 'The current machine-readable mindswap state as JSON.',
},
async () => readStableResource(projectRoot, 'state')
);
server.registerResource(
'mindswap_decisions_recent',
'mindswap://decisions/recent',
{
title: 'Recent Decisions',
description: 'Recent decisions and conflict signals as JSON.',
},
async () => readStableResource(projectRoot, 'decisions')
);
server.registerResource(
'mindswap_memory_current',
'mindswap://memory/current',
{
title: 'Structured Memory',
description: 'All structured memory items as JSON.',
},
async () => readStableResource(projectRoot, 'memory')
);
server.registerResource(
'mindswap_handoff_current',
'mindswap://handoff/current',
{
title: 'Current Handoff',
description: 'The generated HANDOFF.md content or a synthesized fallback.',
},
async () => readStableResource(projectRoot, 'handoff')
);
// ═══════════════════════════════════════════════════
// PROMPTS: workflow templates for common handoff actions
// ═══════════════════════════════════════════════════
server.registerPrompt(
'mindswap_start_work',
{
title: 'Start Work',
description: 'Prepare a focused prompt for continuing active work in this repo.',
argsSchema: {
goal: z.string().optional().describe('Optional goal or feature to focus on'),
tool: z.string().optional().describe('Optional AI tool name for wording adjustments'),
compact: z.string().optional().describe('Set to "true" for a shorter prompt body'),
},
},
async ({ goal, tool, compact }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildStartWorkPrompt(projectRoot, { goal, tool, compact: String(compact).toLowerCase() === 'true' }),
},
}],
})
);
server.registerPrompt(
'mindswap_resume_work',
{
title: 'Resume Work',
description: 'Prepare a restart prompt that emphasizes blockers and the next best action.',
argsSchema: {
compact: z.string().optional().describe('Set to "true" for a shorter prompt body'),
},
},
async ({ compact }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildResumeWorkPrompt(projectRoot, { compact: String(compact).toLowerCase() === 'true' }),
},
}],
})
);
server.registerPrompt(
'mindswap_prepare_handoff',
{
title: 'Prepare Handoff',
description: 'Generate a handoff prompt that asks for the exact summary another agent needs.',
argsSchema: {
audience: z.string().optional().describe('Optional recipient or tool name'),
},
},
async ({ audience }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildHandoffPrompt(projectRoot, { audience }),
},
}],
})
);
server.registerPrompt(
'mindswap_review_conflicts',
{
title: 'Review Conflicts',
description: 'Review decision and dependency conflicts before making changes.',
argsSchema: {
focus: z.string().optional().describe('Optional area to focus on, such as auth or database'),
},
},
async ({ focus }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildConflictReviewPrompt(projectRoot, { focus }),
},
}],
})
);
return server;
}
async function startMCPServer() {
const projectRoot = process.cwd();
const server = createMCPServer(projectRoot);
const transport = new StdioServerTransport();

@@ -105,2 +303,133 @@ await server.connect(transport);

async function startMCPHttpServer(options = {}) {
const projectRoot = options.projectRoot || process.cwd();
const server = createMCPServer(projectRoot);
const host = options.host || '127.0.0.1';
const port = options.port ?? 3000;
const pathName = normalizeHttpPath(options.path || '/mcp');
const allowedOrigin = options.origin || '*';
const token = options.token || null;
const transports = new Map();
const httpServer = http.createServer(async (req, res) => {
try {
if (req.method === 'OPTIONS') {
applyCorsHeaders(res, allowedOrigin);
res.writeHead(204);
res.end();
return;
}
if (normalizeHttpPath(req.url || '/') !== pathName) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32601, message: 'Not found.' },
id: null,
}));
return;
}
if (token && !requestHasValidToken(req, token)) {
applyCorsHeaders(res, allowedOrigin);
res.writeHead(401, {
'content-type': 'application/json',
'www-authenticate': 'Bearer realm="mindswap"',
});
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32001, message: 'Unauthorized.' },
id: null,
}));
return;
}
applyCorsHeaders(res, allowedOrigin);
const body = await readRequestBody(req);
let parsedBody;
if (body) {
try {
parsedBody = JSON.parse(body);
} catch {
res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32700, message: 'Invalid JSON body.' },
id: null,
}));
return;
}
}
const sessionId = req.headers['mcp-session-id'];
let transport = sessionId ? transports.get(String(sessionId)) : null;
if (!transport) {
const isInitialize = parsedBody && isInitializeRequest(parsedBody);
if (!sessionId && isInitialize) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true,
onsessioninitialized: newSessionId => {
transports.set(String(newSessionId), transport);
},
onsessionclosed: closedSessionId => {
transports.delete(String(closedSessionId));
},
});
await server.connect(transport);
} else if (!sessionId) {
res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32000, message: 'Bad Request: no session ID provided.' },
id: null,
}));
return;
} else {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32000, message: 'Unknown session.' },
id: null,
}));
return;
}
}
await transport.handleRequest(req, res, parsedBody);
} catch (err) {
if (!res.headersSent) {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32603, message: err.message || 'Internal server error' },
id: null,
}));
}
}
});
await new Promise((resolve, reject) => {
httpServer.once('error', reject);
httpServer.listen(port, host, resolve);
});
const address = httpServer.address();
const actualPort = typeof address === 'object' && address ? address.port : port;
const localUrl = `http://${host}:${actualPort}${pathName}`;
return {
server: httpServer,
url: localUrl,
close: async () => {
for (const transport of transports.values()) {
await transport.close().catch(() => {});
}
await new Promise(resolve => httpServer.close(() => resolve()));
},
};
}
// ═══════════════════════════════════════════════════

@@ -470,2 +799,263 @@ // Tool implementations

function renderContextText(projectRoot, focus = 'all', compact = false) {
const context = getContext(projectRoot, focus, compact);
return context?.content?.[0]?.text || '';
}
function readStableResource(projectRoot, kind) {
const state = readState(projectRoot);
const liveData = gatherLiveData(projectRoot);
const memory = readMemory(projectRoot);
const handoffPath = path.join(projectRoot, 'HANDOFF.md');
const handoffText = fs.existsSync(handoffPath) ? fs.readFileSync(handoffPath, 'utf-8') : renderContextText(projectRoot, 'all', false);
switch (kind) {
case 'context':
return buildTextResource('mindswap://context/current', renderContextText(projectRoot, 'all', false));
case 'state':
return buildJsonResource('mindswap://state/current', state);
case 'decisions':
return buildJsonResource('mindswap://decisions/recent', {
decisions: liveData.decisions.slice(-20),
conflicts: findAllConflicts(projectRoot),
dependency_conflicts: checkDepsVsDecisions(projectRoot),
});
case 'memory':
return buildJsonResource('mindswap://memory/current', memory);
case 'handoff':
return buildTextResource('mindswap://handoff/current', handoffText);
default:
throw new Error(`unknown resource kind: ${kind}`);
}
}
function buildTextResource(uri, text) {
return {
contents: [{
uri,
mimeType: 'text/plain',
text,
}],
};
}
function buildJsonResource(uri, value) {
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(value, null, 2),
}],
};
}
function normalizeHttpPath(value) {
let pathname = String(value || '/').split('?')[0] || '/';
if (!pathname.startsWith('/')) pathname = `/${pathname}`;
if (pathname.length > 1 && pathname.endsWith('/')) pathname = pathname.replace(/\/+$/, '');
return pathname || '/';
}
function applyCorsHeaders(res, origin) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, MCP-Session-Id, Last-Event-ID');
res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id, MCP-Protocol-Version');
}
function requestHasValidToken(req, token) {
const auth = req.headers.authorization || req.headers.Authorization;
if (!auth) return false;
const [scheme, value] = String(auth).split(/\s+/, 2);
return /^bearer$/i.test(scheme) && value === token;
}
function isInitializeRequest(body) {
return Boolean(body && body.method === 'initialize');
}
function readRequestBody(req) {
return new Promise((resolve, reject) => {
if (req.method === 'GET' || req.method === 'DELETE' || req.method === 'HEAD') {
resolve('');
return;
}
const chunks = [];
req.on('data', chunk => chunks.push(Buffer.from(chunk)));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
req.on('error', reject);
});
}
function buildStartWorkPrompt(projectRoot, { goal, tool, compact } = {}) {
const contextText = renderContextText(projectRoot, compact ? 'task' : 'all', Boolean(compact));
const lines = ['You are starting work in this repository.'];
if (tool) lines.push(`Target tool: ${tool}.`);
if (goal) lines.push(`Goal: ${goal}.`);
lines.push('');
lines.push('Use the context below, identify the next safe action, and call out blockers before suggesting implementation steps.');
if (contextText) {
lines.push('');
lines.push(contextText);
}
return lines.join('\n');
}
function buildResumeWorkPrompt(projectRoot, { compact } = {}) {
const briefing = buildResumeBriefing(readState(projectRoot), gatherResumeData(projectRoot), { compact });
const lines = [
'Resume this workstream from the current repo state.',
'',
briefing.summary,
'',
'State:',
...briefing.stateLines.map(line => `- ${line}`),
'',
'Recommendation:',
`- ${briefing.recommendation.summary}`,
...briefing.recommendation.next_steps.map(step => `- ${step}`),
];
if (briefing.recommendation.command) {
lines.push(`- Next command: ${briefing.recommendation.command}`);
}
return lines.join('\n');
}
function buildHandoffPrompt(projectRoot, { audience } = {}) {
const contextText = renderContextText(projectRoot, 'all', false);
const lines = [
audience ? `Prepare a handoff for ${audience}.` : 'Prepare a handoff for the next agent.',
'Summarize what changed, what is still open, and what should happen next.',
'Include files, commands, blockers, and any unresolved decisions.',
];
if (contextText) {
lines.push('');
lines.push(contextText);
}
return lines.join('\n');
}
function buildConflictReviewPrompt(projectRoot, { focus } = {}) {
const contextText = renderContextText(projectRoot, 'decisions', false);
const lines = [
focus ? `Review conflicts with a focus on ${focus}.` : 'Review the current decision and dependency conflicts.',
'Identify contradictions, explain the impact, and propose the smallest safe resolution.',
];
if (contextText) {
lines.push('');
lines.push(contextText);
}
return lines.join('\n');
}
function manageMemory(projectRoot, opts = {}) {
const dataDir = getDataDir(projectRoot);
if (!fs.existsSync(dataDir)) {
return {
content: [{ type: 'text', text: 'mindswap not initialized. Run `npx mindswap init` first.' }],
};
}
const action = String(opts.action || '').toLowerCase();
const now = new Date().toISOString();
let result = null;
switch (action) {
case 'list': {
const items = listMemoryItems(projectRoot, {
type: opts.type,
status: opts.status,
author: opts.author,
source: opts.source,
created_after: opts.after,
created_before: opts.before,
includeArchived: opts.status === 'archived' || opts.hard === true,
limit: opts.limit || 20,
});
result = { action, count: items.length, items };
break;
}
case 'get': {
if (!opts.id) throw new Error('memory get requires an id');
const item = getMemoryItemById(projectRoot, opts.id);
result = item ? { action, item } : { action, item: null };
break;
}
case 'add': {
if (!opts.message) throw new Error('memory add requires a message');
const item = appendMemoryItem(projectRoot, {
type: opts.type || 'decision',
tag: opts.tag || 'general',
message: opts.message,
status: opts.status || undefined,
author: opts.author || null,
source: opts.source || 'cli',
created_at: now,
});
result = { action, item };
break;
}
case 'update': {
if (!opts.id) throw new Error('memory update requires an id');
const item = updateMemoryItem(projectRoot, opts.id, {
type: opts.type,
tag: opts.tag,
message: opts.message,
status: opts.status,
author: opts.author,
source: opts.source,
updated_at: now,
});
if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
break;
}
case 'resolve': {
if (!opts.id) throw new Error('memory resolve requires an id');
const item = resolveMemoryItem(projectRoot, opts.id, {
message: opts.message,
tag: opts.tag,
author: opts.author,
source: opts.source,
resolved_at: now,
});
if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
break;
}
case 'archive': {
if (!opts.id) throw new Error('memory archive requires an id');
const item = archiveMemoryItem(projectRoot, opts.id, {
message: opts.message,
tag: opts.tag,
author: opts.author,
source: opts.source,
archived_at: now,
});
if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
break;
}
case 'delete': {
if (!opts.id) throw new Error('memory delete requires an id');
const item = deleteMemoryItem(projectRoot, opts.id, { hard: Boolean(opts.hard), archived_at: now });
if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item, deleted: Boolean(opts.hard) };
break;
}
default:
throw new Error(`unknown memory action: ${opts.action}`);
}
const text = opts.json
? JSON.stringify(result, null, 2)
: formatMemoryResult(result);
return {
content: [{ type: 'text', text }],
};
}
// ═══════════════════════════════════════════════════

@@ -592,2 +1182,41 @@ // Helper — gather live project data

module.exports = { startMCPServer, searchContext, tokenize, scoreText, formatMemorySection };
function formatMemoryResult(result) {
if (!result) return 'No memory result.';
if (result.action === 'list') {
const lines = [`Memory items: ${result.count}`];
for (const item of result.items || []) {
lines.push(`- [${item.type}/${item.status}] ${item.id}: ${item.message}`);
}
return lines.join('\n');
}
if (result.item) {
const item = result.item;
return [
`${result.action.toUpperCase()} memory item`,
`- id: ${item.id}`,
`- type: ${item.type}`,
`- status: ${item.status}`,
`- tag: ${item.tag}`,
`- message: ${item.message}`,
item.source ? `- source: ${item.source}` : null,
item.author ? `- author: ${item.author}` : null,
].filter(Boolean).join('\n');
}
return `${result.action} complete.`;
}
module.exports = {
createMCPServer,
startMCPServer,
startMCPHttpServer,
manageMemory,
searchContext,
tokenize,
scoreText,
formatMemorySection,
readStableResource,
buildStartWorkPrompt,
buildResumeWorkPrompt,
buildHandoffPrompt,
buildConflictReviewPrompt,
};

@@ -7,2 +7,3 @@ const fs = require('fs');

const MEMORY_TYPES = new Set(['decision', 'blocker', 'assumption', 'question', 'resolution']);
const MEMORY_STATUSES = new Set(['open', 'resolved', 'archived']);

@@ -51,3 +52,9 @@ function getMemoryPath(projectRoot) {

const now = item.created_at || new Date().toISOString();
const status = item.status || (normalizedType === 'resolution' ? 'resolved' : 'open');
const status = normalizeStatus(item.status || (normalizedType === 'resolution' ? 'resolved' : 'open'));
const resolvedAt = status === 'resolved'
? (item.resolved_at || now)
: (item.resolved_at || null);
const archivedAt = status === 'archived'
? (item.archived_at || now)
: (item.archived_at || null);
const entry = {

@@ -60,4 +67,7 @@ id: item.id || generateId(),

created_at: now,
resolved_at: item.resolved_at || (status === 'resolved' ? now : null),
updated_at: item.updated_at || now,
resolved_at: resolvedAt,
archived_at: archivedAt,
source: item.source || 'cli',
author: item.author || null,
metadata: item.metadata || {},

@@ -79,7 +89,33 @@ };

}
if (opts.id) {
const ids = Array.isArray(opts.id) ? opts.id : [opts.id];
items = items.filter(item => ids.includes(item.id));
}
if (opts.status) {
items = items.filter(item => item.status === opts.status);
}
if (opts.source) {
const sources = Array.isArray(opts.source) ? opts.source : [opts.source];
items = items.filter(item => sources.includes(item.source));
}
if (opts.author) {
const authors = Array.isArray(opts.author) ? opts.author : [opts.author];
items = items.filter(item => authors.includes(item.author));
}
if (opts.created_after) {
const after = timestampValue(opts.created_after);
items = items.filter(item => timestampValue(item.created_at) >= after);
}
if (opts.created_before) {
const before = timestampValue(opts.created_before);
items = items.filter(item => timestampValue(item.created_at) <= before);
}
if (!opts.includeArchived) {
items = items.filter(item => item.status !== 'archived');
}
if (opts.limit) {
items = items.slice(-opts.limit);
const limit = Number(opts.limit);
if (Number.isFinite(limit) && limit > 0) {
items = items.slice(-limit);
}
}

@@ -98,2 +134,92 @@

function getMemoryItemById(projectRoot, id) {
if (!id) return null;
return getMemoryItems(projectRoot, { includeArchived: true }).find(item => item.id === id) || null;
}
function updateMemoryItem(projectRoot, id, updates = {}) {
const memory = readMemory(projectRoot);
const index = memory.items.findIndex(item => item.id === id);
if (index === -1) return null;
const current = memory.items[index];
const next = { ...current };
const now = updates.updated_at || new Date().toISOString();
if (updates.type) next.type = normalizeType(updates.type);
if (updates.tag !== undefined) next.tag = updates.tag;
if (updates.message !== undefined) next.message = updates.message;
if (updates.source !== undefined) next.source = updates.source;
if (updates.author !== undefined) next.author = updates.author;
if (updates.metadata !== undefined) {
next.metadata = mergeMetadata(current.metadata, updates.metadata);
}
if (updates.status !== undefined) {
next.status = normalizeStatus(updates.status, current.status);
}
if (updates.resolved_at !== undefined) {
next.resolved_at = updates.resolved_at;
}
if (updates.archived_at !== undefined) {
next.archived_at = updates.archived_at;
}
if (next.status === 'resolved' && !next.resolved_at) {
next.resolved_at = now;
}
if (next.status === 'archived' && !next.archived_at) {
next.archived_at = now;
}
if (next.status !== 'resolved' && updates.resolved_at === null) {
next.resolved_at = null;
}
if (next.status !== 'archived' && updates.archived_at === null) {
next.archived_at = null;
}
next.updated_at = now;
memory.items[index] = next;
writeMemory(projectRoot, memory);
return next;
}
function resolveMemoryItem(projectRoot, id, updates = {}) {
const resolvedAt = updates.resolved_at || new Date().toISOString();
return updateMemoryItem(projectRoot, id, {
...updates,
status: 'resolved',
resolved_at: resolvedAt,
});
}
function archiveMemoryItem(projectRoot, id, updates = {}) {
const archivedAt = updates.archived_at || new Date().toISOString();
return updateMemoryItem(projectRoot, id, {
...updates,
status: 'archived',
archived_at: archivedAt,
});
}
function deleteMemoryItem(projectRoot, id, opts = {}) {
const memory = readMemory(projectRoot);
const index = memory.items.findIndex(item => item.id === id);
if (index === -1) return null;
const item = memory.items[index];
if (opts.hard) {
memory.items.splice(index, 1);
writeMemory(projectRoot, memory);
return item;
}
return archiveMemoryItem(projectRoot, id, {
archived_at: opts.archived_at || new Date().toISOString(),
});
}
function listMemoryItems(projectRoot, opts = {}) {
return getMemoryItems(projectRoot, opts);
}
function normalizeType(type) {

@@ -103,2 +229,6 @@ return MEMORY_TYPES.has(type) ? type : 'decision';

function normalizeStatus(status, fallback = 'open') {
return MEMORY_STATUSES.has(status) ? status : fallback;
}
function generateId() {

@@ -108,5 +238,21 @@ return Math.random().toString(36).slice(2, 10);

function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isNaN(parsed) ? 0 : parsed;
}
function mergeMetadata(existing, incoming) {
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
return incoming && typeof incoming === 'object' && !Array.isArray(incoming) ? { ...incoming } : {};
}
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) {
return { ...existing };
}
return { ...existing, ...incoming };
}
module.exports = {
MEMORY_FILE,
MEMORY_TYPES,
MEMORY_STATUSES,
getMemoryPath,

@@ -121,3 +267,10 @@ getDefaultMemory,

getRecentMemoryItems,
getMemoryItemById,
updateMemoryItem,
resolveMemoryItem,
archiveMemoryItem,
deleteMemoryItem,
listMemoryItems,
normalizeType,
normalizeStatus,
};