@locusai/sdk 0.4.14 → 0.5.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.
@@ -439,13 +439,18 @@ __export(exports_worker, {
439
439
  AgentWorker: () => AgentWorker
440
440
  });
441
441
  module.exports = __toCommonJS(exports_worker);
442
-
443
- // src/ai/anthropic-client.ts
444
- var import_sdk = __toESM(require("@anthropic-ai/sdk"));
442
+ var import_shared3 = require("@locusai/shared");
445
443
 
446
444
  // src/core/config.ts
447
445
  var import_node_path = require("node:path");
448
- var DEFAULT_MODEL = "sonnet";
446
+ var PROVIDER = {
447
+ CLAUDE: "claude",
448
+ CODEX: "codex"
449
+ };
450
+ var DEFAULT_MODEL = {
451
+ [PROVIDER.CLAUDE]: "sonnet",
452
+ [PROVIDER.CODEX]: "gpt-5.1-codex-mini"
453
+ };
449
454
  var LOCUS_CONFIG = {
450
455
  dir: ".locus",
451
456
  configFile: "config.json",
@@ -460,122 +465,9 @@ function getLocusPath(projectPath, fileName) {
460
465
  return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
461
466
  }
462
467
 
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
468
  // src/ai/claude-runner.ts
515
469
  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
- }
470
+ var import_node_path2 = require("node:path");
579
471
 
580
472
  // src/utils/colors.ts
581
473
  var ESC = "\x1B[";
@@ -625,9 +517,270 @@ var c = {
625
517
  underline: (t) => c.text(t, "underline")
626
518
  };
627
519
 
