@locusai/sdk 0.4.9 → 0.4.11
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/worker.js +1284 -0
- package/dist/index-node.js +210 -207
- package/dist/orchestrator.d.ts.map +1 -1
- package/package.json +9 -10
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
19
|
+
var __toCommonJS = (from) => {
|
|
20
|
+
var entry = __moduleCache.get(from), desc;
|
|
21
|
+
if (entry)
|
|
22
|
+
return entry;
|
|
23
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
25
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
26
|
+
get: () => from[key],
|
|
27
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
28
|
+
}));
|
|
29
|
+
__moduleCache.set(from, entry);
|
|
30
|
+
return entry;
|
|
31
|
+
};
|
|
32
|
+
var __export = (target, all) => {
|
|
33
|
+
for (var name in all)
|
|
34
|
+
__defProp(target, name, {
|
|
35
|
+
get: all[name],
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
set: (newValue) => all[name] = () => newValue
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/index.ts
|
|
43
|
+
var exports_src = {};
|
|
44
|
+
__export(exports_src, {
|
|
45
|
+
WorkspacesModule: () => WorkspacesModule,
|
|
46
|
+
TasksModule: () => TasksModule,
|
|
47
|
+
SprintsModule: () => SprintsModule,
|
|
48
|
+
OrganizationsModule: () => OrganizationsModule,
|
|
49
|
+
LocusEvent: () => LocusEvent,
|
|
50
|
+
LocusEmitter: () => LocusEmitter,
|
|
51
|
+
LocusClient: () => LocusClient,
|
|
52
|
+
InvitationsModule: () => InvitationsModule,
|
|
53
|
+
DocsModule: () => DocsModule,
|
|
54
|
+
CiModule: () => CiModule,
|
|
55
|
+
AuthModule: () => AuthModule
|
|
56
|
+
});
|
|
57
|
+
module.exports = __toCommonJS(exports_src);
|
|
58
|
+
var import_axios = __toESM(require("axios"));
|
|
59
|
+
|
|
60
|
+
// src/events.ts
|
|
61
|
+
var import_events = require("events");
|
|
62
|
+
var LocusEvent;
|
|
63
|
+
((LocusEvent2) => {
|
|
64
|
+
LocusEvent2["TOKEN_EXPIRED"] = "TOKEN_EXPIRED";
|
|
65
|
+
LocusEvent2["AUTH_ERROR"] = "AUTH_ERROR";
|
|
66
|
+
LocusEvent2["REQUEST_ERROR"] = "REQUEST_ERROR";
|
|
67
|
+
})(LocusEvent ||= {});
|
|
68
|
+
|
|
69
|
+
class LocusEmitter extends import_events.EventEmitter {
|
|
70
|
+
on(event, listener) {
|
|
71
|
+
return super.on(event, listener);
|
|
72
|
+
}
|
|
73
|
+
emit(event, ...args) {
|
|
74
|
+
return super.emit(event, ...args);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/modules/base.ts
|
|
79
|
+
class BaseModule {
|
|
80
|
+
api;
|
|
81
|
+
emitter;
|
|
82
|
+
constructor(api, emitter) {
|
|
83
|
+
this.api = api;
|
|
84
|
+
this.emitter = emitter;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/modules/auth.ts
|
|
89
|
+
class AuthModule extends BaseModule {
|
|
90
|
+
async getMe() {
|
|
91
|
+
const { data } = await this.api.get("/auth/me");
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
async requestRegisterOtp(email) {
|
|
95
|
+
const { data } = await this.api.post("/auth/register-otp", { email });
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
async requestLoginOtp(email) {
|
|
99
|
+
const { data } = await this.api.post("/auth/login-otp", { email });
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
async verifyLogin(body) {
|
|
103
|
+
const { data } = await this.api.post("/auth/verify-login", body);
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
async completeRegistration(body) {
|
|
107
|
+
const { data } = await this.api.post("/auth/complete-registration", body);
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/modules/ci.ts
|
|
113
|
+
class CiModule extends BaseModule {
|
|
114
|
+
async report(body) {
|
|
115
|
+
const { data } = await this.api.post("/ci/report", body);
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/modules/docs.ts
|
|
121
|
+
class DocsModule extends BaseModule {
|
|
122
|
+
async create(workspaceId, body) {
|
|
123
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/docs`, body);
|
|
124
|
+
return data.doc;
|
|
125
|
+
}
|
|
126
|
+
async list(workspaceId) {
|
|
127
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/docs`);
|
|
128
|
+
return data.docs;
|
|
129
|
+
}
|
|
130
|
+
async getById(id, workspaceId) {
|
|
131
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/docs/${id}`);
|
|
132
|
+
return data.doc;
|
|
133
|
+
}
|
|
134
|
+
async update(id, workspaceId, body) {
|
|
135
|
+
const { data } = await this.api.put(`/workspaces/${workspaceId}/docs/${id}`, body);
|
|
136
|
+
return data.doc;
|
|
137
|
+
}
|
|
138
|
+
async delete(id, workspaceId) {
|
|
139
|
+
await this.api.delete(`/workspaces/${workspaceId}/docs/${id}`);
|
|
140
|
+
}
|
|
141
|
+
async listGroups(workspaceId) {
|
|
142
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/doc-groups`);
|
|
143
|
+
return data.groups;
|
|
144
|
+
}
|
|
145
|
+
async createGroup(workspaceId, body) {
|
|
146
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/doc-groups`, body);
|
|
147
|
+
return data.group;
|
|
148
|
+
}
|
|
149
|
+
async updateGroup(id, workspaceId, body) {
|
|
150
|
+
const { data } = await this.api.patch(`/workspaces/${workspaceId}/doc-groups/${id}`, body);
|
|
151
|
+
return data.group;
|
|
152
|
+
}
|
|
153
|
+
async deleteGroup(id, workspaceId) {
|
|
154
|
+
await this.api.delete(`/workspaces/${workspaceId}/doc-groups/${id}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/modules/invitations.ts
|
|
159
|
+
class InvitationsModule extends BaseModule {
|
|
160
|
+
async create(orgId, body) {
|
|
161
|
+
const { data } = await this.api.post(`/org/${orgId}/invitations`, body);
|
|
162
|
+
return data.invitation;
|
|
163
|
+
}
|
|
164
|
+
async list(orgId) {
|
|
165
|
+
const { data } = await this.api.get(`/org/${orgId}/invitations`);
|
|
166
|
+
return data.invitations;
|
|
167
|
+
}
|
|
168
|
+
async verify(token) {
|
|
169
|
+
const { data } = await this.api.get(`/invitations/verify/${token}`);
|
|
170
|
+
return data;
|
|
171
|
+
}
|
|
172
|
+
async accept(body) {
|
|
173
|
+
const { data } = await this.api.post("/invitations/accept", body);
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
async revoke(orgId, id) {
|
|
177
|
+
await this.api.delete(`/org/${orgId}/invitations/${id}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/modules/organizations.ts
|
|
182
|
+
class OrganizationsModule extends BaseModule {
|
|
183
|
+
async list() {
|
|
184
|
+
const { data } = await this.api.get("/organizations");
|
|
185
|
+
return data.organizations;
|
|
186
|
+
}
|
|
187
|
+
async getById(id) {
|
|
188
|
+
const { data } = await this.api.get(`/organizations/${id}`);
|
|
189
|
+
return data.organization;
|
|
190
|
+
}
|
|
191
|
+
async listMembers(id) {
|
|
192
|
+
const { data } = await this.api.get(`/organizations/${id}/members`);
|
|
193
|
+
return data.members;
|
|
194
|
+
}
|
|
195
|
+
async addMember(id, body) {
|
|
196
|
+
const { data } = await this.api.post(`/organizations/${id}/members`, body);
|
|
197
|
+
return data.membership;
|
|
198
|
+
}
|
|
199
|
+
async removeMember(orgId, userId) {
|
|
200
|
+
await this.api.delete(`/organizations/${orgId}/members/${userId}`);
|
|
201
|
+
}
|
|
202
|
+
async delete(orgId) {
|
|
203
|
+
await this.api.delete(`/organizations/${orgId}`);
|
|
204
|
+
}
|
|
205
|
+
async listApiKeys(orgId) {
|
|
206
|
+
const { data } = await this.api.get(`/organizations/${orgId}/api-keys`);
|
|
207
|
+
return data.apiKeys;
|
|
208
|
+
}
|
|
209
|
+
async createApiKey(orgId, name) {
|
|
210
|
+
const { data } = await this.api.post(`/organizations/${orgId}/api-keys`, { name });
|
|
211
|
+
return data.apiKey;
|
|
212
|
+
}
|
|
213
|
+
async deleteApiKey(orgId, keyId) {
|
|
214
|
+
await this.api.delete(`/organizations/${orgId}/api-keys/${keyId}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/modules/sprints.ts
|
|
219
|
+
class SprintsModule extends BaseModule {
|
|
220
|
+
async list(workspaceId) {
|
|
221
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/sprints`);
|
|
222
|
+
return data.sprints;
|
|
223
|
+
}
|
|
224
|
+
async getActive(workspaceId) {
|
|
225
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/sprints/active`);
|
|
226
|
+
return data.sprint;
|
|
227
|
+
}
|
|
228
|
+
async getById(id, workspaceId) {
|
|
229
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/sprints/${id}`);
|
|
230
|
+
return data.sprint;
|
|
231
|
+
}
|
|
232
|
+
async create(workspaceId, body) {
|
|
233
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints`, body);
|
|
234
|
+
return data.sprint;
|
|
235
|
+
}
|
|
236
|
+
async update(id, workspaceId, body) {
|
|
237
|
+
const { data } = await this.api.patch(`/workspaces/${workspaceId}/sprints/${id}`, body);
|
|
238
|
+
return data.sprint;
|
|
239
|
+
}
|
|
240
|
+
async delete(id, workspaceId) {
|
|
241
|
+
await this.api.delete(`/workspaces/${workspaceId}/sprints/${id}`);
|
|
242
|
+
}
|
|
243
|
+
async start(id, workspaceId) {
|
|
244
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/start`);
|
|
245
|
+
return data.sprint;
|
|
246
|
+
}
|
|
247
|
+
async complete(id, workspaceId) {
|
|
248
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/complete`);
|
|
249
|
+
return data.sprint;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/modules/tasks.ts
|
|
254
|
+
var import_shared = require("@locusai/shared");
|
|
255
|
+
class TasksModule extends BaseModule {
|
|
256
|
+
async list(workspaceId, options) {
|
|
257
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks`);
|
|
258
|
+
let tasks = data.tasks;
|
|
259
|
+
if (options?.sprintId) {
|
|
260
|
+
tasks = tasks.filter((t) => t.sprintId === options.sprintId);
|
|
261
|
+
}
|
|
262
|
+
if (options?.status) {
|
|
263
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
264
|
+
tasks = tasks.filter((t) => statuses.includes(t.status));
|
|
265
|
+
}
|
|
266
|
+
return tasks;
|
|
267
|
+
}
|
|
268
|
+
async getAvailable(workspaceId, sprintId) {
|
|
269
|
+
const tasks = await this.list(workspaceId, {
|
|
270
|
+
sprintId
|
|
271
|
+
});
|
|
272
|
+
return tasks.filter((t) => t.status === import_shared.TaskStatus.BACKLOG || t.status === import_shared.TaskStatus.IN_PROGRESS && !t.assignedTo);
|
|
273
|
+
}
|
|
274
|
+
async getById(id, workspaceId) {
|
|
275
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}`);
|
|
276
|
+
return data.task;
|
|
277
|
+
}
|
|
278
|
+
async create(workspaceId, body) {
|
|
279
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/tasks`, body);
|
|
280
|
+
return data.task;
|
|
281
|
+
}
|
|
282
|
+
async update(id, workspaceId, body) {
|
|
283
|
+
const { data } = await this.api.patch(`/workspaces/${workspaceId}/tasks/${id}`, body);
|
|
284
|
+
return data.task;
|
|
285
|
+
}
|
|
286
|
+
async delete(id, workspaceId) {
|
|
287
|
+
await this.api.delete(`/workspaces/${workspaceId}/tasks/${id}`);
|
|
288
|
+
}
|
|
289
|
+
async getBacklog(workspaceId) {
|
|
290
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/backlog`);
|
|
291
|
+
return data.tasks;
|
|
292
|
+
}
|
|
293
|
+
async addComment(id, workspaceId, body) {
|
|
294
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/tasks/${id}/comment`, body);
|
|
295
|
+
return data.comment;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/modules/workspaces.ts
|
|
300
|
+
class WorkspacesModule extends BaseModule {
|
|
301
|
+
async listAll() {
|
|
302
|
+
const { data } = await this.api.get("/workspaces");
|
|
303
|
+
return data.workspaces;
|
|
304
|
+
}
|
|
305
|
+
async listByOrg(orgId) {
|
|
306
|
+
const { data } = await this.api.get(`/workspaces/org/${orgId}`);
|
|
307
|
+
return data.workspaces;
|
|
308
|
+
}
|
|
309
|
+
async create(body) {
|
|
310
|
+
const { orgId, ...bodyWithoutOrgId } = body;
|
|
311
|
+
const { data } = await this.api.post(`/workspaces/org/${orgId}`, bodyWithoutOrgId);
|
|
312
|
+
return data.workspace;
|
|
313
|
+
}
|
|
314
|
+
async createWithAutoOrg(body) {
|
|
315
|
+
const { data } = await this.api.post("/workspaces", body);
|
|
316
|
+
return data.workspace;
|
|
317
|
+
}
|
|
318
|
+
async getById(id) {
|
|
319
|
+
const { data } = await this.api.get(`/workspaces/${id}`);
|
|
320
|
+
return data.workspace;
|
|
321
|
+
}
|
|
322
|
+
async update(id, body) {
|
|
323
|
+
const { data } = await this.api.put(`/workspaces/${id}`, body);
|
|
324
|
+
return data.workspace;
|
|
325
|
+
}
|
|
326
|
+
async delete(id) {
|
|
327
|
+
await this.api.delete(`/workspaces/${id}`);
|
|
328
|
+
}
|
|
329
|
+
async getStats(id) {
|
|
330
|
+
const { data } = await this.api.get(`/workspaces/${id}/stats`);
|
|
331
|
+
return data;
|
|
332
|
+
}
|
|
333
|
+
async getActivity(id, limit) {
|
|
334
|
+
const { data } = await this.api.get(`/workspaces/${id}/activity`, {
|
|
335
|
+
params: { limit }
|
|
336
|
+
});
|
|
337
|
+
return data.activity;
|
|
338
|
+
}
|
|
339
|
+
async dispatch(id, workerId, sprintId) {
|
|
340
|
+
const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
|
|
341
|
+
return data.task;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/index.ts
|
|
346
|
+
class LocusClient {
|
|
347
|
+
api;
|
|
348
|
+
emitter;
|
|
349
|
+
auth;
|
|
350
|
+
tasks;
|
|
351
|
+
sprints;
|
|
352
|
+
workspaces;
|
|
353
|
+
organizations;
|
|
354
|
+
invitations;
|
|
355
|
+
docs;
|
|
356
|
+
ci;
|
|
357
|
+
constructor(config) {
|
|
358
|
+
this.emitter = new LocusEmitter;
|
|
359
|
+
this.api = import_axios.default.create({
|
|
360
|
+
baseURL: config.baseUrl,
|
|
361
|
+
timeout: config.timeout || 1e4,
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/json",
|
|
364
|
+
...config.token ? { Authorization: `Bearer ${config.token}` } : {}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
this.setupInterceptors();
|
|
368
|
+
this.auth = new AuthModule(this.api, this.emitter);
|
|
369
|
+
this.tasks = new TasksModule(this.api, this.emitter);
|
|
370
|
+
this.sprints = new SprintsModule(this.api, this.emitter);
|
|
371
|
+
this.workspaces = new WorkspacesModule(this.api, this.emitter);
|
|
372
|
+
this.organizations = new OrganizationsModule(this.api, this.emitter);
|
|
373
|
+
this.invitations = new InvitationsModule(this.api, this.emitter);
|
|
374
|
+
this.docs = new DocsModule(this.api, this.emitter);
|
|
375
|
+
this.ci = new CiModule(this.api, this.emitter);
|
|
376
|
+
if (config.retryOptions) {
|
|
377
|
+
this.setupRetryInterceptor(config.retryOptions);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
setupRetryInterceptor(retryOptions) {
|
|
381
|
+
this.api.interceptors.response.use(undefined, async (error) => {
|
|
382
|
+
const config = error.config;
|
|
383
|
+
if (!config || !retryOptions) {
|
|
384
|
+
return Promise.reject(error);
|
|
385
|
+
}
|
|
386
|
+
config._retryCount = config._retryCount || 0;
|
|
387
|
+
const maxRetries = retryOptions.maxRetries ?? 3;
|
|
388
|
+
const shouldRetry = config._retryCount < maxRetries && (retryOptions.retryCondition ? retryOptions.retryCondition(error) : !error.response || error.response.status >= 500);
|
|
389
|
+
if (shouldRetry) {
|
|
390
|
+
config._retryCount++;
|
|
391
|
+
const delay = Math.min((retryOptions.initialDelay ?? 1000) * Math.pow(retryOptions.factor ?? 2, config._retryCount - 1), retryOptions.maxDelay ?? 5000);
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
393
|
+
return this.api(config);
|
|
394
|
+
}
|
|
395
|
+
return Promise.reject(error);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
setupInterceptors() {
|
|
399
|
+
this.api.interceptors.response.use((response) => {
|
|
400
|
+
if (response.data && typeof response.data === "object" && "data" in response.data) {
|
|
401
|
+
response.data = response.data.data;
|
|
402
|
+
}
|
|
403
|
+
return response;
|
|
404
|
+
}, (error) => {
|
|
405
|
+
const status = error.response?.status;
|
|
406
|
+
let message;
|
|
407
|
+
if (error.response?.data?.error?.message && typeof error.response.data.error.message === "string") {
|
|
408
|
+
message = error.response.data.error.message;
|
|
409
|
+
} else if (error.response?.data?.message && typeof error.response.data.message === "string") {
|
|
410
|
+
message = error.response.data.message;
|
|
411
|
+
} else if (error.message && typeof error.message === "string") {
|
|
412
|
+
message = error.message;
|
|
413
|
+
} else {
|
|
414
|
+
message = "An error occurred";
|
|
415
|
+
}
|
|
416
|
+
const enhancedError = new Error(message);
|
|
417
|
+
enhancedError.name = `HTTP${status || "Error"}`;
|
|
418
|
+
if (status === 401) {
|
|
419
|
+
this.emitter.emit("TOKEN_EXPIRED" /* TOKEN_EXPIRED */);
|
|
420
|
+
this.emitter.emit("AUTH_ERROR" /* AUTH_ERROR */, enhancedError);
|
|
421
|
+
} else {
|
|
422
|
+
this.emitter.emit("REQUEST_ERROR" /* REQUEST_ERROR */, enhancedError);
|
|
423
|
+
}
|
|
424
|
+
return Promise.reject(enhancedError);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
setToken(token) {
|
|
428
|
+
if (token) {
|
|
429
|
+
this.api.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
430
|
+
} else {
|
|
431
|
+
delete this.api.defaults.headers.common.Authorization;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/agent/worker.ts
|
|
437
|
+
var exports_worker = {};
|
|
438
|
+
__export(exports_worker, {
|
|
439
|
+
AgentWorker: () => AgentWorker
|
|
440
|
+
});
|
|
441
|
+
module.exports = __toCommonJS(exports_worker);
|
|
442
|
+
|
|
443
|
+
// src/ai/anthropic-client.ts
|
|
444
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
445
|
+
|
|
446
|
+
// src/core/config.ts
|
|
447
|
+
var import_node_path = require("node:path");
|
|
448
|
+
var DEFAULT_MODEL = "sonnet";
|
|
449
|
+
var LOCUS_CONFIG = {
|
|
450
|
+
dir: ".locus",
|
|
451
|
+
configFile: "config.json",
|
|
452
|
+
indexFile: "codebase-index.json",
|
|
453
|
+
contextFile: "CLAUDE.md",
|
|
454
|
+
artifactsDir: "artifacts"
|
|
455
|
+
};
|
|
456
|
+
function getLocusPath(projectPath, fileName) {
|
|
457
|
+
if (fileName === "contextFile") {
|
|
458
|
+
return import_node_path.join(projectPath, LOCUS_CONFIG.contextFile);
|
|
459
|
+
}
|
|
460
|
+
return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/ai/anthropic-client.ts
|
|
464
|
+
class AnthropicClient {
|
|
465
|
+
client;
|
|
466
|
+
model;
|
|
467
|
+
constructor(config) {
|
|
468
|
+
this.client = new import_sdk.default({
|
|
469
|
+
apiKey: config.apiKey
|
|
470
|
+
});
|
|
471
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
472
|
+
}
|
|
473
|
+
async run(options) {
|
|
474
|
+
const { systemPrompt, cacheableContext = [], userPrompt } = options;
|
|
475
|
+
const systemContent = [];
|
|
476
|
+
if (systemPrompt) {
|
|
477
|
+
systemContent.push({
|
|
478
|
+
type: "text",
|
|
479
|
+
text: systemPrompt
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
for (let i = 0;i < cacheableContext.length; i++) {
|
|
483
|
+
const isLast = i === cacheableContext.length - 1;
|
|
484
|
+
systemContent.push({
|
|
485
|
+
type: "text",
|
|
486
|
+
text: cacheableContext[i],
|
|
487
|
+
...isLast && {
|
|
488
|
+
cache_control: { type: "ephemeral" }
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const response = await this.client.messages.create({
|
|
493
|
+
model: this.model,
|
|
494
|
+
max_tokens: 8000,
|
|
495
|
+
system: systemContent,
|
|
496
|
+
messages: [
|
|
497
|
+
{
|
|
498
|
+
role: "user",
|
|
499
|
+
content: userPrompt
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
});
|
|
503
|
+
const textBlocks = response.content.filter((block) => block.type === "text");
|
|
504
|
+
return textBlocks.map((block) => block.text).join(`
|
|
505
|
+
`);
|
|
506
|
+
}
|
|
507
|
+
async runSimple(prompt) {
|
|
508
|
+
return this.run({
|
|
509
|
+
userPrompt: prompt
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/ai/claude-runner.ts
|
|
515
|
+
var import_node_child_process = require("node:child_process");
|
|
516
|
+
class ClaudeRunner {
|
|
517
|
+
projectPath;
|
|
518
|
+
model;
|
|
519
|
+
constructor(projectPath, model = DEFAULT_MODEL) {
|
|
520
|
+
this.projectPath = projectPath;
|
|
521
|
+
this.model = model;
|
|
522
|
+
}
|
|
523
|
+
async run(prompt, _isPlanning = false) {
|
|
524
|
+
const maxRetries = 3;
|
|
525
|
+
let lastError = null;
|
|
526
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
527
|
+
try {
|
|
528
|
+
return await this.executeRun(prompt);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const err = error;
|
|
531
|
+
lastError = err;
|
|
532
|
+
const isLastAttempt = attempt === maxRetries;
|
|
533
|
+
if (!isLastAttempt) {
|
|
534
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
535
|
+
console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
536
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw lastError || new Error("Claude CLI failed after multiple attempts");
|
|
541
|
+
}
|
|
542
|
+
executeRun(prompt) {
|
|
543
|
+
return new Promise((resolve, reject) => {
|
|
544
|
+
const args = [
|
|
545
|
+
"--dangerously-skip-permissions",
|
|
546
|
+
"--print",
|
|
547
|
+
"--model",
|
|
548
|
+
this.model
|
|
549
|
+
];
|
|
550
|
+
const claude = import_node_child_process.spawn("claude", args, {
|
|
551
|
+
cwd: this.projectPath,
|
|
552
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
553
|
+
env: process.env,
|
|
554
|
+
shell: true
|
|
555
|
+
});
|
|
556
|
+
let output = "";
|
|
557
|
+
let errorOutput = "";
|
|
558
|
+
claude.stdout.on("data", (data) => {
|
|
559
|
+
output += data.toString();
|
|
560
|
+
});
|
|
561
|
+
claude.stderr.on("data", (data) => {
|
|
562
|
+
errorOutput += data.toString();
|
|
563
|
+
});
|
|
564
|
+
claude.on("error", (err) => reject(new Error(`Failed to start Claude CLI (shell: true): ${err.message}. Please ensure the 'claude' command is available in your PATH.`)));
|
|
565
|
+
claude.on("close", (code) => {
|
|
566
|
+
if (code === 0)
|
|
567
|
+
resolve(output);
|
|
568
|
+
else {
|
|
569
|
+
const detail = errorOutput.trim();
|
|
570
|
+
const message = detail ? `Claude CLI error (exit code ${code}): ${detail}` : `Claude CLI exited with code ${code}. Please ensure the Claude CLI is installed and you are logged in (run 'claude' manually to check).`;
|
|
571
|
+
reject(new Error(message));
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
claude.stdin.write(prompt);
|
|
575
|
+
claude.stdin.end();
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/utils/colors.ts
|
|
581
|
+
var ESC = "\x1B[";
|
|
582
|
+
var RESET = `${ESC}0m`;
|
|
583
|
+
var colors = {
|
|
584
|
+
reset: RESET,
|
|
585
|
+
bold: `${ESC}1m`,
|
|
586
|
+
dim: `${ESC}2m`,
|
|
587
|
+
italic: `${ESC}3m`,
|
|
588
|
+
underline: `${ESC}4m`,
|
|
589
|
+
black: `${ESC}30m`,
|
|
590
|
+
red: `${ESC}31m`,
|
|
591
|
+
green: `${ESC}32m`,
|
|
592
|
+
yellow: `${ESC}33m`,
|
|
593
|
+
blue: `${ESC}34m`,
|
|
594
|
+
magenta: `${ESC}35m`,
|
|
595
|
+
cyan: `${ESC}36m`,
|
|
596
|
+
white: `${ESC}37m`,
|
|
597
|
+
gray: `${ESC}90m`,
|
|
598
|
+
brightRed: `${ESC}91m`,
|
|
599
|
+
brightGreen: `${ESC}92m`,
|
|
600
|
+
brightYellow: `${ESC}93m`,
|
|
601
|
+
brightBlue: `${ESC}94m`,
|
|
602
|
+
brightMagenta: `${ESC}95m`,
|
|
603
|
+
brightCyan: `${ESC}96m`,
|
|
604
|
+
brightWhite: `${ESC}97m`
|
|
605
|
+
};
|
|
606
|
+
var c = {
|
|
607
|
+
text: (text, ...colorNames) => {
|
|
608
|
+
const codes = colorNames.map((name) => colors[name]).join("");
|
|
609
|
+
return `${codes}${text}${RESET}`;
|
|
610
|
+
},
|
|
611
|
+
bold: (t) => c.text(t, "bold"),
|
|
612
|
+
dim: (t) => c.text(t, "dim"),
|
|
613
|
+
red: (t) => c.text(t, "red"),
|
|
614
|
+
green: (t) => c.text(t, "green"),
|
|
615
|
+
yellow: (t) => c.text(t, "yellow"),
|
|
616
|
+
blue: (t) => c.text(t, "blue"),
|
|
617
|
+
magenta: (t) => c.text(t, "magenta"),
|
|
618
|
+
cyan: (t) => c.text(t, "cyan"),
|
|
619
|
+
gray: (t) => c.text(t, "gray"),
|
|
620
|
+
success: (t) => c.text(t, "green", "bold"),
|
|
621
|
+
error: (t) => c.text(t, "red", "bold"),
|
|
622
|
+
warning: (t) => c.text(t, "yellow", "bold"),
|
|
623
|
+
info: (t) => c.text(t, "cyan", "bold"),
|
|
624
|
+
primary: (t) => c.text(t, "blue", "bold"),
|
|
625
|
+
underline: (t) => c.text(t, "underline")
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/agent/artifact-syncer.ts
|
|
629
|
+
var import_node_fs = require("node:fs");
|
|
630
|
+
var import_node_path2 = require("node:path");
|
|
631
|
+
class ArtifactSyncer {
|
|
632
|
+
deps;
|
|
633
|
+
constructor(deps) {
|
|
634
|
+
this.deps = deps;
|
|
635
|
+
}
|
|
636
|
+
async getOrCreateArtifactsGroup() {
|
|
637
|
+
try {
|
|
638
|
+
const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
|
|
639
|
+
const artifactsGroup = groups.find((g) => g.name === "Artifacts");
|
|
640
|
+
if (artifactsGroup) {
|
|
641
|
+
return artifactsGroup.id;
|
|
642
|
+
}
|
|
643
|
+
const newGroup = await this.deps.client.docs.createGroup(this.deps.workspaceId, {
|
|
644
|
+
name: "Artifacts",
|
|
645
|
+
order: 999
|
|
646
|
+
});
|
|
647
|
+
this.deps.log("Created 'Artifacts' group for agent-generated docs", "info");
|
|
648
|
+
return newGroup.id;
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.deps.log(`Failed to get/create Artifacts group: ${error}`, "error");
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async sync() {
|
|
655
|
+
const artifactsDir = getLocusPath(this.deps.projectPath, "artifactsDir");
|
|
656
|
+
if (!import_node_fs.existsSync(artifactsDir)) {
|
|
657
|
+
import_node_fs.mkdirSync(artifactsDir, { recursive: true });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const files = import_node_fs.readdirSync(artifactsDir);
|
|
662
|
+
if (files.length === 0)
|
|
663
|
+
return;
|
|
664
|
+
this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
|
|
665
|
+
const artifactsGroupId = await this.getOrCreateArtifactsGroup();
|
|
666
|
+
const existingDocs = await this.deps.client.docs.list(this.deps.workspaceId);
|
|
667
|
+
for (const file of files) {
|
|
668
|
+
const filePath = import_node_path2.join(artifactsDir, file);
|
|
669
|
+
if (import_node_fs.statSync(filePath).isFile()) {
|
|
670
|
+
const content = import_node_fs.readFileSync(filePath, "utf-8");
|
|
671
|
+
const title = file.replace(/\.md$/, "").trim();
|
|
672
|
+
if (!title)
|
|
673
|
+
continue;
|
|
674
|
+
const existing = existingDocs.find((d) => d.title === title);
|
|
675
|
+
if (existing) {
|
|
676
|
+
if (existing.content !== content || existing.groupId !== artifactsGroupId) {
|
|
677
|
+
await this.deps.client.docs.update(existing.id, this.deps.workspaceId, { content, groupId: artifactsGroupId });
|
|
678
|
+
this.deps.log(`Updated artifact: ${file}`, "success");
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
await this.deps.client.docs.create(this.deps.workspaceId, {
|
|
682
|
+
title,
|
|
683
|
+
content,
|
|
684
|
+
groupId: artifactsGroupId
|
|
685
|
+
});
|
|
686
|
+
this.deps.log(`Created artifact: ${file}`, "success");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
this.deps.log(`Failed to sync artifacts: ${error}`, "error");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/core/indexer.ts
|
|
697
|
+
var import_node_fs2 = require("node:fs");
|
|
698
|
+
var import_node_path3 = require("node:path");
|
|
699
|
+
var import_globby = require("globby");
|
|
700
|
+
|
|
701
|
+
class CodebaseIndexer {
|
|
702
|
+
projectPath;
|
|
703
|
+
indexPath;
|
|
704
|
+
constructor(projectPath) {
|
|
705
|
+
this.projectPath = projectPath;
|
|
706
|
+
this.indexPath = import_node_path3.join(projectPath, ".locus", "codebase-index.json");
|
|
707
|
+
}
|
|
708
|
+
async index(onProgress, treeSummarizer) {
|
|
709
|
+
if (!treeSummarizer) {
|
|
710
|
+
throw new Error("A treeSummarizer is required for this indexing method.");
|
|
711
|
+
}
|
|
712
|
+
if (onProgress)
|
|
713
|
+
onProgress("Generating file tree...");
|
|
714
|
+
const gitmodulesPath = import_node_path3.join(this.projectPath, ".gitmodules");
|
|
715
|
+
const submoduleIgnores = [];
|
|
716
|
+
if (import_node_fs2.existsSync(gitmodulesPath)) {
|
|
717
|
+
try {
|
|
718
|
+
const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
|
|
719
|
+
const lines = content.split(`
|
|
720
|
+
`);
|
|
721
|
+
for (const line of lines) {
|
|
722
|
+
const match = line.match(/^\s*path\s*=\s*(.*)$/);
|
|
723
|
+
const path = match?.[1]?.trim();
|
|
724
|
+
if (path) {
|
|
725
|
+
submoduleIgnores.push(`${path}/**`);
|
|
726
|
+
submoduleIgnores.push(`**/${path}/**`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch {}
|
|
730
|
+
}
|
|
731
|
+
const files = await import_globby.globby(["**/*"], {
|
|
732
|
+
cwd: this.projectPath,
|
|
733
|
+
gitignore: true,
|
|
734
|
+
ignore: [
|
|
735
|
+
...submoduleIgnores,
|
|
736
|
+
"**/node_modules/**",
|
|
737
|
+
"**/dist/**",
|
|
738
|
+
"**/build/**",
|
|
739
|
+
"**/target/**",
|
|
740
|
+
"**/bin/**",
|
|
741
|
+
"**/obj/**",
|
|
742
|
+
"**/.next/**",
|
|
743
|
+
"**/.svelte-kit/**",
|
|
744
|
+
"**/.nuxt/**",
|
|
745
|
+
"**/.cache/**",
|
|
746
|
+
"**/out/**",
|
|
747
|
+
"**/__tests__/**",
|
|
748
|
+
"**/coverage/**",
|
|
749
|
+
"**/*.test.*",
|
|
750
|
+
"**/*.spec.*",
|
|
751
|
+
"**/*.d.ts",
|
|
752
|
+
"**/tsconfig.tsbuildinfo",
|
|
753
|
+
"**/.locus/*.json",
|
|
754
|
+
"**/.locus/*.md",
|
|
755
|
+
"**/.locus/!(artifacts)/**",
|
|
756
|
+
"**/.git/**",
|
|
757
|
+
"**/.svn/**",
|
|
758
|
+
"**/.hg/**",
|
|
759
|
+
"**/.vscode/**",
|
|
760
|
+
"**/.idea/**",
|
|
761
|
+
"**/.DS_Store",
|
|
762
|
+
"**/bun.lock",
|
|
763
|
+
"**/package-lock.json",
|
|
764
|
+
"**/yarn.lock",
|
|
765
|
+
"**/pnpm-lock.yaml",
|
|
766
|
+
"**/Cargo.lock",
|
|
767
|
+
"**/go.sum",
|
|
768
|
+
"**/poetry.lock",
|
|
769
|
+
"**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
|
|
770
|
+
]
|
|
771
|
+
});
|
|
772
|
+
const treeString = files.join(`
|
|
773
|
+
`);
|
|
774
|
+
if (onProgress)
|
|
775
|
+
onProgress("AI is analyzing codebase structure...");
|
|
776
|
+
const index = await treeSummarizer(treeString);
|
|
777
|
+
index.lastIndexed = new Date().toISOString();
|
|
778
|
+
return index;
|
|
779
|
+
}
|
|
780
|
+
loadIndex() {
|
|
781
|
+
if (import_node_fs2.existsSync(this.indexPath)) {
|
|
782
|
+
try {
|
|
783
|
+
return JSON.parse(import_node_fs2.readFileSync(this.indexPath, "utf-8"));
|
|
784
|
+
} catch {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
saveIndex(index) {
|
|
791
|
+
const dir = import_node_path3.dirname(this.indexPath);
|
|
792
|
+
if (!import_node_fs2.existsSync(dir)) {
|
|
793
|
+
import_node_fs2.mkdirSync(dir, { recursive: true });
|
|
794
|
+
}
|
|
795
|
+
import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/agent/codebase-indexer-service.ts
|
|
800
|
+
class CodebaseIndexerService {
|
|
801
|
+
deps;
|
|
802
|
+
indexer;
|
|
803
|
+
constructor(deps) {
|
|
804
|
+
this.deps = deps;
|
|
805
|
+
this.indexer = new CodebaseIndexer(deps.projectPath);
|
|
806
|
+
}
|
|
807
|
+
async reindex() {
|
|
808
|
+
try {
|
|
809
|
+
this.deps.log("Reindexing codebase...", "info");
|
|
810
|
+
const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
|
|
811
|
+
const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
|
|
812
|
+
1. Key symbols (classes, functions, types) and their locations
|
|
813
|
+
2. Responsibilities of each directory/file
|
|
814
|
+
3. Overall project structure
|
|
815
|
+
|
|
816
|
+
Analyze this file tree and provide a JSON response with:
|
|
817
|
+
- "symbols": object mapping symbol names to file paths (array)
|
|
818
|
+
- "responsibilities": object mapping paths to brief descriptions
|
|
819
|
+
|
|
820
|
+
File tree:
|
|
821
|
+
${tree}
|
|
822
|
+
|
|
823
|
+
Return ONLY valid JSON, no markdown formatting.`;
|
|
824
|
+
let response;
|
|
825
|
+
if (this.deps.anthropicClient) {
|
|
826
|
+
response = await this.deps.anthropicClient.run({
|
|
827
|
+
systemPrompt: "You are a codebase analysis expert specialized in extracting structure and symbols from file trees.",
|
|
828
|
+
userPrompt: prompt
|
|
829
|
+
});
|
|
830
|
+
} else {
|
|
831
|
+
response = await this.deps.claudeRunner.run(prompt, true);
|
|
832
|
+
}
|
|
833
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
834
|
+
if (jsonMatch) {
|
|
835
|
+
return JSON.parse(jsonMatch[0]);
|
|
836
|
+
}
|
|
837
|
+
return { symbols: {}, responsibilities: {}, lastIndexed: "" };
|
|
838
|
+
});
|
|
839
|
+
this.indexer.saveIndex(index);
|
|
840
|
+
this.deps.log("Codebase reindexed successfully", "success");
|
|
841
|
+
} catch (error) {
|
|
842
|
+
this.deps.log(`Failed to reindex codebase: ${error}`, "error");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/agent/sprint-planner.ts
|
|
848
|
+
class SprintPlanner {
|
|
849
|
+
deps;
|
|
850
|
+
constructor(deps) {
|
|
851
|
+
this.deps = deps;
|
|
852
|
+
}
|
|
853
|
+
async planSprint(sprint, tasks2) {
|
|
854
|
+
this.deps.log(`Planning sprint: ${sprint.name}`, "info");
|
|
855
|
+
try {
|
|
856
|
+
const taskList = tasks2.map((t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`).join(`
|
|
857
|
+
`);
|
|
858
|
+
let plan;
|
|
859
|
+
if (this.deps.anthropicClient) {
|
|
860
|
+
const systemPrompt = `You are an expert project manager and lead engineer specialized in sprint planning and task prioritization.`;
|
|
861
|
+
const userPrompt = `# Sprint Planning: ${sprint.name}
|
|
862
|
+
|
|
863
|
+
## Tasks
|
|
864
|
+
${taskList}
|
|
865
|
+
|
|
866
|
+
## Instructions
|
|
867
|
+
1. Analyze dependencies between these tasks.
|
|
868
|
+
2. Prioritize them for the most efficient execution.
|
|
869
|
+
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
870
|
+
4. Output your final plan. The plan should clearly state the order of execution.
|
|
871
|
+
|
|
872
|
+
**IMPORTANT**:
|
|
873
|
+
- Do NOT create any files on the filesystem during this planning phase.
|
|
874
|
+
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
875
|
+
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
876
|
+
plan = await this.deps.anthropicClient.run({
|
|
877
|
+
systemPrompt,
|
|
878
|
+
userPrompt
|
|
879
|
+
});
|
|
880
|
+
} else {
|
|
881
|
+
const planningPrompt = `# Sprint Planning: ${sprint.name}
|
|
882
|
+
|
|
883
|
+
You are an expert project manager and lead engineer. You need to create a mindmap and execution plan for the following tasks in this sprint.
|
|
884
|
+
|
|
885
|
+
## Tasks
|
|
886
|
+
${taskList}
|
|
887
|
+
|
|
888
|
+
## Instructions
|
|
889
|
+
1. Analyze dependencies between these tasks.
|
|
890
|
+
2. Prioritize them for the most efficient execution.
|
|
891
|
+
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
892
|
+
4. Output your final plan. The plan should clearly state the order of execution.
|
|
893
|
+
|
|
894
|
+
**IMPORTANT**:
|
|
895
|
+
- Do NOT create any files on the filesystem during this planning phase.
|
|
896
|
+
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
897
|
+
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
898
|
+
plan = await this.deps.claudeRunner.run(planningPrompt, true);
|
|
899
|
+
}
|
|
900
|
+
this.deps.log("Sprint mindmap generated and posted to server.", "success");
|
|
901
|
+
return plan;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
this.deps.log(`Sprint planning failed: ${error}`, "error");
|
|
904
|
+
return sprint.mindmap || "";
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/core/prompt-builder.ts
|
|
910
|
+
var import_node_fs3 = require("node:fs");
|
|
911
|
+
var import_shared2 = require("@locusai/shared");
|
|
912
|
+
class PromptBuilder {
|
|
913
|
+
projectPath;
|
|
914
|
+
constructor(projectPath) {
|
|
915
|
+
this.projectPath = projectPath;
|
|
916
|
+
}
|
|
917
|
+
async build(task) {
|
|
918
|
+
let prompt = `# Task: ${task.title}
|
|
919
|
+
|
|
920
|
+
`;
|
|
921
|
+
const roleText = this.roleToText(task.assigneeRole);
|
|
922
|
+
if (roleText) {
|
|
923
|
+
prompt += `## Role
|
|
924
|
+
You are acting as a ${roleText}.
|
|
925
|
+
|
|
926
|
+
`;
|
|
927
|
+
}
|
|
928
|
+
prompt += `## Description
|
|
929
|
+
${task.description || "No description provided."}
|
|
930
|
+
|
|
931
|
+
`;
|
|
932
|
+
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
933
|
+
if (import_node_fs3.existsSync(contextPath)) {
|
|
934
|
+
try {
|
|
935
|
+
const context = import_node_fs3.readFileSync(contextPath, "utf-8");
|
|
936
|
+
prompt += `## Project Context (from CLAUDE.md)
|
|
937
|
+
${context}
|
|
938
|
+
|
|
939
|
+
`;
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.warn(`Warning: Could not read context file: ${err}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
945
|
+
if (import_node_fs3.existsSync(indexPath)) {
|
|
946
|
+
prompt += `## Codebase Overview
|
|
947
|
+
There is an index file in the .locus/codebase-index.json and if you need you can check it.
|
|
948
|
+
|
|
949
|
+
`;
|
|
950
|
+
}
|
|
951
|
+
if (task.docs && task.docs.length > 0) {
|
|
952
|
+
prompt += `## Attached Documents
|
|
953
|
+
`;
|
|
954
|
+
for (const doc of task.docs) {
|
|
955
|
+
prompt += `### ${doc.title}
|
|
956
|
+
${doc.content || "(No content)"}
|
|
957
|
+
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (task.acceptanceChecklist && task.acceptanceChecklist.length > 0) {
|
|
962
|
+
prompt += `## Acceptance Criteria
|
|
963
|
+
`;
|
|
964
|
+
for (const item of task.acceptanceChecklist) {
|
|
965
|
+
prompt += `- ${item.done ? "[x]" : "[ ]"} ${item.text}
|
|
966
|
+
`;
|
|
967
|
+
}
|
|
968
|
+
prompt += `
|
|
969
|
+
`;
|
|
970
|
+
}
|
|
971
|
+
if (task.comments && task.comments.length > 0) {
|
|
972
|
+
const comments = task.comments.slice(0, 5);
|
|
973
|
+
prompt += `## Task History & Feedback
|
|
974
|
+
`;
|
|
975
|
+
prompt += `Review the following comments for context or rejection feedback:
|
|
976
|
+
|
|
977
|
+
`;
|
|
978
|
+
for (const comment of comments) {
|
|
979
|
+
const date = new Date(comment.createdAt).toLocaleString();
|
|
980
|
+
prompt += `### ${comment.author} (${date})
|
|
981
|
+
${comment.text}
|
|
982
|
+
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
prompt += `## Instructions
|
|
987
|
+
1. Complete this task.
|
|
988
|
+
2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
|
|
989
|
+
3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
|
|
990
|
+
4. When finished successfully, output: <promise>COMPLETE</promise>
|
|
991
|
+
`;
|
|
992
|
+
return prompt;
|
|
993
|
+
}
|
|
994
|
+
roleToText(role) {
|
|
995
|
+
if (!role) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
switch (role) {
|
|
999
|
+
case import_shared2.AssigneeRole.BACKEND:
|
|
1000
|
+
return "Backend Engineer";
|
|
1001
|
+
case import_shared2.AssigneeRole.FRONTEND:
|
|
1002
|
+
return "Frontend Engineer";
|
|
1003
|
+
case import_shared2.AssigneeRole.PM:
|
|
1004
|
+
return "Product Manager";
|
|
1005
|
+
case import_shared2.AssigneeRole.QA:
|
|
1006
|
+
return "QA Engineer";
|
|
1007
|
+
case import_shared2.AssigneeRole.DESIGN:
|
|
1008
|
+
return "Product Designer";
|
|
1009
|
+
default:
|
|
1010
|
+
return "engineer";
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/agent/task-executor.ts
|
|
1016
|
+
class TaskExecutor {
|
|
1017
|
+
deps;
|
|
1018
|
+
promptBuilder;
|
|
1019
|
+
constructor(deps) {
|
|
1020
|
+
this.deps = deps;
|
|
1021
|
+
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
1022
|
+
}
|
|
1023
|
+
updateSprintPlan(sprintPlan) {
|
|
1024
|
+
this.deps.sprintPlan = sprintPlan;
|
|
1025
|
+
}
|
|
1026
|
+
async execute(task) {
|
|
1027
|
+
this.deps.log(`Executing: ${task.title}`, "info");
|
|
1028
|
+
let basePrompt = await this.promptBuilder.build(task);
|
|
1029
|
+
if (this.deps.sprintPlan) {
|
|
1030
|
+
basePrompt = `## Sprint Context
|
|
1031
|
+
${this.deps.sprintPlan}
|
|
1032
|
+
|
|
1033
|
+
${basePrompt}`;
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
let plan = "";
|
|
1037
|
+
if (this.deps.anthropicClient) {
|
|
1038
|
+
this.deps.log("Phase 1: Planning (Anthropic SDK)...", "info");
|
|
1039
|
+
const cacheableContext = [basePrompt];
|
|
1040
|
+
plan = await this.deps.anthropicClient.run({
|
|
1041
|
+
systemPrompt: "You are an expert software engineer. Analyze the task carefully and create a detailed implementation plan.",
|
|
1042
|
+
cacheableContext,
|
|
1043
|
+
userPrompt: `## Phase 1: Planning
|
|
1044
|
+
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`
|
|
1045
|
+
});
|
|
1046
|
+
} else {
|
|
1047
|
+
this.deps.log("Skipping Phase 1: Planning (No Anthropic API Key)...", "info");
|
|
1048
|
+
}
|
|
1049
|
+
this.deps.log("Starting Execution...", "info");
|
|
1050
|
+
let executionPrompt = basePrompt;
|
|
1051
|
+
if (plan) {
|
|
1052
|
+
executionPrompt += `
|
|
1053
|
+
|
|
1054
|
+
## Phase 2: Execution
|
|
1055
|
+
Based on the plan, execute the task:
|
|
1056
|
+
|
|
1057
|
+
${plan}`;
|
|
1058
|
+
} else {
|
|
1059
|
+
executionPrompt += `
|
|
1060
|
+
|
|
1061
|
+
## Execution
|
|
1062
|
+
Execute the task directly.`;
|
|
1063
|
+
}
|
|
1064
|
+
executionPrompt += `
|
|
1065
|
+
|
|
1066
|
+
When finished, output: <promise>COMPLETE</promise>`;
|
|
1067
|
+
const output = await this.deps.claudeRunner.run(executionPrompt);
|
|
1068
|
+
const success = output.includes("<promise>COMPLETE</promise>");
|
|
1069
|
+
return {
|
|
1070
|
+
success,
|
|
1071
|
+
summary: success ? "Task completed by Claude" : "Claude did not signal completion"
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
return { success: false, summary: `Error: ${error}` };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/agent/worker.ts
|
|
1080
|
+
class AgentWorker {
|
|
1081
|
+
config;
|
|
1082
|
+
client;
|
|
1083
|
+
claudeRunner;
|
|
1084
|
+
anthropicClient;
|
|
1085
|
+
sprintPlanner;
|
|
1086
|
+
indexerService;
|
|
1087
|
+
artifactSyncer;
|
|
1088
|
+
taskExecutor;
|
|
1089
|
+
consecutiveEmpty = 0;
|
|
1090
|
+
maxEmpty = 10;
|
|
1091
|
+
maxTasks = 50;
|
|
1092
|
+
tasksCompleted = 0;
|
|
1093
|
+
pollInterval = 1e4;
|
|
1094
|
+
sprintPlan = null;
|
|
1095
|
+
constructor(config) {
|
|
1096
|
+
this.config = config;
|
|
1097
|
+
const projectPath = config.projectPath || process.cwd();
|
|
1098
|
+
this.client = new LocusClient({
|
|
1099
|
+
baseUrl: config.apiBase,
|
|
1100
|
+
token: config.apiKey,
|
|
1101
|
+
retryOptions: {
|
|
1102
|
+
maxRetries: 3,
|
|
1103
|
+
initialDelay: 1000,
|
|
1104
|
+
maxDelay: 5000,
|
|
1105
|
+
factor: 2
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
this.claudeRunner = new ClaudeRunner(projectPath, config.model);
|
|
1109
|
+
this.anthropicClient = config.anthropicApiKey ? new AnthropicClient({
|
|
1110
|
+
apiKey: config.anthropicApiKey,
|
|
1111
|
+
model: config.model
|
|
1112
|
+
}) : null;
|
|
1113
|
+
const logFn = this.log.bind(this);
|
|
1114
|
+
this.sprintPlanner = new SprintPlanner({
|
|
1115
|
+
anthropicClient: this.anthropicClient,
|
|
1116
|
+
claudeRunner: this.claudeRunner,
|
|
1117
|
+
log: logFn
|
|
1118
|
+
});
|
|
1119
|
+
this.indexerService = new CodebaseIndexerService({
|
|
1120
|
+
anthropicClient: this.anthropicClient,
|
|
1121
|
+
claudeRunner: this.claudeRunner,
|
|
1122
|
+
projectPath,
|
|
1123
|
+
log: logFn
|
|
1124
|
+
});
|
|
1125
|
+
this.artifactSyncer = new ArtifactSyncer({
|
|
1126
|
+
client: this.client,
|
|
1127
|
+
workspaceId: config.workspaceId,
|
|
1128
|
+
projectPath,
|
|
1129
|
+
log: logFn
|
|
1130
|
+
});
|
|
1131
|
+
this.taskExecutor = new TaskExecutor({
|
|
1132
|
+
anthropicClient: this.anthropicClient,
|
|
1133
|
+
claudeRunner: this.claudeRunner,
|
|
1134
|
+
projectPath,
|
|
1135
|
+
sprintPlan: null,
|
|
1136
|
+
log: logFn
|
|
1137
|
+
});
|
|
1138
|
+
if (this.anthropicClient) {
|
|
1139
|
+
this.log("Using Anthropic SDK with prompt caching for planning phases", "info");
|
|
1140
|
+
} else {
|
|
1141
|
+
this.log("Using Claude CLI for all phases (no Anthropic API key provided)", "info");
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
log(message, level = "info") {
|
|
1145
|
+
const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
1146
|
+
const colorFn = {
|
|
1147
|
+
info: c.cyan,
|
|
1148
|
+
success: c.green,
|
|
1149
|
+
warn: c.yellow,
|
|
1150
|
+
error: c.red
|
|
1151
|
+
}[level];
|
|
1152
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
1153
|
+
console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
|
|
1154
|
+
}
|
|
1155
|
+
async getActiveSprint() {
|
|
1156
|
+
try {
|
|
1157
|
+
if (this.config.sprintId) {
|
|
1158
|
+
return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
|
|
1159
|
+
}
|
|
1160
|
+
return await this.client.sprints.getActive(this.config.workspaceId);
|
|
1161
|
+
} catch (_error) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async getNextTask() {
|
|
1166
|
+
try {
|
|
1167
|
+
const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
|
|
1168
|
+
return task;
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
this.log(`No task dispatched: ${error instanceof Error ? error.message : String(error)}`, "info");
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async executeTask(task) {
|
|
1175
|
+
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
1176
|
+
this.taskExecutor.updateSprintPlan(this.sprintPlan);
|
|
1177
|
+
const result = await this.taskExecutor.execute(fullTask);
|
|
1178
|
+
await this.indexerService.reindex();
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
async run() {
|
|
1182
|
+
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
1183
|
+
const sprint = await this.getActiveSprint();
|
|
1184
|
+
if (sprint) {
|
|
1185
|
+
this.log(`Found active sprint: ${sprint.name} (${sprint.id})`, "info");
|
|
1186
|
+
const tasks2 = await this.client.tasks.list(this.config.workspaceId, {
|
|
1187
|
+
sprintId: sprint.id
|
|
1188
|
+
});
|
|
1189
|
+
this.log(`Sprint tasks found: ${tasks2.length}`, "info");
|
|
1190
|
+
const latestTaskCreation = tasks2.reduce((latest, task) => {
|
|
1191
|
+
const taskDate = new Date(task.createdAt);
|
|
1192
|
+
return taskDate > latest ? taskDate : latest;
|
|
1193
|
+
}, new Date(0));
|
|
1194
|
+
const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
|
|
1195
|
+
if (tasks2.length <= 1) {
|
|
1196
|
+
this.log("Skipping mindmap generation (only one task in sprint).", "info");
|
|
1197
|
+
this.sprintPlan = null;
|
|
1198
|
+
} else {
|
|
1199
|
+
const needsPlanning = !sprint.mindmap || sprint.mindmap.trim() === "" || latestTaskCreation > mindmapDate;
|
|
1200
|
+
if (needsPlanning) {
|
|
1201
|
+
if (sprint.mindmap && latestTaskCreation > mindmapDate) {
|
|
1202
|
+
this.log("New tasks have been added to the sprint since last mindmap. Regenerating...", "warn");
|
|
1203
|
+
}
|
|
1204
|
+
this.sprintPlan = await this.sprintPlanner.planSprint(sprint, tasks2);
|
|
1205
|
+
await this.client.sprints.update(sprint.id, this.config.workspaceId, {
|
|
1206
|
+
mindmap: this.sprintPlan,
|
|
1207
|
+
mindmapUpdatedAt: new Date
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
this.log("Using existing sprint mindmap.", "info");
|
|
1211
|
+
this.sprintPlan = sprint.mindmap ?? null;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
this.log("No active sprint found for planning.", "warn");
|
|
1216
|
+
}
|
|
1217
|
+
while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
|
|
1218
|
+
const task = await this.getNextTask();
|
|
1219
|
+
if (!task) {
|
|
1220
|
+
this.consecutiveEmpty++;
|
|
1221
|
+
if (this.consecutiveEmpty >= this.maxEmpty)
|
|
1222
|
+
break;
|
|
1223
|
+
await new Promise((r) => setTimeout(r, this.pollInterval));
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
this.consecutiveEmpty = 0;
|
|
1227
|
+
this.log(`Claimed: ${task.title}`, "success");
|
|
1228
|
+
const result = await this.executeTask(task);
|
|
1229
|
+
await this.artifactSyncer.sync();
|
|
1230
|
+
if (result.success) {
|
|
1231
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
1232
|
+
status: "VERIFICATION"
|
|
1233
|
+
});
|
|
1234
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
1235
|
+
author: this.config.agentId,
|
|
1236
|
+
text: `✅ ${result.summary}`
|
|
1237
|
+
});
|
|
1238
|
+
this.tasksCompleted++;
|
|
1239
|
+
} else {
|
|
1240
|
+
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
1241
|
+
status: "BACKLOG",
|
|
1242
|
+
assignedTo: null
|
|
1243
|
+
});
|
|
1244
|
+
await this.client.tasks.addComment(task.id, this.config.workspaceId, {
|
|
1245
|
+
author: this.config.agentId,
|
|
1246
|
+
text: `❌ ${result.summary}`
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
process.exit(0);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
|
|
1254
|
+
const args = process.argv.slice(2);
|
|
1255
|
+
const config = {};
|
|
1256
|
+
for (let i = 0;i < args.length; i++) {
|
|
1257
|
+
const arg = args[i];
|
|
1258
|
+
if (arg === "--agent-id")
|
|
1259
|
+
config.agentId = args[++i];
|
|
1260
|
+
else if (arg === "--workspace-id")
|
|
1261
|
+
config.workspaceId = args[++i];
|
|
1262
|
+
else if (arg === "--sprint-id")
|
|
1263
|
+
config.sprintId = args[++i];
|
|
1264
|
+
else if (arg === "--api-base")
|
|
1265
|
+
config.apiBase = args[++i];
|
|
1266
|
+
else if (arg === "--api-key")
|
|
1267
|
+
config.apiKey = args[++i];
|
|
1268
|
+
else if (arg === "--anthropic-api-key")
|
|
1269
|
+
config.anthropicApiKey = args[++i];
|
|
1270
|
+
else if (arg === "--project-path")
|
|
1271
|
+
config.projectPath = args[++i];
|
|
1272
|
+
else if (arg === "--model")
|
|
1273
|
+
config.model = args[++i];
|
|
1274
|
+
}
|
|
1275
|
+
if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
|
|
1276
|
+
console.error("Missing required arguments");
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
const worker = new AgentWorker(config);
|
|
1280
|
+
worker.run().catch((err) => {
|
|
1281
|
+
console.error("Fatal worker error:", err);
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
});
|
|
1284
|
+
}
|