vektor-slipstream 1.2.2 → 1.3.0

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/index.js ADDED
@@ -0,0 +1,480 @@
1
+ /**
2
+ * cloak/index.js
3
+ * MCP server — exposes all 4 CLOAK tools to Claude Code.
4
+ * Wires cortex, axon, cerebellum, and token into a single MCP endpoint.
5
+ *
6
+ * Usage:
7
+ * node index.js # stdio MCP server (Claude Code)
8
+ * CLOAK_HTTP=1 node index.js # HTTP MCP server (port 3747)
9
+ *
10
+ * Claude Code .claude/settings.json:
11
+ * {
12
+ * "mcpServers": {
13
+ * "cloak": {
14
+ * "command": "node",
15
+ * "args": ["/path/to/cloak/index.js"],
16
+ * "env": {
17
+ * "VEKTOR_LICENCE_KEY": "your-key",
18
+ * "CLOAK_PROJECT_PATH": "/path/to/your/project"
19
+ * }
20
+ * }
21
+ * },
22
+ * "hooks": {
23
+ * "SessionStart": [{ "type": "command", "command": "node /path/to/cloak/index.js --hook SessionStart" }],
24
+ * "Stop": [{ "type": "command", "command": "node /path/to/cloak/index.js --hook Stop" }],
25
+ * "PreToolUse": [{ "type": "command", "command": "node /path/to/cloak/index.js --hook PreToolUse" }],
26
+ * "PostToolUse": [{ "type": "command", "command": "node /path/to/cloak/index.js --hook PostToolUse" }]
27
+ * }
28
+ * }
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ const { createMemory } = require('vektor-slipstream');
34
+ const cortex = require('./cortex');
35
+ const axon = require('./axon');
36
+ const cerebellum = require('./cerebellum');
37
+ const token = require('./token');
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Config
41
+ // ---------------------------------------------------------------------------
42
+ const CONFIG = {
43
+ projectPath : process.env.CLOAK_PROJECT_PATH || process.cwd(),
44
+ licenceKey : process.env.VEKTOR_LICENCE_KEY || '',
45
+ agentId : process.env.CLOAK_AGENT_ID || 'cloak-agent',
46
+ httpPort : parseInt(process.env.CLOAK_PORT || '3747'),
47
+ debug : process.env.CLOAK_DEBUG === '1',
48
+ };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Vektor memory instance — singleton, shared across all tools
52
+ // ---------------------------------------------------------------------------
53
+ let _memory = null;
54
+
55
+ async function getMemory() {
56
+ if (_memory) return _memory;
57
+ _memory = await createMemory({
58
+ agentId : CONFIG.agentId,
59
+ licenceKey : CONFIG.licenceKey,
60
+ });
61
+ return _memory;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // MCP Tool definitions
66
+ // ---------------------------------------------------------------------------
67
+ const TOOLS = [
68
+ {
69
+ name : 'cloak_cortex',
70
+ description: 'Scan project files and build a token-aware anatomy index in Vektor entity graph. Call once on project init and after major refactors. Returns file count, total tokens, and anatomy summary.',
71
+ inputSchema: {
72
+ type : 'object',
73
+ properties: {
74
+ force: {
75
+ type : 'boolean',
76
+ description: 'Re-scan all files even if unchanged since last scan. Default: false.',
77
+ default : false,
78
+ },
79
+ projectPath: {
80
+ type : 'string',
81
+ description: 'Override project path. Defaults to CLOAK_PROJECT_PATH env var.',
82
+ },
83
+ },
84
+ },
85
+ },
86
+ {
87
+ name : 'cloak_axon',
88
+ description: 'Session boundary handler. Call on session start to load last context, call on session stop to save a MemCell summary. Also accepts hook payloads from Claude Code.',
89
+ inputSchema: {
90
+ type : 'object',
91
+ properties: {
92
+ action: {
93
+ type : 'string',
94
+ enum : ['start', 'stop'],
95
+ description: 'start: load last session context and seed new session. stop: save session MemCell summary to memory.',
96
+ },
97
+ narrative: {
98
+ type : 'string',
99
+ description: 'Optional: brief description of what this session accomplished and why. Stored as session intent in the MemCell. Example: "Refactored auth module to fix null reference bug on empty DB responses."',
100
+ },
101
+ },
102
+ required: ['action'],
103
+ },
104
+ },
105
+ {
106
+ name : 'cloak_cerebellum',
107
+ description: 'Pre-write enforcer. Check a write against known error patterns before executing. Also records new errors and Do-Not-Repeat rules. Auto-detects [RESOLVED_BY] relationships.',
108
+ inputSchema: {
109
+ type : 'object',
110
+ properties: {
111
+ action: {
112
+ type : 'string',
113
+ enum : ['check', 'record_error', 'add_dnr'],
114
+ description: 'check: check content before write. record_error: store a new error pattern. add_dnr: add a Do-Not-Repeat rule.',
115
+ },
116
+ content: {
117
+ type : 'string',
118
+ description: 'Content to check or write (required for check action).',
119
+ },
120
+ filePath: {
121
+ type : 'string',
122
+ description: 'File path being written.',
123
+ },
124
+ description: {
125
+ type : 'string',
126
+ description: 'Error description (required for record_error).',
127
+ },
128
+ errorType: {
129
+ type : 'string',
130
+ description: 'Error category e.g. null_reference, type_mismatch.',
131
+ },
132
+ fix: {
133
+ type : 'string',
134
+ description: 'What fixed the error (optional).',
135
+ },
136
+ severity: {
137
+ type : 'string',
138
+ enum : ['warn', 'block'],
139
+ description: 'Warning severity. Default: warn.',
140
+ },
141
+ keyword: {
142
+ type : 'string',
143
+ description: 'Trigger keyword for DNR rule (required for add_dnr).',
144
+ },
145
+ rule: {
146
+ type : 'string',
147
+ description: 'DNR rule description (required for add_dnr).',
148
+ },
149
+ alternative: {
150
+ type : 'string',
151
+ description: 'What to do instead (optional for add_dnr).',
152
+ },
153
+ },
154
+ required: ['action'],
155
+ },
156
+ },
157
+ {
158
+ name : 'cloak_token',
159
+ description: 'Token ledger. Track reads/writes, detect waste patterns, get session summary and historical waste report.',
160
+ inputSchema: {
161
+ type : 'object',
162
+ properties: {
163
+ action: {
164
+ type : 'string',
165
+ enum : ['summary', 'waste_report'],
166
+ description: 'summary: get current session token stats. waste_report: get historical waste analysis over N days.',
167
+ },
168
+ days: {
169
+ type : 'integer',
170
+ description: 'Lookback days for waste report. Default: 30.',
171
+ default : 30,
172
+ },
173
+ },
174
+ required: ['action'],
175
+ },
176
+ },
177
+ ];
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Tool handlers
181
+ // ---------------------------------------------------------------------------
182
+ async function handleTool(name, input) {
183
+ const memory = await getMemory();
184
+
185
+ switch (name) {
186
+
187
+ // ── cloak_cortex ────────────────────────────────────────────────────────
188
+ case 'cloak_cortex': {
189
+ const result = await cortex.runCortex({
190
+ projectPath: input.projectPath || CONFIG.projectPath,
191
+ memory,
192
+ force: input.force || false,
193
+ });
194
+ return {
195
+ success : true,
196
+ scanned : result.scanned,
197
+ skipped : result.skipped,
198
+ written : result.written,
199
+ totalTokens: result.totalTokens,
200
+ topFiles : result.anatomy
201
+ .sort((a,b) => b.tokens - a.tokens)
202
+ .slice(0, 10)
203
+ .map(f => `${f.path}: ~${f.tokens}t`),
204
+ message : `Anatomy indexed: ${result.written} new nodes, ${result.skipped} unchanged, ~${result.totalTokens} total tokens`,
205
+ };
206
+ }
207
+
208
+ // ── cloak_axon ──────────────────────────────────────────────────────────
209
+ case 'cloak_axon': {
210
+ switch (input.action) {
211
+
212
+ case 'start': {
213
+ const result = await axon.onSessionStart({
214
+ projectPath: CONFIG.projectPath,
215
+ memory,
216
+ });
217
+ // Start token ledger in sync
218
+ token.startSession(result.sessionId);
219
+ return {
220
+ success : true,
221
+ sessionId : result.sessionId,
222
+ hasLastContext: !!result.lastContext,
223
+ lastContext : result.lastContext || null,
224
+ message : result.lastContext
225
+ ? `Session ${result.sessionId} started. Last session context loaded.`
226
+ : `Session ${result.sessionId} started. No prior context (first run or new project).`,
227
+ };
228
+ }
229
+
230
+ case 'stop': {
231
+ // Get token summary before stopping session
232
+ const tokenSummary = token.getSessionSummary();
233
+ const tokenCount = tokenSummary?.totalTokens || 0;
234
+
235
+ // Write token ledger node to Vektor if there was activity
236
+ if (tokenSummary && tokenCount > 0) {
237
+ const tokenNode = token.buildTokenNode(tokenSummary);
238
+ try { await memory.remember(tokenNode); } catch {}
239
+ }
240
+
241
+ const result = await axon.onSessionStop({
242
+ memory,
243
+ tokenCount,
244
+ narrative: input.narrative || '',
245
+ });
246
+ return {
247
+ success : true,
248
+ sessionId : result.sessionId,
249
+ written : result.written,
250
+ events : result.events,
251
+ duration : `${result.duration}min`,
252
+ tokenCount,
253
+ wasteRate : tokenSummary?.wasteRate || '0%',
254
+ message : result.written
255
+ ? `Session MemCell written. ${result.events} events, ~${tokenCount} tokens, ${tokenSummary?.wasteRate || '0%'} waste.`
256
+ : `Session ended but MemCell write failed.`,
257
+ };
258
+ }
259
+
260
+ case 'hook': {
261
+ if (!input.hookPayload) return { success: false, error: 'hookPayload required for hook action' };
262
+ const result = await axon.handleHookPayload({
263
+ payload : input.hookPayload,
264
+ memory,
265
+ projectPath: CONFIG.projectPath,
266
+ tokenCount : token.getSessionSummary()?.totalTokens || 0,
267
+ });
268
+ return { success: true, result };
269
+ }
270
+
271
+ default:
272
+ return { success: false, error: `Unknown axon action: ${input.action}` };
273
+ }
274
+ }
275
+
276
+ // ── cloak_cerebellum ────────────────────────────────────────────────────
277
+ case 'cloak_cerebellum': {
278
+ switch (input.action) {
279
+
280
+ case 'check': {
281
+ if (!input.content) return { success: false, error: 'content required for check action' };
282
+ const result = await cerebellum.checkWrite({
283
+ content : input.content,
284
+ filePath: input.filePath,
285
+ memory,
286
+ });
287
+ return {
288
+ success : true,
289
+ warnings : result.warnings,
290
+ hasBlockers : result.hasBlockers,
291
+ shouldProceed: result.shouldProceed,
292
+ message : result.warnings.length > 0
293
+ ? `${result.warnings.length} pattern(s) matched. Review warnings before proceeding.`
294
+ : 'No known error patterns matched. Safe to proceed.',
295
+ };
296
+ }
297
+
298
+ case 'record_error': {
299
+ if (!input.description) return { success: false, error: 'description required for record_error' };
300
+ const result = await cerebellum.recordError({
301
+ description: input.description,
302
+ filePath : input.filePath,
303
+ errorType : input.errorType,
304
+ fix : input.fix,
305
+ severity : input.severity,
306
+ memory,
307
+ });
308
+ return { success: result.written, message: result.written ? 'Error pattern recorded.' : result.error };
309
+ }
310
+
311
+ case 'add_dnr': {
312
+ if (!input.keyword || !input.rule) return { success: false, error: 'keyword and rule required for add_dnr' };
313
+ const result = await cerebellum.addDNRRule({
314
+ keyword : input.keyword,
315
+ rule : input.rule,
316
+ alternative: input.alternative,
317
+ memory,
318
+ });
319
+ return { success: result.written, message: result.written ? `DNR rule added for keyword: ${input.keyword}` : result.error };
320
+ }
321
+
322
+ case 'hook': {
323
+ if (!input.hookPayload) return { success: false, error: 'hookPayload required for hook action' };
324
+ const result = await cerebellum.handleHookPayload({ payload: input.hookPayload, memory });
325
+ return { success: true, result };
326
+ }
327
+
328
+ default:
329
+ return { success: false, error: `Unknown cerebellum action: ${input.action}` };
330
+ }
331
+ }
332
+
333
+ // ── cloak_token ─────────────────────────────────────────────────────────
334
+ case 'cloak_token': {
335
+ switch (input.action) {
336
+
337
+ case 'summary': {
338
+ const summary = token.getSessionSummary();
339
+ if (!summary) return { success: false, error: 'No active session. Call cloak_axon start first.' };
340
+ return { success: true, ...summary };
341
+ }
342
+
343
+ case 'waste_report': {
344
+ const report = await token.getWasteReport({ memory, days: input.days || 30 });
345
+ return { success: true, ...report };
346
+ }
347
+
348
+ case 'hook': {
349
+ if (!input.hookPayload) return { success: false, error: 'hookPayload required for hook action' };
350
+ token.handleHookPayload({
351
+ payload : input.hookPayload,
352
+ sessionId: axon._sessionLog.sessionId,
353
+ });
354
+ return { success: true };
355
+ }
356
+
357
+ default:
358
+ return { success: false, error: `Unknown token action: ${input.action}` };
359
+ }
360
+ }
361
+
362
+ default:
363
+ return { success: false, error: `Unknown tool: ${name}` };
364
+ }
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Hook-only mode — called from Claude Code hook scripts (not MCP)
369
+ // Usage: node index.js --hook PreToolUse
370
+ // Claude Code pipes JSON payload via stdin
371
+ // ---------------------------------------------------------------------------
372
+ async function runHookMode(hookName) {
373
+ let payload = {};
374
+
375
+ try {
376
+ const stdin = fs.readFileSync('/dev/stdin', 'utf8');
377
+ if (stdin.trim()) payload = JSON.parse(stdin);
378
+ } catch {
379
+ // stdin not available or not JSON — use empty payload
380
+ }
381
+
382
+ payload.hook_event_name = hookName;
383
+
384
+ const memory = await getMemory();
385
+
386
+ // Route to all relevant handlers
387
+ await Promise.allSettled([
388
+ axon.handleHookPayload({ payload, memory, projectPath: CONFIG.projectPath,
389
+ tokenCount: token.getSessionSummary()?.totalTokens || 0 }),
390
+ cerebellum.handleHookPayload({ payload, memory }),
391
+ Promise.resolve(token.handleHookPayload({ payload,
392
+ sessionId: axon._sessionLog.sessionId })),
393
+ ]);
394
+
395
+ process.exit(0);
396
+ }
397
+
398
+ // ---------------------------------------------------------------------------
399
+ // MCP stdio server (standard Claude Code MCP protocol)
400
+ // ---------------------------------------------------------------------------
401
+ const readline = require('readline');
402
+ const fs = require('fs');
403
+
404
+ function sendResponse(response) {
405
+ process.stdout.write(JSON.stringify(response) + '\n');
406
+ }
407
+
408
+ async function runMCPServer() {
409
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
410
+
411
+ rl.on('line', async line => {
412
+ let request;
413
+ try { request = JSON.parse(line); } catch { return; }
414
+
415
+ const { id, method, params } = request;
416
+
417
+ try {
418
+ switch (method) {
419
+
420
+ case 'initialize':
421
+ sendResponse({
422
+ jsonrpc: '2.0', id,
423
+ result: {
424
+ protocolVersion: '2024-11-05',
425
+ capabilities : { tools: {} },
426
+ serverInfo : { name: 'cloak', version: '1.0.0' },
427
+ },
428
+ });
429
+ break;
430
+
431
+ case 'tools/list':
432
+ sendResponse({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
433
+ break;
434
+
435
+ case 'tools/call': {
436
+ const { name, arguments: args } = params;
437
+ const result = await handleTool(name, args || {});
438
+ sendResponse({
439
+ jsonrpc: '2.0', id,
440
+ result: {
441
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
442
+ },
443
+ });
444
+ break;
445
+ }
446
+
447
+ default:
448
+ sendResponse({
449
+ jsonrpc: '2.0', id,
450
+ error: { code: -32601, message: `Method not found: ${method}` },
451
+ });
452
+ }
453
+ } catch (err) {
454
+ if (CONFIG.debug) console.error('[cloak]', err);
455
+ sendResponse({
456
+ jsonrpc: '2.0', id,
457
+ error: { code: -32603, message: err.message },
458
+ });
459
+ }
460
+ });
461
+
462
+ process.stderr.write('[cloak] MCP server ready\n');
463
+ }
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Entry point
467
+ // ---------------------------------------------------------------------------
468
+ const args = process.argv.slice(2);
469
+
470
+ if (args[0] === '--hook' && args[1]) {
471
+ runHookMode(args[1]).catch(err => {
472
+ process.stderr.write(`[cloak] Hook error: ${err.message}\n`);
473
+ process.exit(1);
474
+ });
475
+ } else {
476
+ runMCPServer().catch(err => {
477
+ process.stderr.write(`[cloak] Fatal: ${err.message}\n`);
478
+ process.exit(1);
479
+ });
480
+ }