@locusai/sdk 0.4.5 → 0.4.9

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.
Files changed (58) hide show
  1. package/dist/index-node.js +1590 -20
  2. package/dist/index.js +429 -121
  3. package/dist/orchestrator.d.ts.map +1 -1
  4. package/package.json +12 -23
  5. package/dist/agent/artifact-syncer.js +0 -77
  6. package/dist/agent/codebase-indexer-service.js +0 -55
  7. package/dist/agent/index.js +0 -5
  8. package/dist/agent/sprint-planner.js +0 -68
  9. package/dist/agent/task-executor.js +0 -60
  10. package/dist/agent/worker.js +0 -252
  11. package/dist/ai/anthropic-client.js +0 -70
  12. package/dist/ai/claude-runner.js +0 -71
  13. package/dist/ai/index.js +0 -2
  14. package/dist/core/config.js +0 -15
  15. package/dist/core/index.js +0 -3
  16. package/dist/core/indexer.js +0 -113
  17. package/dist/core/prompt-builder.js +0 -83
  18. package/dist/events.js +0 -15
  19. package/dist/modules/auth.js +0 -23
  20. package/dist/modules/base.js +0 -8
  21. package/dist/modules/ci.js +0 -7
  22. package/dist/modules/docs.js +0 -38
  23. package/dist/modules/invitations.js +0 -22
  24. package/dist/modules/organizations.js +0 -39
  25. package/dist/modules/sprints.js +0 -34
  26. package/dist/modules/tasks.js +0 -56
  27. package/dist/modules/workspaces.js +0 -49
  28. package/dist/orchestrator.js +0 -356
  29. package/dist/utils/colors.js +0 -54
  30. package/dist/utils/retry.js +0 -37
  31. package/src/agent/artifact-syncer.ts +0 -111
  32. package/src/agent/codebase-indexer-service.ts +0 -71
  33. package/src/agent/index.ts +0 -5
  34. package/src/agent/sprint-planner.ts +0 -86
  35. package/src/agent/task-executor.ts +0 -85
  36. package/src/agent/worker.ts +0 -322
  37. package/src/ai/anthropic-client.ts +0 -93
  38. package/src/ai/claude-runner.ts +0 -86
  39. package/src/ai/index.ts +0 -2
  40. package/src/core/config.ts +0 -21
  41. package/src/core/index.ts +0 -3
  42. package/src/core/indexer.ts +0 -131
  43. package/src/core/prompt-builder.ts +0 -91
  44. package/src/events.ts +0 -35
  45. package/src/index-node.ts +0 -23
  46. package/src/index.ts +0 -159
  47. package/src/modules/auth.ts +0 -48
  48. package/src/modules/base.ts +0 -9
  49. package/src/modules/ci.ts +0 -12
  50. package/src/modules/docs.ts +0 -84
  51. package/src/modules/invitations.ts +0 -45
  52. package/src/modules/organizations.ts +0 -90
  53. package/src/modules/sprints.ts +0 -69
  54. package/src/modules/tasks.ts +0 -110
  55. package/src/modules/workspaces.ts +0 -94
  56. package/src/orchestrator.ts +0 -473
  57. package/src/utils/colors.ts +0 -63
  58. package/src/utils/retry.ts +0 -56
