@locusai/cli 0.25.2 → 0.25.4

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.
Files changed (2) hide show
  1. package/bin/locus.js +277 -88
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -3352,46 +3352,155 @@ var init_lock = __esm(() => {
3352
3352
  });
3353
3353
 
3354
3354
  // src/skills/installer.ts
3355
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync8 } from "node:fs";
3355
+ import {
3356
+ existsSync as existsSync11,
3357
+ mkdirSync as mkdirSync8,
3358
+ readFileSync as readFileSync8,
3359
+ renameSync as renameSync2,
3360
+ rmSync,
3361
+ writeFileSync as writeFileSync8
3362
+ } from "node:fs";
3363
+ import { tmpdir } from "node:os";
3356
3364
  import { join as join11 } from "node:path";
3357
3365
  async function installSkill(projectRoot, name, content, source) {
3358
3366
  const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3359
3367
  const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3360
- mkdirSync8(claudeDir, { recursive: true });
3361
- mkdirSync8(agentsDir, { recursive: true });
3362
- writeFileSync8(join11(claudeDir, "SKILL.md"), content, "utf-8");
3363
- writeFileSync8(join11(agentsDir, "SKILL.md"), content, "utf-8");
3364
- const lock = readLockFile(projectRoot);
3365
- lock.skills[name] = {
3366
- source,
3367
- sourceType: "github",
3368
- computedHash: computeSkillHash(content)
3369
- };
3370
- writeLockFile(projectRoot, lock);
3368
+ const stagingDir = join11(tmpdir(), `locus-skill-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
3369
+ const stagingClaudeDir = join11(stagingDir, "claude");
3370
+ const stagingAgentsDir = join11(stagingDir, "agents");
3371
+ let wroteClaudeDir = false;
3372
+ let wroteAgentsDir = false;
3373
+ try {
3374
+ try {
3375
+ mkdirSync8(stagingClaudeDir, { recursive: true });
3376
+ mkdirSync8(stagingAgentsDir, { recursive: true });
3377
+ writeFileSync8(join11(stagingClaudeDir, "SKILL.md"), content, "utf-8");
3378
+ writeFileSync8(join11(stagingAgentsDir, "SKILL.md"), content, "utf-8");
3379
+ } catch (err) {
3380
+ throw new SkillInstallError(name, "stage", err);
3381
+ }
3382
+ try {
3383
+ if (!content || content.trim().length === 0) {
3384
+ throw new Error("Skill content is empty");
3385
+ }
3386
+ const expectedHash = computeSkillHash(content);
3387
+ const stagedContent = readFileSync8(join11(stagingClaudeDir, "SKILL.md"), "utf-8");
3388
+ const stagedHash = computeSkillHash(stagedContent);
3389
+ if (stagedHash !== expectedHash) {
3390
+ throw new Error("Staged file hash does not match expected content");
3391
+ }
3392
+ } catch (err) {
3393
+ if (err instanceof SkillInstallError)
3394
+ throw err;
3395
+ throw new SkillInstallError(name, "validate", err);
3396
+ }
3397
+ try {
3398
+ mkdirSync8(join11(projectRoot, CLAUDE_SKILLS_DIR), { recursive: true });
3399
+ mkdirSync8(join11(projectRoot, AGENTS_SKILLS_DIR), { recursive: true });
3400
+ if (existsSync11(claudeDir)) {
3401
+ rmSync(claudeDir, { recursive: true, force: true });
3402
+ }
3403
+ if (existsSync11(agentsDir)) {
3404
+ rmSync(agentsDir, { recursive: true, force: true });
3405
+ }
3406
+ try {
3407
+ renameSync2(stagingClaudeDir, claudeDir);
3408
+ wroteClaudeDir = true;
3409
+ } catch {
3410
+ mkdirSync8(claudeDir, { recursive: true });
3411
+ writeFileSync8(join11(claudeDir, "SKILL.md"), content, "utf-8");
3412
+ wroteClaudeDir = true;
3413
+ }
3414
+ try {
3415
+ renameSync2(stagingAgentsDir, agentsDir);
3416
+ wroteAgentsDir = true;
3417
+ } catch {
3418
+ mkdirSync8(agentsDir, { recursive: true });
3419
+ writeFileSync8(join11(agentsDir, "SKILL.md"), content, "utf-8");
3420
+ wroteAgentsDir = true;
3421
+ }
3422
+ } catch (err) {
3423
+ if (err instanceof SkillInstallError)
3424
+ throw err;
3425
+ throw new SkillInstallError(name, "write", err);
3426
+ }
3427
+ try {
3428
+ const lock = readLockFile(projectRoot);
3429
+ lock.skills[name] = {
3430
+ source,
3431
+ sourceType: "github",
3432
+ computedHash: computeSkillHash(content)
3433
+ };
3434
+ writeLockFile(projectRoot, lock);
3435
+ } catch (err) {
3436
+ throw new SkillInstallError(name, "register", err);
3437
+ }
3438
+ } catch (err) {
3439
+ if (wroteClaudeDir && existsSync11(claudeDir)) {
3440
+ rmSync(claudeDir, { recursive: true, force: true });
3441
+ }
3442
+ if (wroteAgentsDir && existsSync11(agentsDir)) {
3443
+ rmSync(agentsDir, { recursive: true, force: true });
3444
+ }
3445
+ throw err;
3446
+ } finally {
3447
+ if (existsSync11(stagingDir)) {
3448
+ rmSync(stagingDir, { recursive: true, force: true });
3449
+ }
3450
+ }
3371
3451
  }
3372
3452
  async function removeSkill(projectRoot, name) {
3373
- if (!isSkillInstalled(projectRoot, name)) {
3453
+ const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3454
+ const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3455
+ const lock = readLockFile(projectRoot);
3456
+ const inLockFile = name in lock.skills;
3457
+ const hasClaude = existsSync11(claudeDir);
3458
+ const hasAgents = existsSync11(agentsDir);
3459
+ if (!inLockFile && !hasClaude && !hasAgents) {
3374
3460
  console.warn(`Skill "${name}" is not installed.`);
3375
3461
  return;
3376
3462
  }
3377
- const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3378
- const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3379
- if (existsSync11(claudeDir)) {
3463
+ if (hasClaude) {
3380
3464
  rmSync(claudeDir, { recursive: true, force: true });
3381
3465
  }
3382
- if (existsSync11(agentsDir)) {
3466
+ if (hasAgents) {
3383
3467
  rmSync(agentsDir, { recursive: true, force: true });
3384
3468
  }
3385
- const lock = readLockFile(projectRoot);
3386
- delete lock.skills[name];
3387
- writeLockFile(projectRoot, lock);
3469
+ if (inLockFile) {
3470
+ delete lock.skills[name];
3471
+ writeLockFile(projectRoot, lock);
3472
+ }
3388
3473
  }
3389
3474
  function isSkillInstalled(projectRoot, name) {
3390
3475
  const lock = readLockFile(projectRoot);
3391
3476
  return name in lock.skills;
3392
3477
  }
3478
+ function hasOrphanedSkillFiles(projectRoot, name) {
3479
+ const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3480
+ const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3481
+ const inLockFile = isSkillInstalled(projectRoot, name);
3482
+ return !inLockFile && (existsSync11(claudeDir) || existsSync11(agentsDir));
3483
+ }
3484
+ var SkillInstallError;
3393
3485
  var init_installer = __esm(() => {
3394
3486
  init_lock();
3487
+ SkillInstallError = class SkillInstallError extends Error {
3488
+ skillName;
3489
+ step;
3490
+ constructor(skillName, step, cause) {
3491
+ const stepLabel = {
3492
+ stage: "staging files to temp directory",
3493
+ validate: "validating skill content",
3494
+ write: "writing skill files to project",
3495
+ register: "updating skills-lock.json"
3496
+ };
3497
+ const reason = cause instanceof Error ? cause.message : String(cause ?? "");
3498
+ super(`Failed to install '${skillName}' during ${stepLabel[step]}${reason ? `: ${reason}` : ""}`);
3499
+ this.skillName = skillName;
3500
+ this.step = step;
3501
+ this.name = "SkillInstallError";
3502
+ }
3503
+ };
3395
3504
  });
3396
3505
 
3397
3506
  // src/skills/registry.ts
@@ -3565,7 +3674,22 @@ async function installRemoteSkill(name) {
3565
3674
  }
3566
3675
  const cwd = process.cwd();
3567
3676
  const source = `${REGISTRY_REPO}/${entry.path}`;
3568
- await installSkill(cwd, name, content, source);
3677
+ try {
3678
+ await installSkill(cwd, name, content, source);
3679
+ } catch (err) {
3680
+ if (err instanceof SkillInstallError) {
3681
+ process.stderr.write(`${red2("✗")} ${err.message}
3682
+ `);
3683
+ process.stderr.write(` To clean up and retry: ${bold2(`locus skills remove ${name}`)} then ${bold2(`locus skills install ${name}`)}
3684
+ `);
3685
+ } else {
3686
+ process.stderr.write(`${red2("✗")} Unexpected error installing skill '${name}'.
3687
+ `);
3688
+ process.stderr.write(` ${dim2(err.message)}
3689
+ `);
3690
+ }
3691
+ process.exit(1);
3692
+ }
3569
3693
  process.stderr.write(`
3570
3694
  ${green("✓")} Installed skill '${bold2(name)}' from ${REGISTRY_REPO}
3571
3695
  `);
@@ -3584,7 +3708,9 @@ async function removeInstalledSkill(name) {
3584
3708
  process.exit(1);
3585
3709
  }
3586
3710
  const cwd = process.cwd();
3587
- if (!isSkillInstalled(cwd, name)) {
3711
+ const installed = isSkillInstalled(cwd, name);
3712
+ const orphaned = hasOrphanedSkillFiles(cwd, name);
3713
+ if (!installed && !orphaned) {
3588
3714
  process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' is not installed.
3589
3715
  `);
3590
3716
  process.stderr.write(` Run ${bold2("locus skills list --installed")} to see installed skills.
@@ -3592,8 +3718,13 @@ async function removeInstalledSkill(name) {
3592
3718
  process.exit(1);
3593
3719
  }
3594
3720
  await removeSkill(cwd, name);
3595
- process.stderr.write(`${green("✓")} Removed skill '${bold2(name)}'
3721
+ if (orphaned) {
3722
+ process.stderr.write(`${green("✓")} Cleaned up orphaned skill files for '${bold2(name)}'
3723
+ `);
3724
+ } else {
3725
+ process.stderr.write(`${green("✓")} Removed skill '${bold2(name)}'
3596
3726
  `);
3727
+ }
3597
3728
  }
3598
3729
  async function updateSkills(name) {
3599
3730
  const cwd = process.cwd();
@@ -3649,7 +3780,20 @@ async function updateSkills(name) {
3649
3780
  continue;
3650
3781
  }
3651
3782
  const source = `${REGISTRY_REPO}/${entry.path}`;
3652
- await installSkill(cwd, skillName, content, source);
3783
+ try {
3784
+ await installSkill(cwd, skillName, content, source);
3785
+ } catch (err) {
3786
+ if (err instanceof SkillInstallError) {
3787
+ process.stderr.write(`${red2("✗")} ${err.message}
3788
+ `);
3789
+ process.stderr.write(` To clean up: ${bold2(`locus skills remove ${skillName}`)}
3790
+ `);
3791
+ } else {
3792
+ process.stderr.write(`${red2("✗")} Failed to update '${skillName}': ${dim2(err.message)}
3793
+ `);
3794
+ }
3795
+ continue;
3796
+ }
3653
3797
  updatedCount++;
3654
3798
  process.stderr.write(`${green("✓")} Updated '${bold2(skillName)}' (hash changed)
3655
3799
  `);
@@ -3743,7 +3887,7 @@ ${bold2("Subcommands:")}
3743
3887
  ${cyan2("list")} List available skills from the registry
3744
3888
  ${cyan2("list")} ${dim2("--installed")} List locally installed skills
3745
3889
  ${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
3746
- ${cyan2("remove")} ${dim2("<name>")} Remove an installed skill
3890
+ ${cyan2("remove")} ${dim2("<name>")} Remove an installed skill (alias: ${cyan2("uninstall")})
3747
3891
  ${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
3748
3892
  ${cyan2("info")} ${dim2("<name>")} Show skill metadata and install status
3749
3893
 
@@ -3778,7 +3922,8 @@ async function skillsCommand(args, flags) {
3778
3922
  await installRemoteSkill(skillName);
3779
3923
  break;
3780
3924
  }
3781
- case "remove": {
3925
+ case "remove":
3926
+ case "uninstall": {
3782
3927
  const skillName = args[1];
3783
3928
  await removeInstalledSkill(skillName);
3784
3929
  break;
@@ -3796,7 +3941,7 @@ async function skillsCommand(args, flags) {
3796
3941
  default:
3797
3942
  process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
3798
3943
  `);
3799
- process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")}, ${bold2("update")}, ${bold2("info")}
3944
+ process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
3800
3945
  `);
3801
3946
  process.exit(1);
3802
3947
  }
@@ -3947,7 +4092,7 @@ __export(exports_logs, {
3947
4092
  import {
3948
4093
  existsSync as existsSync12,
3949
4094
  readdirSync as readdirSync2,
3950
- readFileSync as readFileSync8,
4095
+ readFileSync as readFileSync9,
3951
4096
  statSync as statSync2,
3952
4097
  unlinkSync as unlinkSync2
3953
4098
  } from "node:fs";
@@ -3974,7 +4119,7 @@ async function logsCommand(cwd, options) {
3974
4119
  return viewLog(logFiles[0], options.level, options.lines ?? 50);
3975
4120
  }
3976
4121
  function viewLog(logFile, levelFilter, maxLines) {
3977
- const content = readFileSync8(logFile, "utf-8");
4122
+ const content = readFileSync9(logFile, "utf-8");
3978
4123
  const lines = content.trim().split(`
3979
4124
  `).filter(Boolean);
3980
4125
  process.stderr.write(`
@@ -4011,7 +4156,7 @@ async function tailLog(logFile, levelFilter) {
4011
4156
  `);
4012
4157
  let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
4013
4158
  if (existsSync12(logFile)) {
4014
- const content = readFileSync8(logFile, "utf-8");
4159
+ const content = readFileSync9(logFile, "utf-8");
4015
4160
  const lines = content.trim().split(`
4016
4161
  `).filter(Boolean);
4017
4162
  const recent = lines.slice(-10);
@@ -4034,7 +4179,7 @@ async function tailLog(logFile, levelFilter) {
4034
4179
  const currentSize = statSync2(logFile).size;
4035
4180
  if (currentSize <= lastSize)
4036
4181
  return;
4037
- const content = readFileSync8(logFile, "utf-8");
4182
+ const content = readFileSync9(logFile, "utf-8");
4038
4183
  const allLines = content.trim().split(`
4039
4184
  `).filter(Boolean);
4040
4185
  const oldContent = content.slice(0, lastSize);
@@ -4400,7 +4545,7 @@ var init_stream_renderer = __esm(() => {
4400
4545
  // src/repl/clipboard.ts
4401
4546
  import { execSync as execSync6 } from "node:child_process";
4402
4547
  import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
4403
- import { tmpdir } from "node:os";
4548
+ import { tmpdir as tmpdir2 } from "node:os";
4404
4549
  import { join as join13 } from "node:path";
4405
4550
  function readClipboardImage() {
4406
4551
  if (process.platform === "darwin") {
@@ -4466,12 +4611,12 @@ function readLinuxClipboardImage() {
4466
4611
  }
4467
4612
  var STABLE_DIR;
4468
4613
  var init_clipboard = __esm(() => {
4469
- STABLE_DIR = join13(tmpdir(), "locus-images");
4614
+ STABLE_DIR = join13(tmpdir2(), "locus-images");
4470
4615
  });
4471
4616
 
4472
4617
  // src/repl/image-detect.ts
4473
4618
  import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
4474
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
4619
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
4475
4620
  import { basename, extname, join as join14, resolve } from "node:path";
4476
4621
  function detectImages(input) {
4477
4622
  const detected = [];
@@ -4676,7 +4821,7 @@ var init_image_detect = __esm(() => {
4676
4821
  ".tif",
4677
4822
  ".tiff"
4678
4823
  ]);
4679
- STABLE_DIR2 = join14(tmpdir2(), "locus-images");
4824
+ STABLE_DIR2 = join14(tmpdir3(), "locus-images");
4680
4825
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
4681
4826
  });
4682
4827
 
@@ -5689,17 +5834,17 @@ import {
5689
5834
  mkdirSync as mkdirSync11,
5690
5835
  mkdtempSync,
5691
5836
  readdirSync as readdirSync3,
5692
- readFileSync as readFileSync9,
5837
+ readFileSync as readFileSync10,
5693
5838
  rmSync as rmSync2,
5694
5839
  statSync as statSync3
5695
5840
  } from "node:fs";
5696
- import { tmpdir as tmpdir3 } from "node:os";
5841
+ import { tmpdir as tmpdir4 } from "node:os";
5697
5842
  import { dirname as dirname3, join as join15, relative } from "node:path";
5698
5843
  import { promisify } from "node:util";
5699
5844
  function parseIgnoreFile(filePath) {
5700
5845
  if (!existsSync15(filePath))
5701
5846
  return [];
5702
- const content = readFileSync9(filePath, "utf-8");
5847
+ const content = readFileSync10(filePath, "utf-8");
5703
5848
  const rules = [];
5704
5849
  for (const rawLine of content.split(`
5705
5850
  `)) {
@@ -5810,7 +5955,7 @@ function backupIgnoredFiles(projectRoot) {
5810
5955
  return NOOP_BACKUP;
5811
5956
  let backupDir;
5812
5957
  try {
5813
- backupDir = mkdtempSync(join15(tmpdir3(), "locus-sandbox-backup-"));
5958
+ backupDir = mkdtempSync(join15(tmpdir4(), "locus-sandbox-backup-"));
5814
5959
  } catch (err) {
5815
5960
  log.debug("Failed to create sandbox backup dir", {
5816
5961
  error: err instanceof Error ? err.message : String(err)
@@ -8028,7 +8173,7 @@ var init_sprint = __esm(() => {
8028
8173
 
8029
8174
  // src/core/prompt-builder.ts
8030
8175
  import { execSync as execSync9 } from "node:child_process";
8031
- import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
8176
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
8032
8177
  import { join as join16 } from "node:path";
8033
8178
  function buildExecutionPrompt(ctx) {
8034
8179
  const sections = [];
@@ -8256,7 +8401,7 @@ function readFileSafe(path) {
8256
8401
  try {
8257
8402
  if (!existsSync16(path))
8258
8403
  return null;
8259
- return readFileSync10(path, "utf-8");
8404
+ return readFileSync11(path, "utf-8");
8260
8405
  } catch {
8261
8406
  return null;
8262
8407
  }
@@ -8825,7 +8970,7 @@ class CombinedCompletion {
8825
8970
  var init_completions = () => {};
8826
8971
 
8827
8972
  // src/repl/input-history.ts
8828
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "node:fs";
8973
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
8829
8974
  import { dirname as dirname5, join as join18 } from "node:path";
8830
8975
 
8831
8976
  class InputHistory {
@@ -8873,7 +9018,7 @@ class InputHistory {
8873
9018
  try {
8874
9019
  if (!existsSync17(this.filePath))
8875
9020
  return;
8876
- const content = readFileSync11(this.filePath, "utf-8");
9021
+ const content = readFileSync12(this.filePath, "utf-8");
8877
9022
  this.entries = content.split(`
8878
9023
  `).map((line) => this.unescape(line)).filter(Boolean);
8879
9024
  } catch {}
@@ -8915,7 +9060,7 @@ import {
8915
9060
  existsSync as existsSync18,
8916
9061
  mkdirSync as mkdirSync13,
8917
9062
  readdirSync as readdirSync6,
8918
- readFileSync as readFileSync12,
9063
+ readFileSync as readFileSync13,
8919
9064
  unlinkSync as unlinkSync3,
8920
9065
  writeFileSync as writeFileSync10
8921
9066
  } from "node:fs";
@@ -8958,7 +9103,7 @@ class SessionManager {
8958
9103
  const exactPath = this.getSessionPath(idOrPrefix);
8959
9104
  if (existsSync18(exactPath)) {
8960
9105
  try {
8961
- return JSON.parse(readFileSync12(exactPath, "utf-8"));
9106
+ return JSON.parse(readFileSync13(exactPath, "utf-8"));
8962
9107
  } catch {
8963
9108
  return null;
8964
9109
  }
@@ -8966,7 +9111,7 @@ class SessionManager {
8966
9111
  const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
8967
9112
  if (matches.length === 1) {
8968
9113
  try {
8969
- return JSON.parse(readFileSync12(matches[0], "utf-8"));
9114
+ return JSON.parse(readFileSync13(matches[0], "utf-8"));
8970
9115
  } catch {
8971
9116
  return null;
8972
9117
  }
@@ -8991,7 +9136,7 @@ class SessionManager {
8991
9136
  const sessions = [];
8992
9137
  for (const file of files) {
8993
9138
  try {
8994
- const session = JSON.parse(readFileSync12(file, "utf-8"));
9139
+ const session = JSON.parse(readFileSync13(file, "utf-8"));
8995
9140
  sessions.push({
8996
9141
  id: session.id,
8997
9142
  created: session.created,
@@ -9018,7 +9163,7 @@ class SessionManager {
9018
9163
  let pruned = 0;
9019
9164
  const withStats = files.map((f) => {
9020
9165
  try {
9021
- const session = JSON.parse(readFileSync12(f, "utf-8"));
9166
+ const session = JSON.parse(readFileSync13(f, "utf-8"));
9022
9167
  return { path: f, updated: new Date(session.updated).getTime() };
9023
9168
  } catch {
9024
9169
  return { path: f, updated: 0 };
@@ -9072,7 +9217,7 @@ var init_session_manager = __esm(() => {
9072
9217
  // src/repl/voice.ts
9073
9218
  import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
9074
9219
  import { existsSync as existsSync19, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
9075
- import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
9220
+ import { cpus, homedir as homedir4, platform, tmpdir as tmpdir5 } from "node:os";
9076
9221
  import { join as join20 } from "node:path";
9077
9222
  function getWhisperModelPath() {
9078
9223
  return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
@@ -9284,7 +9429,7 @@ function ensureBuildDeps(pm) {
9284
9429
  }
9285
9430
  function buildWhisperFromSource(pm) {
9286
9431
  const out = process.stderr;
9287
- const buildDir = join20(tmpdir4(), `locus-whisper-build-${process.pid}`);
9432
+ const buildDir = join20(tmpdir5(), `locus-whisper-build-${process.pid}`);
9288
9433
  if (!ensureBuildDeps(pm)) {
9289
9434
  out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
9290
9435
  `);
@@ -9442,7 +9587,7 @@ class VoiceController {
9442
9587
  onStateChange;
9443
9588
  constructor(options) {
9444
9589
  this.onStateChange = options.onStateChange;
9445
- this.tempFile = join20(tmpdir4(), `locus-voice-${process.pid}.wav`);
9590
+ this.tempFile = join20(tmpdir5(), `locus-voice-${process.pid}.wav`);
9446
9591
  this.deps = checkDependencies();
9447
9592
  }
9448
9593
  getState() {
@@ -10384,7 +10529,8 @@ ${green("✓")} Issue #${issueNumber} completed ${dim2(`(${timer.formatted()})`)
10384
10529
  `);
10385
10530
  let prNumber;
10386
10531
  if (config.agent.autoPR && !options.skipPR) {
10387
- prNumber = await createIssuePR(projectRoot, config, issue);
10532
+ const workDir = options.worktreePath ?? projectRoot;
10533
+ prNumber = await createIssuePR(workDir, config, issue);
10388
10534
  }
10389
10535
  if (config.agent.autoLabel) {
10390
10536
  try {
@@ -10465,29 +10611,56 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
10465
10611
  summary: extractSummary(aiResult.output)
10466
10612
  };
10467
10613
  }
10468
- async function createIssuePR(projectRoot, config, issue) {
10614
+ async function createIssuePR(workDir, config, issue) {
10469
10615
  try {
10616
+ try {
10617
+ commitDirtySubmodules(workDir, issue.number, issue.title);
10618
+ const status = execSync14("git status --porcelain", {
10619
+ cwd: workDir,
10620
+ encoding: "utf-8",
10621
+ stdio: ["pipe", "pipe", "pipe"]
10622
+ }).trim();
10623
+ if (status) {
10624
+ execSync14("git add -A", {
10625
+ cwd: workDir,
10626
+ encoding: "utf-8",
10627
+ stdio: ["pipe", "pipe", "pipe"]
10628
+ });
10629
+ const message = `chore: complete #${issue.number} - ${issue.title}
10630
+
10631
+ Co-Authored-By: LocusAgent <agent@locusai.team>`;
10632
+ execSync14("git commit -F -", {
10633
+ input: message,
10634
+ cwd: workDir,
10635
+ encoding: "utf-8",
10636
+ stdio: ["pipe", "pipe", "pipe"]
10637
+ });
10638
+ process.stderr.write(` ${dim2(`Committed uncommitted changes for #${issue.number}`)}
10639
+ `);
10640
+ }
10641
+ } catch {}
10470
10642
  const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
10471
- cwd: projectRoot,
10643
+ cwd: workDir,
10472
10644
  encoding: "utf-8",
10473
10645
  stdio: ["pipe", "pipe", "pipe"]
10474
10646
  }).trim();
10475
10647
  const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10476
- cwd: projectRoot,
10648
+ cwd: workDir,
10477
10649
  encoding: "utf-8",
10478
10650
  stdio: ["pipe", "pipe", "pipe"]
10479
10651
  }).trim();
10480
10652
  if (!diff) {
10481
- getLogger().verbose("No changes to create PR for");
10653
+ process.stderr.write(` ${yellow2("⚠")} No changes detected skipping PR creation
10654
+ `);
10482
10655
  return;
10483
10656
  }
10484
- pushSubmoduleBranches(projectRoot);
10657
+ pushSubmoduleBranches(workDir);
10485
10658
  execSync14(`git push -u origin ${currentBranch}`, {
10486
- cwd: projectRoot,
10659
+ cwd: workDir,
10487
10660
  encoding: "utf-8",
10488
10661
  stdio: ["pipe", "pipe", "pipe"]
10489
10662
  });
10490
- const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
10663
+ const submoduleSummary = getSubmoduleChangeSummary(workDir, config.agent.baseBranch);
10491
10664
  let prBody = `Closes #${issue.number}`;
10492
10665
  if (submoduleSummary) {
10493
10666
  prBody += `
@@ -10502,7 +10675,7 @@ ${submoduleSummary}`;
10502
10675
  const prTitle = `${issue.title} (#${issue.number})`;
10503
10676
  let prNumber;
10504
10677
  try {
10505
- const existing = execSync14(`gh pr list --head ${currentBranch} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10678
+ const existing = execSync14(`gh pr list --head ${currentBranch} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10506
10679
  const parsed = JSON.parse(existing);
10507
10680
  if (Array.isArray(parsed) && parsed.length > 0) {
10508
10681
  prNumber = parsed[0].number;
@@ -10512,7 +10685,7 @@ ${submoduleSummary}`;
10512
10685
  try {
10513
10686
  execSync14(`gh pr edit ${prNumber} --title ${JSON.stringify(prTitle)} --body-file -`, {
10514
10687
  input: prBody,
10515
- cwd: projectRoot,
10688
+ cwd: workDir,
10516
10689
  encoding: "utf-8",
10517
10690
  stdio: ["pipe", "pipe", "pipe"]
10518
10691
  });
@@ -10522,13 +10695,15 @@ ${submoduleSummary}`;
10522
10695
  getLogger().warn(`Failed to update PR #${prNumber}: ${editErr}`);
10523
10696
  }
10524
10697
  } else {
10525
- prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
10698
+ prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: workDir });
10526
10699
  process.stderr.write(` ${green("✓")} Created PR #${prNumber}
10527
10700
  `);
10528
10701
  }
10529
10702
  return prNumber;
10530
10703
  } catch (e) {
10531
- getLogger().warn(`Failed to create PR: ${e}`);
10704
+ const errorMsg = e instanceof Error ? e.message : String(e);
10705
+ process.stderr.write(` ${red2("✗")} Failed to create PR: ${errorMsg}
10706
+ `);
10532
10707
  return;
10533
10708
  }
10534
10709
  }
@@ -10693,7 +10868,7 @@ var init_conflict = __esm(() => {
10693
10868
  import {
10694
10869
  existsSync as existsSync21,
10695
10870
  mkdirSync as mkdirSync15,
10696
- readFileSync as readFileSync13,
10871
+ readFileSync as readFileSync14,
10697
10872
  unlinkSync as unlinkSync5,
10698
10873
  writeFileSync as writeFileSync11
10699
10874
  } from "node:fs";
@@ -10716,7 +10891,7 @@ function loadRunState(projectRoot, sprintName) {
10716
10891
  if (!existsSync21(path))
10717
10892
  return null;
10718
10893
  try {
10719
- return JSON.parse(readFileSync13(path, "utf-8"));
10894
+ return JSON.parse(readFileSync14(path, "utf-8"));
10720
10895
  } catch {
10721
10896
  getLogger().warn("Corrupted run state file, ignoring");
10722
10897
  return null;
@@ -11508,7 +11683,7 @@ async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandbo
11508
11683
  ${bold2("Running sprint issue")} ${cyan2(`#${issueNumber}`)} ${dim2("(sequential)")}
11509
11684
 
11510
11685
  `);
11511
- await executeIssue(projectRoot, {
11686
+ const result2 = await executeIssue(projectRoot, {
11512
11687
  issueNumber,
11513
11688
  provider: execution.provider,
11514
11689
  model: execution.model,
@@ -11517,6 +11692,11 @@ ${bold2("Running sprint issue")} ${cyan2(`#${issueNumber}`)} ${dim2("(sequential
11517
11692
  sandboxName: execution.sandboxName,
11518
11693
  containerWorkdir: execution.containerWorkdir
11519
11694
  });
11695
+ if (!result2.success) {
11696
+ process.stderr.write(`
11697
+ ${red2("✗")} Issue #${issueNumber} execution failed${result2.error ? `: ${result2.error}` : ""}
11698
+ `);
11699
+ }
11520
11700
  return;
11521
11701
  }
11522
11702
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -11559,6 +11739,15 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
11559
11739
  });
11560
11740
  log.info(`Checked out ${config.agent.baseBranch}`);
11561
11741
  } catch {}
11742
+ if (result.prNumber) {
11743
+ process.stderr.write(`
11744
+ ${green("✓")} Issue #${issueNumber} done — PR #${result.prNumber} opened
11745
+ `);
11746
+ } else if (config.agent.autoPR) {
11747
+ process.stderr.write(`
11748
+ ${yellow2("⚠")} Issue #${issueNumber} done — no PR was created (check errors above)
11749
+ `);
11750
+ }
11562
11751
  } else {
11563
11752
  process.stderr.write(` ${yellow2("⚠")} Branch ${dim2(branchName)} preserved for debugging.
11564
11753
  `);
@@ -12302,7 +12491,7 @@ import {
12302
12491
  existsSync as existsSync25,
12303
12492
  mkdirSync as mkdirSync17,
12304
12493
  readdirSync as readdirSync8,
12305
- readFileSync as readFileSync14,
12494
+ readFileSync as readFileSync15,
12306
12495
  writeFileSync as writeFileSync12
12307
12496
  } from "node:fs";
12308
12497
  import { join as join26 } from "node:path";
@@ -12358,7 +12547,7 @@ function loadPlanFile(projectRoot, id) {
12358
12547
  if (!match)
12359
12548
  return null;
12360
12549
  try {
12361
- const content = readFileSync14(join26(dir, match), "utf-8");
12550
+ const content = readFileSync15(join26(dir, match), "utf-8");
12362
12551
  return JSON.parse(content);
12363
12552
  } catch {
12364
12553
  return null;
@@ -12443,7 +12632,7 @@ ${bold2("Saved Plans:")}
12443
12632
  for (const file of files) {
12444
12633
  const id = file.replace(".json", "");
12445
12634
  try {
12446
- const content = readFileSync14(join26(dir, file), "utf-8");
12635
+ const content = readFileSync15(join26(dir, file), "utf-8");
12447
12636
  const plan = JSON.parse(content);
12448
12637
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
12449
12638
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -12558,7 +12747,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
12558
12747
  }
12559
12748
  let updatedPlan;
12560
12749
  try {
12561
- const content = readFileSync14(planPath, "utf-8");
12750
+ const content = readFileSync15(planPath, "utf-8");
12562
12751
  updatedPlan = JSON.parse(content);
12563
12752
  } catch {
12564
12753
  process.stderr.write(`
@@ -12701,7 +12890,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12701
12890
  }
12702
12891
  let plan;
12703
12892
  try {
12704
- const content = readFileSync14(planPath, "utf-8");
12893
+ const content = readFileSync15(planPath, "utf-8");
12705
12894
  plan = JSON.parse(content);
12706
12895
  } catch {
12707
12896
  process.stderr.write(`
@@ -12875,14 +13064,14 @@ ${directive}${sprintName ? `
12875
13064
  </directive>`);
12876
13065
  const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
12877
13066
  if (existsSync25(locusPath)) {
12878
- const content = readFileSync14(locusPath, "utf-8");
13067
+ const content = readFileSync15(locusPath, "utf-8");
12879
13068
  parts.push(`<project-context>
12880
13069
  ${content.slice(0, 3000)}
12881
13070
  </project-context>`);
12882
13071
  }
12883
13072
  const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
12884
13073
  if (existsSync25(learningsPath)) {
12885
- const content = readFileSync14(learningsPath, "utf-8");
13074
+ const content = readFileSync15(learningsPath, "utf-8");
12886
13075
  parts.push(`<past-learnings>
12887
13076
  ${content.slice(0, 2000)}
12888
13077
  </past-learnings>`);
@@ -12936,7 +13125,7 @@ ${feedback}
12936
13125
  </feedback>`);
12937
13126
  const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
12938
13127
  if (existsSync25(locusPath)) {
12939
- const content = readFileSync14(locusPath, "utf-8");
13128
+ const content = readFileSync15(locusPath, "utf-8");
12940
13129
  parts.push(`<project-context>
12941
13130
  ${content.slice(0, 3000)}
12942
13131
  </project-context>`);
@@ -13118,7 +13307,7 @@ __export(exports_review, {
13118
13307
  reviewCommand: () => reviewCommand
13119
13308
  });
13120
13309
  import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
13121
- import { existsSync as existsSync26, readFileSync as readFileSync15 } from "node:fs";
13310
+ import { existsSync as existsSync26, readFileSync as readFileSync16 } from "node:fs";
13122
13311
  import { join as join27 } from "node:path";
13123
13312
  function printHelp3() {
13124
13313
  process.stderr.write(`
@@ -13290,7 +13479,7 @@ You are an expert code reviewer for the ${config.github.owner}/${config.github.r
13290
13479
  </role>`);
13291
13480
  const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13292
13481
  if (existsSync26(locusPath)) {
13293
- const content = readFileSync15(locusPath, "utf-8");
13482
+ const content = readFileSync16(locusPath, "utf-8");
13294
13483
  parts.push(`<project-context>
13295
13484
  ${content.slice(0, 2000)}
13296
13485
  </project-context>`);
@@ -13608,7 +13797,7 @@ import {
13608
13797
  existsSync as existsSync27,
13609
13798
  mkdirSync as mkdirSync18,
13610
13799
  readdirSync as readdirSync9,
13611
- readFileSync as readFileSync16,
13800
+ readFileSync as readFileSync17,
13612
13801
  unlinkSync as unlinkSync6,
13613
13802
  writeFileSync as writeFileSync13
13614
13803
  } from "node:fs";
@@ -13690,7 +13879,7 @@ ${bold2("Discussions:")}
13690
13879
  `);
13691
13880
  for (const file of files) {
13692
13881
  const id = file.replace(".md", "");
13693
- const content = readFileSync16(join28(dir, file), "utf-8");
13882
+ const content = readFileSync17(join28(dir, file), "utf-8");
13694
13883
  const titleMatch = content.match(/^#\s+(.+)/m);
13695
13884
  const title = titleMatch ? titleMatch[1] : id;
13696
13885
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -13720,7 +13909,7 @@ function showDiscussion(projectRoot, id) {
13720
13909
  `);
13721
13910
  return;
13722
13911
  }
13723
- const content = readFileSync16(join28(dir, match), "utf-8");
13912
+ const content = readFileSync17(join28(dir, match), "utf-8");
13724
13913
  process.stdout.write(`${content}
13725
13914
  `);
13726
13915
  }
@@ -13768,7 +13957,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
13768
13957
  `);
13769
13958
  return;
13770
13959
  }
13771
- const content = readFileSync16(join28(dir, match), "utf-8");
13960
+ const content = readFileSync17(join28(dir, match), "utf-8");
13772
13961
  const titleMatch = content.match(/^#\s+(.+)/m);
13773
13962
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
13774
13963
  await planCommand(projectRoot, [
@@ -13911,14 +14100,14 @@ You are a senior software architect and consultant for the ${config.github.owner
13911
14100
  </role>`);
13912
14101
  const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
13913
14102
  if (existsSync27(locusPath)) {
13914
- const content = readFileSync16(locusPath, "utf-8");
14103
+ const content = readFileSync17(locusPath, "utf-8");
13915
14104
  parts.push(`<project-context>
13916
14105
  ${content.slice(0, 3000)}
13917
14106
  </project-context>`);
13918
14107
  }
13919
14108
  const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
13920
14109
  if (existsSync27(learningsPath)) {
13921
- const content = readFileSync16(learningsPath, "utf-8");
14110
+ const content = readFileSync17(learningsPath, "utf-8");
13922
14111
  parts.push(`<past-learnings>
13923
14112
  ${content.slice(0, 2000)}
13924
14113
  </past-learnings>`);
@@ -13989,7 +14178,7 @@ __export(exports_artifacts, {
13989
14178
  formatDate: () => formatDate2,
13990
14179
  artifactsCommand: () => artifactsCommand
13991
14180
  });
13992
- import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "node:fs";
14181
+ import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync18, statSync as statSync5 } from "node:fs";
13993
14182
  import { join as join29 } from "node:path";
13994
14183
  function printHelp6() {
13995
14184
  process.stderr.write(`
@@ -14035,7 +14224,7 @@ function readArtifact(projectRoot, name) {
14035
14224
  return null;
14036
14225
  const stat = statSync5(filePath);
14037
14226
  return {
14038
- content: readFileSync17(filePath, "utf-8"),
14227
+ content: readFileSync18(filePath, "utf-8"),
14039
14228
  info: {
14040
14229
  name: fileName.replace(/\.md$/, ""),
14041
14230
  fileName,
@@ -14392,7 +14581,7 @@ __export(exports_sandbox2, {
14392
14581
  });
14393
14582
  import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
14394
14583
  import { createHash as createHash2 } from "node:crypto";
14395
- import { existsSync as existsSync29, readFileSync as readFileSync18 } from "node:fs";
14584
+ import { existsSync as existsSync29, readFileSync as readFileSync19 } from "node:fs";
14396
14585
  import { basename as basename4, join as join30 } from "node:path";
14397
14586
  import { createInterface as createInterface3 } from "node:readline";
14398
14587
  function printSandboxHelp() {
@@ -14923,7 +15112,7 @@ async function handleLogs(projectRoot, args) {
14923
15112
  }
14924
15113
  function detectPackageManager2(projectRoot) {
14925
15114
  try {
14926
- const raw = readFileSync18(join30(projectRoot, "package.json"), "utf-8");
15115
+ const raw = readFileSync19(join30(projectRoot, "package.json"), "utf-8");
14927
15116
  const pkgJson = JSON.parse(raw);
14928
15117
  if (typeof pkgJson.packageManager === "string") {
14929
15118
  const name = pkgJson.packageManager.split("@")[0];
@@ -15216,7 +15405,7 @@ init_context();
15216
15405
  init_logger();
15217
15406
  init_rate_limiter();
15218
15407
  init_terminal();
15219
- import { existsSync as existsSync30, readFileSync as readFileSync19 } from "node:fs";
15408
+ import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
15220
15409
  import { join as join31 } from "node:path";
15221
15410
  import { fileURLToPath } from "node:url";
15222
15411
  function getCliVersion() {
@@ -15226,7 +15415,7 @@ function getCliVersion() {
15226
15415
  return fallbackVersion;
15227
15416
  }
15228
15417
  try {
15229
- const parsed = JSON.parse(readFileSync19(packageJsonPath, "utf-8"));
15418
+ const parsed = JSON.parse(readFileSync20(packageJsonPath, "utf-8"));
15230
15419
  return parsed.version ?? fallbackVersion;
15231
15420
  } catch {
15232
15421
  return fallbackVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@locusai/sdk": "^0.25.2",
39
+ "@locusai/sdk": "^0.25.4",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },