@locusai/cli 0.8.0 → 0.8.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/README.md +155 -0
- package/bin/agent/worker.js +103 -46
- package/bin/locus.js +303 -128
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @locusai/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for [Locus](https://locusai.dev) - an AI-native project management platform for engineering teams.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @locusai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with other package managers:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add -g @locusai/cli
|
|
16
|
+
|
|
17
|
+
# yarn
|
|
18
|
+
yarn global add @locusai/cli
|
|
19
|
+
|
|
20
|
+
# bun
|
|
21
|
+
bun add -g @locusai/cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Initialize Locus in your project
|
|
28
|
+
locus init
|
|
29
|
+
|
|
30
|
+
# Index your codebase for AI context
|
|
31
|
+
locus index
|
|
32
|
+
|
|
33
|
+
# Run an agent to work on tasks
|
|
34
|
+
locus run --api-key YOUR_API_KEY
|
|
35
|
+
|
|
36
|
+
# Execute a prompt with repository context
|
|
37
|
+
locus exec "Explain the authentication flow"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### `locus init`
|
|
43
|
+
|
|
44
|
+
Initialize Locus in the current directory. Creates the necessary configuration files and directory structure:
|
|
45
|
+
|
|
46
|
+
- `.locus/` - Configuration directory
|
|
47
|
+
- `.locus/config.json` - Project settings
|
|
48
|
+
- `CLAUDE.md` - AI instructions and context
|
|
49
|
+
- `.agent/skills/` - Domain-specific agent skills
|
|
50
|
+
|
|
51
|
+
Running `init` on an already initialized project will update the configuration to the latest version.
|
|
52
|
+
|
|
53
|
+
### `locus index`
|
|
54
|
+
|
|
55
|
+
Index the codebase for AI context. This analyzes your project structure and creates a searchable index that helps AI agents understand your codebase.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
locus index [options]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--dir <path> Project directory (default: current directory)
|
|
62
|
+
--model <name> AI model to use
|
|
63
|
+
--provider <name> AI provider: claude or codex (default: claude)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `locus run`
|
|
67
|
+
|
|
68
|
+
Start an agent to work on tasks from your Locus workspace.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
locus run [options]
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
--api-key <key> Your Locus API key (required)
|
|
75
|
+
--workspace <id> Workspace ID to connect to
|
|
76
|
+
--sprint <id> Sprint ID to work on
|
|
77
|
+
--model <name> AI model to use
|
|
78
|
+
--provider <name> AI provider: claude or codex (default: claude)
|
|
79
|
+
--api-url <url> Custom API URL
|
|
80
|
+
--dir <path> Project directory (default: current directory)
|
|
81
|
+
--skip-planning Skip the planning phase
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `locus exec`
|
|
85
|
+
|
|
86
|
+
Run a prompt with repository context. Supports both single execution and interactive REPL mode.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
locus exec "your prompt" [options]
|
|
90
|
+
locus exec --interactive [options]
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
--interactive, -i Start interactive REPL mode
|
|
94
|
+
--session, -s <id> Resume a previous session
|
|
95
|
+
--model <name> AI model to use
|
|
96
|
+
--provider <name> AI provider: claude or codex (default: claude)
|
|
97
|
+
--dir <path> Project directory (default: current directory)
|
|
98
|
+
--no-stream Disable streaming output
|
|
99
|
+
--no-status Disable status display
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Session Management
|
|
103
|
+
|
|
104
|
+
Manage your exec sessions with these subcommands:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List recent sessions
|
|
108
|
+
locus exec sessions list
|
|
109
|
+
|
|
110
|
+
# Show messages from a session
|
|
111
|
+
locus exec sessions show <session-id>
|
|
112
|
+
|
|
113
|
+
# Delete a session
|
|
114
|
+
locus exec sessions delete <session-id>
|
|
115
|
+
|
|
116
|
+
# Clear all sessions
|
|
117
|
+
locus exec sessions clear
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
Locus stores its configuration in the `.locus/` directory within your project:
|
|
123
|
+
|
|
124
|
+
- `config.json` - Project settings including workspace ID and version
|
|
125
|
+
- `codebase-index.json` - Indexed codebase structure
|
|
126
|
+
|
|
127
|
+
The `CLAUDE.md` file in your project root provides AI instructions and context that agents use when working on your codebase.
|
|
128
|
+
|
|
129
|
+
## AI Providers
|
|
130
|
+
|
|
131
|
+
Locus supports multiple AI providers:
|
|
132
|
+
|
|
133
|
+
- **Claude** (default) - Anthropic's Claude models
|
|
134
|
+
- **Codex** - OpenAI Codex models
|
|
135
|
+
|
|
136
|
+
Specify the provider with the `--provider` flag:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
locus exec "your prompt" --provider codex
|
|
140
|
+
locus run --api-key YOUR_KEY --provider claude
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
|
|
145
|
+
- Node.js 18 or later
|
|
146
|
+
- A Locus API key (for `run` command)
|
|
147
|
+
|
|
148
|
+
## Links
|
|
149
|
+
|
|
150
|
+
- [Documentation](https://locusai.dev/docs)
|
|
151
|
+
- [Website](https://locusai.dev)
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/bin/agent/worker.js
CHANGED
|
@@ -17011,6 +17011,10 @@ var require_ignore = __commonJS((exports, module) => {
|
|
|
17011
17011
|
define(module.exports, Symbol.for("setupWindows"), setupWindows);
|
|
17012
17012
|
});
|
|
17013
17013
|
|
|
17014
|
+
// ../sdk/src/agent/worker.ts
|
|
17015
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
17016
|
+
import { join as join6 } from "node:path";
|
|
17017
|
+
|
|
17014
17018
|
// ../sdk/src/core/config.ts
|
|
17015
17019
|
import { join } from "node:path";
|
|
17016
17020
|
var PROVIDER = {
|
|
@@ -17029,7 +17033,9 @@ var LOCUS_CONFIG = {
|
|
|
17029
17033
|
artifactsDir: "artifacts",
|
|
17030
17034
|
documentsDir: "documents",
|
|
17031
17035
|
agentSkillsDir: ".agent/skills",
|
|
17032
|
-
sessionsDir: "sessions"
|
|
17036
|
+
sessionsDir: "sessions",
|
|
17037
|
+
reviewsDir: "reviews",
|
|
17038
|
+
plansDir: "plans"
|
|
17033
17039
|
};
|
|
17034
17040
|
function getLocusPath(projectPath, fileName) {
|
|
17035
17041
|
if (fileName === "contextFile") {
|
|
@@ -17120,7 +17126,7 @@ class ClaudeRunner {
|
|
|
17120
17126
|
setEventEmitter(emitter) {
|
|
17121
17127
|
this.eventEmitter = emitter;
|
|
17122
17128
|
}
|
|
17123
|
-
async run(prompt
|
|
17129
|
+
async run(prompt) {
|
|
17124
17130
|
const maxRetries = 3;
|
|
17125
17131
|
let lastError = null;
|
|
17126
17132
|
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
@@ -36686,7 +36692,7 @@ File tree:
|
|
|
36686
36692
|
${tree}
|
|
36687
36693
|
|
|
36688
36694
|
Return ONLY valid JSON, no markdown formatting.`;
|
|
36689
|
-
const response = await this.deps.aiRunner.run(prompt
|
|
36695
|
+
const response = await this.deps.aiRunner.run(prompt);
|
|
36690
36696
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
36691
36697
|
if (jsonMatch) {
|
|
36692
36698
|
return JSON.parse(jsonMatch[0]);
|
|
@@ -36749,6 +36755,66 @@ class DocumentFetcher {
|
|
|
36749
36755
|
}
|
|
36750
36756
|
}
|
|
36751
36757
|
|
|
36758
|
+
// ../sdk/src/agent/review-service.ts
|
|
36759
|
+
import { execSync } from "node:child_process";
|
|
36760
|
+
|
|
36761
|
+
class ReviewService {
|
|
36762
|
+
deps;
|
|
36763
|
+
constructor(deps) {
|
|
36764
|
+
this.deps = deps;
|
|
36765
|
+
}
|
|
36766
|
+
async reviewStagedChanges(sprint2) {
|
|
36767
|
+
const { projectPath, log } = this.deps;
|
|
36768
|
+
try {
|
|
36769
|
+
execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
36770
|
+
log("Staged all changes for review.", "info");
|
|
36771
|
+
} catch (err) {
|
|
36772
|
+
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
36773
|
+
return null;
|
|
36774
|
+
}
|
|
36775
|
+
let diff;
|
|
36776
|
+
try {
|
|
36777
|
+
diff = execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
36778
|
+
cwd: projectPath,
|
|
36779
|
+
maxBuffer: 10 * 1024 * 1024
|
|
36780
|
+
}).toString();
|
|
36781
|
+
} catch (err) {
|
|
36782
|
+
log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
36783
|
+
return null;
|
|
36784
|
+
}
|
|
36785
|
+
if (!diff.trim()) {
|
|
36786
|
+
return null;
|
|
36787
|
+
}
|
|
36788
|
+
const sprintInfo = sprint2 ? `Sprint: ${sprint2.name} (${sprint2.id})` : "No active sprint";
|
|
36789
|
+
const reviewPrompt = `# Code Review Request
|
|
36790
|
+
|
|
36791
|
+
## Context
|
|
36792
|
+
${sprintInfo}
|
|
36793
|
+
Date: ${new Date().toISOString()}
|
|
36794
|
+
|
|
36795
|
+
## Staged Changes (git diff)
|
|
36796
|
+
\`\`\`diff
|
|
36797
|
+
${diff}
|
|
36798
|
+
\`\`\`
|
|
36799
|
+
|
|
36800
|
+
## Instructions
|
|
36801
|
+
You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
|
|
36802
|
+
|
|
36803
|
+
1. **Summary** — Brief overview of what changed and why.
|
|
36804
|
+
2. **Files Changed** — List each file with a short description of changes.
|
|
36805
|
+
3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
|
|
36806
|
+
4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
|
|
36807
|
+
5. **Recommendations** — Actionable suggestions for improvement.
|
|
36808
|
+
6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
|
|
36809
|
+
|
|
36810
|
+
Keep the review concise but thorough. Focus on substance over style.
|
|
36811
|
+
Do NOT output <promise>COMPLETE</promise> — just output the review report as markdown.`;
|
|
36812
|
+
log("Running AI review on staged changes...", "info");
|
|
36813
|
+
const report = await this.deps.aiRunner.run(reviewPrompt);
|
|
36814
|
+
return report;
|
|
36815
|
+
}
|
|
36816
|
+
}
|
|
36817
|
+
|
|
36752
36818
|
// ../sdk/src/core/prompt-builder.ts
|
|
36753
36819
|
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
36754
36820
|
import { homedir } from "node:os";
|
|
@@ -37113,29 +37179,8 @@ class TaskExecutor {
|
|
|
37113
37179
|
taskContext: context
|
|
37114
37180
|
});
|
|
37115
37181
|
try {
|
|
37116
|
-
let plan = null;
|
|
37117
|
-
this.deps.log("Phase 1: Planning (CLI)...", "info");
|
|
37118
|
-
const planningPrompt = `${basePrompt}
|
|
37119
|
-
|
|
37120
|
-
## Phase 1: Planning
|
|
37121
|
-
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
|
|
37122
|
-
plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
37123
37182
|
this.deps.log("Starting Execution...", "info");
|
|
37124
|
-
|
|
37125
|
-
if (plan != null) {
|
|
37126
|
-
executionPrompt += `
|
|
37127
|
-
|
|
37128
|
-
## Phase 2: Execution
|
|
37129
|
-
Based on the plan, execute the task:
|
|
37130
|
-
|
|
37131
|
-
${plan}`;
|
|
37132
|
-
} else {
|
|
37133
|
-
executionPrompt += `
|
|
37134
|
-
|
|
37135
|
-
## Execution
|
|
37136
|
-
Execute the task directly.`;
|
|
37137
|
-
}
|
|
37138
|
-
executionPrompt += `
|
|
37183
|
+
const executionPrompt = `${basePrompt}
|
|
37139
37184
|
|
|
37140
37185
|
When finished, output: <promise>COMPLETE</promise>`;
|
|
37141
37186
|
const output = await this.deps.aiRunner.run(executionPrompt);
|
|
@@ -37169,11 +37214,9 @@ class AgentWorker {
|
|
|
37169
37214
|
indexerService;
|
|
37170
37215
|
documentFetcher;
|
|
37171
37216
|
taskExecutor;
|
|
37172
|
-
|
|
37173
|
-
maxEmpty = 60;
|
|
37217
|
+
reviewService;
|
|
37174
37218
|
maxTasks = 50;
|
|
37175
37219
|
tasksCompleted = 0;
|
|
37176
|
-
pollInterval = 1e4;
|
|
37177
37220
|
constructor(config2) {
|
|
37178
37221
|
this.config = config2;
|
|
37179
37222
|
const projectPath = config2.projectPath || process.cwd();
|
|
@@ -37210,6 +37253,11 @@ class AgentWorker {
|
|
|
37210
37253
|
projectPath,
|
|
37211
37254
|
log
|
|
37212
37255
|
});
|
|
37256
|
+
this.reviewService = new ReviewService({
|
|
37257
|
+
aiRunner: this.aiRunner,
|
|
37258
|
+
projectPath,
|
|
37259
|
+
log
|
|
37260
|
+
});
|
|
37213
37261
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
37214
37262
|
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
37215
37263
|
}
|
|
@@ -37255,33 +37303,42 @@ class AgentWorker {
|
|
|
37255
37303
|
await this.indexerService.reindex();
|
|
37256
37304
|
return result;
|
|
37257
37305
|
}
|
|
37306
|
+
async runStagedChangesReview(sprint2) {
|
|
37307
|
+
try {
|
|
37308
|
+
const report = await this.reviewService.reviewStagedChanges(sprint2);
|
|
37309
|
+
if (report) {
|
|
37310
|
+
const reviewsDir = join6(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
|
|
37311
|
+
if (!existsSync5(reviewsDir)) {
|
|
37312
|
+
mkdirSync3(reviewsDir, { recursive: true });
|
|
37313
|
+
}
|
|
37314
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
37315
|
+
const sprintSlug = sprint2?.name ? sprint2.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
|
|
37316
|
+
const fileName = `review-${sprintSlug}-${timestamp}.md`;
|
|
37317
|
+
const filePath = join6(reviewsDir, fileName);
|
|
37318
|
+
writeFileSync3(filePath, report);
|
|
37319
|
+
this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
|
|
37320
|
+
} else {
|
|
37321
|
+
this.log("No staged changes to review.", "info");
|
|
37322
|
+
}
|
|
37323
|
+
} catch (err) {
|
|
37324
|
+
this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
37325
|
+
}
|
|
37326
|
+
}
|
|
37258
37327
|
async run() {
|
|
37259
37328
|
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
37260
37329
|
const sprint2 = await this.getActiveSprint();
|
|
37261
37330
|
if (sprint2) {
|
|
37262
|
-
this.log(`Active sprint found: ${sprint2.name}
|
|
37263
|
-
try {
|
|
37264
|
-
await this.client.sprints.triggerAIPlanning(sprint2.id, this.config.workspaceId);
|
|
37265
|
-
this.log(`Sprint plan sync checked on server.`, "success");
|
|
37266
|
-
} catch (err) {
|
|
37267
|
-
this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
37268
|
-
}
|
|
37331
|
+
this.log(`Active sprint found: ${sprint2.name}`, "info");
|
|
37269
37332
|
} else {
|
|
37270
|
-
this.log("No active sprint found
|
|
37333
|
+
this.log("No active sprint found.", "warn");
|
|
37271
37334
|
}
|
|
37272
|
-
while (this.tasksCompleted < this.maxTasks
|
|
37335
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
37273
37336
|
const task2 = await this.getNextTask();
|
|
37274
37337
|
if (!task2) {
|
|
37275
|
-
|
|
37276
|
-
|
|
37277
|
-
|
|
37278
|
-
this.consecutiveEmpty++;
|
|
37279
|
-
if (this.consecutiveEmpty >= this.maxEmpty)
|
|
37280
|
-
break;
|
|
37281
|
-
await new Promise((r) => setTimeout(r, this.pollInterval));
|
|
37282
|
-
continue;
|
|
37338
|
+
this.log("No tasks remaining. Running review on staged changes...", "info");
|
|
37339
|
+
await this.runStagedChangesReview(sprint2);
|
|
37340
|
+
break;
|
|
37283
37341
|
}
|
|
37284
|
-
this.consecutiveEmpty = 0;
|
|
37285
37342
|
this.log(`Claimed: ${task2.title}`, "success");
|
|
37286
37343
|
const result = await this.executeTask(task2);
|
|
37287
37344
|
try {
|
package/bin/locus.js
CHANGED
|
@@ -6341,7 +6341,7 @@ File tree:
|
|
|
6341
6341
|
${tree}
|
|
6342
6342
|
|
|
6343
6343
|
Return ONLY valid JSON, no markdown formatting.`;
|
|
6344
|
-
const response = await this.deps.aiRunner.run(prompt
|
|
6344
|
+
const response = await this.deps.aiRunner.run(prompt);
|
|
6345
6345
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
6346
6346
|
if (jsonMatch) {
|
|
6347
6347
|
return JSON.parse(jsonMatch[0]);
|
|
@@ -6389,14 +6389,22 @@ var init_config = __esm(() => {
|
|
|
6389
6389
|
artifactsDir: "artifacts",
|
|
6390
6390
|
documentsDir: "documents",
|
|
6391
6391
|
agentSkillsDir: ".agent/skills",
|
|
6392
|
-
sessionsDir: "sessions"
|
|
6392
|
+
sessionsDir: "sessions",
|
|
6393
|
+
reviewsDir: "reviews",
|
|
6394
|
+
plansDir: "plans"
|
|
6393
6395
|
};
|
|
6394
6396
|
LOCUS_GITIGNORE_PATTERNS = [
|
|
6395
6397
|
"# Locus AI - Session data (user-specific, can grow large)",
|
|
6396
6398
|
".locus/sessions/",
|
|
6397
6399
|
"",
|
|
6398
6400
|
"# Locus AI - Artifacts (local-only, user-specific)",
|
|
6399
|
-
".locus/artifacts/"
|
|
6401
|
+
".locus/artifacts/",
|
|
6402
|
+
"",
|
|
6403
|
+
"# Locus AI - Review reports (generated per sprint)",
|
|
6404
|
+
".locus/reviews/",
|
|
6405
|
+
"",
|
|
6406
|
+
"# Locus AI - Plans (generated per task)",
|
|
6407
|
+
".locus/plans/"
|
|
6400
6408
|
];
|
|
6401
6409
|
});
|
|
6402
6410
|
|
|
@@ -6448,6 +6456,67 @@ var init_document_fetcher = __esm(() => {
|
|
|
6448
6456
|
init_config();
|
|
6449
6457
|
});
|
|
6450
6458
|
|
|
6459
|
+
// ../sdk/src/agent/review-service.ts
|
|
6460
|
+
import { execSync } from "node:child_process";
|
|
6461
|
+
|
|
6462
|
+
class ReviewService {
|
|
6463
|
+
deps;
|
|
6464
|
+
constructor(deps) {
|
|
6465
|
+
this.deps = deps;
|
|
6466
|
+
}
|
|
6467
|
+
async reviewStagedChanges(sprint) {
|
|
6468
|
+
const { projectPath, log } = this.deps;
|
|
6469
|
+
try {
|
|
6470
|
+
execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
6471
|
+
log("Staged all changes for review.", "info");
|
|
6472
|
+
} catch (err) {
|
|
6473
|
+
log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
6474
|
+
return null;
|
|
6475
|
+
}
|
|
6476
|
+
let diff;
|
|
6477
|
+
try {
|
|
6478
|
+
diff = execSync("git diff --cached --stat && echo '---' && git diff --cached", {
|
|
6479
|
+
cwd: projectPath,
|
|
6480
|
+
maxBuffer: 10 * 1024 * 1024
|
|
6481
|
+
}).toString();
|
|
6482
|
+
} catch (err) {
|
|
6483
|
+
log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
6484
|
+
return null;
|
|
6485
|
+
}
|
|
6486
|
+
if (!diff.trim()) {
|
|
6487
|
+
return null;
|
|
6488
|
+
}
|
|
6489
|
+
const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
|
|
6490
|
+
const reviewPrompt = `# Code Review Request
|
|
6491
|
+
|
|
6492
|
+
## Context
|
|
6493
|
+
${sprintInfo}
|
|
6494
|
+
Date: ${new Date().toISOString()}
|
|
6495
|
+
|
|
6496
|
+
## Staged Changes (git diff)
|
|
6497
|
+
\`\`\`diff
|
|
6498
|
+
${diff}
|
|
6499
|
+
\`\`\`
|
|
6500
|
+
|
|
6501
|
+
## Instructions
|
|
6502
|
+
You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
|
|
6503
|
+
|
|
6504
|
+
1. **Summary** — Brief overview of what changed and why.
|
|
6505
|
+
2. **Files Changed** — List each file with a short description of changes.
|
|
6506
|
+
3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
|
|
6507
|
+
4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
|
|
6508
|
+
5. **Recommendations** — Actionable suggestions for improvement.
|
|
6509
|
+
6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
|
|
6510
|
+
|
|
6511
|
+
Keep the review concise but thorough. Focus on substance over style.
|
|
6512
|
+
Do NOT output <promise>COMPLETE</promise> — just output the review report as markdown.`;
|
|
6513
|
+
log("Running AI review on staged changes...", "info");
|
|
6514
|
+
const report = await this.deps.aiRunner.run(reviewPrompt);
|
|
6515
|
+
return report;
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
var init_review_service = () => {};
|
|
6519
|
+
|
|
6451
6520
|
// ../../node_modules/zod/v4/core/core.js
|
|
6452
6521
|
function $constructor(name, initializer, params) {
|
|
6453
6522
|
function init(inst, def) {
|
|
@@ -21645,29 +21714,8 @@ class TaskExecutor {
|
|
|
21645
21714
|
taskContext: context
|
|
21646
21715
|
});
|
|
21647
21716
|
try {
|
|
21648
|
-
let plan = null;
|
|
21649
|
-
this.deps.log("Phase 1: Planning (CLI)...", "info");
|
|
21650
|
-
const planningPrompt = `${basePrompt}
|
|
21651
|
-
|
|
21652
|
-
## Phase 1: Planning
|
|
21653
|
-
Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
|
|
21654
|
-
plan = await this.deps.aiRunner.run(planningPrompt, true);
|
|
21655
21717
|
this.deps.log("Starting Execution...", "info");
|
|
21656
|
-
|
|
21657
|
-
if (plan != null) {
|
|
21658
|
-
executionPrompt += `
|
|
21659
|
-
|
|
21660
|
-
## Phase 2: Execution
|
|
21661
|
-
Based on the plan, execute the task:
|
|
21662
|
-
|
|
21663
|
-
${plan}`;
|
|
21664
|
-
} else {
|
|
21665
|
-
executionPrompt += `
|
|
21666
|
-
|
|
21667
|
-
## Execution
|
|
21668
|
-
Execute the task directly.`;
|
|
21669
|
-
}
|
|
21670
|
-
executionPrompt += `
|
|
21718
|
+
const executionPrompt = `${basePrompt}
|
|
21671
21719
|
|
|
21672
21720
|
When finished, output: <promise>COMPLETE</promise>`;
|
|
21673
21721
|
const output = await this.deps.aiRunner.run(executionPrompt);
|
|
@@ -21768,7 +21816,7 @@ class ClaudeRunner {
|
|
|
21768
21816
|
setEventEmitter(emitter) {
|
|
21769
21817
|
this.eventEmitter = emitter;
|
|
21770
21818
|
}
|
|
21771
|
-
async run(prompt
|
|
21819
|
+
async run(prompt) {
|
|
21772
21820
|
const maxRetries = 3;
|
|
21773
21821
|
let lastError = null;
|
|
21774
21822
|
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
@@ -37854,6 +37902,8 @@ var init_src2 = __esm(() => {
|
|
|
37854
37902
|
});
|
|
37855
37903
|
|
|
37856
37904
|
// ../sdk/src/agent/worker.ts
|
|
37905
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
37906
|
+
import { join as join6 } from "node:path";
|
|
37857
37907
|
function resolveProvider(value) {
|
|
37858
37908
|
if (!value || value.startsWith("--")) {
|
|
37859
37909
|
console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
|
|
@@ -37872,11 +37922,9 @@ class AgentWorker {
|
|
|
37872
37922
|
indexerService;
|
|
37873
37923
|
documentFetcher;
|
|
37874
37924
|
taskExecutor;
|
|
37875
|
-
|
|
37876
|
-
maxEmpty = 60;
|
|
37925
|
+
reviewService;
|
|
37877
37926
|
maxTasks = 50;
|
|
37878
37927
|
tasksCompleted = 0;
|
|
37879
|
-
pollInterval = 1e4;
|
|
37880
37928
|
constructor(config2) {
|
|
37881
37929
|
this.config = config2;
|
|
37882
37930
|
const projectPath = config2.projectPath || process.cwd();
|
|
@@ -37913,6 +37961,11 @@ class AgentWorker {
|
|
|
37913
37961
|
projectPath,
|
|
37914
37962
|
log
|
|
37915
37963
|
});
|
|
37964
|
+
this.reviewService = new ReviewService({
|
|
37965
|
+
aiRunner: this.aiRunner,
|
|
37966
|
+
projectPath,
|
|
37967
|
+
log
|
|
37968
|
+
});
|
|
37916
37969
|
const providerLabel = provider === "codex" ? "Codex" : "Claude";
|
|
37917
37970
|
this.log(`Using ${providerLabel} CLI for all phases`, "info");
|
|
37918
37971
|
}
|
|
@@ -37958,33 +38011,42 @@ class AgentWorker {
|
|
|
37958
38011
|
await this.indexerService.reindex();
|
|
37959
38012
|
return result;
|
|
37960
38013
|
}
|
|
38014
|
+
async runStagedChangesReview(sprint2) {
|
|
38015
|
+
try {
|
|
38016
|
+
const report = await this.reviewService.reviewStagedChanges(sprint2);
|
|
38017
|
+
if (report) {
|
|
38018
|
+
const reviewsDir = join6(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
|
|
38019
|
+
if (!existsSync5(reviewsDir)) {
|
|
38020
|
+
mkdirSync3(reviewsDir, { recursive: true });
|
|
38021
|
+
}
|
|
38022
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
38023
|
+
const sprintSlug = sprint2?.name ? sprint2.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
|
|
38024
|
+
const fileName = `review-${sprintSlug}-${timestamp}.md`;
|
|
38025
|
+
const filePath = join6(reviewsDir, fileName);
|
|
38026
|
+
writeFileSync3(filePath, report);
|
|
38027
|
+
this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
|
|
38028
|
+
} else {
|
|
38029
|
+
this.log("No staged changes to review.", "info");
|
|
38030
|
+
}
|
|
38031
|
+
} catch (err) {
|
|
38032
|
+
this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
38033
|
+
}
|
|
38034
|
+
}
|
|
37961
38035
|
async run() {
|
|
37962
38036
|
this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
|
|
37963
38037
|
const sprint2 = await this.getActiveSprint();
|
|
37964
38038
|
if (sprint2) {
|
|
37965
|
-
this.log(`Active sprint found: ${sprint2.name}
|
|
37966
|
-
try {
|
|
37967
|
-
await this.client.sprints.triggerAIPlanning(sprint2.id, this.config.workspaceId);
|
|
37968
|
-
this.log(`Sprint plan sync checked on server.`, "success");
|
|
37969
|
-
} catch (err) {
|
|
37970
|
-
this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
|
|
37971
|
-
}
|
|
38039
|
+
this.log(`Active sprint found: ${sprint2.name}`, "info");
|
|
37972
38040
|
} else {
|
|
37973
|
-
this.log("No active sprint found
|
|
38041
|
+
this.log("No active sprint found.", "warn");
|
|
37974
38042
|
}
|
|
37975
|
-
while (this.tasksCompleted < this.maxTasks
|
|
38043
|
+
while (this.tasksCompleted < this.maxTasks) {
|
|
37976
38044
|
const task2 = await this.getNextTask();
|
|
37977
38045
|
if (!task2) {
|
|
37978
|
-
|
|
37979
|
-
|
|
37980
|
-
|
|
37981
|
-
this.consecutiveEmpty++;
|
|
37982
|
-
if (this.consecutiveEmpty >= this.maxEmpty)
|
|
37983
|
-
break;
|
|
37984
|
-
await new Promise((r) => setTimeout(r, this.pollInterval));
|
|
37985
|
-
continue;
|
|
38046
|
+
this.log("No tasks remaining. Running review on staged changes...", "info");
|
|
38047
|
+
await this.runStagedChangesReview(sprint2);
|
|
38048
|
+
break;
|
|
37986
38049
|
}
|
|
37987
|
-
this.consecutiveEmpty = 0;
|
|
37988
38050
|
this.log(`Claimed: ${task2.title}`, "success");
|
|
37989
38051
|
const result = await this.executeTask(task2);
|
|
37990
38052
|
try {
|
|
@@ -38024,6 +38086,7 @@ var init_worker = __esm(() => {
|
|
|
38024
38086
|
init_colors();
|
|
38025
38087
|
init_codebase_indexer_service();
|
|
38026
38088
|
init_document_fetcher();
|
|
38089
|
+
init_review_service();
|
|
38027
38090
|
init_task_executor();
|
|
38028
38091
|
if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
|
|
38029
38092
|
process.title = "locus-worker";
|
|
@@ -38068,6 +38131,7 @@ var init_worker = __esm(() => {
|
|
|
38068
38131
|
var init_agent2 = __esm(() => {
|
|
38069
38132
|
init_codebase_indexer_service();
|
|
38070
38133
|
init_document_fetcher();
|
|
38134
|
+
init_review_service();
|
|
38071
38135
|
init_task_executor();
|
|
38072
38136
|
init_worker();
|
|
38073
38137
|
});
|
|
@@ -38493,14 +38557,14 @@ var init_event_emitter = __esm(() => {
|
|
|
38493
38557
|
|
|
38494
38558
|
// ../sdk/src/exec/history-manager.ts
|
|
38495
38559
|
import {
|
|
38496
|
-
existsSync as
|
|
38497
|
-
mkdirSync as
|
|
38560
|
+
existsSync as existsSync6,
|
|
38561
|
+
mkdirSync as mkdirSync4,
|
|
38498
38562
|
readdirSync as readdirSync2,
|
|
38499
38563
|
readFileSync as readFileSync5,
|
|
38500
38564
|
rmSync,
|
|
38501
|
-
writeFileSync as
|
|
38565
|
+
writeFileSync as writeFileSync4
|
|
38502
38566
|
} from "node:fs";
|
|
38503
|
-
import { join as
|
|
38567
|
+
import { join as join7 } from "node:path";
|
|
38504
38568
|
function generateSessionId2() {
|
|
38505
38569
|
const timestamp = Date.now().toString(36);
|
|
38506
38570
|
const random = Math.random().toString(36).substring(2, 9);
|
|
@@ -38511,26 +38575,26 @@ class HistoryManager {
|
|
|
38511
38575
|
historyDir;
|
|
38512
38576
|
maxSessions;
|
|
38513
38577
|
constructor(projectPath, options) {
|
|
38514
|
-
this.historyDir = options?.historyDir ??
|
|
38578
|
+
this.historyDir = options?.historyDir ?? join7(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
|
|
38515
38579
|
this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
38516
38580
|
this.ensureHistoryDir();
|
|
38517
38581
|
}
|
|
38518
38582
|
ensureHistoryDir() {
|
|
38519
|
-
if (!
|
|
38520
|
-
|
|
38583
|
+
if (!existsSync6(this.historyDir)) {
|
|
38584
|
+
mkdirSync4(this.historyDir, { recursive: true });
|
|
38521
38585
|
}
|
|
38522
38586
|
}
|
|
38523
38587
|
getSessionPath(sessionId) {
|
|
38524
|
-
return
|
|
38588
|
+
return join7(this.historyDir, `${sessionId}.json`);
|
|
38525
38589
|
}
|
|
38526
38590
|
saveSession(session) {
|
|
38527
38591
|
const filePath = this.getSessionPath(session.id);
|
|
38528
38592
|
session.updatedAt = Date.now();
|
|
38529
|
-
|
|
38593
|
+
writeFileSync4(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
38530
38594
|
}
|
|
38531
38595
|
loadSession(sessionId) {
|
|
38532
38596
|
const filePath = this.getSessionPath(sessionId);
|
|
38533
|
-
if (!
|
|
38597
|
+
if (!existsSync6(filePath)) {
|
|
38534
38598
|
return null;
|
|
38535
38599
|
}
|
|
38536
38600
|
try {
|
|
@@ -38542,7 +38606,7 @@ class HistoryManager {
|
|
|
38542
38606
|
}
|
|
38543
38607
|
deleteSession(sessionId) {
|
|
38544
38608
|
const filePath = this.getSessionPath(sessionId);
|
|
38545
|
-
if (!
|
|
38609
|
+
if (!existsSync6(filePath)) {
|
|
38546
38610
|
return false;
|
|
38547
38611
|
}
|
|
38548
38612
|
try {
|
|
@@ -38630,7 +38694,7 @@ class HistoryManager {
|
|
|
38630
38694
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
38631
38695
|
}
|
|
38632
38696
|
sessionExists(sessionId) {
|
|
38633
|
-
return
|
|
38697
|
+
return existsSync6(this.getSessionPath(sessionId));
|
|
38634
38698
|
}
|
|
38635
38699
|
findSessionByPartialId(partialId) {
|
|
38636
38700
|
const sessions = this.listSessions();
|
|
@@ -38649,7 +38713,7 @@ class HistoryManager {
|
|
|
38649
38713
|
for (const file2 of files) {
|
|
38650
38714
|
if (file2.endsWith(".json")) {
|
|
38651
38715
|
try {
|
|
38652
|
-
rmSync(
|
|
38716
|
+
rmSync(join7(this.historyDir, file2));
|
|
38653
38717
|
deleted++;
|
|
38654
38718
|
} catch {}
|
|
38655
38719
|
}
|
|
@@ -38932,8 +38996,8 @@ var init_exec = __esm(() => {
|
|
|
38932
38996
|
|
|
38933
38997
|
// ../sdk/src/orchestrator.ts
|
|
38934
38998
|
import { spawn as spawn3 } from "node:child_process";
|
|
38935
|
-
import { existsSync as
|
|
38936
|
-
import { dirname as dirname2, join as
|
|
38999
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
39000
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
38937
39001
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
38938
39002
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
38939
39003
|
var AgentOrchestrator;
|
|
@@ -39034,8 +39098,8 @@ ${c.success("✅ Orchestrator finished")}`);
|
|
|
39034
39098
|
const potentialPaths = [];
|
|
39035
39099
|
const currentModulePath = fileURLToPath2(import.meta.url);
|
|
39036
39100
|
const currentModuleDir = dirname2(currentModulePath);
|
|
39037
|
-
potentialPaths.push(
|
|
39038
|
-
const workerPath = potentialPaths.find((p) =>
|
|
39101
|
+
potentialPaths.push(join8(currentModuleDir, "agent", "worker.js"), join8(currentModuleDir, "worker.js"), join8(currentModuleDir, "agent", "worker.ts"));
|
|
39102
|
+
const workerPath = potentialPaths.find((p) => existsSync7(p));
|
|
39039
39103
|
if (!workerPath) {
|
|
39040
39104
|
throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
|
|
39041
39105
|
}
|
|
@@ -40281,22 +40345,22 @@ import { parseArgs } from "node:util";
|
|
|
40281
40345
|
init_index_node();
|
|
40282
40346
|
|
|
40283
40347
|
// src/utils/version.ts
|
|
40284
|
-
import { existsSync as
|
|
40285
|
-
import { dirname as dirname3, join as
|
|
40348
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
40349
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
40286
40350
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
40287
40351
|
function getVersion() {
|
|
40288
40352
|
try {
|
|
40289
40353
|
const __filename2 = fileURLToPath3(import.meta.url);
|
|
40290
40354
|
const __dirname2 = dirname3(__filename2);
|
|
40291
|
-
const bundledPath =
|
|
40292
|
-
const sourcePath =
|
|
40293
|
-
if (
|
|
40355
|
+
const bundledPath = join9(__dirname2, "..", "package.json");
|
|
40356
|
+
const sourcePath = join9(__dirname2, "..", "..", "package.json");
|
|
40357
|
+
if (existsSync8(bundledPath)) {
|
|
40294
40358
|
const pkg = JSON.parse(readFileSync6(bundledPath, "utf-8"));
|
|
40295
40359
|
if (pkg.name === "@locusai/cli") {
|
|
40296
40360
|
return pkg.version || "0.0.0";
|
|
40297
40361
|
}
|
|
40298
40362
|
}
|
|
40299
|
-
if (
|
|
40363
|
+
if (existsSync8(sourcePath)) {
|
|
40300
40364
|
const pkg = JSON.parse(readFileSync6(sourcePath, "utf-8"));
|
|
40301
40365
|
if (pkg.name === "@locusai/cli") {
|
|
40302
40366
|
return pkg.version || "0.0.0";
|
|
@@ -40319,12 +40383,12 @@ function printBanner() {
|
|
|
40319
40383
|
}
|
|
40320
40384
|
// src/utils/helpers.ts
|
|
40321
40385
|
init_index_node();
|
|
40322
|
-
import { existsSync as
|
|
40323
|
-
import { join as
|
|
40386
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
40387
|
+
import { join as join10 } from "node:path";
|
|
40324
40388
|
function isProjectInitialized(projectPath) {
|
|
40325
|
-
const locusDir =
|
|
40326
|
-
const configPath =
|
|
40327
|
-
return
|
|
40389
|
+
const locusDir = join10(projectPath, LOCUS_CONFIG.dir);
|
|
40390
|
+
const configPath = join10(locusDir, LOCUS_CONFIG.configFile);
|
|
40391
|
+
return existsSync9(locusDir) && existsSync9(configPath);
|
|
40328
40392
|
}
|
|
40329
40393
|
function requireInitialization(projectPath, command) {
|
|
40330
40394
|
if (!isProjectInitialized(projectPath)) {
|
|
@@ -40649,6 +40713,7 @@ function showHelp2() {
|
|
|
40649
40713
|
${c.success("init")} Initialize Locus in the current directory
|
|
40650
40714
|
${c.success("index")} Index the codebase for AI context
|
|
40651
40715
|
${c.success("run")} Start an agent to work on tasks
|
|
40716
|
+
${c.success("review")} Review staged changes with AI
|
|
40652
40717
|
${c.success("exec")} Run a prompt with repository context
|
|
40653
40718
|
${c.dim("--interactive, -i Start interactive REPL mode")}
|
|
40654
40719
|
${c.dim("--session, -s <id> Resume a previous session")}
|
|
@@ -40665,6 +40730,7 @@ function showHelp2() {
|
|
|
40665
40730
|
${c.dim("$")} ${c.primary("locus init")}
|
|
40666
40731
|
${c.dim("$")} ${c.primary("locus index")}
|
|
40667
40732
|
${c.dim("$")} ${c.primary("locus run --api-key YOUR_KEY")}
|
|
40733
|
+
${c.dim("$")} ${c.primary("locus review")}
|
|
40668
40734
|
${c.dim("$")} ${c.primary("locus exec sessions list")}
|
|
40669
40735
|
|
|
40670
40736
|
For more information, visit: ${c.underline("https://locusai.dev/docs")}
|
|
@@ -40676,8 +40742,8 @@ import { parseArgs as parseArgs2 } from "node:util";
|
|
|
40676
40742
|
|
|
40677
40743
|
// src/config-manager.ts
|
|
40678
40744
|
init_index_node();
|
|
40679
|
-
import { existsSync as
|
|
40680
|
-
import { join as
|
|
40745
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
40746
|
+
import { join as join11 } from "node:path";
|
|
40681
40747
|
|
|
40682
40748
|
// src/templates/skills.ts
|
|
40683
40749
|
var DEFAULT_SKILLS = [
|
|
@@ -40966,12 +41032,53 @@ Guidance for understanding and maintaining the overall project architecture.
|
|
|
40966
41032
|
|
|
40967
41033
|
// src/config-manager.ts
|
|
40968
41034
|
var LOCUS_GITIGNORE_MARKER = "# Locus AI";
|
|
41035
|
+
var CLAUDE_MD_TEMPLATE = `# CLAUDE.md
|
|
41036
|
+
|
|
41037
|
+
## Planning First
|
|
41038
|
+
|
|
41039
|
+
Every task must be planned before writing code. Create \`.locus/plans/<task-name>.md\` with: goal, approach, affected files, and acceptance criteria. Update the plan if the approach changes. Mark complete when done.
|
|
41040
|
+
|
|
41041
|
+
## Code
|
|
41042
|
+
|
|
41043
|
+
- Follow the existing formatter, linter, and code style. Run them before finishing.
|
|
41044
|
+
- Keep changes minimal and atomic. Separate refactors from behavioral changes.
|
|
41045
|
+
- No new dependencies without explicit approval.
|
|
41046
|
+
- Never put raw secrets or credentials in the codebase.
|
|
41047
|
+
|
|
41048
|
+
## Testing
|
|
41049
|
+
|
|
41050
|
+
- Every behavioral change needs a test. Bug fixes need a regression test.
|
|
41051
|
+
- Run the relevant test suite before marking work complete.
|
|
41052
|
+
- Don't modify tests just to make them pass — understand why they fail.
|
|
41053
|
+
|
|
41054
|
+
## Communication
|
|
41055
|
+
|
|
41056
|
+
- If the plan needs to change, update it and explain why before continuing.
|
|
41057
|
+
`;
|
|
40969
41058
|
function updateGitignore(projectPath) {
|
|
40970
|
-
const gitignorePath =
|
|
41059
|
+
const gitignorePath = join11(projectPath, ".gitignore");
|
|
40971
41060
|
let content = "";
|
|
40972
|
-
|
|
41061
|
+
const locusBlock = LOCUS_GITIGNORE_PATTERNS.join(`
|
|
41062
|
+
`);
|
|
41063
|
+
if (existsSync10(gitignorePath)) {
|
|
40973
41064
|
content = readFileSync7(gitignorePath, "utf-8");
|
|
40974
41065
|
if (content.includes(LOCUS_GITIGNORE_MARKER)) {
|
|
41066
|
+
const lines = content.split(`
|
|
41067
|
+
`);
|
|
41068
|
+
const startIdx = lines.findIndex((l) => l.includes(LOCUS_GITIGNORE_MARKER));
|
|
41069
|
+
let endIdx = startIdx;
|
|
41070
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
41071
|
+
if (lines[i].startsWith(LOCUS_GITIGNORE_MARKER) || lines[i].startsWith(".locus/") || lines[i].trim() === "") {
|
|
41072
|
+
endIdx = i;
|
|
41073
|
+
} else {
|
|
41074
|
+
break;
|
|
41075
|
+
}
|
|
41076
|
+
}
|
|
41077
|
+
const before = lines.slice(0, startIdx);
|
|
41078
|
+
const after = lines.slice(endIdx + 1);
|
|
41079
|
+
content = [...before, locusBlock, ...after].join(`
|
|
41080
|
+
`);
|
|
41081
|
+
writeFileSync5(gitignorePath, content);
|
|
40975
41082
|
return;
|
|
40976
41083
|
}
|
|
40977
41084
|
if (content.length > 0 && !content.endsWith(`
|
|
@@ -40984,10 +41091,9 @@ function updateGitignore(projectPath) {
|
|
|
40984
41091
|
`;
|
|
40985
41092
|
}
|
|
40986
41093
|
}
|
|
40987
|
-
content += `${
|
|
40988
|
-
`)}
|
|
41094
|
+
content += `${locusBlock}
|
|
40989
41095
|
`;
|
|
40990
|
-
|
|
41096
|
+
writeFileSync5(gitignorePath, content);
|
|
40991
41097
|
}
|
|
40992
41098
|
|
|
40993
41099
|
class ConfigManager {
|
|
@@ -40996,27 +41102,35 @@ class ConfigManager {
|
|
|
40996
41102
|
this.projectPath = projectPath;
|
|
40997
41103
|
}
|
|
40998
41104
|
async init(version2) {
|
|
40999
|
-
const locusConfigDir =
|
|
41105
|
+
const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
|
|
41000
41106
|
const locusConfigPath = getLocusPath(this.projectPath, "configFile");
|
|
41001
41107
|
const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
|
|
41002
|
-
if (!
|
|
41003
|
-
|
|
41004
|
-
|
|
41005
|
-
|
|
41006
|
-
|
|
41007
|
-
`;
|
|
41008
|
-
writeFileSync4(claudeMdPath, template);
|
|
41108
|
+
if (!existsSync10(claudeMdPath)) {
|
|
41109
|
+
writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
|
|
41110
|
+
}
|
|
41111
|
+
if (!existsSync10(locusConfigDir)) {
|
|
41112
|
+
mkdirSync5(locusConfigDir, { recursive: true });
|
|
41009
41113
|
}
|
|
41010
|
-
|
|
41011
|
-
|
|
41114
|
+
const locusSubdirs = [
|
|
41115
|
+
LOCUS_CONFIG.artifactsDir,
|
|
41116
|
+
LOCUS_CONFIG.documentsDir,
|
|
41117
|
+
LOCUS_CONFIG.sessionsDir,
|
|
41118
|
+
LOCUS_CONFIG.reviewsDir,
|
|
41119
|
+
LOCUS_CONFIG.plansDir
|
|
41120
|
+
];
|
|
41121
|
+
for (const subdir of locusSubdirs) {
|
|
41122
|
+
const subdirPath = join11(locusConfigDir, subdir);
|
|
41123
|
+
if (!existsSync10(subdirPath)) {
|
|
41124
|
+
mkdirSync5(subdirPath, { recursive: true });
|
|
41125
|
+
}
|
|
41012
41126
|
}
|
|
41013
|
-
if (!
|
|
41127
|
+
if (!existsSync10(locusConfigPath)) {
|
|
41014
41128
|
const config2 = {
|
|
41015
41129
|
version: version2,
|
|
41016
41130
|
createdAt: new Date().toISOString(),
|
|
41017
41131
|
projectPath: "."
|
|
41018
41132
|
};
|
|
41019
|
-
|
|
41133
|
+
writeFileSync5(locusConfigPath, JSON.stringify(config2, null, 2));
|
|
41020
41134
|
}
|
|
41021
41135
|
const skillLocations = [
|
|
41022
41136
|
LOCUS_CONFIG.agentSkillsDir,
|
|
@@ -41026,15 +41140,15 @@ class ConfigManager {
|
|
|
41026
41140
|
".gemini/skills"
|
|
41027
41141
|
];
|
|
41028
41142
|
for (const location of skillLocations) {
|
|
41029
|
-
const skillsDir =
|
|
41030
|
-
if (!
|
|
41031
|
-
|
|
41143
|
+
const skillsDir = join11(this.projectPath, location);
|
|
41144
|
+
if (!existsSync10(skillsDir)) {
|
|
41145
|
+
mkdirSync5(skillsDir, { recursive: true });
|
|
41032
41146
|
}
|
|
41033
41147
|
for (const skill of DEFAULT_SKILLS) {
|
|
41034
|
-
const skillPath =
|
|
41035
|
-
if (!
|
|
41036
|
-
|
|
41037
|
-
|
|
41148
|
+
const skillPath = join11(skillsDir, skill.name);
|
|
41149
|
+
if (!existsSync10(skillPath)) {
|
|
41150
|
+
mkdirSync5(skillPath, { recursive: true });
|
|
41151
|
+
writeFileSync5(join11(skillPath, "SKILL.md"), skill.content);
|
|
41038
41152
|
}
|
|
41039
41153
|
}
|
|
41040
41154
|
}
|
|
@@ -41042,7 +41156,7 @@ class ConfigManager {
|
|
|
41042
41156
|
}
|
|
41043
41157
|
loadConfig() {
|
|
41044
41158
|
const path3 = getLocusPath(this.projectPath, "configFile");
|
|
41045
|
-
if (
|
|
41159
|
+
if (existsSync10(path3)) {
|
|
41046
41160
|
return JSON.parse(readFileSync7(path3, "utf-8"));
|
|
41047
41161
|
}
|
|
41048
41162
|
return null;
|
|
@@ -41062,7 +41176,7 @@ class ConfigManager {
|
|
|
41062
41176
|
skillsCreated: [],
|
|
41063
41177
|
gitignoreUpdated: false
|
|
41064
41178
|
};
|
|
41065
|
-
const locusConfigDir =
|
|
41179
|
+
const locusConfigDir = join11(this.projectPath, LOCUS_CONFIG.dir);
|
|
41066
41180
|
const claudeMdPath = getLocusPath(this.projectPath, "contextFile");
|
|
41067
41181
|
const config2 = this.loadConfig();
|
|
41068
41182
|
if (config2) {
|
|
@@ -41073,24 +41187,21 @@ class ConfigManager {
|
|
|
41073
41187
|
result.versionUpdated = true;
|
|
41074
41188
|
}
|
|
41075
41189
|
}
|
|
41076
|
-
if (!
|
|
41077
|
-
|
|
41078
|
-
|
|
41079
|
-
# Workflow
|
|
41080
|
-
- Run lint and typecheck before completion
|
|
41081
|
-
`;
|
|
41082
|
-
writeFileSync4(claudeMdPath, template);
|
|
41190
|
+
if (!existsSync10(claudeMdPath)) {
|
|
41191
|
+
writeFileSync5(claudeMdPath, CLAUDE_MD_TEMPLATE);
|
|
41083
41192
|
result.directoriesCreated.push("CLAUDE.md");
|
|
41084
41193
|
}
|
|
41085
41194
|
const locusSubdirs = [
|
|
41086
41195
|
LOCUS_CONFIG.artifactsDir,
|
|
41087
41196
|
LOCUS_CONFIG.documentsDir,
|
|
41088
|
-
LOCUS_CONFIG.sessionsDir
|
|
41197
|
+
LOCUS_CONFIG.sessionsDir,
|
|
41198
|
+
LOCUS_CONFIG.reviewsDir,
|
|
41199
|
+
LOCUS_CONFIG.plansDir
|
|
41089
41200
|
];
|
|
41090
41201
|
for (const subdir of locusSubdirs) {
|
|
41091
|
-
const subdirPath =
|
|
41092
|
-
if (!
|
|
41093
|
-
|
|
41202
|
+
const subdirPath = join11(locusConfigDir, subdir);
|
|
41203
|
+
if (!existsSync10(subdirPath)) {
|
|
41204
|
+
mkdirSync5(subdirPath, { recursive: true });
|
|
41094
41205
|
result.directoriesCreated.push(`.locus/${subdir}`);
|
|
41095
41206
|
}
|
|
41096
41207
|
}
|
|
@@ -41102,24 +41213,25 @@ class ConfigManager {
|
|
|
41102
41213
|
".gemini/skills"
|
|
41103
41214
|
];
|
|
41104
41215
|
for (const location of skillLocations) {
|
|
41105
|
-
const skillsDir =
|
|
41106
|
-
if (!
|
|
41107
|
-
|
|
41216
|
+
const skillsDir = join11(this.projectPath, location);
|
|
41217
|
+
if (!existsSync10(skillsDir)) {
|
|
41218
|
+
mkdirSync5(skillsDir, { recursive: true });
|
|
41108
41219
|
result.directoriesCreated.push(location);
|
|
41109
41220
|
}
|
|
41110
41221
|
for (const skill of DEFAULT_SKILLS) {
|
|
41111
|
-
const skillPath =
|
|
41112
|
-
if (!
|
|
41113
|
-
|
|
41114
|
-
|
|
41222
|
+
const skillPath = join11(skillsDir, skill.name);
|
|
41223
|
+
if (!existsSync10(skillPath)) {
|
|
41224
|
+
mkdirSync5(skillPath, { recursive: true });
|
|
41225
|
+
writeFileSync5(join11(skillPath, "SKILL.md"), skill.content);
|
|
41115
41226
|
result.skillsCreated.push(`${location}/${skill.name}`);
|
|
41116
41227
|
}
|
|
41117
41228
|
}
|
|
41118
41229
|
}
|
|
41119
|
-
const gitignorePath =
|
|
41120
|
-
const
|
|
41230
|
+
const gitignorePath = join11(this.projectPath, ".gitignore");
|
|
41231
|
+
const gitignoreBefore = existsSync10(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
|
|
41121
41232
|
updateGitignore(this.projectPath);
|
|
41122
|
-
|
|
41233
|
+
const gitignoreAfter = readFileSync7(gitignorePath, "utf-8");
|
|
41234
|
+
if (gitignoreBefore !== gitignoreAfter) {
|
|
41123
41235
|
result.gitignoreUpdated = true;
|
|
41124
41236
|
}
|
|
41125
41237
|
return result;
|
|
@@ -41136,7 +41248,7 @@ class ConfigManager {
|
|
|
41136
41248
|
}
|
|
41137
41249
|
saveConfig(config2) {
|
|
41138
41250
|
const path3 = getLocusPath(this.projectPath, "configFile");
|
|
41139
|
-
|
|
41251
|
+
writeFileSync5(path3, JSON.stringify(config2, null, 2));
|
|
41140
41252
|
}
|
|
41141
41253
|
}
|
|
41142
41254
|
|
|
@@ -41156,7 +41268,7 @@ Return ONLY a JSON object with this structure:
|
|
|
41156
41268
|
|
|
41157
41269
|
File Tree:
|
|
41158
41270
|
${tree}`;
|
|
41159
|
-
const output = await this.aiRunner.run(prompt
|
|
41271
|
+
const output = await this.aiRunner.run(prompt);
|
|
41160
41272
|
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
41161
41273
|
if (jsonMatch)
|
|
41162
41274
|
return JSON.parse(jsonMatch[0]);
|
|
@@ -41264,9 +41376,69 @@ async function initCommand() {
|
|
|
41264
41376
|
For more information, visit: ${c.underline("https://locusai.dev/docs")}
|
|
41265
41377
|
`);
|
|
41266
41378
|
}
|
|
41267
|
-
// src/commands/
|
|
41379
|
+
// src/commands/review.ts
|
|
41268
41380
|
init_index_node();
|
|
41381
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
41382
|
+
import { join as join12 } from "node:path";
|
|
41269
41383
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
41384
|
+
async function reviewCommand(args) {
|
|
41385
|
+
const { values } = parseArgs3({
|
|
41386
|
+
args,
|
|
41387
|
+
options: {
|
|
41388
|
+
model: { type: "string" },
|
|
41389
|
+
provider: { type: "string" },
|
|
41390
|
+
dir: { type: "string" }
|
|
41391
|
+
},
|
|
41392
|
+
strict: false
|
|
41393
|
+
});
|
|
41394
|
+
const projectPath = values.dir || process.cwd();
|
|
41395
|
+
requireInitialization(projectPath, "review");
|
|
41396
|
+
const provider = resolveProvider2(values.provider);
|
|
41397
|
+
const model = values.model || DEFAULT_MODEL[provider];
|
|
41398
|
+
const aiRunner = createAiRunner(provider, {
|
|
41399
|
+
projectPath,
|
|
41400
|
+
model
|
|
41401
|
+
});
|
|
41402
|
+
const reviewService = new ReviewService({
|
|
41403
|
+
aiRunner,
|
|
41404
|
+
projectPath,
|
|
41405
|
+
log: (msg, level) => {
|
|
41406
|
+
switch (level) {
|
|
41407
|
+
case "error":
|
|
41408
|
+
console.log(` ${c.error("✖")} ${msg}`);
|
|
41409
|
+
break;
|
|
41410
|
+
case "success":
|
|
41411
|
+
console.log(` ${c.success("✔")} ${msg}`);
|
|
41412
|
+
break;
|
|
41413
|
+
default:
|
|
41414
|
+
console.log(` ${c.dim(msg)}`);
|
|
41415
|
+
}
|
|
41416
|
+
}
|
|
41417
|
+
});
|
|
41418
|
+
console.log(`
|
|
41419
|
+
${c.primary("\uD83D\uDD0D")} ${c.bold("Reviewing staged changes...")}
|
|
41420
|
+
`);
|
|
41421
|
+
const report = await reviewService.reviewStagedChanges(null);
|
|
41422
|
+
if (!report) {
|
|
41423
|
+
console.log(` ${c.dim("No changes to review.")}
|
|
41424
|
+
`);
|
|
41425
|
+
return;
|
|
41426
|
+
}
|
|
41427
|
+
const reviewsDir = join12(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
|
|
41428
|
+
if (!existsSync11(reviewsDir)) {
|
|
41429
|
+
mkdirSync6(reviewsDir, { recursive: true });
|
|
41430
|
+
}
|
|
41431
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
41432
|
+
const reportPath = join12(reviewsDir, `review-${timestamp}.md`);
|
|
41433
|
+
writeFileSync6(reportPath, report, "utf-8");
|
|
41434
|
+
console.log(`
|
|
41435
|
+
${c.success("✔")} ${c.success("Review complete!")}`);
|
|
41436
|
+
console.log(` ${c.dim("Report saved to:")} ${c.primary(reportPath)}
|
|
41437
|
+
`);
|
|
41438
|
+
}
|
|
41439
|
+
// src/commands/run.ts
|
|
41440
|
+
init_index_node();
|
|
41441
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
41270
41442
|
|
|
41271
41443
|
// src/workspace-resolver.ts
|
|
41272
41444
|
init_index_node();
|
|
@@ -41307,7 +41479,7 @@ class WorkspaceResolver {
|
|
|
41307
41479
|
|
|
41308
41480
|
// src/commands/run.ts
|
|
41309
41481
|
async function runCommand(args) {
|
|
41310
|
-
const { values } =
|
|
41482
|
+
const { values } = parseArgs4({
|
|
41311
41483
|
args,
|
|
41312
41484
|
options: {
|
|
41313
41485
|
"api-key": { type: "string" },
|
|
@@ -41391,6 +41563,9 @@ async function main() {
|
|
|
41391
41563
|
case "exec":
|
|
41392
41564
|
await execCommand(args);
|
|
41393
41565
|
break;
|
|
41566
|
+
case "review":
|
|
41567
|
+
await reviewCommand(args);
|
|
41568
|
+
break;
|
|
41394
41569
|
default:
|
|
41395
41570
|
showHelp2();
|
|
41396
41571
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@locusai/cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "CLI for Locus - AI-native project management platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"author": "",
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@locusai/sdk": "^0.8.
|
|
35
|
+
"@locusai/sdk": "^0.8.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {}
|
|
38
38
|
}
|