@locusai/sdk 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/artifact-syncer.d.ts.map +1 -1
- package/dist/agent/index.d.ts +0 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/task-executor.d.ts +1 -3
- package/dist/agent/task-executor.d.ts.map +1 -1
- package/dist/agent/worker.d.ts +0 -2
- package/dist/agent/worker.d.ts.map +1 -1
- package/dist/agent/worker.js +340 -121
- package/dist/ai/claude-runner.d.ts +1 -0
- package/dist/ai/claude-runner.d.ts.map +1 -1
- package/dist/ai/codex-runner.d.ts.map +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/prompt-builder.d.ts +9 -1
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/index-node.d.ts +1 -0
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +346 -127
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -3
- package/dist/modules/ai.d.ts +47 -0
- package/dist/modules/ai.d.ts.map +1 -0
- package/dist/modules/auth.d.ts +7 -1
- package/dist/modules/auth.d.ts.map +1 -1
- package/dist/modules/organizations.d.ts +3 -3
- package/dist/modules/organizations.d.ts.map +1 -1
- package/dist/modules/sprints.d.ts +1 -0
- package/dist/modules/sprints.d.ts.map +1 -1
- package/dist/modules/tasks.d.ts +6 -0
- package/dist/modules/tasks.d.ts.map +1 -1
- package/dist/modules/workspaces.d.ts +18 -0
- package/dist/modules/workspaces.d.ts.map +1 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/agent/sprint-planner.d.ts +0 -16
- package/dist/agent/sprint-planner.d.ts.map +0 -1
package/dist/index-node.js
CHANGED
|
@@ -52,7 +52,8 @@ __export(exports_src, {
|
|
|
52
52
|
InvitationsModule: () => InvitationsModule,
|
|
53
53
|
DocsModule: () => DocsModule,
|
|
54
54
|
CiModule: () => CiModule,
|
|
55
|
-
AuthModule: () => AuthModule
|
|
55
|
+
AuthModule: () => AuthModule,
|
|
56
|
+
AIModule: () => AIModule
|
|
56
57
|
});
|
|
57
58
|
module.exports = __toCommonJS(exports_src);
|
|
58
59
|
var import_axios = __toESM(require("axios"));
|
|
@@ -85,12 +86,46 @@ class BaseModule {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// src/modules/ai.ts
|
|
90
|
+
class AIModule extends BaseModule {
|
|
91
|
+
async chat(workspaceId, body) {
|
|
92
|
+
const { data } = await this.api.post(`/ai/${workspaceId}/chat`, body, { timeout: 300000 });
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
async detectIntent(workspaceId, body) {
|
|
96
|
+
const { data } = await this.api.post(`/ai/${workspaceId}/chat/intent`, body, { timeout: 300000 });
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
async executeIntent(workspaceId, body) {
|
|
100
|
+
const { data } = await this.api.post(`/ai/${workspaceId}/chat/execute`, body, { timeout: 300000 });
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
async listSessions(workspaceId) {
|
|
104
|
+
const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
async getSession(workspaceId, sessionId) {
|
|
108
|
+
const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
getChatStreamUrl(workspaceId, sessionId) {
|
|
112
|
+
return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
|
|
113
|
+
}
|
|
114
|
+
async deleteSession(workspaceId, sessionId) {
|
|
115
|
+
await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
// src/modules/auth.ts
|
|
89
120
|
class AuthModule extends BaseModule {
|
|
90
|
-
async
|
|
121
|
+
async getProfile() {
|
|
91
122
|
const { data } = await this.api.get("/auth/me");
|
|
92
123
|
return data;
|
|
93
124
|
}
|
|
125
|
+
async getApiKeyInfo() {
|
|
126
|
+
const { data } = await this.api.get("/auth/api-key");
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
94
129
|
async requestRegisterOtp(email) {
|
|
95
130
|
const { data } = await this.api.post("/auth/register-otp", { email });
|
|
96
131
|
return data;
|
|
@@ -248,6 +283,10 @@ class SprintsModule extends BaseModule {
|
|
|
248
283
|
const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/complete`);
|
|
249
284
|
return data.sprint;
|
|
250
285
|
}
|
|
286
|
+
async triggerAIPlanning(id, workspaceId) {
|
|
287
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/trigger-ai-planning`);
|
|
288
|
+
return data.sprint;
|
|
289
|
+
}
|
|
251
290
|
}
|
|
252
291
|
|
|
253
292
|
// src/modules/tasks.ts
|
|
@@ -294,6 +333,16 @@ class TasksModule extends BaseModule {
|
|
|
294
333
|
const { data } = await this.api.post(`/workspaces/${workspaceId}/tasks/${id}/comment`, body);
|
|
295
334
|
return data.comment;
|
|
296
335
|
}
|
|
336
|
+
async batchUpdate(ids, workspaceId, updates) {
|
|
337
|
+
await this.api.patch(`/workspaces/${workspaceId}/tasks/batch`, {
|
|
338
|
+
ids,
|
|
339
|
+
updates
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async getContext(id, workspaceId) {
|
|
343
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
|
|
344
|
+
return data;
|
|
345
|
+
}
|
|
297
346
|
}
|
|
298
347
|
|
|
299
348
|
// src/modules/workspaces.ts
|
|
@@ -340,6 +389,17 @@ class WorkspacesModule extends BaseModule {
|
|
|
340
389
|
const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
|
|
341
390
|
return data.task;
|
|
342
391
|
}
|
|
392
|
+
async listApiKeys(workspaceId) {
|
|
393
|
+
const { data } = await this.api.get(`/workspaces/${workspaceId}/api-keys`);
|
|
394
|
+
return data.apiKeys;
|
|
395
|
+
}
|
|
396
|
+
async createApiKey(workspaceId, name) {
|
|
397
|
+
const { data } = await this.api.post(`/workspaces/${workspaceId}/api-keys`, { name });
|
|
398
|
+
return data.apiKey;
|
|
399
|
+
}
|
|
400
|
+
async deleteApiKey(workspaceId, keyId) {
|
|
401
|
+
await this.api.delete(`/workspaces/${workspaceId}/api-keys/${keyId}`);
|
|
402
|
+
}
|
|
343
403
|
}
|
|
344
404
|
|
|
345
405
|
// src/index.ts
|
|
@@ -347,6 +407,7 @@ class LocusClient {
|
|
|
347
407
|
api;
|
|
348
408
|
emitter;
|
|
349
409
|
auth;
|
|
410
|
+
ai;
|
|
350
411
|
tasks;
|
|
351
412
|
sprints;
|
|
352
413
|
workspaces;
|
|
@@ -358,7 +419,7 @@ class LocusClient {
|
|
|
358
419
|
this.emitter = new LocusEmitter;
|
|
359
420
|
this.api = import_axios.default.create({
|
|
360
421
|
baseURL: config.baseUrl,
|
|
361
|
-
timeout: config.timeout ||
|
|
422
|
+
timeout: config.timeout || 60000,
|
|
362
423
|
headers: {
|
|
363
424
|
"Content-Type": "application/json",
|
|
364
425
|
...config.token ? { Authorization: `Bearer ${config.token}` } : {}
|
|
@@ -366,6 +427,7 @@ class LocusClient {
|
|
|
366
427
|
});
|
|
367
428
|
this.setupInterceptors();
|
|
368
429
|
this.auth = new AuthModule(this.api, this.emitter);
|
|
430
|
+
this.ai = new AIModule(this.api, this.emitter);
|
|
369
431
|
this.tasks = new TasksModule(this.api, this.emitter);
|
|
370
432
|
this.sprints = new SprintsModule(this.api, this.emitter);
|
|
371
433
|
this.workspaces = new WorkspacesModule(this.api, this.emitter);
|
|
@@ -439,7 +501,6 @@ __export(exports_worker, {
|
|
|
439
501
|
AgentWorker: () => AgentWorker
|
|
440
502
|
});
|
|
441
503
|
module.exports = __toCommonJS(exports_worker);
|
|
442
|
-
var import_shared3 = require("@locusai/shared");
|
|
443
504
|
|
|
444
505
|
// src/core/config.ts
|
|
445
506
|
var import_node_path = require("node:path");
|
|
@@ -456,7 +517,9 @@ var LOCUS_CONFIG = {
|
|
|
456
517
|
configFile: "config.json",
|
|
457
518
|
indexFile: "codebase-index.json",
|
|
458
519
|
contextFile: "CLAUDE.md",
|
|
459
|
-
artifactsDir: "artifacts"
|
|
520
|
+
artifactsDir: "artifacts",
|
|
521
|
+
documentsDir: "documents",
|
|
522
|
+
agentSkillsDir: ".agent/skills"
|
|
460
523
|
};
|
|
461
524
|
function getLocusPath(projectPath, fileName) {
|
|
462
525
|
if (fileName === "contextFile") {
|
|
@@ -571,6 +634,7 @@ class ClaudeRunner {
|
|
|
571
634
|
let finalResult = "";
|
|
572
635
|
let errorOutput = "";
|
|
573
636
|
let buffer = "";
|
|
637
|
+
let stderrBuffer = "";
|
|
574
638
|
claude.stdout.on("data", (data) => {
|
|
575
639
|
buffer += data.toString();
|
|
576
640
|
const lines = buffer.split(`
|
|
@@ -583,14 +647,27 @@ class ClaudeRunner {
|
|
|
583
647
|
}
|
|
584
648
|
});
|
|
585
649
|
claude.stderr.on("data", (data) => {
|
|
586
|
-
const
|
|
587
|
-
errorOutput +=
|
|
588
|
-
|
|
650
|
+
const chunk = data.toString();
|
|
651
|
+
errorOutput += chunk;
|
|
652
|
+
stderrBuffer += chunk;
|
|
653
|
+
const lines = stderrBuffer.split(`
|
|
654
|
+
`);
|
|
655
|
+
stderrBuffer = lines.pop() || "";
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
if (!this.shouldSuppressLine(line)) {
|
|
658
|
+
process.stderr.write(`${line}
|
|
659
|
+
`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
589
662
|
});
|
|
590
663
|
claude.on("error", (err) => {
|
|
591
664
|
reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
|
|
592
665
|
});
|
|
593
666
|
claude.on("close", (code) => {
|
|
667
|
+
if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
|
|
668
|
+
process.stderr.write(`${stderrBuffer}
|
|
669
|
+
`);
|
|
670
|
+
}
|
|
594
671
|
process.stdout.write(`
|
|
595
672
|
`);
|
|
596
673
|
if (code === 0) {
|
|
@@ -623,12 +700,8 @@ class ClaudeRunner {
|
|
|
623
700
|
return null;
|
|
624
701
|
}
|
|
625
702
|
handleEvent(event) {
|
|
626
|
-
const { type,
|
|
627
|
-
if (type === "
|
|
628
|
-
if (delta.type === "text_delta" && delta.text) {
|
|
629
|
-
this.log?.(delta.text, "info");
|
|
630
|
-
}
|
|
631
|
-
} else if (type === "content_block_start" && content_block) {
|
|
703
|
+
const { type, content_block } = event;
|
|
704
|
+
if (type === "content_block_start" && content_block) {
|
|
632
705
|
if (content_block.type === "tool_use" && content_block.name) {
|
|
633
706
|
this.log?.(`
|
|
634
707
|
|
|
@@ -637,6 +710,10 @@ ${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
|
|
|
637
710
|
}
|
|
638
711
|
}
|
|
639
712
|
}
|
|
713
|
+
shouldSuppressLine(line) {
|
|
714
|
+
const infoLogRegex = /^\[\d{2}:\d{2}:\d{2}\]\s\[.*?\]\sℹ\s*$/;
|
|
715
|
+
return infoLogRegex.test(line.trim());
|
|
716
|
+
}
|
|
640
717
|
createExecutionError(code, detail) {
|
|
641
718
|
const errorMsg = detail.trim();
|
|
642
719
|
const message = errorMsg ? `Claude CLI error (exit code ${code}): ${errorMsg}` : `Claude CLI exited with code ${code}. Please ensure the Claude CLI is installed and you are logged in.`;
|
|
@@ -715,7 +792,13 @@ class CodexRunner {
|
|
|
715
792
|
});
|
|
716
793
|
}
|
|
717
794
|
buildArgs(outputPath) {
|
|
718
|
-
const args = [
|
|
795
|
+
const args = [
|
|
796
|
+
"exec",
|
|
797
|
+
"--full-auto",
|
|
798
|
+
"--skip-git-repo-check",
|
|
799
|
+
"--output-last-message",
|
|
800
|
+
outputPath
|
|
801
|
+
];
|
|
719
802
|
if (this.model) {
|
|
720
803
|
args.push("--model", this.model);
|
|
721
804
|
}
|
|
@@ -781,6 +864,7 @@ function createAiRunner(provider, config) {
|
|
|
781
864
|
// src/agent/artifact-syncer.ts
|
|
782
865
|
var import_node_fs2 = require("node:fs");
|
|
783
866
|
var import_node_path4 = require("node:path");
|
|
867
|
+
var import_shared2 = require("@locusai/shared");
|
|
784
868
|
class ArtifactSyncer {
|
|
785
869
|
deps;
|
|
786
870
|
constructor(deps) {
|
|
@@ -812,10 +896,13 @@ class ArtifactSyncer {
|
|
|
812
896
|
}
|
|
813
897
|
try {
|
|
814
898
|
const files = import_node_fs2.readdirSync(artifactsDir);
|
|
815
|
-
if (files.length === 0)
|
|
816
|
-
return;
|
|
817
899
|
this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
|
|
818
|
-
const
|
|
900
|
+
const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
|
|
901
|
+
const groupMap = new Map(groups.map((g) => [g.id, g.name]));
|
|
902
|
+
let artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
|
|
903
|
+
if (!artifactsGroupId) {
|
|
904
|
+
artifactsGroupId = await this.getOrCreateArtifactsGroup();
|
|
905
|
+
}
|
|
819
906
|
const existingDocs = await this.deps.client.docs.list(this.deps.workspaceId);
|
|
820
907
|
for (const file of files) {
|
|
821
908
|
const filePath = import_node_path4.join(artifactsDir, file);
|
|
@@ -834,12 +921,35 @@ class ArtifactSyncer {
|
|
|
834
921
|
await this.deps.client.docs.create(this.deps.workspaceId, {
|
|
835
922
|
title,
|
|
836
923
|
content,
|
|
837
|
-
groupId: artifactsGroupId
|
|
924
|
+
groupId: artifactsGroupId,
|
|
925
|
+
type: import_shared2.DocType.GENERAL
|
|
838
926
|
});
|
|
839
927
|
this.deps.log(`Created artifact: ${file}`, "success");
|
|
840
928
|
}
|
|
841
929
|
}
|
|
842
930
|
}
|
|
931
|
+
for (const doc of existingDocs) {
|
|
932
|
+
if (doc.groupId === artifactsGroupId) {
|
|
933
|
+
const fileName = `${doc.title}.md`;
|
|
934
|
+
const filePath = import_node_path4.join(artifactsDir, fileName);
|
|
935
|
+
if (!import_node_fs2.existsSync(filePath)) {
|
|
936
|
+
import_node_fs2.writeFileSync(filePath, doc.content || "");
|
|
937
|
+
this.deps.log(`Fetched artifact: ${fileName}`, "success");
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
|
|
941
|
+
const groupName = groupMap.get(doc.groupId || "") || "General";
|
|
942
|
+
const groupDir = import_node_path4.join(documentsDir, groupName);
|
|
943
|
+
if (!import_node_fs2.existsSync(groupDir)) {
|
|
944
|
+
import_node_fs2.mkdirSync(groupDir, { recursive: true });
|
|
945
|
+
}
|
|
946
|
+
const fileName = `${doc.title}.md`;
|
|
947
|
+
const filePath = import_node_path4.join(groupDir, fileName);
|
|
948
|
+
if (!import_node_fs2.existsSync(filePath) || import_node_fs2.readFileSync(filePath, "utf-8") !== doc.content) {
|
|
949
|
+
import_node_fs2.writeFileSync(filePath, doc.content || "");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
843
953
|
} catch (error) {
|
|
844
954
|
this.deps.log(`Failed to sync artifacts: ${error}`, "error");
|
|
845
955
|
}
|
|
@@ -1107,53 +1217,17 @@ Return ONLY valid JSON, no markdown formatting.`;
|
|
|
1107
1217
|
}
|
|
1108
1218
|
}
|
|
1109
1219
|
|
|
1110
|
-
// src/agent/sprint-planner.ts
|
|
1111
|
-
class SprintPlanner {
|
|
1112
|
-
deps;
|
|
1113
|
-
constructor(deps) {
|
|
1114
|
-
this.deps = deps;
|
|
1115
|
-
}
|
|
1116
|
-
async planSprint(sprint, tasks2) {
|
|
1117
|
-
this.deps.log(`Planning sprint: ${sprint.name}`, "info");
|
|
1118
|
-
try {
|
|
1119
|
-
const taskList = tasks2.map((t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`).join(`
|
|
1120
|
-
`);
|
|
1121
|
-
const planningPrompt = `# Sprint Planning: ${sprint.name}
|
|
1122
|
-
|
|
1123
|
-
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.
|
|
1124
|
-
|
|
1125
|
-
## Tasks
|
|
1126
|
-
${taskList}
|
|
1127
|
-
|
|
1128
|
-
## Instructions
|
|
1129
|
-
1. Analyze dependencies between these tasks.
|
|
1130
|
-
2. Prioritize them for the most efficient execution.
|
|
1131
|
-
3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
|
|
1132
|
-
4. Output your final plan. The plan should clearly state the order of execution.
|
|
1133
|
-
|
|
1134
|
-
**IMPORTANT**:
|
|
1135
|
-
- Do NOT create any files on the filesystem during this planning phase.
|
|
1136
|
-
- Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
|
|
1137
|
-
- Your output will be saved as the official sprint mindmap on the server.`;
|
|
1138
|
-
const plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
1139
|
-
this.deps.log("Sprint mindmap generated and posted to server.", "success");
|
|
1140
|
-
return plan;
|
|
1141
|
-
} catch (error) {
|
|
1142
|
-
this.deps.log(`Sprint planning failed: ${error}`, "error");
|
|
1143
|
-
return sprint.mindmap || "";
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
1220
|
// src/core/prompt-builder.ts
|
|
1149
1221
|
var import_node_fs4 = require("node:fs");
|
|
1150
|
-
var
|
|
1222
|
+
var import_node_os2 = require("node:os");
|
|
1223
|
+
var import_node_path6 = require("node:path");
|
|
1224
|
+
var import_shared3 = require("@locusai/shared");
|
|
1151
1225
|
class PromptBuilder {
|
|
1152
1226
|
projectPath;
|
|
1153
1227
|
constructor(projectPath) {
|
|
1154
1228
|
this.projectPath = projectPath;
|
|
1155
1229
|
}
|
|
1156
|
-
async build(task) {
|
|
1230
|
+
async build(task, options = {}) {
|
|
1157
1231
|
let prompt = `# Task: ${task.title}
|
|
1158
1232
|
|
|
1159
1233
|
`;
|
|
@@ -1168,18 +1242,81 @@ You are acting as a ${roleText}.
|
|
|
1168
1242
|
${task.description || "No description provided."}
|
|
1169
1243
|
|
|
1170
1244
|
`;
|
|
1245
|
+
const projectConfig = this.getProjectConfig();
|
|
1246
|
+
if (projectConfig) {
|
|
1247
|
+
prompt += `## Project Metadata
|
|
1248
|
+
`;
|
|
1249
|
+
prompt += `- Version: ${projectConfig.version || "Unknown"}
|
|
1250
|
+
`;
|
|
1251
|
+
prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
|
|
1252
|
+
|
|
1253
|
+
`;
|
|
1254
|
+
}
|
|
1255
|
+
let serverContext = null;
|
|
1256
|
+
if (options.taskContext) {
|
|
1257
|
+
try {
|
|
1258
|
+
serverContext = JSON.parse(options.taskContext);
|
|
1259
|
+
} catch {
|
|
1260
|
+
serverContext = { context: options.taskContext };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1171
1263
|
const contextPath = getLocusPath(this.projectPath, "contextFile");
|
|
1264
|
+
let hasLocalContext = false;
|
|
1172
1265
|
if (import_node_fs4.existsSync(contextPath)) {
|
|
1173
1266
|
try {
|
|
1174
1267
|
const context = import_node_fs4.readFileSync(contextPath, "utf-8");
|
|
1175
|
-
|
|
1268
|
+
if (context.trim().length > 20) {
|
|
1269
|
+
prompt += `## Project Context (Local)
|
|
1176
1270
|
${context}
|
|
1177
1271
|
|
|
1178
1272
|
`;
|
|
1273
|
+
hasLocalContext = true;
|
|
1274
|
+
}
|
|
1179
1275
|
} catch (err) {
|
|
1180
1276
|
console.warn(`Warning: Could not read context file: ${err}`);
|
|
1181
1277
|
}
|
|
1182
1278
|
}
|
|
1279
|
+
if (!hasLocalContext) {
|
|
1280
|
+
const fallback = this.getFallbackContext();
|
|
1281
|
+
if (fallback) {
|
|
1282
|
+
prompt += `## Project Context (README Fallback)
|
|
1283
|
+
${fallback}
|
|
1284
|
+
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (serverContext) {
|
|
1289
|
+
prompt += `## Project Context (Server)
|
|
1290
|
+
`;
|
|
1291
|
+
if (serverContext.project) {
|
|
1292
|
+
prompt += `- Project: ${serverContext.project.name || "Unknown"}
|
|
1293
|
+
`;
|
|
1294
|
+
if (!hasLocalContext && serverContext.project.techStack?.length) {
|
|
1295
|
+
prompt += `- Tech Stack: ${serverContext.project.techStack.join(", ")}
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (serverContext.context) {
|
|
1300
|
+
prompt += `
|
|
1301
|
+
${serverContext.context}
|
|
1302
|
+
`;
|
|
1303
|
+
}
|
|
1304
|
+
prompt += `
|
|
1305
|
+
`;
|
|
1306
|
+
}
|
|
1307
|
+
prompt += this.getProjectStructure();
|
|
1308
|
+
prompt += this.getSkillsInfo();
|
|
1309
|
+
prompt += `## Project Knowledge Base
|
|
1310
|
+
`;
|
|
1311
|
+
prompt += `You have access to the following documentation directories for context:
|
|
1312
|
+
`;
|
|
1313
|
+
prompt += `- Artifacts: \`.locus/artifacts\`
|
|
1314
|
+
`;
|
|
1315
|
+
prompt += `- Documents: \`.locus/documents\`
|
|
1316
|
+
`;
|
|
1317
|
+
prompt += `If you need more information about the project strategies, plans, or architecture, please read files in these directories.
|
|
1318
|
+
|
|
1319
|
+
`;
|
|
1183
1320
|
const indexPath = getLocusPath(this.projectPath, "indexFile");
|
|
1184
1321
|
if (import_node_fs4.existsSync(indexPath)) {
|
|
1185
1322
|
prompt += `## Codebase Overview
|
|
@@ -1188,11 +1325,19 @@ There is an index file in the .locus/codebase-index.json and if you need you can
|
|
|
1188
1325
|
`;
|
|
1189
1326
|
}
|
|
1190
1327
|
if (task.docs && task.docs.length > 0) {
|
|
1191
|
-
prompt += `## Attached Documents
|
|
1328
|
+
prompt += `## Attached Documents (Summarized)
|
|
1329
|
+
`;
|
|
1330
|
+
prompt += `> Full content available on server. Rely on Task Description for specific requirements.
|
|
1331
|
+
|
|
1192
1332
|
`;
|
|
1193
1333
|
for (const doc of task.docs) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1334
|
+
const content = doc.content || "";
|
|
1335
|
+
const limit = 800;
|
|
1336
|
+
const preview = content.slice(0, limit);
|
|
1337
|
+
const isTruncated = content.length > limit;
|
|
1338
|
+
prompt += `### Doc: ${doc.title}
|
|
1339
|
+
${preview}${isTruncated ? `
|
|
1340
|
+
...(truncated)...` : ""}
|
|
1196
1341
|
|
|
1197
1342
|
`;
|
|
1198
1343
|
}
|
|
@@ -1208,7 +1353,7 @@ ${doc.content || "(No content)"}
|
|
|
1208
1353
|
`;
|
|
1209
1354
|
}
|
|
1210
1355
|
if (task.comments && task.comments.length > 0) {
|
|
1211
|
-
const comments = task.comments.slice(0,
|
|
1356
|
+
const comments = task.comments.slice(0, 3);
|
|
1212
1357
|
prompt += `## Task History & Feedback
|
|
1213
1358
|
`;
|
|
1214
1359
|
prompt += `Review the following comments for context or rejection feedback:
|
|
@@ -1230,20 +1375,122 @@ ${comment.text}
|
|
|
1230
1375
|
`;
|
|
1231
1376
|
return prompt;
|
|
1232
1377
|
}
|
|
1378
|
+
getProjectConfig() {
|
|
1379
|
+
const configPath = getLocusPath(this.projectPath, "configFile");
|
|
1380
|
+
if (import_node_fs4.existsSync(configPath)) {
|
|
1381
|
+
try {
|
|
1382
|
+
return JSON.parse(import_node_fs4.readFileSync(configPath, "utf-8"));
|
|
1383
|
+
} catch {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
getFallbackContext() {
|
|
1390
|
+
const readmePath = import_node_path6.join(this.projectPath, "README.md");
|
|
1391
|
+
if (import_node_fs4.existsSync(readmePath)) {
|
|
1392
|
+
try {
|
|
1393
|
+
const content = import_node_fs4.readFileSync(readmePath, "utf-8");
|
|
1394
|
+
const limit = 1000;
|
|
1395
|
+
return content.slice(0, limit) + (content.length > limit ? `
|
|
1396
|
+
...(truncated)...` : "");
|
|
1397
|
+
} catch {
|
|
1398
|
+
return "";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return "";
|
|
1402
|
+
}
|
|
1403
|
+
getProjectStructure() {
|
|
1404
|
+
try {
|
|
1405
|
+
const entries = import_node_fs4.readdirSync(this.projectPath);
|
|
1406
|
+
const folders = entries.filter((e) => {
|
|
1407
|
+
if (e.startsWith(".") || e === "node_modules")
|
|
1408
|
+
return false;
|
|
1409
|
+
try {
|
|
1410
|
+
return import_node_fs4.statSync(import_node_path6.join(this.projectPath, e)).isDirectory();
|
|
1411
|
+
} catch {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
if (folders.length === 0)
|
|
1416
|
+
return "";
|
|
1417
|
+
let structure = `## Project Structure
|
|
1418
|
+
`;
|
|
1419
|
+
structure += `Key directories in this project:
|
|
1420
|
+
`;
|
|
1421
|
+
for (const folder of folders) {
|
|
1422
|
+
structure += `- \`${folder}/\`
|
|
1423
|
+
`;
|
|
1424
|
+
}
|
|
1425
|
+
return `${structure}
|
|
1426
|
+
`;
|
|
1427
|
+
} catch {
|
|
1428
|
+
return "";
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
getSkillsInfo() {
|
|
1432
|
+
const projectSkillsDirs = [
|
|
1433
|
+
LOCUS_CONFIG.agentSkillsDir,
|
|
1434
|
+
".cursor/skills",
|
|
1435
|
+
".claude/skills",
|
|
1436
|
+
".codex/skills",
|
|
1437
|
+
".gemini/skills"
|
|
1438
|
+
];
|
|
1439
|
+
const globalHome = import_node_os2.homedir();
|
|
1440
|
+
const globalSkillsDirs = [
|
|
1441
|
+
import_node_path6.join(globalHome, ".cursor/skills"),
|
|
1442
|
+
import_node_path6.join(globalHome, ".claude/skills"),
|
|
1443
|
+
import_node_path6.join(globalHome, ".codex/skills"),
|
|
1444
|
+
import_node_path6.join(globalHome, ".gemini/skills")
|
|
1445
|
+
];
|
|
1446
|
+
const allSkillNames = new Set;
|
|
1447
|
+
for (const relativePath of projectSkillsDirs) {
|
|
1448
|
+
const fullPath = import_node_path6.join(this.projectPath, relativePath);
|
|
1449
|
+
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1450
|
+
}
|
|
1451
|
+
for (const fullPath of globalSkillsDirs) {
|
|
1452
|
+
this.scanSkillsInDirectory(fullPath, allSkillNames);
|
|
1453
|
+
}
|
|
1454
|
+
const uniqueSkills = Array.from(allSkillNames).sort();
|
|
1455
|
+
if (uniqueSkills.length === 0)
|
|
1456
|
+
return "";
|
|
1457
|
+
return `## Available Agent Skills
|
|
1458
|
+
` + `The project has the following specialized skills available (from project or global locations):
|
|
1459
|
+
` + uniqueSkills.map((s) => `- ${s}`).join(`
|
|
1460
|
+
`) + `
|
|
1461
|
+
|
|
1462
|
+
`;
|
|
1463
|
+
}
|
|
1464
|
+
scanSkillsInDirectory(dirPath, skillSet) {
|
|
1465
|
+
if (!import_node_fs4.existsSync(dirPath))
|
|
1466
|
+
return;
|
|
1467
|
+
try {
|
|
1468
|
+
const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
|
|
1469
|
+
try {
|
|
1470
|
+
return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
|
|
1471
|
+
} catch {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
for (const entry of entries) {
|
|
1476
|
+
skillSet.add(entry);
|
|
1477
|
+
}
|
|
1478
|
+
} catch {}
|
|
1479
|
+
}
|
|
1233
1480
|
roleToText(role) {
|
|
1234
1481
|
if (!role) {
|
|
1235
1482
|
return null;
|
|
1236
1483
|
}
|
|
1237
1484
|
switch (role) {
|
|
1238
|
-
case
|
|
1485
|
+
case import_shared3.AssigneeRole.BACKEND:
|
|
1239
1486
|
return "Backend Engineer";
|
|
1240
|
-
case
|
|
1487
|
+
case import_shared3.AssigneeRole.FRONTEND:
|
|
1241
1488
|
return "Frontend Engineer";
|
|
1242
|
-
case
|
|
1489
|
+
case import_shared3.AssigneeRole.PM:
|
|
1243
1490
|
return "Product Manager";
|
|
1244
|
-
case
|
|
1491
|
+
case import_shared3.AssigneeRole.QA:
|
|
1245
1492
|
return "QA Engineer";
|
|
1246
|
-
case
|
|
1493
|
+
case import_shared3.AssigneeRole.DESIGN:
|
|
1247
1494
|
return "Product Designer";
|
|
1248
1495
|
default:
|
|
1249
1496
|
return "engineer";
|
|
@@ -1259,18 +1506,11 @@ class TaskExecutor {
|
|
|
1259
1506
|
this.deps = deps;
|
|
1260
1507
|
this.promptBuilder = new PromptBuilder(deps.projectPath);
|
|
1261
1508
|
}
|
|
1262
|
-
|
|
1263
|
-
this.deps.sprintPlan = sprintPlan;
|
|
1264
|
-
}
|
|
1265
|
-
async execute(task) {
|
|
1509
|
+
async execute(task, context) {
|
|
1266
1510
|
this.deps.log(`Executing: ${task.title}`, "info");
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
${this.deps.sprintPlan}
|
|
1271
|
-
|
|
1272
|
-
${basePrompt}`;
|
|
1273
|
-
}
|
|
1511
|
+
const basePrompt = await this.promptBuilder.build(task, {
|
|
1512
|
+
taskContext: context
|
|
1513
|
+
});
|
|
1274
1514
|
try {
|
|
1275
1515
|
let plan = null;
|
|
1276
1516
|
if (this.deps.skipPlanning) {
|
|
@@ -1329,16 +1569,14 @@ class AgentWorker {
|
|
|
1329
1569
|
config;
|
|
1330
1570
|
client;
|
|
1331
1571
|
aiRunner;
|
|
1332
|
-
sprintPlanner;
|
|
1333
1572
|
indexerService;
|
|
1334
1573
|
artifactSyncer;
|
|
1335
1574
|
taskExecutor;
|
|
1336
1575
|
consecutiveEmpty = 0;
|
|
1337
|
-
maxEmpty =
|
|
1576
|
+
maxEmpty = 60;
|
|
1338
1577
|
maxTasks = 50;
|
|
1339
1578
|
tasksCompleted = 0;
|
|
1340
1579
|
pollInterval = 1e4;
|
|
1341
|
-
sprintPlan = null;
|
|
1342
1580
|
constructor(config) {
|
|
1343
1581
|
this.config = config;
|
|
1344
1582
|
const projectPath = config.projectPath || process.cwd();
|
|
@@ -1359,10 +1597,6 @@ class AgentWorker {
|
|
|
1359
1597
|
model: config.model,
|
|
1360
1598
|
log
|
|
1361
1599
|
});
|
|
1362
|
-
this.sprintPlanner = new SprintPlanner({
|
|
1363
|
-
aiRunner: this.aiRunner,
|
|
1364
|
-
log
|
|
1365
|
-
});
|
|
1366
1600
|
this.indexerService = new CodebaseIndexerService({
|
|
1367
1601
|
aiRunner: this.aiRunner,
|
|
1368
1602
|
projectPath,
|
|
@@ -1377,7 +1611,6 @@ class AgentWorker {
|
|
|
1377
1611
|
this.taskExecutor = new TaskExecutor({
|
|
1378
1612
|
aiRunner: this.aiRunner,
|
|
1379
1613
|
projectPath,
|
|
1380
|
-
sprintPlan: null,
|
|
1381
1614
|
skipPlanning: config.skipPlanning,
|
|
1382
1615
|
log
|
|
1383
1616
|
});
|
|
@@ -1416,8 +1649,13 @@ class AgentWorker {
|
|
|
1416
1649
|
}
|
|
1417
1650
|
async executeTask(task) {
|
|
1418
1651
|
const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
|
|
1419
|
-
|
|
1420
|
-
|
|
1652
|
+
let context = "";
|
|
1653
|
+
try {
|
|
1654
|
+
context = await this.client.tasks.getContext(task.id, this.config.workspaceId);
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
this.log(`Failed to fetch task context: ${err}`, "warn");
|
|
1657
|
+
}
|
|
1658
|
+
const result = await this.taskExecutor.execute(fullTask, context);
|
|
1421
1659
|
await this.indexerService.reindex();
|
|
1422
1660
|
return result;
|
|
1423
1661
|
}
|
|
@@ -1425,35 +1663,12 @@ class AgentWorker {
|
|
|
1425
1663
|
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
1426
1664
|
const sprint = await this.getActiveSprint();
|
|
1427
1665
|
if (sprint) {
|
|
1428
|
-
this.log(`
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if (activeTasks.length <= 1) {
|
|
1435
|
-
this.log("Skipping mindmap generation (only one task in sprint).", "info");
|
|
1436
|
-
this.sprintPlan = null;
|
|
1437
|
-
} else {
|
|
1438
|
-
const latestTaskCreation = activeTasks.reduce((latest, task) => {
|
|
1439
|
-
const taskDate = new Date(task.createdAt);
|
|
1440
|
-
return taskDate > latest ? taskDate : latest;
|
|
1441
|
-
}, new Date(0));
|
|
1442
|
-
const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
|
|
1443
|
-
const needsPlanning = !sprint.mindmap || sprint.mindmap.trim() === "" || latestTaskCreation > mindmapDate;
|
|
1444
|
-
if (needsPlanning) {
|
|
1445
|
-
if (sprint.mindmap && latestTaskCreation > mindmapDate) {
|
|
1446
|
-
this.log("New tasks have been added to the sprint since last mindmap. Regenerating...", "warn");
|
|
1447
|
-
}
|
|
1448
|
-
this.sprintPlan = await this.sprintPlanner.planSprint(sprint, tasks2);
|
|
1449
|
-
await this.client.sprints.update(sprint.id, this.config.workspaceId, {
|
|
1450
|
-
mindmap: this.sprintPlan,
|
|
1451
|
-
mindmapUpdatedAt: new Date
|
|
1452
|
-
});
|
|
1453
|
-
} else {
|
|
1454
|
-
this.log("Using existing sprint mindmap.", "info");
|
|
1455
|
-
this.sprintPlan = sprint.mindmap ?? null;
|
|
1456
|
-
}
|
|
1666
|
+
this.log(`Active sprint found: ${sprint.name}. Ensuring plan is up to date...`, "info");
|
|
1667
|
+
try {
|
|
1668
|
+
await this.client.sprints.triggerAIPlanning(sprint.id, this.config.workspaceId);
|
|
1669
|
+
this.log(`Sprint plan sync checked on server.`, "success");
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
1457
1672
|
}
|
|
1458
1673
|
} else {
|
|
1459
1674
|
this.log("No active sprint found for planning.", "warn");
|
|
@@ -1473,7 +1688,11 @@ class AgentWorker {
|
|
|
1473
1688
|
this.consecutiveEmpty = 0;
|
|
1474
1689
|
this.log(`Claimed: ${task.title}`, "success");
|
|
1475
1690
|
const result = await this.executeTask(task);
|
|
1476
|
-
|
|
1691
|
+
try {
|
|
1692
|
+
await this.artifactSyncer.sync();
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
this.log(`Artifact sync failed: ${err}`, "error");
|
|
1695
|
+
}
|
|
1477
1696
|
if (result.success) {
|
|
1478
1697
|
this.log(`Completed: ${task.title}`, "success");
|
|
1479
1698
|
await this.client.tasks.update(task.id, this.config.workspaceId, {
|
|
@@ -1510,7 +1729,7 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
|
|
|
1510
1729
|
config.workspaceId = args[++i];
|
|
1511
1730
|
else if (arg === "--sprint-id")
|
|
1512
1731
|
config.sprintId = args[++i];
|
|
1513
|
-
else if (arg === "--api-
|
|
1732
|
+
else if (arg === "--api-url")
|
|
1514
1733
|
config.apiBase = args[++i];
|
|
1515
1734
|
else if (arg === "--api-key")
|
|
1516
1735
|
config.apiKey = args[++i];
|
|
@@ -1547,7 +1766,6 @@ __export(exports_index_node, {
|
|
|
1547
1766
|
TasksModule: () => TasksModule,
|
|
1548
1767
|
TaskExecutor: () => TaskExecutor,
|
|
1549
1768
|
SprintsModule: () => SprintsModule,
|
|
1550
|
-
SprintPlanner: () => SprintPlanner,
|
|
1551
1769
|
PromptBuilder: () => PromptBuilder,
|
|
1552
1770
|
PROVIDER: () => PROVIDER,
|
|
1553
1771
|
OrganizationsModule: () => OrganizationsModule,
|
|
@@ -1566,13 +1784,14 @@ __export(exports_index_node, {
|
|
|
1566
1784
|
AuthModule: () => AuthModule,
|
|
1567
1785
|
ArtifactSyncer: () => ArtifactSyncer,
|
|
1568
1786
|
AgentWorker: () => AgentWorker,
|
|
1569
|
-
AgentOrchestrator: () => AgentOrchestrator
|
|
1787
|
+
AgentOrchestrator: () => AgentOrchestrator,
|
|
1788
|
+
AIModule: () => AIModule
|
|
1570
1789
|
});
|
|
1571
1790
|
module.exports = __toCommonJS(exports_index_node);
|
|
1572
1791
|
// src/orchestrator.ts
|
|
1573
1792
|
var import_node_child_process3 = require("node:child_process");
|
|
1574
1793
|
var import_node_fs5 = require("node:fs");
|
|
1575
|
-
var
|
|
1794
|
+
var import_node_path7 = require("node:path");
|
|
1576
1795
|
var import_node_url = require("node:url");
|
|
1577
1796
|
var import_shared4 = require("@locusai/shared");
|
|
1578
1797
|
var import_events3 = require("events");
|
|
@@ -1668,8 +1887,8 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
1668
1887
|
`);
|
|
1669
1888
|
const potentialPaths = [];
|
|
1670
1889
|
const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
|
|
1671
|
-
const currentModuleDir =
|
|
1672
|
-
potentialPaths.push(
|
|
1890
|
+
const currentModuleDir = import_node_path7.dirname(currentModulePath);
|
|
1891
|
+
potentialPaths.push(import_node_path7.join(currentModuleDir, "agent", "worker.js"), import_node_path7.join(currentModuleDir, "worker.js"), import_node_path7.join(currentModuleDir, "agent", "worker.ts"));
|
|
1673
1892
|
const workerPath = potentialPaths.find((p) => import_node_fs5.existsSync(p));
|
|
1674
1893
|
if (!workerPath) {
|
|
1675
1894
|
throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
|
|
@@ -1679,7 +1898,7 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
1679
1898
|
agentId,
|
|
1680
1899
|
"--workspace-id",
|
|
1681
1900
|
this.config.workspaceId,
|
|
1682
|
-
"--api-
|
|
1901
|
+
"--api-url",
|
|
1683
1902
|
this.config.apiBase,
|
|
1684
1903
|
"--api-key",
|
|
1685
1904
|
this.config.apiKey,
|