@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,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,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
|
+
}
|