squish-memory 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,36 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Squish v0.6.0 - Major tool consolidation milestone
3
+ * Squish v0.7.0 - Dual-Mode CLI + MCP Server
4
4
  *
5
- * This is a significant refactor reducing MCP tools from 18 to 11 (39% reduction).
6
- * Breaking changes in tool API - migration guide: see docs/MIGRATION-v0.6.0.md
5
+ * Modes:
6
+ * - CLI Mode: For OpenClaw bash execution (e.g., `squish remember "text"`)
7
+ * - MCP Mode: For Claude Code (default, no args)
7
8
  *
8
9
  * Features:
9
- * - 11 consolidated MCP tools (from 18)
10
- * - Local mode: SQLite with FTS5 + auto-capture + folder context
11
- * - Team mode: PostgreSQL + pgvector + Redis
12
- * - Plugin system: Hooks for auto-capture, context injection, privacy filtering, folder context generation
13
- * - Privacy-first: Secret detection, <private> tag filtering, async worker pipeline
14
- * - Pluggable embeddings: OpenAI, Ollama, or local TF-IDF
15
- *
16
- * Consolidated MCP Tools (v0.6.0):
17
- * - core_memory: Unified tool for view/edit/append operations (was 3 tools)
18
- * - context_paging: Unified tool for load/evict/view operations (was 3 tools)
19
- * - merge: Unified tool for detect/list/preview/stats/approve/reject/reverse (was 2 tools)
20
- * - context_status, remember, recall, search, observe, context, health (unchanged)
21
- * - lifecycle, summarize_session, protect_memory (unchanged)
22
- *
23
- * Plugin hooks (registered in plugin.json):
24
- * - onInstall: Initialize database, create config files
25
- * - onSessionStart: Inject relevant context + generate folder context
26
- * - onUserPromptSubmit: Auto-capture user prompts with privacy filtering
27
- * - onPostToolUse: Auto-capture tool executions and observe patterns
28
- * - onSessionStop: Finalize observations, summarize via async worker
29
- *
30
- * See src/plugin/plugin-wrapper.ts for hook implementations
31
- * See docs/MIGRATION-v0.6.0.md for migration guide
10
+ * - 11 consolidated MCP tools
11
+ * - Local mode: SQLite with FTS5
12
+ * - Team mode: PostgreSQL + pgvector
13
+ * - OpenClaw CLI commands: remember, search, recall, core_memory
32
14
  */
33
15
  import 'dotenv/config';
16
+ import { Command } from 'commander';
34
17
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
35
18
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
36
19
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
@@ -38,7 +21,6 @@ import { logger } from './core/logger.js';
38
21
  import { checkDatabaseHealth, config } from './db/index.js';
39
22
  import { checkRedisHealth, closeCache } from './core/cache.js';
40
23
  import { rememberMemory, getMemoryById, searchMemories } from './core/memory/memories.js';
41
- import { searchConversations, getRecentConversations } from './core/search/conversations.js';
42
24
  import { createObservation } from './core/observations.js';
43
25
  import { getProjectContext } from './core/context.js';
44
26
  import { startWebServer } from './api/web/web.js';
@@ -49,587 +31,384 @@ import { handleApproveMerge } from './algorithms/merge/handlers/approve-merge.js
49
31
  import { handleRejectMerge } from './algorithms/merge/handlers/reject-merge.js';
50
32
  import { handleReverseMerge } from './algorithms/merge/handlers/reverse-merge.js';
51
33
  import { handleGetMergeStats } from './algorithms/merge/handlers/get-stats.js';
52
- import { forceLifecycleMaintenance } from './core/worker.js';
53
- import { summarizeSession } from './core/summarization.js';
54
- import { storeAgentMemory } from './core/agent-memory.js';
55
- import { getRelatedMemories } from './core/associations.js';
56
- import { protectMemory, pinMemory } from './core/governance.js';
57
- import { isDatabaseUnavailableError, determineOverallStatus } from './core/utils.js';
58
34
  import { searchWithQMD, isQMDAvailable } from './core/search/qmd-search.js';
59
35
  import { initializeCoreMemory, getCoreMemory, editCoreMemorySection, appendCoreMemorySection, getCoreMemoryStats, } from './core/core-memory.js';
60
36
  import { loadMemoryToContext, evictMemoryFromContext, viewLoadedMemories, getContextStatus, } from './core/context-paging.js';
