@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,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CARL Domains — Domain CRUD tools
|
|
3
|
+
* Migrated from DRL-engine MCP, renamed drl_* → carl_*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, readdirSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
const CARL_FOLDER = '.carl';
|
|
10
|
+
|
|
11
|
+
function debugLog(...args) {
|
|
12
|
+
console.error('[CARL:domains]', new Date().toISOString(), ...args);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// FILE PARSING
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
function findCarlFiles(workspacePath) {
|
|
20
|
+
const files = {};
|
|
21
|
+
const carlPath = join(workspacePath, CARL_FOLDER);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(carlPath)) {
|
|
25
|
+
debugLog('CARL folder not found:', carlPath);
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = readdirSync(carlPath);
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.startsWith('.') && !entry.startsWith('sessions') && !entry.startsWith('skool')) {
|
|
32
|
+
files[entry] = join(carlPath, entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
debugLog('Error reading CARL folder:', error.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyManifestEntry(domains, key, value) {
|
|
43
|
+
const suffixes = [
|
|
44
|
+
{ suffix: '_STATE', len: 6, handler: (d, v) => { d.state = ['active', 'true', 'yes', '1'].includes(v.toLowerCase()); } },
|
|
45
|
+
{ suffix: '_ALWAYS_ON', len: 10, handler: (d, v) => { d.always_on = ['true', 'yes', '1'].includes(v.toLowerCase()); } },
|
|
46
|
+
{ suffix: '_RECALL', len: 7, handler: (d, v) => { d.recall = v; } }
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const { suffix, len, handler } of suffixes) {
|
|
50
|
+
if (key.endsWith(suffix)) {
|
|
51
|
+
const domain = key.slice(0, -len);
|
|
52
|
+
if (!domains[domain]) domains[domain] = {};
|
|
53
|
+
handler(domains[domain], value);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseManifest(filepath) {
|
|
60
|
+
const domains = {};
|
|
61
|
+
if (!existsSync(filepath)) return domains;
|
|
62
|
+
|
|
63
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
64
|
+
for (const line of content.split('\n')) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
67
|
+
const [key, value] = trimmed.split('=', 2);
|
|
68
|
+
applyManifestEntry(domains, key.trim(), value.trim());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return domains;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseDomainRules(filepath, domainName) {
|
|
75
|
+
const rules = [];
|
|
76
|
+
if (!existsSync(filepath)) return rules;
|
|
77
|
+
|
|
78
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
79
|
+
const prefix = `${domainName}_RULE_`;
|
|
80
|
+
|
|
81
|
+
for (const line of content.split('\n')) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
84
|
+
const [key, value] = trimmed.split('=', 2);
|
|
85
|
+
const k = key.trim();
|
|
86
|
+
const v = value.trim();
|
|
87
|
+
|
|
88
|
+
if (k.startsWith(prefix) && v) {
|
|
89
|
+
const num = Number.parseInt(k.replace(prefix, ''), 10);
|
|
90
|
+
rules.push({ num, text: v });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rules.sort((a, b) => a.num - b.num);
|
|
95
|
+
return rules;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// TOOL DEFINITIONS
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
export const TOOLS = [
|
|
103
|
+
{
|
|
104
|
+
name: "carl_list_domains",
|
|
105
|
+
description: "List all available CARL domains with state from manifest.",
|
|
106
|
+
inputSchema: { type: "object", properties: {} }
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "carl_get_domain_rules",
|
|
110
|
+
description: "Load rules for a specific CARL domain. Use when user intent matches domain recall keywords.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
domain: { type: "string", description: "Domain name (e.g., 'PROJECTS', 'CONTENT'). Case-insensitive." }
|
|
115
|
+
},
|
|
116
|
+
required: ["domain"]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "carl_create_domain",
|
|
121
|
+
description: "Create a new CARL domain with file and manifest entry.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
domain: { type: "string", description: "Domain name in UPPERCASE (e.g., 'TESTING')" },
|
|
126
|
+
description: { type: "string", description: "Brief description of when this domain applies" },
|
|
127
|
+
recall: { type: "string", description: "Comma-separated recall keywords that trigger this domain" },
|
|
128
|
+
rules: { type: "array", items: { type: "string" }, description: "Array of rule strings (Rule 0 auto-generated from description)" }
|
|
129
|
+
},
|
|
130
|
+
required: ["domain", "description", "recall", "rules"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "carl_toggle_domain",
|
|
135
|
+
description: "Enable or disable a CARL domain by updating the manifest.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {
|
|
139
|
+
domain: { type: "string", description: "Domain name to toggle (e.g., 'PROJECTS')" },
|
|
140
|
+
state: { type: "string", enum: ["active", "inactive"], description: "New state" }
|
|
141
|
+
},
|
|
142
|
+
required: ["domain", "state"]
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "carl_get_manifest",
|
|
147
|
+
description: "Get current CARL manifest showing all domains, states, and recall keywords.",
|
|
148
|
+
inputSchema: { type: "object", properties: {} }
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// TOOL HANDLERS
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
export async function handleTool(name, args, workspacePath) {
|
|
157
|
+
switch (name) {
|
|
158
|
+
case "carl_list_domains": return listDomains(workspacePath);
|
|
159
|
+
case "carl_get_domain_rules": return getDomainRules(args, workspacePath);
|
|
160
|
+
case "carl_create_domain": return createDomain(args, workspacePath);
|
|
161
|
+
case "carl_toggle_domain": return toggleDomain(args, workspacePath);
|
|
162
|
+
case "carl_get_manifest": return getManifest(workspacePath);
|
|
163
|
+
default: return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function listDomains(workspacePath) {
|
|
168
|
+
const files = findCarlFiles(workspacePath);
|
|
169
|
+
debugLog('Listing domains');
|
|
170
|
+
|
|
171
|
+
const domainFiles = Object.entries(files)
|
|
172
|
+
.filter(([type]) => type !== 'manifest' && type !== 'context')
|
|
173
|
+
.map(([type, filepath]) => ({
|
|
174
|
+
domain: type.toUpperCase(),
|
|
175
|
+
file: `.carl/${type}`,
|
|
176
|
+
path: filepath
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
workspace: workspacePath,
|
|
182
|
+
domain_count: domainFiles.length,
|
|
183
|
+
domains: domainFiles,
|
|
184
|
+
special_files: {
|
|
185
|
+
manifest: files.manifest || null,
|
|
186
|
+
context: files.context || null
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function getDomainRules(args, workspacePath) {
|
|
192
|
+
const domain = args.domain.toUpperCase();
|
|
193
|
+
const files = findCarlFiles(workspacePath);
|
|
194
|
+
debugLog('Getting rules for domain:', domain);
|
|
195
|
+
|
|
196
|
+
let domainFile = files[domain.toLowerCase()];
|
|
197
|
+
let isCommandExtraction = false;
|
|
198
|
+
|
|
199
|
+
if (!domainFile && files.commands) {
|
|
200
|
+
debugLog('No dedicated file, checking commands file for:', domain);
|
|
201
|
+
domainFile = files.commands;
|
|
202
|
+
isCommandExtraction = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!domainFile) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
error: `Domain file not found: .carl/${domain.toLowerCase()}`,
|
|
209
|
+
available_domains: Object.keys(files).filter(k => k !== 'manifest' && k !== 'context')
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rules = parseDomainRules(domainFile, domain);
|
|
214
|
+
|
|
215
|
+
if (rules.length === 0) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
error: `No ${domain}_RULE_* entries found`,
|
|
219
|
+
domain
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const formatted = rules.map(r => `[${domain}] Rule ${r.num}: ${r.text}`);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
domain,
|
|
228
|
+
rule_count: rules.length,
|
|
229
|
+
rules: formatted,
|
|
230
|
+
source: isCommandExtraction ? 'commands' : 'dedicated',
|
|
231
|
+
instruction: `${domain} rules loaded. Apply these rules to the current context.`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function createDomain(args, workspacePath) {
|
|
236
|
+
const domain = args.domain.toUpperCase();
|
|
237
|
+
const domainLower = domain.toLowerCase();
|
|
238
|
+
const { description, recall, rules = [] } = args;
|
|
239
|
+
const files = findCarlFiles(workspacePath);
|
|
240
|
+
|
|
241
|
+
debugLog('Creating domain:', domain);
|
|
242
|
+
|
|
243
|
+
if (files[domainLower]) {
|
|
244
|
+
return { success: false, error: `Domain already exists: ${domain}`, existing_file: files[domainLower] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!files.manifest) {
|
|
248
|
+
return { success: false, error: 'Manifest not found' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const domainFilePath = join(workspacePath, CARL_FOLDER, domainLower);
|
|
252
|
+
|
|
253
|
+
let domainContent = `# Dynamic Rules Loader V2 - ${domain} Domain
|
|
254
|
+
# ${'='.repeat(40)}
|
|
255
|
+
# ${description}
|
|
256
|
+
#
|
|
257
|
+
# Domain Configuration (synced with .carl/manifest)
|
|
258
|
+
${domain}_STATE=active
|
|
259
|
+
${domain}_ALWAYS_ON=false
|
|
260
|
+
|
|
261
|
+
# Rule 0: Self-referencing relevance instruction
|
|
262
|
+
${domain}_RULE_0=${domain} rules apply when ${description.toLowerCase()}. If these rules are loaded but the request doesn't involve this domain, note that ${domain} rules are active but may not be needed.
|
|
263
|
+
|
|
264
|
+
# ============================================================================
|
|
265
|
+
# ${domain} Rules
|
|
266
|
+
# ============================================================================
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
for (let i = 0; i < rules.length; i++) {
|
|
270
|
+
domainContent += `${domain}_RULE_${i + 1}=${rules[i]}\n`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
writeFileSync(domainFilePath, domainContent, 'utf-8');
|
|
274
|
+
|
|
275
|
+
const manifestContent = readFileSync(files.manifest, 'utf-8');
|
|
276
|
+
const manifestEntry = `
|
|
277
|
+
# ============================================================================
|
|
278
|
+
# ${domain} - ${description}
|
|
279
|
+
# ============================================================================
|
|
280
|
+
${domain}_STATE=active
|
|
281
|
+
${domain}_ALWAYS_ON=false
|
|
282
|
+
${domain}_RECALL=${recall}
|
|
283
|
+
# ${domain}_EXCLUDE=
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
writeFileSync(files.manifest, manifestContent + manifestEntry, 'utf-8');
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
domain,
|
|
291
|
+
domain_file: domainFilePath,
|
|
292
|
+
rule_count: rules.length + 1,
|
|
293
|
+
recall_keywords: recall,
|
|
294
|
+
message: `Domain ${domain} created. File: .carl/${domainLower}, Manifest updated.`
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function toggleDomain(args, workspacePath) {
|
|
299
|
+
const domain = args.domain.toUpperCase();
|
|
300
|
+
const newState = args.state.toLowerCase();
|
|
301
|
+
const files = findCarlFiles(workspacePath);
|
|
302
|
+
|
|
303
|
+
debugLog('Toggling domain:', domain, 'to', newState);
|
|
304
|
+
|
|
305
|
+
if (!files.manifest) {
|
|
306
|
+
return { success: false, error: 'Manifest not found' };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const content = readFileSync(files.manifest, 'utf-8');
|
|
310
|
+
const lines = content.split('\n');
|
|
311
|
+
const stateKey = `${domain}_STATE`;
|
|
312
|
+
let found = false;
|
|
313
|
+
|
|
314
|
+
const newLines = lines.map(line => {
|
|
315
|
+
if (line.trim().startsWith(stateKey + '=')) {
|
|
316
|
+
found = true;
|
|
317
|
+
return `${stateKey}=${newState}`;
|
|
318
|
+
}
|
|
319
|
+
return line;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (!found) {
|
|
323
|
+
return { success: false, error: `Domain not found in manifest: ${domain}` };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
writeFileSync(files.manifest, newLines.join('\n'), 'utf-8');
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
success: true,
|
|
330
|
+
domain,
|
|
331
|
+
new_state: newState,
|
|
332
|
+
message: `${domain} is now ${newState}. Change persisted to manifest.`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function getManifest(workspacePath) {
|
|
337
|
+
const files = findCarlFiles(workspacePath);
|
|
338
|
+
debugLog('Getting manifest');
|
|
339
|
+
|
|
340
|
+
if (!files.manifest) {
|
|
341
|
+
return { success: false, error: 'Manifest not found' };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const domains = parseManifest(files.manifest);
|
|
345
|
+
|
|
346
|
+
const formatted = {};
|
|
347
|
+
for (const [name, config] of Object.entries(domains)) {
|
|
348
|
+
formatted[name] = {
|
|
349
|
+
state: config.state ? 'active' : 'inactive',
|
|
350
|
+
always_on: config.always_on || false,
|
|
351
|
+
recall: config.recall || ''
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
manifest_path: files.manifest,
|
|
358
|
+
domain_count: Object.keys(domains).length,
|
|
359
|
+
domains: formatted
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CARL PSMM — Per-Session Meta Memory tools
|
|
3
|
+
* Tracks significant meta moments across sessions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
const VALID_TYPES = ['DECISION', 'CORRECTION', 'SHIFT', 'INSIGHT', 'COMMITMENT'];
|
|
10
|
+
|
|
11
|
+
function debugLog(...args) {
|
|
12
|
+
console.error('[CARL:psmm]', new Date().toISOString(), ...args);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getPsmmPath(workspacePath) {
|
|
16
|
+
return join(workspacePath, '.base', 'data', 'psmm.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readPsmm(workspacePath) {
|
|
20
|
+
const filepath = getPsmmPath(workspacePath);
|
|
21
|
+
if (!existsSync(filepath)) {
|
|
22
|
+
return { sessions: {} };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
debugLog('Error reading psmm.json:', error.message);
|
|
28
|
+
return { sessions: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writePsmm(workspacePath, data) {
|
|
33
|
+
const filepath = getPsmmPath(workspacePath);
|
|
34
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTimestamp() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
40
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// TOOL DEFINITIONS
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
export const TOOLS = [
|
|
48
|
+
{
|
|
49
|
+
name: "carl_psmm_log",
|
|
50
|
+
description: "Log a per-session meta memory entry. Types: DECISION, CORRECTION, SHIFT, INSIGHT, COMMITMENT. Auto-creates session if new.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
session_id: { type: "string", description: "Session UUID" },
|
|
55
|
+
type: { type: "string", enum: VALID_TYPES, description: "Entry type" },
|
|
56
|
+
text: { type: "string", description: "Description of the meta moment" }
|
|
57
|
+
},
|
|
58
|
+
required: ["session_id", "type", "text"]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "carl_psmm_get",
|
|
63
|
+
description: "Get all PSMM entries for a specific session by UUID.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
session_id: { type: "string", description: "Session UUID" }
|
|
68
|
+
},
|
|
69
|
+
required: ["session_id"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "carl_psmm_list",
|
|
74
|
+
description: "List all PSMM sessions with entry counts and created timestamps.",
|
|
75
|
+
inputSchema: { type: "object", properties: {} }
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "carl_psmm_clean",
|
|
79
|
+
description: "Remove a stale session's entries from PSMM.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
session_id: { type: "string", description: "Session UUID to remove" }
|
|
84
|
+
},
|
|
85
|
+
required: ["session_id"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ============================================================
|
|
91
|
+
// TOOL HANDLERS
|
|
92
|
+
// ============================================================
|
|
93
|
+
|
|
94
|
+
export async function handleTool(name, args, workspacePath) {
|
|
95
|
+
switch (name) {
|
|
96
|
+
case "carl_psmm_log": return psmmLog(args, workspacePath);
|
|
97
|
+
case "carl_psmm_get": return psmmGet(args, workspacePath);
|
|
98
|
+
case "carl_psmm_list": return psmmList(workspacePath);
|
|
99
|
+
case "carl_psmm_clean": return psmmClean(args, workspacePath);
|
|
100
|
+
default: return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function psmmLog(args, workspacePath) {
|
|
105
|
+
const { session_id, type, text } = args;
|
|
106
|
+
|
|
107
|
+
if (!VALID_TYPES.includes(type)) {
|
|
108
|
+
return { success: false, error: `Invalid type: ${type}. Valid: ${VALID_TYPES.join(', ')}` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
debugLog('Logging PSMM entry:', session_id, type);
|
|
112
|
+
|
|
113
|
+
const data = readPsmm(workspacePath);
|
|
114
|
+
|
|
115
|
+
if (!data.sessions[session_id]) {
|
|
116
|
+
data.sessions[session_id] = {
|
|
117
|
+
created: formatTimestamp(),
|
|
118
|
+
entries: []
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entry = {
|
|
123
|
+
timestamp: formatTimestamp(),
|
|
124
|
+
type,
|
|
125
|
+
text
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
data.sessions[session_id].entries.push(entry);
|
|
129
|
+
writePsmm(workspacePath, data);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
session_id,
|
|
134
|
+
entry_count: data.sessions[session_id].entries.length,
|
|
135
|
+
message: `Logged ${type} entry to session ${session_id.slice(0, 8)}...`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function psmmGet(args, workspacePath) {
|
|
140
|
+
const { session_id } = args;
|
|
141
|
+
debugLog('Getting PSMM for session:', session_id);
|
|
142
|
+
|
|
143
|
+
const data = readPsmm(workspacePath);
|
|
144
|
+
const session = data.sessions[session_id];
|
|
145
|
+
|
|
146
|
+
if (!session) {
|
|
147
|
+
return { entries: [], exists: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
exists: true,
|
|
152
|
+
session_id,
|
|
153
|
+
created: session.created,
|
|
154
|
+
entry_count: session.entries.length,
|
|
155
|
+
entries: session.entries
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function psmmList(workspacePath) {
|
|
160
|
+
debugLog('Listing PSMM sessions');
|
|
161
|
+
|
|
162
|
+
const data = readPsmm(workspacePath);
|
|
163
|
+
const sessions = [];
|
|
164
|
+
|
|
165
|
+
for (const [id, session] of Object.entries(data.sessions)) {
|
|
166
|
+
sessions.push({
|
|
167
|
+
session_id: id,
|
|
168
|
+
created: session.created,
|
|
169
|
+
entry_count: session.entries.length,
|
|
170
|
+
types: [...new Set(session.entries.map(e => e.type))]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
sessions.sort((a, b) => b.created.localeCompare(a.created));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
session_count: sessions.length,
|
|
179
|
+
total_entries: sessions.reduce((sum, s) => sum + s.entry_count, 0),
|
|
180
|
+
sessions
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function psmmClean(args, workspacePath) {
|
|
185
|
+
const { session_id } = args;
|
|
186
|
+
debugLog('Cleaning PSMM session:', session_id);
|
|
187
|
+
|
|
188
|
+
const data = readPsmm(workspacePath);
|
|
189
|
+
|
|
190
|
+
if (!data.sessions[session_id]) {
|
|
191
|
+
return { success: false, error: `Session not found: ${session_id}` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const entryCount = data.sessions[session_id].entries.length;
|
|
195
|
+
delete data.sessions[session_id];
|
|
196
|
+
writePsmm(workspacePath, data);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
session_id,
|
|
201
|
+
entries_removed: entryCount,
|
|
202
|
+
message: `Cleaned session ${session_id.slice(0, 8)}... (${entryCount} entries removed)`
|
|
203
|
+
};
|
|
204
|
+
}
|