@artyfacts/claude 1.2.3 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artyfacts/claude",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Claude adapter for Artyfacts - Execute tasks using Claude Code CLI",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Claude Executor with Tools Support
3
+ *
4
+ * Executes tasks using Claude with full tool support.
5
+ * Uses the Anthropic SDK directly (not the CLI) to enable function calling.
6
+ */
7
+
8
+ import Anthropic from '@anthropic-ai/sdk';
9
+ import { ContextFetcher, createContextFetcher, buildPromptWithContext, TaskFullContext } from './context';
10
+ import { getToolsForPermissions, executeTool, ToolSchema, ApiClient, createApiClient } from './tools';
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface TaskContext {
17
+ /** Task ID (UUID or section_id) */
18
+ taskId: string;
19
+ /** Task heading/title */
20
+ heading: string;
21
+ /** Task content/description */
22
+ content: string;
23
+ /** Parent artifact ID */
24
+ artifactId: string;
25
+ /** Parent artifact title */
26
+ artifactTitle?: string;
27
+ /** Task priority (1=high, 2=medium, 3=low) */
28
+ priority?: number;
29
+ /** Additional context */
30
+ context?: Record<string, unknown>;
31
+ }
32
+
33
+ export interface ExecutionResult {
34
+ /** Whether execution was successful */
35
+ success: boolean;
36
+ /** The generated output */
37
+ output: string;
38
+ /** Summary of what was accomplished */
39
+ summary: string;
40
+ /** Any error message if failed */
41
+ error?: string;
42
+ /** The prompt that was used (for debugging) */
43
+ promptUsed?: string;
44
+ /** Tool calls made during execution */
45
+ toolCalls?: Array<{
46
+ name: string;
47
+ input: Record<string, unknown>;
48
+ output: unknown;
49
+ }>;
50
+ }
51
+
52
+ export interface ExecutorConfig {
53
+ /** Anthropic API key */
54
+ anthropicApiKey?: string;
55
+ /** Model to use (default: claude-sonnet-4-20250514) */
56
+ model?: string;
57
+ /** Timeout in milliseconds (default: 5 minutes) */
58
+ timeout?: number;
59
+ /** System prompt prefix */
60
+ systemPromptPrefix?: string;
61
+ /** Artyfacts API base URL */
62
+ baseUrl: string;
63
+ /** Artyfacts API key */
64
+ apiKey: string;
65
+ /** Agent's permissions (determines available tools) */
66
+ permissions?: string[];
67
+ /** Whether to use full context from API (default: true) */
68
+ useFullContext?: boolean;
69
+ /** Max tool call iterations (default: 10) */
70
+ maxToolIterations?: number;
71
+ }
72
+
73
+ // ============================================================================
74
+ // Constants
75
+ // ============================================================================
76
+
77
+ const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
78
+ const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
79
+ const DEFAULT_MAX_TOOL_ITERATIONS = 10;
80
+
81
+ const DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
82
+
83
+ You have access to tools that let you interact with Artyfacts - creating sections, artifacts, agents, and more.
84
+
85
+ Your job is to complete tasks assigned to you. For each task:
86
+ 1. Understand the requirements from the task heading and content
87
+ 2. Use the available tools to accomplish the task
88
+ 3. Provide a clear summary of what you did
89
+
90
+ Guidelines:
91
+ - USE TOOLS to create tangible outputs (sections, artifacts, agents)
92
+ - Don't just describe what you would do - actually do it using the tools
93
+ - Create sections on artifacts to document your work
94
+ - If creating agents, use the create_agent tool with full details
95
+ - If you cannot complete the task, explain why
96
+
97
+ After completing the task, provide a brief summary of what you accomplished.`;
98
+
99
+ // ============================================================================
100
+ // Executor Class
101
+ // ============================================================================
102
+
103
+ export class ClaudeExecutorWithTools {
104
+ private config: ExecutorConfig;
105
+ private anthropic: Anthropic;
106
+ private contextFetcher: ContextFetcher | null = null;
107
+ private apiClient: ApiClient;
108
+ private tools: ToolSchema[];
109
+
110
+ constructor(config: ExecutorConfig) {
111
+ this.config = {
112
+ model: DEFAULT_MODEL,
113
+ timeout: DEFAULT_TIMEOUT,
114
+ maxToolIterations: DEFAULT_MAX_TOOL_ITERATIONS,
115
+ permissions: ['sections:read', 'sections:write', 'artifacts:read', 'org:read'],
116
+ ...config,
117
+ };
118
+
119
+ // Initialize Anthropic client
120
+ this.anthropic = new Anthropic({
121
+ apiKey: config.anthropicApiKey,
122
+ });
123
+
124
+ // Initialize context fetcher
125
+ this.contextFetcher = createContextFetcher({
126
+ baseUrl: config.baseUrl,
127
+ apiKey: config.apiKey,
128
+ });
129
+
130
+ // Initialize API client for tool handlers
131
+ this.apiClient = createApiClient({
132
+ baseUrl: config.baseUrl,
133
+ apiKey: config.apiKey,
134
+ });
135
+
136
+ // Get tools based on permissions
137
+ this.tools = getToolsForPermissions(this.config.permissions || []);
138
+
139
+ console.log(` 🔧 Loaded ${this.tools.length} tools based on permissions`);
140
+ }
141
+
142
+ /**
143
+ * Execute a task using Claude with tools
144
+ */
145
+ async execute(task: TaskContext): Promise<ExecutionResult> {
146
+ const toolCalls: ExecutionResult['toolCalls'] = [];
147
+
148
+ try {
149
+ let prompt: string;
150
+ let fullContext: TaskFullContext | null = null;
151
+
152
+ // Try to fetch full context if configured
153
+ const useFullContext = this.config.useFullContext !== false && this.contextFetcher;
154
+
155
+ if (useFullContext) {
156
+ try {
157
+ fullContext = await this.contextFetcher!.fetchTaskContext(task.taskId);
158
+ prompt = buildPromptWithContext(fullContext);
159
+ console.log(' 📚 Using full context (org, project, artifact, related sections)');
160
+ } catch (contextError) {
161
+ console.warn(' ⚠️ Could not fetch full context, using minimal prompt');
162
+ console.warn(` ${contextError instanceof Error ? contextError.message : contextError}`);
163
+ prompt = this.buildTaskPrompt(task);
164
+ }
165
+ } else {
166
+ prompt = this.buildTaskPrompt(task);
167
+ }
168
+
169
+ // Build messages array
170
+ const messages: Anthropic.MessageParam[] = [
171
+ { role: 'user', content: prompt }
172
+ ];
173
+
174
+ // Build system prompt
175
+ const systemPrompt = this.config.systemPromptPrefix
176
+ ? `${this.config.systemPromptPrefix}\n\n${DEFAULT_SYSTEM_PROMPT}`
177
+ : DEFAULT_SYSTEM_PROMPT;
178
+
179
+ // Convert tool schemas to Anthropic format
180
+ const anthropicTools: Anthropic.Tool[] = this.tools.map(tool => ({
181
+ name: tool.name,
182
+ description: tool.description,
183
+ input_schema: tool.input_schema as unknown as Anthropic.Tool.InputSchema,
184
+ }));
185
+
186
+ // Execute with tool loop
187
+ let iterations = 0;
188
+ let finalOutput = '';
189
+ let finalSummary = '';
190
+
191
+ while (iterations < (this.config.maxToolIterations || DEFAULT_MAX_TOOL_ITERATIONS)) {
192
+ iterations++;
193
+
194
+ // Call Claude
195
+ const response = await this.anthropic.messages.create({
196
+ model: this.config.model || DEFAULT_MODEL,
197
+ max_tokens: 4096,
198
+ system: systemPrompt,
199
+ tools: anthropicTools.length > 0 ? anthropicTools : undefined,
200
+ messages,
201
+ });
202
+
203
+ // Check stop reason
204
+ if (response.stop_reason === 'end_turn') {
205
+ // Claude is done, extract final text
206
+ const textBlocks = response.content.filter(
207
+ (block): block is Anthropic.TextBlock => block.type === 'text'
208
+ );
209
+ finalOutput = textBlocks.map(b => b.text).join('\n');
210
+ break;
211
+ }
212
+
213
+ if (response.stop_reason === 'tool_use') {
214
+ // Process tool calls
215
+ const toolUseBlocks = response.content.filter(
216
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
217
+ );
218
+
219
+ // Add assistant message with tool use
220
+ messages.push({ role: 'assistant', content: response.content });
221
+
222
+ // Process each tool call
223
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
224
+
225
+ for (const toolUse of toolUseBlocks) {
226
+ console.log(` 🔧 Tool call: ${toolUse.name}`);
227
+
228
+ // Execute the tool
229
+ const result = await executeTool(
230
+ toolUse.name,
231
+ toolUse.input as Record<string, unknown>,
232
+ this.apiClient
233
+ );
234
+
235
+ // Track tool calls
236
+ toolCalls.push({
237
+ name: toolUse.name,
238
+ input: toolUse.input as Record<string, unknown>,
239
+ output: result.data || result.error,
240
+ });
241
+
242
+ // Format result for Claude
243
+ const resultContent = result.success
244
+ ? JSON.stringify(result.data, null, 2)
245
+ : `Error: ${result.error}`;
246
+
247
+ toolResults.push({
248
+ type: 'tool_result',
249
+ tool_use_id: toolUse.id,
250
+ content: resultContent,
251
+ is_error: !result.success,
252
+ });
253
+
254
+ if (result.success) {
255
+ console.log(` ✅ Success`);
256
+ } else {
257
+ console.log(` ❌ Error: ${result.error}`);
258
+ }
259
+ }
260
+
261
+ // Add tool results to messages
262
+ messages.push({ role: 'user', content: toolResults });
263
+
264
+ continue;
265
+ }
266
+
267
+ // Unexpected stop reason
268
+ console.warn(` ⚠️ Unexpected stop reason: ${response.stop_reason}`);
269
+ break;
270
+ }
271
+
272
+ if (iterations >= (this.config.maxToolIterations || DEFAULT_MAX_TOOL_ITERATIONS)) {
273
+ console.warn(' ⚠️ Max tool iterations reached');
274
+ }
275
+
276
+ // Extract summary
277
+ const { content, summary } = this.parseResponse(finalOutput, task.heading);
278
+ finalSummary = summary;
279
+
280
+ return {
281
+ success: true,
282
+ output: content,
283
+ summary: finalSummary,
284
+ promptUsed: prompt,
285
+ toolCalls,
286
+ };
287
+ } catch (error) {
288
+ const errorMessage = error instanceof Error ? error.message : String(error);
289
+
290
+ return {
291
+ success: false,
292
+ output: '',
293
+ summary: `Failed: ${errorMessage}`,
294
+ error: errorMessage,
295
+ toolCalls,
296
+ };
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Build the task prompt (fallback when full context unavailable)
302
+ */
303
+ private buildTaskPrompt(task: TaskContext): string {
304
+ const parts: string[] = [];
305
+
306
+ // Task header
307
+ parts.push(`# Task: ${task.heading}`);
308
+ parts.push('');
309
+
310
+ // Artifact context
311
+ if (task.artifactTitle) {
312
+ parts.push(`**Artifact:** ${task.artifactTitle}`);
313
+ }
314
+ if (task.artifactId) {
315
+ parts.push(`**Artifact ID:** ${task.artifactId}`);
316
+ }
317
+
318
+ // Priority
319
+ if (task.priority) {
320
+ const priorityLabels = ['High', 'Medium', 'Low'];
321
+ parts.push(`**Priority:** ${priorityLabels[task.priority - 1] || 'Medium'}`);
322
+ }
323
+ parts.push('');
324
+
325
+ // Task content
326
+ parts.push('## Description');
327
+ parts.push(task.content || 'No additional description provided.');
328
+ parts.push('');
329
+
330
+ // Additional context
331
+ if (task.context && Object.keys(task.context).length > 0) {
332
+ parts.push('## Additional Context');
333
+ parts.push('```json');
334
+ parts.push(JSON.stringify(task.context, null, 2));
335
+ parts.push('```');
336
+ parts.push('');
337
+ }
338
+
339
+ // Instructions
340
+ parts.push('## Instructions');
341
+ parts.push('Use the available tools to complete this task.');
342
+ parts.push('Create sections, artifacts, or agents as needed.');
343
+ parts.push('Provide a summary when done.');
344
+
345
+ return parts.join('\n');
346
+ }
347
+
348
+ /**
349
+ * Parse the response to extract output and summary
350
+ */
351
+ private parseResponse(
352
+ fullOutput: string,
353
+ taskHeading: string
354
+ ): { content: string; summary: string } {
355
+ // Try to find SUMMARY: line
356
+ const summaryMatch = fullOutput.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
357
+
358
+ if (summaryMatch) {
359
+ const summary = summaryMatch[1].trim();
360
+ const content = fullOutput.replace(/SUMMARY:\s*.+?(?:\n|$)/i, '').trim();
361
+ return { content, summary };
362
+ }
363
+
364
+ // No explicit summary, generate one from first line
365
+ const lines = fullOutput.split('\n').filter((l) => l.trim());
366
+ const firstLine = lines[0] || '';
367
+
368
+ // Take first 100 chars as summary
369
+ const summary = firstLine.length > 100
370
+ ? `${firstLine.substring(0, 97)}...`
371
+ : firstLine || `Completed: ${taskHeading}`;
372
+
373
+ return { content: fullOutput, summary };
374
+ }
375
+
376
+ /**
377
+ * Get available tools for this executor
378
+ */
379
+ getAvailableTools(): ToolSchema[] {
380
+ return this.tools;
381
+ }
382
+
383
+ /**
384
+ * Update permissions (recalculates available tools)
385
+ */
386
+ setPermissions(permissions: string[]): void {
387
+ this.config.permissions = permissions;
388
+ this.tools = getToolsForPermissions(permissions);
389
+ console.log(` 🔧 Updated to ${this.tools.length} tools based on new permissions`);
390
+ }
391
+ }
392
+
393
+ // ============================================================================
394
+ // Factory Function
395
+ // ============================================================================
396
+
397
+ /**
398
+ * Create a Claude executor with tools support
399
+ */
400
+ export function createExecutorWithTools(config: ExecutorConfig): ClaudeExecutorWithTools {
401
+ return new ClaudeExecutorWithTools(config);
402
+ }
package/src/index.ts CHANGED
@@ -101,3 +101,37 @@ export {
101
101
  createContextFetcher,
102
102
  buildPromptWithContext,
103
103
  } from './context';
