@artyfacts/claude 1.1.1 → 1.2.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.js CHANGED
@@ -32,7 +32,10 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ArtyfactsListener: () => ArtyfactsListener,
34
34
  ClaudeExecutor: () => ClaudeExecutor,
35
+ ContextFetcher: () => ContextFetcher,
36
+ buildPromptWithContext: () => buildPromptWithContext,
35
37
  clearCredentials: () => clearCredentials,
38
+ createContextFetcher: () => createContextFetcher,
36
39
  createExecutor: () => createExecutor,
37
40
  createListener: () => createListener,
38
41
  getCredentials: () => getCredentials,
@@ -233,6 +236,153 @@ async function getCredentials(options) {
233
236
 
234
237
  // src/executor.ts
235
238
  var import_child_process = require("child_process");
239
+
240
+ // src/context.ts
241
+ var ContextFetcher = class {
242
+ config;
243
+ constructor(config) {
244
+ this.config = config;
245
+ }
246
+ /**
247
+ * Fetch full context for a task
248
+ */
249
+ async fetchTaskContext(taskId) {
250
+ const response = await fetch(
251
+ `${this.config.baseUrl}/tasks/${taskId}/context`,
252
+ {
253
+ headers: {
254
+ "Authorization": `Bearer ${this.config.apiKey}`,
255
+ "Accept": "application/json"
256
+ }
257
+ }
258
+ );
259
+ if (!response.ok) {
260
+ const errorText = await response.text().catch(() => "Unknown error");
261
+ throw new Error(`Failed to fetch task context: ${response.status} - ${errorText}`);
262
+ }
263
+ const data = await response.json();
264
+ return data;
265
+ }
266
+ };
267
+ function buildPromptWithContext(context) {
268
+ const parts = [];
269
+ parts.push(`You are an AI agent working within the Artyfacts task management system.
270
+
271
+ Your job is to complete the assigned task. You have full context about the organization, project, and related work.
272
+
273
+ Guidelines:
274
+ - Be thorough but concise
275
+ - If the task requires code, provide working code
276
+ - If the task requires analysis, provide structured findings
277
+ - If the task requires a decision, explain your reasoning
278
+ - If you cannot complete the task, explain why
279
+
280
+ Format your response as follows:
281
+ 1. First, provide your main output (the task deliverable)
282
+ 2. End with a brief summary line starting with "SUMMARY:"`);
283
+ parts.push("");
284
+ parts.push("---");
285
+ parts.push("");
286
+ parts.push("## Organization Context");
287
+ parts.push(`**${context.organization.name}**`);
288
+ if (context.organization.context) {
289
+ parts.push("");
290
+ parts.push(formatOrgContext(context.organization.context));
291
+ }
292
+ parts.push("");
293
+ if (context.project) {
294
+ parts.push(`## Project: ${context.project.name}`);
295
+ if (context.project.description) {
296
+ parts.push(context.project.description);
297
+ }
298
+ parts.push("");
299
+ }
300
+ parts.push(`## Artifact: ${context.artifact.title}`);
301
+ if (context.artifact.summary) {
302
+ parts.push(context.artifact.summary);
303
+ }
304
+ if (context.artifact.description) {
305
+ parts.push("");
306
+ parts.push(context.artifact.description);
307
+ }
308
+ parts.push("");
309
+ const relatedSections = context.artifact.sections.filter(
310
+ (s) => s.id !== context.task.id
311
+ );
312
+ if (relatedSections.length > 0) {
313
+ parts.push("### Related Sections:");
314
+ for (const section of relatedSections) {
315
+ const preview = section.content ? section.content.substring(0, 200) + (section.content.length > 200 ? "..." : "") : "No content";
316
+ const statusBadge = section.task_status ? ` [${section.task_status}]` : "";
317
+ parts.push(`- **${section.heading}**${statusBadge}: ${preview}`);
318
+ }
319
+ parts.push("");
320
+ }
321
+ parts.push("---");
322
+ parts.push("");
323
+ parts.push(`## Your Task: ${context.task.heading}`);
324
+ if (context.task.priority) {
325
+ const priorityLabels = ["\u{1F534} High", "\u{1F7E1} Medium", "\u{1F7E2} Low"];
326
+ parts.push(`**Priority:** ${priorityLabels[context.task.priority - 1] || "Medium"}`);
327
+ }
328
+ parts.push("");
329
+ parts.push("### Description");
330
+ parts.push(context.task.content || "No additional description provided.");
331
+ parts.push("");
332
+ if (context.task.expected_output) {
333
+ parts.push("### Expected Output");
334
+ if (context.task.expected_output.format) {
335
+ parts.push(`**Format:** ${context.task.expected_output.format}`);
336
+ }
337
+ if (context.task.expected_output.requirements && context.task.expected_output.requirements.length > 0) {
338
+ parts.push("**Requirements:**");
339
+ for (const req of context.task.expected_output.requirements) {
340
+ parts.push(`- ${req}`);
341
+ }
342
+ }
343
+ parts.push("");
344
+ }
345
+ parts.push("---");
346
+ parts.push("");
347
+ parts.push("Complete this task and provide your output below.");
348
+ return parts.join("\n");
349
+ }
350
+ function formatOrgContext(context) {
351
+ const trimmed = context.trim();
352
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
353
+ try {
354
+ const parsed = JSON.parse(trimmed);
355
+ return formatContextObject(parsed);
356
+ } catch {
357
+ return context;
358
+ }
359
+ }
360
+ return context;
361
+ }
362
+ function formatContextObject(obj, indent = "") {
363
+ if (typeof obj !== "object" || obj === null) {
364
+ return String(obj);
365
+ }
366
+ if (Array.isArray(obj)) {
367
+ return obj.map((item) => `${indent}- ${formatContextObject(item, indent + " ")}`).join("\n");
368
+ }
369
+ const lines = [];
370
+ for (const [key, value] of Object.entries(obj)) {
371
+ const label = key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
372
+ if (typeof value === "object" && value !== null) {
373
+ lines.push(`${indent}**${label}:**`);
374
+ lines.push(formatContextObject(value, indent + " "));
375
+ } else {
376
+ lines.push(`${indent}- **${label}:** ${value}`);
377
+ }
378
+ }
379
+ return lines.join("\n");
380
+ }
381
+ function createContextFetcher(config) {
382
+ return new ContextFetcher(config);
383
+ }
384
+
385
+ // src/executor.ts
236
386
  var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
