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.2
to
3.2.3
+226
src/index-store.js
const fs = require('fs');
const path = require('path');
const { getDataDir } = require('./state');
const { readMemory } = require('./memory');
const { createProjectSnapshot } = require('./project-snapshot');
const { getGlobalProjectRoot, normalizeScope } = require('./scope');
let sqlite = null;
let sqliteLoaded = false;
function getSqlite() {
if (sqliteLoaded) return sqlite;
sqliteLoaded = true;
const originalEmitWarning = process.emitWarning;
process.emitWarning = function emitWarningWithoutSqliteNoise(warning, ...args) {
const message = typeof warning === 'string' ? warning : warning?.message || '';
const type = typeof args[0] === 'string' ? args[0] : args[0]?.type;
if (type === 'ExperimentalWarning' && /SQLite/i.test(message)) return;
return originalEmitWarning.call(process, warning, ...args);
};
try {
sqlite = require('node:sqlite');
} catch {
sqlite = null;
} finally {
process.emitWarning = originalEmitWarning;
}
return sqlite;
}
function isSqliteAvailable() {
const runtime = getSqlite();
return Boolean(runtime && runtime.DatabaseSync);
}
function getIndexDbPath(projectRoot) {
return path.join(getDataDir(projectRoot), 'mindswap.db');
}
function rebuildSearchIndex(projectRoot, opts = {}) {
if (!isSqliteAvailable()) {
return {
ok: false,
indexed: 0,
scope: normalizeScope(opts),
db_path: null,
reason: 'SQLite runtime is not available in this Node.js environment.',
};
}
const scope = normalizeScope(opts);
const dbPath = getIndexDbPath(projectRoot);
const db = openIndexDb(dbPath);
try {
db.exec('DELETE FROM documents;');
let indexed = 0;
if (scope === 'repo' || scope === 'all') {
indexed += indexRepoDocuments(db, projectRoot);
}
if (scope === 'global' || scope === 'all') {
indexed += indexGlobalDocuments(db);
}
return {
ok: true,
indexed,
scope,
db_path: dbPath,
};
} finally {
db.close();
}
}
function searchIndexedEntries(projectRoot, query, opts = {}) {
if (!isSqliteAvailable()) return [];
const dbPath = getIndexDbPath(projectRoot);
if (!fs.existsSync(dbPath)) return [];
const scope = normalizeScope(opts);
const tokens = tokenize(query);
if (tokens.length === 0) return [];
const db = openIndexDb(dbPath);
try {
const rows = db.prepare('SELECT key, scope, type, source, content FROM documents').all();
return rows
.filter(row => scope === 'all' || row.scope === scope)
.map(row => ({ ...row, score: scoreRow(row.content, tokens) }))
.filter(row => row.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, Number(opts.limit) || 10);
} finally {
db.close();
}
}
function openIndexDb(dbPath) {
const { DatabaseSync } = getSqlite();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
key TEXT PRIMARY KEY,
scope TEXT NOT NULL,
type TEXT NOT NULL,
source TEXT NOT NULL,
content TEXT NOT NULL
);
`);
return db;
}
function indexRepoDocuments(db, projectRoot) {
const snapshot = createProjectSnapshot(projectRoot, { historyLimit: 50, recentCommitLimit: 5 });
let count = 0;
for (const [index, line] of snapshot.decisions.entries()) {
insertDocument(db, {
key: `repo:decision:${index}:${line}`,
scope: 'repo',
type: 'decision',
source: 'decisions.log',
content: line,
});
count += 1;
}
for (const [index, entry] of snapshot.history.entries()) {
insertDocument(db, {
key: `repo:history:${index}:${entry.timestamp || ''}:${entry.message || ''}`,
scope: 'repo',
type: 'history',
source: 'history',
content: entry.message || '',
});
count += 1;
}
for (const item of snapshot.memory?.items || []) {
insertDocument(db, {
key: `repo:memory:${item.id || `${item.type}:${item.message}`}`,
scope: 'repo',
type: `memory:${item.type}`,
source: 'memory',
content: item.message || '',
});
count += 1;
}
if (snapshot.state?.current_task?.description) {
insertDocument(db, {
key: `repo:task:${snapshot.state.current_task.description}`,
scope: 'repo',
type: 'task',
source: 'state.current_task',
content: snapshot.state.current_task.description,
});
count += 1;
}
if (snapshot.state?.current_task?.blocker) {
insertDocument(db, {
key: `repo:blocker:${snapshot.state.current_task.blocker}`,
scope: 'repo',
type: 'blocker',
source: 'state.current_task',
content: snapshot.state.current_task.blocker,
});
count += 1;
}
return count;
}
function indexGlobalDocuments(db) {
const memory = readMemory(getGlobalProjectRoot());
let count = 0;
for (const item of memory.items || []) {
insertDocument(db, {
key: `global:memory:${item.id || `${item.type}:${item.message}`}`,
scope: 'global',
type: `memory:${item.type}`,
source: 'global-memory',
content: item.message || '',
});
count += 1;
}
return count;
}
function insertDocument(db, doc) {
db.prepare(`
INSERT OR REPLACE INTO documents (key, scope, type, source, content)
VALUES (?, ?, ?, ?, ?)
`).run(doc.key, doc.scope, doc.type, doc.source, doc.content);
}
function tokenize(query) {
return String(query || '')
.toLowerCase()
.split(/[^a-z0-9]+/i)
.map(token => token.trim())
.filter(Boolean);
}
function scoreRow(content, tokens) {
const haystack = String(content || '').toLowerCase();
let score = 0;
for (const token of tokens) {
if (haystack.includes(token)) score += 1;
}
return score;
}
module.exports = {
isSqliteAvailable,
getIndexDbPath,
rebuildSearchIndex,
searchIndexedEntries,
};
const fs = require('fs');
const path = require('path');
const { isGitRepo, getCurrentBranch, getAllChangedFiles, getRecentCommits } = require('./git');
const { readState, getHistory } = require('./state');
const { readMemory } = require('./memory');
const { parseNativeSessions } = require('./session-parser');
const { importSessions } = require('./session-import');
const { analyzeGuardrails } = require('./guardrails');
const snapshotCache = new Map();
function createProjectSnapshot(projectRoot, opts = {}) {
const signature = buildSnapshotSignature(projectRoot, opts);
const cached = snapshotCache.get(signature);
if (cached) return cached;
const gitRepo = isGitRepo(projectRoot);
const changedFiles = gitRepo ? getAllChangedFiles(projectRoot) : [];
const branch = gitRepo ? getCurrentBranch(projectRoot) : null;
const state = readState(projectRoot);
const history = getHistory(projectRoot, opts.historyLimit || 20);
const recentCommits = gitRepo ? getRecentCommits(projectRoot, opts.recentCommitLimit || 5) : [];
const memory = readMemory(projectRoot);
const decisions = readDecisionLines(projectRoot);
const snapshot = {
projectRoot,
gitRepo,
branch,
changedFiles,
recentCommits,
state,
history,
memory,
decisions,
};
const includeNativeSessions = opts.includeNativeSessions !== false;
const includeImportedSessions = opts.includeImportedSessions !== false;
const includeGuardrails = opts.includeGuardrails !== false;
defineLazyProperty(snapshot, 'nativeSessions', includeNativeSessions ? () => parseNativeSessions(projectRoot) || [] : () => []);
defineLazyProperty(snapshot, 'importedSessions', includeImportedSessions ? () => importSessions(projectRoot) || [] : () => []);
defineLazyProperty(snapshot, 'guardrails', includeGuardrails ? () => analyzeGuardrails(projectRoot, {
changedFiles,
diffContent: '',
}) : () => ({ warnings: [], surface: [], decisionLines: [] }));
snapshotCache.set(signature, snapshot);
return snapshot;
}
function readDecisionLines(projectRoot) {
const decisionsPath = path.join(projectRoot, '.mindswap', 'decisions.log');
if (!fs.existsSync(decisionsPath)) return [];
return fs.readFileSync(decisionsPath, 'utf-8')
.split('\n')
.filter(line => line.startsWith('['));
}
function buildSnapshotSignature(projectRoot, opts = {}) {
const parts = [
projectRoot,
String(opts.historyLimit || 20),
String(opts.recentCommitLimit || 5),
String(opts.includeNativeSessions !== false),
String(opts.includeImportedSessions !== false),
String(opts.includeGuardrails !== false),
fileSignature(path.join(projectRoot, '.mindswap', 'state.json')),
dirSignature(path.join(projectRoot, '.mindswap', 'history')),
fileSignature(path.join(projectRoot, '.mindswap', 'memory.json')),
fileSignature(path.join(projectRoot, '.mindswap', 'decisions.log')),
dirSignature(path.join(projectRoot, '.claude')),
dirSignature(path.join(projectRoot, '.claude', 'projects')),
dirSignature(path.join(projectRoot, '.cursor')),
dirSignature(path.join(projectRoot, '.cursor', 'rules')),
fileSignature(path.join(projectRoot, '.aider.conf.yml')),
fileSignature(path.join(projectRoot, 'CONVENTIONS.md')),
fileSignature(path.join(projectRoot, 'CLAUDE.md')),
fileSignature(path.join(projectRoot, 'CODEX.md')),
fileSignature(path.join(projectRoot, 'AGENTS.md')),
fileSignature(path.join(projectRoot, 'HANDOFF.md')),
dirSignature(path.join(projectRoot, '.amp')),
dirSignature(path.join(projectRoot, '.cline')),
dirSignature(path.join(projectRoot, '.roo')),
];
return parts.join('|');
}
function fileSignature(filePath) {
try {
const stat = fs.statSync(filePath);
if (!stat.isFile()) return 'f:na';
return `f:${stat.mtimeMs}:${stat.size}`;
} catch {
return 'f:missing';
}
}
function dirSignature(dirPath) {
try {
const stat = fs.statSync(dirPath);
if (!stat.isDirectory()) return 'd:na';
return `d:${stat.mtimeMs}:${stat.size}`;
} catch {
return 'd:missing';
}
}
function defineLazyProperty(target, key, loader) {
let loaded = false;
let value;
Object.defineProperty(target, key, {
enumerable: true,
configurable: false,
get() {
if (!loaded) {
value = loader();
loaded = true;
}
return value;
},
});
}
module.exports = {
createProjectSnapshot,
readDecisionLines,
};
const chalk = require('chalk');
const { rebuildSearchIndex, isSqliteAvailable } = require('./index-store');
async function reindex(projectRoot, opts = {}) {
const report = rebuildSearchIndex(projectRoot, opts);
if (opts.json) {
console.log(JSON.stringify(report, null, 2));
return report;
}
console.log(chalk.bold('\n⚡ Reindex\n'));
if (!isSqliteAvailable()) {
console.log(chalk.yellow(' SQLite indexing is not available in this Node.js runtime.'));
console.log();
return report;
}
console.log(chalk.green(` Indexed ${report.indexed} searchable record${report.indexed === 1 ? '' : 's'}`));
console.log(chalk.dim(` Scope: ${report.scope}`));
console.log(chalk.dim(` DB: ${report.db_path}`));
console.log();
return report;
}
module.exports = {
reindex,
};
const fs = require('fs');
const os = require('os');
const { getDataDir } = require('./state');
function getGlobalProjectRoot() {
return os.homedir();
}
function normalizeScope(opts = {}) {
if (opts.scope) return String(opts.scope).toLowerCase();
if (opts.global) return 'global';
return 'repo';
}
function resolveMemoryRoots(projectRoot, opts = {}) {
const scope = normalizeScope(opts);
if (scope === 'global') return [getGlobalProjectRoot()];
if (scope === 'all') return [projectRoot, getGlobalProjectRoot()];
return [projectRoot];
}
function canUseRepoScope(projectRoot) {
return fs.existsSync(getDataDir(projectRoot));
}
module.exports = {
getGlobalProjectRoot,
normalizeScope,
resolveMemoryRoots,
canUseRepoScope,
};
+60
-2

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

const { sync } = require('../src/sync');
const { manageMemory, startMCPServer, startMCPHttpServer } = require('../src/mcp-server');
const { manageMemory, searchContext, startMCPServer, startMCPHttpServer } = require('../src/mcp-server');
const { save } = require('../src/save');

@@ -26,2 +26,3 @@ const { pr } = require('../src/pr');

const { buildRegistryReport, readRegistryManifest, writeRegistryManifest } = require('../src/registry');
const { reindex } = require('../src/reindex');

@@ -101,2 +102,4 @@ const program = new Command();

.option('--type <type>', 'Memory type: decision, blocker, assumption, question, resolution')
.option('--global', 'Write to global personal memory')
.option('--scope <scope>', 'Memory scope: repo or global')
.action(async (message, opts) => {

@@ -124,2 +127,4 @@ try {

.option('--hard', 'Permanently delete instead of archiving')
.option('--global', 'Use global personal memory scope')
.option('--scope <scope>', 'Memory scope: repo, global, all')
.option('--json', 'Output as JSON')

@@ -144,2 +149,4 @@ .action(async (action, id, messageParts, opts) => {

hard: opts.hard,
global: opts.global,
scope: opts.scope,
json: opts.json,

@@ -319,4 +326,37 @@ });

program
.command('search <query...>')
.description('Raw search over project memory, history, decisions, and optional global memory.')
.option('--type <type>', 'Search type: all, decisions, history', 'all')
.option('--global', 'Search global personal memory')
.option('--scope <scope>', 'Search scope: repo, global, all')
.option('--json', 'Output as JSON')
.action(async (queryParts, opts) => {
try {
const query = Array.isArray(queryParts) ? queryParts.join(' ').trim() : String(queryParts || '').trim();
const result = searchContext(process.cwd(), query, opts.type || 'all', null, {
global: opts.global,
scope: opts.scope,
});
const text = result?.content?.[0]?.text || '';
if (opts.json) {
process.stdout.write(`${JSON.stringify({
query,
type: opts.type || 'all',
scope: opts.scope || (opts.global ? 'global' : 'repo'),
text,
}, null, 2)}\n`);
return;
}
process.stdout.write(`${text}\n`);
} catch (err) {
console.error(chalk.red('Error:'), err.message);
process.exit(1);
}
});
program
.command('ask <question...>')
.description('Answer a question from project memory using semantic search and cited sources.')
.option('--global', 'Search global personal memory')
.option('--scope <scope>', 'Search scope: repo, global, all')
.option('--json', 'Output as JSON')

@@ -379,2 +419,18 @@ .action(async (question, opts) => {

// ─── reindex ───
program
.command('reindex')
.description('Rebuild the local SQLite search index from repo and/or global memory.')
.option('--global', 'Reindex global personal memory only')
.option('--scope <scope>', 'Reindex scope: repo, global, all')
.option('--json', 'Output as JSON')
.action(async (opts) => {
try {
await reindex(process.cwd(), opts);
} catch (err) {
console.error(chalk.red('Error:'), err.message);
process.exit(1);
}
});
// ─── mcp ───

@@ -627,7 +683,9 @@ program

console.log(chalk.bold.green(`\n✓ MCP server configured for ${configured} tool${configured > 1 ? 's' : ''}!\n`));
console.log(chalk.dim(' 3 tools available to AI:'));
console.log(chalk.dim(' 4 tools available to AI:'));
console.log(chalk.white(' mindswap_get_context ') + chalk.dim('— "What do I need to know?"'));
console.log(chalk.white(' mindswap_save_context ') + chalk.dim('— "Here\'s what I did"'));
console.log(chalk.white(' mindswap_search ') + chalk.dim('— "What did we decide about X?"'));
console.log(chalk.white(' mindswap_memory ') + chalk.dim('— "Track blockers/questions/assumptions"'));
console.log(chalk.dim(' Stable resources and workflow prompts are also exposed when supported by the client.'));
console.log(chalk.dim('\n Restart your AI tool to activate.\n'));
}
+2
-2
{
"name": "mindswap",
"version": "3.2.2",
"mcpName": "io.github.shiporbleed/mindswap",
"version": "3.2.3",
"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.",

@@ -6,0 +6,0 @@ "main": "src/index.js",

@@ -6,3 +6,3 @@ # mindswap

Keep project context in the repo so AI tools can continue work without re-explaining the same codebase.
Keep project context and personal AI memory local so tools can continue work without re-explaining the same context.

@@ -37,5 +37,23 @@ ## Why it exists

- `memory` to manage blockers, assumptions, questions, and resolutions
- `--global` memory and ask scope for personal cross-tool memory under `~/.mindswap/`
- `reindex` to rebuild the local SQLite search index from your file-based memory
- `sync` to share continuity state across machines
- `mcp` and `mcp-http` to expose the same context to AI clients
## Global personal memory
MindSwap now supports two local memory scopes:
- repo memory in `<repo>/.mindswap/`
- personal memory in `~/.mindswap/`
Use global memory when a preference or learning should follow you across projects and tools.
```bash
npx mindswap log "Prefer concise explanations" --type assumption --global
npx mindswap memory list --scope all
npx mindswap ask "What explanation style should we use?" --scope all
npx mindswap reindex --scope all
```
## MCP and AI tools

@@ -42,0 +60,0 @@

{
"$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.",
"name": "io.github.ShipOrBleed/mindswap",
"title": "Mindswap",
"description": "Local-first AI context and memory server for cross-tool coding continuity.",
"repository": {

@@ -10,3 +10,3 @@ "url": "https://github.com/ShipOrBleed/mindswap.git",

},
"version": "3.2.1",
"version": "3.2.3",
"packages": [

@@ -16,8 +16,14 @@ {

"identifier": "mindswap",
"version": "3.2.1",
"version": "3.2.3",
"transport": {
"type": "stdio"
}
},
"packageArguments": [
{
"type": "positional",
"value": "mcp"
}
]
}
]
}

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

const { searchContext } = require('./mcp-server');
const { createProjectSnapshot } = require('./project-snapshot');

@@ -20,4 +21,5 @@ async function ask(projectRoot, question, opts = {}) {

const state = readState(projectRoot);
const search = searchContext(projectRoot, query, 'all');
const snapshot = createProjectSnapshot(projectRoot, { historyLimit: 20, recentCommitLimit: 5 });
const state = snapshot.state || readState(projectRoot);
const search = searchContext(projectRoot, query, 'all', snapshot, opts);
const results = parseSearchResults(search?.content?.[0]?.text || '');

@@ -24,0 +26,0 @@ const payload = buildAnswerPayload(query, results, state);

@@ -7,6 +7,9 @@ const fs = require('fs');

const { appendMemoryItem, normalizeType } = require('./memory');
const { normalizeScope, getGlobalProjectRoot } = require('./scope');
async function log(projectRoot, message, opts = {}) {
const dataDir = getDataDir(projectRoot);
if (!fs.existsSync(dataDir)) {
const scope = normalizeScope(opts);
const targetRoot = scope === 'global' ? getGlobalProjectRoot() : projectRoot;
const dataDir = getDataDir(targetRoot);
if (!fs.existsSync(dataDir) && scope !== 'global') {
console.log(chalk.yellow('\nmindswap not initialized. Run: npx mindswap init\n'));

@@ -17,3 +20,3 @@ return;

// Check for conflicts with existing decisions
const conflicts = checkConflicts(projectRoot, message);
const conflicts = scope === 'global' ? [] : checkConflicts(projectRoot, message);

@@ -27,6 +30,10 @@ const decisionsPath = path.join(dataDir, 'decisions.log');

const entry = `[${timestamp}] [${tag}] ${message}`;
fs.mkdirSync(path.dirname(decisionsPath), { recursive: true });
if (!fs.existsSync(decisionsPath)) {
fs.writeFileSync(decisionsPath, '', 'utf-8');
}
fs.appendFileSync(decisionsPath, entry + '\n', 'utf-8');
}
const memoryItem = appendMemoryItem(projectRoot, {
const memoryItem = appendMemoryItem(targetRoot, {
type,

@@ -42,3 +49,5 @@ tag,

const { generate } = require('./generate');
await generate(projectRoot, { handoff: true, quiet: true });
if (scope !== 'global') {
await generate(projectRoot, { handoff: true, quiet: true });
}
} catch {}

@@ -49,4 +58,5 @@

console.log(chalk.dim(' Tag: ') + chalk.white(tag));
console.log(chalk.dim(' Scope: ') + chalk.white(scope));
console.log(chalk.dim(' Message: ') + chalk.white(message));
console.log(chalk.dim(' Memory: ') + chalk.green('.mindswap/memory.json'));
console.log(chalk.dim(' Memory: ') + chalk.green(scope === 'global' ? '~/.mindswap/memory.json' : '.mindswap/memory.json'));
if (type === 'decision') {

@@ -53,0 +63,0 @@ console.log(chalk.dim(' File: ') + chalk.green('.mindswap/decisions.log'));

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

const { getSyncHubPath, readHubSnapshot, buildSyncReport, buildLocalSnapshot } = require('./sync');
const { createProjectSnapshot } = require('./project-snapshot');

@@ -34,9 +35,2 @@ async function doctor(projectRoot, opts = {}) {

const checks = [];
const live = {
branch: null,
changedFiles: [],
recentCommits: [],
decisions: [],
history: [],
};

@@ -49,2 +43,10 @@ if (!fs.existsSync(dataDir)) {

addCheck(checks, 'ok', 'mindswap data directory exists');
const snapshot = createProjectSnapshot(projectRoot, { historyLimit: 10, recentCommitLimit: 5 });
const live = {
branch: snapshot.branch,
changedFiles: snapshot.changedFiles,
recentCommits: snapshot.recentCommits,
decisions: snapshot.decisions,
history: snapshot.history,
};

@@ -92,3 +94,3 @@ const statePath = path.join(dataDir, 'state.json');

try {
state = readState(projectRoot);
state = snapshot.state || readState(projectRoot);
} catch (err) {

@@ -95,0 +97,0 @@ addCheck(checks, 'issue', 'state.json could not be read', err.message);

const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const { readState, getDataDir, getHistory } = require('./state');
const { isGitRepo, getCurrentBranch, getAllChangedFiles, getDiffSummary, getDiffContent, getRecentCommits } = require('./git');
const { readState, getDataDir } = require('./state');
const { isGitRepo, getDiffSummary, getDiffContent } = require('./git');
const { buildNarrative, buildCompactNarrative, summarizeFiles } = require('./narrative');

@@ -10,5 +10,5 @@ const { scanAndRedact, printSecretWarnings } = require('./secrets');

const { teamSection } = require('./team');
const { getOpenMemoryItems, getMemoryItems } = require('./memory');
const { parseNativeSessions, getSessionSummary } = require('./session-parser');
const { analyzeGuardrails, buildGuardrailSection } = require('./guardrails');
const { getSessionSummary } = require('./session-parser');
const { buildGuardrailSection } = require('./guardrails');
const { createProjectSnapshot } = require('./project-snapshot');

@@ -167,25 +167,37 @@ const SECTION_START = '<!-- mindswap:start -->';

function gatherLiveData(projectRoot) {
const data = { branch: null, changedFiles: [], diffSummary: '', recentCommits: [], diff: '' };
const snapshot = createProjectSnapshot(projectRoot, {
historyLimit: 5,
recentCommitLimit: 5,
});
const data = {
branch: snapshot.branch,
changedFiles: snapshot.changedFiles,
diffSummary: '',
recentCommits: snapshot.recentCommits,
diff: '',
decisions: snapshot.decisions.slice(-10),
structuredMemory: getStructuredMemory(snapshot),
history: snapshot.history.slice(-5),
};
if (isGitRepo(projectRoot)) {
data.branch = getCurrentBranch(projectRoot);
data.changedFiles = getAllChangedFiles(projectRoot);
data.diffSummary = getDiffSummary(projectRoot);
data.recentCommits = getRecentCommits(projectRoot, 5);
data.diff = getDiffContent(projectRoot, 150);
}
const decisionsPath = path.join(projectRoot, '.mindswap', 'decisions.log');
data.decisions = [];
if (fs.existsSync(decisionsPath)) {
data.decisions = fs.readFileSync(decisionsPath, 'utf-8')
.split('\n')
.filter(l => l.startsWith('['))
.slice(-10);
}
data.structuredMemory = getStructuredMemory(projectRoot);
data.history = getHistory(projectRoot, 5);
data.nativeSessions = parseNativeSessions(projectRoot);
data.guardrails = analyzeGuardrails(projectRoot, {
changedFiles: data.changedFiles,
diffContent: data.diff,
Object.defineProperty(data, 'nativeSessions', {
enumerable: true,
get() {
return snapshot.nativeSessions;
},
});
Object.defineProperty(data, 'guardrails', {
enumerable: true,
get() {
return snapshot.guardrails;
},
});
return data;

@@ -469,8 +481,9 @@ }

function getStructuredMemory(projectRoot) {
function getStructuredMemory(snapshot) {
const items = Array.isArray(snapshot?.memory?.items) ? snapshot.memory.items : [];
return {
blockers: getOpenMemoryItems(projectRoot, 'blocker', 5),
assumptions: getOpenMemoryItems(projectRoot, 'assumption', 5),
questions: getOpenMemoryItems(projectRoot, 'question', 5),
resolutions: getMemoryItems(projectRoot, { type: 'resolution', limit: 5 }),
blockers: items.filter(item => item.type === 'blocker' && item.status === 'open').slice(-5),
assumptions: items.filter(item => item.type === 'assumption' && item.status === 'open').slice(-5),
questions: items.filter(item => item.type === 'question' && item.status === 'open').slice(-5),
resolutions: items.filter(item => item.type === 'resolution').slice(-5),
};

@@ -477,0 +490,0 @@ }

@@ -11,3 +11,2 @@ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');

const { readState, getDataDir, getHistory } = require('./state');
const { isGitRepo, getCurrentBranch, getAllChangedFiles, getRecentCommits } = require('./git');
const { buildNarrative, buildCompactNarrative, calculateQualityScore } = require('./narrative');

@@ -35,2 +34,5 @@ const { findAllConflicts, checkDepsVsDecisions } = require('./conflicts');

const { buildResumeBriefing, gatherResumeData } = require('./resume');
const { createProjectSnapshot, readDecisionLines } = require('./project-snapshot');
const { normalizeScope, resolveMemoryRoots, getGlobalProjectRoot, canUseRepoScope } = require('./scope');
const { isSqliteAvailable, getIndexDbPath, searchIndexedEntries } = require('./index-store');

@@ -63,3 +65,4 @@ /**

async ({ focus, compact }) => {
return getContext(projectRoot, focus, compact);
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForContext(focus, compact));
return getContext(projectRoot, focus, compact, snapshot);
}

@@ -110,5 +113,10 @@ );

.describe('Where to search: "all" searches everything, "decisions" only searches decision log, "history" only searches session history'),
global: z.boolean().default(false)
.describe('Search global personal memory'),
scope: z.enum(['repo', 'global', 'all']).optional()
.describe('Search scope: repo, global, or all'),
},
async ({ query, type }) => {
return searchContext(projectRoot, query, type);
async ({ query, type, global, scope }) => {
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForSearch(type));
return searchContext(projectRoot, query, type, snapshot, { global, scope });
}

@@ -149,2 +157,6 @@ );

.describe('Hard delete instead of archiving'),
global: z.boolean().default(false)
.describe('Use global personal memory scope'),
scope: z.enum(['repo', 'global', 'all']).optional()
.describe('Memory scope: repo, global, or all. Writes require repo or global.'),
json: z.boolean().default(false)

@@ -225,11 +237,14 @@ .describe('Return JSON instead of formatted text'),

},
async ({ goal, tool, compact }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildStartWorkPrompt(projectRoot, { goal, tool, compact: String(compact).toLowerCase() === 'true' }),
},
}],
})
async ({ goal, tool, compact }) => {
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForPrompt('start', compact));
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: buildStartWorkPrompt(projectRoot, { goal, tool, compact: String(compact).toLowerCase() === 'true' }, snapshot),
},
}],
};
}
);

@@ -246,11 +261,14 @@

},
async ({ compact }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildResumeWorkPrompt(projectRoot, { compact: String(compact).toLowerCase() === 'true' }),
},
}],
})
async ({ compact }) => {
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForPrompt('resume', compact));
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: buildResumeWorkPrompt(projectRoot, { compact: String(compact).toLowerCase() === 'true' }, snapshot),
},
}],
};
}
);

@@ -267,11 +285,14 @@

},
async ({ audience }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildHandoffPrompt(projectRoot, { audience }),
},
}],
})
async ({ audience }) => {
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForPrompt('handoff'));
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: buildHandoffPrompt(projectRoot, { audience }, snapshot),
},
}],
};
}
);

@@ -288,11 +309,14 @@

},
async ({ focus }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: buildConflictReviewPrompt(projectRoot, { focus }),
},
}],
})
async ({ focus }) => {
const snapshot = createProjectSnapshot(projectRoot, getSnapshotOptionsForPrompt('conflicts'));
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: buildConflictReviewPrompt(projectRoot, { focus }, snapshot),
},
}],
};
}
);

@@ -445,3 +469,3 @@

function getContext(projectRoot, focus, compact) {
function getContext(projectRoot, focus, compact, snapshot = null) {
const dataDir = getDataDir(projectRoot);

@@ -457,4 +481,5 @@ if (!fs.existsSync(dataDir)) {

const state = readState(projectRoot);
const liveData = gatherLiveData(projectRoot);
const currentSnapshot = snapshot || createProjectSnapshot(projectRoot, getSnapshotOptionsForContext(focus, compact));
const state = currentSnapshot.state;
const liveData = snapshotToLiveData(currentSnapshot, getLiveDataOptionsForContext(focus, compact));

@@ -497,3 +522,3 @@ if (compact) {

const memoryLines = formatMemorySection(projectRoot);
const memoryLines = formatMemorySection(currentSnapshot);
if (memoryLines.length > 0) {

@@ -680,5 +705,6 @@ sections.push(`## Structured Memory\n${memoryLines.join('\n')}`);