104
+
105
+ // Executor with Tools - Types
106
+ export type {
107
+ ExecutorConfig as ExecutorWithToolsConfig,
108
+ } from './executor-with-tools';
109
+
110
+ // Executor with Tools - Classes
111
+ export {
112
+ ClaudeExecutorWithTools,
113
+ createExecutorWithTools,
114
+ } from './executor-with-tools';
115
+
116
+ // Tools Registry
117
+ export {
118
+ getAllToolSchemas,
119
+ getToolSchema,
120
+ getToolsForPermissions,
121
+ isToolAllowed,
122
+ getRequiredPermission,
123
+ permissionToTools,
124
+ executeTool,
125
+ handlers as toolHandlers,
126
+ createApiClient,
127
+ } from './tools';
128
+
129
+ // Tools Types
130
+ export type {
131
+ ToolSchema,
132
+ ToolResult,
133
+ ToolHandler,
134
+ ApiClient,
135
+ ToolCall,
136
+ ToolCallResult,
137
+ } from './tools';
package/src/listener.ts CHANGED
@@ -224,12 +224,27 @@ export class ArtyfactsListener {
224
224
  private handleMessage(event: MessageEvent, eventType?: string): void {
225
225
  try {
226
226
  const data = JSON.parse(event.data);
227
+
228
+ // Normalize snake_case to camelCase for task data
229
+ const rawData = data.data || data;
230
+ const normalizedData = rawData.task_id ? {
231
+ taskId: rawData.task_id,
232
+ sectionId: rawData.section_id,
233
+ artifactId: rawData.artifact_id,
234
+ artifactTitle: rawData.artifact_title,
235
+ heading: rawData.heading,
236
+ content: rawData.content,
237
+ assignedTo: rawData.assigned_to,
238
+ assignedAt: rawData.assigned_at,
239
+ priority: rawData.priority,
240
+ ...rawData, // Keep original fields too
241
+ } : rawData;
227
242
 
228
243
  // Normalize event structure
229
244
  const artyfactsEvent: ArtyfactsEvent = {
230
245
  type: eventType || data.type || 'unknown',
231
246
  timestamp: data.timestamp || new Date().toISOString(),
232
- data: data.data || data,
247
+ data: normalizedData,
233
248
  };
234
249
 
235
250
  // Route to type-specific callbacks
@@ -0,0 +1,68 @@
1
+ /**
2
+ * API Client for Artyfacts Tools
3
+ *
4
+ * Simple HTTP client that wraps the Artyfacts API.
5
+ */
6
+
7
+ import { ApiClient } from './types';
8
+
9
+ export interface ApiClientConfig {
10
+ baseUrl: string;
11
+ apiKey: string;
12
+ agentId?: string;
13
+ }
14
+
15
+ /**
16
+ * Create an API client for Artyfacts
17
+ */
18
+ export function createApiClient(config: ApiClientConfig): ApiClient {
19
+ const headers: Record<string, string> = {
20
+ 'Authorization': `Bearer ${config.apiKey}`,
21
+ 'Content-Type': 'application/json',
22
+ };
23
+
24
+ if (config.agentId) {
25
+ headers['X-Agent-ID'] = config.agentId;
26
+ }
27
+
28
+ const baseUrl = config.baseUrl.replace(/\/$/, '');
29
+
30
+ async function request<T>(
31
+ method: string,
32
+ path: string,
33
+ body?: unknown
34
+ ): Promise<T> {
35
+ const url = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
36
+
37
+ const options: RequestInit = {
38
+ method,
39
+ headers,
40
+ };
41
+
42
+ if (body && method !== 'GET') {
43
+ options.body = JSON.stringify(body);
44
+ }
45
+
46
+ const response = await fetch(url, options);
47
+
48
+ if (!response.ok) {
49
+ const errorData = await response.json().catch(() => ({ error: 'Request failed' })) as { error?: string };
50
+ throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
51
+ }
52
+
53
+ // Handle empty responses
54
+ const text = await response.text();
55
+ if (!text) {
56
+ return {} as T;
57
+ }
58
+
59
+ return JSON.parse(text);
60
+ }
61
+
62
+ return {
63
+ get: <T>(path: string) => request<T>('GET', path),
64
+ post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
65
+ patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
66
+ delete: <T>(path: string) => request<T>('DELETE', path),
67
+ };
68
+ }