@chrisai/base 2.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/README.md +157 -0
- package/bin/install.js +340 -0
- package/package.json +40 -0
- package/src/commands/audit-claude-md.md +31 -0
- package/src/commands/audit.md +33 -0
- package/src/commands/carl-hygiene.md +33 -0
- package/src/commands/groom.md +35 -0
- package/src/commands/history.md +27 -0
- package/src/commands/pulse.md +33 -0
- package/src/commands/scaffold.md +33 -0
- package/src/commands/status.md +28 -0
- package/src/commands/surface-convert.md +35 -0
- package/src/commands/surface-create.md +34 -0
- package/src/commands/surface-list.md +27 -0
- package/src/framework/context/base-principles.md +71 -0
- package/src/framework/frameworks/audit-strategies.md +53 -0
- package/src/framework/frameworks/satellite-registration.md +44 -0
- package/src/framework/tasks/audit-claude-md.md +68 -0
- package/src/framework/tasks/audit.md +64 -0
- package/src/framework/tasks/carl-hygiene.md +160 -0
- package/src/framework/tasks/groom.md +164 -0
- package/src/framework/tasks/history.md +34 -0
- package/src/framework/tasks/pulse.md +83 -0
- package/src/framework/tasks/scaffold.md +167 -0
- package/src/framework/tasks/status.md +35 -0
- package/src/framework/tasks/surface-convert.md +143 -0
- package/src/framework/tasks/surface-create.md +184 -0
- package/src/framework/tasks/surface-list.md +42 -0
- package/src/framework/templates/active-md.md +112 -0
- package/src/framework/templates/backlog-md.md +100 -0
- package/src/framework/templates/state-md.md +48 -0
- package/src/framework/templates/workspace-json.md +50 -0
- package/src/hooks/_template.py +129 -0
- package/src/hooks/active-hook.py +115 -0
- package/src/hooks/backlog-hook.py +107 -0
- package/src/hooks/base-pulse-check.py +206 -0
- package/src/hooks/psmm-injector.py +67 -0
- package/src/hooks/satellite-detection.py +131 -0
- package/src/packages/base-mcp/index.js +108 -0
- package/src/packages/base-mcp/package.json +10 -0
- package/src/packages/base-mcp/tools/surfaces.js +404 -0
- package/src/packages/carl-mcp/index.js +115 -0
- package/src/packages/carl-mcp/package.json +10 -0
- package/src/packages/carl-mcp/tools/decisions.js +269 -0
- package/src/packages/carl-mcp/tools/domains.js +361 -0
- package/src/packages/carl-mcp/tools/psmm.js +204 -0
- package/src/packages/carl-mcp/tools/staging.js +245 -0
- package/src/skill/base.md +111 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CARL MCP — Consolidated Tool Server
|
|
4
|
+
* Context Augmentation & Reinforcement Layer
|
|
5
|
+
*
|
|
6
|
+
* Consolidates: DRL-engine + decision-logger + PSMM (new) + Staging (new)
|
|
7
|
+
* 4 tool groups, all optional, activated by CARL rules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
// Tool group imports
|
|
17
|
+
import { TOOLS as domainTools, handleTool as handleDomain } from './tools/domains.js';
|
|
18
|
+
import { TOOLS as decisionTools, handleTool as handleDecision } from './tools/decisions.js';
|
|
19
|
+
import { TOOLS as psmmTools, handleTool as handlePsmm } from './tools/psmm.js';
|
|
20
|
+
import { TOOLS as stagingTools, handleTool as handleStaging } from './tools/staging.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// CONFIGURATION
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
// Resolve workspace from this file's location: carl-mcp/ → .base/ → workspace root
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const WORKSPACE_PATH = path.resolve(__dirname, '../..');
|
|
29
|
+
|
|
30
|
+
function debugLog(...args) {
|
|
31
|
+
console.error('[CARL]', new Date().toISOString(), ...args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================
|
|
35
|
+
// TOOL REGISTRY
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
const ALL_TOOLS = [...domainTools, ...decisionTools, ...psmmTools, ...stagingTools];
|
|
39
|
+
|
|
40
|
+
// Build handler lookup: tool name → handler function
|
|
41
|
+
const TOOL_HANDLERS = {};
|
|
42
|
+
for (const tool of domainTools) TOOL_HANDLERS[tool.name] = handleDomain;
|
|
43
|
+
for (const tool of decisionTools) TOOL_HANDLERS[tool.name] = handleDecision;
|
|
44
|
+
for (const tool of psmmTools) TOOL_HANDLERS[tool.name] = handlePsmm;
|
|
45
|
+
for (const tool of stagingTools) TOOL_HANDLERS[tool.name] = handleStaging;
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// MCP SERVER
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
const server = new Server({
|
|
52
|
+
name: "carl-mcp",
|
|
53
|
+
version: "1.0.0",
|
|
54
|
+
}, {
|
|
55
|
+
capabilities: {
|
|
56
|
+
tools: {},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
debugLog('CARL MCP Server initialized');
|
|
61
|
+
debugLog('Workspace:', WORKSPACE_PATH);
|
|
62
|
+
debugLog('Tool groups: domains (%d), decisions (%d), psmm (%d), staging (%d)',
|
|
63
|
+
domainTools.length, decisionTools.length, psmmTools.length, stagingTools.length);
|
|
64
|
+
debugLog('Total tools:', ALL_TOOLS.length);
|
|
65
|
+
|
|
66
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
67
|
+
debugLog('List tools request');
|
|
68
|
+
return { tools: ALL_TOOLS };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
72
|
+
const { name, arguments: args } = request.params;
|
|
73
|
+
debugLog('Call tool:', name);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const handler = TOOL_HANDLERS[name];
|
|
77
|
+
if (!handler) {
|
|
78
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await handler(name, args || {}, WORKSPACE_PATH);
|
|
82
|
+
|
|
83
|
+
if (result === null) {
|
|
84
|
+
throw new Error(`Tool ${name} returned null — handler mismatch`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
89
|
+
isError: false,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
debugLog('Error:', error.message);
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// RUN
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
async function runServer() {
|
|
105
|
+
const transport = new StdioServerTransport();
|
|
106
|
+
await server.connect(transport);
|
|
107
|
+
console.error("CARL MCP Server running on stdio");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await runServer();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Fatal error:", error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CARL Decisions — Per-domain decision logging/recall tools
|
|
3
|
+
* Reads/writes .carl/decisions/{domain}.json files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
function debugLog(...args) {
|
|
10
|
+
console.error('[CARL:decisions]', new Date().toISOString(), ...args);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getDecisionsDir(workspacePath) {
|
|
14
|
+
return join(workspacePath, '.carl', 'decisions');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getDomainFilePath(workspacePath, domain) {
|
|
18
|
+
return join(getDecisionsDir(workspacePath), `${domain.toLowerCase()}.json`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readDomainDecisions(workspacePath, domain) {
|
|
22
|
+
const filepath = getDomainFilePath(workspacePath, domain);
|
|
23
|
+
if (!existsSync(filepath)) {
|
|
24
|
+
return { domain: domain.toLowerCase(), decisions: [], archived: [] };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
debugLog('Error reading domain decisions:', error.message);
|
|
30
|
+
return { domain: domain.toLowerCase(), decisions: [], archived: [] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeDomainDecisions(workspacePath, domain, data) {
|
|
35
|
+
const filepath = getDomainFilePath(workspacePath, domain);
|
|
36
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function listAllDomainFiles(workspacePath) {
|
|
40
|
+
const dir = getDecisionsDir(workspacePath);
|
|
41
|
+
if (!existsSync(dir)) return [];
|
|
42
|
+
return readdirSync(dir)
|
|
43
|
+
.filter(f => f.endsWith('.json'))
|
|
44
|
+
.map(f => f.replace('.json', ''));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nextDecisionId(domain, decisions) {
|
|
48
|
+
const prefix = `${domain.toLowerCase()}-`;
|
|
49
|
+
const nums = decisions
|
|
50
|
+
.map(d => {
|
|
51
|
+
const m = d.id.match(new RegExp(`^${prefix}(\\d+)$`));
|
|
52
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
53
|
+
})
|
|
54
|
+
.filter(n => n > 0);
|
|
55
|
+
const max = nums.length > 0 ? Math.max(...nums) : 0;
|
|
56
|
+
return `${prefix}${String(max + 1).padStart(3, '0')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// TOOL DEFINITIONS
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
export const TOOLS = [
|
|
64
|
+
{
|
|
65
|
+
name: "carl_log_decision",
|
|
66
|
+
description: "Log a decision to a CARL domain. Auto-creates domain file if new.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
domain: { type: "string", description: "Domain name (e.g., 'casegate', 'development', 'global')" },
|
|
71
|
+
decision: { type: "string", description: "The decision made" },
|
|
72
|
+
rationale: { type: "string", description: "Why this decision was made" },
|
|
73
|
+
recall: { type: "string", description: "Comma-separated keywords for when to recall this decision" }
|
|
74
|
+
},
|
|
75
|
+
required: ["domain", "decision", "rationale", "recall"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "carl_list_decision_domains",
|
|
80
|
+
description: "List all decision domain files with counts.",
|
|
81
|
+
inputSchema: { type: "object", properties: {} }
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "carl_get_decisions",
|
|
85
|
+
description: "Get all decisions for a domain.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
domain: { type: "string", description: "Domain name (e.g., 'casegate', 'development')" }
|
|
90
|
+
},
|
|
91
|
+
required: ["domain"]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "carl_search_decisions",
|
|
96
|
+
description: "Search decisions by keyword across all domain files.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
keyword: { type: "string", description: "Keyword to search for" }
|
|
101
|
+
},
|
|
102
|
+
required: ["keyword"]
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "carl_archive_decision",
|
|
107
|
+
description: "Move a decision to the archived array in its domain file.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
id: { type: "string", description: "Decision ID (e.g., 'casegate-003')" }
|
|
112
|
+
},
|
|
113
|
+
required: ["id"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// TOOL HANDLERS
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
export async function handleTool(name, args, workspacePath) {
|
|
123
|
+
switch (name) {
|
|
124
|
+
case "carl_log_decision": return logDecision(args, workspacePath);
|
|
125
|
+
case "carl_list_decision_domains": return listDecisionDomains(workspacePath);
|
|
126
|
+
case "carl_get_decisions": return getDecisions(args, workspacePath);
|
|
127
|
+
case "carl_search_decisions": return searchDecisions(args, workspacePath);
|
|
128
|
+
case "carl_archive_decision": return archiveDecision(args, workspacePath);
|
|
129
|
+
default: return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function logDecision(args, workspacePath) {
|
|
134
|
+
const { domain, decision, rationale, recall } = args;
|
|
135
|
+
const domainLower = domain.toLowerCase();
|
|
136
|
+
debugLog('Logging decision to domain:', domainLower);
|
|
137
|
+
|
|
138
|
+
const data = readDomainDecisions(workspacePath, domainLower);
|
|
139
|
+
const id = nextDecisionId(domainLower, data.decisions);
|
|
140
|
+
const date = new Date().toISOString().split('T')[0];
|
|
141
|
+
const recallList = recall.split(',').map(k => k.trim()).filter(Boolean);
|
|
142
|
+
|
|
143
|
+
const entry = {
|
|
144
|
+
id,
|
|
145
|
+
decision,
|
|
146
|
+
rationale,
|
|
147
|
+
date,
|
|
148
|
+
source: "manual",
|
|
149
|
+
recall: recallList
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
data.decisions.push(entry);
|
|
153
|
+
data.domain = domainLower;
|
|
154
|
+
if (!data.archived) data.archived = [];
|
|
155
|
+
writeDomainDecisions(workspacePath, domainLower, data);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
id,
|
|
160
|
+
domain: domainLower,
|
|
161
|
+
message: `Logged: ${id} to ${domainLower}`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function listDecisionDomains(workspacePath) {
|
|
166
|
+
debugLog('Listing decision domains');
|
|
167
|
+
const domainNames = listAllDomainFiles(workspacePath);
|
|
168
|
+
const domains = [];
|
|
169
|
+
|
|
170
|
+
for (const name of domainNames) {
|
|
171
|
+
const data = readDomainDecisions(workspacePath, name);
|
|
172
|
+
domains.push({
|
|
173
|
+
domain: name,
|
|
174
|
+
active_count: data.decisions.length,
|
|
175
|
+
archived_count: (data.archived || []).length
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalActive = domains.reduce((sum, d) => sum + d.active_count, 0);
|
|
180
|
+
const totalArchived = domains.reduce((sum, d) => sum + d.archived_count, 0);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
domain_count: domains.length,
|
|
185
|
+
total_active: totalActive,
|
|
186
|
+
total_archived: totalArchived,
|
|
187
|
+
domains
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function getDecisions(args, workspacePath) {
|
|
192
|
+
const { domain } = args;
|
|
193
|
+
const domainLower = domain.toLowerCase();
|
|
194
|
+
debugLog('Getting decisions for:', domainLower);
|
|
195
|
+
|
|
196
|
+
const filepath = getDomainFilePath(workspacePath, domainLower);
|
|
197
|
+
if (!existsSync(filepath)) {
|
|
198
|
+
return { error: `No decisions file for domain: ${domainLower}` };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const data = readDomainDecisions(workspacePath, domainLower);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
domain: domainLower,
|
|
205
|
+
count: data.decisions.length,
|
|
206
|
+
archived_count: (data.archived || []).length,
|
|
207
|
+
decisions: data.decisions,
|
|
208
|
+
archived: data.archived || []
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function searchDecisions(args, workspacePath) {
|
|
213
|
+
const { keyword } = args;
|
|
214
|
+
debugLog('Searching for:', keyword);
|
|
215
|
+
|
|
216
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
217
|
+
const domainNames = listAllDomainFiles(workspacePath);
|
|
218
|
+
const results = [];
|
|
219
|
+
|
|
220
|
+
for (const name of domainNames) {
|
|
221
|
+
const data = readDomainDecisions(workspacePath, name);
|
|
222
|
+
for (const d of data.decisions) {
|
|
223
|
+
const searchText = [
|
|
224
|
+
d.decision,
|
|
225
|
+
d.rationale,
|
|
226
|
+
...(d.recall || [])
|
|
227
|
+
].join(' ').toLowerCase();
|
|
228
|
+
|
|
229
|
+
if (searchText.includes(lowerKeyword)) {
|
|
230
|
+
results.push({ ...d, domain: name });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { keyword, count: results.length, decisions: results };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function archiveDecision(args, workspacePath) {
|
|
239
|
+
const { id } = args;
|
|
240
|
+
debugLog('Archiving:', id);
|
|
241
|
+
|
|
242
|
+
// Extract domain from ID (e.g., 'casegate-003' -> 'casegate')
|
|
243
|
+
const dashIdx = id.lastIndexOf('-');
|
|
244
|
+
if (dashIdx === -1) {
|
|
245
|
+
return { error: `Invalid decision ID format: ${id}. Expected: {domain}-{number}` };
|
|
246
|
+
}
|
|
247
|
+
const domainLower = id.slice(0, dashIdx);
|
|
248
|
+
|
|
249
|
+
const filepath = getDomainFilePath(workspacePath, domainLower);
|
|
250
|
+
if (!existsSync(filepath)) {
|
|
251
|
+
return { error: `No decisions file for domain: ${domainLower}` };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const data = readDomainDecisions(workspacePath, domainLower);
|
|
255
|
+
const idx = data.decisions.findIndex(d => d.id === id);
|
|
256
|
+
|
|
257
|
+
if (idx === -1) {
|
|
258
|
+
return { error: `Decision not found: ${id}` };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const decision = data.decisions.splice(idx, 1)[0];
|
|
262
|
+
decision.archived_date = new Date().toISOString().split('T')[0];
|
|
263
|
+
|
|
264
|
+
if (!data.archived) data.archived = [];
|
|
265
|
+
data.archived.push(decision);
|
|
266
|
+
writeDomainDecisions(workspacePath, domainLower, data);
|
|
267
|
+
|
|
268
|
+
return { success: true, message: `Archived: ${id}` };
|
|
269
|
+
}
|