@4pm/cli 1.8.4 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +541 -423
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33965,6 +33965,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
|
|
|
33965
33965
|
/** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
|
|
33966
33966
|
ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
|
|
33967
33967
|
});
|
|
33968
|
+
var provisionRequestSchema = external_exports.object({
|
|
33969
|
+
mode: external_exports.enum(["sync", "force"]).optional().default("sync")
|
|
33970
|
+
});
|
|
33971
|
+
var provisionForUserRequestSchema = external_exports.object({
|
|
33972
|
+
userId: external_exports.string().uuid(),
|
|
33973
|
+
projectId: external_exports.string().uuid(),
|
|
33974
|
+
mode: external_exports.enum(["sync", "force"]).optional().default("sync")
|
|
33975
|
+
});
|
|
33968
33976
|
var listWorkersQuerySchema = baseRequestSchema.extend({
|
|
33969
33977
|
status: external_exports.enum(["online", "offline"]).optional(),
|
|
33970
33978
|
/** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
|
|
@@ -34240,7 +34248,13 @@ var repoSpecSchema = external_exports.object({
|
|
|
34240
34248
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
34241
34249
|
url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
|
|
34242
34250
|
/** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
|
|
34243
|
-
subdir: external_exports.string().max(120).optional().default("")
|
|
34251
|
+
subdir: external_exports.string().max(120).optional().default(""),
|
|
34252
|
+
/**
|
|
34253
|
+
* Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
|
|
34254
|
+
* worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
|
|
34255
|
+
* fast-forward pulls it.
|
|
34256
|
+
*/
|
|
34257
|
+
branch: external_exports.string().max(200).optional().default("")
|
|
34244
34258
|
});
|
|
34245
34259
|
function repoName(url) {
|
|
34246
34260
|
const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
|
|
@@ -34367,9 +34381,282 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
34367
34381
|
projectId: external_exports.string().uuid().optional()
|
|
34368
34382
|
});
|
|
34369
34383
|
|
|
34384
|
+
// src/core/scaffold.ts
|
|
34385
|
+
import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
|
|
34386
|
+
import { existsSync as existsSync4 } from "fs";
|
|
34387
|
+
import { execFile as execFile2 } from "child_process";
|
|
34388
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
34389
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
34390
|
+
import { promisify as promisify2 } from "util";
|
|
34391
|
+
|
|
34392
|
+
// src/core/ai-assist.ts
|
|
34393
|
+
import { execFile } from "child_process";
|
|
34394
|
+
import { promisify } from "util";
|
|
34395
|
+
var run = promisify(execFile);
|
|
34396
|
+
function aiCli() {
|
|
34397
|
+
return process.env.AI_CLI || "claude";
|
|
34398
|
+
}
|
|
34399
|
+
function aiArgs(cli, prompt) {
|
|
34400
|
+
if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
|
|
34401
|
+
if (cli === "codex") return ["exec", "--json", prompt];
|
|
34402
|
+
return ["-p", prompt];
|
|
34403
|
+
}
|
|
34404
|
+
function estimateTokens(prompt, output) {
|
|
34405
|
+
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
34406
|
+
}
|
|
34407
|
+
function parseAi(cli, stdout, prompt) {
|
|
34408
|
+
if (cli === "claude") {
|
|
34409
|
+
try {
|
|
34410
|
+
const j = JSON.parse(stdout);
|
|
34411
|
+
const text2 = (j.result ?? "").trim();
|
|
34412
|
+
const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
|
|
34413
|
+
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
|
|
34414
|
+
} catch {
|
|
34415
|
+
}
|
|
34416
|
+
}
|
|
34417
|
+
if (cli === "codex") {
|
|
34418
|
+
try {
|
|
34419
|
+
let text2 = "";
|
|
34420
|
+
let tokens = 0;
|
|
34421
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
34422
|
+
const obj = JSON.parse(line);
|
|
34423
|
+
if (obj.text) text2 = obj.text;
|
|
34424
|
+
else if (obj.message) text2 = obj.message;
|
|
34425
|
+
if (obj.usage) {
|
|
34426
|
+
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
34427
|
+
}
|
|
34428
|
+
}
|
|
34429
|
+
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
|
|
34430
|
+
} catch {
|
|
34431
|
+
}
|
|
34432
|
+
}
|
|
34433
|
+
const text = stdout.trim();
|
|
34434
|
+
return { text, tokens: estimateTokens(prompt, text) };
|
|
34435
|
+
}
|
|
34436
|
+
async function runAi(prompt) {
|
|
34437
|
+
const cli = aiCli();
|
|
34438
|
+
const { stdout } = await run(cli, aiArgs(cli, prompt), {
|
|
34439
|
+
timeout: 12e4,
|
|
34440
|
+
maxBuffer: 8 * 1024 * 1024
|
|
34441
|
+
});
|
|
34442
|
+
return parseAi(cli, stdout, prompt);
|
|
34443
|
+
}
|
|
34444
|
+
async function aiGenerate(prompt) {
|
|
34445
|
+
const { text } = await runAi(prompt);
|
|
34446
|
+
return text;
|
|
34447
|
+
}
|
|
34448
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
34449
|
+
const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
|
|
34450
|
+
${input}`;
|
|
34451
|
+
const { text, tokens } = await runAi(prompt);
|
|
34452
|
+
const start = text.indexOf("{");
|
|
34453
|
+
const end = text.lastIndexOf("}");
|
|
34454
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34455
|
+
try {
|
|
34456
|
+
const j = JSON.parse(text.slice(start, end + 1));
|
|
34457
|
+
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
34458
|
+
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
34459
|
+
} catch {
|
|
34460
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34461
|
+
}
|
|
34462
|
+
}
|
|
34463
|
+
|
|
34464
|
+
// src/core/scaffold.ts
|
|
34465
|
+
var run2 = promisify2(execFile2);
|
|
34466
|
+
function reposOf(spec) {
|
|
34467
|
+
const repos = spec?.repos;
|
|
34468
|
+
return Array.isArray(repos) ? repos : [];
|
|
34469
|
+
}
|
|
34470
|
+
function subDirName(repo) {
|
|
34471
|
+
const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
|
|
34472
|
+
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
34473
|
+
}
|
|
34474
|
+
function cloneArgs(url, dest, branch) {
|
|
34475
|
+
const b = (branch ?? "").trim();
|
|
34476
|
+
return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
|
|
34477
|
+
}
|
|
34478
|
+
async function updateRepo(dir2, label, branch, emit) {
|
|
34479
|
+
const b = (branch ?? "").trim();
|
|
34480
|
+
emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
|
|
34481
|
+
await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
34482
|
+
if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
|
|
34483
|
+
await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
34484
|
+
}
|
|
34485
|
+
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
34486
|
+
const mode = opts.mode ?? "clone";
|
|
34487
|
+
const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
|
|
34488
|
+
for (const repo of repos) {
|
|
34489
|
+
if (repo.primary) {
|
|
34490
|
+
if (opts.clonePrimary && repo.url) {
|
|
34491
|
+
if (existsSync4(join6(target, ".git"))) {
|
|
34492
|
+
if (mode === "sync") {
|
|
34493
|
+
await updateRepo(target, "primary repo", repo.branch, emit);
|
|
34494
|
+
} else if (mode === "force") {
|
|
34495
|
+
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
34496
|
+
const tmp = `${target}.4pm-reclone`;
|
|
34497
|
+
await rm(tmp, { recursive: true, force: true });
|
|
34498
|
+
await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
|
|
34499
|
+
for (const entry of await readdir(target)) {
|
|
34500
|
+
if (subDirsToKeep.has(entry)) continue;
|
|
34501
|
+
await rm(join6(target, entry), { recursive: true, force: true });
|
|
34502
|
+
}
|
|
34503
|
+
for (const entry of await readdir(tmp)) {
|
|
34504
|
+
if (subDirsToKeep.has(entry)) continue;
|
|
34505
|
+
await rename(join6(tmp, entry), join6(target, entry));
|
|
34506
|
+
}
|
|
34507
|
+
await rm(tmp, { recursive: true, force: true });
|
|
34508
|
+
} else {
|
|
34509
|
+
emit("git", `Primary repo already present \u2014 skipping clone.`);
|
|
34510
|
+
}
|
|
34511
|
+
continue;
|
|
34512
|
+
}
|
|
34513
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
34514
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
34515
|
+
continue;
|
|
34516
|
+
}
|
|
34517
|
+
emit("git", "Initializing the primary repo\u2026");
|
|
34518
|
+
await run2("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
34519
|
+
if (repo.url) {
|
|
34520
|
+
await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
34521
|
+
() => void 0
|
|
34522
|
+
);
|
|
34523
|
+
}
|
|
34524
|
+
continue;
|
|
34525
|
+
}
|
|
34526
|
+
const dir2 = join6(target, subDirName(repo));
|
|
34527
|
+
if (repo.url) {
|
|
34528
|
+
if (existsSync4(join6(dir2, ".git"))) {
|
|
34529
|
+
if (mode === "sync") {
|
|
34530
|
+
await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
|
|
34531
|
+
} else if (mode === "force") {
|
|
34532
|
+
emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
|
|
34533
|
+
await rm(dir2, { recursive: true, force: true });
|
|
34534
|
+
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
34535
|
+
} else {
|
|
34536
|
+
emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
|
|
34537
|
+
}
|
|
34538
|
+
continue;
|
|
34539
|
+
}
|
|
34540
|
+
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
34541
|
+
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
34542
|
+
} else {
|
|
34543
|
+
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
34544
|
+
await mkdir(dir2, { recursive: true });
|
|
34545
|
+
await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
34546
|
+
}
|
|
34547
|
+
}
|
|
34548
|
+
}
|
|
34549
|
+
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
34550
|
+
if (repos.length === 0) return;
|
|
34551
|
+
await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
|
|
34552
|
+
}
|
|
34553
|
+
function sampleDir() {
|
|
34554
|
+
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
34555
|
+
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
34556
|
+
const bundled = resolve(here, "project-sample");
|
|
34557
|
+
for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
|
|
34558
|
+
if (existsSync4(candidate)) return candidate;
|
|
34559
|
+
}
|
|
34560
|
+
return bundled;
|
|
34561
|
+
}
|
|
34562
|
+
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
34563
|
+
let lastStep = "start";
|
|
34564
|
+
const emit = (step, message) => {
|
|
34565
|
+
lastStep = step;
|
|
34566
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34567
|
+
};
|
|
34568
|
+
try {
|
|
34569
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34570
|
+
const target = join6(root, payload.projectName);
|
|
34571
|
+
await mkdir(target, { recursive: true });
|
|
34572
|
+
emit("copy", "Copying the sample template\u2026");
|
|
34573
|
+
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
34574
|
+
if (payload.spec) {
|
|
34575
|
+
emit("spec", "Writing project.spec.json\u2026");
|
|
34576
|
+
await writeFile(
|
|
34577
|
+
join6(target, "project.spec.json"),
|
|
34578
|
+
JSON.stringify(payload.spec, null, 2),
|
|
34579
|
+
"utf8"
|
|
34580
|
+
);
|
|
34581
|
+
}
|
|
34582
|
+
const repos = reposOf(payload.spec);
|
|
34583
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
34584
|
+
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
34585
|
+
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
34586
|
+
await writeTemplateMarker(target);
|
|
34587
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
34588
|
+
return { ok: true, path: target };
|
|
34589
|
+
} catch (err) {
|
|
34590
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
34591
|
+
}
|
|
34592
|
+
}
|
|
34593
|
+
async function writeTemplateMarker(target) {
|
|
34594
|
+
const dir2 = join6(target, ".4pm");
|
|
34595
|
+
await mkdir(dir2, { recursive: true });
|
|
34596
|
+
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
34597
|
+
await writeFile(join6(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
34598
|
+
}
|
|
34599
|
+
async function aiInit(target, spec, emit) {
|
|
34600
|
+
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
34601
|
+
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
34602
|
+
if (subagents.length > 0) {
|
|
34603
|
+
await mkdir(join6(target, ".claude", "agents"), { recursive: true });
|
|
34604
|
+
for (const sa of subagents) {
|
|
34605
|
+
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34606
|
+
if (!name) continue;
|
|
34607
|
+
const body = `---
|
|
34608
|
+
name: ${name}
|
|
34609
|
+
---
|
|
34610
|
+
|
|
34611
|
+
${(sa.description || "").trim()}
|
|
34612
|
+
`;
|
|
34613
|
+
await writeFile(join6(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
34614
|
+
}
|
|
34615
|
+
}
|
|
34616
|
+
const specJson = JSON.stringify(spec);
|
|
34617
|
+
await generateFile(
|
|
34618
|
+
join6(target, "README.md"),
|
|
34619
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
34620
|
+
${specJson}`
|
|
34621
|
+
);
|
|
34622
|
+
await generateFile(
|
|
34623
|
+
join6(target, "CLAUDE.md"),
|
|
34624
|
+
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
34625
|
+
${specJson}`
|
|
34626
|
+
);
|
|
34627
|
+
}
|
|
34628
|
+
async function generateFile(path, prompt) {
|
|
34629
|
+
try {
|
|
34630
|
+
const text = await aiGenerate(prompt);
|
|
34631
|
+
if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
|
|
34632
|
+
} catch {
|
|
34633
|
+
}
|
|
34634
|
+
}
|
|
34635
|
+
async function addProject(payload, profileDir2, onProgress) {
|
|
34636
|
+
let lastStep = "start";
|
|
34637
|
+
const emit = (step, message) => {
|
|
34638
|
+
lastStep = step;
|
|
34639
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34640
|
+
};
|
|
34641
|
+
try {
|
|
34642
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34643
|
+
const target = join6(root, payload.projectName);
|
|
34644
|
+
await mkdir(target, { recursive: true });
|
|
34645
|
+
const repos = payload.repos ?? [];
|
|
34646
|
+
const mode = payload.mode ?? "clone";
|
|
34647
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
|
|
34648
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
34649
|
+
return { ok: true, path: target };
|
|
34650
|
+
} catch (err) {
|
|
34651
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34652
|
+
onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
|
|
34653
|
+
return { ok: false, error: message, step: lastStep };
|
|
34654
|
+
}
|
|
34655
|
+
}
|
|
34656
|
+
|
|
34370
34657
|
// src/core/worker-metrics.ts
|
|
34371
34658
|
import { statfs, readFile } from "fs/promises";
|
|
34372
|
-
import { existsSync as
|
|
34659
|
+
import { existsSync as existsSync5 } from "fs";
|
|
34373
34660
|
import { totalmem, freemem, cpus, platform, arch } from "os";
|
|
34374
34661
|
var CGROUP = "/sys/fs/cgroup";
|
|
34375
34662
|
var MB = 1024 * 1024;
|
|
@@ -34419,7 +34706,7 @@ function parseMaybeMax(text) {
|
|
|
34419
34706
|
return Number.isFinite(n) ? n : null;
|
|
34420
34707
|
}
|
|
34421
34708
|
function createWorkerMetricsSampler(diskPath) {
|
|
34422
|
-
const cgroupV2 =
|
|
34709
|
+
const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
|
|
34423
34710
|
let cgPrev = null;
|
|
34424
34711
|
let osPrev = null;
|
|
34425
34712
|
async function cgroupCpuCount() {
|
|
@@ -34734,6 +35021,10 @@ var WsChannels = {
|
|
|
34734
35021
|
// serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
|
|
34735
35022
|
// reconnect. `ws_token` seeds these on connect; this channel only carries updates.
|
|
34736
35023
|
PROJECT_TOKENS: "project.tokens",
|
|
35024
|
+
// server → cli (ADR-0289): update the cli to the latest version now (idle-aware self-update +
|
|
35025
|
+
// re-exec). Sent by the manual operator "Update" action and the update-on-connect hook when the
|
|
35026
|
+
// connecting cli is outdated. No payload; honours the machine's local autoUpdate opt-out.
|
|
35027
|
+
CLI_UPDATE: "cli.update",
|
|
34737
35028
|
// command lifecycle
|
|
34738
35029
|
COMMAND_ANNOUNCE: "command.announce",
|
|
34739
35030
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
@@ -34847,9 +35138,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
34847
35138
|
import { spawn as spawn2 } from "child_process";
|
|
34848
35139
|
|
|
34849
35140
|
// src/common/logger/logger.ts
|
|
34850
|
-
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
35141
|
+
import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
|
|
34851
35142
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
34852
|
-
import { join as
|
|
35143
|
+
import { join as join7 } from "path";
|
|
34853
35144
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
34854
35145
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
34855
35146
|
var DEFAULT_RETENTION_DAYS = 14;
|
|
@@ -34898,8 +35189,8 @@ var CliLogger = class {
|
|
|
34898
35189
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
34899
35190
|
const entry = { timestamp, level, ...this.base, event, ...fields };
|
|
34900
35191
|
const line = JSON.stringify(entry) + "\n";
|
|
34901
|
-
const path =
|
|
34902
|
-
this.queue = this.queue.then(() =>
|
|
35192
|
+
const path = join7(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
|
|
35193
|
+
this.queue = this.queue.then(() => mkdir2(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
|
|
34903
35194
|
console.warn(`Failed to write cli log: ${err}`);
|
|
34904
35195
|
});
|
|
34905
35196
|
}
|
|
@@ -34907,13 +35198,13 @@ var CliLogger = class {
|
|
|
34907
35198
|
async cleanupOldLogs() {
|
|
34908
35199
|
const dir2 = this.dir;
|
|
34909
35200
|
if (!dir2) return;
|
|
34910
|
-
const files = await
|
|
35201
|
+
const files = await readdir2(dir2).catch(() => []);
|
|
34911
35202
|
const cutoff = Date.now() - this.retentionDays * DAY_MS;
|
|
34912
35203
|
for (const file of files) {
|
|
34913
35204
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
34914
35205
|
if (!match) continue;
|
|
34915
35206
|
if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
|
|
34916
|
-
await unlink(
|
|
35207
|
+
await unlink(join7(dir2, file)).catch(() => void 0);
|
|
34917
35208
|
}
|
|
34918
35209
|
}
|
|
34919
35210
|
}
|
|
@@ -34924,7 +35215,7 @@ function readRecentLogLines(dir2, limit) {
|
|
|
34924
35215
|
const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
|
|
34925
35216
|
const latest = files[files.length - 1];
|
|
34926
35217
|
if (!latest) return [];
|
|
34927
|
-
const lines = readFileSync6(
|
|
35218
|
+
const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
|
|
34928
35219
|
return lines.slice(-limit);
|
|
34929
35220
|
} catch {
|
|
34930
35221
|
return [];
|
|
@@ -34937,8 +35228,8 @@ function readLogUpload(dir2) {
|
|
|
34937
35228
|
const latest = files[files.length - 1];
|
|
34938
35229
|
if (!latest) return null;
|
|
34939
35230
|
let totalBytes = 0;
|
|
34940
|
-
for (const f of files) totalBytes += statSync(
|
|
34941
|
-
let content = readFileSync6(
|
|
35231
|
+
for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
|
|
35232
|
+
let content = readFileSync6(join7(dir2, latest), "utf8");
|
|
34942
35233
|
if (content.length > LOG_UPLOAD_CONTENT_MAX) {
|
|
34943
35234
|
content = `\u2026[truncated]
|
|
34944
35235
|
${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
@@ -34981,7 +35272,7 @@ function classifyToolFailure(code, out) {
|
|
|
34981
35272
|
}
|
|
34982
35273
|
return { reason: "other", retryable: false };
|
|
34983
35274
|
}
|
|
34984
|
-
function
|
|
35275
|
+
function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
34985
35276
|
return new Promise((resolve10) => {
|
|
34986
35277
|
let out = "";
|
|
34987
35278
|
let buf = "";
|
|
@@ -35021,13 +35312,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
35021
35312
|
});
|
|
35022
35313
|
}
|
|
35023
35314
|
async function detectOne(cmd, versionArg) {
|
|
35024
|
-
const { code, out } = await
|
|
35315
|
+
const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
|
|
35025
35316
|
if (code !== 0) return null;
|
|
35026
35317
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
35027
35318
|
return first ?? "";
|
|
35028
35319
|
}
|
|
35029
35320
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
35030
|
-
const { out } = await
|
|
35321
|
+
const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
35031
35322
|
if (!out.trim()) return [];
|
|
35032
35323
|
let deps = {};
|
|
35033
35324
|
try {
|
|
@@ -35103,7 +35394,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
35103
35394
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
35104
35395
|
}
|
|
35105
35396
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
35106
|
-
const { code } = await
|
|
35397
|
+
const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
35107
35398
|
return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
|
|
35108
35399
|
}
|
|
35109
35400
|
function setToolAutoUpdate(profileDir2, name, enabled) {
|
|
@@ -35134,7 +35425,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
35134
35425
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
35135
35426
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
35136
35427
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
35137
|
-
const { code, out } = await
|
|
35428
|
+
const { code, out } = await run3(manager, args, onLine, timeoutMs);
|
|
35138
35429
|
if (code === 0) return { ok: true };
|
|
35139
35430
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
35140
35431
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -35200,6 +35491,9 @@ var UpdateScheduler = class {
|
|
|
35200
35491
|
lastDoneDateKey = null;
|
|
35201
35492
|
/** Date key marked due but deferred because the cli was busy (stays set past the hour). */
|
|
35202
35493
|
pendingDateKey = null;
|
|
35494
|
+
/** A push/connect-triggered update requested now (ADR-0289) — runs when idle, independent of the
|
|
35495
|
+
* daily policy; cleared once it runs. */
|
|
35496
|
+
forcedPending = false;
|
|
35203
35497
|
/** Guards against a second update while one is in flight. */
|
|
35204
35498
|
updating = false;
|
|
35205
35499
|
/** Apply the latest policy (called on each ws_token) and start the tick timer once. */
|
|
@@ -35210,6 +35504,25 @@ var UpdateScheduler = class {
|
|
|
35210
35504
|
this.timer.unref?.();
|
|
35211
35505
|
}
|
|
35212
35506
|
}
|
|
35507
|
+
/**
|
|
35508
|
+
* Request an update to latest NOW (ADR-0289) — the server's `cli.update` push or the
|
|
35509
|
+
* update-on-connect hook. Idle-aware like the daily tick (defers while a command runs) and
|
|
35510
|
+
* honours the local `autoUpdate:false` opt-out, but is independent of the daily org policy. Also
|
|
35511
|
+
* ensures the tick timer is running so the request is served even before the first ws_token.
|
|
35512
|
+
*/
|
|
35513
|
+
updateNow() {
|
|
35514
|
+
if (this.optedOutLocally()) {
|
|
35515
|
+
logger.info("update.push.optedOut");
|
|
35516
|
+
return;
|
|
35517
|
+
}
|
|
35518
|
+
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35519
|
+
this.forcedPending = true;
|
|
35520
|
+
if (!this.timer) {
|
|
35521
|
+
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
35522
|
+
this.timer.unref?.();
|
|
35523
|
+
}
|
|
35524
|
+
void this.tick();
|
|
35525
|
+
}
|
|
35213
35526
|
/** Stop the scheduler when the session ends (logout / replaced / stopped). */
|
|
35214
35527
|
stop() {
|
|
35215
35528
|
if (this.timer) {
|
|
@@ -35242,12 +35555,19 @@ var UpdateScheduler = class {
|
|
|
35242
35555
|
return null;
|
|
35243
35556
|
}
|
|
35244
35557
|
}
|
|
35245
|
-
/** One evaluation:
|
|
35558
|
+
/** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
|
|
35246
35559
|
async tick() {
|
|
35247
|
-
|
|
35248
|
-
if (!p || !p.autoUpdateDaily || this.updating) return;
|
|
35560
|
+
if (this.updating) return;
|
|
35249
35561
|
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35250
35562
|
if (this.optedOutLocally()) return;
|
|
35563
|
+
if (this.forcedPending) {
|
|
35564
|
+
if (this.bus.busy !== null) return;
|
|
35565
|
+
this.forcedPending = false;
|
|
35566
|
+
await this.runUpdate();
|
|
35567
|
+
return;
|
|
35568
|
+
}
|
|
35569
|
+
const p = this.policy;
|
|
35570
|
+
if (!p || !p.autoUpdateDaily) return;
|
|
35251
35571
|
const clock = this.orgClock();
|
|
35252
35572
|
if (!clock) return;
|
|
35253
35573
|
if (clock.dateKey === this.lastDoneDateKey) return;
|
|
@@ -35305,14 +35625,14 @@ var UpdateScheduler = class {
|
|
|
35305
35625
|
};
|
|
35306
35626
|
|
|
35307
35627
|
// src/core/ai-profile-state.ts
|
|
35308
|
-
import { existsSync as
|
|
35309
|
-
import { join as
|
|
35628
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35629
|
+
import { join as join8 } from "path";
|
|
35310
35630
|
function statePath(profileDir2) {
|
|
35311
|
-
return
|
|
35631
|
+
return join8(profileDir2, "ai-profiles.json");
|
|
35312
35632
|
}
|
|
35313
35633
|
function readState(profileDir2) {
|
|
35314
35634
|
const path = statePath(profileDir2);
|
|
35315
|
-
if (!
|
|
35635
|
+
if (!existsSync6(path)) return {};
|
|
35316
35636
|
try {
|
|
35317
35637
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
35318
35638
|
} catch {
|
|
@@ -35370,7 +35690,7 @@ function clearPinnedCredential(profileDir2) {
|
|
|
35370
35690
|
// src/utils/ai-cli.ts
|
|
35371
35691
|
import { readFileSync as readFileSync8 } from "fs";
|
|
35372
35692
|
import { homedir as homedir3 } from "os";
|
|
35373
|
-
import { basename, isAbsolute, join as
|
|
35693
|
+
import { basename, isAbsolute, join as join9 } from "path";
|
|
35374
35694
|
var REQUIRED_AI_ARGS = {
|
|
35375
35695
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
35376
35696
|
codex: ["exec", "--json"],
|
|
@@ -35507,9 +35827,9 @@ function claudeCredentialKeys(config) {
|
|
|
35507
35827
|
return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
|
|
35508
35828
|
}
|
|
35509
35829
|
function resolveHomePath(value) {
|
|
35510
|
-
if (value.startsWith("~")) return
|
|
35830
|
+
if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
|
|
35511
35831
|
if (isAbsolute(value)) return value;
|
|
35512
|
-
return
|
|
35832
|
+
return join9(homedir3(), value);
|
|
35513
35833
|
}
|
|
35514
35834
|
function profileDirs(profiles) {
|
|
35515
35835
|
if (!profiles) return [];
|
|
@@ -35555,11 +35875,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
|
|
|
35555
35875
|
}
|
|
35556
35876
|
function claudeHomeDirs(config) {
|
|
35557
35877
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
35558
|
-
return dirs.length > 0 ? dirs : [
|
|
35878
|
+
return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
|
|
35559
35879
|
}
|
|
35560
35880
|
function profileDisplayLabel(dir2) {
|
|
35561
35881
|
try {
|
|
35562
|
-
const raw = JSON.parse(readFileSync8(
|
|
35882
|
+
const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
|
|
35563
35883
|
const email = raw.oauthAccount?.emailAddress;
|
|
35564
35884
|
if (email && email.trim()) return email;
|
|
35565
35885
|
} catch {
|
|
@@ -35661,11 +35981,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
|
|
|
35661
35981
|
// src/core/claude-usage.ts
|
|
35662
35982
|
import { spawn as spawn3 } from "child_process";
|
|
35663
35983
|
import { readFileSync as readFileSync9 } from "fs";
|
|
35664
|
-
import { join as
|
|
35984
|
+
import { join as join10 } from "path";
|
|
35665
35985
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
35666
35986
|
function readCreds(dir2) {
|
|
35667
35987
|
try {
|
|
35668
|
-
const raw = JSON.parse(readFileSync9(
|
|
35988
|
+
const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
|
|
35669
35989
|
const oauth = raw.claudeAiOauth ?? raw;
|
|
35670
35990
|
return oauth.accessToken ? oauth : null;
|
|
35671
35991
|
} catch {
|
|
@@ -35774,8 +36094,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
|
|
|
35774
36094
|
}
|
|
35775
36095
|
|
|
35776
36096
|
// src/core/command-history.ts
|
|
35777
|
-
import { existsSync as
|
|
35778
|
-
import { join as
|
|
36097
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
36098
|
+
import { join as join11 } from "path";
|
|
35779
36099
|
var MAX_ENTRIES = 1e3;
|
|
35780
36100
|
var filePath = null;
|
|
35781
36101
|
var entries = [];
|
|
@@ -35837,8 +36157,8 @@ async function uploadHistory(cfg) {
|
|
|
35837
36157
|
}
|
|
35838
36158
|
}
|
|
35839
36159
|
function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
|
|
35840
|
-
filePath =
|
|
35841
|
-
if (
|
|
36160
|
+
filePath = join11(profileDir2, "command-history.json");
|
|
36161
|
+
if (existsSync7(filePath)) {
|
|
35842
36162
|
try {
|
|
35843
36163
|
entries = JSON.parse(readFileSync10(filePath, "utf8"));
|
|
35844
36164
|
} catch {
|
|
@@ -35894,16 +36214,16 @@ function finishCommand(commandId, exitCode, usage) {
|
|
|
35894
36214
|
}
|
|
35895
36215
|
|
|
35896
36216
|
// src/core/command-images.ts
|
|
35897
|
-
import { existsSync as
|
|
35898
|
-
import { join as
|
|
36217
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
36218
|
+
import { join as join12 } from "path";
|
|
35899
36219
|
function attachmentsBaseDir(physicRoot) {
|
|
35900
|
-
return
|
|
36220
|
+
return join12(physicRoot, ".4pm", "attachments");
|
|
35901
36221
|
}
|
|
35902
36222
|
var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
35903
36223
|
async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
35904
36224
|
const map = /* @__PURE__ */ new Map();
|
|
35905
36225
|
if (!images.length) return map;
|
|
35906
|
-
const dir2 =
|
|
36226
|
+
const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
|
|
35907
36227
|
mkdirSync3(dir2, { recursive: true });
|
|
35908
36228
|
let n = 0;
|
|
35909
36229
|
for (const img of images) {
|
|
@@ -35915,7 +36235,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
|
35915
36235
|
continue;
|
|
35916
36236
|
}
|
|
35917
36237
|
const ext = commandImageExt(reply.mime ?? img.mime);
|
|
35918
|
-
const file =
|
|
36238
|
+
const file = join12(dir2, `img-${n}.${ext}`);
|
|
35919
36239
|
writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
|
|
35920
36240
|
map.set(img.placeholder, file);
|
|
35921
36241
|
} catch (err) {
|
|
@@ -35937,12 +36257,12 @@ function rewriteImagePlaceholders(prompt, paths) {
|
|
|
35937
36257
|
}
|
|
35938
36258
|
function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
35939
36259
|
const base = attachmentsBaseDir(physicRoot);
|
|
35940
|
-
if (!
|
|
36260
|
+
if (!existsSync8(base)) return 0;
|
|
35941
36261
|
const cutoff = Date.now() - maxAgeMs;
|
|
35942
36262
|
let removed = 0;
|
|
35943
36263
|
try {
|
|
35944
36264
|
for (const entry of readdirSync3(base)) {
|
|
35945
|
-
const dir2 =
|
|
36265
|
+
const dir2 = join12(base, entry);
|
|
35946
36266
|
try {
|
|
35947
36267
|
if (statSync2(dir2).mtimeMs < cutoff) {
|
|
35948
36268
|
rmSync3(dir2, { recursive: true, force: true });
|
|
@@ -35959,18 +36279,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
|
35959
36279
|
// src/core/command-output-store.ts
|
|
35960
36280
|
import {
|
|
35961
36281
|
appendFileSync,
|
|
35962
|
-
existsSync as
|
|
36282
|
+
existsSync as existsSync9,
|
|
35963
36283
|
mkdirSync as mkdirSync4,
|
|
35964
36284
|
readFileSync as readFileSync11,
|
|
35965
36285
|
readdirSync as readdirSync4,
|
|
35966
36286
|
rmSync as rmSync4,
|
|
35967
36287
|
statSync as statSync3
|
|
35968
36288
|
} from "fs";
|
|
35969
|
-
import { join as
|
|
36289
|
+
import { join as join13 } from "path";
|
|
35970
36290
|
var KEEP_FILES = 200;
|
|
35971
36291
|
var dir = null;
|
|
35972
36292
|
function initCommandOutput(profileDir2) {
|
|
35973
|
-
dir =
|
|
36293
|
+
dir = join13(profileDir2, "command-output");
|
|
35974
36294
|
try {
|
|
35975
36295
|
mkdirSync4(dir, { recursive: true });
|
|
35976
36296
|
prune();
|
|
@@ -35980,14 +36300,14 @@ function initCommandOutput(profileDir2) {
|
|
|
35980
36300
|
function appendCommandOutput(commandId, text) {
|
|
35981
36301
|
if (!dir || !text) return;
|
|
35982
36302
|
try {
|
|
35983
|
-
appendFileSync(
|
|
36303
|
+
appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
|
|
35984
36304
|
} catch {
|
|
35985
36305
|
}
|
|
35986
36306
|
}
|
|
35987
36307
|
function readCommandOutput(commandId) {
|
|
35988
36308
|
if (!dir) return null;
|
|
35989
|
-
const path =
|
|
35990
|
-
if (!
|
|
36309
|
+
const path = join13(dir, `${commandId}.log`);
|
|
36310
|
+
if (!existsSync9(path)) return null;
|
|
35991
36311
|
try {
|
|
35992
36312
|
return readFileSync11(path, "utf8");
|
|
35993
36313
|
} catch {
|
|
@@ -35997,8 +36317,8 @@ function readCommandOutput(commandId) {
|
|
|
35997
36317
|
function prune() {
|
|
35998
36318
|
if (!dir) return;
|
|
35999
36319
|
try {
|
|
36000
|
-
const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(
|
|
36001
|
-
for (const { f } of files.slice(KEEP_FILES)) rmSync4(
|
|
36320
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join13(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
|
|
36321
|
+
for (const { f } of files.slice(KEEP_FILES)) rmSync4(join13(dir, f), { force: true });
|
|
36002
36322
|
} catch {
|
|
36003
36323
|
}
|
|
36004
36324
|
}
|
|
@@ -36007,7 +36327,7 @@ function pruneCommandOutputByAge(days) {
|
|
|
36007
36327
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
36008
36328
|
try {
|
|
36009
36329
|
for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
|
|
36010
|
-
const p =
|
|
36330
|
+
const p = join13(dir, f);
|
|
36011
36331
|
if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
|
|
36012
36332
|
}
|
|
36013
36333
|
} catch {
|
|
@@ -36016,12 +36336,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
36016
36336
|
|
|
36017
36337
|
// src/core/support-answer.ts
|
|
36018
36338
|
import { spawn as spawn4 } from "child_process";
|
|
36019
|
-
import { execFile } from "child_process";
|
|
36339
|
+
import { execFile as execFile3 } from "child_process";
|
|
36020
36340
|
import { createHash as createHash3 } from "crypto";
|
|
36021
|
-
import { existsSync as
|
|
36341
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
36022
36342
|
import { randomUUID } from "crypto";
|
|
36023
|
-
import { join as
|
|
36024
|
-
import { promisify } from "util";
|
|
36343
|
+
import { join as join14 } from "path";
|
|
36344
|
+
import { promisify as promisify3 } from "util";
|
|
36025
36345
|
|
|
36026
36346
|
// src/core/ai-stream.ts
|
|
36027
36347
|
function assistantText(ev) {
|
|
@@ -36116,22 +36436,22 @@ function createAiStreamParser(cli) {
|
|
|
36116
36436
|
apiError: () => ({ ...apiError })
|
|
36117
36437
|
};
|
|
36118
36438
|
}
|
|
36119
|
-
function
|
|
36439
|
+
function estimateTokens2(text) {
|
|
36120
36440
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
36121
36441
|
}
|
|
36122
36442
|
|
|
36123
36443
|
// src/core/support-answer.ts
|
|
36124
36444
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
36125
|
-
var execFileAsync =
|
|
36445
|
+
var execFileAsync = promisify3(execFile3);
|
|
36126
36446
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
36127
36447
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
36128
36448
|
var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
36129
36449
|
function kbRoot(profileDir2) {
|
|
36130
|
-
return
|
|
36450
|
+
return join14(profileDir2, "support-kb");
|
|
36131
36451
|
}
|
|
36132
36452
|
function repoDir(profileDir2, url, branch) {
|
|
36133
36453
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
36134
|
-
return
|
|
36454
|
+
return join14(kbRoot(profileDir2), key);
|
|
36135
36455
|
}
|
|
36136
36456
|
function authedUrl(url, token) {
|
|
36137
36457
|
if (!token) return url;
|
|
@@ -36140,7 +36460,7 @@ function authedUrl(url, token) {
|
|
|
36140
36460
|
}
|
|
36141
36461
|
async function ensureRepo(profileDir2, repo) {
|
|
36142
36462
|
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
36143
|
-
if (
|
|
36463
|
+
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
36144
36464
|
const url = authedUrl(repo.url, repo.token);
|
|
36145
36465
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36146
36466
|
mkdirSync5(kbRoot(profileDir2), { recursive: true });
|
|
@@ -36151,11 +36471,11 @@ async function ensureRepo(profileDir2, repo) {
|
|
|
36151
36471
|
}
|
|
36152
36472
|
async function refreshSupportKb(profileDir2) {
|
|
36153
36473
|
const root = kbRoot(profileDir2);
|
|
36154
|
-
if (!
|
|
36474
|
+
if (!existsSync10(root)) return;
|
|
36155
36475
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36156
36476
|
for (const entry of readdirSync5(root)) {
|
|
36157
|
-
const dir2 =
|
|
36158
|
-
if (!
|
|
36477
|
+
const dir2 = join14(root, entry);
|
|
36478
|
+
if (!existsSync10(join14(dir2, ".git"))) continue;
|
|
36159
36479
|
try {
|
|
36160
36480
|
await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
|
|
36161
36481
|
} catch (err) {
|
|
@@ -36170,7 +36490,7 @@ function collectDocs(dir2) {
|
|
|
36170
36490
|
if (total >= MAX_CONTEXT_CHARS) return;
|
|
36171
36491
|
for (const entry of readdirSync5(current2)) {
|
|
36172
36492
|
if (entry === ".git" || entry === "node_modules") continue;
|
|
36173
|
-
const full =
|
|
36493
|
+
const full = join14(current2, entry);
|
|
36174
36494
|
const st = statSync4(full);
|
|
36175
36495
|
if (st.isDirectory()) {
|
|
36176
36496
|
walk2(full);
|
|
@@ -36235,11 +36555,11 @@ function imageExt(mime) {
|
|
|
36235
36555
|
}
|
|
36236
36556
|
}
|
|
36237
36557
|
function materializeImages2(profileDir2, images, question) {
|
|
36238
|
-
const dir2 =
|
|
36558
|
+
const dir2 = join14(profileDir2, "help-images", randomUUID());
|
|
36239
36559
|
mkdirSync5(dir2, { recursive: true });
|
|
36240
36560
|
let rewritten = question;
|
|
36241
36561
|
images.forEach((img, i) => {
|
|
36242
|
-
const file =
|
|
36562
|
+
const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
|
|
36243
36563
|
writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
|
|
36244
36564
|
if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
|
|
36245
36565
|
});
|
|
@@ -36334,7 +36654,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
36334
36654
|
if (!text) return { body: "", error: "empty answer" };
|
|
36335
36655
|
const { body, moderation } = parseModeratedAnswer(text);
|
|
36336
36656
|
if (!body) return { body: "", error: "empty answer" };
|
|
36337
|
-
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
36657
|
+
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
|
|
36338
36658
|
return {
|
|
36339
36659
|
body,
|
|
36340
36660
|
...moderation ? { moderation } : {},
|
|
@@ -36381,12 +36701,12 @@ function reportToolResult(tool, ok, message) {
|
|
|
36381
36701
|
}
|
|
36382
36702
|
|
|
36383
36703
|
// src/core/autonomous.ts
|
|
36384
|
-
import { readFile as readFile2, writeFile, chmod } from "fs/promises";
|
|
36704
|
+
import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
|
|
36385
36705
|
import { readFileSync as readFileSync13 } from "fs";
|
|
36386
|
-
import { execFile as
|
|
36387
|
-
import { join as
|
|
36388
|
-
import { promisify as
|
|
36389
|
-
var
|
|
36706
|
+
import { execFile as execFile4, spawn as spawn5 } from "child_process";
|
|
36707
|
+
import { join as join15 } from "path";
|
|
36708
|
+
import { promisify as promisify4 } from "util";
|
|
36709
|
+
var run4 = promisify4(execFile4);
|
|
36390
36710
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
36391
36711
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
36392
36712
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
@@ -36419,11 +36739,11 @@ function tableCell(value) {
|
|
|
36419
36739
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
36420
36740
|
}
|
|
36421
36741
|
function tickScript(root) {
|
|
36422
|
-
return
|
|
36742
|
+
return join15(root, TICK_REL);
|
|
36423
36743
|
}
|
|
36424
36744
|
async function crontabList() {
|
|
36425
36745
|
try {
|
|
36426
|
-
const { stdout } = await
|
|
36746
|
+
const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
|
|
36427
36747
|
return stdout;
|
|
36428
36748
|
} catch {
|
|
36429
36749
|
return "";
|
|
@@ -36442,8 +36762,8 @@ async function cronInstalled(root) {
|
|
|
36442
36762
|
return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
|
|
36443
36763
|
}
|
|
36444
36764
|
async function getAutonomousStatus(root) {
|
|
36445
|
-
const settings = parseJson(await readText(
|
|
36446
|
-
const hist = parseJson(await readText(
|
|
36765
|
+
const settings = parseJson(await readText(join15(root, SETTINGS_REL))) ?? {};
|
|
36766
|
+
const hist = parseJson(await readText(join15(root, HISTORIES_REL))) ?? {};
|
|
36447
36767
|
const records = Array.isArray(hist.records) ? hist.records : [];
|
|
36448
36768
|
const last = records[records.length - 1];
|
|
36449
36769
|
const ticks = hist.ticks ?? {};
|
|
@@ -36460,9 +36780,9 @@ async function getAutonomousStatus(root) {
|
|
|
36460
36780
|
}
|
|
36461
36781
|
function isAutonomousRunning(root) {
|
|
36462
36782
|
try {
|
|
36463
|
-
const s = JSON.parse(readFileSync13(
|
|
36783
|
+
const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
|
|
36464
36784
|
if (s.paused === true) return false;
|
|
36465
|
-
const h = JSON.parse(readFileSync13(
|
|
36785
|
+
const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
|
|
36466
36786
|
const ts = h.records?.[h.records.length - 1]?.ts;
|
|
36467
36787
|
if (!ts) return false;
|
|
36468
36788
|
const t2 = Date.parse(ts.replace(" ", "T"));
|
|
@@ -36473,10 +36793,10 @@ function isAutonomousRunning(root) {
|
|
|
36473
36793
|
}
|
|
36474
36794
|
async function readAutonomous(root) {
|
|
36475
36795
|
const [settings, approvals, status, ...books] = await Promise.all([
|
|
36476
|
-
readText(
|
|
36477
|
-
readText(
|
|
36796
|
+
readText(join15(root, SETTINGS_REL)),
|
|
36797
|
+
readText(join15(root, APPROVALS_REL), "{}"),
|
|
36478
36798
|
getAutonomousStatus(root),
|
|
36479
|
-
...Object.values(BOOK_FILES).map((f) => readText(
|
|
36799
|
+
...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
|
|
36480
36800
|
]);
|
|
36481
36801
|
const keys = Object.keys(BOOK_FILES);
|
|
36482
36802
|
const bookMap = {};
|
|
@@ -36485,7 +36805,7 @@ async function readAutonomous(root) {
|
|
|
36485
36805
|
}
|
|
36486
36806
|
async function readAutonomousLogs(root, date) {
|
|
36487
36807
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
36488
|
-
const text = await readText(
|
|
36808
|
+
const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
36489
36809
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
36490
36810
|
return { date: day, lines };
|
|
36491
36811
|
}
|
|
@@ -36509,22 +36829,22 @@ async function writeAutonomous(root, req, by) {
|
|
|
36509
36829
|
switch (req.kind) {
|
|
36510
36830
|
case "settings": {
|
|
36511
36831
|
if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
|
|
36512
|
-
await
|
|
36832
|
+
await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
|
|
36513
36833
|
break;
|
|
36514
36834
|
}
|
|
36515
36835
|
case "approvals": {
|
|
36516
|
-
const map = parseJson(await readText(
|
|
36836
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
36517
36837
|
if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36518
36838
|
else delete map[req.taskId];
|
|
36519
|
-
await
|
|
36839
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
36520
36840
|
break;
|
|
36521
36841
|
}
|
|
36522
36842
|
case "userTodo": {
|
|
36523
|
-
const cur = await readText(
|
|
36843
|
+
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
36524
36844
|
const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
36525
36845
|
const row = `| | ${tableCell(req.content)} | ${trace} |
|
|
36526
36846
|
`;
|
|
36527
|
-
await
|
|
36847
|
+
await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
36528
36848
|
break;
|
|
36529
36849
|
}
|
|
36530
36850
|
case "cron": {
|
|
@@ -36540,7 +36860,7 @@ async function writeAutonomous(root, req, by) {
|
|
|
36540
36860
|
}
|
|
36541
36861
|
|
|
36542
36862
|
// src/core/network-probe.ts
|
|
36543
|
-
import { existsSync as
|
|
36863
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
36544
36864
|
import { connect } from "net";
|
|
36545
36865
|
var CANARY_TARGETS = [
|
|
36546
36866
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -36549,7 +36869,7 @@ var CANARY_TARGETS = [
|
|
|
36549
36869
|
var PROBE_TIMEOUT_MS = 2500;
|
|
36550
36870
|
function detectContainerized() {
|
|
36551
36871
|
try {
|
|
36552
|
-
if (
|
|
36872
|
+
if (existsSync11("/.dockerenv")) return true;
|
|
36553
36873
|
const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
|
|
36554
36874
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
36555
36875
|
} catch {
|
|
@@ -36619,7 +36939,7 @@ async function probeNetwork() {
|
|
|
36619
36939
|
// src/core/git-auth.ts
|
|
36620
36940
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
36621
36941
|
import { homedir as homedir4 } from "os";
|
|
36622
|
-
import { join as
|
|
36942
|
+
import { join as join16 } from "path";
|
|
36623
36943
|
function tokenTarget(method) {
|
|
36624
36944
|
if (method === "gitlab-group-token") {
|
|
36625
36945
|
return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
|
|
@@ -36637,7 +36957,7 @@ function applyGitAuth(method) {
|
|
|
36637
36957
|
try {
|
|
36638
36958
|
const line = `https://${target.user}:${token}@${target.host}
|
|
36639
36959
|
`;
|
|
36640
|
-
writeFileSync8(
|
|
36960
|
+
writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
|
|
36641
36961
|
return true;
|
|
36642
36962
|
} catch {
|
|
36643
36963
|
return false;
|
|
@@ -36822,7 +37142,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
36822
37142
|
const apiErr = parser.apiError();
|
|
36823
37143
|
if (finalExit === 0 && !apiErr.isError) {
|
|
36824
37144
|
const usage = parser.usage();
|
|
36825
|
-
if (usage.tokens === 0) usage.tokens =
|
|
37145
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
|
|
36826
37146
|
reportToolResult(attempt.cmd, true);
|
|
36827
37147
|
return {
|
|
36828
37148
|
exitCode: 0,
|
|
@@ -36874,8 +37194,8 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
36874
37194
|
}
|
|
36875
37195
|
|
|
36876
37196
|
// src/ui/slash-commands.ts
|
|
36877
|
-
import { existsSync as
|
|
36878
|
-
import { join as
|
|
37197
|
+
import { existsSync as existsSync12 } from "fs";
|
|
37198
|
+
import { join as join17 } from "path";
|
|
36879
37199
|
var CONFIG_KEYS = [
|
|
36880
37200
|
"aiCli",
|
|
36881
37201
|
"claudeHome",
|
|
@@ -36887,7 +37207,7 @@ var CONFIG_KEYS = [
|
|
|
36887
37207
|
"aiEnv"
|
|
36888
37208
|
];
|
|
36889
37209
|
function configPath(profileDir2) {
|
|
36890
|
-
return
|
|
37210
|
+
return join17(profileDir2, "config.json");
|
|
36891
37211
|
}
|
|
36892
37212
|
function parseConfigAssignment(key, valueTokens) {
|
|
36893
37213
|
const value = valueTokens.join(" ").trim();
|
|
@@ -36937,7 +37257,7 @@ function runConfig(ctx) {
|
|
|
36937
37257
|
ctx.print(t("slash.config.wroteDefault", { verb, path }));
|
|
36938
37258
|
ctx.print(t("slash.config.addClaudeHint"));
|
|
36939
37259
|
};
|
|
36940
|
-
if (
|
|
37260
|
+
if (existsSync12(path)) {
|
|
36941
37261
|
ctx.confirm(
|
|
36942
37262
|
t("slash.config.replaceConfirm", { path }),
|
|
36943
37263
|
() => writeDefaults(t("slash.config.verbReplaced"))
|
|
@@ -37022,7 +37342,7 @@ function runOutput(ctx) {
|
|
|
37022
37342
|
}
|
|
37023
37343
|
function runLogs(ctx) {
|
|
37024
37344
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
37025
|
-
const dir2 =
|
|
37345
|
+
const dir2 = join17(ctx.info.profileDir, "logs");
|
|
37026
37346
|
const lines = readRecentLogLines(dir2, n);
|
|
37027
37347
|
if (lines.length === 0) {
|
|
37028
37348
|
ctx.print(t("slash.logs.none"));
|
|
@@ -37337,13 +37657,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
|
|
|
37337
37657
|
}
|
|
37338
37658
|
|
|
37339
37659
|
// src/core/git-commit-identity.ts
|
|
37340
|
-
import { execFile as
|
|
37341
|
-
import { promisify as
|
|
37342
|
-
var
|
|
37660
|
+
import { execFile as execFile5 } from "child_process";
|
|
37661
|
+
import { promisify as promisify5 } from "util";
|
|
37662
|
+
var run5 = promisify5(execFile5);
|
|
37343
37663
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
37344
37664
|
if (!machineUsername) return;
|
|
37345
37665
|
try {
|
|
37346
|
-
await
|
|
37666
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
37347
37667
|
} catch {
|
|
37348
37668
|
}
|
|
37349
37669
|
}
|
|
@@ -37442,7 +37762,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
37442
37762
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
37443
37763
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
37444
37764
|
if (perPromptLimit > 0) {
|
|
37445
|
-
const estimated =
|
|
37765
|
+
const estimated = estimateTokens2(prompt);
|
|
37446
37766
|
if (estimated > perPromptLimit) {
|
|
37447
37767
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
37448
37768
|
return;
|
|
@@ -37859,15 +38179,15 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
37859
38179
|
}
|
|
37860
38180
|
|
|
37861
38181
|
// src/core/fs-browse.ts
|
|
37862
|
-
import { readdir as
|
|
37863
|
-
import { resolve, sep as sep2 } from "path";
|
|
38182
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
38183
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
37864
38184
|
async function listDir(path, root) {
|
|
37865
38185
|
if (!root) return { path: "", entries: [] };
|
|
37866
|
-
const base =
|
|
37867
|
-
const requested =
|
|
38186
|
+
const base = resolve2(root);
|
|
38187
|
+
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
37868
38188
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
37869
38189
|
try {
|
|
37870
|
-
const dirents = await
|
|
38190
|
+
const dirents = await readdir3(target, { withFileTypes: true });
|
|
37871
38191
|
const entries2 = dirents.map((d) => ({
|
|
37872
38192
|
name: d.name,
|
|
37873
38193
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -37883,12 +38203,12 @@ async function listDir(path, root) {
|
|
|
37883
38203
|
|
|
37884
38204
|
// src/core/fs-read.ts
|
|
37885
38205
|
import { readFile as readFile3 } from "fs/promises";
|
|
37886
|
-
import { resolve as
|
|
38206
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
37887
38207
|
var MAX_BYTES = 256 * 1024;
|
|
37888
38208
|
function resolveInRoot(root, relPath) {
|
|
37889
38209
|
if (!root) return null;
|
|
37890
|
-
const base =
|
|
37891
|
-
const target =
|
|
38210
|
+
const base = resolve3(root);
|
|
38211
|
+
const target = resolve3(base, relPath || ".");
|
|
37892
38212
|
return target === base || target.startsWith(base + sep3) ? target : null;
|
|
37893
38213
|
}
|
|
37894
38214
|
async function readWorkerFile(root, path) {
|
|
@@ -37907,13 +38227,13 @@ async function readWorkerFile(root, path) {
|
|
|
37907
38227
|
}
|
|
37908
38228
|
|
|
37909
38229
|
// src/core/fs-write.ts
|
|
37910
|
-
import { mkdir as
|
|
37911
|
-
import { dirname as
|
|
38230
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
38231
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
37912
38232
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
37913
38233
|
function resolveInRoot2(root, relPath) {
|
|
37914
38234
|
if (!root) return null;
|
|
37915
|
-
const base =
|
|
37916
|
-
const target =
|
|
38235
|
+
const base = resolve4(root);
|
|
38236
|
+
const target = resolve4(base, relPath || ".");
|
|
37917
38237
|
return target === base || target.startsWith(base + sep4) ? target : null;
|
|
37918
38238
|
}
|
|
37919
38239
|
async function writeWorkerFile(root, relPath, content) {
|
|
@@ -37926,8 +38246,8 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37926
38246
|
return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
|
|
37927
38247
|
}
|
|
37928
38248
|
try {
|
|
37929
|
-
await
|
|
37930
|
-
await
|
|
38249
|
+
await mkdir3(dirname4(target), { recursive: true });
|
|
38250
|
+
await writeFile3(target, content, "utf8");
|
|
37931
38251
|
return { ok: true, path: target, bytes };
|
|
37932
38252
|
} catch (err) {
|
|
37933
38253
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -37935,12 +38255,12 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37935
38255
|
}
|
|
37936
38256
|
|
|
37937
38257
|
// src/core/fs-mutate.ts
|
|
37938
|
-
import { mkdir as
|
|
37939
|
-
import { basename as basename3, dirname as
|
|
38258
|
+
import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
|
|
38259
|
+
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
37940
38260
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
37941
38261
|
function resolveInRoot3(root, rel) {
|
|
37942
|
-
const base =
|
|
37943
|
-
const target =
|
|
38262
|
+
const base = resolve5(root);
|
|
38263
|
+
const target = resolve5(base, rel || ".");
|
|
37944
38264
|
return target === base || target.startsWith(base + sep5) ? target : null;
|
|
37945
38265
|
}
|
|
37946
38266
|
async function isDir(p) {
|
|
@@ -37965,39 +38285,39 @@ async function mutateFs(root, req) {
|
|
|
37965
38285
|
switch (req.op) {
|
|
37966
38286
|
case "mkdir": {
|
|
37967
38287
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37968
|
-
if (!target || target ===
|
|
37969
|
-
await
|
|
38288
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
38289
|
+
await mkdir4(target, { recursive: true });
|
|
37970
38290
|
return { ok: true, path: target };
|
|
37971
38291
|
}
|
|
37972
38292
|
case "create": {
|
|
37973
38293
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37974
|
-
if (!target || target ===
|
|
38294
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
37975
38295
|
if (await exists(target)) return { ok: false, path: target, error: "already exists" };
|
|
37976
38296
|
const content = req.content ?? "";
|
|
37977
38297
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
|
|
37978
38298
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
37979
38299
|
}
|
|
37980
|
-
await
|
|
37981
|
-
await
|
|
38300
|
+
await mkdir4(dirname5(target), { recursive: true });
|
|
38301
|
+
await writeFile4(target, content, "utf8");
|
|
37982
38302
|
return { ok: true, path: target };
|
|
37983
38303
|
}
|
|
37984
38304
|
case "move": {
|
|
37985
38305
|
const from = resolveInRoot3(root, req.from ?? "");
|
|
37986
38306
|
let to = resolveInRoot3(root, req.to ?? "");
|
|
37987
38307
|
if (!from || !to) return escaped(req.from ?? req.to ?? "");
|
|
37988
|
-
if (from ===
|
|
37989
|
-
if (await isDir(to)) to =
|
|
37990
|
-
if (to !==
|
|
38308
|
+
if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
|
|
38309
|
+
if (await isDir(to)) to = resolve5(to, basename3(from));
|
|
38310
|
+
if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
37991
38311
|
if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
|
|
37992
|
-
await
|
|
37993
|
-
await
|
|
38312
|
+
await mkdir4(dirname5(to), { recursive: true });
|
|
38313
|
+
await rename2(from, to);
|
|
37994
38314
|
return { ok: true, path: to };
|
|
37995
38315
|
}
|
|
37996
38316
|
case "delete": {
|
|
37997
38317
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37998
38318
|
if (!target) return escaped(req.path ?? "");
|
|
37999
|
-
if (target ===
|
|
38000
|
-
await
|
|
38319
|
+
if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
38320
|
+
await rm2(target, { recursive: true, force: true });
|
|
38001
38321
|
return { ok: true, path: target };
|
|
38002
38322
|
}
|
|
38003
38323
|
default:
|
|
@@ -38009,12 +38329,12 @@ async function mutateFs(root, req) {
|
|
|
38009
38329
|
}
|
|
38010
38330
|
|
|
38011
38331
|
// src/core/fs-transfer.ts
|
|
38012
|
-
import { mkdir as
|
|
38013
|
-
import { basename as basename4, dirname as
|
|
38332
|
+
import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
38333
|
+
import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
|
|
38014
38334
|
function resolveInRoot4(root, relPath) {
|
|
38015
38335
|
if (!root) return null;
|
|
38016
|
-
const base =
|
|
38017
|
-
const target =
|
|
38336
|
+
const base = resolve6(root);
|
|
38337
|
+
const target = resolve6(base, relPath || ".");
|
|
38018
38338
|
return target === base || target.startsWith(base + sep6) ? target : null;
|
|
38019
38339
|
}
|
|
38020
38340
|
var MIME_BY_EXT = {
|
|
@@ -38053,8 +38373,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
38053
38373
|
return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
38054
38374
|
}
|
|
38055
38375
|
try {
|
|
38056
|
-
await
|
|
38057
|
-
await
|
|
38376
|
+
await mkdir5(dirname6(target), { recursive: true });
|
|
38377
|
+
await writeFile5(target, buf);
|
|
38058
38378
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
38059
38379
|
} catch (err) {
|
|
38060
38380
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -38129,14 +38449,14 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
38129
38449
|
|
|
38130
38450
|
// src/core/git-diff.ts
|
|
38131
38451
|
import { readFile as readFile5 } from "fs/promises";
|
|
38132
|
-
import { execFile as
|
|
38133
|
-
import { basename as basename5, dirname as
|
|
38134
|
-
import { promisify as
|
|
38135
|
-
var
|
|
38452
|
+
import { execFile as execFile6 } from "child_process";
|
|
38453
|
+
import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
|
|
38454
|
+
import { promisify as promisify6 } from "util";
|
|
38455
|
+
var run6 = promisify6(execFile6);
|
|
38136
38456
|
function resolveInRoot5(root, relPath) {
|
|
38137
38457
|
if (!root) return null;
|
|
38138
|
-
const base =
|
|
38139
|
-
const target =
|
|
38458
|
+
const base = resolve7(root);
|
|
38459
|
+
const target = resolve7(base, relPath || ".");
|
|
38140
38460
|
return target === base || target.startsWith(base + sep7) ? target : null;
|
|
38141
38461
|
}
|
|
38142
38462
|
function looksBinary(buf) {
|
|
@@ -38151,7 +38471,7 @@ async function gitDiff(root, path) {
|
|
|
38151
38471
|
if (!target) {
|
|
38152
38472
|
return { path, oldContent: "", newContent: "", isBinary: false };
|
|
38153
38473
|
}
|
|
38154
|
-
const dir2 =
|
|
38474
|
+
const dir2 = dirname7(target);
|
|
38155
38475
|
const base = basename5(target);
|
|
38156
38476
|
let newBuf = null;
|
|
38157
38477
|
try {
|
|
@@ -38160,7 +38480,7 @@ async function gitDiff(root, path) {
|
|
|
38160
38480
|
}
|
|
38161
38481
|
let oldBuf = null;
|
|
38162
38482
|
try {
|
|
38163
|
-
const { stdout } = await
|
|
38483
|
+
const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
|
|
38164
38484
|
cwd: dir2,
|
|
38165
38485
|
timeout: 15e3,
|
|
38166
38486
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -38190,12 +38510,12 @@ async function gitDiff(root, path) {
|
|
|
38190
38510
|
}
|
|
38191
38511
|
|
|
38192
38512
|
// src/core/git-env.ts
|
|
38193
|
-
import { execFile as
|
|
38194
|
-
import { promisify as
|
|
38195
|
-
var
|
|
38513
|
+
import { execFile as execFile7 } from "child_process";
|
|
38514
|
+
import { promisify as promisify7 } from "util";
|
|
38515
|
+
var run7 = promisify7(execFile7);
|
|
38196
38516
|
async function hasBinary(bin, args = ["--version"]) {
|
|
38197
38517
|
try {
|
|
38198
|
-
await
|
|
38518
|
+
await run7(bin, args, { timeout: 5e3 });
|
|
38199
38519
|
return true;
|
|
38200
38520
|
} catch {
|
|
38201
38521
|
return false;
|
|
@@ -38208,7 +38528,7 @@ async function checkGitEnv(provider) {
|
|
|
38208
38528
|
let account = null;
|
|
38209
38529
|
if (installed) {
|
|
38210
38530
|
try {
|
|
38211
|
-
const { stdout, stderr } = await
|
|
38531
|
+
const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
|
|
38212
38532
|
const out = `${stdout}
|
|
38213
38533
|
${stderr}`;
|
|
38214
38534
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -38221,14 +38541,14 @@ ${stderr}`;
|
|
|
38221
38541
|
}
|
|
38222
38542
|
|
|
38223
38543
|
// src/core/git-ssh-key.ts
|
|
38224
|
-
import { execFile as
|
|
38225
|
-
import { readFile as readFile6, rm as
|
|
38226
|
-
import { join as
|
|
38227
|
-
import { promisify as
|
|
38228
|
-
var
|
|
38544
|
+
import { execFile as execFile8 } from "child_process";
|
|
38545
|
+
import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
|
|
38546
|
+
import { join as join18 } from "path";
|
|
38547
|
+
import { promisify as promisify8 } from "util";
|
|
38548
|
+
var run8 = promisify8(execFile8);
|
|
38229
38549
|
function keyPaths(profileDir2) {
|
|
38230
|
-
const dir2 =
|
|
38231
|
-
const key =
|
|
38550
|
+
const dir2 = join18(profileDir2, ".ssh");
|
|
38551
|
+
const key = join18(dir2, "id_4pm");
|
|
38232
38552
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
38233
38553
|
}
|
|
38234
38554
|
async function readPublicKey(pub) {
|
|
@@ -38240,14 +38560,14 @@ async function readPublicKey(pub) {
|
|
|
38240
38560
|
}
|
|
38241
38561
|
async function fingerprintOf(pub) {
|
|
38242
38562
|
try {
|
|
38243
|
-
const { stdout } = await
|
|
38563
|
+
const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
38244
38564
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
38245
38565
|
} catch {
|
|
38246
38566
|
return null;
|
|
38247
38567
|
}
|
|
38248
38568
|
}
|
|
38249
38569
|
async function configureGit(key) {
|
|
38250
|
-
await
|
|
38570
|
+
await run8(
|
|
38251
38571
|
"git",
|
|
38252
38572
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
38253
38573
|
{ timeout: 5e3 }
|
|
@@ -38255,10 +38575,10 @@ async function configureGit(key) {
|
|
|
38255
38575
|
}
|
|
38256
38576
|
async function generate(profileDir2) {
|
|
38257
38577
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
38258
|
-
await
|
|
38259
|
-
await
|
|
38260
|
-
await
|
|
38261
|
-
await
|
|
38578
|
+
await mkdir6(dir2, { recursive: true });
|
|
38579
|
+
await rm3(key, { force: true });
|
|
38580
|
+
await rm3(pub, { force: true });
|
|
38581
|
+
await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
38262
38582
|
timeout: 15e3
|
|
38263
38583
|
});
|
|
38264
38584
|
await configureGit(key);
|
|
@@ -38266,9 +38586,9 @@ async function generate(profileDir2) {
|
|
|
38266
38586
|
}
|
|
38267
38587
|
async function remove(profileDir2) {
|
|
38268
38588
|
const { key, pub } = keyPaths(profileDir2);
|
|
38269
|
-
await
|
|
38270
|
-
await
|
|
38271
|
-
await
|
|
38589
|
+
await rm3(key, { force: true });
|
|
38590
|
+
await rm3(pub, { force: true });
|
|
38591
|
+
await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
38272
38592
|
() => void 0
|
|
38273
38593
|
);
|
|
38274
38594
|
return { publicKey: null, fingerprint: null };
|
|
@@ -38289,22 +38609,22 @@ async function manageSshKey(op, profileDir2) {
|
|
|
38289
38609
|
}
|
|
38290
38610
|
|
|
38291
38611
|
// src/core/git-history.ts
|
|
38292
|
-
import { readdir as
|
|
38293
|
-
import { execFile as
|
|
38294
|
-
import { resolve as
|
|
38295
|
-
import { promisify as
|
|
38296
|
-
var
|
|
38612
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
38613
|
+
import { execFile as execFile9 } from "child_process";
|
|
38614
|
+
import { resolve as resolve8, sep as sep8 } from "path";
|
|
38615
|
+
import { promisify as promisify9 } from "util";
|
|
38616
|
+
var run9 = promisify9(execFile9);
|
|
38297
38617
|
var FS = "";
|
|
38298
38618
|
var RS = "";
|
|
38299
38619
|
function resolveRepo(root, repo) {
|
|
38300
38620
|
if (!root) return null;
|
|
38301
|
-
const base =
|
|
38302
|
-
const requested =
|
|
38621
|
+
const base = resolve8(root);
|
|
38622
|
+
const requested = resolve8(base, repo && repo.trim() ? repo : ".");
|
|
38303
38623
|
return requested === base || requested.startsWith(base + sep8) ? requested : base;
|
|
38304
38624
|
}
|
|
38305
38625
|
async function git(cwd2, args) {
|
|
38306
38626
|
try {
|
|
38307
|
-
const { stdout } = await
|
|
38627
|
+
const { stdout } = await run9("git", args, {
|
|
38308
38628
|
cwd: cwd2,
|
|
38309
38629
|
timeout: 15e3,
|
|
38310
38630
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -38316,7 +38636,7 @@ async function git(cwd2, args) {
|
|
|
38316
38636
|
}
|
|
38317
38637
|
async function gitRepos(root) {
|
|
38318
38638
|
if (!root) return { repos: [] };
|
|
38319
|
-
const base =
|
|
38639
|
+
const base = resolve8(root);
|
|
38320
38640
|
const found = /* @__PURE__ */ new Set();
|
|
38321
38641
|
const repos = [];
|
|
38322
38642
|
const add = async (dir2) => {
|
|
@@ -38330,16 +38650,16 @@ async function gitRepos(root) {
|
|
|
38330
38650
|
};
|
|
38331
38651
|
await add(base);
|
|
38332
38652
|
try {
|
|
38333
|
-
const lvl1 = await
|
|
38653
|
+
const lvl1 = await readdir4(base, { withFileTypes: true });
|
|
38334
38654
|
for (const d of lvl1) {
|
|
38335
38655
|
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
38336
|
-
const p1 =
|
|
38656
|
+
const p1 = resolve8(base, d.name);
|
|
38337
38657
|
await add(p1);
|
|
38338
38658
|
try {
|
|
38339
|
-
const lvl2 = await
|
|
38659
|
+
const lvl2 = await readdir4(p1, { withFileTypes: true });
|
|
38340
38660
|
for (const d2 of lvl2) {
|
|
38341
38661
|
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
38342
|
-
await add(
|
|
38662
|
+
await add(resolve8(p1, d2.name));
|
|
38343
38663
|
}
|
|
38344
38664
|
} catch {
|
|
38345
38665
|
}
|
|
@@ -38457,79 +38777,7 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
38457
38777
|
}
|
|
38458
38778
|
|
|
38459
38779
|
// src/core/ws-client/handlers/misc.ts
|
|
38460
|
-
import { join as
|
|
38461
|
-
|
|
38462
|
-
// src/core/ai-assist.ts
|
|
38463
|
-
import { execFile as execFile8 } from "child_process";
|
|
38464
|
-
import { promisify as promisify8 } from "util";
|
|
38465
|
-
var run8 = promisify8(execFile8);
|
|
38466
|
-
function aiCli() {
|
|
38467
|
-
return process.env.AI_CLI || "claude";
|
|
38468
|
-
}
|
|
38469
|
-
function aiArgs(cli, prompt) {
|
|
38470
|
-
if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
|
|
38471
|
-
if (cli === "codex") return ["exec", "--json", prompt];
|
|
38472
|
-
return ["-p", prompt];
|
|
38473
|
-
}
|
|
38474
|
-
function estimateTokens2(prompt, output) {
|
|
38475
|
-
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
38476
|
-
}
|
|
38477
|
-
function parseAi(cli, stdout, prompt) {
|
|
38478
|
-
if (cli === "claude") {
|
|
38479
|
-
try {
|
|
38480
|
-
const j = JSON.parse(stdout);
|
|
38481
|
-
const text2 = (j.result ?? "").trim();
|
|
38482
|
-
const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
|
|
38483
|
-
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
|
|
38484
|
-
} catch {
|
|
38485
|
-
}
|
|
38486
|
-
}
|
|
38487
|
-
if (cli === "codex") {
|
|
38488
|
-
try {
|
|
38489
|
-
let text2 = "";
|
|
38490
|
-
let tokens = 0;
|
|
38491
|
-
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
38492
|
-
const obj = JSON.parse(line);
|
|
38493
|
-
if (obj.text) text2 = obj.text;
|
|
38494
|
-
else if (obj.message) text2 = obj.message;
|
|
38495
|
-
if (obj.usage) {
|
|
38496
|
-
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
38497
|
-
}
|
|
38498
|
-
}
|
|
38499
|
-
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
|
|
38500
|
-
} catch {
|
|
38501
|
-
}
|
|
38502
|
-
}
|
|
38503
|
-
const text = stdout.trim();
|
|
38504
|
-
return { text, tokens: estimateTokens2(prompt, text) };
|
|
38505
|
-
}
|
|
38506
|
-
async function runAi(prompt) {
|
|
38507
|
-
const cli = aiCli();
|
|
38508
|
-
const { stdout } = await run8(cli, aiArgs(cli, prompt), {
|
|
38509
|
-
timeout: 12e4,
|
|
38510
|
-
maxBuffer: 8 * 1024 * 1024
|
|
38511
|
-
});
|
|
38512
|
-
return parseAi(cli, stdout, prompt);
|
|
38513
|
-
}
|
|
38514
|
-
async function aiGenerate(prompt) {
|
|
38515
|
-
const { text } = await runAi(prompt);
|
|
38516
|
-
return text;
|
|
38517
|
-
}
|
|
38518
|
-
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
38519
|
-
const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
|
|
38520
|
-
${input}`;
|
|
38521
|
-
const { text, tokens } = await runAi(prompt);
|
|
38522
|
-
const start = text.indexOf("{");
|
|
38523
|
-
const end = text.lastIndexOf("}");
|
|
38524
|
-
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
38525
|
-
try {
|
|
38526
|
-
const j = JSON.parse(text.slice(start, end + 1));
|
|
38527
|
-
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
38528
|
-
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
38529
|
-
} catch {
|
|
38530
|
-
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
38531
|
-
}
|
|
38532
|
-
}
|
|
38780
|
+
import { join as join19 } from "path";
|
|
38533
38781
|
|
|
38534
38782
|
// src/core/outbound-review.ts
|
|
38535
38783
|
function compilePatterns(sources, fallback) {
|
|
@@ -38605,9 +38853,13 @@ async function evaluateReview(payload) {
|
|
|
38605
38853
|
// src/core/ws-client/handlers/misc.ts
|
|
38606
38854
|
function handleMiscChannels(ctx, message, payload) {
|
|
38607
38855
|
switch (message.channel) {
|
|
38856
|
+
case WsChannels.CLI_UPDATE: {
|
|
38857
|
+
ctx.updateCliNow();
|
|
38858
|
+
return true;
|
|
38859
|
+
}
|
|
38608
38860
|
case WsChannels.LOG_READ: {
|
|
38609
38861
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
38610
|
-
const lines = readRecentLogLines(
|
|
38862
|
+
const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
|
|
38611
38863
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
38612
38864
|
return true;
|
|
38613
38865
|
}
|
|
@@ -38640,161 +38892,6 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
38640
38892
|
// src/core/ws-client/handlers/project.ts
|
|
38641
38893
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
|
|
38642
38894
|
import { join as join20 } from "path";
|
|
38643
|
-
|
|
38644
|
-
// src/core/scaffold.ts
|
|
38645
|
-
import { cp, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
|
|
38646
|
-
import { existsSync as existsSync12 } from "fs";
|
|
38647
|
-
import { execFile as execFile9 } from "child_process";
|
|
38648
|
-
import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
|
|
38649
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
38650
|
-
import { promisify as promisify9 } from "util";
|
|
38651
|
-
var run9 = promisify9(execFile9);
|
|
38652
|
-
function reposOf(spec) {
|
|
38653
|
-
const repos = spec?.repos;
|
|
38654
|
-
return Array.isArray(repos) ? repos : [];
|
|
38655
|
-
}
|
|
38656
|
-
function subDirName(repo) {
|
|
38657
|
-
const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
|
|
38658
|
-
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
38659
|
-
}
|
|
38660
|
-
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
38661
|
-
for (const repo of repos) {
|
|
38662
|
-
if (repo.primary) {
|
|
38663
|
-
if (opts.clonePrimary && repo.url) {
|
|
38664
|
-
if (existsSync12(join19(target, ".git"))) {
|
|
38665
|
-
emit("git", `Primary repo already present \u2014 skipping clone.`);
|
|
38666
|
-
continue;
|
|
38667
|
-
}
|
|
38668
|
-
emit("git", `Cloning ${repo.url}\u2026`);
|
|
38669
|
-
await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
38670
|
-
continue;
|
|
38671
|
-
}
|
|
38672
|
-
emit("git", "Initializing the primary repo\u2026");
|
|
38673
|
-
await run9("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
38674
|
-
if (repo.url) {
|
|
38675
|
-
await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
38676
|
-
() => void 0
|
|
38677
|
-
);
|
|
38678
|
-
}
|
|
38679
|
-
continue;
|
|
38680
|
-
}
|
|
38681
|
-
const dir2 = join19(target, subDirName(repo));
|
|
38682
|
-
if (repo.url) {
|
|
38683
|
-
if (existsSync12(join19(dir2, ".git"))) {
|
|
38684
|
-
emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
|
|
38685
|
-
continue;
|
|
38686
|
-
}
|
|
38687
|
-
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
38688
|
-
await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
38689
|
-
} else {
|
|
38690
|
-
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
38691
|
-
await mkdir6(dir2, { recursive: true });
|
|
38692
|
-
await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
38693
|
-
}
|
|
38694
|
-
}
|
|
38695
|
-
}
|
|
38696
|
-
function sampleDir() {
|
|
38697
|
-
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
38698
|
-
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
38699
|
-
const bundled = resolve8(here, "project-sample");
|
|
38700
|
-
for (const candidate of [bundled, resolve8(here, "../project-sample"), resolve8(here, "../../project-sample")]) {
|
|
38701
|
-
if (existsSync12(candidate)) return candidate;
|
|
38702
|
-
}
|
|
38703
|
-
return bundled;
|
|
38704
|
-
}
|
|
38705
|
-
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
38706
|
-
let lastStep = "start";
|
|
38707
|
-
const emit = (step, message) => {
|
|
38708
|
-
lastStep = step;
|
|
38709
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38710
|
-
};
|
|
38711
|
-
try {
|
|
38712
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38713
|
-
const target = join19(root, payload.projectName);
|
|
38714
|
-
await mkdir6(target, { recursive: true });
|
|
38715
|
-
emit("copy", "Copying the sample template\u2026");
|
|
38716
|
-
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
38717
|
-
if (payload.spec) {
|
|
38718
|
-
emit("spec", "Writing project.spec.json\u2026");
|
|
38719
|
-
await writeFile5(
|
|
38720
|
-
join19(target, "project.spec.json"),
|
|
38721
|
-
JSON.stringify(payload.spec, null, 2),
|
|
38722
|
-
"utf8"
|
|
38723
|
-
);
|
|
38724
|
-
}
|
|
38725
|
-
const repos = reposOf(payload.spec);
|
|
38726
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
38727
|
-
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
38728
|
-
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
38729
|
-
await writeTemplateMarker(target);
|
|
38730
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
38731
|
-
return { ok: true, path: target };
|
|
38732
|
-
} catch (err) {
|
|
38733
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38734
|
-
}
|
|
38735
|
-
}
|
|
38736
|
-
async function writeTemplateMarker(target) {
|
|
38737
|
-
const dir2 = join19(target, ".4pm");
|
|
38738
|
-
await mkdir6(dir2, { recursive: true });
|
|
38739
|
-
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
38740
|
-
await writeFile5(join19(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
38741
|
-
}
|
|
38742
|
-
async function aiInit(target, spec, emit) {
|
|
38743
|
-
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
38744
|
-
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
38745
|
-
if (subagents.length > 0) {
|
|
38746
|
-
await mkdir6(join19(target, ".claude", "agents"), { recursive: true });
|
|
38747
|
-
for (const sa of subagents) {
|
|
38748
|
-
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
38749
|
-
if (!name) continue;
|
|
38750
|
-
const body = `---
|
|
38751
|
-
name: ${name}
|
|
38752
|
-
---
|
|
38753
|
-
|
|
38754
|
-
${(sa.description || "").trim()}
|
|
38755
|
-
`;
|
|
38756
|
-
await writeFile5(join19(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
38757
|
-
}
|
|
38758
|
-
}
|
|
38759
|
-
const specJson = JSON.stringify(spec);
|
|
38760
|
-
await generateFile(
|
|
38761
|
-
join19(target, "README.md"),
|
|
38762
|
-
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
38763
|
-
${specJson}`
|
|
38764
|
-
);
|
|
38765
|
-
await generateFile(
|
|
38766
|
-
join19(target, "CLAUDE.md"),
|
|
38767
|
-
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
38768
|
-
${specJson}`
|
|
38769
|
-
);
|
|
38770
|
-
}
|
|
38771
|
-
async function generateFile(path, prompt) {
|
|
38772
|
-
try {
|
|
38773
|
-
const text = await aiGenerate(prompt);
|
|
38774
|
-
if (text.trim()) await writeFile5(path, text.trim() + "\n", "utf8");
|
|
38775
|
-
} catch {
|
|
38776
|
-
}
|
|
38777
|
-
}
|
|
38778
|
-
async function addProject(payload, profileDir2, onProgress) {
|
|
38779
|
-
let lastStep = "start";
|
|
38780
|
-
const emit = (step, message) => {
|
|
38781
|
-
lastStep = step;
|
|
38782
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38783
|
-
};
|
|
38784
|
-
try {
|
|
38785
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38786
|
-
const target = join19(root, payload.projectName);
|
|
38787
|
-
await mkdir6(target, { recursive: true });
|
|
38788
|
-
const repos = payload.repos ?? [];
|
|
38789
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
|
|
38790
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
38791
|
-
return { ok: true, path: target };
|
|
38792
|
-
} catch (err) {
|
|
38793
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38794
|
-
}
|
|
38795
|
-
}
|
|
38796
|
-
|
|
38797
|
-
// src/core/ws-client/handlers/project.ts
|
|
38798
38895
|
function handleProjectChannels(ctx, message, payload) {
|
|
38799
38896
|
switch (message.channel) {
|
|
38800
38897
|
case WsChannels.PHYSIC_SYNC: {
|
|
@@ -38815,6 +38912,7 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
38815
38912
|
ctx.physicRoot = ctx.physicFolderPath(sync.newName);
|
|
38816
38913
|
ctx.bus.setProject(sync.newName);
|
|
38817
38914
|
ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
|
|
38915
|
+
void ctx.cloneServingRepos().catch(() => void 0);
|
|
38818
38916
|
return true;
|
|
38819
38917
|
}
|
|
38820
38918
|
case WsChannels.PHYSIC_DELETE: {
|
|
@@ -39088,7 +39186,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
39088
39186
|
}
|
|
39089
39187
|
|
|
39090
39188
|
// src/core/agents.ts
|
|
39091
|
-
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as
|
|
39189
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
|
|
39092
39190
|
import { join as join21 } from "path";
|
|
39093
39191
|
var AGENTS_REL = ".claude/agents";
|
|
39094
39192
|
var SKILLS_REL = ".claude/skills";
|
|
@@ -39113,7 +39211,7 @@ async function listAgents(root) {
|
|
|
39113
39211
|
const subagents = [];
|
|
39114
39212
|
const skills = [];
|
|
39115
39213
|
try {
|
|
39116
|
-
for (const e of await
|
|
39214
|
+
for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
|
|
39117
39215
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
39118
39216
|
const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
|
|
39119
39217
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
@@ -39121,7 +39219,7 @@ async function listAgents(root) {
|
|
|
39121
39219
|
} catch {
|
|
39122
39220
|
}
|
|
39123
39221
|
try {
|
|
39124
|
-
for (const e of await
|
|
39222
|
+
for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
|
|
39125
39223
|
if (!e.isDirectory()) continue;
|
|
39126
39224
|
const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
39127
39225
|
skills.push({ name: e.name, description: fm.description });
|
|
@@ -39151,8 +39249,8 @@ async function writeAgent(root, req) {
|
|
|
39151
39249
|
if (!n) return { ok: false, error: "invalid name" };
|
|
39152
39250
|
try {
|
|
39153
39251
|
if (req.action === "delete") {
|
|
39154
|
-
if (req.kind === "subagent") await
|
|
39155
|
-
else await
|
|
39252
|
+
if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
|
|
39253
|
+
else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
39156
39254
|
return { ok: true };
|
|
39157
39255
|
}
|
|
39158
39256
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
@@ -39273,7 +39371,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
39273
39371
|
|
|
39274
39372
|
// src/core/packages.ts
|
|
39275
39373
|
import { createHash as createHash4 } from "crypto";
|
|
39276
|
-
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as
|
|
39374
|
+
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
|
|
39277
39375
|
import { dirname as dirname9, join as join24, relative } from "path";
|
|
39278
39376
|
var AGENTS_REL2 = ".claude/agents";
|
|
39279
39377
|
var SKILLS_REL2 = ".claude/skills";
|
|
@@ -39293,7 +39391,7 @@ async function listFilesRel(dir2) {
|
|
|
39293
39391
|
async function walk2(d) {
|
|
39294
39392
|
let entries2;
|
|
39295
39393
|
try {
|
|
39296
|
-
entries2 = await
|
|
39394
|
+
entries2 = await readdir6(d, { withFileTypes: true });
|
|
39297
39395
|
} catch {
|
|
39298
39396
|
return;
|
|
39299
39397
|
}
|
|
@@ -39446,11 +39544,11 @@ async function removePackage(root, slug) {
|
|
|
39446
39544
|
const entry = manifest[n];
|
|
39447
39545
|
if (!entry) return { ok: false, error: "not installed" };
|
|
39448
39546
|
if (entry.kind === "subagent") {
|
|
39449
|
-
await
|
|
39450
|
-
await
|
|
39451
|
-
await
|
|
39547
|
+
await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
39548
|
+
await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
39549
|
+
await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
39452
39550
|
} else {
|
|
39453
|
-
await
|
|
39551
|
+
await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
39454
39552
|
}
|
|
39455
39553
|
delete manifest[n];
|
|
39456
39554
|
await writeManifest(root, manifest);
|
|
@@ -39461,7 +39559,7 @@ async function removePackage(root, slug) {
|
|
|
39461
39559
|
}
|
|
39462
39560
|
|
|
39463
39561
|
// src/core/graph.ts
|
|
39464
|
-
import { readFile as readFile11, readdir as
|
|
39562
|
+
import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
|
|
39465
39563
|
import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
39466
39564
|
var MAX_FILES = 2e3;
|
|
39467
39565
|
var MAX_NODES = 3e3;
|
|
@@ -39474,7 +39572,7 @@ async function walk(root, accept) {
|
|
|
39474
39572
|
const dir2 = stack.pop();
|
|
39475
39573
|
let entries2;
|
|
39476
39574
|
try {
|
|
39477
|
-
entries2 = await
|
|
39575
|
+
entries2 = await readdir7(dir2, { withFileTypes: true });
|
|
39478
39576
|
} catch {
|
|
39479
39577
|
continue;
|
|
39480
39578
|
}
|
|
@@ -40170,6 +40268,9 @@ var WsClient = class {
|
|
|
40170
40268
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
40171
40269
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
40172
40270
|
physicRoot = null;
|
|
40271
|
+
/** Declared repos of the served project (ADR-0289), from each ws_token — cloned into the physic
|
|
40272
|
+
* root on connect (and after a PHYSIC_SYNC creates the folder) when any is missing its `.git`. */
|
|
40273
|
+
servingRepos = [];
|
|
40173
40274
|
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
40174
40275
|
* Push uses the account already logged in with gh/glab on the worker. */
|
|
40175
40276
|
machineUsername = "";
|
|
@@ -40287,9 +40388,24 @@ var WsClient = class {
|
|
|
40287
40388
|
setConsoleWatching: (on) => self.setConsoleWatching(on),
|
|
40288
40389
|
setMetricsWatching: (on) => self.setMetricsWatching(on),
|
|
40289
40390
|
ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
|
|
40290
|
-
physicFolderPath: (projectName) => self.physicFolderPath(projectName)
|
|
40391
|
+
physicFolderPath: (projectName) => self.physicFolderPath(projectName),
|
|
40392
|
+
cloneServingRepos: () => self.ensureServingReposCloned(),
|
|
40393
|
+
updateCliNow: () => self.updateScheduler.updateNow()
|
|
40291
40394
|
};
|
|
40292
40395
|
}
|
|
40396
|
+
/**
|
|
40397
|
+
* Clone any declared repo missing from the served physic root (ADR-0289) — idempotent (skips a
|
|
40398
|
+
* repo that already has `.git`). Best-effort: a clone failure never disrupts the session (the web
|
|
40399
|
+
* still surfaces "not provisioned"). No-op when idle (no physic root) or no repos declared.
|
|
40400
|
+
*/
|
|
40401
|
+
async ensureServingReposCloned() {
|
|
40402
|
+
if (!this.physicRoot || this.servingRepos.length === 0) return;
|
|
40403
|
+
try {
|
|
40404
|
+
await ensureReposCloned(this.physicRoot, this.servingRepos, (_step, message) => this.bus.log(message));
|
|
40405
|
+
} catch (err) {
|
|
40406
|
+
logger.warn("repos.clone.failed", { error: err instanceof Error ? err.message : String(err) });
|
|
40407
|
+
}
|
|
40408
|
+
}
|
|
40293
40409
|
/**
|
|
40294
40410
|
* Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
|
|
40295
40411
|
* DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
|
|
@@ -40473,6 +40589,8 @@ var WsClient = class {
|
|
|
40473
40589
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
40474
40590
|
applyGitAuth(token.gitAuth ?? null);
|
|
40475
40591
|
this.machineUsername = token.machineUsername ?? "";
|
|
40592
|
+
this.servingRepos = token.repos ?? [];
|
|
40593
|
+
void this.ensureServingReposCloned();
|
|
40476
40594
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
40477
40595
|
this.credential.scope = token.scope;
|
|
40478
40596
|
writeCredential(this.context.profileDir, this.credential);
|