@artyfacts/claude 1.2.6 → 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/dist/index.d.mts +225 -7
- package/dist/index.d.ts +225 -7
- package/dist/index.js +5185 -2
- package/dist/index.mjs +5173 -1
- package/package.json +1 -1
- package/src/executor-with-tools.ts +402 -0
- package/src/index.ts +34 -0
- package/src/tools/api-client.ts +68 -0
- package/src/tools/handlers.ts +460 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/registry.ts +780 -0
- package/src/tools/types.ts +82 -0
package/package.json
CHANGED
|
@@ -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';
|
|
@@ -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
|
+
}
|