function searchContext(projectRoot, query, type) {
function searchContext(projectRoot, query, type, snapshot = null, opts = {}) {
const dataDir = getDataDir(projectRoot);
if (!fs.existsSync(dataDir)) {
const scope = normalizeScope(opts);
if (!fs.existsSync(dataDir) && scope === 'repo') {
return {

@@ -689,2 +715,29 @@ content: [{ type: 'text', text: 'mindswap not initialized. Run `npx mindswap init` first.' }],

const preferIndex = opts.index !== false && type === 'all' && isSqliteAvailable();
if (preferIndex) {
const indexPath = getIndexDbPath(projectRoot);
if (fs.existsSync(indexPath)) {
const hits = searchIndexedEntries(projectRoot, query, { scope, limit: 15 });
if (hits.length > 0) {
const formatted = hits
.map(hit => {
const label = formatIndexHitType(hit);
const score = Math.max(1, Number(hit.score) || 1);
return `[${label}] (${score}) ${hit.content}`;
})
.join('\n');
return {
content: [{
type: 'text',
text: `Found ${hits.length} indexed result(s) for "${query}":\n\n${formatted}`,
}],
};
}
}
}
const currentSnapshot = fs.existsSync(dataDir)
? (snapshot || createProjectSnapshot(projectRoot, { historyLimit: 50, recentCommitLimit: 5 }))
: null;
const queryTokens = tokenize(query);

@@ -695,16 +748,10 @@ const results = [];

// Search decisions
if (type === 'all' || type === 'decisions') {
const decisionsPath = path.join(dataDir, 'decisions.log');
if (fs.existsSync(decisionsPath)) {
const lines = fs.readFileSync(decisionsPath, 'utf-8')
.split('\n')
.filter(l => l.startsWith('['));
for (const line of lines) {
addScoredResult(results, seen, {
type: 'decision',
content: line,
source: 'decisions.log',
}, queryTokens, 1.2);
}
if (currentSnapshot && (type === 'all' || type === 'decisions')) {
for (const line of currentSnapshot.decisions) {
addScoredResult(results, seen, {
type: 'decision',
content: createSearchSnippet(stripDecisionPrefix(line), queryTokens),
source: 'decisions.log',
key: line,
}, queryTokens, 1.4, line, 4);
}

@@ -714,10 +761,10 @@ }

// Search history
if (type === 'all' || type === 'history') {
const history = getHistory(projectRoot, 50);
for (const entry of history) {
if (currentSnapshot && (type === 'all' || type === 'history')) {
for (const entry of currentSnapshot.history) {
addScoredResult(results, seen, {
type: 'history',
content: `[${entry.timestamp}] ${entry.message}${entry.ai_tool ? ` (${entry.ai_tool})` : ''}`,
content: createSearchSnippet(entry.message, queryTokens),
source: 'history',
}, queryTokens, 1.0, JSON.stringify(entry));
key: JSON.stringify(entry),
}, queryTokens, 1.1, JSON.stringify(entry), 2);
}

@@ -727,19 +774,20 @@ }

// Search current state
if (type === 'all') {
const memoryItems = getRecentMemoryItems(projectRoot, 50);
for (const item of memoryItems) {
if (currentSnapshot && type === 'all') {
for (const item of listMemoryItemsFromSnapshot(currentSnapshot, { limit: 50 })) {
addScoredResult(results, seen, {
type: `memory:${item.type}`,
content: `${item.type}: ${item.message} [${item.status}]`,
content: createSearchSnippet(`${item.type}: ${item.message}`, queryTokens),
source: 'memory',
}, queryTokens, item.status === 'open' ? 1.15 : 1.0, `${item.type} ${item.tag} ${item.status} ${item.message}`);
key: item.id || `${item.type}:${item.tag}:${item.message}`,
}, queryTokens, item.status === 'open' ? 1.4 : 1.0, `${item.type} ${item.tag} ${item.status} ${item.message}`, item.status === 'open' ? 3 : 1);
}
const state = readState(projectRoot);
const state = currentSnapshot.state;
if (state.current_task?.description) {
addScoredResult(results, seen, {
type: 'task',
content: `Current task: ${state.current_task.description} [${state.current_task.status}]`,
content: createSearchSnippet(`Current task: ${state.current_task.description}`, queryTokens),
source: 'state.current_task',
}, queryTokens, 1.35, state.current_task.description);
key: state.current_task.description,
}, queryTokens, 1.8, state.current_task.description, 5);
}

@@ -749,5 +797,6 @@ if (state.project?.tech_stack?.length) {

type: 'project',
content: `Tech stack includes: ${state.project.tech_stack.join(', ')}`,
content: createSearchSnippet(`Tech stack: ${state.project.tech_stack.join(', ')}`, queryTokens),
source: 'state.project',
}, queryTokens, 0.9, state.project.tech_stack.join(' '));
key: state.project.tech_stack.join(' '),
}, queryTokens, 0.8, state.project.tech_stack.join(' '), 1);
}

@@ -757,11 +806,11 @@ if (state.current_task?.blocker) {

type: 'blocker',
content: `Current blocker: ${state.current_task.blocker}`,
content: createSearchSnippet(`Current blocker: ${state.current_task.blocker}`, queryTokens),
source: 'state.current_task',
}, queryTokens, 1.15, state.current_task.blocker);
key: state.current_task.blocker,
}, queryTokens, 1.7, state.current_task.blocker, 4);
}
}
if (type === 'all') {
const nativeSessions = parseNativeSessions(projectRoot) || [];
for (const session of nativeSessions) {
if (currentSnapshot && type === 'all') {
for (const session of currentSnapshot.nativeSessions) {
const combined = [

@@ -777,9 +826,9 @@ session.summary || '',

type: 'native-session',
content: `${session.tool}${session.timestamp ? ` @ ${session.timestamp}` : ''}: ${session.summary || 'session context'}`,
content: createSearchSnippet(`${session.tool}: ${session.summary || 'session context'}`, queryTokens),
source: session.tool,
}, queryTokens, 0.9, combined || session.rawText || session.tool);
key: `${session.tool}:${session.timestamp || ''}:${session.summary || session.rawText || ''}`,
}, queryTokens, 0.85, combined || session.rawText || session.tool, 0);
}
const imported = importSessions(projectRoot) || [];
for (const session of imported) {
for (const session of currentSnapshot.importedSessions) {
const sourceLabel = session.tool || 'session';

@@ -789,8 +838,20 @@ const combined = [...(session.decisions || []), ...(session.context || [])].join(' ');

type: 'imported',
content: `${sourceLabel}: ${(session.context || session.decisions || []).slice(0, 3).join(' | ')}`,
content: createSearchSnippet(`${sourceLabel}: ${(session.context || session.decisions || []).slice(0, 3).join(' | ')}`, queryTokens),
source: sourceLabel,
}, queryTokens, 0.8, combined);
key: `${sourceLabel}:${(session.context || session.decisions || []).join(' | ')}`,
}, queryTokens, 0.75, combined, 0);
}
}
if (type === 'all' && (scope === 'global' || scope === 'all')) {
for (const item of listMemoryItems(getGlobalProjectRoot(), { limit: 50 })) {
addScoredResult(results, seen, {
type: `global:${item.type}`,
content: createSearchSnippet(`${item.type}: ${item.message}`, queryTokens),
source: 'global-memory',
key: `global:${item.id || `${item.type}:${item.tag}:${item.message}`}`,
}, queryTokens, item.status === 'open' ? 1.15 : 0.9, `${item.type} ${item.tag} ${item.status} ${item.message}`, item.status === 'open' ? 2 : 1);
}
}
if (results.length === 0) {

@@ -806,3 +867,3 @@ return {

const topResults = results
.sort((a, b) => b.score - a.score)
.sort((a, b) => (b.rank - a.rank) || (b.score - a.score))
.slice(0, 15);

@@ -818,17 +879,30 @@ const formatted = topResults.map(r => `[${r.type}] (${Math.round(r.score)}) ${r.content}`).join('\n');

function renderContextText(projectRoot, focus = 'all', compact = false) {
const context = getContext(projectRoot, focus, compact);
function formatIndexHitType(hit) {
const scope = hit.scope === 'global' ? 'global' : 'repo';
const raw = String(hit.type || '').trim();
if (!raw) return scope;
if (scope === 'global') {
// Match existing global labels: global:<memory-type>
if (raw.startsWith('memory:')) return `global:${raw.slice('memory:'.length)}`;
return `global:${raw}`;
}
return raw;
}
function renderContextText(projectRoot, focus = 'all', compact = false, snapshot = null) {
const context = getContext(projectRoot, focus, compact, snapshot);
return context?.content?.[0]?.text || '';
}
function readStableResource(projectRoot, kind) {
const state = readState(projectRoot);
const liveData = gatherLiveData(projectRoot);
const memory = readMemory(projectRoot);
function readStableResource(projectRoot, kind, snapshot = null) {
const currentSnapshot = snapshot || createProjectSnapshot(projectRoot, getSnapshotOptionsForResource(kind));
const state = currentSnapshot.state;
const liveData = snapshotToLiveData(currentSnapshot, getLiveDataOptionsForResource(kind));
const memory = currentSnapshot.memory;
const handoffPath = path.join(projectRoot, 'HANDOFF.md');
const handoffText = fs.existsSync(handoffPath) ? fs.readFileSync(handoffPath, 'utf-8') : renderContextText(projectRoot, 'all', false);
const handoffText = fs.existsSync(handoffPath) ? fs.readFileSync(handoffPath, 'utf-8') : renderContextText(projectRoot, 'all', false, currentSnapshot);
switch (kind) {
case 'context':
return buildTextResource('mindswap://context/current', renderContextText(projectRoot, 'all', false));
return buildTextResource('mindswap://context/current', renderContextText(projectRoot, 'all', false, currentSnapshot));
case 'state':

@@ -909,4 +983,4 @@ return buildJsonResource('mindswap://state/current', state);

function buildStartWorkPrompt(projectRoot, { goal, tool, compact } = {}) {
const contextText = renderContextText(projectRoot, compact ? 'task' : 'all', Boolean(compact));
function buildStartWorkPrompt(projectRoot, { goal, tool, compact } = {}, snapshot = null) {
const contextText = renderContextText(projectRoot, compact ? 'task' : 'all', Boolean(compact), snapshot);
const lines = ['You are starting work in this repository.'];

@@ -924,4 +998,5 @@ if (tool) lines.push(`Target tool: ${tool}.`);

function buildResumeWorkPrompt(projectRoot, { compact } = {}) {
const briefing = buildResumeBriefing(readState(projectRoot), gatherResumeData(projectRoot), { compact });
function buildResumeWorkPrompt(projectRoot, { compact } = {}, snapshot = null) {
const currentSnapshot = snapshot || createProjectSnapshot(projectRoot, getSnapshotOptionsForPrompt('resume', compact));
const briefing = buildResumeBriefing(currentSnapshot.state, gatherResumeData(projectRoot, currentSnapshot), { compact });
const lines = [

@@ -947,4 +1022,4 @@ 'Resume this workstream from the current repo state.',

function buildHandoffPrompt(projectRoot, { audience } = {}) {
const contextText = renderContextText(projectRoot, 'all', false);
function buildHandoffPrompt(projectRoot, { audience } = {}, snapshot = null) {
const contextText = renderContextText(projectRoot, 'all', false, snapshot);
const lines = [

@@ -962,4 +1037,4 @@ audience ? `Prepare a handoff for ${audience}.` : 'Prepare a handoff for the next agent.',

function buildConflictReviewPrompt(projectRoot, { focus } = {}) {
const contextText = renderContextText(projectRoot, 'decisions', false);
function buildConflictReviewPrompt(projectRoot, { focus } = {}, snapshot = null) {
const contextText = renderContextText(projectRoot, 'decisions', false, snapshot);
const lines = [

@@ -977,4 +1052,9 @@ focus ? `Review conflicts with a focus on ${focus}.` : 'Review the current decision and dependency conflicts.',

function manageMemory(projectRoot, opts = {}) {
const dataDir = getDataDir(projectRoot);
if (!fs.existsSync(dataDir)) {
const scope = normalizeScope(opts);
if (scope === 'all' && !['list', 'get'].includes(String(opts.action || '').toLowerCase())) {
throw new Error('memory writes require repo or global scope');
}
const repoReady = canUseRepoScope(projectRoot);
if (!repoReady && scope === 'repo') {
return {

@@ -987,2 +1067,7 @@ content: [{ type: 'text', text: 'mindswap not initialized. Run `npx mindswap init` first.' }],

const now = new Date().toISOString();
const roots = resolveMemoryRoots(projectRoot, opts).filter(root => {
if (root === projectRoot) return repoReady;
return true;
});
const primaryRoot = roots[0];

@@ -992,3 +1077,3 @@ let result = null;

case 'list': {
const items = listMemoryItems(projectRoot, {
const items = roots.flatMap(root => listMemoryItems(root, {
type: opts.type,

@@ -1002,3 +1087,3 @@ status: opts.status,

limit: opts.limit || 20,
});
}).map(item => ({ ...item, scope: root === getGlobalProjectRoot() ? 'global' : 'repo' })));
result = { action, count: items.length, items };

@@ -1009,3 +1094,8 @@ break;

if (!opts.id) throw new Error('memory get requires an id');
const item = getMemoryItemById(projectRoot, opts.id);
const item = roots
.map(root => {
const found = getMemoryItemById(root, opts.id);
return found ? { ...found, scope: root === getGlobalProjectRoot() ? 'global' : 'repo' } : null;
})
.find(Boolean);
result = item ? { action, item } : { action, item: null };

@@ -1016,3 +1106,3 @@ break;

if (!opts.message) throw new Error('memory add requires a message');
const item = appendMemoryItem(projectRoot, {
const item = appendMemoryItem(primaryRoot, {
type: opts.type || 'decision',

@@ -1026,3 +1116,3 @@ tag: opts.tag || 'general',

});
result = { action, item };
result = { action, item: { ...item, scope: primaryRoot === getGlobalProjectRoot() ? 'global' : 'repo' } };
break;

@@ -1032,3 +1122,4 @@ }

if (!opts.id) throw new Error('memory update requires an id');
const item = updateMemoryItem(projectRoot, opts.id, {
const targetRoot = roots.find(root => getMemoryItemById(root, opts.id)) || primaryRoot;
const item = updateMemoryItem(targetRoot, opts.id, {
type: opts.type,

@@ -1043,3 +1134,3 @@ tag: opts.tag,

if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
result = { action, item: { ...item, scope: targetRoot === getGlobalProjectRoot() ? 'global' : 'repo' } };
break;

@@ -1049,3 +1140,4 @@ }

if (!opts.id) throw new Error('memory resolve requires an id');
const item = resolveMemoryItem(projectRoot, opts.id, {
const targetRoot = roots.find(root => getMemoryItemById(root, opts.id)) || primaryRoot;
const item = resolveMemoryItem(targetRoot, opts.id, {
message: opts.message,

@@ -1058,3 +1150,3 @@ tag: opts.tag,

if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
result = { action, item: { ...item, scope: targetRoot === getGlobalProjectRoot() ? 'global' : 'repo' } };
break;

@@ -1064,3 +1156,4 @@ }

if (!opts.id) throw new Error('memory archive requires an id');
const item = archiveMemoryItem(projectRoot, opts.id, {
const targetRoot = roots.find(root => getMemoryItemById(root, opts.id)) || primaryRoot;
const item = archiveMemoryItem(targetRoot, opts.id, {
message: opts.message,

@@ -1073,3 +1166,3 @@ tag: opts.tag,

if (!item) throw new Error(`memory item not found: ${opts.id}`);
result = { action, item };
result = { action, item: { ...item, scope: targetRoot === getGlobalProjectRoot() ? 'global' : 'repo' } };
break;

@@ -1079,5 +1172,10 @@ }

if (!opts.id) throw new Error('memory delete requires an id');
const item = deleteMemoryItem(projectRoot, opts.id, { hard: Boolean(opts.hard), archived_at: now });
const targetRoot = roots.find(root => getMemoryItemById(root, opts.id)) || primaryRoot;
const item = deleteMemoryItem(targetRoot, 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) };
result = {
action,
item: { ...item, scope: targetRoot === getGlobalProjectRoot() ? 'global' : 'repo' },
deleted: Boolean(opts.hard),
};
break;

@@ -1103,32 +1201,17 @@ }

function gatherLiveData(projectRoot) {
const data = {
branch: null,
changedFiles: [],
recentCommits: [],
decisions: [],
history: [],
nativeSessions: [],
return snapshotToLiveData(createProjectSnapshot(projectRoot, getSnapshotOptionsForContext('all', false)), getLiveDataOptionsForContext('all', false));
}
function snapshotToLiveData(snapshot, opts = {}) {
return {
branch: snapshot.branch,
changedFiles: snapshot.changedFiles,
recentCommits: snapshot.recentCommits,
decisions: snapshot.decisions,
history: snapshot.history,
nativeSessions: opts.includeNativeSessions === false ? [] : snapshot.nativeSessions,
structuredMemory: snapshot.memory?.items || [],
importedSessions: opts.includeImportedSessions === false ? [] : snapshot.importedSessions,
guardrails: opts.includeGuardrails === false ? null : snapshot.guardrails,
};
if (isGitRepo(projectRoot)) {
data.branch = getCurrentBranch(projectRoot);
data.changedFiles = getAllChangedFiles(projectRoot);
data.recentCommits = getRecentCommits(projectRoot, 5);
}
const decisionsPath = path.join(projectRoot, '.mindswap', 'decisions.log');
if (fs.existsSync(decisionsPath)) {
data.decisions = fs.readFileSync(decisionsPath, 'utf-8')
.split('\n')
.filter(l => l.startsWith('['));
}
data.structuredMemory = getRecentMemoryItems(projectRoot, 20);
data.history = getHistory(projectRoot, 5);
data.nativeSessions = parseNativeSessions(projectRoot);
data.guardrails = analyzeGuardrails(projectRoot, {
changedFiles: data.changedFiles,
diffContent: '',
});
return data;
}

@@ -1153,10 +1236,10 @@

function addScoredResult(results, seen, entry, queryTokens, weight, haystackText = '') {
function addScoredResult(results, seen, entry, queryTokens, weight, haystackText = '', rank = 0) {
const text = haystackText || entry.content || '';
const score = scoreText(text, queryTokens, weight);
if (score <= 0) return;
const key = `${entry.type}::${entry.content}`;
const key = `${entry.type}::${entry.key || entry.content}`;
if (seen.has(key)) return;
seen.add(key);
results.push({ ...entry, score });
results.push({ ...entry, score, rank: entry.rank ?? rank ?? 0 });
}

@@ -1191,2 +1274,192 @@

function stripDecisionPrefix(line) {
return String(line || '').replace(/^\[.*?\]\s*\[.*?\]\s*/, '').trim();
}
function createSearchSnippet(text, queryTokens, maxLength = 140) {
const clean = String(text || '').replace(/\s+/g, ' ').trim();
if (!clean) return '';
if (!queryTokens || queryTokens.length === 0) return clean.slice(0, maxLength);
const lower = clean.toLowerCase();
let index = -1;
for (const token of queryTokens) {
const candidate = lower.indexOf(token);
if (candidate >= 0 && (index < 0 || candidate < index)) {
index = candidate;
}
}
if (index < 0) {
return clean.length > maxLength ? `${clean.slice(0, maxLength - 1)}…` : clean;
}
const start = Math.max(0, index - 40);
const end = Math.min(clean.length, index + 100);
const prefix = start > 0 ? '…' : '';
const suffix = end < clean.length ? '…' : '';
const snippet = clean.slice(start, end);
return `${prefix}${snippet}${suffix}`;
}
function getSnapshotOptionsForContext(focus, compact) {
const base = { historyLimit: 20, recentCommitLimit: 5 };
if (compact || focus === 'task') {
return {
...base,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
}
if (focus === 'recent') {
return {
...base,
includeNativeSessions: true,
includeImportedSessions: false,
includeGuardrails: false,
};
}
if (focus === 'decisions') {
return {
...base,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: true,
};
}
return {
...base,
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
}
function getLiveDataOptionsForContext(focus, compact) {
if (compact || focus === 'task') {
return {
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
}
if (focus === 'recent') {
return {
includeNativeSessions: true,
includeImportedSessions: false,
includeGuardrails: false,
};
}
if (focus === 'decisions') {
return {
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: true,
};
}
return {
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
}
function getSnapshotOptionsForSearch(type) {
const base = { historyLimit: 50, recentCommitLimit: 5 };
if (type === 'decisions' || type === 'history') {
return {
...base,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
}
return {
...base,
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: false,
};
}
function getSnapshotOptionsForResource(kind) {
const base = { historyLimit: 20, recentCommitLimit: 5 };
switch (kind) {
case 'context':
case 'handoff':
return {
...base,
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
case 'decisions':
return {
...base,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
case 'state':
case 'memory':
default:
return {
...base,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
}
}
function getLiveDataOptionsForResource(kind) {
switch (kind) {
case 'context':
case 'handoff':
return {
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
default:
return {
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: false,
};
}
}
function getSnapshotOptionsForPrompt(kind, compact) {
if (kind === 'conflicts') {
return {
historyLimit: 20,
recentCommitLimit: 5,
includeNativeSessions: false,
includeImportedSessions: false,
includeGuardrails: true,
};
}
if (kind === 'resume') {
return {
historyLimit: 20,
recentCommitLimit: 5,
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
}
if (kind === 'start') {
return getSnapshotOptionsForContext('all', String(compact).toLowerCase() === 'true');
}
return {
historyLimit: 20,
recentCommitLimit: 5,
includeNativeSessions: true,
includeImportedSessions: true,
includeGuardrails: true,
};
}
const QUERY_ALIASES = {

@@ -1211,8 +1484,8 @@ auth: ['authentication', 'login', 'session', 'jwt', 'token'],

function formatMemorySection(projectRoot) {
function formatMemorySection(snapshot) {
const lines = [];
for (const item of getOpenMemoryItems(projectRoot, 'blocker', 5)) lines.push(`- BLOCKER: ${item.message}`);
for (const item of getOpenMemoryItems(projectRoot, 'question', 5)) lines.push(`- QUESTION: ${item.message}`);
for (const item of getOpenMemoryItems(projectRoot, 'assumption', 5)) lines.push(`- ASSUMPTION: ${item.message}`);
for (const item of getRecentMemoryItems(projectRoot, 10).filter(item => item.type === 'resolution').slice(-5)) {
for (const item of getSnapshotMemoryItems(snapshot, { type: 'blocker', status: 'open', limit: 5 })) lines.push(`- BLOCKER: ${item.message}`);
for (const item of getSnapshotMemoryItems(snapshot, { type: 'question', status: 'open', limit: 5 })) lines.push(`- QUESTION: ${item.message}`);
for (const item of getSnapshotMemoryItems(snapshot, { type: 'assumption', status: 'open', limit: 5 })) lines.push(`- ASSUMPTION: ${item.message}`);
for (const item of getSnapshotMemoryItems(snapshot, { type: 'resolution', limit: 10 }).slice(-5)) {
lines.push(`- RESOLUTION: ${item.message}`);

@@ -1223,2 +1496,36 @@ }

function getSnapshotMemoryItems(snapshot, opts = {}) {
const items = Array.isArray(snapshot.memory?.items) ? snapshot.memory.items.slice() : [];
let filtered = items;
if (opts.type) {
const types = Array.isArray(opts.type) ? opts.type : [opts.type];
filtered = filtered.filter(item => types.includes(item.type));
}
if (opts.status) {
filtered = filtered.filter(item => item.status === opts.status);
}
if (opts.source) {
const sources = Array.isArray(opts.source) ? opts.source : [opts.source];
filtered = filtered.filter(item => sources.includes(item.source));
}
if (opts.author) {
const authors = Array.isArray(opts.author) ? opts.author : [opts.author];
filtered = filtered.filter(item => authors.includes(item.author));
}
if (opts.limit) {
const limit = Number(opts.limit);
if (Number.isFinite(limit) && limit > 0) {
filtered = filtered.slice(-limit);
}
}
return filtered;
}
function listMemoryItemsFromSnapshot(snapshot, opts = {}) {
return getSnapshotMemoryItems(snapshot, {
...opts,
includeArchived: opts.includeArchived || opts.status === 'archived',
});
}
function formatMemoryResult(result) {

@@ -1225,0 +1532,0 @@ if (!result) return 'No memory result.';

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

const memoryPath = getMemoryPath(projectRoot);
fs.mkdirSync(path.dirname(memoryPath), { recursive: true });
if (!fs.existsSync(memoryPath)) {

@@ -24,0 +25,0 @@ fs.writeFileSync(memoryPath, JSON.stringify(getDefaultMemory(), null, 2), 'utf-8');

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

title: options.title || humanizeName(packageJson.name),
description: options.description || packageJson.description || '',
description: options.description || 'Local-first AI context and memory server for cross-tool coding continuity.',
repository: repositoryUrl ? {

@@ -43,2 +43,6 @@ url: normalizeRepositoryUrl(repositoryUrl),

},
packageArguments: [{
type: 'positional',
value: 'mcp',
}],
}],

@@ -45,0 +49,0 @@ };

const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const { readState, getDataDir, getHistory } = require('./state');
const { isGitRepo, getCurrentBranch, getAllChangedFiles, getRecentCommits } = require('./git');
const { readState, getDataDir } = require('./state');
const { findAllConflicts, checkDepsVsDecisions } = require('./conflicts');
const { calculateQualityScore } = require('./narrative');
const { getOpenMemoryItems, getRecentMemoryItems } = require('./memory');
const { parseNativeSessions } = require('./session-parser');
const { createProjectSnapshot } = require('./project-snapshot');

@@ -18,4 +16,5 @@ async function resume(projectRoot, opts = {}) {

const state = readState(projectRoot);
const live = gatherResumeData(projectRoot);
const snapshot = createProjectSnapshot(projectRoot, { historyLimit: 20, recentCommitLimit: 5 });
const state = snapshot.state;
const live = gatherResumeData(projectRoot, snapshot);
const briefing = buildResumeBriefing(state, live, opts);

@@ -47,12 +46,13 @@

function gatherResumeData(projectRoot) {
const branch = isGitRepo(projectRoot) ? getCurrentBranch(projectRoot) : null;
const changedFiles = isGitRepo(projectRoot) ? getAllChangedFiles(projectRoot) : [];
const recentCommits = isGitRepo(projectRoot) ? getRecentCommits(projectRoot, 5) : [];
const history = getHistory(projectRoot, 10);
const nativeSessions = parseNativeSessions(projectRoot);
const decisions = readDecisions(projectRoot);
const structuredMemory = getRecentMemoryItems(projectRoot, 20);
const blockers = getOpenMemoryItems(projectRoot, 'blocker', 5);
const questions = getOpenMemoryItems(projectRoot, 'question', 5);
function gatherResumeData(projectRoot, snapshot = null) {
const liveSnapshot = snapshot || createProjectSnapshot(projectRoot, { historyLimit: 20, recentCommitLimit: 5 });
const branch = liveSnapshot.branch;
const changedFiles = liveSnapshot.changedFiles;
const recentCommits = liveSnapshot.recentCommits;
const history = liveSnapshot.history || [];
const nativeSessions = liveSnapshot.nativeSessions || [];
const decisions = liveSnapshot.decisions || readDecisions(projectRoot);
const structuredMemory = Array.isArray(liveSnapshot.memory?.items) ? liveSnapshot.memory.items.slice(-20) : [];
const blockers = structuredMemory.filter(item => item.type === 'blocker' && item.status === 'open').slice(-5);
const questions = structuredMemory.filter(item => item.type === 'question' && item.status === 'open').slice(-5);
const conflicts = findAllConflicts(projectRoot);

@@ -59,0 +59,0 @@ const depConflicts = checkDepsVsDecisions(projectRoot);