@artyfacts/openclaw 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,1119 @@
1
+ // src/auth.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import * as readline from "readline";
6
+ var CREDENTIALS_DIR = path.join(os.homedir(), ".artyfacts");
7
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "openclaw-credentials.json");
8
+ var DEFAULT_BASE_URL = "https://artyfacts.dev/api/v1";
9
+ var CLIENT_ID = "artyfacts-openclaw";
10
+ function loadCredentials() {
11
+ try {
12
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
13
+ return null;
14
+ }
15
+ const data = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
16
+ const credentials = JSON.parse(data);
17
+ if (credentials.expiresAt) {
18
+ const expiresAt = new Date(credentials.expiresAt);
19
+ if (expiresAt < /* @__PURE__ */ new Date()) {
20
+ console.log("\u26A0\uFE0F Credentials have expired");
21
+ return null;
22
+ }
23
+ }
24
+ return credentials;
25
+ } catch (error) {
26
+ console.error("Failed to load credentials:", error);
27
+ return null;
28
+ }
29
+ }
30
+ function saveCredentials(credentials) {
31
+ try {
32
+ if (!fs.existsSync(CREDENTIALS_DIR)) {
33
+ fs.mkdirSync(CREDENTIALS_DIR, { mode: 448, recursive: true });
34
+ }
35
+ fs.writeFileSync(
36
+ CREDENTIALS_FILE,
37
+ JSON.stringify(credentials, null, 2),
38
+ { mode: 384 }
39
+ );
40
+ } catch (error) {
41
+ throw new Error(`Failed to save credentials: ${error}`);
42
+ }
43
+ }
44
+ function clearCredentials() {
45
+ try {
46
+ if (fs.existsSync(CREDENTIALS_FILE)) {
47
+ fs.unlinkSync(CREDENTIALS_FILE);
48
+ }
49
+ } catch (error) {
50
+ console.error("Failed to clear credentials:", error);
51
+ }
52
+ }
53
+ async function runDeviceAuth(baseUrl = DEFAULT_BASE_URL) {
54
+ console.log("\u{1F510} Starting device authentication...\n");
55
+ const deviceAuth = await requestDeviceCode(baseUrl);
56
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
57
+ console.log("\u{1F4CB} To authenticate, visit:");
58
+ console.log(` ${deviceAuth.verificationUri}`);
59
+ console.log("");
60
+ console.log("\u{1F511} Enter this code:");
61
+ console.log(` ${deviceAuth.userCode}`);
62
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
63
+ console.log("\u23F3 Waiting for authentication...\n");
64
+ const credentials = await pollForToken(
65
+ baseUrl,
66
+ deviceAuth.deviceCode,
67
+ deviceAuth.interval,
68
+ deviceAuth.expiresIn
69
+ );
70
+ saveCredentials(credentials);
71
+ console.log("\u2705 Authentication successful!");
72
+ console.log(` Agent ID: ${credentials.agentId}`);
73
+ if (credentials.agentName) {
74
+ console.log(` Agent Name: ${credentials.agentName}`);
75
+ }
76
+ console.log("");
77
+ return credentials;
78
+ }
79
+ async function requestDeviceCode(baseUrl) {
80
+ const response = await fetch(`${baseUrl}/auth/device`, {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": "application/json"
84
+ },
85
+ body: JSON.stringify({
86
+ client_id: CLIENT_ID,
87
+ scope: "agent:execute"
88
+ })
89
+ });
90
+ if (!response.ok) {
91
+ const error = await response.text();
92
+ throw new Error(`Failed to start device auth: ${error}`);
93
+ }
94
+ const data = await response.json();
95
+ return {
96
+ deviceCode: data.deviceCode || data.device_code || "",
97
+ userCode: data.userCode || data.user_code || "",
98
+ verificationUri: data.verificationUri || data.verification_uri || `https://artyfacts.dev/auth/device`,
99
+ expiresIn: data.expiresIn || data.expires_in || 600,
100
+ interval: data.interval || 5
101
+ };
102
+ }
103
+ async function pollForToken(baseUrl, deviceCode, interval, expiresIn) {
104
+ const startTime = Date.now();
105
+ const timeoutMs = expiresIn * 1e3;
106
+ while (true) {
107
+ if (Date.now() - startTime > timeoutMs) {
108
+ throw new Error("Device authentication timed out");
109
+ }
110
+ await sleep(interval * 1e3);
111
+ const response = await fetch(`${baseUrl}/auth/device/token`, {
112
+ method: "POST",
113
+ headers: {
114
+ "Content-Type": "application/json"
115
+ },
116
+ body: JSON.stringify({
117
+ device_code: deviceCode,
118
+ client_id: CLIENT_ID
119
+ })
120
+ });
121
+ if (response.ok) {
122
+ const data = await response.json();
123
+ return {
124
+ apiKey: data.apiKey,
125
+ agentId: data.agentId,
126
+ agentName: data.agentName,
127
+ expiresAt: data.expiresAt
128
+ };
129
+ }
130
+ const errorData = await response.json().catch(() => ({}));
131
+ const errorCode = errorData.error || errorData.code;
132
+ if (errorCode === "authorization_pending") {
133
+ process.stdout.write(".");
134
+ continue;
135
+ }
136
+ if (errorCode === "slow_down") {
137
+ interval = Math.min(interval * 2, 30);
138
+ continue;
139
+ }
140
+ if (errorCode === "expired_token") {
141
+ throw new Error("Device code expired. Please try again.");
142
+ }
143
+ if (errorCode === "access_denied") {
144
+ throw new Error("Authorization was denied.");
145
+ }
146
+ throw new Error(`Authentication failed: ${errorData.message || errorCode || response.statusText}`);
147
+ }
148
+ }
149
+ async function promptForApiKey() {
150
+ const rl = readline.createInterface({
151
+ input: process.stdin,
152
+ output: process.stdout
153
+ });
154
+ const question = (prompt) => {
155
+ return new Promise((resolve) => {
156
+ rl.question(prompt, resolve);
157
+ });
158
+ };
159
+ console.log("\u{1F511} Manual Configuration\n");
160
+ console.log("Enter your Artyfacts credentials:\n");
161
+ const apiKey = await question("API Key: ");
162
+ const agentId = await question("Agent ID: ");
163
+ const agentName = await question("Agent Name (optional): ");
164
+ rl.close();
165
+ if (!apiKey || !agentId) {
166
+ throw new Error("API Key and Agent ID are required");
167
+ }
168
+ const credentials = {
169
+ apiKey: apiKey.trim(),
170
+ agentId: agentId.trim(),
171
+ agentName: agentName.trim() || void 0
172
+ };
173
+ saveCredentials(credentials);
174
+ console.log("\n\u2705 Credentials saved!");
175
+ return credentials;
176
+ }
177
+ function sleep(ms) {
178
+ return new Promise((resolve) => setTimeout(resolve, ms));
179
+ }
180
+ async function getCredentials(options) {
181
+ if (!options?.forceAuth) {
182
+ const existing = loadCredentials();
183
+ if (existing) {
184
+ return existing;
185
+ }
186
+ }
187
+ return runDeviceAuth(options?.baseUrl);
188
+ }
189
+
190
+ // src/context.ts
191
+ var ContextFetcher = class {
192
+ constructor(config) {
193
+ this.config = config;
194
+ }
195
+ /**
196
+ * Fetch full context for a task
197
+ */
198
+ async fetchTaskContext(taskId) {
199
+ const response = await fetch(
200
+ `${this.config.baseUrl}/tasks/${taskId}/context`,
201
+ {
202
+ headers: {
203
+ "Authorization": `Bearer ${this.config.apiKey}`,
204
+ "Accept": "application/json"
205
+ }
206
+ }
207
+ );
208
+ if (!response.ok) {
209
+ const errorText = await response.text().catch(() => "Unknown error");
210
+ throw new Error(`Failed to fetch task context: ${response.status} - ${errorText}`);
211
+ }
212
+ const data = await response.json();
213
+ return data;
214
+ }
215
+ };
216
+ function buildPromptWithContext(context) {
217
+ const parts = [];
218
+ parts.push(`You are an AI agent working within the Artyfacts task management system.
219
+
220
+ Your job is to complete the assigned task. You have full context about the organization, project, and related work.
221
+
222
+ ## Available Tools
223
+
224
+ You have access to Artyfacts MCP tools. USE THEM to complete your task:
225
+
226
+ - **create_artifact** - Create new artifacts (documents, specs, reports)
227
+ - **create_section** - Add sections to artifacts (content, tasks, decisions)
228
+ - **update_section** - Update existing sections
229
+ - **create_agent** - Create new AI agents with specific roles
230
+ - **list_artifacts** - Query existing artifacts
231
+ - **list_sections** - Query sections within an artifact
232
+ - **complete_task** - Mark a task as complete
233
+ - **block_task** - Block a task with a reason
234
+ - **create_blocker** - Create a decision blocker
235
+
236
+ IMPORTANT: When asked to create agents or update artifacts, USE THE TOOLS. Don't just describe what you would do - actually do it with the tools.
237
+
238
+ ## Guidelines
239
+
240
+ - Be thorough but concise
241
+ - USE THE TOOLS to take action, don't just analyze
242
+ - If the task requires creating something, use create_artifact or create_section
243
+ - If the task requires creating agents, use create_agent
244
+ - If you cannot complete the task, explain why
245
+
246
+ Format your response as follows:
247
+ 1. First, use the tools to complete the task
248
+ 2. Then summarize what you did
249
+ 3. End with a brief summary line starting with "SUMMARY:"`);
250
+ parts.push("");
251
+ parts.push("---");
252
+ parts.push("");
253
+ parts.push("## Organization Context");
254
+ parts.push(`**${context.organization.name}**`);
255
+ if (context.organization.context) {
256
+ parts.push("");
257
+ parts.push(formatOrgContext(context.organization.context));
258
+ }
259
+ parts.push("");
260
+ if (context.project) {
261
+ parts.push(`## Project: ${context.project.name}`);
262
+ if (context.project.description) {
263
+ parts.push(context.project.description);
264
+ }
265
+ parts.push("");
266
+ }
267
+ parts.push(`## Artifact: ${context.artifact.title}`);
268
+ if (context.artifact.summary) {
269
+ parts.push(context.artifact.summary);
270
+ }
271
+ if (context.artifact.description) {
272
+ parts.push("");
273
+ parts.push(context.artifact.description);
274
+ }
275
+ parts.push("");
276
+ const relatedSections = context.artifact.sections.filter(
277
+ (s) => s.id !== context.task.id
278
+ );
279
+ if (relatedSections.length > 0) {
280
+ parts.push("### Related Sections:");
281
+ for (const section of relatedSections) {
282
+ const preview = section.content ? section.content.substring(0, 200) + (section.content.length > 200 ? "..." : "") : "No content";
283
+ const statusBadge = section.task_status ? ` [${section.task_status}]` : "";
284
+ parts.push(`- **${section.heading}**${statusBadge}: ${preview}`);
285
+ }
286
+ parts.push("");
287
+ }
288
+ parts.push("---");
289
+ parts.push("");
290
+ parts.push(`## Your Task: ${context.task.heading}`);
291
+ if (context.task.priority) {
292
+ const priorityLabels = ["\u{1F534} High", "\u{1F7E1} Medium", "\u{1F7E2} Low"];
293
+ parts.push(`**Priority:** ${priorityLabels[context.task.priority - 1] || "Medium"}`);
294
+ }
295
+ parts.push("");
296
+ parts.push("### Description");
297
+ parts.push(context.task.content || "No additional description provided.");
298
+ parts.push("");
299
+ if (context.task.expected_output) {
300
+ parts.push("### Expected Output");
301
+ if (context.task.expected_output.format) {
302
+ parts.push(`**Format:** ${context.task.expected_output.format}`);
303
+ }
304
+ if (context.task.expected_output.requirements && context.task.expected_output.requirements.length > 0) {
305
+ parts.push("**Requirements:**");
306
+ for (const req of context.task.expected_output.requirements) {
307
+ parts.push(`- ${req}`);
308
+ }
309
+ }
310
+ parts.push("");
311
+ }
312
+ parts.push("---");
313
+ parts.push("");
314
+ parts.push("Complete this task and provide your output below.");
315
+ return parts.join("\n");
316
+ }
317
+ function formatOrgContext(context) {
318
+ const trimmed = context.trim();
319
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
320
+ try {
321
+ const parsed = JSON.parse(trimmed);
322
+ return formatContextObject(parsed);
323
+ } catch {
324
+ return context;
325
+ }
326
+ }
327
+ return context;
328
+ }
329
+ function formatContextObject(obj, indent = "") {
330
+ if (typeof obj !== "object" || obj === null) {
331
+ return String(obj);
332
+ }
333
+ if (Array.isArray(obj)) {
334
+ return obj.map((item) => `${indent}- ${formatContextObject(item, indent + " ")}`).join("\n");
335
+ }
336
+ const lines = [];
337
+ for (const [key, value] of Object.entries(obj)) {
338
+ const label = key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
339
+ if (typeof value === "object" && value !== null) {
340
+ lines.push(`${indent}**${label}:**`);
341
+ lines.push(formatContextObject(value, indent + " "));
342
+ } else {
343
+ lines.push(`${indent}- **${label}:** ${value}`);
344
+ }
345
+ }
346
+ return lines.join("\n");
347
+ }
348
+ function createContextFetcher(config) {
349
+ return new ContextFetcher(config);
350
+ }
351
+
352
+ // src/executor.ts
353
+ import { spawn, spawnSync } from "child_process";
354
+ var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
355
+ var DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
356
+
357
+ Your job is to complete tasks assigned to you using the available tools.
358
+
359
+ ## Available Tools
360
+
361
+ You have access to Artyfacts MCP tools. USE THEM to complete your task:
362
+
363
+ - **create_artifact** - Create new artifacts (documents, specs, reports)
364
+ - **create_section** - Add sections to artifacts (content, tasks, decisions)
365
+ - **update_section** - Update existing sections
366
+ - **create_agent** - Create new AI agents with specific roles
367
+ - **list_artifacts** - Query existing artifacts
368
+ - **list_sections** - Query sections within an artifact
369
+ - **complete_task** - Mark a task as complete
370
+ - **block_task** - Block a task with a reason
371
+ - **create_blocker** - Create a decision blocker
372
+
373
+ IMPORTANT: When asked to create agents or update artifacts, USE THE TOOLS. Don't just describe what you would do - actually do it.
374
+
375
+ ## Guidelines
376
+
377
+ - USE THE TOOLS to take action
378
+ - If creating something, use create_artifact or create_section
379
+ - If creating agents, use create_agent
380
+ - If you cannot complete the task, explain why
381
+
382
+ Format your response as follows:
383
+ 1. First, use the tools to complete the task
384
+ 2. Summarize what you accomplished
385
+ 3. End with a brief summary line starting with "SUMMARY:"`;
386
+ var OpenClawExecutor = class {
387
+ constructor(config = {}) {
388
+ this.contextFetcher = null;
389
+ this.config = {
390
+ ...config,
391
+ timeout: config.timeout || DEFAULT_TIMEOUT,
392
+ openclawPath: config.openclawPath || "openclaw"
393
+ };
394
+ if (config.baseUrl && config.apiKey) {
395
+ this.contextFetcher = createContextFetcher({
396
+ baseUrl: config.baseUrl,
397
+ apiKey: config.apiKey
398
+ });
399
+ }
400
+ }
401
+ /**
402
+ * Check if OpenClaw CLI is installed
403
+ */
404
+ async isInstalled() {
405
+ try {
406
+ const result = spawnSync(this.config.openclawPath || "openclaw", ["--version"], {
407
+ encoding: "utf-8",
408
+ stdio: ["pipe", "pipe", "pipe"]
409
+ });
410
+ return result.status === 0;
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+ /**
416
+ * Get OpenClaw version
417
+ */
418
+ async getVersion() {
419
+ try {
420
+ const result = spawnSync(this.config.openclawPath || "openclaw", ["--version"], {
421
+ encoding: "utf-8",
422
+ stdio: ["pipe", "pipe", "pipe"]
423
+ });
424
+ if (result.status === 0) {
425
+ return result.stdout.trim();
426
+ }
427
+ return null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ /**
433
+ * Execute a task using OpenClaw CLI
434
+ */
435
+ async execute(task) {
436
+ try {
437
+ let prompt;
438
+ let fullContext = null;
439
+ const useFullContext = this.config.useFullContext !== false && this.contextFetcher;
440
+ if (useFullContext) {
441
+ try {
442
+ fullContext = await this.contextFetcher.fetchTaskContext(task.taskId);
443
+ prompt = buildPromptWithContext(fullContext);
444
+ console.log(" \u{1F4DA} Using full context (org, project, artifact, related sections)");
445
+ } catch (contextError) {
446
+ console.warn(" \u26A0\uFE0F Could not fetch full context, using minimal prompt");
447
+ console.warn(` ${contextError instanceof Error ? contextError.message : contextError}`);
448
+ prompt = this.buildTaskPrompt(task);
449
+ }
450
+ } else {
451
+ prompt = this.buildTaskPrompt(task);
452
+ }
453
+ const output = await this.runOpenClaw(prompt);
454
+ const { content, summary } = this.parseResponse(output, task.heading);
455
+ return {
456
+ success: true,
457
+ output: content,
458
+ summary,
459
+ promptUsed: prompt
460
+ };
461
+ } catch (error) {
462
+ const errorMessage = error instanceof Error ? error.message : String(error);
463
+ return {
464
+ success: false,
465
+ output: "",
466
+ summary: `Failed: ${errorMessage}`,
467
+ error: errorMessage
468
+ };
469
+ }
470
+ }
471
+ /**
472
+ * Run OpenClaw CLI with the given prompt
473
+ */
474
+ runOpenClaw(prompt) {
475
+ return new Promise((resolve, reject) => {
476
+ const openclawPath = this.config.openclawPath || "openclaw";
477
+ const args = [
478
+ "prompt",
479
+ "--output-format",
480
+ "text"
481
+ // Get plain text output
482
+ ];
483
+ if (this.config.sessionKey) {
484
+ args.push("--session", this.config.sessionKey);
485
+ }
486
+ args.push(prompt);
487
+ const proc = spawn(openclawPath, args, {
488
+ stdio: ["pipe", "pipe", "pipe"],
489
+ timeout: this.config.timeout,
490
+ env: {
491
+ ...process.env,
492
+ // Pass Artyfacts API key for MCP tools
493
+ ARTYFACTS_API_KEY: this.config.apiKey || process.env.ARTYFACTS_API_KEY || "",
494
+ ARTYFACTS_BASE_URL: this.config.baseUrl || "https://artyfacts.dev/api/v1"
495
+ }
496
+ });
497
+ let stdout = "";
498
+ let stderr = "";
499
+ proc.stdout.on("data", (data) => {
500
+ stdout += data.toString();
501
+ });
502
+ proc.stderr.on("data", (data) => {
503
+ stderr += data.toString();
504
+ });
505
+ proc.on("close", (code) => {
506
+ if (code === 0) {
507
+ resolve(stdout.trim());
508
+ } else {
509
+ reject(new Error(stderr || `OpenClaw exited with code ${code}`));
510
+ }
511
+ });
512
+ proc.on("error", (err) => {
513
+ if (err.code === "ENOENT") {
514
+ reject(new Error(
515
+ "OpenClaw CLI not found. Please install it:\n npm install -g openclaw"
516
+ ));
517
+ } else {
518
+ reject(err);
519
+ }
520
+ });
521
+ });
522
+ }
523
+ /**
524
+ * Build the task prompt
525
+ */
526
+ buildTaskPrompt(task) {
527
+ const parts = [];
528
+ const systemPrompt = this.config.systemPromptPrefix ? `${this.config.systemPromptPrefix}
529
+
530
+ ${DEFAULT_SYSTEM_PROMPT}` : DEFAULT_SYSTEM_PROMPT;
531
+ parts.push(systemPrompt);
532
+ parts.push("");
533
+ parts.push("---");
534
+ parts.push("");
535
+ parts.push(`# Task: ${task.heading}`);
536
+ parts.push("");
537
+ if (task.artifactTitle) {
538
+ parts.push(`**Part of:** ${task.artifactTitle}`);
539
+ parts.push(`**Artifact ID:** ${task.artifactId}`);
540
+ parts.push("");
541
+ }
542
+ if (task.priority) {
543
+ const priorityLabel = ["High", "Medium", "Low"][task.priority - 1] || "Unknown";
544
+ parts.push(`**Priority:** ${priorityLabel}`);
545
+ parts.push("");
546
+ }
547
+ parts.push("## Description");
548
+ parts.push("");
549
+ parts.push(task.content || "No description provided.");
550
+ parts.push("");
551
+ if (task.context && Object.keys(task.context).length > 0) {
552
+ parts.push("## Additional Context");
553
+ parts.push("");
554
+ parts.push("```json");
555
+ parts.push(JSON.stringify(task.context, null, 2));
556
+ parts.push("```");
557
+ parts.push("");
558
+ }
559
+ parts.push("---");
560
+ parts.push("");
561
+ parts.push("Please complete this task using the available Artyfacts tools.");
562
+ parts.push("When done, provide a summary of what you accomplished.");
563
+ return parts.join("\n");
564
+ }
565
+ /**
566
+ * Parse the response to extract content and summary
567
+ */
568
+ parseResponse(output, fallbackSummary) {
569
+ const summaryMatch = output.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
570
+ if (summaryMatch) {
571
+ const summary2 = summaryMatch[1].trim();
572
+ const content = output.replace(/SUMMARY:\s*.+?(?:\n|$)/i, "").trim();
573
+ return { content, summary: summary2 };
574
+ }
575
+ const lines = output.split("\n").filter((l) => l.trim());
576
+ const summary = lines.length > 0 ? lines[lines.length - 1].slice(0, 200) : `Completed: ${fallbackSummary}`;
577
+ return { content: output, summary };
578
+ }
579
+ };
580
+ function createExecutor(config) {
581
+ return new OpenClawExecutor(config);
582
+ }
583
+
584
+ // src/listener.ts
585
+ import EventSource from "eventsource";
586
+ var DEFAULT_BASE_URL2 = "https://artyfacts.dev/api/v1";
587
+ var EVENT_TYPES = [
588
+ "connected",
589
+ "heartbeat",
590
+ "task_assigned",
591
+ "task_unblocked",
592
+ "blocker_resolved",
593
+ "notification",
594
+ "mcp_connect_request",
595
+ "connection_status"
596
+ ];
597
+ var ArtyfactsListener = class {
598
+ constructor(config) {
599
+ this.eventSource = null;
600
+ this.callbacks = /* @__PURE__ */ new Map();
601
+ this.allCallbacks = /* @__PURE__ */ new Set();
602
+ this.state = "disconnected";
603
+ this.reconnectAttempts = 0;
604
+ this.maxReconnectAttempts = 10;
605
+ this.reconnectDelay = 1e3;
606
+ if (!config.apiKey) {
607
+ throw new Error("API key is required");
608
+ }
609
+ if (!config.agentId) {
610
+ throw new Error("Agent ID is required");
611
+ }
612
+ this.config = {
613
+ ...config,
614
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL2
615
+ };
616
+ }
617
+ /**
618
+ * Get current connection state
619
+ */
620
+ get connectionState() {
621
+ return this.state;
622
+ }
623
+ /**
624
+ * Check if connected
625
+ */
626
+ get isConnected() {
627
+ return this.state === "connected";
628
+ }
629
+ /**
630
+ * Subscribe to all events
631
+ */
632
+ subscribe(callback) {
633
+ this.allCallbacks.add(callback);
634
+ return () => {
635
+ this.allCallbacks.delete(callback);
636
+ };
637
+ }
638
+ /**
639
+ * Subscribe to a specific event type
640
+ */
641
+ on(type, callback) {
642
+ if (!this.callbacks.has(type)) {
643
+ this.callbacks.set(type, /* @__PURE__ */ new Set());
644
+ }
645
+ this.callbacks.get(type).add(callback);
646
+ return () => {
647
+ const typeCallbacks = this.callbacks.get(type);
648
+ if (typeCallbacks) {
649
+ typeCallbacks.delete(callback);
650
+ if (typeCallbacks.size === 0) {
651
+ this.callbacks.delete(type);
652
+ }
653
+ }
654
+ };
655
+ }
656
+ /**
657
+ * Connect to the SSE stream
658
+ */
659
+ connect() {
660
+ if (this.eventSource) {
661
+ return;
662
+ }
663
+ this.setState("connecting");
664
+ const url = new URL(`${this.config.baseUrl}/events/stream`);
665
+ url.searchParams.set("apiKey", this.config.apiKey);
666
+ url.searchParams.set("agentId", this.config.agentId);
667
+ this.eventSource = new EventSource(url.toString(), {
668
+ headers: {
669
+ "Authorization": `Bearer ${this.config.apiKey}`
670
+ }
671
+ });
672
+ this.eventSource.onopen = () => {
673
+ this.reconnectAttempts = 0;
674
+ this.reconnectDelay = 1e3;
675
+ this.setState("connected");
676
+ };
677
+ this.eventSource.onmessage = (event) => {
678
+ this.handleMessage(event);
679
+ };
680
+ this.eventSource.onerror = (event) => {
681
+ this.handleError(event);
682
+ };
683
+ for (const eventType of EVENT_TYPES) {
684
+ this.eventSource.addEventListener(eventType, (event) => {
685
+ this.handleMessage(event, eventType);
686
+ });
687
+ }
688
+ }
689
+ /**
690
+ * Disconnect from the SSE stream
691
+ */
692
+ disconnect() {
693
+ if (this.eventSource) {
694
+ this.eventSource.close();
695
+ this.eventSource = null;
696
+ }
697
+ this.setState("disconnected");
698
+ }
699
+ /**
700
+ * Reconnect to the SSE stream
701
+ */
702
+ reconnect() {
703
+ this.disconnect();
704
+ this.connect();
705
+ }
706
+ /**
707
+ * Handle incoming SSE message
708
+ */
709
+ handleMessage(event, eventType) {
710
+ try {
711
+ const data = JSON.parse(event.data);
712
+ const rawData = data.data || data;
713
+ const normalizedData = rawData.task_id ? {
714
+ taskId: rawData.task_id,
715
+ sectionId: rawData.section_id,
716
+ artifactId: rawData.artifact_id,
717
+ artifactTitle: rawData.artifact_title,
718
+ heading: rawData.heading,
719
+ content: rawData.content,
720
+ assignedTo: rawData.assigned_to,
721
+ assignedAt: rawData.assigned_at,
722
+ priority: rawData.priority,
723
+ ...rawData
724
+ // Keep original fields too
725
+ } : rawData;
726
+ const artyfactsEvent = {
727
+ type: eventType || data.type || "unknown",
728
+ timestamp: data.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
729
+ data: normalizedData
730
+ };
731
+ const typeCallbacks = this.callbacks.get(artyfactsEvent.type);
732
+ if (typeCallbacks) {
733
+ for (const callback of typeCallbacks) {
734
+ this.safeCallCallback(callback, artyfactsEvent);
735
+ }
736
+ }
737
+ for (const callback of this.allCallbacks) {
738
+ this.safeCallCallback(callback, artyfactsEvent);
739
+ }
740
+ } catch (err) {
741
+ console.error("[Listener] Failed to parse SSE message:", event.data, err);
742
+ }
743
+ }
744
+ /**
745
+ * Safely call a callback, handling async and errors
746
+ */
747
+ async safeCallCallback(callback, event) {
748
+ try {
749
+ await callback(event);
750
+ } catch (err) {
751
+ console.error(`[Listener] Error in event callback for '${event.type}':`, err);
752
+ }
753
+ }
754
+ /**
755
+ * Handle SSE error
756
+ */
757
+ handleError(event) {
758
+ if (this.eventSource?.readyState === EventSource.CONNECTING) {
759
+ this.setState("reconnecting");
760
+ } else if (this.eventSource?.readyState === EventSource.CLOSED) {
761
+ this.setState("disconnected");
762
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
763
+ this.reconnectAttempts++;
764
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
765
+ console.log(
766
+ `[Listener] Connection lost, reconnecting in ${this.reconnectDelay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
767
+ );
768
+ setTimeout(() => {
769
+ if (this.state === "disconnected") {
770
+ this.connect();
771
+ }
772
+ }, this.reconnectDelay);
773
+ } else {
774
+ const error = new Error("Max reconnection attempts reached");
775
+ this.config.onError?.(error);
776
+ }
777
+ }
778
+ }
779
+ /**
780
+ * Update connection state
781
+ */
782
+ setState(state) {
783
+ if (this.state !== state) {
784
+ this.state = state;
785
+ this.config.onStateChange?.(state);
786
+ }
787
+ }
788
+ };
789
+ function createListener(config) {
790
+ return new ArtyfactsListener(config);
791
+ }
792
+
793
+ // src/mcp.ts
794
+ import { spawnSync as spawnSync2 } from "child_process";
795
+ var DEFAULT_MCP_NAME = "artyfacts";
796
+ var DEFAULT_BASE_URL3 = "https://artyfacts.dev/api/v1";
797
+ var McpHandler = class {
798
+ constructor(config) {
799
+ this.config = {
800
+ name: DEFAULT_MCP_NAME,
801
+ baseUrl: DEFAULT_BASE_URL3,
802
+ ...config
803
+ };
804
+ this.openclawPath = config.openclawPath || "openclaw";
805
+ }
806
+ /**
807
+ * Check if Artyfacts MCP server is already configured
808
+ */
809
+ isConfigured() {
810
+ try {
811
+ const result = spawnSync2(this.openclawPath, ["mcp", "show", this.config.name || DEFAULT_MCP_NAME], {
812
+ encoding: "utf-8",
813
+ stdio: ["pipe", "pipe", "pipe"]
814
+ });
815
+ if (result.status === 0) {
816
+ return {
817
+ configured: true,
818
+ name: this.config.name
819
+ };
820
+ }
821
+ return { configured: false };
822
+ } catch (error) {
823
+ return {
824
+ configured: false,
825
+ error: error instanceof Error ? error.message : String(error)
826
+ };
827
+ }
828
+ }
829
+ /**
830
+ * Configure Artyfacts MCP server in OpenClaw
831
+ *
832
+ * Uses: openclaw mcp set <name> --command "npx" --args "-y @artyfacts/mcp-server"
833
+ */
834
+ configure() {
835
+ console.log("\u{1F527} Configuring Artyfacts tools for OpenClaw...");
836
+ try {
837
+ const result = spawnSync2(this.openclawPath, [
838
+ "mcp",
839
+ "set",
840
+ this.config.name || DEFAULT_MCP_NAME,
841
+ "--command",
842
+ "npx",
843
+ "--args",
844
+ "-y",
845
+ "--args",
846
+ "@artyfacts/mcp-server",
847
+ "--env",
848
+ `ARTYFACTS_API_KEY=${this.config.apiKey}`,
849
+ "--env",
850
+ `ARTYFACTS_BASE_URL=${this.config.baseUrl || DEFAULT_BASE_URL3}`
851
+ ], {
852
+ encoding: "utf-8",
853
+ stdio: ["pipe", "pipe", "pipe"]
854
+ });
855
+ if (result.status === 0) {
856
+ console.log("\u2705 MCP tools configured! OpenClaw now has access to Artyfacts.");
857
+ return true;
858
+ } else {
859
+ return this.configureAlternative();
860
+ }
861
+ } catch (error) {
862
+ console.log("\u26A0\uFE0F Could not configure MCP:", error instanceof Error ? error.message : error);
863
+ this.printManualInstructions();
864
+ return false;
865
+ }
866
+ }
867
+ /**
868
+ * Alternative configuration approach using JSON config
869
+ */
870
+ configureAlternative() {
871
+ try {
872
+ const mcpConfig = JSON.stringify({
873
+ command: "npx",
874
+ args: ["-y", "@artyfacts/mcp-server"],
875
+ env: {
876
+ ARTYFACTS_API_KEY: this.config.apiKey,
877
+ ARTYFACTS_BASE_URL: this.config.baseUrl || DEFAULT_BASE_URL3
878
+ }
879
+ });
880
+ const result = spawnSync2(this.openclawPath, [
881
+ "mcp",
882
+ "set",
883
+ this.config.name || DEFAULT_MCP_NAME,
884
+ "--config",
885
+ mcpConfig
886
+ ], {
887
+ encoding: "utf-8",
888
+ stdio: ["pipe", "pipe", "pipe"]
889
+ });
890
+ if (result.status === 0) {
891
+ console.log("\u2705 MCP tools configured! OpenClaw now has access to Artyfacts.");
892
+ return true;
893
+ } else {
894
+ console.log("\u26A0\uFE0F Could not configure MCP:", result.stderr || result.stdout);
895
+ this.printManualInstructions();
896
+ return false;
897
+ }
898
+ } catch (error) {
899
+ console.log("\u26A0\uFE0F Could not configure MCP:", error instanceof Error ? error.message : error);
900
+ this.printManualInstructions();
901
+ return false;
902
+ }
903
+ }
904
+ /**
905
+ * Print manual configuration instructions
906
+ */
907
+ printManualInstructions() {
908
+ console.log("");
909
+ console.log("You can manually add the Artyfacts MCP server with:");
910
+ console.log("");
911
+ console.log(` openclaw mcp set ${this.config.name || DEFAULT_MCP_NAME} \\`);
912
+ console.log(` --command "npx" \\`);
913
+ console.log(` --args "-y" --args "@artyfacts/mcp-server" \\`);
914
+ console.log(` --env "ARTYFACTS_API_KEY=${this.config.apiKey}" \\`);
915
+ console.log(` --env "ARTYFACTS_BASE_URL=${this.config.baseUrl || DEFAULT_BASE_URL3}"`);
916
+ console.log("");
917
+ console.log("Or add to your openclaw.json:");
918
+ console.log("");
919
+ console.log(` "mcp": {`);
920
+ console.log(` "${this.config.name || DEFAULT_MCP_NAME}": {`);
921
+ console.log(` "command": "npx",`);
922
+ console.log(` "args": ["-y", "@artyfacts/mcp-server"],`);
923
+ console.log(` "env": {`);
924
+ console.log(` "ARTYFACTS_API_KEY": "<your-api-key>",`);
925
+ console.log(` "ARTYFACTS_BASE_URL": "${this.config.baseUrl || DEFAULT_BASE_URL3}"`);
926
+ console.log(` }`);
927
+ console.log(` }`);
928
+ console.log(` }`);
929
+ console.log("");
930
+ }
931
+ /**
932
+ * Remove Artyfacts MCP configuration
933
+ */
934
+ remove() {
935
+ try {
936
+ const result = spawnSync2(this.openclawPath, [
937
+ "mcp",
938
+ "unset",
939
+ this.config.name || DEFAULT_MCP_NAME
940
+ ], {
941
+ encoding: "utf-8",
942
+ stdio: ["pipe", "pipe", "pipe"]
943
+ });
944
+ if (result.status === 0) {
945
+ console.log("\u2705 Artyfacts MCP configuration removed.");
946
+ return true;
947
+ } else {
948
+ console.log("\u26A0\uFE0F Could not remove MCP config:", result.stderr || result.stdout);
949
+ return false;
950
+ }
951
+ } catch (error) {
952
+ console.log("\u26A0\uFE0F Could not remove MCP config:", error instanceof Error ? error.message : error);
953
+ return false;
954
+ }
955
+ }
956
+ /**
957
+ * Ensure MCP is configured, configure if needed
958
+ */
959
+ ensureConfigured() {
960
+ const status = this.isConfigured();
961
+ if (status.configured) {
962
+ console.log("\u2705 Artyfacts MCP tools already configured");
963
+ return true;
964
+ }
965
+ return this.configure();
966
+ }
967
+ };
968
+ function createMcpHandler(config) {
969
+ return new McpHandler(config);
970
+ }
971
+
972
+ // src/introspect.ts
973
+ import { readFile, readdir } from "fs/promises";
974
+ import { join as join2 } from "path";
975
+ import { homedir as homedir2 } from "os";
976
+ import { execSync } from "child_process";
977
+ var OPENCLAW_DIR = join2(homedir2(), ".openclaw");
978
+ var CONFIG_FILE = join2(OPENCLAW_DIR, "openclaw.json");
979
+ var CRON_FILE = join2(OPENCLAW_DIR, "cron", "jobs.json");
980
+ var SKILLS_DIR = join2(OPENCLAW_DIR, "skills");
981
+ function parseJson5(content) {
982
+ const stripped = content.split("\n").map((line) => {
983
+ const commentIndex = line.indexOf("//");
984
+ if (commentIndex === -1) return line;
985
+ const beforeComment = line.slice(0, commentIndex);
986
+ const quotes = (beforeComment.match(/"/g) || []).length;
987
+ if (quotes % 2 === 0) {
988
+ return beforeComment;
989
+ }
990
+ return line;
991
+ }).join("\n");
992
+ const noTrailing = stripped.replace(/,(\s*[}\]])/g, "$1");
993
+ return JSON.parse(noTrailing);
994
+ }
995
+ async function readCronJobs() {
996
+ try {
997
+ const content = await readFile(CRON_FILE, "utf-8");
998
+ const jobs = JSON.parse(content);
999
+ return Array.isArray(jobs) ? jobs : [];
1000
+ } catch (err) {
1001
+ return [];
1002
+ }
1003
+ }
1004
+ async function readMainConfig() {
1005
+ try {
1006
+ const content = await readFile(CONFIG_FILE, "utf-8");
1007
+ const config = parseJson5(content);
1008
+ const agentsConfig = config.agents;
1009
+ const defaults = agentsConfig?.defaults;
1010
+ const heartbeat = defaults?.heartbeat;
1011
+ const agentsList = agentsConfig?.list || [];
1012
+ const channelsConfig = config.channels;
1013
+ const channelNames = channelsConfig ? Object.keys(channelsConfig).filter((k) => k !== "defaults") : [];
1014
+ return {
1015
+ heartbeat: heartbeat || null,
1016
+ agents: agentsList,
1017
+ channels: channelNames
1018
+ };
1019
+ } catch (err) {
1020
+ return { heartbeat: null, agents: [], channels: [] };
1021
+ }
1022
+ }
1023
+ async function readMcpServers() {
1024
+ try {
1025
+ const output = execSync("openclaw mcp list --json 2>/dev/null", {
1026
+ encoding: "utf-8",
1027
+ timeout: 5e3
1028
+ });
1029
+ const servers = JSON.parse(output);
1030
+ return Array.isArray(servers) ? servers : [];
1031
+ } catch (err) {
1032
+ return [];
1033
+ }
1034
+ }
1035
+ async function readSkills() {
1036
+ const skills = [];
1037
+ async function scanDir(dir, prefix = "") {
1038
+ try {
1039
+ const entries = await readdir(dir, { withFileTypes: true });
1040
+ for (const entry of entries) {
1041
+ const fullPath = join2(dir, entry.name);
1042
+ if (entry.isDirectory()) {
1043
+ await scanDir(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name);
1044
+ } else if (entry.name === "SKILL.md") {
1045
+ const skillName = prefix || "root";
1046
+ const content = await readFile(fullPath, "utf-8");
1047
+ const lines = content.split("\n");
1048
+ const descLine = lines.find((l) => l.trim() && !l.startsWith("#"));
1049
+ skills.push({
1050
+ name: skillName,
1051
+ path: fullPath,
1052
+ description: descLine?.trim().slice(0, 200),
1053
+ content
1054
+ });
1055
+ }
1056
+ }
1057
+ } catch (err) {
1058
+ }
1059
+ }
1060
+ await scanDir(SKILLS_DIR);
1061
+ return skills;
1062
+ }
1063
+ async function introspect() {
1064
+ const [crons, mainConfig, mcpServers, skills] = await Promise.all([
1065
+ readCronJobs(),
1066
+ readMainConfig(),
1067
+ readMcpServers(),
1068
+ readSkills()
1069
+ ]);
1070
+ return {
1071
+ crons,
1072
+ heartbeat: mainConfig.heartbeat,
1073
+ agents: mainConfig.agents,
1074
+ mcpServers,
1075
+ skills,
1076
+ channels: mainConfig.channels
1077
+ };
1078
+ }
1079
+ function summarize(config) {
1080
+ const parts = [];
1081
+ if (config.crons.length > 0) {
1082
+ parts.push(`${config.crons.length} cron job(s)`);
1083
+ }
1084
+ if (config.heartbeat) {
1085
+ parts.push(`heartbeat: ${config.heartbeat.every || "30m"}`);
1086
+ }
1087
+ if (config.agents.length > 0) {
1088
+ parts.push(`${config.agents.length} agent(s)`);
1089
+ }
1090
+ if (config.mcpServers.length > 0) {
1091
+ parts.push(`${config.mcpServers.length} MCP server(s)`);
1092
+ }
1093
+ if (config.skills.length > 0) {
1094
+ parts.push(`${config.skills.length} skill(s)`);
1095
+ }
1096
+ if (config.channels.length > 0) {
1097
+ parts.push(`channels: ${config.channels.join(", ")}`);
1098
+ }
1099
+ return parts.length > 0 ? parts.join(" | ") : "No configuration found";
1100
+ }
1101
+
1102
+ export {
1103
+ loadCredentials,
1104
+ saveCredentials,
1105
+ clearCredentials,
1106
+ promptForApiKey,
1107
+ getCredentials,
1108
+ ContextFetcher,
1109
+ buildPromptWithContext,
1110
+ createContextFetcher,
1111
+ OpenClawExecutor,
1112
+ createExecutor,
1113
+ ArtyfactsListener,
1114
+ createListener,
1115
+ McpHandler,
1116
+ createMcpHandler,
1117
+ introspect,
1118
+ summarize
1119
+ };