61
- const VERSION = '0.6.0';
62
- const TOOLS = [
63
- // ============================================================================
64
- // Core Memory Tool (Tier 1 - Always-In-Context)
65
- // ============================================================================
66
- {
67
- name: 'core_memory',
68
- description: 'View or edit your core memory (always-visible). Use this to see your persona, user info, project context, and working notes. Update this when you learn something important that should always be visible.',
69
- inputSchema: {
70
- type: 'object',
71
- properties: {
72
- action: {
73
- type: 'string',
74
- enum: ['view', 'edit', 'append'],
75
- description: 'Memory action: view all, edit specific section, or append to section'
76
- },
77
- projectId: { type: 'string', description: 'Project ID' },
78
- section: {
79
- type: 'string',
80
- enum: ['persona', 'user_info', 'project_context', 'working_notes'],
81
- description: 'Section name (required for edit/append actions)'
82
- },
83
- content: { type: 'string', description: 'New content for section (edit action)' },
84
- text: { type: 'string', description: 'Text to append to section (append action)' },
85
- },
86
- required: ['action', 'projectId']
37
+ import { ensureDataDirectory } from './db/bootstrap.js';
38
+ const VERSION = '0.7.0';
39
+ // ============================================================================
40
+ // CLI MODE DETECTION
41
+ // ============================================================================
42
+ const args = process.argv.slice(2);
43
+ const hasCliArgs = args.length > 0 && args[0] !== '--mcp';
44
+ if (hasCliArgs) {
45
+ // === CLI MODE (for OpenClaw) ===
46
+ runCliMode().catch((e) => {
47
+ console.error(JSON.stringify({ error: e.message }, null, 2));
48
+ process.exit(1);
49
+ });
50
+ }
51
+ else {
52
+ // === MCP MODE (for Claude Code) - DEFAULT ===
53
+ runMcpMode().catch((e) => {
54
+ logger.error('Fatal error', e);
55
+ process.exit(1);
56
+ });
57
+ }
58
+ // ============================================================================
59
+ // CLI MODE (for OpenClaw bash execution)
60
+ // ============================================================================
61
+ async function runCliMode() {
62
+ const program = new Command();
63
+ program
64
+ .name('squish')
65
+ .description('Squish - Persistent memory for AI assistants')
66
+ .version(VERSION);
67
+ // Initialize data directory before any command
68
+ program.hook('preAction', async () => {
69
+ await ensureDataDirectory();
70
+ });
71
+ // squish remember "content" --type fact --tags tag1,tag2
72
+ program
73
+ .command('remember <content>')
74
+ .description('Store a memory')
75
+ .option('-t, --type <type>', 'Memory type (observation, fact, decision, context, preference)', 'observation')
76
+ .option('-T, --tags <tags>', 'Comma-separated tags', '')
77
+ .option('-p, --project <project>', 'Project path', process.cwd())
78
+ .action(async (content, options) => {
79
+ try {
80
+ const result = await rememberMemory({
81
+ content,
82
+ type: options.type,
83
+ tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : [],
84
+ project: options.project,
85
+ });
86
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
87
87
  }
88
- },
89
- // ============================================================================
90
- // Context Paging Tool (Tier 2 - Working Set Management)
91
- // Note: Claude manages its own context/tokens. These track your working set.
92
- // ============================================================================
93
- {
94
- name: 'context_paging',
95
- description: 'Manage your working memory set. Load specific memories you want to keep visible, evict ones you no longer need, or view what is currently loaded.',
96
- inputSchema: {
97
- type: 'object',
98
- properties: {
99
- action: {
100
- type: 'string',
101
- enum: ['load', 'evict', 'view'],
102
- description: 'Paging action: load memory, evict memory, or view all loaded'
103
- },
104
- sessionId: { type: 'string', description: 'Session ID' },
105
- memoryId: { type: 'string', description: 'Memory UUID (required for load/evict actions)' },
106
- },
107
- required: ['action', 'sessionId']
88
+ catch (error) {
89
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
90
+ process.exit(1);
91
+ }
92
+ });
93
+ // squish search "query" --type fact --limit 10
94
+ program
95
+ .command('search <query>')
96
+ .description('Search memories')
97
+ .option('-t, --type <type>', 'Filter by memory type')
98
+ .option('-l, --limit <number>', 'Max results', '10')
99
+ .option('-p, --project <project>', 'Project path', process.cwd())
100
+ .action(async (query, options) => {
101
+ try {
102
+ const results = await searchMemories({
103
+ query,
104
+ type: options.type,
105
+ limit: parseInt(options.limit, 10),
106
+ project: options.project,
107
+ });
108
+ console.log(JSON.stringify({ ok: true, query, count: results?.length || 0, results }, null, 2));
108
109
  }
109
- },
110
- {
111
- name: 'context_status',
112
- description: 'View comprehensive context window status and token usage',
113
- inputSchema: {
114
- type: 'object',
115
- properties: {
116
- sessionId: { type: 'string', description: 'Session ID' },
117
- projectId: { type: 'string', description: 'Project ID' },
118
- },
119
- required: ['sessionId', 'projectId']
110
+ catch (error) {
111
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
112
+ process.exit(1);
120
113
  }
121
- },
122
- // ============================================================================
123
- // Memory Tools (Original)
124
- // ============================================================================
125
- {
126
- name: 'remember',
127
- description: 'Store information for future use. Use this when you learn something important that you might need to recall later. Perfect for facts, decisions, code snippets, configuration details, or user preferences.',
128
- inputSchema: {
129
- type: 'object',
130
- properties: {
131
- content: { type: 'string', description: 'Content to store' },
132
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
133
- tags: { type: 'array', items: { type: 'string' } },
134
- project: { type: 'string', description: 'Project path' },
135
- metadata: { type: 'object', description: 'Custom metadata' },
136
- agentId: { type: 'string', description: 'Agent identifier (optional)' },
137
- agentRole: { type: 'string', description: 'Agent role (optional)' },
138
- visibilityScope: { type: 'string', enum: ['private', 'project', 'team', 'global'], description: 'Visibility scope for agent memories' },
139
- sector: { type: 'string', enum: ['episodic', 'semantic', 'procedural', 'autobiographical', 'working'], description: 'Memory sector classification' }
140
- },
141
- required: ['content']
114
+ });
115
+ // squish recall <memoryId>
116
+ program
117
+ .command('recall <memoryId>')
118
+ .description('Retrieve a memory by ID')
119
+ .action(async (memoryId) => {
120
+ try {
121
+ const memory = await getMemoryById(String(memoryId));
122
+ console.log(JSON.stringify({ ok: true, found: !!memory, memory }, null, 2));
142
123
  }
143
- },
144
- {
145
- name: 'recall',
146
- description: 'Retrieve a specific stored memory by ID (like reading a file). Use this when you have a memory ID and want to get its full content.',
147
- inputSchema: {
148
- type: 'object',
149
- properties: {
150
- id: { type: 'string', description: 'Memory UUID' }
151
- },
152
- required: ['id']
124
+ catch (error) {
125
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
126
+ process.exit(1);
153
127
  }
154
- },
155
- {
156
- name: 'search',
157
- description: 'Search your stored memories for information (like grep). Use this when you need to find memories matching a query, or when the user asks about something you might have remembered. Leave query empty to list recent memories.',
158
- inputSchema: {
159
- type: 'object',
160
- properties: {
161
- query: { type: 'string', description: 'Search query (not required for scope=recent)' },
162
- scope: {
163
- type: 'string',
164
- enum: ['memories', 'conversations', 'recent'],
165
- default: 'memories',
166
- description: 'Search scope: memories, conversations, or recent items'
167
- },
168
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
169
- tags: { type: 'array', items: { type: 'string' } },
170
- limit: { type: 'number', default: 10 },
171
- project: { type: 'string' },
172
- role: { type: 'string', enum: ['user', 'assistant'], description: 'Filter by role (conversations scope)' },
173
- n: { type: 'number', description: 'Number of recent items (recent scope)' },
174
- before: { type: 'string', description: 'ISO datetime for recent scope' },
175
- after: { type: 'string', description: 'ISO datetime for recent scope' }
128
+ });
129
+ // squish core_memory view
130
+ // squish core_memory edit persona --content "I am helpful"
131
+ // squish core_memory append user_info --text "Prefers TypeScript"
132
+ program
133
+ .command('core_memory')
134
+ .description('Manage core memory (always-visible context)')
135
+ .argument('[action]', 'view, edit, append', 'view')
136
+ .option('-s, --section <section>', 'Section: persona, user_info, project_context, working_notes')
137
+ .option('-c, --content <content>', 'New content (for edit)')
138
+ .option('-t, --text <text>', 'Text to append (for append)')
139
+ .option('-p, --project <project>', 'Project path', process.cwd())
140
+ .action(async (action, options) => {
141
+ try {
142
+ const project = options.project;
143
+ switch (action) {
144
+ case 'view':
145
+ await initializeCoreMemory(project);
146
+ const core = await getCoreMemory(project);
147
+ const stats = await getCoreMemoryStats(project);
148
+ console.log(JSON.stringify({ ok: true, action, content: core, stats }, null, 2));
149
+ break;
150
+ case 'edit':
151
+ if (!options.section || !options.content) {
152
+ console.log(JSON.stringify({ ok: false, error: '--section and --content required for edit' }, null, 2));
153
+ process.exit(1);
154
+ }
155
+ await initializeCoreMemory(project);
156
+ const editResult = await editCoreMemorySection(project, options.section, String(options.content));
157
+ console.log(JSON.stringify({ ok: editResult.success, action: 'edit', section: options.section, ...editResult }, null, 2));
158
+ break;
159
+ case 'append':
160
+ if (!options.section || !options.text) {
161
+ console.log(JSON.stringify({ ok: false, error: '--section and --text required for append' }, null, 2));
162
+ process.exit(1);
163
+ }
164
+ await initializeCoreMemory(project);
165
+ const appendResult = await appendCoreMemorySection(project, options.section, String(options.text));
166
+ console.log(JSON.stringify({ ok: appendResult.success, action: 'append', section: options.section, ...appendResult }, null, 2));
167
+ break;
168
+ default:
169
+ console.log(JSON.stringify({ ok: false, error: `Unknown action: ${action}` }, null, 2));
170
+ process.exit(1);
176
171
  }
177
172
  }
178
- },
179
- {
180
- name: 'observe',
181
- description: 'Record an observation about your work (tool usage, patterns, errors). Use this to document what you\'re learning about the codebase and problems you encounter.',
182
- inputSchema: {
183
- type: 'object',
184
- properties: {
185
- type: { type: 'string', enum: ['tool_use', 'file_change', 'error', 'pattern', 'insight'] },
186
- action: { type: 'string', description: 'Action taken' },
187
- target: { type: 'string', description: 'Target of action' },
188
- summary: { type: 'string', description: 'Summary' },
189
- details: { type: 'object' },
190
- session: { type: 'string', description: 'Session ID' }
191
- },
192
- required: ['type', 'action', 'summary']
173
+ catch (error) {
174
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
175
+ process.exit(1);
193
176
  }
194
- },
195
- {
196
- name: 'context',
197
- description: 'Get project context',
198
- inputSchema: {
199
- type: 'object',
200
- properties: {
201
- project: { type: 'string', description: 'Project path' },
202
- include: {
203
- type: 'array',
204
- items: { type: 'string', enum: ['memories', 'observations', 'entities', 'messages'] },
205
- default: ['memories', 'observations']
177
+ });
178
+ await program.parseAsync(process.argv);
179
+ }
180
+ // ============================================================================
181
+ // MCP MODE (for Claude Code) - DEFAULT
182
+ // ============================================================================
183
+ async function runMcpMode() {
184
+ const TOOLS = [
185
+ // Core Memory Tool
186
+ {
187
+ name: 'core_memory',
188
+ description: 'View or edit your core memory (always-visible). Use this to see your persona, user info, project context, and working notes.',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ action: { type: 'string', enum: ['view', 'edit', 'append'] },
193
+ projectId: { type: 'string' },
194
+ section: { type: 'string', enum: ['persona', 'user_info', 'project_context', 'working_notes'] },
195
+ content: { type: 'string' },
196
+ text: { type: 'string' },
206
197
  },
207
- limit: { type: 'number', default: 10 }
208
- },
209
- required: ['project']
210
- }
211
- },
212
- {
213
- name: 'init',
214
- description: 'Initialize Squish memory system for the current project. Creates the database structure and default settings. Run once when starting to use Squish in a new project.',
215
- inputSchema: {
216
- type: 'object',
217
- properties: {
218
- projectPath: {
219
- type: 'string',
220
- description: 'Project path (optional, defaults to current directory)'
198
+ required: ['action', 'projectId']
199
+ }
200
+ },
201
+ // Context Paging
202
+ {
203
+ name: 'context_paging',
204
+ description: 'Manage your working memory set. Load, evict, or view loaded memories.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ action: { type: 'string', enum: ['load', 'evict', 'view'] },
209
+ sessionId: { type: 'string' },
210
+ memoryId: { type: 'string' },
211
+ },
212
+ required: ['action', 'sessionId']
213
+ }
214
+ },
215
+ {
216
+ name: 'context_status',
217
+ description: 'View comprehensive context window status and token usage',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ sessionId: { type: 'string' },
222
+ projectId: { type: 'string' },
223
+ },
224
+ required: ['sessionId', 'projectId']
225
+ }
226
+ },
227
+ // Memory Tools
228
+ {
229
+ name: 'remember',
230
+ description: 'Store information for future use. Perfect for facts, decisions, code snippets, configuration details, or user preferences.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ content: { type: 'string' },
235
+ type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
236
+ tags: { type: 'array', items: { type: 'string' } },
237
+ project: { type: 'string' },
238
+ metadata: { type: 'object' },
239
+ },
240
+ required: ['content']
241
+ }
242
+ },
243
+ {
244
+ name: 'recall',
245
+ description: 'Retrieve a specific stored memory by ID',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: { id: { type: 'string' } },
249
+ required: ['id']
250
+ }
251
+ },
252
+ {
253
+ name: 'search',
254
+ description: 'Search your stored memories. Leave query empty to list recent memories.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ query: { type: 'string' },
259
+ scope: { type: 'string', enum: ['memories', 'conversations', 'recent'], default: 'memories' },
260
+ type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
261
+ tags: { type: 'array', items: { type: 'string' } },
262
+ limit: { type: 'number', default: 10 },
263
+ project: { type: 'string' },
221
264
  }
222
265
  }
223
- }
224
- },
225
- {
226
- name: 'health',
227
- description: 'Check service status',
228
- inputSchema: { type: 'object', properties: {} }
229
- },
230
- {
231
- name: 'merge',
232
- description: 'Manage memory merges: detect duplicates, list proposals, preview merges, get statistics, approve, reject, or reverse merges',
233
- inputSchema: {
234
- type: 'object',
235
- properties: {
236
- action: {
237
- type: 'string',
238
- enum: ['detect', 'list', 'preview', 'stats', 'approve', 'reject', 'reverse'],
239
- description: 'Merge action: detect duplicates, list proposals, preview merge, view stats, approve, reject, or reverse'
266
+ },
267
+ {
268
+ name: 'observe',
269
+ description: 'Record an observation about your work (tool usage, patterns, errors)',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ type: { type: 'string', enum: ['tool_use', 'file_change', 'error', 'pattern', 'insight'] },
274
+ action: { type: 'string' },
275
+ target: { type: 'string' },
276
+ summary: { type: 'string' },
277
+ details: { type: 'object' },
240
278
  },
241
- projectId: { type: 'string', description: 'Project ID (required for detect, list, stats)' },
242
- proposalId: { type: 'string', description: 'Proposal ID (required for preview, approve, reject)' },
243
- mergeHistoryId: { type: 'string', description: 'Merge history ID (required for reverse action)' },
244
- threshold: { type: 'number', description: 'Similarity threshold 0-1 (detect action only)' },
245
- memoryType: { type: 'string', enum: ['fact', 'preference', 'decision', 'observation', 'context'], description: 'Memory type filter (detect action only)' },
246
- status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'expired'], description: 'Proposal status filter (list action only)' },
247
- reviewNotes: { type: 'string', description: 'Notes or reason for decision (approve/reject only)' },
248
- limit: { type: 'number', description: 'Max results to return' }
249
- },
250
- required: ['action']
251
- }
252
- },
253
- {
254
- name: 'lifecycle',
255
- description: 'Run memory lifecycle maintenance (decay, eviction, tier updates)',
256
- inputSchema: {
257
- type: 'object',
258
- properties: {
259
- project: { type: 'string', description: 'Project path (optional)' },
260
- force: { type: 'boolean', description: 'Force immediate execution' }
279
+ required: ['type', 'action', 'summary']
280
+ }
281
+ },
282
+ {
283
+ name: 'context',
284
+ description: 'Get project context',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ project: { type: 'string' },
289
+ include: { type: 'array', items: { type: 'string' }, default: ['memories', 'observations'] },
290
+ limit: { type: 'number', default: 10 }
291
+ },
292
+ required: ['project']
293
+ }
294
+ },
295
+ {
296
+ name: 'init',
297
+ description: 'Initialize Squish memory system for the current project',
298
+ inputSchema: {
299
+ type: 'object',
300
+ properties: { projectPath: { type: 'string' } }
301
+ }
302
+ },
303
+ {
304
+ name: 'health',
305
+ description: 'Check service status',
306
+ inputSchema: { type: 'object', properties: {} }
307
+ },
308
+ {
309
+ name: 'merge',
310
+ description: 'Manage memory merges: detect, list, preview, approve, reject, reverse',
311
+ inputSchema: {
312
+ type: 'object',
313
+ properties: {
314
+ action: { type: 'string', enum: ['detect', 'list', 'preview', 'stats', 'approve', 'reject', 'reverse'] },
315
+ projectId: { type: 'string' },
316
+ proposalId: { type: 'string' },
317
+ threshold: { type: 'number' },
318
+ },
319
+ required: ['action']
320
+ }
321
+ },
322
+ {
323
+ name: 'qmd_search',
324
+ description: 'Search memories using QMD hybrid search (BM25 + vector + rerank)',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ query: { type: 'string' },
329
+ type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
330
+ limit: { type: 'number', default: 10 },
331
+ },
332
+ required: ['query']
261
333
  }