237
387
  var DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
238
388
 
@@ -253,25 +403,51 @@ Format your response as follows:
253
403
  2. End with a brief summary line starting with "SUMMARY:"`;
254
404
  var ClaudeExecutor = class {
255
405
  config;
406
+ contextFetcher = null;
256
407
  constructor(config = {}) {
257
408
  this.config = {
258
409
  ...config,
259
410
  timeout: config.timeout || DEFAULT_TIMEOUT,
260
411
  claudePath: config.claudePath || "claude"
261
412
  };
413
+ if (config.baseUrl && config.apiKey) {
414
+ this.contextFetcher = createContextFetcher({
415
+ baseUrl: config.baseUrl,
416
+ apiKey: config.apiKey
417
+ });
418
+ }
262
419
  }
263
420
  /**
264
421
  * Execute a task using Claude Code CLI
422
+ *
423
+ * If full context is available (baseUrl + apiKey configured), fetches
424
+ * organization, project, artifact, and related sections for a rich prompt.
265
425
  */
266
426
  async execute(task) {
267
427
  try {
268
- const prompt = this.buildTaskPrompt(task);
428
+ let prompt;
429
+ let fullContext = null;
430
+ const useFullContext = this.config.useFullContext !== false && this.contextFetcher;
431
+ if (useFullContext) {
432
+ try {
433
+ fullContext = await this.contextFetcher.fetchTaskContext(task.taskId);
434
+ prompt = buildPromptWithContext(fullContext);
435
+ console.log(" \u{1F4DA} Using full context (org, project, artifact, related sections)");
436
+ } catch (contextError) {
437
+ console.warn(" \u26A0\uFE0F Could not fetch full context, using minimal prompt");
438
+ console.warn(` ${contextError instanceof Error ? contextError.message : contextError}`);
439
+ prompt = this.buildTaskPrompt(task);
440
+ }
441
+ } else {
442
+ prompt = this.buildTaskPrompt(task);
443
+ }
269
444
  const output = await this.runClaude(prompt);
270
445
  const { content, summary } = this.parseResponse(output, task.heading);
271
446
  return {
272
447
  success: true,
273
448
  output: content,
274
- summary
449
+ summary,
450
+ promptUsed: prompt
275
451
  };
276
452
  } catch (error) {
277
453
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -601,7 +777,10 @@ function createListener(config) {
601
777
  0 && (module.exports = {
602
778
  ArtyfactsListener,
603
779
  ClaudeExecutor,
780
+ ContextFetcher,
781
+ buildPromptWithContext,
604
782
  clearCredentials,
783
+ createContextFetcher,
605
784
  createExecutor,
606
785
  createListener,
607
786
  getCredentials,
package/dist/index.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  ArtyfactsListener,
3
3
  ClaudeExecutor,
4
+ ContextFetcher,
5
+ buildPromptWithContext,
4
6
  clearCredentials,
7
+ createContextFetcher,
5
8
  createExecutor,
6
9
  createListener,
7
10
  getCredentials,
@@ -9,11 +12,14 @@ import {
9
12
  promptForApiKey,
10
13
  runDeviceAuth,
11
14
  saveCredentials
12
- } from "./chunk-4RRGLYN6.mjs";
15
+ } from "./chunk-EROV5RIA.mjs";
13
16
  export {
14
17
  ArtyfactsListener,
15
18
  ClaudeExecutor,
19
+ ContextFetcher,
20
+ buildPromptWithContext,
16
21
  clearCredentials,
22
+ createContextFetcher,
17
23
  createExecutor,
18
24
  createListener,
19
25
  getCredentials,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artyfacts/claude",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",
package/src/cli.ts CHANGED
@@ -126,6 +126,130 @@ program
126
126
  }
127
127
  });
128
128
 
129
+ // ============================================================================
130
+ // Task Queue Polling
131
+ // ============================================================================
132
+
133
+ /**
134
+ * Check for claimable tasks and process them
135
+ */
136
+ async function checkAndClaimTasks(
137
+ baseUrl: string,
138
+ apiKey: string,
139
+ agentId: string,
140
+ activeTasks: Set<string>,
141
+ executor: ClaudeExecutor,
142
+ dryRun: boolean
143
+ ): Promise<void> {
144
+ try {
145
+ // Fetch claimable tasks from queue
146
+ const response = await fetch(`${baseUrl}/tasks/queue?limit=5`, {
147
+ headers: {
148
+ 'Authorization': `Bearer ${apiKey}`,
149
+ },
150
+ });
151
+
152
+ if (!response.ok) {
153
+ console.log('⚠️ Could not fetch task queue');
154
+ return;
155
+ }
156
+
157
+ const data = await response.json() as { tasks?: Array<{
158
+ id: string;
159
+ section_id: string;
160
+ heading: string;
161
+ content: string;
162
+ artifact_id: string;
163
+ artifact_title?: string;
164
+ priority?: number;
165
+ }> };
166
+
167
+ const tasks = data.tasks || [];
168
+
169
+ if (tasks.length === 0) {
170
+ console.log('📭 No claimable tasks in queue');
171
+ return;
172
+ }
173
+
174
+ console.log(`📬 Found ${tasks.length} claimable task(s)`);
175
+
176
+ // Process first available task
177
+ for (const task of tasks) {
178
+ if (activeTasks.has(task.section_id)) {
179
+ continue; // Already processing
180
+ }
181
+
182
+ // Try to claim the task
183
+ console.log(`\n[Claiming] ${task.heading}`);
184
+
185
+ const claimResponse = await fetch(`${baseUrl}/tasks/${task.section_id}/claim`, {
186
+ method: 'POST',
187
+ headers: {
188
+ 'Authorization': `Bearer ${apiKey}`,
189
+ 'Content-Type': 'application/json',
190
+ },
191
+ });
192
+
193
+ if (!claimResponse.ok) {
194
+ const error = await claimResponse.json().catch(() => ({})) as { error?: string };
195
+ console.log(` ⚠️ Could not claim: ${error.error || 'Unknown error'}`);
196
+ continue;
197
+ }
198
+
199
+ // Successfully claimed - now execute
200
+ activeTasks.add(task.section_id);
201
+ console.log(' ✓ Claimed!');
202
+
203
+ if (dryRun) {
204
+ console.log(' 📋 Dry run - not executing');
205
+ activeTasks.delete(task.section_id);
206
+ continue;
207
+ }
208
+
209
+ console.log(' → Executing with Claude...');
210
+
211
+ try {
212
+ const result = await executor.execute({
213
+ taskId: task.section_id,
214
+ heading: task.heading,
215
+ content: task.content,
216
+ artifactId: task.artifact_id,
217
+ artifactTitle: task.artifact_title,
218
+ priority: task.priority,
219
+ });
220
+
221
+ if (result.success) {
222
+ await completeTask({
223
+ baseUrl,
224
+ apiKey,
225
+ taskId: task.section_id,
226
+ output: result.output,
227
+ summary: result.summary,
228
+ });
229
+ console.log(` → ✅ Completed! ${result.summary}`);
230
+ } else {
231
+ console.log(` → ❌ Failed: ${result.error}`);
232
+ await blockTask({
233
+ baseUrl,
234
+ apiKey,
235
+ taskId: task.section_id,
236
+ reason: result.error || 'Execution failed',
237
+ });
238
+ }
239
+ } catch (error) {
240
+ console.error(` → ❌ Error:`, error instanceof Error ? error.message : error);
241
+ } finally {
242
+ activeTasks.delete(task.section_id);
243
+ }
244
+
245
+ // Only process one task at a time for now
246
+ break;
247
+ }
248
+ } catch (error) {
249
+ console.error('Error checking task queue:', error instanceof Error ? error.message : error);
250
+ }
251
+ }
252
+
129
253
  // ============================================================================
130
254
  // Main Agent Loop
131
255
  // ============================================================================
@@ -145,10 +269,14 @@ async function runAgent(options: {
145
269
  process.exit(1);
146
270
  }
147
271
 
148
- // Create executor (uses Claude Code CLI)
272
+ // Create executor (uses Claude Code CLI + Artyfacts context API)
149
273
  let executor: ClaudeExecutor | null = null;
150
274
  if (!options.dryRun) {
151
- executor = createExecutor();
275
+ executor = createExecutor({
276
+ // Pass API credentials so executor can fetch full context
277
+ baseUrl: options.baseUrl,
278
+ apiKey: credentials.apiKey,
279
+ });
152
280
 
153
281
  // Check if Claude Code is installed
154
282
  const installed = await executor.isInstalled();
@@ -168,6 +296,7 @@ async function runAgent(options: {
168
296
  process.exit(1);
169
297
  }
170
298
  console.log('✅ Claude Code connected');
299
+ console.log('📚 Context fetching enabled (will fetch org/project/artifact context)');
171
300
  }
172
301
 
173
302
  // Create listener
@@ -183,6 +312,11 @@ async function runAgent(options: {
183
312
  case 'connected':
184
313
  console.log(`✅ Connected as ${credentials.agentId}`);
185
314
  console.log('👂 Listening for tasks...\n');
315
+
316
+ // Check for existing claimable tasks on connect
317
+ if (!options.dryRun) {
318
+ checkAndClaimTasks(options.baseUrl, credentials.apiKey, credentials.agentId, activeTasks, executor!, options.dryRun);
319
+ }
186
320
  break;
187
321
  case 'reconnecting':
188
322
  console.log('🔄 Reconnecting...');
package/src/context.ts ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Context fetcher for Artyfacts tasks
3
+ *
4
+ * Fetches full context including organization, project, artifact, and related sections.
5
+ */
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ export interface OrganizationContext {
12
+ id: string;
13
+ name: string;
14
+ context?: string;
15
+ }
16
+
17
+ export interface ProjectContext {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ }
22
+
23
+ export interface SectionContext {
24
+ id: string;
25
+ heading: string;
26
+ content?: string;
27
+ section_type?: string;
28
+ task_status?: string;
29
+ }
30
+
31
+ export interface ArtifactContext {
32
+ id: string;
33
+ title: string;
34
+ summary?: string;
35
+ description?: string;
36
+ artifact_type?: string;
37
+ sections: SectionContext[];
38
+ }
39
+
40
+ export interface TaskFullContext {
41
+ task: {
42
+ id: string;
43
+ heading: string;
44
+ content: string;
45
+ expected_output?: {
46
+ format?: string;
47
+ requirements?: string[];
48
+ };
49
+ priority?: number;
50
+ };
51
+ artifact: ArtifactContext;
52
+ project?: ProjectContext;
53
+ organization: OrganizationContext;
54
+ }
55
+
56
+ export interface ContextFetcherConfig {
57
+ baseUrl: string;
58
+ apiKey: string;
59
+ }
60
+
61
+ // ============================================================================
62
+ // Context Fetcher
63
+ // ============================================================================
64
+
65
+ export class ContextFetcher {
66
+ private config: ContextFetcherConfig;
67
+
68
+ constructor(config: ContextFetcherConfig) {
69
+ this.config = config;
70
+ }
71
+
72
+ /**
73
+ * Fetch full context for a task
74
+ */
75
+ async fetchTaskContext(taskId: string): Promise<TaskFullContext> {
76
+ const response = await fetch(
77
+ `${this.config.baseUrl}/tasks/${taskId}/context`,
78
+ {
79
+ headers: {
80
+ 'Authorization': `Bearer ${this.config.apiKey}`,
81
+ 'Accept': 'application/json',
82
+ },
83
+ }
84
+ );
85
+
86
+ if (!response.ok) {
87
+ const errorText = await response.text().catch(() => 'Unknown error');
88
+ throw new Error(`Failed to fetch task context: ${response.status} - ${errorText}`);
89
+ }
90
+
91
+ const data = await response.json();
92
+ return data as TaskFullContext;
93
+ }
94
+ }
95
+
96
+ // ============================================================================
97
+ // Prompt Builder
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Build a rich prompt with full context
102
+ */
103
+ export function buildPromptWithContext(context: TaskFullContext): string {
104
+ const parts: string[] = [];
105
+
106
+ // System instruction
107
+ parts.push(`You are an AI agent working within the Artyfacts task management system.
108
+
109
+ Your job is to complete the assigned task. You have full context about the organization, project, and related work.
110
+
111
+ Guidelines:
112
+ - Be thorough but concise
113
+ - If the task requires code, provide working code
114
+ - If the task requires analysis, provide structured findings
115
+ - If the task requires a decision, explain your reasoning
116
+ - If you cannot complete the task, explain why
117
+
118
+ Format your response as follows:
119
+ 1. First, provide your main output (the task deliverable)
120
+ 2. End with a brief summary line starting with "SUMMARY:"`);
121
+
122
+ parts.push('');
123
+ parts.push('---');
124
+ parts.push('');
125
+
126
+ // Organization context
127
+ parts.push('## Organization Context');
128
+ parts.push(`**${context.organization.name}**`);
129
+ if (context.organization.context) {
130
+ parts.push('');
131
+ parts.push(formatOrgContext(context.organization.context));
132
+ }
133
+ parts.push('');
134
+
135
+ // Project context (if available)
136
+ if (context.project) {
137
+ parts.push(`## Project: ${context.project.name}`);
138
+ if (context.project.description) {
139
+ parts.push(context.project.description);
140
+ }
141
+ parts.push('');
142
+ }
143
+
144
+ // Artifact context
145
+ parts.push(`## Artifact: ${context.artifact.title}`);
146
+ if (context.artifact.summary) {
147
+ parts.push(context.artifact.summary);
148
+ }
149
+ if (context.artifact.description) {
150
+ parts.push('');
151
+ parts.push(context.artifact.description);
152
+ }
153
+ parts.push('');
154
+
155
+ // Related sections (non-task sections for context, or other tasks for awareness)
156
+ const relatedSections = context.artifact.sections.filter(
157
+ s => s.id !== context.task.id
158
+ );
159
+
160
+ if (relatedSections.length > 0) {
161
+ parts.push('### Related Sections:');
162
+ for (const section of relatedSections) {
163
+ const preview = section.content
164
+ ? section.content.substring(0, 200) + (section.content.length > 200 ? '...' : '')
165
+ : 'No content';
166
+ const statusBadge = section.task_status ? ` [${section.task_status}]` : '';
167
+ parts.push(`- **${section.heading}**${statusBadge}: ${preview}`);
168
+ }
169
+ parts.push('');
170
+ }
171
+
172
+ // The task itself
173
+ parts.push('---');
174
+ parts.push('');
175
+ parts.push(`## Your Task: ${context.task.heading}`);
176
+
177
+ if (context.task.priority) {
178
+ const priorityLabels = ['🔴 High', '🟡 Medium', '🟢 Low'];
179
+ parts.push(`**Priority:** ${priorityLabels[context.task.priority - 1] || 'Medium'}`);
180
+ }
181
+ parts.push('');
182
+
183
+ parts.push('### Description');
184
+ parts.push(context.task.content || 'No additional description provided.');
185
+ parts.push('');
186
+
187
+ // Expected output
188
+ if (context.task.expected_output) {
189
+ parts.push('### Expected Output');
190
+ if (context.task.expected_output.format) {
191
+ parts.push(`**Format:** ${context.task.expected_output.format}`);
192
+ }
193
+ if (context.task.expected_output.requirements && context.task.expected_output.requirements.length > 0) {
194
+ parts.push('**Requirements:**');
195
+ for (const req of context.task.expected_output.requirements) {
196
+ parts.push(`- ${req}`);
197
+ }
198
+ }
199
+ parts.push('');
200
+ }
201
+
202
+ // Final instruction
203
+ parts.push('---');
204
+ parts.push('');
205
+ parts.push('Complete this task and provide your output below.');
206
+
207
+ return parts.join('\n');
208
+ }
209
+
210
+ /**
211
+ * Format organization context (may contain markdown or JSON)
212
+ */
213
+ function formatOrgContext(context: string): string {
214
+ // If it looks like JSON, try to parse and format it nicely
215
+ const trimmed = context.trim();
216
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
217
+ try {
218
+ const parsed = JSON.parse(trimmed);
219
+ // Convert to readable bullet points
220
+ return formatContextObject(parsed);
221
+ } catch {
222
+ // Not valid JSON, return as-is
223
+ return context;
224
+ }
225
+ }
226
+ return context;
227
+ }
228
+
229
+ /**
230
+ * Format a context object into readable bullet points
231
+ */
232
+ function formatContextObject(obj: unknown, indent = ''): string {
233
+ if (typeof obj !== 'object' || obj === null) {
234
+ return String(obj);
235
+ }
236
+
237
+ if (Array.isArray(obj)) {
238
+ return obj.map(item => `${indent}- ${formatContextObject(item, indent + ' ')}`).join('\n');
239
+ }
240
+
241
+ const lines: string[] = [];
242
+ for (const [key, value] of Object.entries(obj)) {
243
+ const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
244
+ if (typeof value === 'object' && value !== null) {
245
+ lines.push(`${indent}**${label}:**`);
246
+ lines.push(formatContextObject(value, indent + ' '));
247
+ } else {
248
+ lines.push(`${indent}- **${label}:** ${value}`);
249
+ }
250
+ }
251
+ return lines.join('\n');
252
+ }
253
+
254
+ // ============================================================================
255
+ // Factory
256
+ // ============================================================================
257
+
258
+ export function createContextFetcher(config: ContextFetcherConfig): ContextFetcher {
259
+ return new ContextFetcher(config);
260
+ }