@@ -1,473 +0,0 @@
1
- import { ChildProcess, spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { Task, TaskPriority, TaskStatus } from "@locusai/shared";
5
- import { EventEmitter } from "events";
6
- import { LocusClient } from "./index.js";
7
- import { c } from "./utils/colors.js";
8
-
9
- export interface AgentConfig {
10
- id: string;
11
- maxConcurrentTasks: number;
12
- }
13
-
14
- export interface AgentState {
15
- id: string;
16
- status: "IDLE" | "WORKING" | "COMPLETED" | "FAILED";
17
- currentTaskId: string | null;
18
- tasksCompleted: number;
19
- tasksFailed: number;
20
- lastHeartbeat: Date;
21
- process?: ChildProcess;
22
- }
23
-
24
- export interface OrchestratorConfig {
25
- workspaceId: string;
26
- sprintId: string;
27
- apiBase: string;
28
- maxIterations: number;
29
- projectPath: string;
30
- apiKey: string;
31
- anthropicApiKey?: string;
32
- model?: string;
33
- }
34
-
35
- export class AgentOrchestrator extends EventEmitter {
36
- private client: LocusClient;
37
- private config: OrchestratorConfig;
38
- private agents: Map<string, AgentState> = new Map();
39
- private isRunning = false;
40
- private processedTasks: Set<string> = new Set();
41
- private resolvedSprintId: string | null = null;
42
-
43
- constructor(config: OrchestratorConfig) {
44
- super();
45
- this.config = config;
46
- this.client = new LocusClient({
47
- baseUrl: config.apiBase,
48
- token: config.apiKey,
49
- });
50
- }
51
-
52
- /**
53
- * Resolve the sprint ID - use provided or find active sprint
54
- */
55
- private async resolveSprintId(): Promise<string> {
56
- if (this.config.sprintId) {
57
- return this.config.sprintId;
58
- }
59
-
60
- // Try to find active sprint in workspace
61
- try {
62
- const sprint = await this.client.sprints.getActive(
63
- this.config.workspaceId
64
- );
65
- if (sprint?.id) {
66
- console.log(c.info(`📋 Using active sprint: ${sprint.name}`));
67
- return sprint.id;
68
- }
69
- } catch {
70
- // No active sprint found, will work with all tasks
71
- }
72
-
73
- console.log(
74
- c.dim("ℹ No sprint specified, working with all workspace tasks")
75
- );
76
- return "";
77
- }
78
-
79
- /**
80
- * Start the orchestrator with N agents
81
- */
82
- async start(): Promise<void> {
83
- if (this.isRunning) {
84
- throw new Error("Orchestrator is already running");
85
- }
86
-
87
- this.isRunning = true;
88
- this.processedTasks.clear();
89
-
90
- try {
91
- await this.orchestrationLoop();
92
- } catch (error) {
93
- this.emit("error", error);
94
- throw error;
95
- } finally {
96
- await this.cleanup();
97
- }
98
- }
99
-
100
- /**
101
- * Main orchestration loop - runs 1 agent continuously
102
- */
103
- private async orchestrationLoop(): Promise<void> {
104
- // Resolve sprint ID first
105
- this.resolvedSprintId = await this.resolveSprintId();
106
-
107
- this.emit("started", {
108
- timestamp: new Date(),
109
- config: this.config,
110
- sprintId: this.resolvedSprintId,
111
- });
112
-
113
- console.log(`\n${c.primary("🤖 Locus Agent Orchestrator")}`);
114
- console.log(c.dim("----------------------------------------------"));
115
- console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
116
- if (this.resolvedSprintId) {
117
- console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
118
- }
119
- console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
120
- console.log(c.dim("----------------------------------------------\n"));
121
-
122
- // Check if there are tasks to work on before spawning
123
- const tasks = await this.getAvailableTasks();
124
-
125
- if (tasks.length === 0) {
126
- console.log(c.dim("ℹ No available tasks found in the backlog."));
127
- return;
128
- }
129
-
130
- // Spawn single agent
131
- await this.spawnAgent();
132
-
133
- // Wait for agent to complete
134
- while (this.agents.size > 0 && this.isRunning) {
135
- await this.reapAgents();
136
-
137
- if (this.agents.size === 0) {
138
- break;
139
- }
140
-
141
- await this.sleep(2000);
142
- }
143
-
144
- console.log(`\n${c.success("✅ Orchestrator finished")}`);
145
- }
146
-
147
- /**
148
- * Find the package root by looking for package.json
149
- */
150
- private findPackageRoot(startPath: string): string {
151
- let currentDir = startPath;
152
- while (currentDir !== "/") {
153
- if (existsSync(join(currentDir, "package.json"))) {
154
- return currentDir;
155
- }
156
- currentDir = dirname(currentDir);
157
- }
158
- // Fallback to startPath if not found
159
- return startPath;
160
- }
161
-
162
- /**
163
- * Spawn a single agent process
164
- */
165
- private async spawnAgent(): Promise<void> {
166
- const agentId = `agent-${Date.now()}-${Math.random()
167
- .toString(36)
168
- .slice(2, 9)}`;
169
-
170
- const agentState: AgentState = {
171
- id: agentId,
172
- status: "IDLE",
173
- currentTaskId: null,
174
- tasksCompleted: 0,
175
- tasksFailed: 0,
176
- lastHeartbeat: new Date(),
177
- };
178
-
179
- this.agents.set(agentId, agentState);
180
-
181
- console.log(`${c.primary("🚀 Agent started:")} ${c.bold(agentId)}\n`);
182
-
183
- // Build arguments for agent worker
184
- // Try multiple resolution strategies
185
- const potentialPaths: string[] = [];
186
-
187
- // Strategy 1: Use import.meta.resolve to find the installed SDK package
188
- try {
189
- // Resolve the SDK's index to find the package location
190
- const sdkIndexPath = import.meta.resolve("@locusai/sdk");
191
- const sdkDir = dirname(sdkIndexPath.replace("file://", ""));
192
- // In production, files are in dist/; sdkDir points to dist/ or src/
193
- const sdkRoot = this.findPackageRoot(sdkDir);
194
- potentialPaths.push(
195
- join(sdkRoot, "dist", "agent", "worker.js"),
196
- join(sdkRoot, "src", "agent", "worker.ts")
197
- );
198
- } catch {
199
- // import.meta.resolve failed, continue with fallback strategies
200
- }
201
-
202
- // Strategy 2: Find package root from __dirname (works in dev/local)
203
- const packageRoot = this.findPackageRoot(__dirname);
204
- potentialPaths.push(
205
- join(packageRoot, "dist", "agent", "worker.js"),
206
- join(packageRoot, "src", "agent", "worker.ts"),
207
- join(__dirname, "agent", "worker.ts"),
208
- join(__dirname, "agent", "worker.js")
209
- );
210
-
211
- const workerPath = potentialPaths.find((p) => existsSync(p));
212
-
213
- // Verify worker file exists
214
- if (!workerPath) {
215
- throw new Error(
216
- `Worker file not found. Checked: ${potentialPaths.join(", ")}. ` +
217
- `Make sure the SDK is properly built and installed.`
218
- );
219
- }
220
-
221
- const workerArgs = [
222
- "--agent-id",
223
- agentId,
224
- "--workspace-id",
225
- this.config.workspaceId,
226
- "--api-base",
227
- this.config.apiBase,
228
- "--api-key",
229
- this.config.apiKey,
230
- "--project-path",
231
- this.config.projectPath,
232
- ];
233
-
234
- // Add anthropic API key if provided
235
- if (this.config.anthropicApiKey) {
236
- workerArgs.push("--anthropic-api-key", this.config.anthropicApiKey);
237
- }
238
-
239
- // Add model if specified
240
- if (this.config.model) {
241
- workerArgs.push("--model", this.config.model);
242
- }
243
-
244
- // Add sprint ID if resolved
245
- if (this.resolvedSprintId) {
246
- workerArgs.push("--sprint-id", this.resolvedSprintId);
247
- }
248
-
249
- // Use node to run the worker script
250
- const agentProcess = spawn(process.execPath, [workerPath, ...workerArgs]);
251
-
252
- agentState.process = agentProcess;
253
-
254
- agentProcess.on("message", (msg: Record<string, unknown>) => {
255
- if (msg.type === "stats") {
256
- agentState.tasksCompleted = (msg.tasksCompleted as number) || 0;
257
- agentState.tasksFailed = (msg.tasksFailed as number) || 0;
258
- }
259
- });
260
-
261
- agentProcess.stdout?.on("data", (data) => {
262
- process.stdout.write(data.toString());
263
- });
264
-
265
- agentProcess.stderr?.on("data", (data) => {
266
- process.stderr.write(`[${agentId}] ERR: ${data.toString()}`);
267
- });
268
-
269
- agentProcess.on("exit", (code) => {
270
- console.log(`\n${agentId} finished (exit code: ${code})`);
271
- const agent = this.agents.get(agentId);
272
- if (agent) {
273
- agent.status = code === 0 ? "COMPLETED" : "FAILED";
274
-
275
- // Ensure CLI gets the absolute latest stats
276
- this.emit("agent:completed", {
277
- agentId,
278
- status: agent.status,
279
- tasksCompleted: agent.tasksCompleted,
280
- tasksFailed: agent.tasksFailed,
281
- });
282
-
283
- // Remove from active tracking after emitting
284
- this.agents.delete(agentId);
285
- }
286
- });
287
-
288
- this.emit("agent:spawned", { agentId });
289
- }
290
-
291
- /**
292
- * Reap completed agents
293
- */
294
- private async reapAgents(): Promise<void> {
295
- // No-op: agents now remove themselves in the 'exit' listener
296
- // to ensure events are emitted with correct stats before deletion.
297
- }
298
-
299
- /**
300
- * Get available tasks in sprint
301
- */
302
- private async getAvailableTasks(): Promise<Task[]> {
303
- try {
304
- const tasks = await this.client.tasks.getAvailable(
305
- this.config.workspaceId,
306
- this.resolvedSprintId || undefined
307
- );
308
-
309
- return tasks.filter((task) => !this.processedTasks.has(task.id));
310
- } catch (error) {
311
- this.emit("error", error);
312
- return [];
313
- }
314
- }
315
-
316
- /**
317
- * Assign task to agent
318
- */
319
- async assignTaskToAgent(agentId: string): Promise<Task | null> {
320
- const agent = this.agents.get(agentId);
321
- if (!agent) return null;
322
-
323
- try {
324
- const tasks = await this.getAvailableTasks();
325
-
326
- const priorityOrder = [
327
- TaskPriority.CRITICAL,
328
- TaskPriority.HIGH,
329
- TaskPriority.MEDIUM,
330
- TaskPriority.LOW,
331
- ];
332
-
333
- // Find task with highest priority
334
- let task = tasks.sort(
335
- (a, b) =>
336
- priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority)
337
- )[0];
338
-
339
- // Fallback: any available task
340
- if (!task && tasks.length > 0) {
341
- task = tasks[0];
342
- }
343
-
344
- if (!task) return null;
345
-
346
- agent.currentTaskId = task.id;
347
- agent.status = "WORKING";
348
-
349
- this.emit("task:assigned", {
350
- agentId,
351
- taskId: task.id,
352
- title: task.title,
353
- });
354
-
355
- return task;
356
- } catch (error) {
357
- this.emit("error", error);
358
- return null;
359
- }
360
- }
361
-
362
- /**
363
- * Mark task as completed by agent
364
- */
365
- async completeTask(
366
- taskId: string,
367
- agentId: string,
368
- summary?: string
369
- ): Promise<void> {
370
- try {
371
- await this.client.tasks.update(taskId, this.config.workspaceId, {
372
- status: TaskStatus.VERIFICATION,
373
- });
374
-
375
- if (summary) {
376
- await this.client.tasks.addComment(taskId, this.config.workspaceId, {
377
- author: agentId,
378
- text: `✅ Task completed\n\n${summary}`,
379
- });
380
- }
381
-
382
- this.processedTasks.add(taskId);
383
-
384
- const agent = this.agents.get(agentId);
385
- if (agent) {
386
- agent.tasksCompleted += 1;
387
- agent.currentTaskId = null;
388
- agent.status = "IDLE";
389
- }
390
-
391
- this.emit("task:completed", { agentId, taskId });
392
- } catch (error) {
393
- this.emit("error", error);
394
- }
395
- }
396
-
397
- /**
398
- * Mark task as failed
399
- */
400
- async failTask(
401
- taskId: string,
402
- agentId: string,
403
- error: string
404
- ): Promise<void> {
405
- try {
406
- await this.client.tasks.update(taskId, this.config.workspaceId, {
407
- status: TaskStatus.BACKLOG,
408
- assignedTo: null,
409
- });
410
-
411
- await this.client.tasks.addComment(taskId, this.config.workspaceId, {
412
- author: agentId,
413
- text: `❌ Agent failed: ${error}`,
414
- });
415
-
416
- const agent = this.agents.get(agentId);
417
- if (agent) {
418
- agent.tasksFailed += 1;
419
- agent.currentTaskId = null;
420
- agent.status = "IDLE";
421
- }
422
-
423
- this.emit("task:failed", { agentId, taskId, error });
424
- } catch (error) {
425
- this.emit("error", error);
426
- }
427
- }
428
-
429
- /**
430
- * Stop orchestrator
431
- */
432
- async stop(): Promise<void> {
433
- this.isRunning = false;
434
- await this.cleanup();
435
- this.emit("stopped", { timestamp: new Date() });
436
- }
437
-
438
- /**
439
- * Cleanup - kill all agent processes
440
- */
441
- private async cleanup(): Promise<void> {
442
- for (const [agentId, agent] of this.agents.entries()) {
443
- if (agent.process && !agent.process.killed) {
444
- console.log(`Killing agent: ${agentId}`);
445
- agent.process.kill();
446
- }
447
- }
448
-
449
- this.agents.clear();
450
- }
451
-
452
- /**
453
- * Get orchestrator stats
454
- */
455
- getStats() {
456
- return {
457
- activeAgents: this.agents.size,
458
- processedTasks: this.processedTasks.size,
459
- totalTasksCompleted: Array.from(this.agents.values()).reduce(
460
- (sum, agent) => sum + agent.tasksCompleted,
461
- 0
462
- ),
463
- totalTasksFailed: Array.from(this.agents.values()).reduce(
464
- (sum, agent) => sum + agent.tasksFailed,
465
- 0
466
- ),
467
- };
468
- }
469
-
470
- private sleep(ms: number): Promise<void> {
471
- return new Promise((resolve) => setTimeout(resolve, ms));
472
- }
473
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * Simple ANSI color utility for terminal output
3
- * Dependency-free and works in Node.js/Bun environments
4
- */
5
-
6
- const ESC = "\u001b[";
7
- const RESET = `${ESC}0m`;
8
-
9
- const colors = {
10
- reset: RESET,
11
- bold: `${ESC}1m`,
12
- dim: `${ESC}2m`,
13
- italic: `${ESC}3m`,
14
- underline: `${ESC}4m`,
15
-
16
- // Foreground colors
17
- black: `${ESC}30m`,
18
- red: `${ESC}31m`,
19
- green: `${ESC}32m`,
20
- yellow: `${ESC}33m`,
21
- blue: `${ESC}34m`,
22
- magenta: `${ESC}35m`,
23
- cyan: `${ESC}36m`,
24
- white: `${ESC}37m`,
25
- gray: `${ESC}90m`,
26
-
27
- // Foreground bright colors
28
- brightRed: `${ESC}91m`,
29
- brightGreen: `${ESC}92m`,
30
- brightYellow: `${ESC}93m`,
31
- brightBlue: `${ESC}94m`,
32
- brightMagenta: `${ESC}95m`,
33
- brightCyan: `${ESC}96m`,
34
- brightWhite: `${ESC}97m`,
35
- };
36
-
37
- type ColorName = keyof typeof colors;
38
-
39
- export const c = {
40
- text: (text: string, ...colorNames: ColorName[]) => {
41
- const codes = colorNames.map((name) => colors[name]).join("");
42
- return `${codes}${text}${RESET}`;
43
- },
44
-
45
- // Shortcuts
46
- bold: (t: string) => c.text(t, "bold"),
47
- dim: (t: string) => c.text(t, "dim"),
48
- red: (t: string) => c.text(t, "red"),
49
- green: (t: string) => c.text(t, "green"),
50
- yellow: (t: string) => c.text(t, "yellow"),
51
- blue: (t: string) => c.text(t, "blue"),
52
- magenta: (t: string) => c.text(t, "magenta"),
53
- cyan: (t: string) => c.text(t, "cyan"),
54
- gray: (t: string) => c.text(t, "gray"),
55
-
56
- // Combinations
57
- success: (t: string) => c.text(t, "green", "bold"),
58
- error: (t: string) => c.text(t, "red", "bold"),
59
- warning: (t: string) => c.text(t, "yellow", "bold"),
60
- info: (t: string) => c.text(t, "cyan", "bold"),
61
- primary: (t: string) => c.text(t, "blue", "bold"),
62
- underline: (t: string) => c.text(t, "underline"),
63
- };
@@ -1,56 +0,0 @@
1
- import { isAxiosError } from "axios";
2
-
3
- export interface RetryOptions {
4
- maxRetries?: number;
5
- initialDelay?: number;
6
- maxDelay?: number;
7
- factor?: number;
8
- retryCondition?: (error: unknown) => boolean;
9
- }
10
-
11
- export const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
12
- maxRetries: 3,
13
- initialDelay: 1000,
14
- maxDelay: 5000,
15
- factor: 2,
16
- retryCondition: (error: unknown) => {
17
- // Retry on network errors or 5xx server errors
18
- if (isAxiosError(error)) {
19
- if (!error.response) return true; // Network error
20
- return error.response.status >= 500;
21
- }
22
- return true; // Retry on other unknown errors
23
- },
24
- };
25
-
26
- /**
27
- * Retries an async function with exponential backoff
28
- */
29
- export async function withRetry<T>(
30
- fn: () => Promise<T>,
31
- options: RetryOptions = {}
32
- ): Promise<T> {
33
- const config = { ...DEFAULT_RETRY_OPTIONS, ...options };
34
- let lastError: unknown;
35
-
36
- for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
37
- try {
38
- return await fn();
39
- } catch (error: unknown) {
40
- lastError = error;
41
-
42
- if (attempt === config.maxRetries || !config.retryCondition(error)) {
43
- throw error;
44
- }
45
-
46
- const delay = Math.min(
47
- config.initialDelay * Math.pow(config.factor, attempt),
48
- config.maxDelay
49
- );
50
-
51
- await new Promise((resolve) => setTimeout(resolve, delay));
52
- }
53
- }
54
-
55
- throw lastError;
56
- }