262
334
  }
263
- },
264
- {
265
- name: 'summarize_session',
266
- description: 'Summarize a conversation session',
267
- inputSchema: {
268
- type: 'object',
269
- properties: {
270
- conversationId: { type: 'string', description: 'Conversation UUID' },
271
- type: { type: 'string', enum: ['incremental', 'rolling', 'final'], description: 'Summary type' }
272
- },
273
- required: ['conversationId', 'type']
274
- }
275
- },
276
- {
277
- name: 'protect_memory',
278
- description: 'Mark memory as protected (cannot be evicted)',
279
- inputSchema: {
280
- type: 'object',
281
- properties: {
282
- memoryId: { type: 'string', description: 'Memory UUID' },
283
- reason: { type: 'string', description: 'Protection reason' }
284
- },
285
- required: ['memoryId', 'reason']
286
- }
287
- },
288
- {
289
- name: 'pin_memory',
290
- description: 'Pin memory for automatic injection into context',
291
- inputSchema: {
292
- type: 'object',
293
- properties: {
294
- memoryId: { type: 'string', description: 'Memory UUID' }
295
- },
296
- required: ['memoryId']
297
- }
298
- },
299
- {
300
- name: 'get_related',
301
- description: 'Find memories related to a specific memory by ID. Use this for iterative search - if a search result is relevant, find similar memories without needing new queries.',
302
- inputSchema: {
303
- type: 'object',
304
- properties: {
305
- memoryId: { type: 'string', description: 'Memory UUID' },
306
- limit: { type: 'number', description: 'Max results (default: 10)' }
307
- },
308
- required: ['memoryId']
309
- }
310
- },
311
- // ============================================================================
312
- // QMD Hybrid Search Tool (v0.7.0)
313
- // ============================================================================
314
- {
315
- name: 'qmd_search',
316
- description: 'Search memories using QMD hybrid search (BM25 + vector + rerank). Provides higher quality results when QMD is available. Falls back to standard search if QMD is not installed.',
317
- inputSchema: {
318
- type: 'object',
319
- properties: {
320
- query: { type: 'string', description: 'Search query' },
321
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'], description: 'Memory type filter' },
322
- limit: { type: 'number', description: 'Max results (default: 10)', default: 10 },
323
- useHybrid: { type: 'boolean', description: 'Use full hybrid pipeline with reranking (default: true)', default: true },
324
- collection: { type: 'string', description: 'Override default collection mapping' }
325
- },
326
- required: ['query']
335
+ ];
336
+ class Squish {
337
+ server;
338
+ constructor() {
339
+ this.server = new Server({ name: 'squish', version: VERSION }, { capabilities: { tools: {} } });
340
+ this.setup();
327
341
  }
328
- }
329
- ];
330
- class Squish {
331
- server;
332
- constructor() {
333
- this.server = new Server({ name: 'squish', version: VERSION }, { capabilities: { tools: {} } });
334
- this.setup();
335
- }
336
- setup() {
337
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
338
- tools: TOOLS
339
- }));
340
- this.server.setRequestHandler(CallToolRequestSchema, async (req) => {
341
- const { name } = req.params;
342
- const args = (req.params.arguments ?? {});
343
- try {
344
- switch (name) {
345
- case 'core_memory':
346
- return await this.handleCoreMemory(args);
347
- case 'context_paging':
348
- return await this.handleContextPaging(args);
349
- case 'context_status': {
350
- if (!args.sessionId || !args.projectId) {
351
- throw new McpError(ErrorCode.InvalidParams, 'sessionId and projectId are required');
352
- }
353
- try {
342
+ setup() {
343
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
344
+ tools: TOOLS
345
+ }));
346
+ this.server.setRequestHandler(CallToolRequestSchema, async (req) => {
347
+ const { name } = req.params;
348
+ const args = (req.params.arguments ?? {});
349
+ try {
350
+ switch (name) {
351
+ case 'core_memory':
352
+ return await this.handleCoreMemory(args);
353
+ case 'context_paging':
354
+ return await this.handleContextPaging(args);
355
+ case 'context_status': {
354
356
  const result = await getContextStatus(String(args.sessionId), String(args.projectId));
355
357
  return this.jsonResponse({ ok: true, ...result });
356
358
  }
357
- catch (error) {
358
- throw new McpError(ErrorCode.InternalError, `Context status failed: ${error.message}`);
359
- }
360
- }
361
- // Original Memory Tools
362
- case 'remember': {
363
- if (typeof args.content !== 'string' || !args.content) {
364
- throw new McpError(ErrorCode.InvalidParams, 'content is required');
365
- }
366
- try {
367
- // Check if agent context is provided
368
- if (args.agentId) {
369
- const memoryId = await storeAgentMemory(args.content, {
370
- agentId: args.agentId,
371
- agentRole: args.agentRole,
372
- }, {
373
- type: args.type,
374
- visibilityScope: args.visibilityScope,
375
- sector: args.sector,
376
- tags: args.tags,
377
- metadata: args.metadata,
378
- });
379
- return this.jsonResponse({ ok: true, memoryId, agentContext: true });
380
- }
381
- else {
382
- // Standard memory storage
383
- return this.jsonResponse({ ok: true, data: await rememberMemory(args) });
384
- }
385
- }
386
- catch (dbError) {
387
- if (isDatabaseUnavailableError(dbError)) {
388
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - memory storage disabled');
389
- }
390
- throw dbError;
359
+ case 'remember': {
360
+ return this.jsonResponse({ ok: true, data: await rememberMemory(args) });
391
361
  }
392
- }
393
- case 'recall': {
394
- if (!args.id) {
395
- throw new McpError(ErrorCode.InvalidParams, 'id is required');
396
- }
397
- try {
362
+ case 'recall': {
398
363
  const memory = await getMemoryById(String(args.id));
399
364
  return this.jsonResponse({ ok: true, found: !!memory, data: memory });
400
365
  }
401
- catch (dbError) {
402
- if (isDatabaseUnavailableError(dbError)) {
403
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - memory retrieval disabled');
404
- }
405
- throw dbError;
406
- }
407
- }
408
- case 'search': {
409
- const scope = args.scope || 'memories';
410
- try {
411
- if (scope === 'memories') {
412
- if (typeof args.query !== 'string' || !args.query) {
413
- throw new McpError(ErrorCode.InvalidParams, 'query is required for memory search');
414
- }
415
- return this.jsonResponse({ ok: true, scope: 'memories', data: await searchMemories(args) });
416
- }
417
- else if (scope === 'conversations') {
418
- if (typeof args.query !== 'string' || !args.query) {
419
- throw new McpError(ErrorCode.InvalidParams, 'query is required for conversation search');
420
- }
421
- return this.jsonResponse({ ok: true, scope: 'conversations', data: await searchConversations(args) });
422
- }
423
- else if (scope === 'recent') {
424
- return this.jsonResponse({ ok: true, scope: 'recent', data: await getRecentConversations(args) });
425
- }
426
- else {
427
- throw new McpError(ErrorCode.InvalidParams, `Unknown scope: ${scope}`);
428
- }
429
- }
430
- catch (dbError) {
431
- if (isDatabaseUnavailableError(dbError)) {
432
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - search disabled');
433
- }
434
- throw dbError;
435
- }
436
- }
437
- case 'observe': {
438
- if (!args.type || !args.action || !args.summary) {
439
- throw new McpError(ErrorCode.InvalidParams, 'type, action, and summary are required');
366
+ case 'search': {
367
+ return this.jsonResponse({ ok: true, data: await searchMemories(args) });
440
368
  }
441
- try {
369
+ case 'observe':
442
370
  return this.jsonResponse({ ok: true, data: await createObservation(args) });
443
- }
444
- catch (dbError) {
445
- if (isDatabaseUnavailableError(dbError)) {
446
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - observation storage disabled');
447
- }
448
- throw dbError;
449
- }
450
- }
451
- case 'context': {
452
- if (!args.project) {
453
- throw new McpError(ErrorCode.InvalidParams, 'project is required');
454
- }
455
- try {
371
+ case 'context':
456
372
  return this.jsonResponse({ ok: true, data: await getProjectContext(args) });
457
- }
458
- catch (dbError) {
459
- if (isDatabaseUnavailableError(dbError)) {
460
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - context retrieval disabled');
461
- }
462
- throw dbError;
463
- }
464
- }
465
- case 'init': {
466
- const { projectPath } = args;
467
- const targetPath = projectPath || process.cwd();
468
- const { ensureProject } = await import('./core/projects.js');
469
- const { ensureDataDirectory } = await import('./db/bootstrap.js');
470
- const path = await import('path');
471
- try {
472
- // Ensure data directory exists
373
+ case 'init': {
374
+ const { ensureProject } = await import('./core/projects.js');
473
375
  await ensureDataDirectory();
474
- // Ensure project exists
475
- const project = await ensureProject(targetPath);
476
- if (!project) {
477
- return this.jsonResponse({
478
- success: false,
479
- error: 'Failed to create project',
480
- });
481
- }
482
- return this.jsonResponse({
483
- success: true,
484
- message: `Squish initialized for project: ${project.name}`,
485
- project: {
486
- id: project.id,
487
- name: project.name,
488
- path: project.path,
489
- },
490
- nextSteps: [
491
- 'Squish is now ready to capture and store memories',
492
- 'Use /squish:remember to store important information',
493
- 'Use /squish:search to find stored memories',
494
- 'Use /squish:core_memory to view/edit your always-visible context',
495
- ],
496
- });
497
- }
498
- catch (error) {
499
- return this.jsonResponse({
500
- success: false,
501
- error: error instanceof Error ? error.message : String(error),
502
- });
503
- }
504
- }
505
- case 'health':
506
- return this.health();
507
- case 'merge':
508
- return await this.handleMerge(args);
509
- case 'lifecycle': {
510
- const { project } = args;
511
- const result = await forceLifecycleMaintenance(project);
512
- return this.jsonResponse({
513
- success: true,
514
- message: 'Lifecycle maintenance completed',
515
- stats: result,
516
- });
517
- }
518
- case 'summarize_session': {
519
- const { conversationId, type } = args;
520
- if (!conversationId || !type) {
521
- throw new McpError(ErrorCode.InvalidParams, 'conversationId and type are required');
522
- }
523
- const result = await summarizeSession(conversationId, type);
524
- return this.jsonResponse({
525
- success: true,
526
- summaryId: result.summaryId,
527
- tokensSaved: result.tokensSaved,
528
- message: `Session summarized with type: ${type}`,
529
- });
530
- }
531
- case 'protect_memory': {
532
- const { memoryId, reason } = args;
533
- if (!memoryId || !reason) {
534
- throw new McpError(ErrorCode.InvalidParams, 'memoryId and reason are required');
376
+ const project = await ensureProject(args.projectPath || process.cwd());
377
+ return this.jsonResponse({ success: true, project });
535
378
  }
536
- await protectMemory(memoryId, reason);
537
- return this.jsonResponse({
538
- success: true,
539
- memoryId,
540
- message: 'Memory protected from eviction',
541
- });
542
- }
543
- case 'pin_memory': {
544
- const { memoryId } = args;
545
- if (!memoryId) {
546
- throw new McpError(ErrorCode.InvalidParams, 'memoryId is required');
547
- }
548
- await pinMemory(memoryId);
549
- return this.jsonResponse({
550
- success: true,
551
- memoryId,
552
- message: 'Memory pinned for auto-injection',
553
- });
554
- }
555
- case 'get_related': {
556
- const { memoryId, limit } = args;
557
- if (!memoryId) {
558
- throw new McpError(ErrorCode.InvalidParams, 'memoryId is required');
559
- }
560
- const related = await getRelatedMemories(memoryId, limit || 10);
561
- return this.jsonResponse({
562
- success: true,
563
- memoryId,
564
- relatedCount: related.length,
565
- memories: related,
566
- });
567
- }
568
- case 'qmd_search': {
569
- if (typeof args.query !== 'string' || !args.query) {
570
- throw new McpError(ErrorCode.InvalidParams, 'query is required');
571
- }
572
- try {
379
+ case 'health':
380
+ return this.health();
381
+ case 'merge':
382
+ return await this.handleMerge(args);
383
+ case 'qmd_search': {
573
384
  const available = await isQMDAvailable();
574
385
  if (!available) {
575
- return this.jsonResponse({
576
- ok: true,
577
- qmdAvailable: false,
578
- message: 'QMD is not installed. Install with: bun install -g qmd',
579
- fallback: 'Using standard Squish search',
580
- data: await searchMemories(args)
581
- });
582
- }
583
- const results = await searchWithQMD({
584
- query: args.query,
585
- type: args.type,
586
- limit: args.limit || 10,
587
- useHybrid: args.useHybrid ?? true,
588
- collection: args.collection
589
- });
590
- return this.jsonResponse({
591
- ok: true,
592
- qmdAvailable: true,
593
- data: results
594
- });
595
- }
596
- catch (dbError) {
597
- if (isDatabaseUnavailableError(dbError)) {
598
- throw new McpError(ErrorCode.InternalError, 'Database unavailable - search disabled');
386
+ return this.jsonResponse({ ok: true, qmdAvailable: false, data: await searchMemories(args) });
599
387
  }
600
- throw dbError;
388
+ return this.jsonResponse({ ok: true, qmdAvailable: true, data: await searchWithQMD(args) });
601
389
  }
390
+ default:
391
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
602
392
  }
603
- default:
604
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
605
393
  }
606
- }
607
- catch (error) {
608
- if (error instanceof McpError)
609
- throw error;
610
- logger.error('Tool error', error);
611
- throw new McpError(ErrorCode.InternalError, `Tool '${name}' failed`);
612
- }
613
- });
614
- this.server.onerror = (e) => logger.error('MCP Server error', e);
615
- process.on('SIGINT', () => this.shutdown());
616
- process.on('SIGTERM', () => this.shutdown());
617
- }
618
- jsonResponse(payload) {
619
- return {
620
- content: [{
621
- type: 'text',
622
- text: JSON.stringify(payload, null, 2)
623
- }]
624
- };
625
- }
626
- async handleCoreMemory(args) {
627
- const action = args.action;
628
- if (!args.projectId) {
629
- throw new McpError(ErrorCode.InvalidParams, 'projectId is required');
394
+ catch (error) {
395
+ if (error instanceof McpError)
396
+ throw error;
397
+ throw new McpError(ErrorCode.InternalError, `Tool '${name}' failed`);
398
+ }
399
+ });
400
+ this.server.onerror = (e) => logger.error('MCP Server error', e);
401
+ process.on('SIGINT', () => this.shutdown());
402
+ process.on('SIGTERM', () => this.shutdown());
630
403
  }
631
- const projectId = String(args.projectId);
632
- try {
404
+ jsonResponse(payload) {
405
+ return {
406
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
407
+ };
408
+ }
409
+ async handleCoreMemory(args) {
410
+ const action = args.action;
411
+ const projectId = String(args.projectId);
633
412
  await initializeCoreMemory(projectId);
634
413
  if (action === 'view') {
635
414
  const content = await getCoreMemory(projectId);
@@ -637,148 +416,67 @@ class Squish {
637
416
  return this.jsonResponse({ ok: true, action: 'view', content, stats });
638
417
  }
639
418
  if (action === 'edit') {
640
- if (!args.section || typeof args.content !== 'string') {
641
- throw new McpError(ErrorCode.InvalidParams, 'section and content are required for edit action');
642
- }
643
419
  const result = await editCoreMemorySection(projectId, args.section, String(args.content));
644
- if (!result.success) {
645
- throw new McpError(ErrorCode.InvalidParams, result.message || 'Edit failed');
646
- }
647
420
  return this.jsonResponse({ ok: true, action: 'edit', ...result });
648
421
  }
649
422
  if (action === 'append') {
650
- if (!args.section || typeof args.text !== 'string') {
651
- throw new McpError(ErrorCode.InvalidParams, 'section and text are required for append action');
652
- }
653
423
  const result = await appendCoreMemorySection(projectId, args.section, String(args.text));
654
- if (!result.success) {
655
- throw new McpError(ErrorCode.InvalidParams, result.message || 'Append failed');
656
- }
657
424
  return this.jsonResponse({ ok: true, action: 'append', ...result });
658
425
  }
659
- throw new McpError(ErrorCode.InvalidParams, `Unknown core_memory action: ${action}`);
660
- }
661
- catch (error) {
662
- if (error instanceof McpError)
663
- throw error;
664
- throw new McpError(ErrorCode.InternalError, `Core memory failed: ${error.message}`);
665
- }
666
- }
667
- async handleContextPaging(args) {
668
- const action = args.action;
669
- if (!args.sessionId) {
670
- throw new McpError(ErrorCode.InvalidParams, 'sessionId is required');
426
+ throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
671
427
  }
672
- const sessionId = String(args.sessionId);
673
- try {
428
+ async handleContextPaging(args) {
429
+ const action = args.action;
430
+ const sessionId = String(args.sessionId);
674
431
  if (action === 'load') {
675
- if (!args.memoryId) {
676
- throw new McpError(ErrorCode.InvalidParams, 'memoryId is required for load action');
677
- }
678
- const result = await loadMemoryToContext(sessionId, String(args.memoryId));
679
- if (!result.success) {
680
- throw new McpError(ErrorCode.InvalidParams, result.message || 'Load failed');
681
- }
682
- return this.jsonResponse({ ok: true, action: 'load', ...result });
432
+ return this.jsonResponse(await loadMemoryToContext(sessionId, String(args.memoryId)));
683
433
  }
684
434
  if (action === 'evict') {
685
- if (!args.memoryId) {
686
- throw new McpError(ErrorCode.InvalidParams, 'memoryId is required for evict action');
687
- }
688
- const result = await evictMemoryFromContext(sessionId, String(args.memoryId));
689
- if (!result.success) {
690
- throw new McpError(ErrorCode.InvalidParams, result.message || 'Evict failed');
691
- }
692
- return this.jsonResponse({ ok: true, action: 'evict', ...result });
435
+ return this.jsonResponse(await evictMemoryFromContext(sessionId, String(args.memoryId)));
693
436
  }
694
437
  if (action === 'view') {
695
- const result = await viewLoadedMemories(sessionId);
696
- return this.jsonResponse({ ok: true, action: 'view', ...result });
438
+ return this.jsonResponse(await viewLoadedMemories(sessionId));
697
439
  }
698
- throw new McpError(ErrorCode.InvalidParams, `Unknown context_paging action: ${action}`);
440
+ throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
699
441
  }
700
- catch (error) {
701
- if (error instanceof McpError)
702
- throw error;
703
- throw new McpError(ErrorCode.InternalError, `Context paging failed: ${error.message}`);
704
- }
705
- }
706
- async handleMerge(args) {
707
- const action = args.action;
708
- try {
709
- if (action === 'detect') {
442
+ async handleMerge(args) {
443
+ const action = args.action;
444
+ if (action === 'detect')
710
445
  return this.jsonResponse(await handleDetectDuplicates(args));
711
- }
712
- if (action === 'list') {
446
+ if (action === 'list')
713
447
  return this.jsonResponse(await handleListProposals(args));
714
- }
715
- if (action === 'preview') {
448
+ if (action === 'preview')
716
449
  return this.jsonResponse(await handlePreviewMerge(args));
717
- }
718
- if (action === 'stats') {
450
+ if (action === 'stats')
719
451
  return this.jsonResponse(await handleGetMergeStats(args));
720
- }
721
- if (action === 'approve') {
452
+ if (action === 'approve')
722
453
  return this.jsonResponse(await handleApproveMerge(args));
723
- }
724
- if (action === 'reject') {
454
+ if (action === 'reject')
725
455
  return this.jsonResponse(await handleRejectMerge(args));
726
- }
727
- if (action === 'reverse') {
456
+ if (action === 'reverse')
728
457
  return this.jsonResponse(await handleReverseMerge(args));
729
- }
730
- throw new McpError(ErrorCode.InvalidParams, `Unknown merge action: ${action}`);
458
+ throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
731
459
  }
732
- catch (error) {
733
- if (error instanceof McpError)
734
- throw error;
735
- throw new McpError(ErrorCode.InternalError, `Merge operation failed: ${error.message}`);
460
+ async shutdown() {
461
+ await closeCache();
462
+ process.exit(0);
736
463
  }
737
- }
738
- async shutdown() {
739
- await closeCache();
740
- process.exit(0);
741
- }
742
- async health() {
743
- let dbOk = false;
744
- let dbStatus = 'ok';
745
- try {
746
- dbOk = await checkDatabaseHealth();
747
- dbStatus = dbOk ? 'ok' : 'unavailable';
464
+ async health() {
465
+ const dbOk = await checkDatabaseHealth();
466
+ const redisOk = await checkRedisHealth();
467
+ return this.jsonResponse({
468
+ version: VERSION,
469
+ mode: config.isTeamMode ? 'team' : 'local',
470
+ status: dbOk ? 'ok' : 'error',
471
+ });
748
472
  }
749
- catch (error) {
750
- if (isDatabaseUnavailableError(error)) {
751
- dbStatus = 'unavailable';
752
- }
753
- else {
754
- dbStatus = 'error';
755
- }
473
+ async run() {
474
+ const transport = new StdioServerTransport();
475
+ await this.server.connect(transport);
476
+ logger.info(`v${VERSION}`);
477
+ startWebServer();
756
478
  }
757
- const redisOk = await checkRedisHealth();
758
- const overallStatus = determineOverallStatus(dbStatus, redisOk);
759
- return this.jsonResponse({
760
- version: VERSION,
761
- mode: config.isTeamMode ? 'team' : 'local',
762
- database: config.isTeamMode ? 'postgresql' : 'sqlite',
763
- cache: config.redisEnabled ? 'redis' : 'memory',
764
- embeddingsProvider: config.embeddingsProvider,
765
- status: overallStatus,
766
- checks: {
767
- database: dbStatus,
768
- cache: redisOk ? 'ok' : 'error'
769
- }
770
- });
771
- }
772
- async run() {
773
- const transport = new StdioServerTransport();
774
- await this.server.connect(transport);
775
- logger.info(`v${VERSION}`);
776
- // Start web UI server in background
777
- startWebServer();
778
479
  }
480
+ new Squish().run();
779
481
  }
780
- new Squish().run().catch((e) => {
781
- logger.error('Fatal error', e);
782
- process.exit(1);
783
- });
784
482
  //# sourceMappingURL=index.js.map