@locusai/sdk 0.2.1
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/agent/artifact-syncer.d.ts +17 -0
- package/dist/agent/artifact-syncer.d.ts.map +1 -0
- package/dist/agent/artifact-syncer.js +77 -0
- package/dist/agent/codebase-indexer-service.d.ts +18 -0
- package/dist/agent/codebase-indexer-service.d.ts.map +1 -0
- package/dist/agent/codebase-indexer-service.js +55 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +5 -0
- package/dist/agent/sprint-planner.d.ts +17 -0
- package/dist/agent/sprint-planner.d.ts.map +1 -0
- package/dist/agent/sprint-planner.js +62 -0
- package/dist/agent/task-executor.d.ts +24 -0
- package/dist/agent/task-executor.d.ts.map +1 -0
- package/dist/agent/task-executor.js +56 -0
- package/dist/agent/worker.d.ts +37 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +232 -0
- package/dist/ai/anthropic-client.d.ts +33 -0
- package/dist/ai/anthropic-client.d.ts.map +1 -0
- package/dist/ai/anthropic-client.js +70 -0
- package/dist/ai/claude-runner.d.ts +7 -0
- package/dist/ai/claude-runner.d.ts.map +1 -0
- package/dist/ai/claude-runner.js +43 -0
- package/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +2 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +15 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/indexer.d.ts +18 -0
- package/dist/core/indexer.d.ts.map +1 -0
- package/dist/core/indexer.js +73 -0
- package/dist/core/prompt-builder.d.ts +8 -0
- package/dist/core/prompt-builder.d.ts.map +1 -0
- package/dist/core/prompt-builder.js +87 -0
- package/dist/events.d.ts +20 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +15 -0
- package/dist/index-node.d.ts +14 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index-node.js +18 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/modules/auth.d.ts +14 -0
- package/dist/modules/auth.d.ts.map +1 -0
- package/dist/modules/auth.js +23 -0
- package/dist/modules/base.d.ts +8 -0
- package/dist/modules/base.d.ts.map +1 -0
- package/dist/modules/base.js +8 -0
- package/dist/modules/ci.d.ts +8 -0
- package/dist/modules/ci.d.ts.map +1 -0
- package/dist/modules/ci.js +7 -0
- package/dist/modules/docs.d.ts +14 -0
- package/dist/modules/docs.d.ts.map +1 -0
- package/dist/modules/docs.js +38 -0
- package/dist/modules/invitations.d.ts +10 -0
- package/dist/modules/invitations.d.ts.map +1 -0
- package/dist/modules/invitations.js +22 -0
- package/dist/modules/organizations.d.ts +24 -0
- package/dist/modules/organizations.d.ts.map +1 -0
- package/dist/modules/organizations.js +39 -0
- package/dist/modules/sprints.d.ts +13 -0
- package/dist/modules/sprints.d.ts.map +1 -0
- package/dist/modules/sprints.js +34 -0
- package/dist/modules/tasks.d.ts +24 -0
- package/dist/modules/tasks.d.ts.map +1 -0
- package/dist/modules/tasks.js +56 -0
- package/dist/modules/workspaces.d.ts +21 -0
- package/dist/modules/workspaces.d.ts.map +1 -0
- package/dist/modules/workspaces.js +49 -0
- package/dist/orchestrator.d.ts +90 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +326 -0
- package/package.json +53 -0
- package/src/agent/artifact-syncer.ts +111 -0
- package/src/agent/codebase-indexer-service.ts +71 -0
- package/src/agent/index.ts +5 -0
- package/src/agent/sprint-planner.ts +78 -0
- package/src/agent/task-executor.ts +77 -0
- package/src/agent/worker.ts +299 -0
- package/src/ai/anthropic-client.ts +93 -0
- package/src/ai/claude-runner.ts +49 -0
- package/src/ai/index.ts +2 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +3 -0
- package/src/core/indexer.ts +91 -0
- package/src/core/prompt-builder.ts +100 -0
- package/src/events.ts +32 -0
- package/src/index-node.ts +20 -0
- package/src/index.ts +119 -0
- package/src/modules/auth.ts +48 -0
- package/src/modules/base.ts +9 -0
- package/src/modules/ci.ts +12 -0
- package/src/modules/docs.ts +84 -0
- package/src/modules/invitations.ts +45 -0
- package/src/modules/organizations.ts +90 -0
- package/src/modules/sprints.ts +69 -0
- package/src/modules/tasks.ts +110 -0
- package/src/modules/workspaces.ts +94 -0
- package/src/orchestrator.ts +430 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Task } from "@locusai/shared";
|
|
2
|
+
import type { AnthropicClient } from "../ai/anthropic-client";
|
|
3
|
+
import type { ClaudeRunner } from "../ai/claude-runner";
|
|
4
|
+
import { PromptBuilder } from "../core/prompt-builder";
|
|
5
|
+
|
|
6
|
+
export interface TaskExecutorDeps {
|
|
7
|
+
anthropicClient: AnthropicClient | null;
|
|
8
|
+
claudeRunner: ClaudeRunner;
|
|
9
|
+
projectPath: string;
|
|
10
|
+
sprintPlan: string | null;
|
|
11
|
+
log: (message: string, level?: "info" | "success" | "warn" | "error") => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles task execution with two-phase approach (planning + execution)
|
|
16
|
+
*/
|
|
17
|
+
export class TaskExecutor {
|
|
18
|
+
private promptBuilder: PromptBuilder;
|
|
19
|
+
|
|
20
|
+
constructor(private deps: TaskExecutorDeps) {
|
|
21
|
+
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
updateSprintPlan(sprintPlan: string | null) {
|
|
25
|
+
this.deps.sprintPlan = sprintPlan;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async execute(task: Task): Promise<{ success: boolean; summary: string }> {
|
|
29
|
+
this.deps.log(`Executing: ${task.title}`, "info");
|
|
30
|
+
let basePrompt = await this.promptBuilder.build(task);
|
|
31
|
+
|
|
32
|
+
if (this.deps.sprintPlan) {
|
|
33
|
+
basePrompt = `## Sprint Context\n${this.deps.sprintPlan}\n\n${basePrompt}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
let plan: string;
|
|
38
|
+
|
|
39
|
+
if (this.deps.anthropicClient) {
|
|
40
|
+
// Phase 1: Planning (using Anthropic SDK with caching)
|
|
41
|
+
this.deps.log("Phase 1: Planning (Anthropic SDK)...", "info");
|
|
42
|
+
|
|
43
|
+
// Build cacheable context blocks
|
|
44
|
+
const cacheableContext: string[] = [basePrompt];
|
|
45
|
+
|
|
46
|
+
plan = await this.deps.anthropicClient.run({
|
|
47
|
+
systemPrompt:
|
|
48
|
+
"You are an expert software engineer. Analyze the task carefully and create a detailed implementation plan.",
|
|
49
|
+
cacheableContext,
|
|
50
|
+
userPrompt:
|
|
51
|
+
"## Phase 1: Planning\nAnalyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.",
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
// Phase 1: Planning (using Claude CLI)
|
|
55
|
+
this.deps.log("Phase 1: Planning (Claude CLI)...", "info");
|
|
56
|
+
const planningPrompt = `${basePrompt}\n\n## Phase 1: Planning\nAnalyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
|
|
57
|
+
plan = await this.deps.claudeRunner.run(planningPrompt, true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Phase 2: Execution (always using Claude CLI for agentic tools)
|
|
61
|
+
this.deps.log("Plan generated. Starting Phase 2: Execution...", "info");
|
|
62
|
+
const executionPrompt = `${basePrompt}\n\n## Phase 2: Execution\nBased on the plan, execute the task:\n\n${plan}\n\nWhen finished, output: <promise>COMPLETE</promise>`;
|
|
63
|
+
const output = await this.deps.claudeRunner.run(executionPrompt);
|
|
64
|
+
|
|
65
|
+
const success = output.includes("<promise>COMPLETE</promise>");
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success,
|
|
69
|
+
summary: success
|
|
70
|
+
? "Task completed by Claude"
|
|
71
|
+
: "Claude did not signal completion",
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { success: false, summary: `Error: ${error}` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { Sprint, Task, TaskStatus } from "@locusai/shared";
|
|
2
|
+
import { AnthropicClient } from "../ai/anthropic-client";
|
|
3
|
+
import { ClaudeRunner } from "../ai/claude-runner";
|
|
4
|
+
import { LocusClient } from "../index";
|
|
5
|
+
import { ArtifactSyncer } from "./artifact-syncer";
|
|
6
|
+
import { CodebaseIndexerService } from "./codebase-indexer-service";
|
|
7
|
+
import { SprintPlanner } from "./sprint-planner";
|
|
8
|
+
import { TaskExecutor } from "./task-executor";
|
|
9
|
+
|
|
10
|
+
export interface WorkerConfig {
|
|
11
|
+
agentId: string;
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
sprintId?: string;
|
|
14
|
+
apiBase: string;
|
|
15
|
+
projectPath: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
anthropicApiKey?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main agent worker that orchestrates task execution
|
|
23
|
+
* Delegates responsibilities to specialized services
|
|
24
|
+
*/
|
|
25
|
+
export class AgentWorker {
|
|
26
|
+
private client: LocusClient;
|
|
27
|
+
private claudeRunner: ClaudeRunner;
|
|
28
|
+
private anthropicClient: AnthropicClient | null;
|
|
29
|
+
|
|
30
|
+
// Services
|
|
31
|
+
private sprintPlanner: SprintPlanner;
|
|
32
|
+
private indexerService: CodebaseIndexerService;
|
|
33
|
+
private artifactSyncer: ArtifactSyncer;
|
|
34
|
+
private taskExecutor: TaskExecutor;
|
|
35
|
+
|
|
36
|
+
// State
|
|
37
|
+
private consecutiveEmpty = 0;
|
|
38
|
+
private maxEmpty = 10;
|
|
39
|
+
private maxTasks = 50;
|
|
40
|
+
private tasksCompleted = 0;
|
|
41
|
+
private pollInterval = 10_000;
|
|
42
|
+
private sprintPlan: string | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(private config: WorkerConfig) {
|
|
45
|
+
const projectPath = config.projectPath || process.cwd();
|
|
46
|
+
|
|
47
|
+
// Initialize API client
|
|
48
|
+
this.client = new LocusClient({
|
|
49
|
+
baseUrl: config.apiBase,
|
|
50
|
+
token: config.apiKey,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Initialize AI clients
|
|
54
|
+
this.claudeRunner = new ClaudeRunner(projectPath, config.model);
|
|
55
|
+
this.anthropicClient = config.anthropicApiKey
|
|
56
|
+
? new AnthropicClient({
|
|
57
|
+
apiKey: config.anthropicApiKey,
|
|
58
|
+
model: config.model,
|
|
59
|
+
})
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
// Initialize services with dependencies
|
|
63
|
+
const logFn = this.log.bind(this);
|
|
64
|
+
|
|
65
|
+
this.sprintPlanner = new SprintPlanner({
|
|
66
|
+
anthropicClient: this.anthropicClient,
|
|
67
|
+
claudeRunner: this.claudeRunner,
|
|
68
|
+
log: logFn,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.indexerService = new CodebaseIndexerService({
|
|
72
|
+
anthropicClient: this.anthropicClient,
|
|
73
|
+
claudeRunner: this.claudeRunner,
|
|
74
|
+
projectPath,
|
|
75
|
+
log: logFn,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.artifactSyncer = new ArtifactSyncer({
|
|
79
|
+
client: this.client,
|
|
80
|
+
workspaceId: config.workspaceId,
|
|
81
|
+
projectPath,
|
|
82
|
+
log: logFn,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.taskExecutor = new TaskExecutor({
|
|
86
|
+
anthropicClient: this.anthropicClient,
|
|
87
|
+
claudeRunner: this.claudeRunner,
|
|
88
|
+
projectPath,
|
|
89
|
+
sprintPlan: null, // Will be set after sprint planning
|
|
90
|
+
log: logFn,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Log initialization
|
|
94
|
+
if (this.anthropicClient) {
|
|
95
|
+
this.log(
|
|
96
|
+
"Using Anthropic SDK with prompt caching for planning phases",
|
|
97
|
+
"info"
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
this.log(
|
|
101
|
+
"Using Claude CLI for all phases (no Anthropic API key provided)",
|
|
102
|
+
"info"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
log(message: string, level: "info" | "success" | "warn" | "error" = "info") {
|
|
108
|
+
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
109
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
110
|
+
console.log(
|
|
111
|
+
`[${timestamp}] [${this.config.agentId.slice(-8)}] ${prefix} ${message}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async getActiveSprint(): Promise<Sprint | null> {
|
|
116
|
+
try {
|
|
117
|
+
if (this.config.sprintId) {
|
|
118
|
+
return await this.client.sprints.getById(
|
|
119
|
+
this.config.sprintId,
|
|
120
|
+
this.config.workspaceId
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return await this.client.sprints.getActive(this.config.workspaceId);
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async getNextTask(): Promise<Task | null> {
|
|
130
|
+
try {
|
|
131
|
+
const task = await this.client.workspaces.dispatch(
|
|
132
|
+
this.config.workspaceId,
|
|
133
|
+
this.config.agentId,
|
|
134
|
+
this.config.sprintId
|
|
135
|
+
);
|
|
136
|
+
return task;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.log(
|
|
139
|
+
`No task dispatched: ${error instanceof Error ? error.message : String(error)}`,
|
|
140
|
+
"info"
|
|
141
|
+
);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async executeTask(
|
|
147
|
+
task: Task
|
|
148
|
+
): Promise<{ success: boolean; summary: string }> {
|
|
149
|
+
// Reindex codebase before execution to ensure fresh context
|
|
150
|
+
await this.indexerService.reindex();
|
|
151
|
+
|
|
152
|
+
// Fetch full task details to get comments/feedback
|
|
153
|
+
const fullTask = await this.client.tasks.getById(
|
|
154
|
+
task.id,
|
|
155
|
+
this.config.workspaceId
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Update task executor with current sprint plan
|
|
159
|
+
this.taskExecutor.updateSprintPlan(this.sprintPlan);
|
|
160
|
+
|
|
161
|
+
// Execute the task
|
|
162
|
+
const result = await this.taskExecutor.execute(fullTask);
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async run(): Promise<void> {
|
|
168
|
+
this.log(
|
|
169
|
+
`Agent started in ${this.config.projectPath || process.cwd()}`,
|
|
170
|
+
"success"
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Initial Sprint Planning Phase
|
|
174
|
+
const sprint = await this.getActiveSprint();
|
|
175
|
+
if (sprint) {
|
|
176
|
+
this.log(`Found active sprint: ${sprint.name} (${sprint.id})`, "info");
|
|
177
|
+
const tasks = await this.client.tasks.list(this.config.workspaceId, {
|
|
178
|
+
sprintId: sprint.id,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.log(`Sprint tasks found: ${tasks.length}`, "info");
|
|
182
|
+
|
|
183
|
+
const latestTaskCreation = tasks.reduce((latest, task) => {
|
|
184
|
+
const taskDate = new Date(task.createdAt);
|
|
185
|
+
return taskDate > latest ? taskDate : latest;
|
|
186
|
+
}, new Date(0));
|
|
187
|
+
|
|
188
|
+
const mindmapDate = sprint.mindmapUpdatedAt
|
|
189
|
+
? new Date(sprint.mindmapUpdatedAt)
|
|
190
|
+
: new Date(0);
|
|
191
|
+
|
|
192
|
+
const needsPlanning =
|
|
193
|
+
!sprint.mindmap ||
|
|
194
|
+
sprint.mindmap.trim() === "" ||
|
|
195
|
+
latestTaskCreation > mindmapDate;
|
|
196
|
+
|
|
197
|
+
if (needsPlanning) {
|
|
198
|
+
if (sprint.mindmap && latestTaskCreation > mindmapDate) {
|
|
199
|
+
this.log(
|
|
200
|
+
"New tasks have been added to the sprint since last mindmap. Regenerating...",
|
|
201
|
+
"warn"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
this.sprintPlan = await this.sprintPlanner.planSprint(sprint, tasks);
|
|
205
|
+
|
|
206
|
+
// Save mindmap to server
|
|
207
|
+
await this.client.sprints.update(sprint.id, this.config.workspaceId, {
|
|
208
|
+
mindmap: this.sprintPlan,
|
|
209
|
+
mindmapUpdatedAt: new Date(),
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
this.log("Using existing sprint mindmap.", "info");
|
|
213
|
+
this.sprintPlan = sprint.mindmap ?? null;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
this.log("No active sprint found for planning.", "warn");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Main task execution loop
|
|
220
|
+
while (
|
|
221
|
+
this.tasksCompleted < this.maxTasks &&
|
|
222
|
+
this.consecutiveEmpty < this.maxEmpty
|
|
223
|
+
) {
|
|
224
|
+
const task = await this.getNextTask();
|
|
225
|
+
if (!task) {
|
|
226
|
+
this.consecutiveEmpty++;
|
|
227
|
+
if (this.consecutiveEmpty >= this.maxEmpty) break;
|
|
228
|
+
await new Promise((r) => setTimeout(r, this.pollInterval));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.consecutiveEmpty = 0;
|
|
233
|
+
this.log(`Claimed: ${task.title}`, "success");
|
|
234
|
+
|
|
235
|
+
const result = await this.executeTask(task);
|
|
236
|
+
|
|
237
|
+
// Sync artifacts after task execution
|
|
238
|
+
await this.artifactSyncer.sync();
|
|
239
|
+
|
|
240
|
+
if (result.success) {
|
|
241
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
242
|
+
status: "VERIFICATION" as TaskStatus,
|
|
243
|
+
});
|
|
244
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
245
|
+
author: this.config.agentId,
|
|
246
|
+
text: `✅ ${result.summary}`,
|
|
247
|
+
});
|
|
248
|
+
this.tasksCompleted++;
|
|
249
|
+
} else {
|
|
250
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
251
|
+
status: "BACKLOG" as TaskStatus,
|
|
252
|
+
assignedTo: null,
|
|
253
|
+
});
|
|
254
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
255
|
+
author: this.config.agentId,
|
|
256
|
+
text: `❌ ${result.summary}`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// CLI entry point
|
|
265
|
+
if (
|
|
266
|
+
process.argv[1]?.includes("agent-worker") ||
|
|
267
|
+
process.argv[1]?.includes("worker")
|
|
268
|
+
) {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const config: Partial<WorkerConfig> = {};
|
|
271
|
+
for (let i = 0; i < args.length; i++) {
|
|
272
|
+
const arg = args[i];
|
|
273
|
+
if (arg === "--agent-id") config.agentId = args[++i];
|
|
274
|
+
else if (arg === "--workspace-id") config.workspaceId = args[++i];
|
|
275
|
+
else if (arg === "--sprint-id") config.sprintId = args[++i];
|
|
276
|
+
else if (arg === "--api-base") config.apiBase = args[++i];
|
|
277
|
+
else if (arg === "--api-key") config.apiKey = args[++i];
|
|
278
|
+
else if (arg === "--anthropic-api-key") config.anthropicApiKey = args[++i];
|
|
279
|
+
else if (arg === "--project-path") config.projectPath = args[++i];
|
|
280
|
+
else if (arg === "--model") config.model = args[++i];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
!config.agentId ||
|
|
285
|
+
!config.workspaceId ||
|
|
286
|
+
!config.apiBase ||
|
|
287
|
+
!config.apiKey ||
|
|
288
|
+
!config.projectPath
|
|
289
|
+
) {
|
|
290
|
+
console.error("Missing required arguments");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const worker = new AgentWorker(config as WorkerConfig);
|
|
295
|
+
worker.run().catch((err) => {
|
|
296
|
+
console.error("Fatal worker error:", err);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { DEFAULT_MODEL } from "../core/config";
|
|
3
|
+
|
|
4
|
+
export interface AnthropicClientConfig {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CachedPromptOptions {
|
|
10
|
+
systemPrompt?: string;
|
|
11
|
+
cacheableContext?: string[];
|
|
12
|
+
userPrompt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Anthropic Client with Prompt Caching Support
|
|
17
|
+
*
|
|
18
|
+
* This client wraps the official Anthropic SDK and adds support for
|
|
19
|
+
* prompt caching to dramatically reduce latency and costs for repeated
|
|
20
|
+
* context (like codebase indexes, CLAUDE.md, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export class AnthropicClient {
|
|
23
|
+
private client: Anthropic;
|
|
24
|
+
private model: string;
|
|
25
|
+
|
|
26
|
+
constructor(config: AnthropicClientConfig) {
|
|
27
|
+
this.client = new Anthropic({
|
|
28
|
+
apiKey: config.apiKey,
|
|
29
|
+
});
|
|
30
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run a prompt with optional caching for large context blocks
|
|
35
|
+
*
|
|
36
|
+
* @param options - Prompt configuration with cacheable context
|
|
37
|
+
* @returns The generated text response
|
|
38
|
+
*/
|
|
39
|
+
async run(options: CachedPromptOptions): Promise<string> {
|
|
40
|
+
const { systemPrompt, cacheableContext = [], userPrompt } = options;
|
|
41
|
+
|
|
42
|
+
// Build system message with cache breakpoints
|
|
43
|
+
const systemContent: Anthropic.Messages.TextBlockParam[] = [];
|
|
44
|
+
|
|
45
|
+
if (systemPrompt) {
|
|
46
|
+
systemContent.push({
|
|
47
|
+
type: "text",
|
|
48
|
+
text: systemPrompt,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add each cacheable context block with cache_control
|
|
53
|
+
for (let i = 0; i < cacheableContext.length; i++) {
|
|
54
|
+
const isLast = i === cacheableContext.length - 1;
|
|
55
|
+
systemContent.push({
|
|
56
|
+
type: "text",
|
|
57
|
+
text: cacheableContext[i],
|
|
58
|
+
// Only the last block gets the cache breakpoint
|
|
59
|
+
...(isLast && {
|
|
60
|
+
cache_control: { type: "ephemeral" as const },
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await this.client.messages.create({
|
|
66
|
+
model: this.model,
|
|
67
|
+
max_tokens: 8000,
|
|
68
|
+
system: systemContent,
|
|
69
|
+
messages: [
|
|
70
|
+
{
|
|
71
|
+
role: "user",
|
|
72
|
+
content: userPrompt,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Extract text from response
|
|
78
|
+
const textBlocks = response.content.filter(
|
|
79
|
+
(block): block is Anthropic.Messages.TextBlock => block.type === "text"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return textBlocks.map((block) => block.text).join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Simple run without caching (for short prompts)
|
|
87
|
+
*/
|
|
88
|
+
async runSimple(prompt: string): Promise<string> {
|
|
89
|
+
return this.run({
|
|
90
|
+
userPrompt: prompt,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { DEFAULT_MODEL } from "../core/config";
|
|
3
|
+
|
|
4
|
+
export class ClaudeRunner {
|
|
5
|
+
constructor(
|
|
6
|
+
private projectPath: string,
|
|
7
|
+
private model: string = DEFAULT_MODEL
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
run(prompt: string, _isPlanning = false): Promise<string> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const args = [
|
|
13
|
+
"--dangerously-skip-permissions",
|
|
14
|
+
"--print",
|
|
15
|
+
"--model",
|
|
16
|
+
this.model,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const claude = spawn("claude", args, {
|
|
20
|
+
cwd: this.projectPath,
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let output = "";
|
|
25
|
+
let errorOutput = "";
|
|
26
|
+
|
|
27
|
+
claude.stdout.on("data", (data) => {
|
|
28
|
+
output += data.toString();
|
|
29
|
+
process.stdout.write(data.toString());
|
|
30
|
+
});
|
|
31
|
+
claude.stderr.on("data", (data) => {
|
|
32
|
+
errorOutput += data.toString();
|
|
33
|
+
process.stderr.write(data.toString());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
claude.on("error", (err) =>
|
|
37
|
+
reject(new Error(`Failed to start Claude CLI: ${err.message}`))
|
|
38
|
+
);
|
|
39
|
+
claude.on("close", (code) => {
|
|
40
|
+
if (code === 0) resolve(output);
|
|
41
|
+
else
|
|
42
|
+
reject(new Error(`Claude exited with code ${code}: ${errorOutput}`));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
claude.stdin.write(prompt);
|
|
46
|
+
claude.stdin.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/ai/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MODEL = "sonnet";
|
|
4
|
+
|
|
5
|
+
export const LOCUS_CONFIG = {
|
|
6
|
+
dir: ".locus",
|
|
7
|
+
configFile: "config.json",
|
|
8
|
+
indexFile: "codebase-index.json",
|
|
9
|
+
contextFile: "CLAUDE.md",
|
|
10
|
+
artifactsDir: "artifacts",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function getLocusPath(
|
|
14
|
+
projectPath: string,
|
|
15
|
+
fileName: keyof typeof LOCUS_CONFIG
|
|
16
|
+
): string {
|
|
17
|
+
if (fileName === "contextFile") {
|
|
18
|
+
return join(projectPath, LOCUS_CONFIG.contextFile);
|
|
19
|
+
}
|
|
20
|
+
return join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { globby } from "globby";
|
|
4
|
+
|
|
5
|
+
export interface CodebaseIndex {
|
|
6
|
+
symbols: Record<string, string[]>; // symbol -> file paths
|
|
7
|
+
responsibilities: Record<string, string>; // file path -> brief summary
|
|
8
|
+
lastIndexed: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class CodebaseIndexer {
|
|
12
|
+
private projectPath: string;
|
|
13
|
+
private indexPath: string;
|
|
14
|
+
|
|
15
|
+
constructor(projectPath: string) {
|
|
16
|
+
this.projectPath = projectPath;
|
|
17
|
+
this.indexPath = join(projectPath, ".locus", "codebase-index.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates a codebase index by providing the entire file tree to an AI summarizer.
|
|
22
|
+
* This is much more efficient than per-file indexing for large projects.
|
|
23
|
+
*/
|
|
24
|
+
async index(
|
|
25
|
+
onProgress?: (message: string) => void,
|
|
26
|
+
treeSummarizer?: (tree: string) => Promise<CodebaseIndex>
|
|
27
|
+
): Promise<CodebaseIndex> {
|
|
28
|
+
if (!treeSummarizer) {
|
|
29
|
+
throw new Error("A treeSummarizer is required for this indexing method.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (onProgress) onProgress("Generating file tree...");
|
|
33
|
+
|
|
34
|
+
// 1. Get a comprehensive but clean file tree
|
|
35
|
+
const files = await globby(["**/*"], {
|
|
36
|
+
cwd: this.projectPath,
|
|
37
|
+
ignore: [
|
|
38
|
+
"**/node_modules/**",
|
|
39
|
+
"**/dist/**",
|
|
40
|
+
"**/build/**",
|
|
41
|
+
"**/.next/**",
|
|
42
|
+
"**/out/**",
|
|
43
|
+
"**/__tests__/**",
|
|
44
|
+
"**/*.test.*",
|
|
45
|
+
"**/*.spec.*",
|
|
46
|
+
"**/*.d.ts",
|
|
47
|
+
"**/tsconfig.tsbuildinfo",
|
|
48
|
+
"**/.locus/*.json", // Ignore index and other system JSONs
|
|
49
|
+
"**/.locus/*.md", // Ignore system MDs if any (except artifacts handled below)
|
|
50
|
+
"**/.locus/!(artifacts)/**", // Ignore everything in .locus EXCEPT artifacts
|
|
51
|
+
"bun.lock",
|
|
52
|
+
"package-lock.json",
|
|
53
|
+
"yarn.lock",
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Format the tree for the AI
|
|
58
|
+
const treeString = files.join("\n");
|
|
59
|
+
|
|
60
|
+
if (onProgress) onProgress("AI is analyzing codebase structure...");
|
|
61
|
+
|
|
62
|
+
// 2. Ask AI to generate the index based on the tree
|
|
63
|
+
const index = await treeSummarizer(treeString);
|
|
64
|
+
|
|
65
|
+
// 3. Post-process: Ensure symbols are extracted for core files if not provided by AI
|
|
66
|
+
// (AI is good at structure, but might miss specific exports unless it reads the files)
|
|
67
|
+
// For now, we trust the AI's structural summary and can supplement symbols later if needed.
|
|
68
|
+
|
|
69
|
+
index.lastIndexed = new Date().toISOString();
|
|
70
|
+
return index;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
loadIndex(): CodebaseIndex | null {
|
|
74
|
+
if (existsSync(this.indexPath)) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(this.indexPath, "utf-8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
saveIndex(index: CodebaseIndex): void {
|
|
85
|
+
const dir = dirname(this.indexPath);
|
|
86
|
+
if (!existsSync(dir)) {
|
|
87
|
+
mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
90
|
+
}
|
|
91
|
+
}
|