| 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, | ||
| }; |
+31
| 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", |
+19
-1
@@ -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 @@ |
+12
-6
| { | ||
| "$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" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
+4
-2
@@ -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); |
+16
-6
@@ -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')); |
+10
-8
@@ -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); |
+41
-28
| 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 @@ } |
+462
-155
@@ -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.'; |
+1
-0
@@ -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'); |
+5
-1
@@ -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 @@ }; |
+16
-16
| 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); |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
356778
8.53%43
10.26%8879
9.15%89
25.35%49
6.52%