@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.
Files changed (48) hide show
  1. package/README.md +157 -0
  2. package/bin/install.js +340 -0
  3. package/package.json +40 -0
  4. package/src/commands/audit-claude-md.md +31 -0
  5. package/src/commands/audit.md +33 -0
  6. package/src/commands/carl-hygiene.md +33 -0
  7. package/src/commands/groom.md +35 -0
  8. package/src/commands/history.md +27 -0
  9. package/src/commands/pulse.md +33 -0
  10. package/src/commands/scaffold.md +33 -0
  11. package/src/commands/status.md +28 -0
  12. package/src/commands/surface-convert.md +35 -0
  13. package/src/commands/surface-create.md +34 -0
  14. package/src/commands/surface-list.md +27 -0
  15. package/src/framework/context/base-principles.md +71 -0
  16. package/src/framework/frameworks/audit-strategies.md +53 -0
  17. package/src/framework/frameworks/satellite-registration.md +44 -0
  18. package/src/framework/tasks/audit-claude-md.md +68 -0
  19. package/src/framework/tasks/audit.md +64 -0
  20. package/src/framework/tasks/carl-hygiene.md +160 -0
  21. package/src/framework/tasks/groom.md +164 -0
  22. package/src/framework/tasks/history.md +34 -0
  23. package/src/framework/tasks/pulse.md +83 -0
  24. package/src/framework/tasks/scaffold.md +167 -0
  25. package/src/framework/tasks/status.md +35 -0
  26. package/src/framework/tasks/surface-convert.md +143 -0
  27. package/src/framework/tasks/surface-create.md +184 -0
  28. package/src/framework/tasks/surface-list.md +42 -0
  29. package/src/framework/templates/active-md.md +112 -0
  30. package/src/framework/templates/backlog-md.md +100 -0
  31. package/src/framework/templates/state-md.md +48 -0
  32. package/src/framework/templates/workspace-json.md +50 -0
  33. package/src/hooks/_template.py +129 -0
  34. package/src/hooks/active-hook.py +115 -0
  35. package/src/hooks/backlog-hook.py +107 -0
  36. package/src/hooks/base-pulse-check.py +206 -0
  37. package/src/hooks/psmm-injector.py +67 -0
  38. package/src/hooks/satellite-detection.py +131 -0
  39. package/src/packages/base-mcp/index.js +108 -0
  40. package/src/packages/base-mcp/package.json +10 -0
  41. package/src/packages/base-mcp/tools/surfaces.js +404 -0
  42. package/src/packages/carl-mcp/index.js +115 -0
  43. package/src/packages/carl-mcp/package.json +10 -0
  44. package/src/packages/carl-mcp/tools/decisions.js +269 -0
  45. package/src/packages/carl-mcp/tools/domains.js +361 -0
  46. package/src/packages/carl-mcp/tools/psmm.js +204 -0
  47. package/src/packages/carl-mcp/tools/staging.js +245 -0
  48. package/src/skill/base.md +111 -0
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: satellite-detection.py
4
+ Purpose: Session-start hook that scans the workspace recursively for .paul/paul.json
5
+ files, cross-references registered satellites in workspace.json, and
6
+ auto-registers any unregistered projects it finds.
7
+ Triggers: UserPromptSubmit (session context)
8
+ Output: <base-satellites> block if new satellites registered, silent otherwise.
9
+ """
10
+
11
+ import sys
12
+ import json
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ # Workspace root — find .base/ relative to this hook's location
17
+ HOOK_DIR = Path(__file__).resolve().parent
18
+ WORKSPACE_ROOT = HOOK_DIR.parent.parent # hooks/ -> .base/ -> workspace
19
+ BASE_DIR = WORKSPACE_ROOT / ".base"
20
+ MANIFEST_FILE = BASE_DIR / "workspace.json"
21
+
22
+
23
+ def has_hidden_component(path: Path, workspace_root: Path) -> bool:
24
+ """
25
+ Return True if any component of path (relative to workspace_root) starts with '.',
26
+ excluding '.paul' itself (which is the expected target directory).
27
+ """
28
+ try:
29
+ rel = path.relative_to(workspace_root)
30
+ except ValueError:
31
+ return True # Can't relativize — skip it
32
+ return any(part.startswith(".") and part != ".paul" for part in rel.parts)
33
+
34
+
35
+ def find_paul_json_files(workspace_root: Path) -> list[Path]:
36
+ """
37
+ Recursively scan workspace_root for .paul/paul.json files.
38
+ Skips any path that has a hidden directory component (starts with '.').
39
+ """
40
+ results = []
41
+ try:
42
+ for paul_json in workspace_root.rglob(".paul/paul.json"):
43
+ if not has_hidden_component(paul_json, workspace_root):
44
+ results.append(paul_json)
45
+ except (OSError, PermissionError):
46
+ pass
47
+ return results
48
+
49
+
50
+ def main():
51
+ # Skip if BASE is not installed
52
+ if not BASE_DIR.exists() or not MANIFEST_FILE.exists():
53
+ sys.exit(0)
54
+
55
+ try:
56
+ with open(MANIFEST_FILE, "r") as f:
57
+ manifest = json.load(f)
58
+ except (json.JSONDecodeError, OSError):
59
+ sys.exit(0)
60
+
61
+ satellites = manifest.get("satellites", {})
62
+ new_registrations = []
63
+ activity_refreshed = []
64
+
65
+ paul_files = find_paul_json_files(WORKSPACE_ROOT)
66
+
67
+ for paul_json_path in paul_files:
68
+ try:
69
+ with open(paul_json_path, "r") as f:
70
+ paul_data = json.load(f)
71
+ except (json.JSONDecodeError, OSError):
72
+ continue # Malformed or unreadable — skip silently
73
+
74
+ name = paul_data.get("name")
75
+ if not name:
76
+ continue # No name field — skip
77
+
78
+ # Read last_activity from paul.json timestamps (if present)
79
+ last_activity = paul_data.get("timestamps", {}).get("updated_at")
80
+
81
+ if name in satellites:
82
+ # Already registered — refresh last_activity if available
83
+ if last_activity and satellites[name].get("last_activity") != last_activity:
84
+ satellites[name]["last_activity"] = last_activity
85
+ activity_refreshed.append(name)
86
+ continue
87
+
88
+ # New satellite — derive relative path
89
+ project_dir = paul_json_path.parent.parent
90
+ try:
91
+ rel_path = str(project_dir.relative_to(WORKSPACE_ROOT))
92
+ except ValueError:
93
+ continue # Can't relativize — skip
94
+
95
+ # Build registration entry
96
+ entry = {
97
+ "path": rel_path,
98
+ "engine": "paul",
99
+ "state": f"{rel_path}/.paul/STATE.md",
100
+ "registered": datetime.now().strftime("%Y-%m-%d"),
101
+ "groom_check": True,
102
+ }
103
+ if last_activity:
104
+ entry["last_activity"] = last_activity
105
+
106
+ satellites[name] = entry
107
+ new_registrations.append(name)
108
+
109
+ if new_registrations or activity_refreshed:
110
+ # Write updated manifest
111
+ try:
112
+ manifest["satellites"] = satellites
113
+ with open(MANIFEST_FILE, "w") as f:
114
+ json.dump(manifest, f, indent=2)
115
+ f.write("\n")
116
+ except OSError:
117
+ sys.exit(0) # Write failed — silent exit
118
+
119
+ if new_registrations:
120
+ names_str = ", ".join(new_registrations)
121
+ n = len(new_registrations)
122
+ print(f"<base-satellites>\nAuto-registered {n} new satellite(s): {names_str}\n</base-satellites>")
123
+
124
+ sys.exit(0)
125
+
126
+
127
+ if __name__ == "__main__":
128
+ try:
129
+ main()
130
+ except Exception:
131
+ sys.exit(0)
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BASE MCP — Surface CRUD Server
4
+ * Builder's Automated State Engine
5
+ *
6
+ * Generic CRUD operations for any registered data surface.
7
+ * Surfaces are registered in workspace.json and stored as JSON in .base/data/.
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 surfaceTools, handleTool as handleSurface } from './tools/surfaces.js';
18
+
19
+ // ============================================================
20
+ // CONFIGURATION
21
+ // ============================================================
22
+
23
+ // Resolve workspace from this file's location: base-mcp/ → .base/ → workspace root
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const WORKSPACE_PATH = path.resolve(__dirname, '../..');
26
+
27
+ function debugLog(...args) {
28
+ console.error('[BASE]', new Date().toISOString(), ...args);
29
+ }
30
+
31
+ // ============================================================
32
+ // TOOL REGISTRY
33
+ // ============================================================
34
+
35
+ const ALL_TOOLS = [...surfaceTools];
36
+
37
+ // Build handler lookup: tool name → handler function
38
+ const TOOL_HANDLERS = {};
39
+ for (const tool of surfaceTools) TOOL_HANDLERS[tool.name] = handleSurface;
40
+
41
+ // ============================================================
42
+ // MCP SERVER
43
+ // ============================================================
44
+
45
+ const server = new Server({
46
+ name: "base-mcp",
47
+ version: "1.0.0",
48
+ }, {
49
+ capabilities: {
50
+ tools: {},
51
+ },
52
+ });
53
+
54
+ debugLog('BASE MCP Server initialized');
55
+ debugLog('Workspace:', WORKSPACE_PATH);
56
+ debugLog('Surface tools:', surfaceTools.length);
57
+ debugLog('Total tools:', ALL_TOOLS.length);
58
+
59
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
60
+ debugLog('List tools request');
61
+ return { tools: ALL_TOOLS };
62
+ });
63
+
64
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
+ const { name, arguments: args } = request.params;
66
+ debugLog('Call tool:', name);
67
+
68
+ try {
69
+ const handler = TOOL_HANDLERS[name];
70
+ if (!handler) {
71
+ throw new Error(`Unknown tool: ${name}`);
72
+ }
73
+
74
+ const result = await handler(name, args || {}, WORKSPACE_PATH);
75
+
76
+ if (result === null) {
77
+ throw new Error(`Tool ${name} returned null — handler mismatch`);
78
+ }
79
+
80
+ return {
81
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
82
+ isError: false,
83
+ };
84
+ } catch (error) {
85
+ debugLog('Error:', error.message);
86
+ return {
87
+ content: [{ type: "text", text: `Error: ${error.message}` }],
88
+ isError: true,
89
+ };
90
+ }
91
+ });
92
+
93
+ // ============================================================
94
+ // RUN
95
+ // ============================================================
96
+
97
+ async function runServer() {
98
+ const transport = new StdioServerTransport();
99
+ await server.connect(transport);
100
+ console.error("BASE MCP Server running on stdio");
101
+ }
102
+
103
+ try {
104
+ await runServer();
105
+ } catch (error) {
106
+ console.error("Fatal error:", error);
107
+ process.exit(1);
108
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "base-mcp",
3
+ "version": "1.0.0",
4
+ "description": "BASE Surface CRUD Server - Generic operations for registered data surfaces",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "dependencies": {
8
+ "@modelcontextprotocol/sdk": "^1.0.0"
9
+ }
10
+ }
@@ -0,0 +1,404 @@
1
+ /**
2
+ * BASE Surfaces — Generic CRUD tools for registered data surfaces
3
+ * Operates on any surface registered in workspace.json
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+
9
+ function debugLog(...args) {
10
+ console.error('[BASE:surfaces]', new Date().toISOString(), ...args);
11
+ }
12
+
13
+ // ============================================================
14
+ // HELPERS
15
+ // ============================================================
16
+
17
+ function getWorkspaceJson(workspacePath) {
18
+ const filepath = join(workspacePath, '.base', 'workspace.json');
19
+ if (!existsSync(filepath)) {
20
+ throw new Error('workspace.json not found at ' + filepath);
21
+ }
22
+ try {
23
+ return JSON.parse(readFileSync(filepath, 'utf-8'));
24
+ } catch (error) {
25
+ throw new Error('Failed to parse workspace.json: ' + error.message);
26
+ }
27
+ }
28
+
29
+ function getSurfaceConfig(workspacePath, surface) {
30
+ const ws = getWorkspaceJson(workspacePath);
31
+ const surfaces = ws.surfaces || {};
32
+ const config = surfaces[surface];
33
+ if (!config) {
34
+ throw new Error(`Surface "${surface}" is not registered in workspace.json. Registered surfaces: ${Object.keys(surfaces).join(', ') || 'none'}`);
35
+ }
36
+ return config;
37
+ }
38
+
39
+ function getSurfacePath(workspacePath, config) {
40
+ return join(workspacePath, '.base', config.file);
41
+ }
42
+
43
+ function readSurface(workspacePath, surface) {
44
+ const config = getSurfaceConfig(workspacePath, surface);
45
+ const filepath = getSurfacePath(workspacePath, config);
46
+ if (!existsSync(filepath)) {
47
+ return { surface, version: 1, last_modified: null, items: [], archived: [] };
48
+ }
49
+ try {
50
+ return JSON.parse(readFileSync(filepath, 'utf-8'));
51
+ } catch (error) {
52
+ debugLog(`Error reading ${surface} data:`, error.message);
53
+ return { surface, version: 1, last_modified: null, items: [], archived: [] };
54
+ }
55
+ }
56
+
57
+ function writeSurface(workspacePath, surface, data) {
58
+ const config = getSurfaceConfig(workspacePath, surface);
59
+ const filepath = getSurfacePath(workspacePath, config);
60
+ data.last_modified = formatTimestamp();
61
+ writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
62
+ }
63
+
64
+ function generateId(prefix, items) {
65
+ let max = 0;
66
+ for (const item of items) {
67
+ const id = item.id || '';
68
+ const match = id.match(new RegExp(`^${prefix}-(\\d+)$`));
69
+ if (match) {
70
+ const num = parseInt(match[1], 10);
71
+ if (num > max) max = num;
72
+ }
73
+ }
74
+ return `${prefix}-${String(max + 1).padStart(3, '0')}`;
75
+ }
76
+
77
+ function formatTimestamp() {
78
+ return new Date().toISOString();
79
+ }
80
+
81
+ // ============================================================
82
+ // TOOL DEFINITIONS
83
+ // ============================================================
84
+
85
+ export const TOOLS = [
86
+ {
87
+ name: "base_list_surfaces",
88
+ description: "List all registered data surfaces from workspace.json with item counts. Returns surface names, descriptions, and current item counts.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {},
92
+ required: []
93
+ }
94
+ },
95
+ {
96
+ name: "base_get_surface",
97
+ description: "Read all items from a registered data surface. Returns the full surface object including metadata, items array, and archived array.",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ surface: {
102
+ type: "string",
103
+ description: "Surface name (e.g., 'active', 'backlog')"
104
+ }
105
+ },
106
+ required: ["surface"]
107
+ }
108
+ },
109
+ {
110
+ name: "base_get_item",
111
+ description: "Get a specific item by ID from a data surface.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ surface: {
116
+ type: "string",
117
+ description: "Surface name (e.g., 'active', 'backlog')"
118
+ },
119
+ id: {
120
+ type: "string",
121
+ description: "Item ID (e.g., 'ACT-001', 'BL-003')"
122
+ }
123
+ },
124
+ required: ["surface", "id"]
125
+ }
126
+ },
127
+ {
128
+ name: "base_add_item",
129
+ description: "Add a new item to a data surface. Auto-generates ID from surface schema's id_prefix. Validates required fields if schema defines them.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ surface: {
134
+ type: "string",
135
+ description: "Surface name (e.g., 'active', 'backlog')"
136
+ },
137
+ data: {
138
+ type: "object",
139
+ description: "Item data object with fields matching the surface schema"
140
+ }
141
+ },
142
+ required: ["surface", "data"]
143
+ }
144
+ },
145
+ {
146
+ name: "base_update_item",
147
+ description: "Update an existing item's fields by ID. Performs shallow merge — only specified fields are updated, others preserved.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ surface: {
152
+ type: "string",
153
+ description: "Surface name (e.g., 'active', 'backlog')"
154
+ },
155
+ id: {
156
+ type: "string",
157
+ description: "Item ID to update"
158
+ },
159
+ data: {
160
+ type: "object",
161
+ description: "Fields to update (shallow merge onto existing item)"
162
+ }
163
+ },
164
+ required: ["surface", "id", "data"]
165
+ }
166
+ },
167
+ {
168
+ name: "base_archive_item",
169
+ description: "Archive an item by ID — removes from items array and moves to archived array with timestamp.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ surface: {
174
+ type: "string",
175
+ description: "Surface name (e.g., 'active', 'backlog')"
176
+ },
177
+ id: {
178
+ type: "string",
179
+ description: "Item ID to archive"
180
+ }
181
+ },
182
+ required: ["surface", "id"]
183
+ }
184
+ },
185
+ {
186
+ name: "base_search",
187
+ description: "Search across one or all registered surfaces by keyword. Case-insensitive substring match across all string fields in each item.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ query: {
192
+ type: "string",
193
+ description: "Search query (case-insensitive substring match)"
194
+ },
195
+ surface: {
196
+ type: "string",
197
+ description: "Optional: limit search to one surface. Omit to search all."
198
+ }
199
+ },
200
+ required: ["query"]
201
+ }
202
+ }
203
+ ];
204
+
205
+ // ============================================================
206
+ // TOOL HANDLERS
207
+ // ============================================================
208
+
209
+ function handleListSurfaces(workspacePath) {
210
+ const ws = getWorkspaceJson(workspacePath);
211
+ const surfaces = ws.surfaces || {};
212
+ const result = [];
213
+
214
+ for (const [name, config] of Object.entries(surfaces)) {
215
+ const filepath = join(workspacePath, '.base', config.file);
216
+ let itemsCount = 0;
217
+ if (existsSync(filepath)) {
218
+ try {
219
+ const data = JSON.parse(readFileSync(filepath, 'utf-8'));
220
+ itemsCount = (data.items || []).length;
221
+ } catch { /* ignore parse errors */ }
222
+ }
223
+ result.push({
224
+ name,
225
+ description: config.description || '',
226
+ file: config.file,
227
+ items_count: itemsCount,
228
+ hook: config.hook || false
229
+ });
230
+ }
231
+
232
+ return { surfaces: result, count: result.length };
233
+ }
234
+
235
+ function handleGetSurface(args, workspacePath) {
236
+ const { surface } = args;
237
+ if (!surface) throw new Error('Missing required parameter: surface');
238
+ return readSurface(workspacePath, surface);
239
+ }
240
+
241
+ function handleGetItem(args, workspacePath) {
242
+ const { surface, id } = args;
243
+ if (!surface) throw new Error('Missing required parameter: surface');
244
+ if (!id) throw new Error('Missing required parameter: id');
245
+
246
+ const data = readSurface(workspacePath, surface);
247
+ const item = data.items.find(i => i.id === id);
248
+ if (!item) {
249
+ throw new Error(`Item "${id}" not found in surface "${surface}". Available IDs: ${data.items.map(i => i.id).join(', ') || 'none'}`);
250
+ }
251
+ return item;
252
+ }
253
+
254
+ function handleAddItem(args, workspacePath) {
255
+ const { surface, data: itemData } = args;
256
+ if (!surface) throw new Error('Missing required parameter: surface');
257
+ if (!itemData) throw new Error('Missing required parameter: data');
258
+
259
+ const config = getSurfaceConfig(workspacePath, surface);
260
+ const schema = config.schema || {};
261
+
262
+ // Validate required fields
263
+ if (schema.required_fields) {
264
+ for (const field of schema.required_fields) {
265
+ if (itemData[field] === undefined || itemData[field] === null || itemData[field] === '') {
266
+ throw new Error(`Missing required field "${field}" for surface "${surface}". Required: ${schema.required_fields.join(', ')}`);
267
+ }
268
+ }
269
+ }
270
+
271
+ const surfaceData = readSurface(workspacePath, surface);
272
+
273
+ // Generate ID
274
+ const prefix = schema.id_prefix || surface.toUpperCase().slice(0, 3);
275
+ const newId = generateId(prefix, surfaceData.items);
276
+
277
+ const newItem = {
278
+ id: newId,
279
+ ...itemData,
280
+ added: formatTimestamp()
281
+ };
282
+
283
+ surfaceData.items.push(newItem);
284
+ writeSurface(workspacePath, surface, surfaceData);
285
+
286
+ debugLog(`Added item ${newId} to ${surface}`);
287
+ return newItem;
288
+ }
289
+
290
+ function handleUpdateItem(args, workspacePath) {
291
+ const { surface, id, data: updateData } = args;
292
+ if (!surface) throw new Error('Missing required parameter: surface');
293
+ if (!id) throw new Error('Missing required parameter: id');
294
+ if (!updateData) throw new Error('Missing required parameter: data');
295
+
296
+ const surfaceData = readSurface(workspacePath, surface);
297
+ const index = surfaceData.items.findIndex(i => i.id === id);
298
+ if (index === -1) {
299
+ throw new Error(`Item "${id}" not found in surface "${surface}"`);
300
+ }
301
+
302
+ // Shallow merge — preserve existing fields, update specified ones
303
+ surfaceData.items[index] = {
304
+ ...surfaceData.items[index],
305
+ ...updateData,
306
+ id, // Prevent ID overwrite
307
+ updated: formatTimestamp()
308
+ };
309
+
310
+ writeSurface(workspacePath, surface, surfaceData);
311
+
312
+ debugLog(`Updated item ${id} in ${surface}`);
313
+ return surfaceData.items[index];
314
+ }
315
+
316
+ function handleArchiveItem(args, workspacePath) {
317
+ const { surface, id } = args;
318
+ if (!surface) throw new Error('Missing required parameter: surface');
319
+ if (!id) throw new Error('Missing required parameter: id');
320
+
321
+ const surfaceData = readSurface(workspacePath, surface);
322
+ const index = surfaceData.items.findIndex(i => i.id === id);
323
+ if (index === -1) {
324
+ throw new Error(`Item "${id}" not found in surface "${surface}"`);
325
+ }
326
+
327
+ // Remove from items
328
+ const [item] = surfaceData.items.splice(index, 1);
329
+ item.archived = formatTimestamp();
330
+
331
+ // Add to archived array
332
+ if (!surfaceData.archived) surfaceData.archived = [];
333
+ surfaceData.archived.push(item);
334
+
335
+ writeSurface(workspacePath, surface, surfaceData);
336
+
337
+ debugLog(`Archived item ${id} from ${surface}`);
338
+ return item;
339
+ }
340
+
341
+ function handleSearch(args, workspacePath) {
342
+ const { query, surface: targetSurface } = args;
343
+ if (!query) throw new Error('Missing required parameter: query');
344
+
345
+ const ws = getWorkspaceJson(workspacePath);
346
+ const surfaces = ws.surfaces || {};
347
+ const queryLower = query.toLowerCase();
348
+ const results = [];
349
+
350
+ const surfacesToSearch = targetSurface
351
+ ? { [targetSurface]: getSurfaceConfig(workspacePath, targetSurface) }
352
+ : surfaces;
353
+
354
+ for (const [name, config] of Object.entries(surfacesToSearch)) {
355
+ const filepath = join(workspacePath, '.base', config.file);
356
+ if (!existsSync(filepath)) continue;
357
+
358
+ let data;
359
+ try {
360
+ data = JSON.parse(readFileSync(filepath, 'utf-8'));
361
+ } catch { continue; }
362
+
363
+ for (const item of (data.items || [])) {
364
+ for (const [field, value] of Object.entries(item)) {
365
+ if (typeof value === 'string' && value.toLowerCase().includes(queryLower)) {
366
+ results.push({
367
+ surface: name,
368
+ id: item.id,
369
+ title: item.title || item.id,
370
+ match_field: field
371
+ });
372
+ break; // One match per item is enough
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ return { results, count: results.length, query };
379
+ }
380
+
381
+ // ============================================================
382
+ // HANDLER DISPATCH
383
+ // ============================================================
384
+
385
+ export function handleTool(name, args, workspacePath) {
386
+ switch (name) {
387
+ case 'base_list_surfaces':
388
+ return handleListSurfaces(workspacePath);
389
+ case 'base_get_surface':
390
+ return handleGetSurface(args, workspacePath);
391
+ case 'base_get_item':
392
+ return handleGetItem(args, workspacePath);
393
+ case 'base_add_item':
394
+ return handleAddItem(args, workspacePath);
395
+ case 'base_update_item':
396
+ return handleUpdateItem(args, workspacePath);
397
+ case 'base_archive_item':
398
+ return handleArchiveItem(args, workspacePath);
399
+ case 'base_search':
400
+ return handleSearch(args, workspacePath);
401
+ default:
402
+ return null;
403
+ }
404
+ }