628
- // src/agent/artifact-syncer.ts
520
+ // src/ai/claude-runner.ts
521
+ class ClaudeRunner {
522
+ model;
523
+ log;
524
+ projectPath;
525
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
526
+ this.model = model;
527
+ this.log = log;
528
+ this.projectPath = import_node_path2.resolve(projectPath);
529
+ }
530
+ async run(prompt, _isPlanning = false) {
531
+ const maxRetries = 3;
532
+ let lastError = null;
533
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
534
+ try {
535
+ return await this.executeRun(prompt);
536
+ } catch (error) {
537
+ const err = error;
538
+ lastError = err;
539
+ const isLastAttempt = attempt === maxRetries;
540
+ if (!isLastAttempt) {
541
+ const delay = Math.pow(2, attempt) * 1000;
542
+ console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
543
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
544
+ }
545
+ }
546
+ }
547
+ throw lastError || new Error("Claude CLI failed after multiple attempts");
548
+ }
549
+ executeRun(prompt) {
550
+ return new Promise((resolve2, reject) => {
551
+ const args = [
552
+ "--dangerously-skip-permissions",
553
+ "--print",
554
+ "--verbose",
555
+ "--output-format",
556
+ "stream-json",
557
+ "--include-partial-messages",
558
+ "--model",
559
+ this.model
560
+ ];
561
+ const env = {
562
+ ...process.env,
563
+ FORCE_COLOR: "1",
564
+ TERM: "xterm-256color"
565
+ };
566
+ const claude = import_node_child_process.spawn("claude", args, {
567
+ cwd: this.projectPath,
568
+ stdio: ["pipe", "pipe", "pipe"],
569
+ env
570
+ });
571
+ let finalResult = "";
572
+ let errorOutput = "";
573
+ let buffer = "";
574
+ claude.stdout.on("data", (data) => {
575
+ buffer += data.toString();
576
+ const lines = buffer.split(`
577
+ `);
578
+ buffer = lines.pop() || "";
579
+ for (const line of lines) {
580
+ const result = this.handleStreamLine(line);
581
+ if (result)
582
+ finalResult = result;
583
+ }
584
+ });
585
+ claude.stderr.on("data", (data) => {
586
+ const msg = data.toString();
587
+ errorOutput += msg;
588
+ process.stderr.write(msg);
589
+ });
590
+ claude.on("error", (err) => {
591
+ reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
592
+ });
593
+ claude.on("close", (code) => {
594
+ process.stdout.write(`
595
+ `);
596
+ if (code === 0) {
597
+ resolve2(finalResult);
598
+ } else {
599
+ reject(this.createExecutionError(code, errorOutput));
600
+ }
601
+ });
602
+ claude.stdin.write(prompt);
603
+ claude.stdin.end();
604
+ });
605
+ }
606
+ handleStreamLine(line) {
607
+ if (!line.trim())
608
+ return null;
609
+ try {
610
+ const item = JSON.parse(line);
611
+ return this.processStreamItem(item);
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+ processStreamItem(item) {
617
+ if (item.type === "result") {
618
+ return item.result || "";
619
+ }
620
+ if (item.type === "stream_event" && item.event) {
621
+ this.handleEvent(item.event);
622
+ }
623
+ return null;
624
+ }
625
+ handleEvent(event) {
626
+ const { type, delta, content_block } = event;
627
+ if (type === "content_block_delta" && delta) {
628
+ if (delta.type === "text_delta" && delta.text) {
629
+ this.log?.(delta.text, "info");
630
+ }
631
+ } else if (type === "content_block_start" && content_block) {
632
+ if (content_block.type === "tool_use" && content_block.name) {
633
+ this.log?.(`
634
+
635
+ ${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
636
+ `, "info");
637
+ }
638
+ }
639
+ }
640
+ createExecutionError(code, detail) {
641
+ const errorMsg = detail.trim();
642
+ 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.`;
643
+ return new Error(message);
644
+ }
645
+ }
646
+
647
+ // src/ai/codex-runner.ts
648
+ var import_node_child_process2 = require("node:child_process");
649
+ var import_node_crypto = require("node:crypto");
629
650
  var import_node_fs = require("node:fs");
630
- var import_node_path2 = require("node:path");
651
+ var import_node_os = require("node:os");
652
+ var import_node_path3 = require("node:path");
653
+ class CodexRunner {
654
+ projectPath;
655
+ model;
656
+ log;
657
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
658
+ this.projectPath = projectPath;
659
+ this.model = model;
660
+ this.log = log;
661
+ }
662
+ async run(prompt) {
663
+ const maxRetries = 3;
664
+ let lastError = null;
665
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
666
+ try {
667
+ return await this.executeRun(prompt);
668
+ } catch (error) {
669
+ lastError = error;
670
+ if (attempt < maxRetries) {
671
+ const delay = Math.pow(2, attempt) * 1000;
672
+ console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
673
+ await this.sleep(delay);
674
+ }
675
+ }
676
+ }
677
+ throw lastError || new Error("Codex CLI failed after multiple attempts");
678
+ }
679
+ executeRun(prompt) {
680
+ return new Promise((resolve2, reject) => {
681
+ const outputPath = import_node_path3.join(import_node_os.tmpdir(), `locus-codex-${import_node_crypto.randomUUID()}.txt`);
682
+ const args = this.buildArgs(outputPath);
683
+ const codex = import_node_child_process2.spawn("codex", args, {
684
+ cwd: this.projectPath,
685
+ stdio: ["pipe", "pipe", "pipe"],
686
+ env: process.env,
687
+ shell: false
688
+ });
689
+ let output = "";
690
+ let errorOutput = "";
691
+ const handleOutput = (data) => {
692
+ const msg = data.toString();
693
+ output += msg;
694
+ this.streamToConsole(msg);
695
+ };
696
+ codex.stdout.on("data", handleOutput);
697
+ codex.stderr.on("data", (data) => {
698
+ const msg = data.toString();
699
+ errorOutput += msg;
700
+ this.streamToConsole(msg);
701
+ });
702
+ codex.on("error", (err) => {
703
+ reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
704
+ });
705
+ codex.on("close", (code) => {
706
+ this.cleanupTempFile(outputPath);
707
+ if (code === 0) {
708
+ resolve2(this.readOutput(outputPath, output));
709
+ } else {
710
+ reject(this.createErrorFromOutput(code, errorOutput));
711
+ }
712
+ });
713
+ codex.stdin.write(prompt);
714
+ codex.stdin.end();
715
+ });
716
+ }
717
+ buildArgs(outputPath) {
718
+ const args = ["exec", "--full-auto", "--output-last-message", outputPath];
719
+ if (this.model) {
720
+ args.push("--model", this.model);
721
+ }
722
+ args.push("-");
723
+ return args;
724
+ }
725
+ streamToConsole(chunk) {
726
+ for (const rawLine of chunk.split(`
727
+ `)) {
728
+ const line = rawLine.trim();
729
+ if (line && this.shouldDisplay(line)) {
730
+ const formattedLine = "[Codex]: ".concat(line.replace(/\*/g, ""));
731
+ this.log?.(formattedLine, "info");
732
+ }
733
+ }
734
+ }
735
+ shouldDisplay(line) {
736
+ return [
737
+ /^thinking\b/,
738
+ /^\*\*/,
739
+ /^Plan update\b/,
740
+ /^[→•✓]/
741
+ ].some((pattern) => pattern.test(line));
742
+ }
743
+ readOutput(outputPath, fallback) {
744
+ if (import_node_fs.existsSync(outputPath)) {
745
+ try {
746
+ const text = import_node_fs.readFileSync(outputPath, "utf-8").trim();
747
+ if (text)
748
+ return text;
749
+ } catch {}
750
+ }
751
+ return fallback.trim();
752
+ }
753
+ createErrorFromOutput(code, errorOutput) {
754
+ const detail = errorOutput.trim();
755
+ const message = detail ? `Codex CLI error (exit code ${code}): ${detail}` : `Codex CLI exited with code ${code}. ` + `Ensure Codex CLI is installed and you are logged in.`;
756
+ return new Error(message);
757
+ }
758
+ cleanupTempFile(path) {
759
+ try {
760
+ if (import_node_fs.existsSync(path))
761
+ import_node_fs.unlinkSync(path);
762
+ } catch {}
763
+ }
764
+ sleep(ms) {
765
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
766
+ }
767
+ }
768
+
769
+ // src/ai/factory.ts
770
+ function createAiRunner(provider, config) {
771
+ const resolvedProvider = provider ?? PROVIDER.CLAUDE;
772
+ const model = config.model ?? DEFAULT_MODEL[resolvedProvider];
773
+ switch (resolvedProvider) {
774
+ case PROVIDER.CODEX:
775
+ return new CodexRunner(config.projectPath, model, config.log);
776
+ default:
777
+ return new ClaudeRunner(config.projectPath, model, config.log);
778
+ }
779
+ }
780
+
781
+ // src/agent/artifact-syncer.ts
782
+ var import_node_fs2 = require("node:fs");
783
+ var import_node_path4 = require("node:path");
631
784
  class ArtifactSyncer {
632
785
  deps;
633
786
  constructor(deps) {
@@ -653,21 +806,21 @@ class ArtifactSyncer {
653
806
  }
654
807
  async sync() {
655
808
  const artifactsDir = getLocusPath(this.deps.projectPath, "artifactsDir");
656
- if (!import_node_fs.existsSync(artifactsDir)) {
657
- import_node_fs.mkdirSync(artifactsDir, { recursive: true });
809
+ if (!import_node_fs2.existsSync(artifactsDir)) {
810
+ import_node_fs2.mkdirSync(artifactsDir, { recursive: true });
658
811
  return;
659
812
  }
660
813
  try {
661
- const files = import_node_fs.readdirSync(artifactsDir);
814
+ const files = import_node_fs2.readdirSync(artifactsDir);
662
815
  if (files.length === 0)
663
816
  return;
664
817
  this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
665
818
  const artifactsGroupId = await this.getOrCreateArtifactsGroup();
666
819
  const existingDocs = await this.deps.client.docs.list(this.deps.workspaceId);
667
820
  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");
821
+ const filePath = import_node_path4.join(artifactsDir, file);
822
+ if (import_node_fs2.statSync(filePath).isFile()) {
823
+ const content = import_node_fs2.readFileSync(filePath, "utf-8");
671
824
  const title = file.replace(/\.md$/, "").trim();
672
825
  if (!title)
673
826
  continue;
@@ -694,8 +847,8 @@ class ArtifactSyncer {
694
847
  }
695
848
 
696
849
  // src/core/indexer.ts
697
- var import_node_fs2 = require("node:fs");
698
- var import_node_path3 = require("node:path");
850
+ var import_node_fs3 = require("node:fs");
851
+ var import_node_path5 = require("node:path");
699
852
  var import_globby = require("globby");
700
853
 
701
854
  class CodebaseIndexer {
@@ -703,7 +856,7 @@ class CodebaseIndexer {
703
856
  indexPath;
704
857
  constructor(projectPath) {
705
858
  this.projectPath = projectPath;
706
- this.indexPath = import_node_path3.join(projectPath, ".locus", "codebase-index.json");
859
+ this.indexPath = import_node_path5.join(projectPath, ".locus", "codebase-index.json");
707
860
  }
708
861
  async index(onProgress, treeSummarizer) {
709
862
  if (!treeSummarizer) {
@@ -711,11 +864,11 @@ class CodebaseIndexer {
711
864
  }
712
865
  if (onProgress)
713
866
  onProgress("Generating file tree...");
714
- const gitmodulesPath = import_node_path3.join(this.projectPath, ".gitmodules");
867
+ const gitmodulesPath = import_node_path5.join(this.projectPath, ".gitmodules");
715
868
  const submoduleIgnores = [];
716
- if (import_node_fs2.existsSync(gitmodulesPath)) {
869
+ if (import_node_fs3.existsSync(gitmodulesPath)) {
717
870
  try {
718
- const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
871
+ const content = import_node_fs3.readFileSync(gitmodulesPath, "utf-8");
719
872
  const lines = content.split(`
720
873
  `);
721
874
  for (const line of lines) {
@@ -778,9 +931,9 @@ class CodebaseIndexer {
778
931
  return index;
779
932
  }
780
933
  loadIndex() {
781
- if (import_node_fs2.existsSync(this.indexPath)) {
934
+ if (import_node_fs3.existsSync(this.indexPath)) {
782
935
  try {
783
- return JSON.parse(import_node_fs2.readFileSync(this.indexPath, "utf-8"));
936
+ return JSON.parse(import_node_fs3.readFileSync(this.indexPath, "utf-8"));
784
937
  } catch {
785
938
  return null;
786
939
  }
@@ -788,11 +941,11 @@ class CodebaseIndexer {
788
941
  return null;
789
942
  }
790
943
  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 });
944
+ const dir = import_node_path5.dirname(this.indexPath);
945
+ if (!import_node_fs3.existsSync(dir)) {
946
+ import_node_fs3.mkdirSync(dir, { recursive: true });
794
947
  }
795
- import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
948
+ import_node_fs3.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
796
949
  }
797
950
  }
798
951
 
@@ -821,15 +974,7 @@ File tree:
821
974
  ${tree}
822
975
 
823
976
  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
- }
977
+ const response = await this.deps.aiRunner.run(prompt, true);
833
978
  const jsonMatch = response.match(/\{[\s\S]*\}/);
834
979
  if (jsonMatch) {
835
980
  return JSON.parse(jsonMatch[0]);
@@ -855,30 +1000,7 @@ class SprintPlanner {
855
1000
  try {
856
1001
  const taskList = tasks2.map((t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`).join(`
857
1002
  `);
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}
1003
+ const planningPrompt = `# Sprint Planning: ${sprint.name}
882
1004
 
883
1005
  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
1006
 
@@ -895,8 +1017,7 @@ ${taskList}
895
1017
  - Do NOT create any files on the filesystem during this planning phase.
896
1018
  - Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
897
1019
  - Your output will be saved as the official sprint mindmap on the server.`;
898
- plan = await this.deps.claudeRunner.run(planningPrompt, true);
899
- }
1020
+ const plan = await this.deps.aiRunner.run(planningPrompt, true);
900
1021
  this.deps.log("Sprint mindmap generated and posted to server.", "success");
