@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.
- package/bin/locus.js +1227 -270
- 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: "
|
|
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
|
|
2232
|
-
mkdirSync as
|
|
2580
|
+
existsSync as existsSync8,
|
|
2581
|
+
mkdirSync as mkdirSync7,
|
|
2233
2582
|
readFileSync as readFileSync5,
|
|
2234
2583
|
renameSync,
|
|
2235
|
-
writeFileSync as
|
|
2584
|
+
writeFileSync as writeFileSync6
|
|
2236
2585
|
} from "node:fs";
|
|
2237
2586
|
import { homedir as homedir2 } from "node:os";
|
|
2238
|
-
import { join as
|
|
2587
|
+
import { join as join8 } from "node:path";
|
|
2239
2588
|
function getPackagesDir() {
|
|
2240
2589
|
const home = process.env.HOME || homedir2();
|
|
2241
|
-
const dir =
|
|
2242
|
-
if (!
|
|
2243
|
-
|
|
2590
|
+
const dir = join8(home, ".locus", "packages");
|
|
2591
|
+
if (!existsSync8(dir)) {
|
|
2592
|
+
mkdirSync7(dir, { recursive: true });
|
|
2244
2593
|
}
|
|
2245
|
-
const pkgJson =
|
|
2246
|
-
if (!
|
|
2247
|
-
|
|
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
|
|
2602
|
+
return join8(getPackagesDir(), "registry.json");
|
|
2254
2603
|
}
|
|
2255
2604
|
function loadRegistry() {
|
|
2256
2605
|
const registryPath = getRegistryPath();
|
|
2257
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2296
|
-
return
|
|
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
|
|
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 || !
|
|
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
|
|
2535
|
-
import { join as
|
|
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 =
|
|
2613
|
-
if (!
|
|
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
|
|
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
|
|
3246
|
+
import { join as join10 } from "node:path";
|
|
2898
3247
|
async function logsCommand(cwd, options) {
|
|
2899
|
-
const logsDir =
|
|
2900
|
-
if (!
|
|
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 =
|
|
2956
|
-
if (
|
|
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 (!
|
|
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) =>
|
|
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
|
|
3345
|
-
import { existsSync as
|
|
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
|
|
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 (!
|
|
3359
|
-
|
|
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 =
|
|
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 =
|
|
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" &&
|
|
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 =
|
|
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 =
|
|
3403
|
-
|
|
3404
|
-
if (
|
|
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 =
|
|
3761
|
+
STABLE_DIR = join11(tmpdir(), "locus-images");
|
|
3413
3762
|
});
|
|
3414
3763
|
|
|
3415
3764
|
// src/repl/image-detect.ts
|
|
3416
|
-
import { copyFileSync, existsSync as
|
|
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
|
|
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 =
|
|
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 (!
|
|
3518
|
-
|
|
3866
|
+
if (!existsSync13(targetDir)) {
|
|
3867
|
+
mkdirSync9(targetDir, { recursive: true });
|
|
3519
3868
|
}
|
|
3520
|
-
const dest =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
3600
|
-
|
|
3948
|
+
if (!existsSync13(STABLE_DIR2)) {
|
|
3949
|
+
mkdirSync9(STABLE_DIR2, { recursive: true });
|
|
3601
3950
|
}
|
|
3602
|
-
const dest =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4601
|
-
mkdirSync as
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
5104
|
+
const dest = join13(backupDir, rel);
|
|
4736
5105
|
try {
|
|
4737
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
7078
|
-
import { existsSync as
|
|
7079
|
-
import { join as
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
7155
|
-
if (
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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("/") ?
|
|
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
|
|
7870
|
-
import { dirname as dirname5, join as
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
7926
|
-
|
|
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
|
-
|
|
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
|
|
7957
|
-
mkdirSync as
|
|
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
|
|
8344
|
+
writeFileSync as writeFileSync8
|
|
7962
8345
|
} from "node:fs";
|
|
7963
|
-
import { basename as basename3, join as
|
|
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 =
|
|
7969
|
-
if (!
|
|
7970
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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,
|
|
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
|
|
8591
|
-
import { existsSync as
|
|
8592
|
-
import { join as
|
|
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
|
|
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
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
9961
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
9013
9962
|
function git3(args, cwd) {
|
|
9014
|
-
return
|
|
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
|
|
9144
|
-
mkdirSync as
|
|
10092
|
+
existsSync as existsSync20,
|
|
10093
|
+
mkdirSync as mkdirSync14,
|
|
9145
10094
|
readFileSync as readFileSync12,
|
|
9146
|
-
unlinkSync as
|
|
9147
|
-
writeFileSync as
|
|
10095
|
+
unlinkSync as unlinkSync5,
|
|
10096
|
+
writeFileSync as writeFileSync9
|
|
9148
10097
|
} from "node:fs";
|
|
9149
|
-
import { dirname as dirname6, join as
|
|
10098
|
+
import { dirname as dirname6, join as join20 } from "node:path";
|
|
9150
10099
|
function getRunStatePath(projectRoot) {
|
|
9151
|
-
return
|
|
10100
|
+
return join20(projectRoot, ".locus", "run-state.json");
|
|
9152
10101
|
}
|
|
9153
10102
|
function loadRunState(projectRoot) {
|
|
9154
10103
|
const path = getRunStatePath(projectRoot);
|
|
9155
|
-
if (!
|
|
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 (!
|
|
9168
|
-
|
|
10116
|
+
if (!existsSync20(dir)) {
|
|
10117
|
+
mkdirSync14(dir, { recursive: true });
|
|
9169
10118
|
}
|
|
9170
|
-
|
|
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 (
|
|
9176
|
-
|
|
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
|
|
9316
|
-
import { existsSync as
|
|
9317
|
-
import { join as
|
|
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
|
|
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
|
|
10282
|
+
return join21(projectRoot, ".locus", "worktrees");
|
|
9334
10283
|
}
|
|
9335
10284
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
9336
|
-
return
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
10261
|
-
mkdirSync as
|
|
11209
|
+
existsSync as existsSync22,
|
|
11210
|
+
mkdirSync as mkdirSync15,
|
|
10262
11211
|
readdirSync as readdirSync8,
|
|
10263
11212
|
readFileSync as readFileSync13,
|
|
10264
|
-
writeFileSync as
|
|
11213
|
+
writeFileSync as writeFileSync10
|
|
10265
11214
|
} from "node:fs";
|
|
10266
|
-
import { join as
|
|
10267
|
-
function
|
|
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
|
|
11246
|
+
return join22(projectRoot, ".locus", "plans");
|
|
10298
11247
|
}
|
|
10299
11248
|
function ensurePlansDir(projectRoot) {
|
|
10300
11249
|
const dir = getPlansDir(projectRoot);
|
|
10301
|
-
if (!
|
|
10302
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
10717
|
-
if (
|
|
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 =
|
|
10724
|
-
if (
|
|
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
|
|
10905
|
-
import { existsSync as
|
|
10906
|
-
import { join as
|
|
10907
|
-
function
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
11075
|
-
if (
|
|
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
|
|
11138
|
-
function
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
11397
|
-
mkdirSync as
|
|
12345
|
+
existsSync as existsSync24,
|
|
12346
|
+
mkdirSync as mkdirSync16,
|
|
11398
12347
|
readdirSync as readdirSync9,
|
|
11399
12348
|
readFileSync as readFileSync15,
|
|
11400
|
-
unlinkSync as
|
|
11401
|
-
writeFileSync as
|
|
12349
|
+
unlinkSync as unlinkSync6,
|
|
12350
|
+
writeFileSync as writeFileSync11
|
|
11402
12351
|
} from "node:fs";
|
|
11403
|
-
import { join as
|
|
11404
|
-
function
|
|
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
|
|
12374
|
+
return join24(projectRoot, ".locus", "discussions");
|
|
11426
12375
|
}
|
|
11427
12376
|
function ensureDiscussionsDir(projectRoot) {
|
|
11428
12377
|
const dir = getDiscussionsDir(projectRoot);
|
|
11429
|
-
if (!
|
|
11430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
11700
|
-
if (
|
|
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 =
|
|
11707
|
-
if (
|
|
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
|
|
11780
|
-
import { join as
|
|
11781
|
-
function
|
|
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
|
|
12749
|
+
return join25(projectRoot, ".locus", "artifacts");
|
|
11801
12750
|
}
|
|
11802
12751
|
function listArtifacts(projectRoot) {
|
|
11803
12752
|
const dir = getArtifactsDir(projectRoot);
|
|
11804
|
-
if (!
|
|
12753
|
+
if (!existsSync25(dir))
|
|
11805
12754
|
return [];
|
|
11806
12755
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11807
|
-
const filePath =
|
|
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 =
|
|
11821
|
-
if (!
|
|
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
|
-
|
|
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
|
|
12939
|
+
import { execSync as execSync19, spawn as spawn7 } from "node:child_process";
|
|
11991
12940
|
import { createHash } from "node:crypto";
|
|
11992
|
-
import { existsSync as
|
|
11993
|
-
import { basename as basename4, join as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
13398
|
+
function detectPackageManager2(projectRoot) {
|
|
12450
13399
|
try {
|
|
12451
|
-
const raw = readFileSync17(
|
|
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 (
|
|
13409
|
+
if (existsSync26(join26(projectRoot, "bun.lock")) || existsSync26(join26(projectRoot, "bun.lockb"))) {
|
|
12461
13410
|
return "bun";
|
|
12462
13411
|
}
|
|
12463
|
-
if (
|
|
13412
|
+
if (existsSync26(join26(projectRoot, "yarn.lock"))) {
|
|
12464
13413
|
return "yarn";
|
|
12465
13414
|
}
|
|
12466
|
-
if (
|
|
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 =
|
|
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 =
|
|
12516
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
12690
|
-
import { join as
|
|
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 =
|
|
12695
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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));
|