@claudetools/tools 0.1.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.
@@ -0,0 +1,338 @@
1
+ // =============================================================================
2
+ // Auto Project Registration System
3
+ // =============================================================================
4
+ // Automatically registers projects with the ClaudeTools API when used in
5
+ // unregistered directories. Caches project bindings locally for fast lookups.
6
+ // =============================================================================
7
+ import { mcpLogger } from '../logger.js';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import * as os from 'os';
11
+ import { execSync } from 'child_process';
12
+ import { API_BASE_URL, PROJECTS_FILE, CLAUDETOOLS_DIR } from './config.js';
13
+ import { getConfig } from './config-manager.js';
14
+ // -----------------------------------------------------------------------------
15
+ // System Registration
16
+ // -----------------------------------------------------------------------------
17
+ /**
18
+ * Get unique system identifier (hostname)
19
+ */
20
+ function getSystemInfo() {
21
+ return {
22
+ hostname: os.hostname(),
23
+ platform: os.platform(),
24
+ username: os.userInfo().username,
25
+ };
26
+ }
27
+ /**
28
+ * Get or register this machine as a system
29
+ * Returns system_id for binding projects
30
+ */
31
+ async function ensureSystemRegistered() {
32
+ const cache = loadProjectsCache();
33
+ // Check if we already have a system_id cached
34
+ if (cache.system_id) {
35
+ mcpLogger.debug('REGISTRATION', `Using cached system ID: ${cache.system_id}`);
36
+ return cache.system_id;
37
+ }
38
+ // Register this system
39
+ const systemInfo = getSystemInfo();
40
+ mcpLogger.info('REGISTRATION', `Registering new system: ${systemInfo.hostname}`);
41
+ try {
42
+ const config = getConfig();
43
+ const apiKey = config.apiKey || process.env.MEMORY_API_KEY || process.env.CLAUDETOOLS_API_KEY;
44
+ if (!apiKey) {
45
+ throw new Error('No API key found. Set CLAUDETOOLS_API_KEY or MEMORY_API_KEY in environment, ' +
46
+ 'or configure via ~/.claudetools/config.json');
47
+ }
48
+ const response = await fetch(`${API_BASE_URL}/api/v1/systems/register`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'Authorization': `Bearer ${apiKey}`,
53
+ },
54
+ body: JSON.stringify({
55
+ hostname: systemInfo.hostname,
56
+ platform: systemInfo.platform,
57
+ username: systemInfo.username,
58
+ }),
59
+ });
60
+ if (!response.ok) {
61
+ const error = await response.text();
62
+ throw new Error(`System registration failed: ${response.status} ${error}`);
63
+ }
64
+ const result = await response.json();
65
+ const systemId = result.data?.system_id || result.system_id;
66
+ if (!systemId) {
67
+ throw new Error('System registration did not return a system_id');
68
+ }
69
+ mcpLogger.info('REGISTRATION', `System registered successfully: ${systemId}`);
70
+ // Cache the system_id
71
+ cache.system_id = systemId;
72
+ saveProjectsCache(cache);
73
+ return systemId;
74
+ }
75
+ catch (error) {
76
+ mcpLogger.error('REGISTRATION', `System registration failed: ${error}`);
77
+ throw new Error(`Failed to register system: ${error}`);
78
+ }
79
+ }
80
+ // -----------------------------------------------------------------------------
81
+ // Git Detection
82
+ // -----------------------------------------------------------------------------
83
+ /**
84
+ * Try to detect git remote for a directory
85
+ */
86
+ function detectGitRemote(localPath) {
87
+ try {
88
+ const gitRemote = execSync('git config --get remote.origin.url', {
89
+ cwd: localPath,
90
+ encoding: 'utf-8',
91
+ stdio: ['pipe', 'pipe', 'ignore'],
92
+ }).trim();
93
+ return gitRemote || undefined;
94
+ }
95
+ catch {
96
+ // Not a git repo or no remote configured
97
+ return undefined;
98
+ }
99
+ }
100
+ // -----------------------------------------------------------------------------
101
+ // Project Registration
102
+ // -----------------------------------------------------------------------------
103
+ /**
104
+ * Register a new project via API
105
+ */
106
+ async function registerProject(localPath) {
107
+ const systemId = await ensureSystemRegistered();
108
+ const projectName = path.basename(localPath);
109
+ const gitRemote = detectGitRemote(localPath);
110
+ mcpLogger.info('REGISTRATION', `Registering new project: ${projectName} at ${localPath}`);
111
+ try {
112
+ const config = getConfig();
113
+ const apiKey = config.apiKey || process.env.MEMORY_API_KEY || process.env.CLAUDETOOLS_API_KEY;
114
+ if (!apiKey) {
115
+ throw new Error('No API key found. Set CLAUDETOOLS_API_KEY or MEMORY_API_KEY in environment, ' +
116
+ 'or configure via ~/.claudetools/config.json');
117
+ }
118
+ // Step 1: Create project
119
+ const projectResponse = await fetch(`${API_BASE_URL}/api/v1/projects`, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ 'Authorization': `Bearer ${apiKey}`,
124
+ },
125
+ body: JSON.stringify({
126
+ name: projectName,
127
+ description: `Auto-registered project from ${localPath}`,
128
+ git_remote: gitRemote,
129
+ }),
130
+ });
131
+ if (!projectResponse.ok) {
132
+ const error = await projectResponse.text();
133
+ throw new Error(`Project creation failed: ${projectResponse.status} ${error}`);
134
+ }
135
+ const projectResult = await projectResponse.json();
136
+ const projectId = projectResult.data?.project_id || projectResult.project_id;
137
+ const orgId = projectResult.data?.org_id || projectResult.org_id || 'default';
138
+ if (!projectId) {
139
+ throw new Error('Project creation did not return a project_id');
140
+ }
141
+ mcpLogger.info('REGISTRATION', `Project created: ${projectId}`);
142
+ // Step 2: Create binding (link project to local path on this system)
143
+ const bindingResponse = await fetch(`${API_BASE_URL}/api/v1/systems/${systemId}/bindings`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'Authorization': `Bearer ${apiKey}`,
148
+ },
149
+ body: JSON.stringify({
150
+ project_id: projectId,
151
+ local_path: localPath,
152
+ git_remote: gitRemote,
153
+ }),
154
+ });
155
+ if (!bindingResponse.ok) {
156
+ const error = await bindingResponse.text();
157
+ throw new Error(`Binding creation failed: ${bindingResponse.status} ${error}`);
158
+ }
159
+ const bindingResult = await bindingResponse.json();
160
+ const bindingId = bindingResult.data?.binding_id || bindingResult.binding_id;
161
+ if (!bindingId) {
162
+ throw new Error('Binding creation did not return a binding_id');
163
+ }
164
+ mcpLogger.info('REGISTRATION', `Binding created: ${bindingId}`);
165
+ const binding = {
166
+ binding_id: bindingId,
167
+ project_id: projectId,
168
+ system_id: systemId,
169
+ local_path: localPath,
170
+ git_remote: gitRemote,
171
+ project_name: projectName,
172
+ org_id: orgId,
173
+ cached_at: new Date().toISOString(),
174
+ };
175
+ // Update local cache
176
+ updateProjectsCache(binding);
177
+ mcpLogger.info('REGISTRATION', `Successfully registered project: ${projectName} → ${projectId}`);
178
+ // Also cache the system_id at top level for future use
179
+ const cache = loadProjectsCache();
180
+ if (!cache.system_id) {
181
+ cache.system_id = systemId;
182
+ saveProjectsCache(cache);
183
+ }
184
+ return binding;
185
+ }
186
+ catch (error) {
187
+ mcpLogger.error('REGISTRATION', `Project registration failed: ${error}`);
188
+ throw new Error(`Failed to register project: ${error}`);
189
+ }
190
+ }
191
+ // -----------------------------------------------------------------------------
192
+ // Cache Management
193
+ // -----------------------------------------------------------------------------
194
+ /**
195
+ * Load projects cache from disk
196
+ */
197
+ function loadProjectsCache() {
198
+ try {
199
+ // Ensure directory exists
200
+ if (!fs.existsSync(CLAUDETOOLS_DIR)) {
201
+ fs.mkdirSync(CLAUDETOOLS_DIR, { recursive: true });
202
+ }
203
+ if (fs.existsSync(PROJECTS_FILE)) {
204
+ const content = fs.readFileSync(PROJECTS_FILE, 'utf-8');
205
+ const cache = JSON.parse(content);
206
+ // Ensure bindings array exists
207
+ if (!cache.bindings) {
208
+ cache.bindings = [];
209
+ }
210
+ return cache;
211
+ }
212
+ }
213
+ catch (error) {
214
+ mcpLogger.error('REGISTRATION', `Error loading projects cache: ${error}`);
215
+ }
216
+ // Return empty cache
217
+ return {
218
+ bindings: [],
219
+ last_sync: new Date().toISOString(),
220
+ };
221
+ }
222
+ /**
223
+ * Save projects cache to disk
224
+ */
225
+ function saveProjectsCache(cache) {
226
+ try {
227
+ // Ensure directory exists
228
+ if (!fs.existsSync(CLAUDETOOLS_DIR)) {
229
+ fs.mkdirSync(CLAUDETOOLS_DIR, { recursive: true });
230
+ }
231
+ cache.last_sync = new Date().toISOString();
232
+ const content = JSON.stringify(cache, null, 2);
233
+ fs.writeFileSync(PROJECTS_FILE, content, 'utf-8');
234
+ mcpLogger.debug('REGISTRATION', `Projects cache saved to ${PROJECTS_FILE}`);
235
+ }
236
+ catch (error) {
237
+ mcpLogger.error('REGISTRATION', `Error saving projects cache: ${error}`);
238
+ throw error;
239
+ }
240
+ }
241
+ /**
242
+ * Update local cache with new binding
243
+ */
244
+ function updateProjectsCache(binding) {
245
+ const cache = loadProjectsCache();
246
+ // Remove any existing binding for this path
247
+ cache.bindings = cache.bindings.filter(b => b.local_path !== binding.local_path);
248
+ // Add new binding
249
+ cache.bindings.push(binding);
250
+ saveProjectsCache(cache);
251
+ }
252
+ // -----------------------------------------------------------------------------
253
+ // Main Entry Point
254
+ // -----------------------------------------------------------------------------
255
+ /**
256
+ * Get or register project for a local path
257
+ * This is the main function called by config.ts
258
+ *
259
+ * Returns the project_id (UUID) for the given path
260
+ */
261
+ export async function getOrRegisterProject(localPath) {
262
+ const config = getConfig();
263
+ // Check if auto-registration is disabled
264
+ if (!config.autoRegister) {
265
+ throw new Error(`No project binding found for ${localPath} and auto-registration is disabled. ` +
266
+ 'Enable auto-registration in ~/.claudetools/config.json or manually register this project.');
267
+ }
268
+ mcpLogger.debug('REGISTRATION', `Looking up project for: ${localPath}`);
269
+ // Load cache and check for existing binding
270
+ const cache = loadProjectsCache();
271
+ // Try exact match first
272
+ const exactMatch = cache.bindings.find(b => b.local_path === localPath);
273
+ if (exactMatch) {
274
+ mcpLogger.debug('REGISTRATION', `Found exact match: ${exactMatch.project_id}`);
275
+ return exactMatch.project_id;
276
+ }
277
+ // Try prefix match (for subdirectories)
278
+ const prefixMatches = cache.bindings
279
+ .filter(b => localPath.startsWith(b.local_path + '/') || localPath === b.local_path)
280
+ .sort((a, b) => b.local_path.length - a.local_path.length);
281
+ if (prefixMatches.length > 0) {
282
+ mcpLogger.debug('REGISTRATION', `Found prefix match: ${prefixMatches[0].project_id}`);
283
+ return prefixMatches[0].project_id;
284
+ }
285
+ // No match found - register new project
286
+ mcpLogger.info('REGISTRATION', `No binding found for ${localPath}, registering...`);
287
+ try {
288
+ const binding = await registerProject(localPath);
289
+ return binding.project_id;
290
+ }
291
+ catch (error) {
292
+ mcpLogger.error('REGISTRATION', `Auto-registration failed: ${error}`);
293
+ throw new Error(`Failed to auto-register project for ${localPath}: ${error}\n\n` +
294
+ 'Please check:\n' +
295
+ '1. API key is configured (CLAUDETOOLS_API_KEY or MEMORY_API_KEY)\n' +
296
+ '2. API URL is correct in ~/.claudetools/config.json\n' +
297
+ '3. Network connectivity to ClaudeTools API\n\n' +
298
+ 'You can disable auto-registration by setting autoRegister: false in config.');
299
+ }
300
+ }
301
+ /**
302
+ * Sync projects from API (for future use)
303
+ * This would fetch all bindings from the API and update local cache
304
+ */
305
+ export async function syncProjectsFromAPI() {
306
+ const systemId = await ensureSystemRegistered();
307
+ mcpLogger.info('REGISTRATION', 'Syncing projects from API...');
308
+ try {
309
+ const config = getConfig();
310
+ const apiKey = config.apiKey || process.env.MEMORY_API_KEY || process.env.CLAUDETOOLS_API_KEY;
311
+ if (!apiKey) {
312
+ throw new Error('No API key found for sync operation');
313
+ }
314
+ const response = await fetch(`${API_BASE_URL}/api/v1/systems/${systemId}/bindings`, {
315
+ method: 'GET',
316
+ headers: {
317
+ 'Authorization': `Bearer ${apiKey}`,
318
+ },
319
+ });
320
+ if (!response.ok) {
321
+ throw new Error(`Failed to fetch bindings: ${response.status}`);
322
+ }
323
+ const result = await response.json();
324
+ const bindings = result.data?.bindings || result.bindings || [];
325
+ // Update cache
326
+ const cache = loadProjectsCache();
327
+ cache.bindings = bindings.map((b) => ({
328
+ ...b,
329
+ cached_at: new Date().toISOString(),
330
+ }));
331
+ saveProjectsCache(cache);
332
+ mcpLogger.info('REGISTRATION', `Synced ${bindings.length} project bindings from API`);
333
+ }
334
+ catch (error) {
335
+ mcpLogger.error('REGISTRATION', `Sync failed: ${error}`);
336
+ throw error;
337
+ }
338
+ }
@@ -0,0 +1,152 @@
1
+ import type { ExpertWorker } from './workers.js';
2
+ export interface Task {
3
+ id: string;
4
+ type: 'epic' | 'task' | 'subtask';
5
+ parent_id: string | null;
6
+ user_id: string;
7
+ project_id: string;
8
+ title: string;
9
+ description: string;
10
+ acceptance_criteria: string | string[];
11
+ status: string;
12
+ priority: string;
13
+ blocked_by: string | string[];
14
+ agent_type: string | null;
15
+ domain: string | null;
16
+ assigned_to: string | null;
17
+ locked_at: string | null;
18
+ lock_expires_at: string | null;
19
+ created_at: string;
20
+ updated_at: string;
21
+ completed_at: string | null;
22
+ estimated_effort: string | null;
23
+ tags: string | string[];
24
+ metadata: Record<string, unknown>;
25
+ }
26
+ export interface TaskContext {
27
+ id: string;
28
+ task_id: string;
29
+ context_type: string;
30
+ content: string;
31
+ source: string | null;
32
+ added_by: string;
33
+ created_at: string;
34
+ }
35
+ export interface DispatchableTask {
36
+ task: Task;
37
+ worker: ExpertWorker;
38
+ context: TaskContext[];
39
+ parentContext?: Task;
40
+ dependencies?: Task[];
41
+ }
42
+ export declare function parseJsonArray(value: string | string[] | null | undefined): string[];
43
+ export declare function createTask(userId: string, projectId: string, taskData: {
44
+ type: 'epic' | 'task' | 'subtask';
45
+ title: string;
46
+ description?: string;
47
+ parent_id?: string;
48
+ priority?: string;
49
+ acceptance_criteria?: string[];
50
+ blocked_by?: string[];
51
+ estimated_effort?: string;
52
+ tags?: string[];
53
+ agent_type?: string;
54
+ domain?: string;
55
+ }): Promise<{
56
+ success: boolean;
57
+ data: Task;
58
+ }>;
59
+ export declare function listTasks(userId: string, projectId: string, filters?: {
60
+ type?: string;
61
+ status?: string;
62
+ parent_id?: string;
63
+ assigned_to?: string;
64
+ limit?: number;
65
+ offset?: number;
66
+ }): Promise<{
67
+ success: boolean;
68
+ data: Task[];
69
+ meta: {
70
+ limit: number;
71
+ offset: number;
72
+ count: number;
73
+ };
74
+ }>;
75
+ export declare function getTask(userId: string, projectId: string, taskId: string, full?: boolean): Promise<{
76
+ success: boolean;
77
+ data: Task | {
78
+ task: Task;
79
+ context: TaskContext[];
80
+ subtasks: Task[];
81
+ parent?: Task;
82
+ };
83
+ }>;
84
+ export declare function claimTask(userId: string, projectId: string, taskId: string, agentId: string, lockDurationMinutes?: number): Promise<{
85
+ success: boolean;
86
+ data: {
87
+ claimed: boolean;
88
+ lock_expires_at: string;
89
+ task: Task;
90
+ context: TaskContext[];
91
+ };
92
+ }>;
93
+ export declare function releaseTask(userId: string, projectId: string, taskId: string, agentId: string, newStatus?: string, workLog?: string): Promise<{
94
+ success: boolean;
95
+ data: {
96
+ released: boolean;
97
+ task: Task;
98
+ };
99
+ }>;
100
+ export declare function updateTaskStatus(userId: string, projectId: string, taskId: string, status: string, agentId?: string): Promise<{
101
+ success: boolean;
102
+ data: Task;
103
+ }>;
104
+ export declare function addTaskContext(userId: string, projectId: string, taskId: string, contextType: string, content: string, addedBy: string, source?: string): Promise<{
105
+ success: boolean;
106
+ data: TaskContext;
107
+ }>;
108
+ export declare function getTaskSummary(userId: string, projectId: string): Promise<{
109
+ success: boolean;
110
+ data: {
111
+ by_status: Record<string, number>;
112
+ by_type: Record<string, number>;
113
+ active_agents: number;
114
+ recent_events: {
115
+ id: string;
116
+ task_id: string;
117
+ event_type: string;
118
+ task_title: string;
119
+ created_at: string;
120
+ }[];
121
+ };
122
+ }>;
123
+ export declare function heartbeatTask(userId: string, projectId: string, taskId: string, agentId: string, extendMinutes?: number): Promise<{
124
+ success: boolean;
125
+ data: {
126
+ extended: boolean;
127
+ new_expires_at: string;
128
+ };
129
+ }>;
130
+ /**
131
+ * Get all tasks ready for parallel dispatch
132
+ * - Filters for 'ready' status
133
+ * - Excludes already claimed tasks
134
+ * - Resolves dependencies (only returns unblocked tasks)
135
+ * - Matches each to appropriate expert worker
136
+ */
137
+ export declare function getDispatchableTasks(userId: string, projectId: string, epicId?: string, maxParallel?: number): Promise<DispatchableTask[]>;
138
+ /**
139
+ * Get full execution context for a worker agent
140
+ */
141
+ export declare function getExecutionContext(userId: string, projectId: string, taskId: string): Promise<{
142
+ task: Task;
143
+ worker: ExpertWorker;
144
+ systemPrompt: string;
145
+ context: TaskContext[];
146
+ parentTask?: Task;
147
+ siblingTasks?: Task[];
148
+ }>;
149
+ /**
150
+ * Find newly unblocked tasks after a completion
151
+ */
152
+ export declare function resolveTaskDependencies(userId: string, projectId: string, completedTaskId: string, epicId?: string): Promise<Task[]>;