901
1022
  return plan;
902
1023
  } catch (error) {
@@ -907,7 +1028,7 @@ ${taskList}
907
1028
  }
908
1029
 
909
1030
  // src/core/prompt-builder.ts
910
- var import_node_fs3 = require("node:fs");
1031
+ var import_node_fs4 = require("node:fs");
911
1032
  var import_shared2 = require("@locusai/shared");
912
1033
  class PromptBuilder {
913
1034
  projectPath;
@@ -930,9 +1051,9 @@ ${task.description || "No description provided."}
930
1051
 
931
1052
  `;
932
1053
  const contextPath = getLocusPath(this.projectPath, "contextFile");
933
- if (import_node_fs3.existsSync(contextPath)) {
1054
+ if (import_node_fs4.existsSync(contextPath)) {
934
1055
  try {
935
- const context = import_node_fs3.readFileSync(contextPath, "utf-8");
1056
+ const context = import_node_fs4.readFileSync(contextPath, "utf-8");
936
1057
  prompt += `## Project Context (from CLAUDE.md)
937
1058
  ${context}
938
1059
 
@@ -942,7 +1063,7 @@ ${context}
942
1063
  }
943
1064
  }
944
1065
  const indexPath = getLocusPath(this.projectPath, "indexFile");
945
- if (import_node_fs3.existsSync(indexPath)) {
1066
+ if (import_node_fs4.existsSync(indexPath)) {
946
1067
  prompt += `## Codebase Overview
947
1068
  There is an index file in the .locus/codebase-index.json and if you need you can check it.
948
1069
 
@@ -1033,22 +1154,20 @@ ${this.deps.sprintPlan}
1033
1154
  ${basePrompt}`;
1034
1155
  }
1035
1156
  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
- });
1157
+ let plan = null;
1158
+ if (this.deps.skipPlanning) {
1159
+ this.deps.log("Skipping Phase 1: Planning (CLI)...", "info");
1046
1160
  } else {
1047
- this.deps.log("Skipping Phase 1: Planning (No Anthropic API Key)...", "info");
1161
+ this.deps.log("Phase 1: Planning (CLI)...", "info");
1162
+ const planningPrompt = `${basePrompt}
1163
+
1164
+ ## Phase 1: Planning
1165
+ Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
1166
+ plan = await this.deps.aiRunner.run(planningPrompt, true);
1048
1167
  }
1049
1168
  this.deps.log("Starting Execution...", "info");
1050
1169
  let executionPrompt = basePrompt;
1051
- if (plan) {
1170
+ if (plan != null) {
1052
1171
  executionPrompt += `
1053
1172
 
1054
1173
  ## Phase 2: Execution
@@ -1064,11 +1183,11 @@ Execute the task directly.`;
1064
1183
  executionPrompt += `
1065
1184
 
1066
1185
  When finished, output: <promise>COMPLETE</promise>`;
1067
- const output = await this.deps.claudeRunner.run(executionPrompt);
1186
+ const output = await this.deps.aiRunner.run(executionPrompt);
1068
1187
  const success = output.includes("<promise>COMPLETE</promise>");
1069
1188
  return {
1070
1189
  success,
1071
- summary: success ? "Task completed by Claude" : "Claude did not signal completion"
1190
+ summary: success ? "Task completed by the agent" : "The agent did not signal completion"
1072
1191
  };
1073
1192
  } catch (error) {
1074
1193
  return { success: false, summary: `Error: ${error}` };
@@ -1077,17 +1196,27 @@ When finished, output: <promise>COMPLETE</promise>`;
1077
1196
  }
1078
1197
 
1079
1198
  // src/agent/worker.ts
1199
+ function resolveProvider(value) {
1200
+ if (!value || value.startsWith("--")) {
1201
+ console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
1202
+ return PROVIDER.CLAUDE;
1203
+ }
1204
+ if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
1205
+ return value;
1206
+ console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
1207
+ return PROVIDER.CLAUDE;
1208
+ }
1209
+
1080
1210
  class AgentWorker {
1081
1211
  config;
1082
1212
  client;
1083
- claudeRunner;
1084
- anthropicClient;
1213
+ aiRunner;
1085
1214
  sprintPlanner;
1086
1215
  indexerService;
1087
1216
  artifactSyncer;
1088
1217
  taskExecutor;
1089
1218
  consecutiveEmpty = 0;
1090
- maxEmpty = 10;
1219
+ maxEmpty = 5;
1091
1220
  maxTasks = 50;
1092
1221
  tasksCompleted = 0;
1093
1222
  pollInterval = 1e4;
@@ -1105,41 +1234,37 @@ class AgentWorker {
1105
1234
  factor: 2
1106
1235
  }
1107
1236
  });
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);
1237
+ const log = this.log.bind(this);
1238
+ const provider = config.provider ?? PROVIDER.CLAUDE;
1239
+ this.aiRunner = createAiRunner(provider, {
1240
+ projectPath,
1241
+ model: config.model,
1242
+ log
1243
+ });
1114
1244
  this.sprintPlanner = new SprintPlanner({
1115
- anthropicClient: this.anthropicClient,
1116
- claudeRunner: this.claudeRunner,
1117
- log: logFn
1245
+ aiRunner: this.aiRunner,
1246
+ log
1118
1247
  });
