@locusai/cli 0.21.17 → 0.22.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.
Files changed (2) hide show
  1. package/bin/locus.js +1227 -270
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -29,7 +29,7 @@ var init_dist = __esm(() => {
29
29
  },
30
30
  ai: {
31
31
  provider: "claude",
32
- model: "claude-sonnet-4-6"
32
+ model: "opus"
33
33
  },
34
34
  agent: {
35
35
  maxParallel: 3,
@@ -2226,35 +2226,384 @@ var init_init = __esm(() => {
2226
2226
  GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/LEARNINGS.md"];
2227
2227
  });
2228
2228
 
2229
+ // src/commands/create.ts
2230
+ var exports_create = {};
2231
+ __export(exports_create, {
2232
+ createCommand: () => createCommand
2233
+ });
2234
+ import { execSync as execSync4 } from "node:child_process";
2235
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2236
+ import { join as join7 } from "node:path";
2237
+ function validateName(name) {
2238
+ if (!name)
2239
+ return "Package name is required.";
2240
+ if (!NAME_PATTERN.test(name))
2241
+ return "Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens.";
2242
+ if (name.startsWith("locus-"))
2243
+ return `Don't include the "locus-" prefix — it's added automatically.`;
2244
+ if (name.length > 50)
2245
+ return "Name must be 50 characters or fewer.";
2246
+ return null;
2247
+ }
2248
+ function capitalize(str) {
2249
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2250
+ }
2251
+ function checkNpmExists(fullName) {
2252
+ try {
2253
+ execSync4(`npm view ${fullName} version 2>/dev/null`, {
2254
+ stdio: "pipe",
2255
+ timeout: 1e4
2256
+ });
2257
+ return true;
2258
+ } catch {
2259
+ return false;
2260
+ }
2261
+ }
2262
+ function parseCreateArgs(args) {
2263
+ let name = "";
2264
+ let description = "";
2265
+ let i = 0;
2266
+ while (i < args.length) {
2267
+ const arg = args[i];
2268
+ if (arg === "--description" || arg === "-D") {
2269
+ description = args[++i] ?? "";
2270
+ } else if (arg === "help" || arg === "--help" || arg === "-h") {
2271
+ return null;
2272
+ } else if (!arg.startsWith("-")) {
2273
+ name = arg;
2274
+ }
2275
+ i++;
2276
+ }
2277
+ return { name, description };
2278
+ }
2279
+ function generatePackageJson(name, displayName, description, sdkVersion) {
2280
+ const pkg = {
2281
+ name: `@locusai/locus-${name}`,
2282
+ version: "0.1.0",
2283
+ description,
2284
+ type: "module",
2285
+ bin: {
2286
+ [`locus-${name}`]: `./bin/locus-${name}.js`
2287
+ },
2288
+ files: ["bin", "package.json", "README.md"],
2289
+ locus: {
2290
+ displayName,
2291
+ description,
2292
+ commands: [name],
2293
+ version: "0.1.0"
2294
+ },
2295
+ scripts: {
2296
+ build: `bun build src/cli.ts --outfile bin/locus-${name}.js --target node`,
2297
+ typecheck: "tsc --noEmit",
2298
+ lint: "biome lint .",
2299
+ format: "biome format --write ."
2300
+ },
2301
+ dependencies: {
2302
+ "@locusai/sdk": `^${sdkVersion}`
2303
+ },
2304
+ devDependencies: {
2305
+ typescript: "^5.8.3"
2306
+ },
2307
+ keywords: ["locusai-package", "locus", name],
2308
+ engines: {
2309
+ node: ">=18"
2310
+ },
2311
+ license: "MIT"
2312
+ };
2313
+ return `${JSON.stringify(pkg, null, 2)}
2314
+ `;
2315
+ }
2316
+ function generateTsconfig() {
2317
+ const config = {
2318
+ compilerOptions: {
2319
+ target: "ES2022",
2320
+ module: "ESNext",
2321
+ moduleResolution: "bundler",
2322
+ strict: true,
2323
+ skipLibCheck: true,
2324
+ esModuleInterop: true,
2325
+ isolatedModules: true,
2326
+ resolveJsonModule: true,
2327
+ noEmit: true,
2328
+ rootDir: "./src"
2329
+ },
2330
+ include: ["src/**/*"],
2331
+ exclude: ["node_modules", "dist", "bin"]
2332
+ };
2333
+ return `${JSON.stringify(config, null, 2)}
2334
+ `;
2335
+ }
2336
+ function generateCliTs() {
2337
+ return `#!/usr/bin/env node
2338
+
2339
+ import { main } from "./index.js";
2340
+
2341
+ main(process.argv.slice(2)).catch((error) => {
2342
+ console.error(\`Fatal error: \${error.message}\`);
2343
+ process.exit(1);
2344
+ });
2345
+ `;
2346
+ }
2347
+ function generateIndexTs(name) {
2348
+ return `import { createLogger, readLocusConfig } from "@locusai/sdk";
2349
+
2350
+ const logger = createLogger("${name}");
2351
+
2352
+ export async function main(args: string[]): Promise<void> {
2353
+ const command = args[0] ?? "help";
2354
+
2355
+ switch (command) {
2356
+ case "start":
2357
+ return handleStart();
2358
+ case "help":
2359
+ case "--help":
2360
+ case "-h":
2361
+ return printHelp();
2362
+ default:
2363
+ console.error(\`Unknown command: \${command}\`);
2364
+ printHelp();
2365
+ process.exit(1);
2366
+ }
2367
+ }
2368
+
2369
+ // ─── Commands ────────────────────────────────────────────────────────────────
2370
+
2371
+ function handleStart(): void {
2372
+ const config = readLocusConfig();
2373
+ logger.info(\`Hello from locus-${name}! Repo: \${config.github.owner}/\${config.github.repo}\`);
2374
+ // TODO: Implement your package logic here
2375
+ }
2376
+
2377
+ // ─── Help ────────────────────────────────────────────────────────────────────
2378
+
2379
+ function printHelp(): void {
2380
+ console.log(\`
2381
+ locus-${name}
2382
+
2383
+ Usage:
2384
+ locus pkg ${name} <command>
2385
+
2386
+ Commands:
2387
+ start Start the ${name} integration
2388
+ help Show this help message
2389
+ \`);
2390
+ }
2391
+ `;
2392
+ }
2393
+ function generateReadme(name, displayName, description) {
2394
+ return `# @locusai/locus-${name}
2395
+
2396
+ ${description}
2397
+
2398
+ ## Installation
2399
+
2400
+ \`\`\`bash
2401
+ locus install ${name}
2402
+ \`\`\`
2403
+
2404
+ ## Usage
2405
+
2406
+ \`\`\`bash
2407
+ locus pkg ${name} start # Start the integration
2408
+ locus pkg ${name} help # Show help
2409
+ \`\`\`
2410
+
2411
+ ## Configuration
2412
+
2413
+ Configure via \`locus config\`:
2414
+
2415
+ \`\`\`bash
2416
+ locus config set packages.${name}.apiKey "your-api-key"
2417
+ \`\`\`
2418
+
2419
+ ## Development
2420
+
2421
+ \`\`\`bash
2422
+ # Build
2423
+ bun run build
2424
+
2425
+ # Type check
2426
+ bun run typecheck
2427
+
2428
+ # Lint
2429
+ bun run lint
2430
+
2431
+ # Test locally
2432
+ locus pkg ${name}
2433
+ \`\`\`
2434
+
2435
+ ## License
2436
+
2437
+ MIT
2438
+ `;
2439
+ }
2440
+ function printHelp() {
2441
+ process.stderr.write(`
2442
+ ${bold2("locus create")} — Scaffold a new Locus community package
2443
+
2444
+ ${bold2("Usage:")}
2445
+ locus create <name> [options]
2446
+
2447
+ ${bold2("Arguments:")}
2448
+ ${cyan2("<name>")} Package short name (e.g. slack, discord, jira)
2449
+
2450
+ ${bold2("Options:")}
2451
+ ${dim2("--description, -D")} Package description (default: auto-generated)
2452
+ ${dim2("--help, -h")} Show this help
2453
+
2454
+ ${bold2("Examples:")}
2455
+ locus create slack ${dim2("# Create packages/slack/")}
2456
+ locus create discord -D "Control Locus via Discord" ${dim2("# With custom description")}
2457
+
2458
+ ${bold2("What gets created:")}
2459
+ packages/<name>/
2460
+ ├── src/
2461
+ │ ├── cli.ts ${dim2("# Entry point")}
2462
+ │ └── index.ts ${dim2("# Main logic with command dispatch")}
2463
+ ├── package.json ${dim2("# Full config with locus manifest")}
2464
+ ├── tsconfig.json ${dim2("# TypeScript configuration")}
2465
+ └── README.md ${dim2("# Package documentation")}
2466
+
2467
+ ${bold2("Next steps after creation:")}
2468
+ ${gray2("1.")} cd packages/<name>
2469
+ ${gray2("2.")} Implement your logic in src/index.ts
2470
+ ${gray2("3.")} bun install && bun run build
2471
+ ${gray2("4.")} Test locally with: locus pkg <name>
2472
+ ${gray2("5.")} Submit a pull request
2473
+
2474
+ `);
2475
+ }
2476
+ async function createCommand(args) {
2477
+ const parsed = parseCreateArgs(args);
2478
+ if (!parsed || !parsed.name) {
2479
+ printHelp();
2480
+ if (parsed && !parsed.name) {
2481
+ process.stderr.write(`${red2("✗")} Package name is required.
2482
+
2483
+ `);
2484
+ process.stderr.write(` Usage: ${bold2("locus create <name>")}
2485
+
2486
+ `);
2487
+ process.exit(1);
2488
+ }
2489
+ return;
2490
+ }
2491
+ const { name } = parsed;
2492
+ const displayName = capitalize(name);
2493
+ const description = parsed.description || `${displayName} integration for Locus`;
2494
+ const fullNpmName = `@locusai/locus-${name}`;
2495
+ process.stderr.write(`
2496
+ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
2497
+
2498
+ `);
2499
+ const nameError = validateName(name);
2500
+ if (nameError) {
2501
+ process.stderr.write(`${red2("✗")} ${nameError}
2502
+ `);
2503
+ process.exit(1);
2504
+ }
2505
+ process.stderr.write(`${green("✓")} Name is valid: ${bold2(name)}
2506
+ `);
2507
+ const packagesDir = join7(process.cwd(), "packages", name);
2508
+ if (existsSync7(packagesDir)) {
2509
+ process.stderr.write(`${red2("✗")} Directory already exists: ${bold2(`packages/${name}/`)}
2510
+ `);
2511
+ process.exit(1);
2512
+ }
2513
+ process.stderr.write(`${cyan2("●")} Checking npm registry...`);
2514
+ const existsOnNpm = checkNpmExists(fullNpmName);
2515
+ if (existsOnNpm) {
2516
+ process.stderr.write(`\r${yellow2("⚠")} Package ${bold2(fullNpmName)} already exists on npm. You may want to choose a different name.
2517
+ `);
2518
+ } else {
2519
+ process.stderr.write(`\r${green("✓")} Name is available on npm
2520
+ `);
2521
+ }
2522
+ let sdkVersion = "0.22.0";
2523
+ try {
2524
+ const sdkPkgPath = join7(process.cwd(), "packages", "sdk", "package.json");
2525
+ if (existsSync7(sdkPkgPath)) {
2526
+ const { readFileSync: readFileSync5 } = await import("node:fs");
2527
+ const sdkPkg = JSON.parse(readFileSync5(sdkPkgPath, "utf-8"));
2528
+ if (sdkPkg.version)
2529
+ sdkVersion = sdkPkg.version;
2530
+ }
2531
+ } catch {}
2532
+ mkdirSync6(join7(packagesDir, "src"), { recursive: true });
2533
+ mkdirSync6(join7(packagesDir, "bin"), { recursive: true });
2534
+ process.stderr.write(`${green("✓")} Created directory structure
2535
+ `);
2536
+ writeFileSync5(join7(packagesDir, "package.json"), generatePackageJson(name, displayName, description, sdkVersion), "utf-8");
2537
+ process.stderr.write(`${green("✓")} Generated package.json
2538
+ `);
2539
+ writeFileSync5(join7(packagesDir, "tsconfig.json"), generateTsconfig(), "utf-8");
2540
+ process.stderr.write(`${green("✓")} Generated tsconfig.json
2541
+ `);
2542
+ writeFileSync5(join7(packagesDir, "src", "cli.ts"), generateCliTs(), "utf-8");
2543
+ process.stderr.write(`${green("✓")} Generated src/cli.ts
2544
+ `);
2545
+ writeFileSync5(join7(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
2546
+ process.stderr.write(`${green("✓")} Generated src/index.ts
2547
+ `);
2548
+ writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, displayName, description), "utf-8");
2549
+ process.stderr.write(`${green("✓")} Generated README.md
2550
+ `);
2551
+ process.stderr.write(`
2552
+ ${bold2(green("Package created!"))}
2553
+
2554
+ `);
2555
+ process.stderr.write(`${bold2("Next steps:")}
2556
+ `);
2557
+ process.stderr.write(` ${gray2("1.")} Implement your logic in ${bold2(`packages/${name}/src/index.ts`)}
2558
+ `);
2559
+ process.stderr.write(` ${gray2("2.")} Install dependencies: ${bold2("bun install")}
2560
+ `);
2561
+ process.stderr.write(` ${gray2("3.")} Build your package: ${bold2(`cd packages/${name} && bun run build`)}
2562
+ `);
2563
+ process.stderr.write(` ${gray2("4.")} Test locally: ${bold2(`locus pkg ${name}`)}
2564
+ `);
2565
+ process.stderr.write(` ${gray2("5.")} Submit a pull request
2566
+ `);
2567
+ process.stderr.write(`
2568
+ ${dim2("See the full guide:")} ${cyan2("packages/sdk/PACKAGE_GUIDE.md")}
2569
+
2570
+ `);
2571
+ }
2572
+ var NAME_PATTERN;
2573
+ var init_create = __esm(() => {
2574
+ init_terminal();
2575
+ NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
2576
+ });
2577
+
2229
2578
  // src/packages/registry.ts
2230
2579
  import {
2231
- existsSync as existsSync7,
2232
- mkdirSync as mkdirSync6,
2580
+ existsSync as existsSync8,
2581
+ mkdirSync as mkdirSync7,
2233
2582
  readFileSync as readFileSync5,
2234
2583
  renameSync,
2235
- writeFileSync as writeFileSync5
2584
+ writeFileSync as writeFileSync6
2236
2585
  } from "node:fs";
2237
2586
  import { homedir as homedir2 } from "node:os";
2238
- import { join as join7 } from "node:path";
2587
+ import { join as join8 } from "node:path";
2239
2588
  function getPackagesDir() {
2240
2589
  const home = process.env.HOME || homedir2();
2241
- const dir = join7(home, ".locus", "packages");
2242
- if (!existsSync7(dir)) {
2243
- mkdirSync6(dir, { recursive: true });
2590
+ const dir = join8(home, ".locus", "packages");
2591
+ if (!existsSync8(dir)) {
2592
+ mkdirSync7(dir, { recursive: true });
2244
2593
  }
2245
- const pkgJson = join7(dir, "package.json");
2246
- if (!existsSync7(pkgJson)) {
2247
- writeFileSync5(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
2594
+ const pkgJson = join8(dir, "package.json");
2595
+ if (!existsSync8(pkgJson)) {
2596
+ writeFileSync6(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
2248
2597
  `, "utf-8");
2249
2598
  }
2250
2599
  return dir;
2251
2600
  }
2252
2601
  function getRegistryPath() {
2253
- return join7(getPackagesDir(), "registry.json");
2602
+ return join8(getPackagesDir(), "registry.json");
2254
2603
  }
2255
2604
  function loadRegistry() {
2256
2605
  const registryPath = getRegistryPath();
2257
- if (!existsSync7(registryPath)) {
2606
+ if (!existsSync8(registryPath)) {
2258
2607
  return { packages: {} };
2259
2608
  }
2260
2609
  try {
@@ -2271,7 +2620,7 @@ function loadRegistry() {
2271
2620
  }
2272
2621
  if (pruned) {
2273
2622
  const tmp = `${registryPath}.tmp`;
2274
- writeFileSync5(tmp, `${JSON.stringify(registry, null, 2)}
2623
+ writeFileSync6(tmp, `${JSON.stringify(registry, null, 2)}
2275
2624
  `, "utf-8");
2276
2625
  renameSync(tmp, registryPath);
2277
2626
  }
@@ -2285,15 +2634,15 @@ function loadRegistry() {
2285
2634
  function saveRegistry(registry) {
2286
2635
  const registryPath = getRegistryPath();
2287
2636
  const tmp = `${registryPath}.tmp`;
2288
- writeFileSync5(tmp, `${JSON.stringify(registry, null, 2)}
2637
+ writeFileSync6(tmp, `${JSON.stringify(registry, null, 2)}
2289
2638
  `, "utf-8");
2290
2639
  renameSync(tmp, registryPath);
2291
2640
  }
2292
2641
  function resolvePackageBinary(packageName) {
2293
2642
  const fullName = normalizePackageName(packageName);
2294
2643
  const binName = fullName.includes("/") ? fullName.split("/").pop() : fullName;
2295
- const binPath = join7(getPackagesDir(), "node_modules", ".bin", binName);
2296
- return existsSync7(binPath) ? binPath : null;
2644
+ const binPath = join8(getPackagesDir(), "node_modules", ".bin", binName);
2645
+ return existsSync8(binPath) ? binPath : null;
2297
2646
  }
2298
2647
  function normalizePackageName(input) {
2299
2648
  if (input.startsWith(SCOPED_PREFIX)) {
@@ -2319,7 +2668,7 @@ __export(exports_pkg, {
2319
2668
  listInstalledPackages: () => listInstalledPackages
2320
2669
  });
2321
2670
  import { spawn } from "node:child_process";
2322
- import { existsSync as existsSync8 } from "node:fs";
2671
+ import { existsSync as existsSync9 } from "node:fs";
2323
2672
  function listInstalledPackages() {
2324
2673
  const registry = loadRegistry();
2325
2674
  const entries = Object.values(registry.packages);
@@ -2398,7 +2747,7 @@ async function pkgCommand(args, _flags) {
2398
2747
  return;
2399
2748
  }
2400
2749
  let binaryPath = entry.binaryPath;
2401
- if (!binaryPath || !existsSync8(binaryPath)) {
2750
+ if (!binaryPath || !existsSync9(binaryPath)) {
2402
2751
  const resolved = resolvePackageBinary(packageName);
2403
2752
  if (resolved) {
2404
2753
  binaryPath = resolved;
@@ -2531,8 +2880,8 @@ __export(exports_install, {
2531
2880
  installCommand: () => installCommand
2532
2881
  });
2533
2882
  import { spawnSync as spawnSync2 } from "node:child_process";
2534
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
2535
- import { join as join8 } from "node:path";
2883
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "node:fs";
2884
+ import { join as join9 } from "node:path";
2536
2885
  function parsePackageArg(raw) {
2537
2886
  if (raw.startsWith("@")) {
2538
2887
  const slashIdx = raw.indexOf("/");
@@ -2609,8 +2958,8 @@ ${red2("✗")} Failed to install ${bold2(packageSpec)}.
2609
2958
  process.exit(1);
2610
2959
  return;
2611
2960
  }
2612
- const installedPkgJsonPath = join8(packagesDir, "node_modules", packageName, "package.json");
2613
- if (!existsSync9(installedPkgJsonPath)) {
2961
+ const installedPkgJsonPath = join9(packagesDir, "node_modules", packageName, "package.json");
2962
+ if (!existsSync10(installedPkgJsonPath)) {
2614
2963
  process.stderr.write(`
2615
2964
  ${red2("✗")} Package installed but package.json not found at:
2616
2965
  `);
@@ -2888,16 +3237,16 @@ __export(exports_logs, {
2888
3237
  logsCommand: () => logsCommand
2889
3238
  });
2890
3239
  import {
2891
- existsSync as existsSync10,
3240
+ existsSync as existsSync11,
2892
3241
  readdirSync as readdirSync2,
2893
3242
  readFileSync as readFileSync7,
2894
3243
  statSync as statSync2,
2895
3244
  unlinkSync as unlinkSync2
2896
3245
  } from "node:fs";
2897
- import { join as join9 } from "node:path";
3246
+ import { join as join10 } from "node:path";
2898
3247
  async function logsCommand(cwd, options) {
2899
- const logsDir = join9(cwd, ".locus", "logs");
2900
- if (!existsSync10(logsDir)) {
3248
+ const logsDir = join10(cwd, ".locus", "logs");
3249
+ if (!existsSync11(logsDir)) {
2901
3250
  process.stderr.write(`${dim2("No logs found.")}
2902
3251
  `);
2903
3252
  return;
@@ -2952,8 +3301,8 @@ async function tailLog(logFile, levelFilter) {
2952
3301
  process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
2953
3302
 
2954
3303
  `);
2955
- let lastSize = existsSync10(logFile) ? statSync2(logFile).size : 0;
2956
- if (existsSync10(logFile)) {
3304
+ let lastSize = existsSync11(logFile) ? statSync2(logFile).size : 0;
3305
+ if (existsSync11(logFile)) {
2957
3306
  const content = readFileSync7(logFile, "utf-8");
2958
3307
  const lines = content.trim().split(`
2959
3308
  `).filter(Boolean);
@@ -2972,7 +3321,7 @@ async function tailLog(logFile, levelFilter) {
2972
3321
  }
2973
3322
  return new Promise((resolve) => {
2974
3323
  const interval = setInterval(() => {
2975
- if (!existsSync10(logFile))
3324
+ if (!existsSync11(logFile))
2976
3325
  return;
2977
3326
  const currentSize = statSync2(logFile).size;
2978
3327
  if (currentSize <= lastSize)
@@ -3032,7 +3381,7 @@ function cleanLogs(logsDir) {
3032
3381
  `);
3033
3382
  }
3034
3383
  function getLogFiles(logsDir) {
3035
- return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join9(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3384
+ return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join10(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3036
3385
  }
3037
3386
  function formatEntry(entry) {
3038
3387
  const time = dim2(new Date(entry.ts).toLocaleTimeString());
@@ -3341,10 +3690,10 @@ var init_stream_renderer = __esm(() => {
3341
3690
  });
3342
3691
 
3343
3692
  // src/repl/clipboard.ts
3344
- import { execSync as execSync4 } from "node:child_process";
3345
- import { existsSync as existsSync11, mkdirSync as mkdirSync7 } from "node:fs";
3693
+ import { execSync as execSync5 } from "node:child_process";
3694
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3346
3695
  import { tmpdir } from "node:os";
3347
- import { join as join10 } from "node:path";
3696
+ import { join as join11 } from "node:path";
3348
3697
  function readClipboardImage() {
3349
3698
  if (process.platform === "darwin") {
3350
3699
  return readMacOSClipboardImage();
@@ -3355,14 +3704,14 @@ function readClipboardImage() {
3355
3704
  return null;
3356
3705
  }
3357
3706
  function ensureStableDir() {
3358
- if (!existsSync11(STABLE_DIR)) {
3359
- mkdirSync7(STABLE_DIR, { recursive: true });
3707
+ if (!existsSync12(STABLE_DIR)) {
3708
+ mkdirSync8(STABLE_DIR, { recursive: true });
3360
3709
  }
3361
3710
  }
3362
3711
  function readMacOSClipboardImage() {
3363
3712
  try {
3364
3713
  ensureStableDir();
3365
- const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3714
+ const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
3366
3715
  const script = [
3367
3716
  `set destPath to POSIX file "${destPath}"`,
3368
3717
  "try",
@@ -3380,13 +3729,13 @@ function readMacOSClipboardImage() {
3380
3729
  `return "ok"`
3381
3730
  ].join(`
3382
3731
  `);
3383
- const result = execSync4("osascript", {
3732
+ const result = execSync5("osascript", {
3384
3733
  input: script,
3385
3734
  encoding: "utf-8",
3386
3735
  timeout: 5000,
3387
3736
  stdio: ["pipe", "pipe", "pipe"]
3388
3737
  }).trim();
3389
- if (result === "ok" && existsSync11(destPath)) {
3738
+ if (result === "ok" && existsSync12(destPath)) {
3390
3739
  return destPath;
3391
3740
  }
3392
3741
  } catch {}
@@ -3394,14 +3743,14 @@ function readMacOSClipboardImage() {
3394
3743
  }
3395
3744
  function readLinuxClipboardImage() {
3396
3745
  try {
3397
- const targets = execSync4("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3746
+ const targets = execSync5("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
3398
3747
  if (!targets.includes("image/png")) {
3399
3748
  return null;
3400
3749
  }
3401
3750
  ensureStableDir();
3402
- const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
3403
- execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3404
- if (existsSync11(destPath)) {
3751
+ const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
3752
+ execSync5(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
3753
+ if (existsSync12(destPath)) {
3405
3754
  return destPath;
3406
3755
  }
3407
3756
  } catch {}
@@ -3409,13 +3758,13 @@ function readLinuxClipboardImage() {
3409
3758
  }
3410
3759
  var STABLE_DIR;
3411
3760
  var init_clipboard = __esm(() => {
3412
- STABLE_DIR = join10(tmpdir(), "locus-images");
3761
+ STABLE_DIR = join11(tmpdir(), "locus-images");
3413
3762
  });
3414
3763
 
3415
3764
  // src/repl/image-detect.ts
3416
- import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
3765
+ import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
3417
3766
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
3418
- import { basename, extname, join as join11, resolve } from "node:path";
3767
+ import { basename, extname, join as join12, resolve } from "node:path";
3419
3768
  function detectImages(input) {
3420
3769
  const detected = [];
3421
3770
  const byResolved = new Map;
@@ -3509,15 +3858,15 @@ function collectReferencedAttachments(input, attachments) {
3509
3858
  return dedupeByResolvedPath(selected);
3510
3859
  }
3511
3860
  function relocateImages(images, projectRoot) {
3512
- const targetDir = join11(projectRoot, ".locus", "tmp", "images");
3861
+ const targetDir = join12(projectRoot, ".locus", "tmp", "images");
3513
3862
  for (const img of images) {
3514
3863
  if (!img.exists)
3515
3864
  continue;
3516
3865
  try {
3517
- if (!existsSync12(targetDir)) {
3518
- mkdirSync8(targetDir, { recursive: true });
3866
+ if (!existsSync13(targetDir)) {
3867
+ mkdirSync9(targetDir, { recursive: true });
3519
3868
  }
3520
- const dest = join11(targetDir, basename(img.stablePath));
3869
+ const dest = join12(targetDir, basename(img.stablePath));
3521
3870
  copyFileSync(img.stablePath, dest);
3522
3871
  img.stablePath = dest;
3523
3872
  } catch {}
@@ -3529,7 +3878,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3529
3878
  return;
3530
3879
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
3531
3880
  if (resolved.startsWith("~/")) {
3532
- resolved = join11(homedir3(), resolved.slice(2));
3881
+ resolved = join12(homedir3(), resolved.slice(2));
3533
3882
  }
3534
3883
  resolved = resolve(resolved);
3535
3884
  const existing = byResolved.get(resolved);
@@ -3542,7 +3891,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
3542
3891
  ]);
3543
3892
  return;
3544
3893
  }
3545
- const exists = existsSync12(resolved);
3894
+ const exists = existsSync13(resolved);
3546
3895
  let stablePath = resolved;
3547
3896
  if (exists) {
3548
3897
  stablePath = copyToStable(resolved);
@@ -3596,10 +3945,10 @@ function dedupeByResolvedPath(images) {
3596
3945
  }
3597
3946
  function copyToStable(sourcePath) {
3598
3947
  try {
3599
- if (!existsSync12(STABLE_DIR2)) {
3600
- mkdirSync8(STABLE_DIR2, { recursive: true });
3948
+ if (!existsSync13(STABLE_DIR2)) {
3949
+ mkdirSync9(STABLE_DIR2, { recursive: true });
3601
3950
  }
3602
- const dest = join11(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3951
+ const dest = join12(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
3603
3952
  copyFileSync(sourcePath, dest);
3604
3953
  return dest;
3605
3954
  } catch {
@@ -3619,7 +3968,7 @@ var init_image_detect = __esm(() => {
3619
3968
  ".tif",
3620
3969
  ".tiff"
3621
3970
  ]);
3622
- STABLE_DIR2 = join11(tmpdir2(), "locus-images");
3971
+ STABLE_DIR2 = join12(tmpdir2(), "locus-images");
3623
3972
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
3624
3973
  });
3625
3974
 
@@ -3628,13 +3977,25 @@ class InputHandler {
3628
3977
  prompt;
3629
3978
  getHistory;
3630
3979
  onTab;
3980
+ activeInsertText = null;
3981
+ activeRender = null;
3631
3982
  locked = false;
3632
3983
  lastInterruptTime = 0;
3984
+ pendingInsert = null;
3633
3985
  constructor(options) {
3634
3986
  this.prompt = options.prompt;
3635
3987
  this.getHistory = options.getHistory ?? (() => []);
3636
3988
  this.onTab = options.onTab;
3637
3989
  }
3990
+ insertTextFromExternal(text) {
3991
+ if (this.activeInsertText && this.activeRender) {
3992
+ this.activeInsertText(text);
3993
+ this.activeRender();
3994
+ }
3995
+ }
3996
+ setInitialBuffer(text) {
3997
+ this.pendingInsert = text;
3998
+ }
3638
3999
  setPrompt(prompt) {
3639
4000
  this.prompt = prompt;
3640
4001
  }
@@ -3671,6 +4032,8 @@ class InputHandler {
3671
4032
  if (resolved)
3672
4033
  return;
3673
4034
  resolved = true;
4035
+ this.activeInsertText = null;
4036
+ this.activeRender = null;
3674
4037
  stdin.removeListener("data", onData);
3675
4038
  if (stdin.isTTY) {
3676
4039
  out.write(DISABLE_BRACKETED_PASTE + DISABLE_KITTY_KEYBOARD);
@@ -4138,6 +4501,12 @@ ${dim2("Press Ctrl+C again to exit")}\r
4138
4501
  if (stdin.isTTY) {
4139
4502
  out.write(ENABLE_BRACKETED_PASTE + ENABLE_KITTY_KEYBOARD);
4140
4503
  }
4504
+ this.activeInsertText = insertText;
4505
+ this.activeRender = render;
4506
+ if (this.pendingInsert) {
4507
+ insertText(this.pendingInsert);
4508
+ this.pendingInsert = null;
4509
+ }
4141
4510
  render();
4142
4511
  });
4143
4512
  }
@@ -4401,7 +4770,7 @@ __export(exports_claude, {
4401
4770
  buildClaudeArgs: () => buildClaudeArgs,
4402
4771
  ClaudeRunner: () => ClaudeRunner
4403
4772
  });
4404
- import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
4773
+ import { execSync as execSync6, spawn as spawn2 } from "node:child_process";
4405
4774
  function buildClaudeArgs(options) {
4406
4775
  const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
4407
4776
  if (options.model) {
@@ -4419,7 +4788,7 @@ class ClaudeRunner {
4419
4788
  aborted = false;
4420
4789
  async isAvailable() {
4421
4790
  try {
4422
- execSync5("claude --version", {
4791
+ execSync6("claude --version", {
4423
4792
  encoding: "utf-8",
4424
4793
  stdio: ["pipe", "pipe", "pipe"]
4425
4794
  });
@@ -4430,7 +4799,7 @@ class ClaudeRunner {
4430
4799
  }
4431
4800
  async getVersion() {
4432
4801
  try {
4433
- const output = execSync5("claude --version", {
4802
+ const output = execSync6("claude --version", {
4434
4803
  encoding: "utf-8",
4435
4804
  stdio: ["pipe", "pipe", "pipe"]
4436
4805
  }).trim();
@@ -4597,8 +4966,8 @@ var init_claude = __esm(() => {
4597
4966
  import { exec } from "node:child_process";
4598
4967
  import {
4599
4968
  cpSync,
4600
- existsSync as existsSync13,
4601
- mkdirSync as mkdirSync9,
4969
+ existsSync as existsSync14,
4970
+ mkdirSync as mkdirSync10,
4602
4971
  mkdtempSync,
4603
4972
  readdirSync as readdirSync3,
4604
4973
  readFileSync as readFileSync8,
@@ -4606,10 +4975,10 @@ import {
4606
4975
  statSync as statSync3
4607
4976
  } from "node:fs";
4608
4977
  import { tmpdir as tmpdir3 } from "node:os";
4609
- import { dirname as dirname3, join as join12, relative } from "node:path";
4978
+ import { dirname as dirname3, join as join13, relative } from "node:path";
4610
4979
  import { promisify } from "node:util";
4611
4980
  function parseIgnoreFile(filePath) {
4612
- if (!existsSync13(filePath))
4981
+ if (!existsSync14(filePath))
4613
4982
  return [];
4614
4983
  const content = readFileSync8(filePath, "utf-8");
4615
4984
  const rules = [];
@@ -4679,7 +5048,7 @@ function findIgnoredPaths(projectRoot, rules) {
4679
5048
  for (const name of entries) {
4680
5049
  if (SKIP_DIRS.has(name))
4681
5050
  continue;
4682
- const fullPath = join12(dir, name);
5051
+ const fullPath = join13(dir, name);
4683
5052
  let stat = null;
4684
5053
  try {
4685
5054
  stat = statSync3(fullPath);
@@ -4713,7 +5082,7 @@ function findIgnoredPaths(projectRoot, rules) {
4713
5082
  }
4714
5083
  function backupIgnoredFiles(projectRoot) {
4715
5084
  const log = getLogger();
4716
- const ignorePath = join12(projectRoot, ".sandboxignore");
5085
+ const ignorePath = join13(projectRoot, ".sandboxignore");
4717
5086
  const rules = parseIgnoreFile(ignorePath);
4718
5087
  if (rules.length === 0)
4719
5088
  return NOOP_BACKUP;
@@ -4722,7 +5091,7 @@ function backupIgnoredFiles(projectRoot) {
4722
5091
  return NOOP_BACKUP;
4723
5092
  let backupDir;
4724
5093
  try {
4725
- backupDir = mkdtempSync(join12(tmpdir3(), "locus-sandbox-backup-"));
5094
+ backupDir = mkdtempSync(join13(tmpdir3(), "locus-sandbox-backup-"));
4726
5095
  } catch (err) {
4727
5096
  log.debug("Failed to create sandbox backup dir", {
4728
5097
  error: err instanceof Error ? err.message : String(err)
@@ -4732,9 +5101,9 @@ function backupIgnoredFiles(projectRoot) {
4732
5101
  const backed = [];
4733
5102
  for (const src of paths) {
4734
5103
  const rel = relative(projectRoot, src);
4735
- const dest = join12(backupDir, rel);
5104
+ const dest = join13(backupDir, rel);
4736
5105
  try {
4737
- mkdirSync9(dirname3(dest), { recursive: true });
5106
+ mkdirSync10(dirname3(dest), { recursive: true });
4738
5107
  cpSync(src, dest, { recursive: true, preserveTimestamps: true });
4739
5108
  backed.push({ src, dest });
4740
5109
  } catch (err) {
@@ -4756,7 +5125,7 @@ function backupIgnoredFiles(projectRoot) {
4756
5125
  restore() {
4757
5126
  for (const { src, dest } of backed) {
4758
5127
  try {
4759
- mkdirSync9(dirname3(src), { recursive: true });
5128
+ mkdirSync10(dirname3(src), { recursive: true });
4760
5129
  cpSync(dest, src, { recursive: true, preserveTimestamps: true });
4761
5130
  } catch (err) {
4762
5131
  log.debug("Failed to restore ignored file (potential data loss)", {
@@ -4774,7 +5143,7 @@ function backupIgnoredFiles(projectRoot) {
4774
5143
  }
4775
5144
  async function enforceSandboxIgnore(sandboxName, projectRoot) {
4776
5145
  const log = getLogger();
4777
- const ignorePath = join12(projectRoot, ".sandboxignore");
5146
+ const ignorePath = join13(projectRoot, ".sandboxignore");
4778
5147
  const rules = parseIgnoreFile(ignorePath);
4779
5148
  if (rules.length === 0)
4780
5149
  return;
@@ -5019,7 +5388,7 @@ var init_claude_sandbox = __esm(() => {
5019
5388
  });
5020
5389
 
5021
5390
  // src/ai/codex.ts
5022
- import { execSync as execSync6, spawn as spawn4 } from "node:child_process";
5391
+ import { execSync as execSync7, spawn as spawn4 } from "node:child_process";
5023
5392
  function buildCodexArgs(model) {
5024
5393
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
5025
5394
  if (model) {
@@ -5035,7 +5404,7 @@ class CodexRunner {
5035
5404
  aborted = false;
5036
5405
  async isAvailable() {
5037
5406
  try {
5038
- execSync6("codex --version", {
5407
+ execSync7("codex --version", {
5039
5408
  encoding: "utf-8",
5040
5409
  stdio: ["pipe", "pipe", "pipe"]
5041
5410
  });
@@ -5046,7 +5415,7 @@ class CodexRunner {
5046
5415
  }
5047
5416
  async getVersion() {
5048
5417
  try {
5049
- const output = execSync6("codex --version", {
5418
+ const output = execSync7("codex --version", {
5050
5419
  encoding: "utf-8",
5051
5420
  stdio: ["pipe", "pipe", "pipe"]
5052
5421
  }).trim();
@@ -7074,9 +7443,9 @@ var init_sprint = __esm(() => {
7074
7443
  });
7075
7444
 
7076
7445
  // src/core/prompt-builder.ts
7077
- import { execSync as execSync7 } from "node:child_process";
7078
- import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
7079
- import { join as join13 } from "node:path";
7446
+ import { execSync as execSync8 } from "node:child_process";
7447
+ import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
7448
+ import { join as join14 } from "node:path";
7080
7449
  function buildExecutionPrompt(ctx) {
7081
7450
  const sections = [];
7082
7451
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -7106,13 +7475,13 @@ function buildFeedbackPrompt(ctx) {
7106
7475
  }
7107
7476
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
7108
7477
  const sections = [];
7109
- const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
7478
+ const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
7110
7479
  if (locusmd) {
7111
7480
  sections.push(`<project-instructions>
7112
7481
  ${locusmd}
7113
7482
  </project-instructions>`);
7114
7483
  }
7115
- const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
7484
+ const learnings = readFileSafe(join14(projectRoot, ".locus", "LEARNINGS.md"));
7116
7485
  if (learnings) {
7117
7486
  sections.push(`<past-learnings>
7118
7487
  ${learnings}
@@ -7139,24 +7508,24 @@ ${userMessage}
7139
7508
  }
7140
7509
  function buildSystemContext(projectRoot) {
7141
7510
  const parts = [];
7142
- const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
7511
+ const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
7143
7512
  if (locusmd) {
7144
7513
  parts.push(`<project-instructions>
7145
7514
  ${locusmd}
7146
7515
  </project-instructions>`);
7147
7516
  }
7148
- const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
7517
+ const learnings = readFileSafe(join14(projectRoot, ".locus", "LEARNINGS.md"));
7149
7518
  if (learnings) {
7150
7519
  parts.push(`<past-learnings>
7151
7520
  ${learnings}
7152
7521
  </past-learnings>`);
7153
7522
  }
7154
- const discussionsDir = join13(projectRoot, ".locus", "discussions");
7155
- if (existsSync14(discussionsDir)) {
7523
+ const discussionsDir = join14(projectRoot, ".locus", "discussions");
7524
+ if (existsSync15(discussionsDir)) {
7156
7525
  try {
7157
7526
  const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
7158
7527
  for (const file of files) {
7159
- const content = readFileSafe(join13(discussionsDir, file));
7528
+ const content = readFileSafe(join14(discussionsDir, file));
7160
7529
  if (content) {
7161
7530
  const name = file.replace(".md", "");
7162
7531
  parts.push(`<discussion name="${name}">
@@ -7223,7 +7592,7 @@ ${parts.join(`
7223
7592
  function buildRepoContext(projectRoot) {
7224
7593
  const parts = [];
7225
7594
  try {
7226
- const tree = execSync7("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
7595
+ const tree = execSync8("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
7227
7596
  if (tree) {
7228
7597
  parts.push(`<file-tree>
7229
7598
  \`\`\`
@@ -7233,7 +7602,7 @@ ${tree}
7233
7602
  }
7234
7603
  } catch {}
7235
7604
  try {
7236
- const gitLog = execSync7("git log --oneline -10", {
7605
+ const gitLog = execSync8("git log --oneline -10", {
7237
7606
  cwd: projectRoot,
7238
7607
  encoding: "utf-8",
7239
7608
  stdio: ["pipe", "pipe", "pipe"]
@@ -7247,7 +7616,7 @@ ${gitLog}
7247
7616
  }
7248
7617
  } catch {}
7249
7618
  try {
7250
- const branch = execSync7("git rev-parse --abbrev-ref HEAD", {
7619
+ const branch = execSync8("git rev-parse --abbrev-ref HEAD", {
7251
7620
  cwd: projectRoot,
7252
7621
  encoding: "utf-8",
7253
7622
  stdio: ["pipe", "pipe", "pipe"]
@@ -7309,7 +7678,7 @@ function buildFeedbackInstructions() {
7309
7678
  }
7310
7679
  function readFileSafe(path) {
7311
7680
  try {
7312
- if (!existsSync14(path))
7681
+ if (!existsSync15(path))
7313
7682
  return null;
7314
7683
  return readFileSync9(path, "utf-8");
7315
7684
  } catch {
@@ -7503,18 +7872,18 @@ var init_diff_renderer = __esm(() => {
7503
7872
  });
7504
7873
 
7505
7874
  // src/repl/commands.ts
7506
- import { execSync as execSync8 } from "node:child_process";
7875
+ import { execSync as execSync9 } from "node:child_process";
7507
7876
  function getSlashCommands() {
7508
7877
  return [
7509
7878
  {
7510
7879
  name: "/help",
7511
- aliases: ["/h", "/?"],
7880
+ aliases: ["/h"],
7512
7881
  description: "Show available commands",
7513
7882
  handler: cmdHelp
7514
7883
  },
7515
7884
  {
7516
7885
  name: "/clear",
7517
- aliases: ["/cls"],
7886
+ aliases: [],
7518
7887
  description: "Clear screen",
7519
7888
  handler: cmdClear
7520
7889
  },
@@ -7550,7 +7919,7 @@ function getSlashCommands() {
7550
7919
  },
7551
7920
  {
7552
7921
  name: "/undo",
7553
- aliases: ["/u"],
7922
+ aliases: [],
7554
7923
  description: "Undo last AI change",
7555
7924
  handler: cmdUndo
7556
7925
  },
@@ -7562,10 +7931,16 @@ function getSlashCommands() {
7562
7931
  },
7563
7932
  {
7564
7933
  name: "/verbose",
7565
- aliases: ["/v"],
7934
+ aliases: [],
7566
7935
  description: "Toggle verbose mode (show agent stderr streams)",
7567
7936
  handler: cmdVerbose
7568
7937
  },
7938
+ {
7939
+ name: "/voice",
7940
+ aliases: ["/v"],
7941
+ description: "Start voice recording (press Enter to stop)",
7942
+ handler: cmdVoice
7943
+ },
7569
7944
  {
7570
7945
  name: "/exit",
7571
7946
  aliases: ["/quit", "/q"],
@@ -7695,7 +8070,7 @@ function cmdModel(args, ctx) {
7695
8070
  }
7696
8071
  function cmdDiff(_args, ctx) {
7697
8072
  try {
7698
- const diff = execSync8("git diff", {
8073
+ const diff = execSync9("git diff", {
7699
8074
  cwd: ctx.projectRoot,
7700
8075
  encoding: "utf-8",
7701
8076
  stdio: ["pipe", "pipe", "pipe"]
@@ -7731,7 +8106,7 @@ function cmdDiff(_args, ctx) {
7731
8106
  }
7732
8107
  function cmdUndo(_args, ctx) {
7733
8108
  try {
7734
- const status = execSync8("git status --porcelain", {
8109
+ const status = execSync9("git status --porcelain", {
7735
8110
  cwd: ctx.projectRoot,
7736
8111
  encoding: "utf-8",
7737
8112
  stdio: ["pipe", "pipe", "pipe"]
@@ -7741,7 +8116,7 @@ function cmdUndo(_args, ctx) {
7741
8116
  `);
7742
8117
  return;
7743
8118
  }
7744
- execSync8("git checkout .", {
8119
+ execSync9("git checkout .", {
7745
8120
  cwd: ctx.projectRoot,
7746
8121
  encoding: "utf-8",
7747
8122
  stdio: ["pipe", "pipe", "pipe"]
@@ -7764,6 +8139,14 @@ function cmdVerbose(_args, ctx) {
7764
8139
  process.stderr.write(`${isOn ? green("✓") : dim2("○")} Verbose mode ${isOn ? bold2("on") : "off"} — agent streams ${isOn ? "visible" : "hidden"}.
7765
8140
  `);
7766
8141
  }
8142
+ function cmdVoice(_args, ctx) {
8143
+ if (ctx.onVoiceToggle) {
8144
+ ctx.onVoiceToggle();
8145
+ } else {
8146
+ process.stderr.write(`${red2("✗")} Voice input not available in this session.
8147
+ `);
8148
+ }
8149
+ }
7767
8150
  function cmdExit(_args, ctx) {
7768
8151
  ctx.onExit();
7769
8152
  }
@@ -7775,7 +8158,7 @@ var init_commands = __esm(() => {
7775
8158
 
7776
8159
  // src/repl/completions.ts
7777
8160
  import { readdirSync as readdirSync5 } from "node:fs";
7778
- import { basename as basename2, dirname as dirname4, join as join14 } from "node:path";
8161
+ import { basename as basename2, dirname as dirname4, join as join15 } from "node:path";
7779
8162
 
7780
8163
  class SlashCommandCompletion {
7781
8164
  commands;
@@ -7830,7 +8213,7 @@ class FilePathCompletion {
7830
8213
  }
7831
8214
  findMatches(partial) {
7832
8215
  try {
7833
- const dir = partial.includes("/") ? join14(this.projectRoot, dirname4(partial)) : this.projectRoot;
8216
+ const dir = partial.includes("/") ? join15(this.projectRoot, dirname4(partial)) : this.projectRoot;
7834
8217
  const prefix = basename2(partial);
7835
8218
  const entries = readdirSync5(dir, { withFileTypes: true });
7836
8219
  return entries.filter((e) => {
@@ -7866,14 +8249,14 @@ class CombinedCompletion {
7866
8249
  var init_completions = () => {};
7867
8250
 
7868
8251
  // src/repl/input-history.ts
7869
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
7870
- import { dirname as dirname5, join as join15 } from "node:path";
8252
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
8253
+ import { dirname as dirname5, join as join16 } from "node:path";
7871
8254
 
7872
8255
  class InputHistory {
7873
8256
  entries = [];
7874
8257
  filePath;
7875
8258
  constructor(projectRoot) {
7876
- this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
8259
+ this.filePath = join16(projectRoot, ".locus", "sessions", ".input-history");
7877
8260
  this.load();
7878
8261
  }
7879
8262
  add(text) {
@@ -7912,7 +8295,7 @@ class InputHistory {
7912
8295
  }
7913
8296
  load() {
7914
8297
  try {
7915
- if (!existsSync15(this.filePath))
8298
+ if (!existsSync16(this.filePath))
7916
8299
  return;
7917
8300
  const content = readFileSync10(this.filePath, "utf-8");
7918
8301
  this.entries = content.split(`
@@ -7922,12 +8305,12 @@ class InputHistory {
7922
8305
  save() {
7923
8306
  try {
7924
8307
  const dir = dirname5(this.filePath);
7925
- if (!existsSync15(dir)) {
7926
- mkdirSync10(dir, { recursive: true });
8308
+ if (!existsSync16(dir)) {
8309
+ mkdirSync11(dir, { recursive: true });
7927
8310
  }
7928
8311
  const content = this.entries.map((e) => this.escape(e)).join(`
7929
8312
  `);
7930
- writeFileSync6(this.filePath, content, "utf-8");
8313
+ writeFileSync7(this.filePath, content, "utf-8");
7931
8314
  } catch {}
7932
8315
  }
7933
8316
  escape(text) {
@@ -7953,21 +8336,21 @@ var init_model_config = __esm(() => {
7953
8336
 
7954
8337
  // src/repl/session-manager.ts
7955
8338
  import {
7956
- existsSync as existsSync16,
7957
- mkdirSync as mkdirSync11,
8339
+ existsSync as existsSync17,
8340
+ mkdirSync as mkdirSync12,
7958
8341
  readdirSync as readdirSync6,
7959
8342
  readFileSync as readFileSync11,
7960
8343
  unlinkSync as unlinkSync3,
7961
- writeFileSync as writeFileSync7
8344
+ writeFileSync as writeFileSync8
7962
8345
  } from "node:fs";
7963
- import { basename as basename3, join as join16 } from "node:path";
8346
+ import { basename as basename3, join as join17 } from "node:path";
7964
8347
 
7965
8348
  class SessionManager {
7966
8349
  sessionsDir;
7967
8350
  constructor(projectRoot) {
7968
- this.sessionsDir = join16(projectRoot, ".locus", "sessions");
7969
- if (!existsSync16(this.sessionsDir)) {
7970
- mkdirSync11(this.sessionsDir, { recursive: true });
8351
+ this.sessionsDir = join17(projectRoot, ".locus", "sessions");
8352
+ if (!existsSync17(this.sessionsDir)) {
8353
+ mkdirSync12(this.sessionsDir, { recursive: true });
7971
8354
  }
7972
8355
  }
7973
8356
  create(options) {
@@ -7992,12 +8375,12 @@ class SessionManager {
7992
8375
  }
7993
8376
  isPersisted(sessionOrId) {
7994
8377
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
7995
- return existsSync16(this.getSessionPath(sessionId));
8378
+ return existsSync17(this.getSessionPath(sessionId));
7996
8379
  }
7997
8380
  load(idOrPrefix) {
7998
8381
  const files = this.listSessionFiles();
7999
8382
  const exactPath = this.getSessionPath(idOrPrefix);
8000
- if (existsSync16(exactPath)) {
8383
+ if (existsSync17(exactPath)) {
8001
8384
  try {
8002
8385
  return JSON.parse(readFileSync11(exactPath, "utf-8"));
8003
8386
  } catch {
@@ -8020,7 +8403,7 @@ class SessionManager {
8020
8403
  save(session) {
8021
8404
  session.updated = new Date().toISOString();
8022
8405
  const path = this.getSessionPath(session.id);
8023
- writeFileSync7(path, `${JSON.stringify(session, null, 2)}
8406
+ writeFileSync8(path, `${JSON.stringify(session, null, 2)}
8024
8407
  `, "utf-8");
8025
8408
  }
8026
8409
  addMessage(session, message) {
@@ -8047,7 +8430,7 @@ class SessionManager {
8047
8430
  }
8048
8431
  delete(sessionId) {
8049
8432
  const path = this.getSessionPath(sessionId);
8050
- if (existsSync16(path)) {
8433
+ if (existsSync17(path)) {
8051
8434
  unlinkSync3(path);
8052
8435
  return true;
8053
8436
  }
@@ -8077,7 +8460,7 @@ class SessionManager {
8077
8460
  const remaining = withStats.length - pruned;
8078
8461
  if (remaining > MAX_SESSIONS) {
8079
8462
  const toRemove = remaining - MAX_SESSIONS;
8080
- const alive = withStats.filter((e) => existsSync16(e.path));
8463
+ const alive = withStats.filter((e) => existsSync17(e.path));
8081
8464
  for (let i = 0;i < toRemove && i < alive.length; i++) {
8082
8465
  try {
8083
8466
  unlinkSync3(alive[i].path);
@@ -8092,7 +8475,7 @@ class SessionManager {
8092
8475
  }
8093
8476
  listSessionFiles() {
8094
8477
  try {
8095
- return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
8478
+ return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join17(this.sessionsDir, f));
8096
8479
  } catch {
8097
8480
  return [];
8098
8481
  }
@@ -8101,7 +8484,7 @@ class SessionManager {
8101
8484
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8102
8485
  }
8103
8486
  getSessionPath(sessionId) {
8104
- return join16(this.sessionsDir, `${sessionId}.json`);
8487
+ return join17(this.sessionsDir, `${sessionId}.json`);
8105
8488
  }
8106
8489
  }
8107
8490
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -8110,8 +8493,544 @@ var init_session_manager = __esm(() => {
8110
8493
  SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
8111
8494
  });
8112
8495
 
8496
+ // src/repl/voice.ts
8497
+ import { execSync as execSync10, spawn as spawn6 } from "node:child_process";
8498
+ import { existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync4 } from "node:fs";
8499
+ import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
8500
+ import { join as join18 } from "node:path";
8501
+ function getWhisperModelPath() {
8502
+ return join18(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
8503
+ }
8504
+ function commandExists(cmd) {
8505
+ try {
8506
+ const which = platform() === "win32" ? "where" : "which";
8507
+ execSync10(`${which} ${cmd}`, { stdio: "pipe" });
8508
+ return true;
8509
+ } catch {
8510
+ return false;
8511
+ }
8512
+ }
8513
+ function findWhisperBinary() {
8514
+ const candidates = ["whisper-cli", "whisper-cpp", "whisper", "main"];
8515
+ for (const name of candidates) {
8516
+ if (commandExists(name))
8517
+ return name;
8518
+ }
8519
+ for (const name of candidates) {
8520
+ const fullPath = join18(LOCUS_BIN_DIR, name);
8521
+ if (existsSync18(fullPath))
8522
+ return fullPath;
8523
+ }
8524
+ if (platform() === "darwin") {
8525
+ const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8526
+ for (const dir of brewDirs) {
8527
+ for (const name of candidates) {
8528
+ const fullPath = join18(dir, name);
8529
+ if (existsSync18(fullPath))
8530
+ return fullPath;
8531
+ }
8532
+ }
8533
+ }
8534
+ return null;
8535
+ }
8536
+ function findSoxRecBinary() {
8537
+ if (commandExists("rec"))
8538
+ return "rec";
8539
+ if (commandExists("sox"))
8540
+ return "sox";
8541
+ if (platform() === "darwin") {
8542
+ const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8543
+ for (const dir of brewDirs) {
8544
+ const recPath = join18(dir, "rec");
8545
+ if (existsSync18(recPath))
8546
+ return recPath;
8547
+ const soxPath = join18(dir, "sox");
8548
+ if (existsSync18(soxPath))
8549
+ return soxPath;
8550
+ }
8551
+ }
8552
+ return null;
8553
+ }
8554
+ function checkDependencies() {
8555
+ const soxBinary = findSoxRecBinary();
8556
+ const whisperBinary = findWhisperBinary();
8557
+ const modelDownloaded = existsSync18(getWhisperModelPath());
8558
+ return {
8559
+ sox: soxBinary !== null,
8560
+ whisper: whisperBinary !== null,
8561
+ whisperBinary,
8562
+ soxBinary,
8563
+ modelDownloaded
8564
+ };
8565
+ }
8566
+ function printDependencyHelp(deps) {
8567
+ const out = process.stderr;
8568
+ out.write(`
8569
+ ${bold2("Voice Input Setup")}
8570
+
8571
+ `);
8572
+ if (!deps.sox) {
8573
+ out.write(` ${red2("✗")} ${bold2("sox")} — audio recording
8574
+ `);
8575
+ if (platform() === "darwin") {
8576
+ out.write(` Install: ${cyan2("brew install sox")}
8577
+ `);
8578
+ } else {
8579
+ out.write(` Install: ${cyan2("sudo apt install sox")} or ${cyan2("sudo dnf install sox")}
8580
+ `);
8581
+ }
8582
+ } else {
8583
+ out.write(` ${green("✓")} ${bold2("sox")} — audio recording ${dim2(`(${deps.soxBinary})`)}
8584
+ `);
8585
+ }
8586
+ if (!deps.whisper) {
8587
+ out.write(` ${red2("✗")} ${bold2("whisper.cpp")} — speech-to-text
8588
+ `);
8589
+ if (platform() === "darwin") {
8590
+ out.write(` Install: ${cyan2("brew install whisper-cpp")}
8591
+ `);
8592
+ } else {
8593
+ out.write(` Install: Build from source — ${cyan2("https://github.com/ggerganov/whisper.cpp")}
8594
+ `);
8595
+ }
8596
+ } else {
8597
+ out.write(` ${green("✓")} ${bold2("whisper.cpp")} — speech-to-text ${dim2(`(${deps.whisperBinary})`)}
8598
+ `);
8599
+ }
8600
+ if (deps.whisper && !deps.modelDownloaded) {
8601
+ out.write(` ${yellow2("!")} Model ${bold2(WHISPER_MODEL)} not downloaded yet — will download on first use (~150MB)
8602
+ `);
8603
+ out.write(` Path: ${dim2(getWhisperModelPath())}
8604
+ `);
8605
+ } else if (deps.whisper && deps.modelDownloaded) {
8606
+ out.write(` ${green("✓")} Model ${bold2(WHISPER_MODEL)} ${dim2("ready")}
8607
+ `);
8608
+ }
8609
+ out.write(`
8610
+ `);
8611
+ if (!deps.sox || !deps.whisper) {
8612
+ out.write(` ${dim2("Install the missing dependencies above, then try again.")}
8613
+
8614
+ `);
8615
+ }
8616
+ }
8617
+ function detectPackageManager() {
8618
+ if (platform() === "darwin") {
8619
+ return commandExists("brew") ? "brew" : null;
8620
+ }
8621
+ if (commandExists("apt-get"))
8622
+ return "apt";
8623
+ if (commandExists("dnf"))
8624
+ return "dnf";
8625
+ if (commandExists("pacman"))
8626
+ return "pacman";
8627
+ return null;
8628
+ }
8629
+ function installSox(pm) {
8630
+ try {
8631
+ switch (pm) {
8632
+ case "brew":
8633
+ execSync10("brew install sox", { stdio: "inherit", timeout: 300000 });
8634
+ break;
8635
+ case "apt":
8636
+ execSync10("sudo apt-get install -y sox", {
8637
+ stdio: "inherit",
8638
+ timeout: 300000
8639
+ });
8640
+ break;
8641
+ case "dnf":
8642
+ execSync10("sudo dnf install -y sox", {
8643
+ stdio: "inherit",
8644
+ timeout: 300000
8645
+ });
8646
+ break;
8647
+ case "pacman":
8648
+ execSync10("sudo pacman -S --noconfirm sox", {
8649
+ stdio: "inherit",
8650
+ timeout: 300000
8651
+ });
8652
+ break;
8653
+ }
8654
+ return true;
8655
+ } catch {
8656
+ return false;
8657
+ }
8658
+ }
8659
+ function installWhisperCpp(pm) {
8660
+ if (pm === "brew") {
8661
+ try {
8662
+ execSync10("brew install whisper-cpp", {
8663
+ stdio: "inherit",
8664
+ timeout: 300000
8665
+ });
8666
+ return true;
8667
+ } catch {
8668
+ return false;
8669
+ }
8670
+ }
8671
+ return buildWhisperFromSource(pm);
8672
+ }
8673
+ function ensureBuildDeps(pm) {
8674
+ const hasCmake = commandExists("cmake");
8675
+ const hasCxx = commandExists("g++") || commandExists("c++");
8676
+ const hasGit = commandExists("git");
8677
+ if (hasCmake && hasCxx && hasGit)
8678
+ return true;
8679
+ process.stderr.write(` ${dim2("Installing build tools...")}
8680
+ `);
8681
+ try {
8682
+ switch (pm) {
8683
+ case "apt":
8684
+ execSync10("sudo apt-get install -y cmake g++ make git", {
8685
+ stdio: "inherit",
8686
+ timeout: 300000
8687
+ });
8688
+ break;
8689
+ case "dnf":
8690
+ execSync10("sudo dnf install -y cmake gcc-c++ make git", {
8691
+ stdio: "inherit",
8692
+ timeout: 300000
8693
+ });
8694
+ break;
8695
+ case "pacman":
8696
+ execSync10("sudo pacman -S --noconfirm cmake gcc make git", {
8697
+ stdio: "inherit",
8698
+ timeout: 300000
8699
+ });
8700
+ break;
8701
+ default:
8702
+ return false;
8703
+ }
8704
+ return true;
8705
+ } catch {
8706
+ return false;
8707
+ }
8708
+ }
8709
+ function buildWhisperFromSource(pm) {
8710
+ const out = process.stderr;
8711
+ const buildDir = join18(tmpdir4(), `locus-whisper-build-${process.pid}`);
8712
+ if (!ensureBuildDeps(pm)) {
8713
+ out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
8714
+ `);
8715
+ return false;
8716
+ }
8717
+ try {
8718
+ mkdirSync13(buildDir, { recursive: true });
8719
+ mkdirSync13(LOCUS_BIN_DIR, { recursive: true });
8720
+ out.write(` ${dim2("Cloning whisper.cpp...")}
8721
+ `);
8722
+ execSync10(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8723
+ const srcDir = join18(buildDir, "whisper.cpp");
8724
+ const numCpus = cpus().length || 2;
8725
+ out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
8726
+ `);
8727
+ execSync10("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8728
+ cwd: srcDir,
8729
+ stdio: ["pipe", "pipe", "pipe"],
8730
+ timeout: 120000
8731
+ });
8732
+ execSync10(`cmake --build build --config Release -j${numCpus}`, {
8733
+ cwd: srcDir,
8734
+ stdio: ["pipe", "pipe", "pipe"],
8735
+ timeout: 600000
8736
+ });
8737
+ const destPath = join18(LOCUS_BIN_DIR, "whisper-cli");
8738
+ const binaryCandidates = [
8739
+ join18(srcDir, "build", "bin", "whisper-cli"),
8740
+ join18(srcDir, "build", "bin", "main")
8741
+ ];
8742
+ for (const candidate of binaryCandidates) {
8743
+ if (existsSync18(candidate)) {
8744
+ execSync10(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8745
+ stdio: "pipe"
8746
+ });
8747
+ return true;
8748
+ }
8749
+ }
8750
+ out.write(` ${red2("✗")} Build completed but whisper-cli binary not found.
8751
+ `);
8752
+ return false;
8753
+ } catch (e) {
8754
+ out.write(` ${red2("✗")} Build failed: ${e instanceof Error ? e.message : String(e)}
8755
+ `);
8756
+ return false;
8757
+ } finally {
8758
+ try {
8759
+ execSync10(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8760
+ } catch {}
8761
+ }
8762
+ }
8763
+ function autoInstallDependencies(deps) {
8764
+ if (platform() === "win32") {
8765
+ process.stderr.write(`
8766
+ ${red2("✗")} Voice input is not supported on Windows.
8767
+
8768
+ `);
8769
+ return false;
8770
+ }
8771
+ const pm = detectPackageManager();
8772
+ if (!pm) {
8773
+ process.stderr.write(`
8774
+ ${red2("✗")} No supported package manager found.
8775
+ `);
8776
+ if (platform() === "darwin") {
8777
+ process.stderr.write(` Install Homebrew first: ${cyan2("https://brew.sh")}
8778
+ `);
8779
+ }
8780
+ process.stderr.write(`
8781
+ `);
8782
+ return false;
8783
+ }
8784
+ const out = process.stderr;
8785
+ out.write(`
8786
+ ${bold2("Installing voice dependencies...")}
8787
+
8788
+ `);
8789
+ if (!deps.sox) {
8790
+ out.write(` ${dim2("Installing")} ${bold2("sox")} ${dim2("(audio recording)...")}
8791
+ `);
8792
+ if (!installSox(pm)) {
8793
+ out.write(` ${red2("✗")} Failed to install sox.
8794
+
8795
+ `);
8796
+ return false;
8797
+ }
8798
+ out.write(` ${green("✓")} sox installed
8799
+
8800
+ `);
8801
+ }
8802
+ if (!deps.whisper) {
8803
+ out.write(` ${dim2("Installing")} ${bold2("whisper.cpp")} ${dim2("(speech-to-text)...")}
8804
+ `);
8805
+ if (!installWhisperCpp(pm)) {
8806
+ out.write(` ${red2("✗")} Failed to install whisper.cpp.
8807
+
8808
+ `);
8809
+ return false;
8810
+ }
8811
+ out.write(` ${green("✓")} whisper.cpp installed
8812
+
8813
+ `);
8814
+ }
8815
+ out.write(`${green("✓")} Voice dependencies ready.
8816
+
8817
+ `);
8818
+ return true;
8819
+ }
8820
+ function downloadModel() {
8821
+ const modelPath = getWhisperModelPath();
8822
+ if (existsSync18(modelPath))
8823
+ return true;
8824
+ mkdirSync13(WHISPER_MODELS_DIR, { recursive: true });
8825
+ const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
8826
+ process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
8827
+ `);
8828
+ try {
8829
+ if (commandExists("curl")) {
8830
+ execSync10(`curl -L -o "${modelPath}" "${url}"`, {
8831
+ stdio: ["pipe", "pipe", "pipe"],
8832
+ timeout: 300000
8833
+ });
8834
+ } else if (commandExists("wget")) {
8835
+ execSync10(`wget -O "${modelPath}" "${url}"`, {
8836
+ stdio: ["pipe", "pipe", "pipe"],
8837
+ timeout: 300000
8838
+ });
8839
+ } else {
8840
+ process.stderr.write(`${red2("✗")} Neither curl nor wget found. Download the model manually:
8841
+ `);
8842
+ process.stderr.write(` ${cyan2(url)}
8843
+ `);
8844
+ process.stderr.write(` Save to: ${dim2(modelPath)}
8845
+ `);
8846
+ return false;
8847
+ }
8848
+ process.stderr.write(`${green("✓")} Model downloaded to ${dim2(modelPath)}
8849
+ `);
8850
+ return true;
8851
+ } catch (e) {
8852
+ process.stderr.write(`${red2("✗")} Failed to download model: ${e instanceof Error ? e.message : String(e)}
8853
+ `);
8854
+ try {
8855
+ unlinkSync4(modelPath);
8856
+ } catch {}
8857
+ return false;
8858
+ }
8859
+ }
8860
+
8861
+ class VoiceController {
8862
+ state = "idle";
8863
+ recordProcess = null;
8864
+ tempFile;
8865
+ deps;
8866
+ onStateChange;
8867
+ constructor(options) {
8868
+ this.onStateChange = options.onStateChange;
8869
+ this.tempFile = join18(tmpdir4(), `locus-voice-${process.pid}.wav`);
8870
+ this.deps = checkDependencies();
8871
+ }
8872
+ getState() {
8873
+ return this.state;
8874
+ }
8875
+ startRecording() {
8876
+ if (this.state !== "idle")
8877
+ return false;
8878
+ this.deps = checkDependencies();
8879
+ if (!this.deps.sox || !this.deps.whisper) {
8880
+ if (!autoInstallDependencies(this.deps)) {
8881
+ return false;
8882
+ }
8883
+ this.deps = checkDependencies();
8884
+ if (!this.deps.sox || !this.deps.whisper) {
8885
+ printDependencyHelp(this.deps);
8886
+ return false;
8887
+ }
8888
+ }
8889
+ if (!this.deps.modelDownloaded) {
8890
+ if (!downloadModel()) {
8891
+ return false;
8892
+ }
8893
+ this.deps.modelDownloaded = true;
8894
+ }
8895
+ const args = this.buildRecordArgs();
8896
+ if (!args)
8897
+ return false;
8898
+ const binary = this.deps.soxBinary;
8899
+ if (!binary) {
8900
+ process.stderr.write(`${red2("✗")} sox binary not found. Please install sox and try again.
8901
+ `);
8902
+ return false;
8903
+ }
8904
+ const spawnArgs = binary === "rec" ? args : ["-d", ...args];
8905
+ this.recordProcess = spawn6(binary, spawnArgs, {
8906
+ stdio: ["pipe", "pipe", "pipe"]
8907
+ });
8908
+ this.recordProcess.on("error", (err) => {
8909
+ process.stderr.write(`\r
8910
+ ${red2("✗")} Recording failed: ${err.message}\r
8911
+ `);
8912
+ this.setState("idle");
8913
+ });
8914
+ this.setState("recording");
8915
+ return true;
8916
+ }
8917
+ buildRecordArgs() {
8918
+ return [
8919
+ "-r",
8920
+ "16000",
8921
+ "-c",
8922
+ "1",
8923
+ "-b",
8924
+ "16",
8925
+ this.tempFile
8926
+ ];
8927
+ }
8928
+ async stopAndTranscribe() {
8929
+ if (!this.recordProcess) {
8930
+ this.setState("idle");
8931
+ return null;
8932
+ }
8933
+ this.recordProcess.kill("SIGTERM");
8934
+ this.recordProcess = null;
8935
+ this.setState("idle");
8936
+ await sleep2(200);
8937
+ if (!existsSync18(this.tempFile)) {
8938
+ return null;
8939
+ }
8940
+ try {
8941
+ const text = await this.transcribe();
8942
+ return text || null;
8943
+ } catch (e) {
8944
+ process.stderr.write(`${red2("✗")} Transcription failed: ${e instanceof Error ? e.message : String(e)}
8945
+ `);
8946
+ return null;
8947
+ } finally {
8948
+ try {
8949
+ unlinkSync4(this.tempFile);
8950
+ } catch {}
8951
+ }
8952
+ }
8953
+ transcribe() {
8954
+ return new Promise((resolve2, reject) => {
8955
+ const binary = this.deps.whisperBinary;
8956
+ if (!binary) {
8957
+ process.stderr.write(`${red2("✗")} whisper.cpp binary not found. Please install whisper.cpp and try again.
8958
+ `);
8959
+ reject(new Error("whisper.cpp binary not found. Please install whisper.cpp and try again."));
8960
+ return;
8961
+ }
8962
+ const modelPath = getWhisperModelPath();
8963
+ const args = [
8964
+ "-m",
8965
+ modelPath,
8966
+ "-f",
8967
+ this.tempFile,
8968
+ "--no-timestamps",
8969
+ "--language",
8970
+ WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
8971
+ ];
8972
+ const proc = spawn6(binary, args, {
8973
+ stdio: ["pipe", "pipe", "pipe"]
8974
+ });
8975
+ let stdout = "";
8976
+ let stderr = "";
8977
+ proc.stdout?.on("data", (data) => {
8978
+ stdout += data.toString();
8979
+ });
8980
+ proc.stderr?.on("data", (data) => {
8981
+ stderr += data.toString();
8982
+ });
8983
+ proc.on("error", (err) => {
8984
+ reject(new Error(`whisper.cpp failed to start: ${err.message}`));
8985
+ });
8986
+ proc.on("close", (code) => {
8987
+ if (code !== 0) {
8988
+ reject(new Error(`whisper.cpp exited with code ${code}: ${stderr.trim()}`));
8989
+ return;
8990
+ }
8991
+ const text = stdout.split(`
8992
+ `).map((line) => line.replace(/^\[.*?\]\s*/, "").trim()).filter((line) => line.length > 0).join(" ").trim();
8993
+ resolve2(text);
8994
+ });
8995
+ });
8996
+ }
8997
+ cancel() {
8998
+ if (this.recordProcess) {
8999
+ this.recordProcess.kill("SIGKILL");
9000
+ this.recordProcess = null;
9001
+ }
9002
+ try {
9003
+ unlinkSync4(this.tempFile);
9004
+ } catch {}
9005
+ this.setState("idle");
9006
+ }
9007
+ setState(state) {
9008
+ this.state = state;
9009
+ this.onStateChange(state);
9010
+ }
9011
+ }
9012
+ function voiceStatusIndicator(state) {
9013
+ switch (state) {
9014
+ case "recording":
9015
+ return `${red2(bold2("[REC]"))} `;
9016
+ case "transcribing":
9017
+ return ` ${yellow2("[...]")} `;
9018
+ default:
9019
+ return "";
9020
+ }
9021
+ }
9022
+ function sleep2(ms) {
9023
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
9024
+ }
9025
+ var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
9026
+ var init_voice = __esm(() => {
9027
+ init_terminal();
9028
+ WHISPER_MODELS_DIR = join18(homedir4(), ".locus", "whisper-models");
9029
+ LOCUS_BIN_DIR = join18(homedir4(), ".locus", "bin");
9030
+ });
9031
+
8113
9032
  // src/repl/repl.ts
8114
- import { execSync as execSync9 } from "node:child_process";
9033
+ import { execSync as execSync11 } from "node:child_process";
8115
9034
  async function startRepl(options) {
8116
9035
  const { projectRoot, config } = options;
8117
9036
  const sessionManager = new SessionManager(projectRoot);
@@ -8129,7 +9048,7 @@ async function startRepl(options) {
8129
9048
  } else {
8130
9049
  let branch = "main";
8131
9050
  try {
8132
- branch = execSync9("git rev-parse --abbrev-ref HEAD", {
9051
+ branch = execSync11("git rev-parse --abbrev-ref HEAD", {
8133
9052
  cwd: projectRoot,
8134
9053
  encoding: "utf-8",
8135
9054
  stdio: ["pipe", "pipe", "pipe"]
@@ -8195,8 +9114,15 @@ async function runInteractiveRepl(session, sessionManager, options) {
8195
9114
  new SlashCommandCompletion(getAllCommandNames()),
8196
9115
  new FilePathCompletion(projectRoot)
8197
9116
  ]);
9117
+ const basePrompt = `${cyan2("locus")} ${dim2(">")} `;
9118
+ const voice = new VoiceController({
9119
+ onStateChange: (state) => {
9120
+ const indicator = voiceStatusIndicator(state);
9121
+ input.setPrompt(indicator ? `${indicator}${basePrompt}` : basePrompt);
9122
+ }
9123
+ });
8198
9124
  const input = new InputHandler({
8199
- prompt: `${cyan2("locus")} ${dim2(">")} `,
9125
+ prompt: basePrompt,
8200
9126
  getHistory: () => history.getEntries(),
8201
9127
  onTab: (text) => completion.complete(text)
8202
9128
  });
@@ -8249,12 +9175,33 @@ async function runInteractiveRepl(session, sessionManager, options) {
8249
9175
  onVerboseToggle: () => {
8250
9176
  verbose = !verbose;
8251
9177
  },
8252
- getVerbose: () => verbose
9178
+ getVerbose: () => verbose,
9179
+ onVoiceToggle: () => {
9180
+ if (voice.getState() !== "idle")
9181
+ return;
9182
+ const started = voice.startRecording();
9183
+ if (started) {
9184
+ process.stderr.write(`${dim2("Recording... press")} ${bold2("Enter")} ${dim2("to stop")}
9185
+ `);
9186
+ }
9187
+ }
8253
9188
  };
8254
9189
  while (!shouldExit) {
8255
9190
  const result = await input.readline();
8256
9191
  switch (result.type) {
8257
9192
  case "submit": {
9193
+ if (voice.getState() === "recording") {
9194
+ process.stderr.write(`${dim2("Transcribing...")}
9195
+ `);
9196
+ const transcribed = await voice.stopAndTranscribe();
9197
+ if (transcribed) {
9198
+ input.setInitialBuffer(transcribed);
9199
+ } else {
9200
+ process.stderr.write(`${yellow2("!")} No speech detected.
9201
+ `);
9202
+ }
9203
+ continue;
9204
+ }
8258
9205
  const text = result.text.trim();
8259
9206
  if (!text)
8260
9207
  continue;
@@ -8305,6 +9252,7 @@ ${red2("✗")} ${msg}
8305
9252
  break;
8306
9253
  }
8307
9254
  }
9255
+ voice.cancel();
8308
9256
  const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
8309
9257
  if (shouldPersistOnExit) {
8310
9258
  sessionManager.save(session);
@@ -8370,7 +9318,7 @@ function printWelcome(session) {
8370
9318
  }
8371
9319
  process.stderr.write(`
8372
9320
  `);
8373
- process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
9321
+ process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline, /v for voice input")}
8374
9322
  `);
8375
9323
  process.stderr.write(`
8376
9324
  `);
@@ -8390,6 +9338,7 @@ var init_repl = __esm(() => {
8390
9338
  init_input_history();
8391
9339
  init_model_config();
8392
9340
  init_session_manager();
9341
+ init_voice();
8393
9342
  LOCUS_LOGO = [
8394
9343
  " ▄█ ",
8395
9344
  " ▄▄████▄▄▄▄ ",
@@ -8587,11 +9536,11 @@ var init_exec = __esm(() => {
8587
9536
  });
8588
9537
 
8589
9538
  // src/core/submodule.ts
8590
- import { execSync as execSync10 } from "node:child_process";
8591
- import { existsSync as existsSync17 } from "node:fs";
8592
- import { join as join17 } from "node:path";
9539
+ import { execSync as execSync12 } from "node:child_process";
9540
+ import { existsSync as existsSync19 } from "node:fs";
9541
+ import { join as join19 } from "node:path";
8593
9542
  function git2(args, cwd) {
8594
- return execSync10(`git ${args}`, {
9543
+ return execSync12(`git ${args}`, {
8595
9544
  cwd,
8596
9545
  encoding: "utf-8",
8597
9546
  stdio: ["pipe", "pipe", "pipe"]
@@ -8605,7 +9554,7 @@ function gitSafe(args, cwd) {
8605
9554
  }
8606
9555
  }
8607
9556
  function hasSubmodules(cwd) {
8608
- return existsSync17(join17(cwd, ".gitmodules"));
9557
+ return existsSync19(join19(cwd, ".gitmodules"));
8609
9558
  }
8610
9559
  function listSubmodules(cwd) {
8611
9560
  if (!hasSubmodules(cwd))
@@ -8625,7 +9574,7 @@ function listSubmodules(cwd) {
8625
9574
  continue;
8626
9575
  submodules.push({
8627
9576
  path,
8628
- absolutePath: join17(cwd, path),
9577
+ absolutePath: join19(cwd, path),
8629
9578
  dirty
8630
9579
  });
8631
9580
  }
@@ -8638,7 +9587,7 @@ function getDirtySubmodules(cwd) {
8638
9587
  const submodules = listSubmodules(cwd);
8639
9588
  const dirty = [];
8640
9589
  for (const sub of submodules) {
8641
- if (!existsSync17(sub.absolutePath))
9590
+ if (!existsSync19(sub.absolutePath))
8642
9591
  continue;
8643
9592
  const status = gitSafe("status --porcelain", sub.absolutePath);
8644
9593
  if (status && status.trim().length > 0) {
@@ -8659,7 +9608,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
8659
9608
  const message = `chore: complete #${issueNumber} - ${issueTitle}
8660
9609
 
8661
9610
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
8662
- execSync10("git commit -F -", {
9611
+ execSync12("git commit -F -", {
8663
9612
  input: message,
8664
9613
  cwd: sub.absolutePath,
8665
9614
  encoding: "utf-8",
@@ -8725,7 +9674,7 @@ function pushSubmoduleBranches(cwd) {
8725
9674
  const log = getLogger();
8726
9675
  const submodules = listSubmodules(cwd);
8727
9676
  for (const sub of submodules) {
8728
- if (!existsSync17(sub.absolutePath))
9677
+ if (!existsSync19(sub.absolutePath))
8729
9678
  continue;
8730
9679
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
8731
9680
  if (!branch || branch === "HEAD")
@@ -8746,7 +9695,7 @@ var init_submodule = __esm(() => {
8746
9695
  });
8747
9696
 
8748
9697
  // src/core/agent.ts
8749
- import { execSync as execSync11 } from "node:child_process";
9698
+ import { execSync as execSync13 } from "node:child_process";
8750
9699
  async function executeIssue(projectRoot, options) {
8751
9700
  const log = getLogger();
8752
9701
  const timer = createTimer();
@@ -8775,7 +9724,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
8775
9724
  }
8776
9725
  let issueComments = [];
8777
9726
  try {
8778
- const commentsRaw = execSync11(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9727
+ const commentsRaw = execSync13(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8779
9728
  if (commentsRaw) {
8780
9729
  issueComments = commentsRaw.split(`
8781
9730
  `).filter(Boolean);
@@ -8939,12 +9888,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
8939
9888
  }
8940
9889
  async function createIssuePR(projectRoot, config, issue) {
8941
9890
  try {
8942
- const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
9891
+ const currentBranch = execSync13("git rev-parse --abbrev-ref HEAD", {
8943
9892
  cwd: projectRoot,
8944
9893
  encoding: "utf-8",
8945
9894
  stdio: ["pipe", "pipe", "pipe"]
8946
9895
  }).trim();
8947
- const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9896
+ const diff = execSync13(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8948
9897
  cwd: projectRoot,
8949
9898
  encoding: "utf-8",
8950
9899
  stdio: ["pipe", "pipe", "pipe"]
@@ -8954,7 +9903,7 @@ async function createIssuePR(projectRoot, config, issue) {
8954
9903
  return;
8955
9904
  }
8956
9905
  pushSubmoduleBranches(projectRoot);
8957
- execSync11(`git push -u origin ${currentBranch}`, {
9906
+ execSync13(`git push -u origin ${currentBranch}`, {
8958
9907
  cwd: projectRoot,
8959
9908
  encoding: "utf-8",
8960
9909
  stdio: ["pipe", "pipe", "pipe"]
@@ -9009,9 +9958,9 @@ var init_agent = __esm(() => {
9009
9958
  });
9010
9959
 
9011
9960
  // src/core/conflict.ts
9012
- import { execSync as execSync12 } from "node:child_process";
9961
+ import { execSync as execSync14 } from "node:child_process";
9013
9962
  function git3(args, cwd) {
9014
- return execSync12(`git ${args}`, {
9963
+ return execSync14(`git ${args}`, {
9015
9964
  cwd,
9016
9965
  encoding: "utf-8",
9017
9966
  stdio: ["pipe", "pipe", "pipe"]
@@ -9140,19 +10089,19 @@ var init_conflict = __esm(() => {
9140
10089
 
9141
10090
  // src/core/run-state.ts
9142
10091
  import {
9143
- existsSync as existsSync18,
9144
- mkdirSync as mkdirSync12,
10092
+ existsSync as existsSync20,
10093
+ mkdirSync as mkdirSync14,
9145
10094
  readFileSync as readFileSync12,
9146
- unlinkSync as unlinkSync4,
9147
- writeFileSync as writeFileSync8
10095
+ unlinkSync as unlinkSync5,
10096
+ writeFileSync as writeFileSync9
9148
10097
  } from "node:fs";
9149
- import { dirname as dirname6, join as join18 } from "node:path";
10098
+ import { dirname as dirname6, join as join20 } from "node:path";
9150
10099
  function getRunStatePath(projectRoot) {
9151
- return join18(projectRoot, ".locus", "run-state.json");
10100
+ return join20(projectRoot, ".locus", "run-state.json");
9152
10101
  }
9153
10102
  function loadRunState(projectRoot) {
9154
10103
  const path = getRunStatePath(projectRoot);
9155
- if (!existsSync18(path))
10104
+ if (!existsSync20(path))
9156
10105
  return null;
9157
10106
  try {
9158
10107
  return JSON.parse(readFileSync12(path, "utf-8"));
@@ -9164,16 +10113,16 @@ function loadRunState(projectRoot) {
9164
10113
  function saveRunState(projectRoot, state) {
9165
10114
  const path = getRunStatePath(projectRoot);
9166
10115
  const dir = dirname6(path);
9167
- if (!existsSync18(dir)) {
9168
- mkdirSync12(dir, { recursive: true });
10116
+ if (!existsSync20(dir)) {
10117
+ mkdirSync14(dir, { recursive: true });
9169
10118
  }
9170
- writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10119
+ writeFileSync9(path, `${JSON.stringify(state, null, 2)}
9171
10120
  `, "utf-8");
9172
10121
  }
9173
10122
  function clearRunState(projectRoot) {
9174
10123
  const path = getRunStatePath(projectRoot);
9175
- if (existsSync18(path)) {
9176
- unlinkSync4(path);
10124
+ if (existsSync20(path)) {
10125
+ unlinkSync5(path);
9177
10126
  }
9178
10127
  }
9179
10128
  function createSprintRunState(sprint, branch, issues) {
@@ -9312,11 +10261,11 @@ var init_shutdown = __esm(() => {
9312
10261
  });
9313
10262
 
9314
10263
  // src/core/worktree.ts
9315
- import { execSync as execSync13 } from "node:child_process";
9316
- import { existsSync as existsSync19, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
9317
- import { join as join19 } from "node:path";
10264
+ import { execSync as execSync15 } from "node:child_process";
10265
+ import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
10266
+ import { join as join21 } from "node:path";
9318
10267
  function git4(args, cwd) {
9319
- return execSync13(`git ${args}`, {
10268
+ return execSync15(`git ${args}`, {
9320
10269
  cwd,
9321
10270
  encoding: "utf-8",
9322
10271
  stdio: ["pipe", "pipe", "pipe"]
@@ -9330,10 +10279,10 @@ function gitSafe3(args, cwd) {
9330
10279
  }
9331
10280
  }
9332
10281
  function getWorktreeDir(projectRoot) {
9333
- return join19(projectRoot, ".locus", "worktrees");
10282
+ return join21(projectRoot, ".locus", "worktrees");
9334
10283
  }
9335
10284
  function getWorktreePath(projectRoot, issueNumber) {
9336
- return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
10285
+ return join21(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9337
10286
  }
9338
10287
  function generateBranchName(issueNumber) {
9339
10288
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -9341,7 +10290,7 @@ function generateBranchName(issueNumber) {
9341
10290
  }
9342
10291
  function getWorktreeBranch(worktreePath) {
9343
10292
  try {
9344
- return execSync13("git branch --show-current", {
10293
+ return execSync15("git branch --show-current", {
9345
10294
  cwd: worktreePath,
9346
10295
  encoding: "utf-8",
9347
10296
  stdio: ["pipe", "pipe", "pipe"]
@@ -9353,7 +10302,7 @@ function getWorktreeBranch(worktreePath) {
9353
10302
  function createWorktree(projectRoot, issueNumber, baseBranch) {
9354
10303
  const log = getLogger();
9355
10304
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9356
- if (existsSync19(worktreePath)) {
10305
+ if (existsSync21(worktreePath)) {
9357
10306
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
9358
10307
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
9359
10308
  return {
@@ -9381,7 +10330,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
9381
10330
  function removeWorktree(projectRoot, issueNumber) {
9382
10331
  const log = getLogger();
9383
10332
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9384
- if (!existsSync19(worktreePath)) {
10333
+ if (!existsSync21(worktreePath)) {
9385
10334
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
9386
10335
  return;
9387
10336
  }
@@ -9400,7 +10349,7 @@ function removeWorktree(projectRoot, issueNumber) {
9400
10349
  function listWorktrees(projectRoot) {
9401
10350
  const log = getLogger();
9402
10351
  const worktreeDir = getWorktreeDir(projectRoot);
9403
- if (!existsSync19(worktreeDir)) {
10352
+ if (!existsSync21(worktreeDir)) {
9404
10353
  return [];
9405
10354
  }
9406
10355
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -9420,7 +10369,7 @@ function listWorktrees(projectRoot) {
9420
10369
  if (!match)
9421
10370
  continue;
9422
10371
  const issueNumber = Number.parseInt(match[1], 10);
9423
- const path = join19(worktreeDir, entry);
10372
+ const path = join21(worktreeDir, entry);
9424
10373
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
9425
10374
  let resolvedPath;
9426
10375
  try {
@@ -9468,7 +10417,7 @@ var exports_run = {};
9468
10417
  __export(exports_run, {
9469
10418
  runCommand: () => runCommand
9470
10419
  });
9471
- import { execSync as execSync14 } from "node:child_process";
10420
+ import { execSync as execSync16 } from "node:child_process";
9472
10421
  function resolveExecutionContext(config, modelOverride) {
9473
10422
  const model = modelOverride ?? config.ai.model;
9474
10423
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -9628,7 +10577,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
9628
10577
  }
9629
10578
  if (!flags.dryRun) {
9630
10579
  try {
9631
- execSync14(`git checkout -B ${branchName}`, {
10580
+ execSync16(`git checkout -B ${branchName}`, {
9632
10581
  cwd: projectRoot,
9633
10582
  encoding: "utf-8",
9634
10583
  stdio: ["pipe", "pipe", "pipe"]
@@ -9678,7 +10627,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
9678
10627
  let sprintContext;
9679
10628
  if (i > 0 && !flags.dryRun) {
9680
10629
  try {
9681
- sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10630
+ sprintContext = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9682
10631
  cwd: projectRoot,
9683
10632
  encoding: "utf-8",
9684
10633
  stdio: ["pipe", "pipe", "pipe"]
@@ -9743,7 +10692,7 @@ ${bold2("Summary:")}
9743
10692
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9744
10693
  if (prNumber !== undefined) {
9745
10694
  try {
9746
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10695
+ execSync16(`git checkout ${config.agent.baseBranch}`, {
9747
10696
  cwd: projectRoot,
9748
10697
  encoding: "utf-8",
9749
10698
  stdio: ["pipe", "pipe", "pipe"]
@@ -9788,7 +10737,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9788
10737
  `);
9789
10738
  if (!flags.dryRun) {
9790
10739
  try {
9791
- execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10740
+ execSync16(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
9792
10741
  cwd: projectRoot,
9793
10742
  encoding: "utf-8",
9794
10743
  stdio: ["pipe", "pipe", "pipe"]
@@ -9813,7 +10762,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9813
10762
  if (!flags.dryRun) {
9814
10763
  if (result.success) {
9815
10764
  try {
9816
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10765
+ execSync16(`git checkout ${config.agent.baseBranch}`, {
9817
10766
  cwd: projectRoot,
9818
10767
  encoding: "utf-8",
9819
10768
  stdio: ["pipe", "pipe", "pipe"]
@@ -9950,13 +10899,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
9950
10899
  `);
9951
10900
  if (state.type === "sprint" && state.branch) {
9952
10901
  try {
9953
- const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
10902
+ const currentBranch = execSync16("git rev-parse --abbrev-ref HEAD", {
9954
10903
  cwd: projectRoot,
9955
10904
  encoding: "utf-8",
9956
10905
  stdio: ["pipe", "pipe", "pipe"]
9957
10906
  }).trim();
9958
10907
  if (currentBranch !== state.branch) {
9959
- execSync14(`git checkout ${state.branch}`, {
10908
+ execSync16(`git checkout ${state.branch}`, {
9960
10909
  cwd: projectRoot,
9961
10910
  encoding: "utf-8",
9962
10911
  stdio: ["pipe", "pipe", "pipe"]
@@ -10023,7 +10972,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
10023
10972
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
10024
10973
  if (prNumber !== undefined) {
10025
10974
  try {
10026
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10975
+ execSync16(`git checkout ${config.agent.baseBranch}`, {
10027
10976
  cwd: projectRoot,
10028
10977
  encoding: "utf-8",
10029
10978
  stdio: ["pipe", "pipe", "pipe"]
@@ -10059,14 +11008,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10059
11008
  process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
10060
11009
  `);
10061
11010
  }
10062
- const status = execSync14("git status --porcelain", {
11011
+ const status = execSync16("git status --porcelain", {
10063
11012
  cwd: projectRoot,
10064
11013
  encoding: "utf-8",
10065
11014
  stdio: ["pipe", "pipe", "pipe"]
10066
11015
  }).trim();
10067
11016
  if (!status)
10068
11017
  return;
10069
- execSync14("git add -A", {
11018
+ execSync16("git add -A", {
10070
11019
  cwd: projectRoot,
10071
11020
  encoding: "utf-8",
10072
11021
  stdio: ["pipe", "pipe", "pipe"]
@@ -10074,7 +11023,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10074
11023
  const message = `chore: complete #${issueNumber} - ${issueTitle}
10075
11024
 
10076
11025
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
10077
- execSync14(`git commit -F -`, {
11026
+ execSync16(`git commit -F -`, {
10078
11027
  input: message,
10079
11028
  cwd: projectRoot,
10080
11029
  encoding: "utf-8",
@@ -10088,7 +11037,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10088
11037
  if (!config.agent.autoPR)
10089
11038
  return;
10090
11039
  try {
10091
- const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
11040
+ const diff = execSync16(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10092
11041
  cwd: projectRoot,
10093
11042
  encoding: "utf-8",
10094
11043
  stdio: ["pipe", "pipe", "pipe"]
@@ -10099,7 +11048,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10099
11048
  return;
10100
11049
  }
10101
11050
  pushSubmoduleBranches(projectRoot);
10102
- execSync14(`git push -u origin ${branchName}`, {
11051
+ execSync16(`git push -u origin ${branchName}`, {
10103
11052
  cwd: projectRoot,
10104
11053
  encoding: "utf-8",
10105
11054
  stdio: ["pipe", "pipe", "pipe"]
@@ -10257,14 +11206,14 @@ __export(exports_plan, {
10257
11206
  parsePlanArgs: () => parsePlanArgs
10258
11207
  });
10259
11208
  import {
10260
- existsSync as existsSync20,
10261
- mkdirSync as mkdirSync13,
11209
+ existsSync as existsSync22,
11210
+ mkdirSync as mkdirSync15,
10262
11211
  readdirSync as readdirSync8,
10263
11212
  readFileSync as readFileSync13,
10264
- writeFileSync as writeFileSync9
11213
+ writeFileSync as writeFileSync10
10265
11214
  } from "node:fs";
10266
- import { join as join20 } from "node:path";
10267
- function printHelp() {
11215
+ import { join as join22 } from "node:path";
11216
+ function printHelp2() {
10268
11217
  process.stderr.write(`
10269
11218
  ${bold2("locus plan")} — AI-powered sprint planning
10270
11219
 
@@ -10294,12 +11243,12 @@ function normalizeSprintName(name) {
10294
11243
  return name.trim().toLowerCase();
10295
11244
  }
10296
11245
  function getPlansDir(projectRoot) {
10297
- return join20(projectRoot, ".locus", "plans");
11246
+ return join22(projectRoot, ".locus", "plans");
10298
11247
  }
10299
11248
  function ensurePlansDir(projectRoot) {
10300
11249
  const dir = getPlansDir(projectRoot);
10301
- if (!existsSync20(dir)) {
10302
- mkdirSync13(dir, { recursive: true });
11250
+ if (!existsSync22(dir)) {
11251
+ mkdirSync15(dir, { recursive: true });
10303
11252
  }
10304
11253
  return dir;
10305
11254
  }
@@ -10308,14 +11257,14 @@ function generateId() {
10308
11257
  }
10309
11258
  function loadPlanFile(projectRoot, id) {
10310
11259
  const dir = getPlansDir(projectRoot);
10311
- if (!existsSync20(dir))
11260
+ if (!existsSync22(dir))
10312
11261
  return null;
10313
11262
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
10314
11263
  const match = files.find((f) => f.startsWith(id));
10315
11264
  if (!match)
10316
11265
  return null;
10317
11266
  try {
10318
- const content = readFileSync13(join20(dir, match), "utf-8");
11267
+ const content = readFileSync13(join22(dir, match), "utf-8");
10319
11268
  return JSON.parse(content);
10320
11269
  } catch {
10321
11270
  return null;
@@ -10323,7 +11272,7 @@ function loadPlanFile(projectRoot, id) {
10323
11272
  }
10324
11273
  async function planCommand(projectRoot, args, flags = {}) {
10325
11274
  if (args[0] === "help" || args.length === 0) {
10326
- printHelp();
11275
+ printHelp2();
10327
11276
  return;
10328
11277
  }
10329
11278
  if (args[0] === "list") {
@@ -10361,7 +11310,7 @@ async function planCommand(projectRoot, args, flags = {}) {
10361
11310
  }
10362
11311
  function handleListPlans(projectRoot) {
10363
11312
  const dir = getPlansDir(projectRoot);
10364
- if (!existsSync20(dir)) {
11313
+ if (!existsSync22(dir)) {
10365
11314
  process.stderr.write(`${dim2("No saved plans yet.")}
10366
11315
  `);
10367
11316
  return;
@@ -10379,7 +11328,7 @@ ${bold2("Saved Plans:")}
10379
11328
  for (const file of files) {
10380
11329
  const id = file.replace(".json", "");
10381
11330
  try {
10382
- const content = readFileSync13(join20(dir, file), "utf-8");
11331
+ const content = readFileSync13(join22(dir, file), "utf-8");
10383
11332
  const plan = JSON.parse(content);
10384
11333
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
10385
11334
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -10490,7 +11439,7 @@ ${bold2("Approving plan:")}
10490
11439
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
10491
11440
  const id = generateId();
10492
11441
  const plansDir = ensurePlansDir(projectRoot);
10493
- const planPath = join20(plansDir, `${id}.json`);
11442
+ const planPath = join22(plansDir, `${id}.json`);
10494
11443
  const planPathRelative = `.locus/plans/${id}.json`;
10495
11444
  const displayDirective = directive;
10496
11445
  process.stderr.write(`
@@ -10532,7 +11481,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
10532
11481
  `);
10533
11482
  return;
10534
11483
  }
10535
- if (!existsSync20(planPath)) {
11484
+ if (!existsSync22(planPath)) {
10536
11485
  process.stderr.write(`
10537
11486
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
10538
11487
  `);
@@ -10564,7 +11513,7 @@ ${yellow2("⚠")} Plan file has no issues.
10564
11513
  plan.sprint = sprintName;
10565
11514
  if (!plan.createdAt)
10566
11515
  plan.createdAt = new Date().toISOString();
10567
- writeFileSync9(planPath, JSON.stringify(plan, null, 2), "utf-8");
11516
+ writeFileSync10(planPath, JSON.stringify(plan, null, 2), "utf-8");
10568
11517
  process.stderr.write(`
10569
11518
  ${bold2("Plan saved:")} ${cyan2(id)}
10570
11519
 
@@ -10713,15 +11662,15 @@ ${directive}${sprintName ? `
10713
11662
 
10714
11663
  **Sprint:** ${sprintName}` : ""}
10715
11664
  </directive>`);
10716
- const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
10717
- if (existsSync20(locusPath)) {
11665
+ const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11666
+ if (existsSync22(locusPath)) {
10718
11667
  const content = readFileSync13(locusPath, "utf-8");
10719
11668
  parts.push(`<project-context>
10720
11669
  ${content.slice(0, 3000)}
10721
11670
  </project-context>`);
10722
11671
  }
10723
- const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
10724
- if (existsSync20(learningsPath)) {
11672
+ const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11673
+ if (existsSync22(learningsPath)) {
10725
11674
  const content = readFileSync13(learningsPath, "utf-8");
10726
11675
  parts.push(`<past-learnings>
10727
11676
  ${content.slice(0, 2000)}
@@ -10901,10 +11850,10 @@ var exports_review = {};
10901
11850
  __export(exports_review, {
10902
11851
  reviewCommand: () => reviewCommand
10903
11852
  });
10904
- import { execFileSync as execFileSync2, execSync as execSync15 } from "node:child_process";
10905
- import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
10906
- import { join as join21 } from "node:path";
10907
- function printHelp2() {
11853
+ import { execFileSync as execFileSync2, execSync as execSync17 } from "node:child_process";
11854
+ import { existsSync as existsSync23, readFileSync as readFileSync14 } from "node:fs";
11855
+ import { join as join23 } from "node:path";
11856
+ function printHelp3() {
10908
11857
  process.stderr.write(`
10909
11858
  ${bold2("locus review")} — AI-powered code review
10910
11859
 
@@ -10927,7 +11876,7 @@ ${bold2("Examples:")}
10927
11876
  }
10928
11877
  async function reviewCommand(projectRoot, args, flags = {}) {
10929
11878
  if (args[0] === "help") {
10930
- printHelp2();
11879
+ printHelp3();
10931
11880
  return;
10932
11881
  }
10933
11882
  const config = loadConfig(projectRoot);
@@ -10987,7 +11936,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
10987
11936
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10988
11937
  let prInfo;
10989
11938
  try {
10990
- const result = execSync15(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11939
+ const result = execSync17(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10991
11940
  const raw = JSON.parse(result);
10992
11941
  prInfo = {
10993
11942
  number: raw.number,
@@ -11071,8 +12020,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
11071
12020
  parts.push(`<role>
11072
12021
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
11073
12022
  </role>`);
11074
- const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
11075
- if (existsSync21(locusPath)) {
12023
+ const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
12024
+ if (existsSync23(locusPath)) {
11076
12025
  const content = readFileSync14(locusPath, "utf-8");
11077
12026
  parts.push(`<project-context>
11078
12027
  ${content.slice(0, 2000)}
@@ -11134,8 +12083,8 @@ var exports_iterate = {};
11134
12083
  __export(exports_iterate, {
11135
12084
  iterateCommand: () => iterateCommand
11136
12085
  });
11137
- import { execSync as execSync16 } from "node:child_process";
11138
- function printHelp3() {
12086
+ import { execSync as execSync18 } from "node:child_process";
12087
+ function printHelp4() {
11139
12088
  process.stderr.write(`
11140
12089
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
11141
12090
 
@@ -11160,7 +12109,7 @@ ${bold2("Examples:")}
11160
12109
  }
11161
12110
  async function iterateCommand(projectRoot, args, flags = {}) {
11162
12111
  if (args[0] === "help") {
11163
- printHelp3();
12112
+ printHelp4();
11164
12113
  return;
11165
12114
  }
11166
12115
  const config = loadConfig(projectRoot);
@@ -11352,12 +12301,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
11352
12301
  }
11353
12302
  function findPRForIssue(projectRoot, issueNumber) {
11354
12303
  try {
11355
- const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12304
+ const result = execSync18(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11356
12305
  const parsed = JSON.parse(result);
11357
12306
  if (parsed.length > 0) {
11358
12307
  return parsed[0].number;
11359
12308
  }
11360
- const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12309
+ const branchResult = execSync18(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11361
12310
  const branchParsed = JSON.parse(branchResult);
11362
12311
  if (branchParsed.length > 0) {
11363
12312
  return branchParsed[0].number;
@@ -11393,15 +12342,15 @@ __export(exports_discuss, {
11393
12342
  discussCommand: () => discussCommand
11394
12343
  });
11395
12344
  import {
11396
- existsSync as existsSync22,
11397
- mkdirSync as mkdirSync14,
12345
+ existsSync as existsSync24,
12346
+ mkdirSync as mkdirSync16,
11398
12347
  readdirSync as readdirSync9,
11399
12348
  readFileSync as readFileSync15,
11400
- unlinkSync as unlinkSync5,
11401
- writeFileSync as writeFileSync10
12349
+ unlinkSync as unlinkSync6,
12350
+ writeFileSync as writeFileSync11
11402
12351
  } from "node:fs";
11403
- import { join as join22 } from "node:path";
11404
- function printHelp4() {
12352
+ import { join as join24 } from "node:path";
12353
+ function printHelp5() {
11405
12354
  process.stderr.write(`
11406
12355
  ${bold2("locus discuss")} — AI-powered architectural discussions
11407
12356
 
@@ -11422,12 +12371,12 @@ ${bold2("Examples:")}
11422
12371
  `);
11423
12372
  }
11424
12373
  function getDiscussionsDir(projectRoot) {
11425
- return join22(projectRoot, ".locus", "discussions");
12374
+ return join24(projectRoot, ".locus", "discussions");
11426
12375
  }
11427
12376
  function ensureDiscussionsDir(projectRoot) {
11428
12377
  const dir = getDiscussionsDir(projectRoot);
11429
- if (!existsSync22(dir)) {
11430
- mkdirSync14(dir, { recursive: true });
12378
+ if (!existsSync24(dir)) {
12379
+ mkdirSync16(dir, { recursive: true });
11431
12380
  }
11432
12381
  return dir;
11433
12382
  }
@@ -11436,7 +12385,7 @@ function generateId2() {
11436
12385
  }
11437
12386
  async function discussCommand(projectRoot, args, flags = {}) {
11438
12387
  if (args[0] === "help") {
11439
- printHelp4();
12388
+ printHelp5();
11440
12389
  return;
11441
12390
  }
11442
12391
  const subcommand = args[0];
@@ -11453,7 +12402,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
11453
12402
  return deleteDiscussion(projectRoot, args[1]);
11454
12403
  }
11455
12404
  if (args.length === 0) {
11456
- printHelp4();
12405
+ printHelp5();
11457
12406
  return;
11458
12407
  }
11459
12408
  const topic = args.join(" ").trim();
@@ -11461,7 +12410,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
11461
12410
  }
11462
12411
  function listDiscussions(projectRoot) {
11463
12412
  const dir = getDiscussionsDir(projectRoot);
11464
- if (!existsSync22(dir)) {
12413
+ if (!existsSync24(dir)) {
11465
12414
  process.stderr.write(`${dim2("No discussions yet.")}
11466
12415
  `);
11467
12416
  return;
@@ -11478,7 +12427,7 @@ ${bold2("Discussions:")}
11478
12427
  `);
11479
12428
  for (const file of files) {
11480
12429
  const id = file.replace(".md", "");
11481
- const content = readFileSync15(join22(dir, file), "utf-8");
12430
+ const content = readFileSync15(join24(dir, file), "utf-8");
11482
12431
  const titleMatch = content.match(/^#\s+(.+)/m);
11483
12432
  const title = titleMatch ? titleMatch[1] : id;
11484
12433
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -11496,7 +12445,7 @@ function showDiscussion(projectRoot, id) {
11496
12445
  return;
11497
12446
  }
11498
12447
  const dir = getDiscussionsDir(projectRoot);
11499
- if (!existsSync22(dir)) {
12448
+ if (!existsSync24(dir)) {
11500
12449
  process.stderr.write(`${red2("✗")} No discussions found.
11501
12450
  `);
11502
12451
  return;
@@ -11508,7 +12457,7 @@ function showDiscussion(projectRoot, id) {
11508
12457
  `);
11509
12458
  return;
11510
12459
  }
11511
- const content = readFileSync15(join22(dir, match), "utf-8");
12460
+ const content = readFileSync15(join24(dir, match), "utf-8");
11512
12461
  process.stdout.write(`${content}
11513
12462
  `);
11514
12463
  }
@@ -11519,7 +12468,7 @@ function deleteDiscussion(projectRoot, id) {
11519
12468
  return;
11520
12469
  }
11521
12470
  const dir = getDiscussionsDir(projectRoot);
11522
- if (!existsSync22(dir)) {
12471
+ if (!existsSync24(dir)) {
11523
12472
  process.stderr.write(`${red2("✗")} No discussions found.
11524
12473
  `);
11525
12474
  return;
@@ -11531,7 +12480,7 @@ function deleteDiscussion(projectRoot, id) {
11531
12480
  `);
11532
12481
  return;
11533
12482
  }
11534
- unlinkSync5(join22(dir, match));
12483
+ unlinkSync6(join24(dir, match));
11535
12484
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
11536
12485
  `);
11537
12486
  }
@@ -11544,7 +12493,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11544
12493
  return;
11545
12494
  }
11546
12495
  const dir = getDiscussionsDir(projectRoot);
11547
- if (!existsSync22(dir)) {
12496
+ if (!existsSync24(dir)) {
11548
12497
  process.stderr.write(`${red2("✗")} No discussions found.
11549
12498
  `);
11550
12499
  return;
@@ -11556,7 +12505,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11556
12505
  `);
11557
12506
  return;
11558
12507
  }
11559
- const content = readFileSync15(join22(dir, match), "utf-8");
12508
+ const content = readFileSync15(join24(dir, match), "utf-8");
11560
12509
  const titleMatch = content.match(/^#\s+(.+)/m);
11561
12510
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
11562
12511
  await planCommand(projectRoot, [
@@ -11681,7 +12630,7 @@ ${turn.content}`;
11681
12630
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
11682
12631
  ].join(`
11683
12632
  `);
11684
- writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
12633
+ writeFileSync11(join24(dir, `${id}.md`), markdown, "utf-8");
11685
12634
  process.stderr.write(`
11686
12635
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
11687
12636
  `);
@@ -11696,15 +12645,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11696
12645
  parts.push(`<role>
11697
12646
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
11698
12647
  </role>`);
11699
- const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11700
- if (existsSync22(locusPath)) {
12648
+ const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
12649
+ if (existsSync24(locusPath)) {
11701
12650
  const content = readFileSync15(locusPath, "utf-8");
11702
12651
  parts.push(`<project-context>
11703
12652
  ${content.slice(0, 3000)}
11704
12653
  </project-context>`);
11705
12654
  }
11706
- const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11707
- if (existsSync22(learningsPath)) {
12655
+ const learningsPath = join24(projectRoot, ".locus", "LEARNINGS.md");
12656
+ if (existsSync24(learningsPath)) {
11708
12657
  const content = readFileSync15(learningsPath, "utf-8");
11709
12658
  parts.push(`<past-learnings>
11710
12659
  ${content.slice(0, 2000)}
@@ -11776,9 +12725,9 @@ __export(exports_artifacts, {
11776
12725
  formatDate: () => formatDate2,
11777
12726
  artifactsCommand: () => artifactsCommand
11778
12727
  });
11779
- import { existsSync as existsSync23, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
11780
- import { join as join23 } from "node:path";
11781
- function printHelp5() {
12728
+ import { existsSync as existsSync25, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
12729
+ import { join as join25 } from "node:path";
12730
+ function printHelp6() {
11782
12731
  process.stderr.write(`
11783
12732
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
11784
12733
 
@@ -11797,14 +12746,14 @@ ${dim2("Artifact names support partial matching.")}
11797
12746
  `);
11798
12747
  }
11799
12748
  function getArtifactsDir(projectRoot) {
11800
- return join23(projectRoot, ".locus", "artifacts");
12749
+ return join25(projectRoot, ".locus", "artifacts");
11801
12750
  }
11802
12751
  function listArtifacts(projectRoot) {
11803
12752
  const dir = getArtifactsDir(projectRoot);
11804
- if (!existsSync23(dir))
12753
+ if (!existsSync25(dir))
11805
12754
  return [];
11806
12755
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11807
- const filePath = join23(dir, fileName);
12756
+ const filePath = join25(dir, fileName);
11808
12757
  const stat = statSync5(filePath);
11809
12758
  return {
11810
12759
  name: fileName.replace(/\.md$/, ""),
@@ -11817,8 +12766,8 @@ function listArtifacts(projectRoot) {
11817
12766
  function readArtifact(projectRoot, name) {
11818
12767
  const dir = getArtifactsDir(projectRoot);
11819
12768
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11820
- const filePath = join23(dir, fileName);
11821
- if (!existsSync23(filePath))
12769
+ const filePath = join25(dir, fileName);
12770
+ if (!existsSync25(filePath))
11822
12771
  return null;
11823
12772
  const stat = statSync5(filePath);
11824
12773
  return {
@@ -11850,7 +12799,7 @@ function formatDate2(date) {
11850
12799
  }
11851
12800
  async function artifactsCommand(projectRoot, args) {
11852
12801
  if (args[0] === "help") {
11853
- printHelp5();
12802
+ printHelp6();
11854
12803
  return;
11855
12804
  }
11856
12805
  const subcommand = args[0];
@@ -11987,10 +12936,10 @@ __export(exports_sandbox2, {
11987
12936
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
11988
12937
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
11989
12938
  });
11990
- import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
12939
+ import { execSync as execSync19, spawn as spawn7 } from "node:child_process";
11991
12940
  import { createHash } from "node:crypto";
11992
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11993
- import { basename as basename4, join as join24 } from "node:path";
12941
+ import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
12942
+ import { basename as basename4, join as join26 } from "node:path";
11994
12943
  import { createInterface as createInterface3 } from "node:readline";
11995
12944
  function printSandboxHelp() {
11996
12945
  process.stderr.write(`
@@ -12155,7 +13104,7 @@ async function handleAgentLogin(projectRoot, agent) {
12155
13104
  process.stderr.write(`${dim2("Login and then exit when ready.")}
12156
13105
 
12157
13106
  `);
12158
- const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
13107
+ const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
12159
13108
  stdio: "inherit"
12160
13109
  });
12161
13110
  await new Promise((resolve2) => {
@@ -12199,7 +13148,7 @@ function handleRemove(projectRoot) {
12199
13148
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
12200
13149
  `);
12201
13150
  try {
12202
- execSync17(`docker sandbox rm ${sandboxName}`, {
13151
+ execSync19(`docker sandbox rm ${sandboxName}`, {
12203
13152
  encoding: "utf-8",
12204
13153
  stdio: ["pipe", "pipe", "pipe"],
12205
13154
  timeout: 15000
@@ -12446,9 +13395,9 @@ async function handleLogs(projectRoot, args) {
12446
13395
  dockerArgs.push(sandboxName);
12447
13396
  await runInteractiveCommand("docker", dockerArgs);
12448
13397
  }
12449
- function detectPackageManager(projectRoot) {
13398
+ function detectPackageManager2(projectRoot) {
12450
13399
  try {
12451
- const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
13400
+ const raw = readFileSync17(join26(projectRoot, "package.json"), "utf-8");
12452
13401
  const pkgJson = JSON.parse(raw);
12453
13402
  if (typeof pkgJson.packageManager === "string") {
12454
13403
  const name = pkgJson.packageManager.split("@")[0];
@@ -12457,13 +13406,13 @@ function detectPackageManager(projectRoot) {
12457
13406
  }
12458
13407
  }
12459
13408
  } catch {}
12460
- if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
13409
+ if (existsSync26(join26(projectRoot, "bun.lock")) || existsSync26(join26(projectRoot, "bun.lockb"))) {
12461
13410
  return "bun";
12462
13411
  }
12463
- if (existsSync24(join24(projectRoot, "yarn.lock"))) {
13412
+ if (existsSync26(join26(projectRoot, "yarn.lock"))) {
12464
13413
  return "yarn";
12465
13414
  }
12466
- if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
13415
+ if (existsSync26(join26(projectRoot, "pnpm-lock.yaml"))) {
12467
13416
  return "pnpm";
12468
13417
  }
12469
13418
  return "npm";
@@ -12484,7 +13433,7 @@ async function runSandboxSetup(sandboxName, projectRoot) {
12484
13433
  const ecosystem = detectProjectEcosystem(projectRoot);
12485
13434
  const isJS = isJavaScriptEcosystem(ecosystem);
12486
13435
  if (isJS) {
12487
- const pm = detectPackageManager(projectRoot);
13436
+ const pm = detectPackageManager2(projectRoot);
12488
13437
  if (pm !== "npm") {
12489
13438
  await ensurePackageManagerInSandbox(sandboxName, pm);
12490
13439
  }
@@ -12512,8 +13461,8 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
12512
13461
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
12513
13462
  `);
12514
13463
  }
12515
- const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
12516
- if (existsSync24(setupScript)) {
13464
+ const setupScript = join26(projectRoot, ".locus", "sandbox-setup.sh");
13465
+ if (existsSync26(setupScript)) {
12517
13466
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
12518
13467
  `);
12519
13468
  const hookOk = await runInteractiveCommand("docker", [
@@ -12594,14 +13543,14 @@ function getActiveProviderSandbox(projectRoot, provider) {
12594
13543
  }
12595
13544
  function runInteractiveCommand(command, args) {
12596
13545
  return new Promise((resolve2) => {
12597
- const child = spawn6(command, args, { stdio: "inherit" });
13546
+ const child = spawn7(command, args, { stdio: "inherit" });
12598
13547
  child.on("close", (code) => resolve2(code === 0));
12599
13548
  child.on("error", () => resolve2(false));
12600
13549
  });
12601
13550
  }
12602
13551
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
12603
13552
  try {
12604
- execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13553
+ execSync19(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
12605
13554
  stdio: ["pipe", "pipe", "pipe"],
12606
13555
  timeout: 120000
12607
13556
  });
@@ -12622,7 +13571,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
12622
13571
  }
12623
13572
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
12624
13573
  try {
12625
- execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
13574
+ execSync19(`docker sandbox exec ${sandboxName} which ${pm}`, {
12626
13575
  stdio: ["pipe", "pipe", "pipe"],
12627
13576
  timeout: 5000
12628
13577
  });
@@ -12631,7 +13580,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12631
13580
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
12632
13581
  `);
12633
13582
  try {
12634
- execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13583
+ execSync19(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
12635
13584
  stdio: "inherit",
12636
13585
  timeout: 120000
12637
13586
  });
@@ -12643,7 +13592,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12643
13592
  }
12644
13593
  async function ensureCodexInSandbox(sandboxName) {
12645
13594
  try {
12646
- execSync17(`docker sandbox exec ${sandboxName} which codex`, {
13595
+ execSync19(`docker sandbox exec ${sandboxName} which codex`, {
12647
13596
  stdio: ["pipe", "pipe", "pipe"],
12648
13597
  timeout: 5000
12649
13598
  });
@@ -12651,7 +13600,7 @@ async function ensureCodexInSandbox(sandboxName) {
12651
13600
  process.stderr.write(`Installing codex in sandbox...
12652
13601
  `);
12653
13602
  try {
12654
- execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13603
+ execSync19(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
12655
13604
  } catch {
12656
13605
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
12657
13606
  `);
@@ -12660,7 +13609,7 @@ async function ensureCodexInSandbox(sandboxName) {
12660
13609
  }
12661
13610
  function isSandboxAlive(name) {
12662
13611
  try {
12663
- const output = execSync17("docker sandbox ls", {
13612
+ const output = execSync19("docker sandbox ls", {
12664
13613
  encoding: "utf-8",
12665
13614
  stdio: ["pipe", "pipe", "pipe"],
12666
13615
  timeout: 5000
@@ -12686,13 +13635,13 @@ init_context();
12686
13635
  init_logger();
12687
13636
  init_rate_limiter();
12688
13637
  init_terminal();
12689
- import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
12690
- import { join as join25 } from "node:path";
13638
+ import { existsSync as existsSync27, readFileSync as readFileSync18 } from "node:fs";
13639
+ import { join as join27 } from "node:path";
12691
13640
  import { fileURLToPath } from "node:url";
12692
13641
  function getCliVersion() {
12693
13642
  const fallbackVersion = "0.0.0";
12694
- const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12695
- if (!existsSync25(packageJsonPath)) {
13643
+ const packageJsonPath = join27(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
13644
+ if (!existsSync27(packageJsonPath)) {
12696
13645
  return fallbackVersion;
12697
13646
  }
12698
13647
  try {
@@ -12842,7 +13791,7 @@ function printLogo() {
12842
13791
  `);
12843
13792
  }
12844
13793
  }
12845
- function printHelp6() {
13794
+ function printHelp7() {
12846
13795
  printLogo();
12847
13796
  process.stderr.write(`
12848
13797
 
@@ -12863,6 +13812,7 @@ ${bold2("Commands:")}
12863
13812
  ${cyan2("status")} Dashboard view of current state
12864
13813
  ${cyan2("config")} View and manage settings
12865
13814
  ${cyan2("logs")} View, tail, and manage execution logs
13815
+ ${cyan2("create")} ${dim2("<name>")} Scaffold a new community package
12866
13816
  ${cyan2("install")} Install a community package
12867
13817
  ${cyan2("uninstall")} Remove an installed package
12868
13818
  ${cyan2("packages")} Manage installed packages (list, outdated)
@@ -12946,7 +13896,7 @@ async function main() {
12946
13896
  process.exit(0);
12947
13897
  }
12948
13898
  if (parsed.flags.help && !parsed.command) {
12949
- printHelp6();
13899
+ printHelp7();
12950
13900
  process.exit(0);
12951
13901
  }
12952
13902
  const command = resolveAlias(parsed.command);
@@ -12957,7 +13907,7 @@ async function main() {
12957
13907
  try {
12958
13908
  const root = getGitRoot(cwd);
12959
13909
  if (isInitialized(root)) {
12960
- logDir = join25(root, ".locus", "logs");
13910
+ logDir = join27(root, ".locus", "logs");
12961
13911
  getRateLimiter(root);
12962
13912
  }
12963
13913
  } catch {}
@@ -12978,7 +13928,7 @@ async function main() {
12978
13928
  printVersionNotice = startVersionCheck2(VERSION);
12979
13929
  }
12980
13930
  if (!command) {
12981
- printHelp6();
13931
+ printHelp7();
12982
13932
  process.exit(0);
12983
13933
  }
12984
13934
  if (command === "init") {
@@ -12987,6 +13937,13 @@ async function main() {
12987
13937
  logger.destroy();
12988
13938
  return;
12989
13939
  }
13940
+ if (command === "create") {
13941
+ const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), exports_create));
13942
+ const createArgs = parsed.flags.help ? ["help"] : parsed.args;
13943
+ await createCommand2(createArgs);
13944
+ logger.destroy();
13945
+ return;
13946
+ }
12990
13947
  if (command === "install") {
12991
13948
  if (parsed.flags.list) {
12992
13949
  const { packagesCommand: packagesCommand2 } = await Promise.resolve().then(() => (init_packages(), exports_packages));