@artyfacts/claude 1.0.0 → 1.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.
- package/README.md +159 -78
- package/bin/artyfacts-claude.js +38 -0
- package/dist/chunk-365PEWTO.mjs +567 -0
- package/dist/chunk-QKDOZSBI.mjs +571 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +791 -0
- package/dist/cli.mjs +189 -726
- package/dist/index.d.mts +178 -249
- package/dist/index.d.ts +178 -249
- package/dist/index.js +472 -486
- package/dist/index.mjs +22 -584
- package/package.json +27 -16
- package/src/auth.ts +344 -0
- package/src/cli.ts +344 -0
- package/src/executor.ts +293 -0
- package/src/index.ts +86 -0
- package/src/listener.ts +313 -0
- package/tsconfig.json +20 -0
- package/bin/cli.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,756 +1,219 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
clearCredentials,
|
|
4
|
+
createExecutor,
|
|
5
|
+
createListener,
|
|
6
|
+
getCredentials,
|
|
7
|
+
loadCredentials,
|
|
8
|
+
promptForApiKey
|
|
9
|
+
} from "./chunk-365PEWTO.mjs";
|
|
2
10
|
|
|
3
11
|
// src/cli.ts
|
|
4
12
|
import { Command } from "commander";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
async fetch(path2, options = {}) {
|
|
21
|
-
const url = `${this.config.baseUrl}${path2}`;
|
|
22
|
-
const response = await fetch(url, {
|
|
23
|
-
...options,
|
|
24
|
-
headers: {
|
|
25
|
-
"Content-Type": "application/json",
|
|
26
|
-
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
27
|
-
...options.headers
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
const error = await response.text();
|
|
32
|
-
throw new Error(`API error (${response.status}): ${error}`);
|
|
13
|
+
var VERSION = "0.1.0";
|
|
14
|
+
var DEFAULT_BASE_URL = "https://artyfacts.dev/api/v1";
|
|
15
|
+
var program = new Command();
|
|
16
|
+
program.name("artyfacts-claude").description("Claude adapter for Artyfacts - Execute tasks using Claude API").version(VERSION);
|
|
17
|
+
program.command("run", { isDefault: true }).description("Start listening for and executing tasks").option("--base-url <url>", "Artyfacts API base URL", DEFAULT_BASE_URL).option("--dry-run", "Print tasks but do not execute", false).action(async (options) => {
|
|
18
|
+
await runAgent(options);
|
|
19
|
+
});
|
|
20
|
+
program.command("login").description("Authenticate with Artyfacts").option("--manual", "Enter credentials manually instead of device auth").option("--base-url <url>", "Artyfacts API base URL", DEFAULT_BASE_URL).action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
if (options.manual) {
|
|
23
|
+
await promptForApiKey();
|
|
24
|
+
} else {
|
|
25
|
+
await getCredentials({ baseUrl: options.baseUrl, forceAuth: true });
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* Get claimable tasks from the queue
|
|
38
|
-
*/
|
|
39
|
-
async getTaskQueue(options) {
|
|
40
|
-
const params = new URLSearchParams();
|
|
41
|
-
if (options?.limit) params.set("limit", options.limit.toString());
|
|
42
|
-
const result = await this.fetch(`/tasks/queue?${params}`);
|
|
43
|
-
return result.tasks;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Claim a task
|
|
47
|
-
*/
|
|
48
|
-
async claimTask(taskId) {
|
|
49
|
-
return this.fetch(`/tasks/${taskId}/claim`, {
|
|
50
|
-
method: "POST",
|
|
51
|
-
body: JSON.stringify({ agent_id: this.config.agentId })
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Complete a task
|
|
56
|
-
*/
|
|
57
|
-
async completeTask(taskId, options) {
|
|
58
|
-
return this.fetch(`/tasks/${taskId}/complete`, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
body: JSON.stringify({
|
|
61
|
-
agent_id: this.config.agentId,
|
|
62
|
-
output_url: options?.outputUrl,
|
|
63
|
-
summary: options?.summary
|
|
64
|
-
})
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Report task as blocked
|
|
69
|
-
*/
|
|
70
|
-
async blockTask(taskId, reason) {
|
|
71
|
-
await this.fetch(`/tasks/${taskId}/block`, {
|
|
72
|
-
method: "POST",
|
|
73
|
-
body: JSON.stringify({
|
|
74
|
-
agent_id: this.config.agentId,
|
|
75
|
-
reason
|
|
76
|
-
})
|
|
77
|
-
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("\u274C Authentication failed:", error instanceof Error ? error.message : error);
|
|
29
|
+
process.exit(1);
|
|
78
30
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
31
|
+
});
|
|
32
|
+
program.command("logout").description("Clear stored credentials").action(() => {
|
|
33
|
+
clearCredentials();
|
|
34
|
+
console.log("\u2705 Credentials cleared");
|
|
35
|
+
});
|
|
36
|
+
program.command("status").description("Check authentication and connection status").action(async () => {
|
|
37
|
+
const credentials = loadCredentials();
|
|
38
|
+
if (!credentials) {
|
|
39
|
+
console.log("\u274C Not authenticated");
|
|
40
|
+
console.log(" Run: npx @artyfacts/claude login");
|
|
41
|
+
process.exit(1);
|
|
84
42
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import { EventEmitter } from "events";
|
|
90
|
-
var ClaudeRunner = class extends EventEmitter {
|
|
91
|
-
config;
|
|
92
|
-
runningTasks = /* @__PURE__ */ new Map();
|
|
93
|
-
constructor(config = {}) {
|
|
94
|
-
super();
|
|
95
|
-
this.config = {
|
|
96
|
-
claudePath: config.claudePath ?? "claude",
|
|
97
|
-
model: config.model ?? "sonnet",
|
|
98
|
-
cwd: config.cwd ?? process.cwd(),
|
|
99
|
-
timeoutMs: config.timeoutMs ?? 5 * 60 * 1e3
|
|
100
|
-
// 5 minutes
|
|
101
|
-
};
|
|
43
|
+
console.log("\u2705 Authenticated");
|
|
44
|
+
console.log(` Agent ID: ${credentials.agentId}`);
|
|
45
|
+
if (credentials.agentName) {
|
|
46
|
+
console.log(` Agent Name: ${credentials.agentName}`);
|
|
102
47
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
48
|
+
const executor = createExecutor();
|
|
49
|
+
const installed = await executor.isInstalled();
|
|
50
|
+
if (installed) {
|
|
51
|
+
console.log("\u2705 Claude Code CLI installed");
|
|
107
52
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
53
|
+
const connected = await executor.testConnection();
|
|
54
|
+
if (connected) {
|
|
55
|
+
console.log("\u2705 Claude Code authenticated and working");
|
|
56
|
+
} else {
|
|
57
|
+
console.log("\u26A0\uFE0F Claude Code not authenticated");
|
|
58
|
+
console.log(" Run: claude login");
|
|
59
|
+
}
|
|
113
60
|
} catch (error) {
|
|
114
|
-
|
|
115
|
-
installed: false,
|
|
116
|
-
authenticated: false,
|
|
117
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Run a task with Claude Code
|
|
123
|
-
*/
|
|
124
|
-
async runTask(taskId, prompt, options) {
|
|
125
|
-
const startedAt = /* @__PURE__ */ new Date();
|
|
126
|
-
const model = options?.model ?? this.config.model;
|
|
127
|
-
const cwd = options?.cwd ?? this.config.cwd;
|
|
128
|
-
const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
|
|
129
|
-
const args = [
|
|
130
|
-
"-p",
|
|
131
|
-
prompt,
|
|
132
|
-
"--print",
|
|
133
|
-
"--output-format",
|
|
134
|
-
"text",
|
|
135
|
-
"--model",
|
|
136
|
-
model
|
|
137
|
-
];
|
|
138
|
-
this.emit("task:start", { taskId, prompt, model });
|
|
139
|
-
const promise = new Promise((resolve) => {
|
|
140
|
-
let output = "";
|
|
141
|
-
let error = "";
|
|
142
|
-
const proc = spawn(this.config.claudePath, args, {
|
|
143
|
-
cwd,
|
|
144
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
145
|
-
env: { ...process.env }
|
|
146
|
-
});
|
|
147
|
-
const timeout = setTimeout(() => {
|
|
148
|
-
proc.kill("SIGTERM");
|
|
149
|
-
resolve({
|
|
150
|
-
success: false,
|
|
151
|
-
output,
|
|
152
|
-
error: "Task timed out",
|
|
153
|
-
exitCode: null,
|
|
154
|
-
durationMs: Date.now() - startedAt.getTime()
|
|
155
|
-
});
|
|
156
|
-
}, timeoutMs);
|
|
157
|
-
proc.stdout?.on("data", (data) => {
|
|
158
|
-
output += data.toString();
|
|
159
|
-
this.emit("task:output", { taskId, chunk: data.toString() });
|
|
160
|
-
});
|
|
161
|
-
proc.stderr?.on("data", (data) => {
|
|
162
|
-
error += data.toString();
|
|
163
|
-
});
|
|
164
|
-
proc.on("close", (code) => {
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
this.runningTasks.delete(taskId);
|
|
167
|
-
const result = {
|
|
168
|
-
success: code === 0,
|
|
169
|
-
output: output.trim(),
|
|
170
|
-
error: error.trim() || void 0,
|
|
171
|
-
exitCode: code,
|
|
172
|
-
durationMs: Date.now() - startedAt.getTime()
|
|
173
|
-
};
|
|
174
|
-
this.emit("task:complete", { taskId, result });
|
|
175
|
-
resolve(result);
|
|
176
|
-
});
|
|
177
|
-
proc.on("error", (err) => {
|
|
178
|
-
clearTimeout(timeout);
|
|
179
|
-
this.runningTasks.delete(taskId);
|
|
180
|
-
resolve({
|
|
181
|
-
success: false,
|
|
182
|
-
output: "",
|
|
183
|
-
error: err.message,
|
|
184
|
-
exitCode: null,
|
|
185
|
-
durationMs: Date.now() - startedAt.getTime()
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
this.runningTasks.set(taskId, {
|
|
189
|
-
taskId,
|
|
190
|
-
process: proc,
|
|
191
|
-
startedAt,
|
|
192
|
-
promise
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
return promise;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Run a raw claude command
|
|
199
|
-
*/
|
|
200
|
-
runCommand(args) {
|
|
201
|
-
return new Promise((resolve) => {
|
|
202
|
-
const startedAt = Date.now();
|
|
203
|
-
let output = "";
|
|
204
|
-
let error = "";
|
|
205
|
-
const proc = spawn(this.config.claudePath, args, {
|
|
206
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
207
|
-
});
|
|
208
|
-
proc.stdout?.on("data", (data) => {
|
|
209
|
-
output += data.toString();
|
|
210
|
-
});
|
|
211
|
-
proc.stderr?.on("data", (data) => {
|
|
212
|
-
error += data.toString();
|
|
213
|
-
});
|
|
214
|
-
proc.on("close", (code) => {
|
|
215
|
-
resolve({
|
|
216
|
-
success: code === 0,
|
|
217
|
-
output: output.trim(),
|
|
218
|
-
error: error.trim() || void 0,
|
|
219
|
-
exitCode: code,
|
|
220
|
-
durationMs: Date.now() - startedAt
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
proc.on("error", (err) => {
|
|
224
|
-
resolve({
|
|
225
|
-
success: false,
|
|
226
|
-
output: "",
|
|
227
|
-
error: err.message,
|
|
228
|
-
exitCode: null,
|
|
229
|
-
durationMs: Date.now() - startedAt
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Cancel a running task
|
|
236
|
-
*/
|
|
237
|
-
cancelTask(taskId) {
|
|
238
|
-
const task = this.runningTasks.get(taskId);
|
|
239
|
-
if (task) {
|
|
240
|
-
task.process.kill("SIGTERM");
|
|
241
|
-
this.runningTasks.delete(taskId);
|
|
242
|
-
this.emit("task:cancelled", { taskId });
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Get count of running tasks
|
|
249
|
-
*/
|
|
250
|
-
getRunningCount() {
|
|
251
|
-
return this.runningTasks.size;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Cancel all running tasks
|
|
255
|
-
*/
|
|
256
|
-
cancelAll() {
|
|
257
|
-
for (const [taskId] of this.runningTasks) {
|
|
258
|
-
this.cancelTask(taskId);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// src/adapter.ts
|
|
264
|
-
var ClaudeAdapter = class extends EventEmitter2 {
|
|
265
|
-
config;
|
|
266
|
-
client;
|
|
267
|
-
runner;
|
|
268
|
-
running = false;
|
|
269
|
-
pollTimer = null;
|
|
270
|
-
activeTasks = /* @__PURE__ */ new Set();
|
|
271
|
-
constructor(config) {
|
|
272
|
-
super();
|
|
273
|
-
this.config = {
|
|
274
|
-
apiKey: config.apiKey,
|
|
275
|
-
baseUrl: config.baseUrl ?? "https://artyfacts.dev/api/v1",
|
|
276
|
-
agentId: config.agentId ?? "claude-agent",
|
|
277
|
-
pollIntervalMs: config.pollIntervalMs ?? 3e4,
|
|
278
|
-
maxConcurrent: config.maxConcurrent ?? 1,
|
|
279
|
-
model: config.model ?? "sonnet",
|
|
280
|
-
cwd: config.cwd ?? process.cwd()
|
|
281
|
-
};
|
|
282
|
-
this.client = new ArtyfactsClient({
|
|
283
|
-
apiKey: this.config.apiKey,
|
|
284
|
-
baseUrl: this.config.baseUrl,
|
|
285
|
-
agentId: this.config.agentId
|
|
286
|
-
});
|
|
287
|
-
this.runner = new ClaudeRunner({
|
|
288
|
-
model: this.config.model,
|
|
289
|
-
cwd: this.config.cwd
|
|
290
|
-
});
|
|
291
|
-
this.runner.on("task:output", (data) => this.emit("task:output", data));
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Check if Claude Code is ready
|
|
295
|
-
*/
|
|
296
|
-
async checkReady() {
|
|
297
|
-
const check = await this.runner.checkInstalled();
|
|
298
|
-
if (!check.installed) {
|
|
299
|
-
return {
|
|
300
|
-
ready: false,
|
|
301
|
-
error: "Claude Code is not installed. Run: npm install -g @anthropic-ai/claude-code"
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
if (!check.authenticated) {
|
|
305
|
-
return {
|
|
306
|
-
ready: false,
|
|
307
|
-
error: "Claude Code is not authenticated. Run: claude login"
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
return { ready: true };
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Start the adapter
|
|
314
|
-
*/
|
|
315
|
-
async start() {
|
|
316
|
-
if (this.running) return;
|
|
317
|
-
const { ready, error } = await this.checkReady();
|
|
318
|
-
if (!ready) {
|
|
319
|
-
throw new Error(error);
|
|
61
|
+
console.log("\u26A0\uFE0F Claude Code error:", error instanceof Error ? error.message : error);
|
|
320
62
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.pollTimer = setInterval(() => {
|
|
325
|
-
this.poll().catch((err) => this.emit("error", err));
|
|
326
|
-
}, this.config.pollIntervalMs);
|
|
63
|
+
} else {
|
|
64
|
+
console.log("\u274C Claude Code CLI not installed");
|
|
65
|
+
console.log(" Install: npm install -g @anthropic-ai/claude-code");
|
|
327
66
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
this.runner.cancelAll();
|
|
339
|
-
this.emit("stopped");
|
|
67
|
+
});
|
|
68
|
+
async function runAgent(options) {
|
|
69
|
+
console.log("\u{1F517} Connecting to Artyfacts...");
|
|
70
|
+
let credentials;
|
|
71
|
+
try {
|
|
72
|
+
credentials = await getCredentials({ baseUrl: options.baseUrl });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("\u274C Authentication failed:", error instanceof Error ? error.message : error);
|
|
75
|
+
process.exit(1);
|
|
340
76
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
77
|
+
let executor = null;
|
|
78
|
+
if (!options.dryRun) {
|
|
79
|
+
executor = createExecutor();
|
|
80
|
+
const installed = await executor.isInstalled();
|
|
81
|
+
if (!installed) {
|
|
82
|
+
console.error("\u274C Claude Code CLI not found");
|
|
83
|
+
console.error(" Install it with: npm install -g @anthropic-ai/claude-code");
|
|
84
|
+
console.error(" Then authenticate with: claude login");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log("\u{1F504} Testing Claude Code connection...");
|
|
88
|
+
const connected = await executor.testConnection();
|
|
89
|
+
if (!connected) {
|
|
90
|
+
console.error("\u274C Claude Code not authenticated");
|
|
91
|
+
console.error(" Run: claude login");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log("\u2705 Claude Code connected");
|
|
95
|
+
}
|
|
96
|
+
const listener = createListener({
|
|
97
|
+
apiKey: credentials.apiKey,
|
|
98
|
+
agentId: credentials.agentId,
|
|
99
|
+
baseUrl: options.baseUrl,
|
|
100
|
+
onStateChange: (state) => {
|
|
101
|
+
switch (state) {
|
|
102
|
+
case "connecting":
|
|
103
|
+
console.log("\u{1F504} Connecting...");
|
|
104
|
+
break;
|
|
105
|
+
case "connected":
|
|
106
|
+
console.log(`\u2705 Connected as ${credentials.agentId}`);
|
|
107
|
+
console.log("\u{1F442} Listening for tasks...\n");
|
|
108
|
+
break;
|
|
109
|
+
case "reconnecting":
|
|
110
|
+
console.log("\u{1F504} Reconnecting...");
|
|
111
|
+
break;
|
|
112
|
+
case "disconnected":
|
|
113
|
+
console.log("\u26A0\uFE0F Disconnected");
|
|
114
|
+
break;
|
|
354
115
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
116
|
+
},
|
|
117
|
+
onError: (error) => {
|
|
118
|
+
console.error("\u274C Connection error:", error.message);
|
|
357
119
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
120
|
+
});
|
|
121
|
+
const activeTasks = /* @__PURE__ */ new Set();
|
|
122
|
+
listener.on("task_assigned", async (event) => {
|
|
123
|
+
const task = event.data;
|
|
124
|
+
if (activeTasks.has(task.taskId)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
activeTasks.add(task.taskId);
|
|
128
|
+
console.log(`
|
|
129
|
+
[Task received] ${task.heading}`);
|
|
130
|
+
if (options.dryRun) {
|
|
131
|
+
console.log(" \u{1F4CB} Dry run - not executing");
|
|
132
|
+
console.log(` \u{1F4C4} Artifact: ${task.artifactTitle || task.artifactId}`);
|
|
133
|
+
console.log(` \u{1F4DD} Content: ${task.content.substring(0, 100)}...`);
|
|
134
|
+
activeTasks.delete(task.taskId);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.log(" \u2192 Executing with Claude...");
|
|
363
138
|
try {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.activeTasks.delete(task.id);
|
|
139
|
+
const taskContext = {
|
|
140
|
+
taskId: task.taskId,
|
|
141
|
+
heading: task.heading,
|
|
142
|
+
content: task.content,
|
|
143
|
+
artifactId: task.artifactId,
|
|
144
|
+
artifactTitle: task.artifactTitle,
|
|
145
|
+
priority: task.priority
|
|
146
|
+
};
|
|
147
|
+
const result = await executor.execute(taskContext);
|
|
374
148
|
if (result.success) {
|
|
375
|
-
await
|
|
376
|
-
|
|
149
|
+
await completeTask({
|
|
150
|
+
baseUrl: options.baseUrl,
|
|
151
|
+
apiKey: credentials.apiKey,
|
|
152
|
+
taskId: task.taskId,
|
|
153
|
+
output: result.output,
|
|
154
|
+
summary: result.summary
|
|
377
155
|
});
|
|
378
|
-
|
|
156
|
+
console.log(` \u2192 \u2705 Completed! ${result.summary}`);
|
|
379
157
|
} else {
|
|
380
|
-
|
|
381
|
-
|
|
158
|
+
console.log(` \u2192 \u274C Failed: ${result.error}`);
|
|
159
|
+
await blockTask({
|
|
160
|
+
baseUrl: options.baseUrl,
|
|
161
|
+
apiKey: credentials.apiKey,
|
|
162
|
+
taskId: task.taskId,
|
|
163
|
+
reason: result.error || "Execution failed"
|
|
164
|
+
});
|
|
382
165
|
}
|
|
383
166
|
} catch (error) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
167
|
+
console.error(` \u2192 \u274C Error:`, error instanceof Error ? error.message : error);
|
|
168
|
+
} finally {
|
|
169
|
+
activeTasks.delete(task.taskId);
|
|
387
170
|
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Build a prompt for Claude from the task
|
|
391
|
-
*/
|
|
392
|
-
buildPrompt(task) {
|
|
393
|
-
return `You are working on a task from Artyfacts.
|
|
394
|
-
|
|
395
|
-
## Task: ${task.heading}
|
|
396
|
-
|
|
397
|
-
## Context
|
|
398
|
-
- Artifact: ${task.artifactTitle}
|
|
399
|
-
- URL: ${task.artifactUrl}
|
|
400
|
-
- Priority: ${task.priority === 1 ? "High" : task.priority === 2 ? "Medium" : "Low"}
|
|
401
|
-
|
|
402
|
-
## Details
|
|
403
|
-
${task.content}
|
|
404
|
-
|
|
405
|
-
## Instructions
|
|
406
|
-
Complete this task to the best of your ability. When finished, provide a brief summary of what you accomplished.
|
|
407
|
-
|
|
408
|
-
If you cannot complete the task, explain why clearly.`;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Extract a summary from Claude's output
|
|
412
|
-
*/
|
|
413
|
-
extractSummary(output) {
|
|
414
|
-
const maxLen = 500;
|
|
415
|
-
if (output.length <= maxLen) return output;
|
|
416
|
-
return "..." + output.slice(-maxLen);
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Check if running
|
|
420
|
-
*/
|
|
421
|
-
isRunning() {
|
|
422
|
-
return this.running;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Get stats
|
|
426
|
-
*/
|
|
427
|
-
getStats() {
|
|
428
|
-
return {
|
|
429
|
-
running: this.running,
|
|
430
|
-
activeTasks: this.activeTasks.size,
|
|
431
|
-
maxConcurrent: this.config.maxConcurrent
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
// src/auth.ts
|
|
437
|
-
import { execSync } from "child_process";
|
|
438
|
-
import * as fs from "fs";
|
|
439
|
-
import * as path from "path";
|
|
440
|
-
import * as os from "os";
|
|
441
|
-
var DeviceAuth = class {
|
|
442
|
-
baseUrl;
|
|
443
|
-
credentialsPath;
|
|
444
|
-
constructor(options) {
|
|
445
|
-
this.baseUrl = options?.baseUrl ?? "https://artyfacts.dev";
|
|
446
|
-
this.credentialsPath = path.join(os.homedir(), ".artyfacts", "credentials.json");
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Check if we have stored credentials
|
|
450
|
-
*/
|
|
451
|
-
hasCredentials() {
|
|
452
|
-
return fs.existsSync(this.credentialsPath);
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Get stored credentials
|
|
456
|
-
*/
|
|
457
|
-
getCredentials() {
|
|
458
|
-
if (!this.hasCredentials()) return null;
|
|
459
|
-
try {
|
|
460
|
-
const data = fs.readFileSync(this.credentialsPath, "utf-8");
|
|
461
|
-
const creds = JSON.parse(data);
|
|
462
|
-
if (creds.expiresAt && Date.now() > creds.expiresAt) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
return creds;
|
|
466
|
-
} catch {
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Get access token (for API calls)
|
|
472
|
-
*/
|
|
473
|
-
getAccessToken() {
|
|
474
|
-
const creds = this.getCredentials();
|
|
475
|
-
return creds?.accessToken ?? null;
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Clear stored credentials (logout)
|
|
479
|
-
*/
|
|
480
|
-
logout() {
|
|
481
|
-
if (fs.existsSync(this.credentialsPath)) {
|
|
482
|
-
fs.unlinkSync(this.credentialsPath);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Start device authorization flow
|
|
487
|
-
*/
|
|
488
|
-
async startDeviceFlow() {
|
|
489
|
-
const response = await fetch(`${this.baseUrl}/api/v1/auth/device`, {
|
|
490
|
-
method: "POST",
|
|
491
|
-
headers: { "Content-Type": "application/json" }
|
|
492
|
-
});
|
|
493
|
-
if (!response.ok) {
|
|
494
|
-
throw new Error(`Failed to start device flow: ${response.status}`);
|
|
495
|
-
}
|
|
496
|
-
return response.json();
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Poll for token after user authorizes
|
|
500
|
-
*/
|
|
501
|
-
async pollForToken(deviceCode, interval, expiresIn) {
|
|
502
|
-
const startTime = Date.now();
|
|
503
|
-
const expiresAt = startTime + expiresIn * 1e3;
|
|
504
|
-
while (Date.now() < expiresAt) {
|
|
505
|
-
await this.sleep(interval * 1e3);
|
|
506
|
-
const response = await fetch(`${this.baseUrl}/api/v1/auth/device/token`, {
|
|
507
|
-
method: "POST",
|
|
508
|
-
headers: { "Content-Type": "application/json" },
|
|
509
|
-
body: JSON.stringify({ deviceCode })
|
|
510
|
-
});
|
|
511
|
-
if (response.ok) {
|
|
512
|
-
return response.json();
|
|
513
|
-
}
|
|
514
|
-
const data = await response.json().catch(() => ({}));
|
|
515
|
-
if (data.error === "authorization_pending") {
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
if (data.error === "slow_down") {
|
|
519
|
-
interval += 5;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (data.error === "expired_token") {
|
|
523
|
-
throw new Error("Authorization expired. Please try again.");
|
|
524
|
-
}
|
|
525
|
-
if (data.error === "access_denied") {
|
|
526
|
-
throw new Error("Authorization denied by user.");
|
|
527
|
-
}
|
|
528
|
-
throw new Error(data.error ?? "Unknown error during authorization");
|
|
529
|
-
}
|
|
530
|
-
throw new Error("Authorization timed out. Please try again.");
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Save credentials to disk
|
|
534
|
-
*/
|
|
535
|
-
saveCredentials(token) {
|
|
536
|
-
const dir = path.dirname(this.credentialsPath);
|
|
537
|
-
if (!fs.existsSync(dir)) {
|
|
538
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
539
|
-
}
|
|
540
|
-
const creds = {
|
|
541
|
-
...token,
|
|
542
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
543
|
-
};
|
|
544
|
-
fs.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
|
|
545
|
-
mode: 384
|
|
546
|
-
// Only user can read/write
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Open URL in browser
|
|
551
|
-
*/
|
|
552
|
-
openBrowser(url) {
|
|
553
|
-
const platform = process.platform;
|
|
554
|
-
try {
|
|
555
|
-
if (platform === "darwin") {
|
|
556
|
-
execSync(`open "${url}"`);
|
|
557
|
-
} else if (platform === "win32") {
|
|
558
|
-
execSync(`start "" "${url}"`);
|
|
559
|
-
} else {
|
|
560
|
-
execSync(`xdg-open "${url}"`);
|
|
561
|
-
}
|
|
562
|
-
} catch {
|
|
563
|
-
console.log(`Please open this URL in your browser: ${url}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Full login flow
|
|
568
|
-
*/
|
|
569
|
-
async login(options) {
|
|
570
|
-
const deviceCode = await this.startDeviceFlow();
|
|
571
|
-
options?.onDeviceCode?.(deviceCode);
|
|
572
|
-
this.openBrowser(deviceCode.verificationUri);
|
|
573
|
-
options?.onWaiting?.();
|
|
574
|
-
const token = await this.pollForToken(
|
|
575
|
-
deviceCode.deviceCode,
|
|
576
|
-
deviceCode.interval,
|
|
577
|
-
deviceCode.expiresIn
|
|
578
|
-
);
|
|
579
|
-
this.saveCredentials(token);
|
|
580
|
-
return token;
|
|
581
|
-
}
|
|
582
|
-
sleep(ms) {
|
|
583
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
// src/cli.ts
|
|
588
|
-
var program = new Command();
|
|
589
|
-
program.name("artyfacts-claude").description("Run Artyfacts tasks with Claude Code").version("1.0.0");
|
|
590
|
-
async function ensureLoggedIn(options = {}) {
|
|
591
|
-
const auth = new DeviceAuth({ baseUrl: options.baseUrl });
|
|
592
|
-
const token = auth.getAccessToken();
|
|
593
|
-
if (token) {
|
|
594
|
-
const creds = auth.getCredentials();
|
|
595
|
-
console.log(chalk.gray(`Logged in as ${creds?.user.email}`));
|
|
596
|
-
return token;
|
|
597
|
-
}
|
|
598
|
-
console.log(chalk.blue("\u{1F517} Opening browser to log in...\n"));
|
|
599
|
-
try {
|
|
600
|
-
const result = await auth.login({
|
|
601
|
-
onDeviceCode: (response) => {
|
|
602
|
-
console.log(chalk.white(` ${response.verificationUri}`));
|
|
603
|
-
console.log();
|
|
604
|
-
console.log(chalk.gray(` Your code: ${chalk.bold(response.userCode)}`));
|
|
605
|
-
console.log();
|
|
606
|
-
},
|
|
607
|
-
onWaiting: () => {
|
|
608
|
-
console.log(chalk.gray(" Waiting for authorization..."));
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
console.log();
|
|
612
|
-
console.log(chalk.green(`\u2713 Logged in as ${result.user.email}`));
|
|
613
|
-
return result.accessToken;
|
|
614
|
-
} catch (error) {
|
|
615
|
-
throw new Error(`Login failed: ${error instanceof Error ? error.message : error}`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
program.command("start", { isDefault: true }).description("Start the adapter (polls for tasks and executes with Claude Code)").option("-u, --base-url <url>", "Artyfacts API URL", "https://artyfacts.dev").option("-a, --agent-id <id>", "Agent ID for attribution", "claude-agent").option("-i, --interval <sec>", "Poll interval in seconds", "30").option("-c, --concurrent <n>", "Max concurrent tasks", "1").option("-m, --model <model>", "Claude model to use", "sonnet").option("-d, --cwd <dir>", "Working directory", process.cwd()).action(async (options) => {
|
|
619
|
-
console.log(chalk.blue("\u{1F680} Artyfacts + Claude Code\n"));
|
|
620
|
-
let apiKey;
|
|
621
|
-
try {
|
|
622
|
-
apiKey = await ensureLoggedIn({ baseUrl: options.baseUrl });
|
|
623
|
-
} catch (error) {
|
|
624
|
-
console.error(chalk.red(`${error instanceof Error ? error.message : error}`));
|
|
625
|
-
process.exit(1);
|
|
626
|
-
}
|
|
627
|
-
console.log();
|
|
628
|
-
console.log(chalk.gray("Checking Claude Code..."));
|
|
629
|
-
const runner = new ClaudeRunner();
|
|
630
|
-
const check = await runner.checkInstalled();
|
|
631
|
-
if (!check.installed) {
|
|
632
|
-
console.error(chalk.red("\u2717 Claude Code not found\n"));
|
|
633
|
-
console.error(chalk.white(" Install it:"));
|
|
634
|
-
console.error(chalk.cyan(" npm install -g @anthropic-ai/claude-code\n"));
|
|
635
|
-
process.exit(1);
|
|
636
|
-
}
|
|
637
|
-
if (!check.authenticated) {
|
|
638
|
-
console.error(chalk.red("\u2717 Claude Code not authenticated\n"));
|
|
639
|
-
console.error(chalk.white(" Run:"));
|
|
640
|
-
console.error(chalk.cyan(" claude login\n"));
|
|
641
|
-
process.exit(1);
|
|
642
|
-
}
|
|
643
|
-
console.log(chalk.green(`\u2713 Claude Code ${check.version}`));
|
|
644
|
-
console.log();
|
|
645
|
-
const adapter = new ClaudeAdapter({
|
|
646
|
-
apiKey,
|
|
647
|
-
baseUrl: `${options.baseUrl}/api/v1`,
|
|
648
|
-
agentId: options.agentId,
|
|
649
|
-
pollIntervalMs: parseInt(options.interval) * 1e3,
|
|
650
|
-
maxConcurrent: parseInt(options.concurrent),
|
|
651
|
-
model: options.model,
|
|
652
|
-
cwd: options.cwd
|
|
653
|
-
});
|
|
654
|
-
adapter.on("started", () => {
|
|
655
|
-
console.log(chalk.green("\u2713 Started"));
|
|
656
|
-
console.log(chalk.gray(` Polling every ${options.interval}s`));
|
|
657
|
-
console.log(chalk.gray(` Model: ${options.model}`));
|
|
658
|
-
console.log();
|
|
659
|
-
console.log(chalk.blue("Waiting for tasks from artyfacts.dev..."));
|
|
660
|
-
console.log(chalk.gray("Create goals and tasks at https://artyfacts.dev\n"));
|
|
661
|
-
});
|
|
662
|
-
adapter.on("poll", (tasks) => {
|
|
663
|
-
if (tasks.length > 0) {
|
|
664
|
-
console.log(chalk.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Found ${tasks.length} task(s)`));
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
adapter.on("task:claimed", (task) => {
|
|
668
|
-
console.log(chalk.yellow(`
|
|
669
|
-
\u25B6 ${task.heading}`));
|
|
670
|
-
console.log(chalk.gray(` From: ${task.artifactTitle}`));
|
|
671
|
-
});
|
|
672
|
-
adapter.on("task:running", () => {
|
|
673
|
-
console.log(chalk.blue(` Running with Claude...`));
|
|
674
|
-
});
|
|
675
|
-
adapter.on("task:completed", (task, result) => {
|
|
676
|
-
console.log(chalk.green(`\u2713 Completed`));
|
|
677
|
-
console.log(chalk.gray(` Duration: ${(result.durationMs / 1e3).toFixed(1)}s
|
|
678
|
-
`));
|
|
679
|
-
});
|
|
680
|
-
adapter.on("task:failed", (task, error) => {
|
|
681
|
-
console.log(chalk.red(`\u2717 Failed: ${error}
|
|
682
|
-
`));
|
|
683
171
|
});
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
console.log(chalk.gray("\nShutting down..."));
|
|
689
|
-
await adapter.stop();
|
|
690
|
-
console.log(chalk.green("Stopped"));
|
|
172
|
+
listener.connect();
|
|
173
|
+
const shutdown = () => {
|
|
174
|
+
console.log("\n\u{1F44B} Disconnecting...");
|
|
175
|
+
listener.disconnect();
|
|
691
176
|
process.exit(0);
|
|
692
177
|
};
|
|
693
178
|
process.on("SIGINT", shutdown);
|
|
694
179
|
process.on("SIGTERM", shutdown);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
auth.logout();
|
|
716
|
-
console.log(chalk.green("\u2713 Logged out"));
|
|
717
|
-
});
|
|
718
|
-
program.command("status").description("Check login and Claude Code status").action(async () => {
|
|
719
|
-
console.log(chalk.blue("Artyfacts + Claude Code Status\n"));
|
|
720
|
-
const auth = new DeviceAuth();
|
|
721
|
-
const creds = auth.getCredentials();
|
|
722
|
-
if (creds) {
|
|
723
|
-
console.log(chalk.green(`\u2713 Artyfacts: ${creds.user.email}`));
|
|
724
|
-
} else {
|
|
725
|
-
console.log(chalk.yellow("\u25CB Artyfacts: Not logged in"));
|
|
726
|
-
console.log(chalk.gray(" Run: npx @artyfacts/claude login"));
|
|
727
|
-
}
|
|
728
|
-
const runner = new ClaudeRunner();
|
|
729
|
-
const check = await runner.checkInstalled();
|
|
730
|
-
if (!check.installed) {
|
|
731
|
-
console.log(chalk.red("\u2717 Claude Code: Not installed"));
|
|
732
|
-
console.log(chalk.gray(" Run: npm install -g @anthropic-ai/claude-code"));
|
|
733
|
-
} else if (!check.authenticated) {
|
|
734
|
-
console.log(chalk.yellow(`\u25CB Claude Code: ${check.version} (not authenticated)`));
|
|
735
|
-
console.log(chalk.gray(" Run: claude login"));
|
|
736
|
-
} else {
|
|
737
|
-
console.log(chalk.green(`\u2713 Claude Code: ${check.version}`));
|
|
738
|
-
}
|
|
739
|
-
console.log();
|
|
740
|
-
if (creds && check.installed && check.authenticated) {
|
|
741
|
-
console.log(chalk.green("Ready! Run: npx @artyfacts/claude"));
|
|
180
|
+
await new Promise(() => {
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function completeTask(options) {
|
|
184
|
+
const response = await fetch(`${options.baseUrl}/tasks/${options.taskId}/complete`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Authorization": `Bearer ${options.apiKey}`,
|
|
188
|
+
"Content-Type": "application/json"
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
task_status: "done",
|
|
192
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
summary: options.summary,
|
|
194
|
+
output: options.output
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const error = await response.text();
|
|
199
|
+
throw new Error(`Failed to complete task: ${error}`);
|
|
742
200
|
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
201
|
+
}
|
|
202
|
+
async function blockTask(options) {
|
|
203
|
+
const response = await fetch(`${options.baseUrl}/tasks/${options.taskId}/block`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"Authorization": `Bearer ${options.apiKey}`,
|
|
207
|
+
"Content-Type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
task_status: "blocked",
|
|
211
|
+
blocked_reason: options.reason
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
const error = await response.text();
|
|
216
|
+
throw new Error(`Failed to block task: ${error}`);
|
|
754
217
|
}
|
|
755
|
-
}
|
|
218
|
+
}
|
|
756
219
|
program.parse();
|