1119
1248
  this.indexerService = new CodebaseIndexerService({
1120
- anthropicClient: this.anthropicClient,
1121
- claudeRunner: this.claudeRunner,
1249
+ aiRunner: this.aiRunner,
1122
1250
  projectPath,
1123
- log: logFn
1251
+ log
1124
1252
  });
1125
1253
  this.artifactSyncer = new ArtifactSyncer({
1126
1254
  client: this.client,
1127
1255
  workspaceId: config.workspaceId,
1128
1256
  projectPath,
1129
- log: logFn
1257
+ log
1130
1258
  });
1131
1259
  this.taskExecutor = new TaskExecutor({
1132
- anthropicClient: this.anthropicClient,
1133
- claudeRunner: this.claudeRunner,
1260
+ aiRunner: this.aiRunner,
1134
1261
  projectPath,
1135
1262
  sprintPlan: null,
1136
- log: logFn
1263
+ skipPlanning: config.skipPlanning,
1264
+ log
1137
1265
  });
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", "info");
1142
- }
1266
+ const providerLabel = provider === "codex" ? "Codex" : "Claude";
1267
+ this.log(`Using ${providerLabel} CLI for all phases`, "info");
1143
1268
  }
1144
1269
  log(message, level = "info") {
1145
1270
  const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
@@ -1186,16 +1311,17 @@ class AgentWorker {
1186
1311
  const tasks2 = await this.client.tasks.list(this.config.workspaceId, {
1187
1312
  sprintId: sprint.id
1188
1313
  });
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) {
1314
+ const activeTasks = tasks2.filter((t) => t.status === import_shared3.TaskStatus.BACKLOG || t.status === import_shared3.TaskStatus.IN_PROGRESS);
1315
+ this.log(`Sprint tasks found: ${activeTasks.length}`, "info");
1316
+ if (activeTasks.length <= 1) {
1196
1317
  this.log("Skipping mindmap generation (only one task in sprint).", "info");
1197
1318
  this.sprintPlan = null;
1198
1319
  } else {
1320
+ const latestTaskCreation = activeTasks.reduce((latest, task) => {
1321
+ const taskDate = new Date(task.createdAt);
1322
+ return taskDate > latest ? taskDate : latest;
1323
+ }, new Date(0));
1324
+ const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
1199
1325
  const needsPlanning = !sprint.mindmap || sprint.mindmap.trim() === "" || latestTaskCreation > mindmapDate;
1200
1326
  if (needsPlanning) {
1201
1327
  if (sprint.mindmap && latestTaskCreation > mindmapDate) {
@@ -1217,6 +1343,9 @@ class AgentWorker {
1217
1343
  while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
1218
1344
  const task = await this.getNextTask();
1219
1345
  if (!task) {
1346
+ if (this.consecutiveEmpty === 0) {
1347
+ this.log("Queue empty, waiting for tasks...", "info");
1348
+ }
1220
1349
  this.consecutiveEmpty++;
1221
1350
  if (this.consecutiveEmpty >= this.maxEmpty)
1222
1351
  break;
@@ -1228,6 +1357,7 @@ class AgentWorker {
1228
1357
  const result = await this.executeTask(task);
1229
1358
  await this.artifactSyncer.sync();
1230
1359
  if (result.success) {
1360
+ this.log(`Completed: ${task.title}`, "success");
1231
1361
  await this.client.tasks.update(task.id, this.config.workspaceId, {
1232
1362
  status: "VERIFICATION"
1233
1363
  });
@@ -1237,6 +1367,7 @@ class AgentWorker {
1237
1367
  });
1238
1368
  this.tasksCompleted++;
1239
1369
  } else {
1370
+ this.log(`Failed: ${task.title} - ${result.summary}`, "error");
1240
1371
  await this.client.tasks.update(task.id, this.config.workspaceId, {
1241
1372
  status: "BACKLOG",
1242
1373
  assignedTo: null
@@ -1265,12 +1396,17 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
1265
1396
  config.apiBase = args[++i];
1266
1397
  else if (arg === "--api-key")
1267
1398
  config.apiKey = args[++i];
1268
- else if (arg === "--anthropic-api-key")
1269
- config.anthropicApiKey = args[++i];
1270
1399
  else if (arg === "--project-path")
1271
1400
  config.projectPath = args[++i];
1272
1401
  else if (arg === "--model")
1273
1402
  config.model = args[++i];
1403
+ else if (arg === "--provider") {
1404
+ const value = args[i + 1];
1405
+ if (value && !value.startsWith("--"))
1406
+ i++;
1407
+ config.provider = resolveProvider(value);
1408
+ } else if (arg === "--skip-planning")
1409
+ config.skipPlanning = true;
1274
1410
  }
1275
1411
  if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
1276
1412
  console.error("Missing required arguments");