@4pm/cli 1.8.4 → 1.9.0-b
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 +564 -425
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29506,17 +29506,38 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
|
|
|
29506
29506
|
`;
|
|
29507
29507
|
|
|
29508
29508
|
// src/version.ts
|
|
29509
|
+
import { execFileSync } from "child_process";
|
|
29509
29510
|
import { readFileSync as readFileSync4 } from "fs";
|
|
29510
29511
|
import { dirname, join as join4 } from "path";
|
|
29511
29512
|
import { fileURLToPath } from "url";
|
|
29513
|
+
function gitDescribeVersion(cwd2) {
|
|
29514
|
+
try {
|
|
29515
|
+
const out = execFileSync("git", ["describe", "--tags", "--match", "cli-v*"], {
|
|
29516
|
+
cwd: cwd2,
|
|
29517
|
+
timeout: 3e3,
|
|
29518
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
29519
|
+
}).toString().trim();
|
|
29520
|
+
const stripped = out.replace(/^cli-v/, "");
|
|
29521
|
+
return stripped || null;
|
|
29522
|
+
} catch {
|
|
29523
|
+
return null;
|
|
29524
|
+
}
|
|
29525
|
+
}
|
|
29512
29526
|
function readCliVersion() {
|
|
29527
|
+
const envVersion = process.env.FOURPM_CLI_VERSION?.trim();
|
|
29528
|
+
if (envVersion) return envVersion.replace(/^cli-v/, "");
|
|
29529
|
+
let version = "0.0.0";
|
|
29513
29530
|
try {
|
|
29514
29531
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
29515
29532
|
const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
|
|
29516
|
-
|
|
29533
|
+
version = pkg.version ?? "0.0.0";
|
|
29534
|
+
if (version === "0.0.0") {
|
|
29535
|
+
const dev = gitDescribeVersion(here);
|
|
29536
|
+
if (dev) version = dev;
|
|
29537
|
+
}
|
|
29517
29538
|
} catch {
|
|
29518
|
-
return "0.0.0";
|
|
29519
29539
|
}
|
|
29540
|
+
return version;
|
|
29520
29541
|
}
|
|
29521
29542
|
var CLI_VERSION = readCliVersion();
|
|
29522
29543
|
|
|
@@ -33965,6 +33986,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
|
|
|
33965
33986
|
/** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
|
|
33966
33987
|
ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
|
|
33967
33988
|
});
|
|
33989
|
+
var provisionRequestSchema = external_exports.object({
|
|
33990
|
+
mode: external_exports.enum(["sync", "force"]).optional().default("sync")
|
|
33991
|
+
});
|
|
33992
|
+
var provisionForUserRequestSchema = external_exports.object({
|
|
33993
|
+
userId: external_exports.string().uuid(),
|
|
33994
|
+
projectId: external_exports.string().uuid(),
|
|
33995
|
+
mode: external_exports.enum(["sync", "force"]).optional().default("sync")
|
|
33996
|
+
});
|
|
33968
33997
|
var listWorkersQuerySchema = baseRequestSchema.extend({
|
|
33969
33998
|
status: external_exports.enum(["online", "offline"]).optional(),
|
|
33970
33999
|
/** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
|
|
@@ -34240,7 +34269,13 @@ var repoSpecSchema = external_exports.object({
|
|
|
34240
34269
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
34241
34270
|
url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
|
|
34242
34271
|
/** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
|
|
34243
|
-
subdir: external_exports.string().max(120).optional().default("")
|
|
34272
|
+
subdir: external_exports.string().max(120).optional().default(""),
|
|
34273
|
+
/**
|
|
34274
|
+
* Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
|
|
34275
|
+
* worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
|
|
34276
|
+
* fast-forward pulls it.
|
|
34277
|
+
*/
|
|
34278
|
+
branch: external_exports.string().max(200).optional().default("")
|
|
34244
34279
|
});
|
|
34245
34280
|
function repoName(url) {
|
|
34246
34281
|
const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
|
|
@@ -34367,9 +34402,282 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
34367
34402
|
projectId: external_exports.string().uuid().optional()
|
|
34368
34403
|
});
|
|
34369
34404
|
|
|
34405
|
+
// src/core/scaffold.ts
|
|
34406
|
+
import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
|
|
34407
|
+
import { existsSync as existsSync4 } from "fs";
|
|
34408
|
+
import { execFile as execFile2 } from "child_process";
|
|
34409
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
34410
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
34411
|
+
import { promisify as promisify2 } from "util";
|
|
34412
|
+
|
|
34413
|
+
// src/core/ai-assist.ts
|
|
34414
|
+
import { execFile } from "child_process";
|
|
34415
|
+
import { promisify } from "util";
|
|
34416
|
+
var run = promisify(execFile);
|
|
34417
|
+
function aiCli() {
|
|
34418
|
+
return process.env.AI_CLI || "claude";
|
|
34419
|
+
}
|
|
34420
|
+
function aiArgs(cli, prompt) {
|
|
34421
|
+
if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
|
|
34422
|
+
if (cli === "codex") return ["exec", "--json", prompt];
|
|
34423
|
+
return ["-p", prompt];
|
|
34424
|
+
}
|
|
34425
|
+
function estimateTokens(prompt, output) {
|
|
34426
|
+
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
34427
|
+
}
|
|
34428
|
+
function parseAi(cli, stdout, prompt) {
|
|
34429
|
+
if (cli === "claude") {
|
|
34430
|
+
try {
|
|
34431
|
+
const j = JSON.parse(stdout);
|
|
34432
|
+
const text2 = (j.result ?? "").trim();
|
|
34433
|
+
const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
|
|
34434
|
+
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
|
|
34435
|
+
} catch {
|
|
34436
|
+
}
|
|
34437
|
+
}
|
|
34438
|
+
if (cli === "codex") {
|
|
34439
|
+
try {
|
|
34440
|
+
let text2 = "";
|
|
34441
|
+
let tokens = 0;
|
|
34442
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
34443
|
+
const obj = JSON.parse(line);
|
|
34444
|
+
if (obj.text) text2 = obj.text;
|
|
34445
|
+
else if (obj.message) text2 = obj.message;
|
|
34446
|
+
if (obj.usage) {
|
|
34447
|
+
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
34448
|
+
}
|
|
34449
|
+
}
|
|
34450
|
+
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
|
|
34451
|
+
} catch {
|
|
34452
|
+
}
|
|
34453
|
+
}
|
|
34454
|
+
const text = stdout.trim();
|
|
34455
|
+
return { text, tokens: estimateTokens(prompt, text) };
|
|
34456
|
+
}
|
|
34457
|
+
async function runAi(prompt) {
|
|
34458
|
+
const cli = aiCli();
|
|
34459
|
+
const { stdout } = await run(cli, aiArgs(cli, prompt), {
|
|
34460
|
+
timeout: 12e4,
|
|
34461
|
+
maxBuffer: 8 * 1024 * 1024
|
|
34462
|
+
});
|
|
34463
|
+
return parseAi(cli, stdout, prompt);
|
|
34464
|
+
}
|
|
34465
|
+
async function aiGenerate(prompt) {
|
|
34466
|
+
const { text } = await runAi(prompt);
|
|
34467
|
+
return text;
|
|
34468
|
+
}
|
|
34469
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
34470
|
+
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:
|
|
34471
|
+
${input}`;
|
|
34472
|
+
const { text, tokens } = await runAi(prompt);
|
|
34473
|
+
const start = text.indexOf("{");
|
|
34474
|
+
const end = text.lastIndexOf("}");
|
|
34475
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34476
|
+
try {
|
|
34477
|
+
const j = JSON.parse(text.slice(start, end + 1));
|
|
34478
|
+
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
34479
|
+
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
34480
|
+
} catch {
|
|
34481
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34482
|
+
}
|
|
34483
|
+
}
|
|
34484
|
+
|
|
34485
|
+
// src/core/scaffold.ts
|
|
34486
|
+
var run2 = promisify2(execFile2);
|
|
34487
|
+
function reposOf(spec) {
|
|
34488
|
+
const repos = spec?.repos;
|
|
34489
|
+
return Array.isArray(repos) ? repos : [];
|
|
34490
|
+
}
|
|
34491
|
+
function subDirName(repo) {
|
|
34492
|
+
const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
|
|
34493
|
+
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
34494
|
+
}
|
|
34495
|
+
function cloneArgs(url, dest, branch) {
|
|
34496
|
+
const b = (branch ?? "").trim();
|
|
34497
|
+
return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
|
|
34498
|
+
}
|
|
34499
|
+
async function updateRepo(dir2, label, branch, emit) {
|
|
34500
|
+
const b = (branch ?? "").trim();
|
|
34501
|
+
emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
|
|
34502
|
+
await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
34503
|
+
if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
|
|
34504
|
+
await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
34505
|
+
}
|
|
34506
|
+
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
34507
|
+
const mode = opts.mode ?? "clone";
|
|
34508
|
+
const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
|
|
34509
|
+
for (const repo of repos) {
|
|
34510
|
+
if (repo.primary) {
|
|
34511
|
+
if (opts.clonePrimary && repo.url) {
|
|
34512
|
+
if (existsSync4(join6(target, ".git"))) {
|
|
34513
|
+
if (mode === "sync") {
|
|
34514
|
+
await updateRepo(target, "primary repo", repo.branch, emit);
|
|
34515
|
+
} else if (mode === "force") {
|
|
34516
|
+
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
34517
|
+
const tmp = `${target}.4pm-reclone`;
|
|
34518
|
+
await rm(tmp, { recursive: true, force: true });
|
|
34519
|
+
await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
|
|
34520
|
+
for (const entry of await readdir(target)) {
|
|
34521
|
+
if (subDirsToKeep.has(entry)) continue;
|
|
34522
|
+
await rm(join6(target, entry), { recursive: true, force: true });
|
|
34523
|
+
}
|
|
34524
|
+
for (const entry of await readdir(tmp)) {
|
|
34525
|
+
if (subDirsToKeep.has(entry)) continue;
|
|
34526
|
+
await rename(join6(tmp, entry), join6(target, entry));
|
|
34527
|
+
}
|
|
34528
|
+
await rm(tmp, { recursive: true, force: true });
|
|
34529
|
+
} else {
|
|
34530
|
+
emit("git", `Primary repo already present \u2014 skipping clone.`);
|
|
34531
|
+
}
|
|
34532
|
+
continue;
|
|
34533
|
+
}
|
|
34534
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
34535
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
34536
|
+
continue;
|
|
34537
|
+
}
|
|
34538
|
+
emit("git", "Initializing the primary repo\u2026");
|
|
34539
|
+
await run2("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
34540
|
+
if (repo.url) {
|
|
34541
|
+
await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
34542
|
+
() => void 0
|
|
34543
|
+
);
|
|
34544
|
+
}
|
|
34545
|
+
continue;
|
|
34546
|
+
}
|
|
34547
|
+
const dir2 = join6(target, subDirName(repo));
|
|
34548
|
+
if (repo.url) {
|
|
34549
|
+
if (existsSync4(join6(dir2, ".git"))) {
|
|
34550
|
+
if (mode === "sync") {
|
|
34551
|
+
await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
|
|
34552
|
+
} else if (mode === "force") {
|
|
34553
|
+
emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
|
|
34554
|
+
await rm(dir2, { recursive: true, force: true });
|
|
34555
|
+
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
34556
|
+
} else {
|
|
34557
|
+
emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
|
|
34558
|
+
}
|
|
34559
|
+
continue;
|
|
34560
|
+
}
|
|
34561
|
+
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
34562
|
+
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
34563
|
+
} else {
|
|
34564
|
+
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
34565
|
+
await mkdir(dir2, { recursive: true });
|
|
34566
|
+
await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
34567
|
+
}
|
|
34568
|
+
}
|
|
34569
|
+
}
|
|
34570
|
+
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
34571
|
+
if (repos.length === 0) return;
|
|
34572
|
+
await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
|
|
34573
|
+
}
|
|
34574
|
+
function sampleDir() {
|
|
34575
|
+
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
34576
|
+
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
34577
|
+
const bundled = resolve(here, "project-sample");
|
|
34578
|
+
for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
|
|
34579
|
+
if (existsSync4(candidate)) return candidate;
|
|
34580
|
+
}
|
|
34581
|
+
return bundled;
|
|
34582
|
+
}
|
|
34583
|
+
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
34584
|
+
let lastStep = "start";
|
|
34585
|
+
const emit = (step, message) => {
|
|
34586
|
+
lastStep = step;
|
|
34587
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34588
|
+
};
|
|
34589
|
+
try {
|
|
34590
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34591
|
+
const target = join6(root, payload.projectName);
|
|
34592
|
+
await mkdir(target, { recursive: true });
|
|
34593
|
+
emit("copy", "Copying the sample template\u2026");
|
|
34594
|
+
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
34595
|
+
if (payload.spec) {
|
|
34596
|
+
emit("spec", "Writing project.spec.json\u2026");
|
|
34597
|
+
await writeFile(
|
|
34598
|
+
join6(target, "project.spec.json"),
|
|
34599
|
+
JSON.stringify(payload.spec, null, 2),
|
|
34600
|
+
"utf8"
|
|
34601
|
+
);
|
|
34602
|
+
}
|
|
34603
|
+
const repos = reposOf(payload.spec);
|
|
34604
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
34605
|
+
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
34606
|
+
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
34607
|
+
await writeTemplateMarker(target);
|
|
34608
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
34609
|
+
return { ok: true, path: target };
|
|
34610
|
+
} catch (err) {
|
|
34611
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
34612
|
+
}
|
|
34613
|
+
}
|
|
34614
|
+
async function writeTemplateMarker(target) {
|
|
34615
|
+
const dir2 = join6(target, ".4pm");
|
|
34616
|
+
await mkdir(dir2, { recursive: true });
|
|
34617
|
+
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
34618
|
+
await writeFile(join6(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
34619
|
+
}
|
|
34620
|
+
async function aiInit(target, spec, emit) {
|
|
34621
|
+
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
34622
|
+
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
34623
|
+
if (subagents.length > 0) {
|
|
34624
|
+
await mkdir(join6(target, ".claude", "agents"), { recursive: true });
|
|
34625
|
+
for (const sa of subagents) {
|
|
34626
|
+
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34627
|
+
if (!name) continue;
|
|
34628
|
+
const body = `---
|
|
34629
|
+
name: ${name}
|
|
34630
|
+
---
|
|
34631
|
+
|
|
34632
|
+
${(sa.description || "").trim()}
|
|
34633
|
+
`;
|
|
34634
|
+
await writeFile(join6(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
34635
|
+
}
|
|
34636
|
+
}
|
|
34637
|
+
const specJson = JSON.stringify(spec);
|
|
34638
|
+
await generateFile(
|
|
34639
|
+
join6(target, "README.md"),
|
|
34640
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
34641
|
+
${specJson}`
|
|
34642
|
+
);
|
|
34643
|
+
await generateFile(
|
|
34644
|
+
join6(target, "CLAUDE.md"),
|
|
34645
|
+
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
34646
|
+
${specJson}`
|
|
34647
|
+
);
|
|
34648
|
+
}
|
|
34649
|
+
async function generateFile(path, prompt) {
|
|
34650
|
+
try {
|
|
34651
|
+
const text = await aiGenerate(prompt);
|
|
34652
|
+
if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
|
|
34653
|
+
} catch {
|
|
34654
|
+
}
|
|
34655
|
+
}
|
|
34656
|
+
async function addProject(payload, profileDir2, onProgress) {
|
|
34657
|
+
let lastStep = "start";
|
|
34658
|
+
const emit = (step, message) => {
|
|
34659
|
+
lastStep = step;
|
|
34660
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34661
|
+
};
|
|
34662
|
+
try {
|
|
34663
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34664
|
+
const target = join6(root, payload.projectName);
|
|
34665
|
+
await mkdir(target, { recursive: true });
|
|
34666
|
+
const repos = payload.repos ?? [];
|
|
34667
|
+
const mode = payload.mode ?? "clone";
|
|
34668
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
|
|
34669
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
34670
|
+
return { ok: true, path: target };
|
|
34671
|
+
} catch (err) {
|
|
34672
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34673
|
+
onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
|
|
34674
|
+
return { ok: false, error: message, step: lastStep };
|
|
34675
|
+
}
|
|
34676
|
+
}
|
|
34677
|
+
|
|
34370
34678
|
// src/core/worker-metrics.ts
|
|
34371
34679
|
import { statfs, readFile } from "fs/promises";
|
|
34372
|
-
import { existsSync as
|
|
34680
|
+
import { existsSync as existsSync5 } from "fs";
|
|
34373
34681
|
import { totalmem, freemem, cpus, platform, arch } from "os";
|
|
34374
34682
|
var CGROUP = "/sys/fs/cgroup";
|
|
34375
34683
|
var MB = 1024 * 1024;
|
|
@@ -34419,7 +34727,7 @@ function parseMaybeMax(text) {
|
|
|
34419
34727
|
return Number.isFinite(n) ? n : null;
|
|
34420
34728
|
}
|
|
34421
34729
|
function createWorkerMetricsSampler(diskPath) {
|
|
34422
|
-
const cgroupV2 =
|
|
34730
|
+
const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
|
|
34423
34731
|
let cgPrev = null;
|
|
34424
34732
|
let osPrev = null;
|
|
34425
34733
|
async function cgroupCpuCount() {
|
|
@@ -34734,6 +35042,10 @@ var WsChannels = {
|
|
|
34734
35042
|
// serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
|
|
34735
35043
|
// reconnect. `ws_token` seeds these on connect; this channel only carries updates.
|
|
34736
35044
|
PROJECT_TOKENS: "project.tokens",
|
|
35045
|
+
// server → cli (ADR-0289): update the cli to the latest version now (idle-aware self-update +
|
|
35046
|
+
// re-exec). Sent by the manual operator "Update" action and the update-on-connect hook when the
|
|
35047
|
+
// connecting cli is outdated. No payload; honours the machine's local autoUpdate opt-out.
|
|
35048
|
+
CLI_UPDATE: "cli.update",
|
|
34737
35049
|
// command lifecycle
|
|
34738
35050
|
COMMAND_ANNOUNCE: "command.announce",
|
|
34739
35051
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
@@ -34847,9 +35159,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
34847
35159
|
import { spawn as spawn2 } from "child_process";
|
|
34848
35160
|
|
|
34849
35161
|
// src/common/logger/logger.ts
|
|
34850
|
-
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
35162
|
+
import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
|
|
34851
35163
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
34852
|
-
import { join as
|
|
35164
|
+
import { join as join7 } from "path";
|
|
34853
35165
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
34854
35166
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
34855
35167
|
var DEFAULT_RETENTION_DAYS = 14;
|
|
@@ -34898,8 +35210,8 @@ var CliLogger = class {
|
|
|
34898
35210
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
34899
35211
|
const entry = { timestamp, level, ...this.base, event, ...fields };
|
|
34900
35212
|
const line = JSON.stringify(entry) + "\n";
|
|
34901
|
-
const path =
|
|
34902
|
-
this.queue = this.queue.then(() =>
|
|
35213
|
+
const path = join7(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
|
|
35214
|
+
this.queue = this.queue.then(() => mkdir2(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
|
|
34903
35215
|
console.warn(`Failed to write cli log: ${err}`);
|
|
34904
35216
|
});
|
|
34905
35217
|
}
|
|
@@ -34907,13 +35219,13 @@ var CliLogger = class {
|
|
|
34907
35219
|
async cleanupOldLogs() {
|
|
34908
35220
|
const dir2 = this.dir;
|
|
34909
35221
|
if (!dir2) return;
|
|
34910
|
-
const files = await
|
|
35222
|
+
const files = await readdir2(dir2).catch(() => []);
|
|
34911
35223
|
const cutoff = Date.now() - this.retentionDays * DAY_MS;
|
|
34912
35224
|
for (const file of files) {
|
|
34913
35225
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
34914
35226
|
if (!match) continue;
|
|
34915
35227
|
if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
|
|
34916
|
-
await unlink(
|
|
35228
|
+
await unlink(join7(dir2, file)).catch(() => void 0);
|
|
34917
35229
|
}
|
|
34918
35230
|
}
|
|
34919
35231
|
}
|
|
@@ -34924,7 +35236,7 @@ function readRecentLogLines(dir2, limit) {
|
|
|
34924
35236
|
const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
|
|
34925
35237
|
const latest = files[files.length - 1];
|
|
34926
35238
|
if (!latest) return [];
|
|
34927
|
-
const lines = readFileSync6(
|
|
35239
|
+
const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
|
|
34928
35240
|
return lines.slice(-limit);
|
|
34929
35241
|
} catch {
|
|
34930
35242
|
return [];
|
|
@@ -34937,8 +35249,8 @@ function readLogUpload(dir2) {
|
|
|
34937
35249
|
const latest = files[files.length - 1];
|
|
34938
35250
|
if (!latest) return null;
|
|
34939
35251
|
let totalBytes = 0;
|
|
34940
|
-
for (const f of files) totalBytes += statSync(
|
|
34941
|
-
let content = readFileSync6(
|
|
35252
|
+
for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
|
|
35253
|
+
let content = readFileSync6(join7(dir2, latest), "utf8");
|
|
34942
35254
|
if (content.length > LOG_UPLOAD_CONTENT_MAX) {
|
|
34943
35255
|
content = `\u2026[truncated]
|
|
34944
35256
|
${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
@@ -34981,7 +35293,7 @@ function classifyToolFailure(code, out) {
|
|
|
34981
35293
|
}
|
|
34982
35294
|
return { reason: "other", retryable: false };
|
|
34983
35295
|
}
|
|
34984
|
-
function
|
|
35296
|
+
function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
34985
35297
|
return new Promise((resolve10) => {
|
|
34986
35298
|
let out = "";
|
|
34987
35299
|
let buf = "";
|
|
@@ -35021,13 +35333,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
35021
35333
|
});
|
|
35022
35334
|
}
|
|
35023
35335
|
async function detectOne(cmd, versionArg) {
|
|
35024
|
-
const { code, out } = await
|
|
35336
|
+
const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
|
|
35025
35337
|
if (code !== 0) return null;
|
|
35026
35338
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
35027
35339
|
return first ?? "";
|
|
35028
35340
|
}
|
|
35029
35341
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
35030
|
-
const { out } = await
|
|
35342
|
+
const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
35031
35343
|
if (!out.trim()) return [];
|
|
35032
35344
|
let deps = {};
|
|
35033
35345
|
try {
|
|
@@ -35103,7 +35415,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
35103
35415
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
35104
35416
|
}
|
|
35105
35417
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
35106
|
-
const { code } = await
|
|
35418
|
+
const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
35107
35419
|
return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
|
|
35108
35420
|
}
|
|
35109
35421
|
function setToolAutoUpdate(profileDir2, name, enabled) {
|
|
@@ -35134,7 +35446,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
35134
35446
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
35135
35447
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
35136
35448
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
35137
|
-
const { code, out } = await
|
|
35449
|
+
const { code, out } = await run3(manager, args, onLine, timeoutMs);
|
|
35138
35450
|
if (code === 0) return { ok: true };
|
|
35139
35451
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
35140
35452
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -35200,6 +35512,9 @@ var UpdateScheduler = class {
|
|
|
35200
35512
|
lastDoneDateKey = null;
|
|
35201
35513
|
/** Date key marked due but deferred because the cli was busy (stays set past the hour). */
|
|
35202
35514
|
pendingDateKey = null;
|
|
35515
|
+
/** A push/connect-triggered update requested now (ADR-0289) — runs when idle, independent of the
|
|
35516
|
+
* daily policy; cleared once it runs. */
|
|
35517
|
+
forcedPending = false;
|
|
35203
35518
|
/** Guards against a second update while one is in flight. */
|
|
35204
35519
|
updating = false;
|
|
35205
35520
|
/** Apply the latest policy (called on each ws_token) and start the tick timer once. */
|
|
@@ -35210,6 +35525,25 @@ var UpdateScheduler = class {
|
|
|
35210
35525
|
this.timer.unref?.();
|
|
35211
35526
|
}
|
|
35212
35527
|
}
|
|
35528
|
+
/**
|
|
35529
|
+
* Request an update to latest NOW (ADR-0289) — the server's `cli.update` push or the
|
|
35530
|
+
* update-on-connect hook. Idle-aware like the daily tick (defers while a command runs) and
|
|
35531
|
+
* honours the local `autoUpdate:false` opt-out, but is independent of the daily org policy. Also
|
|
35532
|
+
* ensures the tick timer is running so the request is served even before the first ws_token.
|
|
35533
|
+
*/
|
|
35534
|
+
updateNow() {
|
|
35535
|
+
if (this.optedOutLocally()) {
|
|
35536
|
+
logger.info("update.push.optedOut");
|
|
35537
|
+
return;
|
|
35538
|
+
}
|
|
35539
|
+
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35540
|
+
this.forcedPending = true;
|
|
35541
|
+
if (!this.timer) {
|
|
35542
|
+
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
35543
|
+
this.timer.unref?.();
|
|
35544
|
+
}
|
|
35545
|
+
void this.tick();
|
|
35546
|
+
}
|
|
35213
35547
|
/** Stop the scheduler when the session ends (logout / replaced / stopped). */
|
|
35214
35548
|
stop() {
|
|
35215
35549
|
if (this.timer) {
|
|
@@ -35242,12 +35576,19 @@ var UpdateScheduler = class {
|
|
|
35242
35576
|
return null;
|
|
35243
35577
|
}
|
|
35244
35578
|
}
|
|
35245
|
-
/** One evaluation:
|
|
35579
|
+
/** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
|
|
35246
35580
|
async tick() {
|
|
35247
|
-
|
|
35248
|
-
if (!p || !p.autoUpdateDaily || this.updating) return;
|
|
35581
|
+
if (this.updating) return;
|
|
35249
35582
|
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35250
35583
|
if (this.optedOutLocally()) return;
|
|
35584
|
+
if (this.forcedPending) {
|
|
35585
|
+
if (this.bus.busy !== null) return;
|
|
35586
|
+
this.forcedPending = false;
|
|
35587
|
+
await this.runUpdate();
|
|
35588
|
+
return;
|
|
35589
|
+
}
|
|
35590
|
+
const p = this.policy;
|
|
35591
|
+
if (!p || !p.autoUpdateDaily) return;
|
|
35251
35592
|
const clock = this.orgClock();
|
|
35252
35593
|
if (!clock) return;
|
|
35253
35594
|
if (clock.dateKey === this.lastDoneDateKey) return;
|
|
@@ -35305,14 +35646,14 @@ var UpdateScheduler = class {
|
|
|
35305
35646
|
};
|
|
35306
35647
|
|
|
35307
35648
|
// src/core/ai-profile-state.ts
|
|
35308
|
-
import { existsSync as
|
|
35309
|
-
import { join as
|
|
35649
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35650
|
+
import { join as join8 } from "path";
|
|
35310
35651
|
function statePath(profileDir2) {
|
|
35311
|
-
return
|
|
35652
|
+
return join8(profileDir2, "ai-profiles.json");
|
|
35312
35653
|
}
|
|
35313
35654
|
function readState(profileDir2) {
|
|
35314
35655
|
const path = statePath(profileDir2);
|
|
35315
|
-
if (!
|
|
35656
|
+
if (!existsSync6(path)) return {};
|
|
35316
35657
|
try {
|
|
35317
35658
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
35318
35659
|
} catch {
|
|
@@ -35370,7 +35711,7 @@ function clearPinnedCredential(profileDir2) {
|
|
|
35370
35711
|
// src/utils/ai-cli.ts
|
|
35371
35712
|
import { readFileSync as readFileSync8 } from "fs";
|
|
35372
35713
|
import { homedir as homedir3 } from "os";
|
|
35373
|
-
import { basename, isAbsolute, join as
|
|
35714
|
+
import { basename, isAbsolute, join as join9 } from "path";
|
|
35374
35715
|
var REQUIRED_AI_ARGS = {
|
|
35375
35716
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
35376
35717
|
codex: ["exec", "--json"],
|
|
@@ -35507,9 +35848,9 @@ function claudeCredentialKeys(config) {
|
|
|
35507
35848
|
return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
|
|
35508
35849
|
}
|
|
35509
35850
|
function resolveHomePath(value) {
|
|
35510
|
-
if (value.startsWith("~")) return
|
|
35851
|
+
if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
|
|
35511
35852
|
if (isAbsolute(value)) return value;
|
|
35512
|
-
return
|
|
35853
|
+
return join9(homedir3(), value);
|
|
35513
35854
|
}
|
|
35514
35855
|
function profileDirs(profiles) {
|
|
35515
35856
|
if (!profiles) return [];
|
|
@@ -35555,11 +35896,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
|
|
|
35555
35896
|
}
|
|
35556
35897
|
function claudeHomeDirs(config) {
|
|
35557
35898
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
35558
|
-
return dirs.length > 0 ? dirs : [
|
|
35899
|
+
return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
|
|
35559
35900
|
}
|
|
35560
35901
|
function profileDisplayLabel(dir2) {
|
|
35561
35902
|
try {
|
|
35562
|
-
const raw = JSON.parse(readFileSync8(
|
|
35903
|
+
const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
|
|
35563
35904
|
const email = raw.oauthAccount?.emailAddress;
|
|
35564
35905
|
if (email && email.trim()) return email;
|
|
35565
35906
|
} catch {
|
|
@@ -35661,11 +36002,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
|
|
|
35661
36002
|
// src/core/claude-usage.ts
|
|
35662
36003
|
import { spawn as spawn3 } from "child_process";
|
|
35663
36004
|
import { readFileSync as readFileSync9 } from "fs";
|
|
35664
|
-
import { join as
|
|
36005
|
+
import { join as join10 } from "path";
|
|
35665
36006
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
35666
36007
|
function readCreds(dir2) {
|
|
35667
36008
|
try {
|
|
35668
|
-
const raw = JSON.parse(readFileSync9(
|
|
36009
|
+
const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
|
|
35669
36010
|
const oauth = raw.claudeAiOauth ?? raw;
|
|
35670
36011
|
return oauth.accessToken ? oauth : null;
|
|
35671
36012
|
} catch {
|
|
@@ -35774,8 +36115,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
|
|
|
35774
36115
|
}
|
|
35775
36116
|
|
|
35776
36117
|
// src/core/command-history.ts
|
|
35777
|
-
import { existsSync as
|
|
35778
|
-
import { join as
|
|
36118
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
36119
|
+
import { join as join11 } from "path";
|
|
35779
36120
|
var MAX_ENTRIES = 1e3;
|
|
35780
36121
|
var filePath = null;
|
|
35781
36122
|
var entries = [];
|
|
@@ -35837,8 +36178,8 @@ async function uploadHistory(cfg) {
|
|
|
35837
36178
|
}
|
|
35838
36179
|
}
|
|
35839
36180
|
function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
|
|
35840
|
-
filePath =
|
|
35841
|
-
if (
|
|
36181
|
+
filePath = join11(profileDir2, "command-history.json");
|
|
36182
|
+
if (existsSync7(filePath)) {
|
|
35842
36183
|
try {
|
|
35843
36184
|
entries = JSON.parse(readFileSync10(filePath, "utf8"));
|
|
35844
36185
|
} catch {
|
|
@@ -35894,16 +36235,16 @@ function finishCommand(commandId, exitCode, usage) {
|
|
|
35894
36235
|
}
|
|
35895
36236
|
|
|
35896
36237
|
// src/core/command-images.ts
|
|
35897
|
-
import { existsSync as
|
|
35898
|
-
import { join as
|
|
36238
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
36239
|
+
import { join as join12 } from "path";
|
|
35899
36240
|
function attachmentsBaseDir(physicRoot) {
|
|
35900
|
-
return
|
|
36241
|
+
return join12(physicRoot, ".4pm", "attachments");
|
|
35901
36242
|
}
|
|
35902
36243
|
var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
35903
36244
|
async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
35904
36245
|
const map = /* @__PURE__ */ new Map();
|
|
35905
36246
|
if (!images.length) return map;
|
|
35906
|
-
const dir2 =
|
|
36247
|
+
const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
|
|
35907
36248
|
mkdirSync3(dir2, { recursive: true });
|
|
35908
36249
|
let n = 0;
|
|
35909
36250
|
for (const img of images) {
|
|
@@ -35915,7 +36256,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
|
35915
36256
|
continue;
|
|
35916
36257
|
}
|
|
35917
36258
|
const ext = commandImageExt(reply.mime ?? img.mime);
|
|
35918
|
-
const file =
|
|
36259
|
+
const file = join12(dir2, `img-${n}.${ext}`);
|
|
35919
36260
|
writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
|
|
35920
36261
|
map.set(img.placeholder, file);
|
|
35921
36262
|
} catch (err) {
|
|
@@ -35937,12 +36278,12 @@ function rewriteImagePlaceholders(prompt, paths) {
|
|
|
35937
36278
|
}
|
|
35938
36279
|
function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
35939
36280
|
const base = attachmentsBaseDir(physicRoot);
|
|
35940
|
-
if (!
|
|
36281
|
+
if (!existsSync8(base)) return 0;
|
|
35941
36282
|
const cutoff = Date.now() - maxAgeMs;
|
|
35942
36283
|
let removed = 0;
|
|
35943
36284
|
try {
|
|
35944
36285
|
for (const entry of readdirSync3(base)) {
|
|
35945
|
-
const dir2 =
|
|
36286
|
+
const dir2 = join12(base, entry);
|
|
35946
36287
|
try {
|
|
35947
36288
|
if (statSync2(dir2).mtimeMs < cutoff) {
|
|
35948
36289
|
rmSync3(dir2, { recursive: true, force: true });
|
|
@@ -35959,18 +36300,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
|
35959
36300
|
// src/core/command-output-store.ts
|
|
35960
36301
|
import {
|
|
35961
36302
|
appendFileSync,
|
|
35962
|
-
existsSync as
|
|
36303
|
+
existsSync as existsSync9,
|
|
35963
36304
|
mkdirSync as mkdirSync4,
|
|
35964
36305
|
readFileSync as readFileSync11,
|
|
35965
36306
|
readdirSync as readdirSync4,
|
|
35966
36307
|
rmSync as rmSync4,
|
|
35967
36308
|
statSync as statSync3
|
|
35968
36309
|
} from "fs";
|
|
35969
|
-
import { join as
|
|
36310
|
+
import { join as join13 } from "path";
|
|
35970
36311
|
var KEEP_FILES = 200;
|
|
35971
36312
|
var dir = null;
|
|
35972
36313
|
function initCommandOutput(profileDir2) {
|
|
35973
|
-
dir =
|
|
36314
|
+
dir = join13(profileDir2, "command-output");
|
|
35974
36315
|
try {
|
|
35975
36316
|
mkdirSync4(dir, { recursive: true });
|
|
35976
36317
|
prune();
|
|
@@ -35980,14 +36321,14 @@ function initCommandOutput(profileDir2) {
|
|
|
35980
36321
|
function appendCommandOutput(commandId, text) {
|
|
35981
36322
|
if (!dir || !text) return;
|
|
35982
36323
|
try {
|
|
35983
|
-
appendFileSync(
|
|
36324
|
+
appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
|
|
35984
36325
|
} catch {
|
|
35985
36326
|
}
|
|
35986
36327
|
}
|
|
35987
36328
|
function readCommandOutput(commandId) {
|
|
35988
36329
|
if (!dir) return null;
|
|
35989
|
-
const path =
|
|
35990
|
-
if (!
|
|
36330
|
+
const path = join13(dir, `${commandId}.log`);
|
|
36331
|
+
if (!existsSync9(path)) return null;
|
|
35991
36332
|
try {
|
|
35992
36333
|
return readFileSync11(path, "utf8");
|
|
35993
36334
|
} catch {
|
|
@@ -35997,8 +36338,8 @@ function readCommandOutput(commandId) {
|
|
|
35997
36338
|
function prune() {
|
|
35998
36339
|
if (!dir) return;
|
|
35999
36340
|
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(
|
|
36341
|
+
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);
|
|
36342
|
+
for (const { f } of files.slice(KEEP_FILES)) rmSync4(join13(dir, f), { force: true });
|
|
36002
36343
|
} catch {
|
|
36003
36344
|
}
|
|
36004
36345
|
}
|
|
@@ -36007,7 +36348,7 @@ function pruneCommandOutputByAge(days) {
|
|
|
36007
36348
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
36008
36349
|
try {
|
|
36009
36350
|
for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
|
|
36010
|
-
const p =
|
|
36351
|
+
const p = join13(dir, f);
|
|
36011
36352
|
if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
|
|
36012
36353
|
}
|
|
36013
36354
|
} catch {
|
|
@@ -36016,12 +36357,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
36016
36357
|
|
|
36017
36358
|
// src/core/support-answer.ts
|
|
36018
36359
|
import { spawn as spawn4 } from "child_process";
|
|
36019
|
-
import { execFile } from "child_process";
|
|
36360
|
+
import { execFile as execFile3 } from "child_process";
|
|
36020
36361
|
import { createHash as createHash3 } from "crypto";
|
|
36021
|
-
import { existsSync as
|
|
36362
|
+
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
36363
|
import { randomUUID } from "crypto";
|
|
36023
|
-
import { join as
|
|
36024
|
-
import { promisify } from "util";
|
|
36364
|
+
import { join as join14 } from "path";
|
|
36365
|
+
import { promisify as promisify3 } from "util";
|
|
36025
36366
|
|
|
36026
36367
|
// src/core/ai-stream.ts
|
|
36027
36368
|
function assistantText(ev) {
|
|
@@ -36116,22 +36457,22 @@ function createAiStreamParser(cli) {
|
|
|
36116
36457
|
apiError: () => ({ ...apiError })
|
|
36117
36458
|
};
|
|
36118
36459
|
}
|
|
36119
|
-
function
|
|
36460
|
+
function estimateTokens2(text) {
|
|
36120
36461
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
36121
36462
|
}
|
|
36122
36463
|
|
|
36123
36464
|
// src/core/support-answer.ts
|
|
36124
36465
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
36125
|
-
var execFileAsync =
|
|
36466
|
+
var execFileAsync = promisify3(execFile3);
|
|
36126
36467
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
36127
36468
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
36128
36469
|
var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
36129
36470
|
function kbRoot(profileDir2) {
|
|
36130
|
-
return
|
|
36471
|
+
return join14(profileDir2, "support-kb");
|
|
36131
36472
|
}
|
|
36132
36473
|
function repoDir(profileDir2, url, branch) {
|
|
36133
36474
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
36134
|
-
return
|
|
36475
|
+
return join14(kbRoot(profileDir2), key);
|
|
36135
36476
|
}
|
|
36136
36477
|
function authedUrl(url, token) {
|
|
36137
36478
|
if (!token) return url;
|
|
@@ -36140,7 +36481,7 @@ function authedUrl(url, token) {
|
|
|
36140
36481
|
}
|
|
36141
36482
|
async function ensureRepo(profileDir2, repo) {
|
|
36142
36483
|
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
36143
|
-
if (
|
|
36484
|
+
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
36144
36485
|
const url = authedUrl(repo.url, repo.token);
|
|
36145
36486
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36146
36487
|
mkdirSync5(kbRoot(profileDir2), { recursive: true });
|
|
@@ -36151,11 +36492,11 @@ async function ensureRepo(profileDir2, repo) {
|
|
|
36151
36492
|
}
|
|
36152
36493
|
async function refreshSupportKb(profileDir2) {
|
|
36153
36494
|
const root = kbRoot(profileDir2);
|
|
36154
|
-
if (!
|
|
36495
|
+
if (!existsSync10(root)) return;
|
|
36155
36496
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36156
36497
|
for (const entry of readdirSync5(root)) {
|
|
36157
|
-
const dir2 =
|
|
36158
|
-
if (!
|
|
36498
|
+
const dir2 = join14(root, entry);
|
|
36499
|
+
if (!existsSync10(join14(dir2, ".git"))) continue;
|
|
36159
36500
|
try {
|
|
36160
36501
|
await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
|
|
36161
36502
|
} catch (err) {
|
|
@@ -36170,7 +36511,7 @@ function collectDocs(dir2) {
|
|
|
36170
36511
|
if (total >= MAX_CONTEXT_CHARS) return;
|
|
36171
36512
|
for (const entry of readdirSync5(current2)) {
|
|
36172
36513
|
if (entry === ".git" || entry === "node_modules") continue;
|
|
36173
|
-
const full =
|
|
36514
|
+
const full = join14(current2, entry);
|
|
36174
36515
|
const st = statSync4(full);
|
|
36175
36516
|
if (st.isDirectory()) {
|
|
36176
36517
|
walk2(full);
|
|
@@ -36235,11 +36576,11 @@ function imageExt(mime) {
|
|
|
36235
36576
|
}
|
|
36236
36577
|
}
|
|
36237
36578
|
function materializeImages2(profileDir2, images, question) {
|
|
36238
|
-
const dir2 =
|
|
36579
|
+
const dir2 = join14(profileDir2, "help-images", randomUUID());
|
|
36239
36580
|
mkdirSync5(dir2, { recursive: true });
|
|
36240
36581
|
let rewritten = question;
|
|
36241
36582
|
images.forEach((img, i) => {
|
|
36242
|
-
const file =
|
|
36583
|
+
const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
|
|
36243
36584
|
writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
|
|
36244
36585
|
if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
|
|
36245
36586
|
});
|
|
@@ -36334,7 +36675,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
36334
36675
|
if (!text) return { body: "", error: "empty answer" };
|
|
36335
36676
|
const { body, moderation } = parseModeratedAnswer(text);
|
|
36336
36677
|
if (!body) return { body: "", error: "empty answer" };
|
|
36337
|
-
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
36678
|
+
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
|
|
36338
36679
|
return {
|
|
36339
36680
|
body,
|
|
36340
36681
|
...moderation ? { moderation } : {},
|
|
@@ -36381,12 +36722,12 @@ function reportToolResult(tool, ok, message) {
|
|
|
36381
36722
|
}
|
|
36382
36723
|
|
|
36383
36724
|
// src/core/autonomous.ts
|
|
36384
|
-
import { readFile as readFile2, writeFile, chmod } from "fs/promises";
|
|
36725
|
+
import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
|
|
36385
36726
|
import { readFileSync as readFileSync13 } from "fs";
|
|
36386
|
-
import { execFile as
|
|
36387
|
-
import { join as
|
|
36388
|
-
import { promisify as
|
|
36389
|
-
var
|
|
36727
|
+
import { execFile as execFile4, spawn as spawn5 } from "child_process";
|
|
36728
|
+
import { join as join15 } from "path";
|
|
36729
|
+
import { promisify as promisify4 } from "util";
|
|
36730
|
+
var run4 = promisify4(execFile4);
|
|
36390
36731
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
36391
36732
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
36392
36733
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
@@ -36419,11 +36760,11 @@ function tableCell(value) {
|
|
|
36419
36760
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
36420
36761
|
}
|
|
36421
36762
|
function tickScript(root) {
|
|
36422
|
-
return
|
|
36763
|
+
return join15(root, TICK_REL);
|
|
36423
36764
|
}
|
|
36424
36765
|
async function crontabList() {
|
|
36425
36766
|
try {
|
|
36426
|
-
const { stdout } = await
|
|
36767
|
+
const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
|
|
36427
36768
|
return stdout;
|
|
36428
36769
|
} catch {
|
|
36429
36770
|
return "";
|
|
@@ -36442,8 +36783,8 @@ async function cronInstalled(root) {
|
|
|
36442
36783
|
return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
|
|
36443
36784
|
}
|
|
36444
36785
|
async function getAutonomousStatus(root) {
|
|
36445
|
-
const settings = parseJson(await readText(
|
|
36446
|
-
const hist = parseJson(await readText(
|
|
36786
|
+
const settings = parseJson(await readText(join15(root, SETTINGS_REL))) ?? {};
|
|
36787
|
+
const hist = parseJson(await readText(join15(root, HISTORIES_REL))) ?? {};
|
|
36447
36788
|
const records = Array.isArray(hist.records) ? hist.records : [];
|
|
36448
36789
|
const last = records[records.length - 1];
|
|
36449
36790
|
const ticks = hist.ticks ?? {};
|
|
@@ -36460,9 +36801,9 @@ async function getAutonomousStatus(root) {
|
|
|
36460
36801
|
}
|
|
36461
36802
|
function isAutonomousRunning(root) {
|
|
36462
36803
|
try {
|
|
36463
|
-
const s = JSON.parse(readFileSync13(
|
|
36804
|
+
const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
|
|
36464
36805
|
if (s.paused === true) return false;
|
|
36465
|
-
const h = JSON.parse(readFileSync13(
|
|
36806
|
+
const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
|
|
36466
36807
|
const ts = h.records?.[h.records.length - 1]?.ts;
|
|
36467
36808
|
if (!ts) return false;
|
|
36468
36809
|
const t2 = Date.parse(ts.replace(" ", "T"));
|
|
@@ -36473,10 +36814,10 @@ function isAutonomousRunning(root) {
|
|
|
36473
36814
|
}
|
|
36474
36815
|
async function readAutonomous(root) {
|
|
36475
36816
|
const [settings, approvals, status, ...books] = await Promise.all([
|
|
36476
|
-
readText(
|
|
36477
|
-
readText(
|
|
36817
|
+
readText(join15(root, SETTINGS_REL)),
|
|
36818
|
+
readText(join15(root, APPROVALS_REL), "{}"),
|
|
36478
36819
|
getAutonomousStatus(root),
|
|
36479
|
-
...Object.values(BOOK_FILES).map((f) => readText(
|
|
36820
|
+
...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
|
|
36480
36821
|
]);
|
|
36481
36822
|
const keys = Object.keys(BOOK_FILES);
|
|
36482
36823
|
const bookMap = {};
|
|
@@ -36485,7 +36826,7 @@ async function readAutonomous(root) {
|
|
|
36485
36826
|
}
|
|
36486
36827
|
async function readAutonomousLogs(root, date) {
|
|
36487
36828
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
36488
|
-
const text = await readText(
|
|
36829
|
+
const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
36489
36830
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
36490
36831
|
return { date: day, lines };
|
|
36491
36832
|
}
|
|
@@ -36509,22 +36850,22 @@ async function writeAutonomous(root, req, by) {
|
|
|
36509
36850
|
switch (req.kind) {
|
|
36510
36851
|
case "settings": {
|
|
36511
36852
|
if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
|
|
36512
|
-
await
|
|
36853
|
+
await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
|
|
36513
36854
|
break;
|
|
36514
36855
|
}
|
|
36515
36856
|
case "approvals": {
|
|
36516
|
-
const map = parseJson(await readText(
|
|
36857
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
36517
36858
|
if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36518
36859
|
else delete map[req.taskId];
|
|
36519
|
-
await
|
|
36860
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
36520
36861
|
break;
|
|
36521
36862
|
}
|
|
36522
36863
|
case "userTodo": {
|
|
36523
|
-
const cur = await readText(
|
|
36864
|
+
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
36524
36865
|
const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
36525
36866
|
const row = `| | ${tableCell(req.content)} | ${trace} |
|
|
36526
36867
|
`;
|
|
36527
|
-
await
|
|
36868
|
+
await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
36528
36869
|
break;
|
|
36529
36870
|
}
|
|
36530
36871
|
case "cron": {
|
|
@@ -36540,7 +36881,7 @@ async function writeAutonomous(root, req, by) {
|
|
|
36540
36881
|
}
|
|
36541
36882
|
|
|
36542
36883
|
// src/core/network-probe.ts
|
|
36543
|
-
import { existsSync as
|
|
36884
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
36544
36885
|
import { connect } from "net";
|
|
36545
36886
|
var CANARY_TARGETS = [
|
|
36546
36887
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -36549,7 +36890,7 @@ var CANARY_TARGETS = [
|
|
|
36549
36890
|
var PROBE_TIMEOUT_MS = 2500;
|
|
36550
36891
|
function detectContainerized() {
|
|
36551
36892
|
try {
|
|
36552
|
-
if (
|
|
36893
|
+
if (existsSync11("/.dockerenv")) return true;
|
|
36553
36894
|
const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
|
|
36554
36895
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
36555
36896
|
} catch {
|
|
@@ -36619,7 +36960,7 @@ async function probeNetwork() {
|
|
|
36619
36960
|
// src/core/git-auth.ts
|
|
36620
36961
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
36621
36962
|
import { homedir as homedir4 } from "os";
|
|
36622
|
-
import { join as
|
|
36963
|
+
import { join as join16 } from "path";
|
|
36623
36964
|
function tokenTarget(method) {
|
|
36624
36965
|
if (method === "gitlab-group-token") {
|
|
36625
36966
|
return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
|
|
@@ -36637,7 +36978,7 @@ function applyGitAuth(method) {
|
|
|
36637
36978
|
try {
|
|
36638
36979
|
const line = `https://${target.user}:${token}@${target.host}
|
|
36639
36980
|
`;
|
|
36640
|
-
writeFileSync8(
|
|
36981
|
+
writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
|
|
36641
36982
|
return true;
|
|
36642
36983
|
} catch {
|
|
36643
36984
|
return false;
|
|
@@ -36822,7 +37163,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
36822
37163
|
const apiErr = parser.apiError();
|
|
36823
37164
|
if (finalExit === 0 && !apiErr.isError) {
|
|
36824
37165
|
const usage = parser.usage();
|
|
36825
|
-
if (usage.tokens === 0) usage.tokens =
|
|
37166
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
|
|
36826
37167
|
reportToolResult(attempt.cmd, true);
|
|
36827
37168
|
return {
|
|
36828
37169
|
exitCode: 0,
|
|
@@ -36874,8 +37215,8 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
36874
37215
|
}
|
|
36875
37216
|
|
|
36876
37217
|
// src/ui/slash-commands.ts
|
|
36877
|
-
import { existsSync as
|
|
36878
|
-
import { join as
|
|
37218
|
+
import { existsSync as existsSync12 } from "fs";
|
|
37219
|
+
import { join as join17 } from "path";
|
|
36879
37220
|
var CONFIG_KEYS = [
|
|
36880
37221
|
"aiCli",
|
|
36881
37222
|
"claudeHome",
|
|
@@ -36887,7 +37228,7 @@ var CONFIG_KEYS = [
|
|
|
36887
37228
|
"aiEnv"
|
|
36888
37229
|
];
|
|
36889
37230
|
function configPath(profileDir2) {
|
|
36890
|
-
return
|
|
37231
|
+
return join17(profileDir2, "config.json");
|
|
36891
37232
|
}
|
|
36892
37233
|
function parseConfigAssignment(key, valueTokens) {
|
|
36893
37234
|
const value = valueTokens.join(" ").trim();
|
|
@@ -36937,7 +37278,7 @@ function runConfig(ctx) {
|
|
|
36937
37278
|
ctx.print(t("slash.config.wroteDefault", { verb, path }));
|
|
36938
37279
|
ctx.print(t("slash.config.addClaudeHint"));
|
|
36939
37280
|
};
|
|
36940
|
-
if (
|
|
37281
|
+
if (existsSync12(path)) {
|
|
36941
37282
|
ctx.confirm(
|
|
36942
37283
|
t("slash.config.replaceConfirm", { path }),
|
|
36943
37284
|
() => writeDefaults(t("slash.config.verbReplaced"))
|
|
@@ -37022,7 +37363,7 @@ function runOutput(ctx) {
|
|
|
37022
37363
|
}
|
|
37023
37364
|
function runLogs(ctx) {
|
|
37024
37365
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
37025
|
-
const dir2 =
|
|
37366
|
+
const dir2 = join17(ctx.info.profileDir, "logs");
|
|
37026
37367
|
const lines = readRecentLogLines(dir2, n);
|
|
37027
37368
|
if (lines.length === 0) {
|
|
37028
37369
|
ctx.print(t("slash.logs.none"));
|
|
@@ -37337,13 +37678,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
|
|
|
37337
37678
|
}
|
|
37338
37679
|
|
|
37339
37680
|
// src/core/git-commit-identity.ts
|
|
37340
|
-
import { execFile as
|
|
37341
|
-
import { promisify as
|
|
37342
|
-
var
|
|
37681
|
+
import { execFile as execFile5 } from "child_process";
|
|
37682
|
+
import { promisify as promisify5 } from "util";
|
|
37683
|
+
var run5 = promisify5(execFile5);
|
|
37343
37684
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
37344
37685
|
if (!machineUsername) return;
|
|
37345
37686
|
try {
|
|
37346
|
-
await
|
|
37687
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
37347
37688
|
} catch {
|
|
37348
37689
|
}
|
|
37349
37690
|
}
|
|
@@ -37442,7 +37783,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
37442
37783
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
37443
37784
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
37444
37785
|
if (perPromptLimit > 0) {
|
|
37445
|
-
const estimated =
|
|
37786
|
+
const estimated = estimateTokens2(prompt);
|
|
37446
37787
|
if (estimated > perPromptLimit) {
|
|
37447
37788
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
37448
37789
|
return;
|
|
@@ -37859,15 +38200,15 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
37859
38200
|
}
|
|
37860
38201
|
|
|
37861
38202
|
// src/core/fs-browse.ts
|
|
37862
|
-
import { readdir as
|
|
37863
|
-
import { resolve, sep as sep2 } from "path";
|
|
38203
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
38204
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
37864
38205
|
async function listDir(path, root) {
|
|
37865
38206
|
if (!root) return { path: "", entries: [] };
|
|
37866
|
-
const base =
|
|
37867
|
-
const requested =
|
|
38207
|
+
const base = resolve2(root);
|
|
38208
|
+
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
37868
38209
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
37869
38210
|
try {
|
|
37870
|
-
const dirents = await
|
|
38211
|
+
const dirents = await readdir3(target, { withFileTypes: true });
|
|
37871
38212
|
const entries2 = dirents.map((d) => ({
|
|
37872
38213
|
name: d.name,
|
|
37873
38214
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -37883,12 +38224,12 @@ async function listDir(path, root) {
|
|
|
37883
38224
|
|
|
37884
38225
|
// src/core/fs-read.ts
|
|
37885
38226
|
import { readFile as readFile3 } from "fs/promises";
|
|
37886
|
-
import { resolve as
|
|
38227
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
37887
38228
|
var MAX_BYTES = 256 * 1024;
|
|
37888
38229
|
function resolveInRoot(root, relPath) {
|
|
37889
38230
|
if (!root) return null;
|
|
37890
|
-
const base =
|
|
37891
|
-
const target =
|
|
38231
|
+
const base = resolve3(root);
|
|
38232
|
+
const target = resolve3(base, relPath || ".");
|
|
37892
38233
|
return target === base || target.startsWith(base + sep3) ? target : null;
|
|
37893
38234
|
}
|
|
37894
38235
|
async function readWorkerFile(root, path) {
|
|
@@ -37907,13 +38248,13 @@ async function readWorkerFile(root, path) {
|
|
|
37907
38248
|
}
|
|
37908
38249
|
|
|
37909
38250
|
// src/core/fs-write.ts
|
|
37910
|
-
import { mkdir as
|
|
37911
|
-
import { dirname as
|
|
38251
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
38252
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
37912
38253
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
37913
38254
|
function resolveInRoot2(root, relPath) {
|
|
37914
38255
|
if (!root) return null;
|
|
37915
|
-
const base =
|
|
37916
|
-
const target =
|
|
38256
|
+
const base = resolve4(root);
|
|
38257
|
+
const target = resolve4(base, relPath || ".");
|
|
37917
38258
|
return target === base || target.startsWith(base + sep4) ? target : null;
|
|
37918
38259
|
}
|
|
37919
38260
|
async function writeWorkerFile(root, relPath, content) {
|
|
@@ -37926,8 +38267,8 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37926
38267
|
return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
|
|
37927
38268
|
}
|
|
37928
38269
|
try {
|
|
37929
|
-
await
|
|
37930
|
-
await
|
|
38270
|
+
await mkdir3(dirname4(target), { recursive: true });
|
|
38271
|
+
await writeFile3(target, content, "utf8");
|
|
37931
38272
|
return { ok: true, path: target, bytes };
|
|
37932
38273
|
} catch (err) {
|
|
37933
38274
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -37935,12 +38276,12 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37935
38276
|
}
|
|
37936
38277
|
|
|
37937
38278
|
// src/core/fs-mutate.ts
|
|
37938
|
-
import { mkdir as
|
|
37939
|
-
import { basename as basename3, dirname as
|
|
38279
|
+
import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
|
|
38280
|
+
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
37940
38281
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
37941
38282
|
function resolveInRoot3(root, rel) {
|
|
37942
|
-
const base =
|
|
37943
|
-
const target =
|
|
38283
|
+
const base = resolve5(root);
|
|
38284
|
+
const target = resolve5(base, rel || ".");
|
|
37944
38285
|
return target === base || target.startsWith(base + sep5) ? target : null;
|
|
37945
38286
|
}
|
|
37946
38287
|
async function isDir(p) {
|
|
@@ -37965,39 +38306,39 @@ async function mutateFs(root, req) {
|
|
|
37965
38306
|
switch (req.op) {
|
|
37966
38307
|
case "mkdir": {
|
|
37967
38308
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37968
|
-
if (!target || target ===
|
|
37969
|
-
await
|
|
38309
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
38310
|
+
await mkdir4(target, { recursive: true });
|
|
37970
38311
|
return { ok: true, path: target };
|
|
37971
38312
|
}
|
|
37972
38313
|
case "create": {
|
|
37973
38314
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37974
|
-
if (!target || target ===
|
|
38315
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
37975
38316
|
if (await exists(target)) return { ok: false, path: target, error: "already exists" };
|
|
37976
38317
|
const content = req.content ?? "";
|
|
37977
38318
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
|
|
37978
38319
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
37979
38320
|
}
|
|
37980
|
-
await
|
|
37981
|
-
await
|
|
38321
|
+
await mkdir4(dirname5(target), { recursive: true });
|
|
38322
|
+
await writeFile4(target, content, "utf8");
|
|
37982
38323
|
return { ok: true, path: target };
|
|
37983
38324
|
}
|
|
37984
38325
|
case "move": {
|
|
37985
38326
|
const from = resolveInRoot3(root, req.from ?? "");
|
|
37986
38327
|
let to = resolveInRoot3(root, req.to ?? "");
|
|
37987
38328
|
if (!from || !to) return escaped(req.from ?? req.to ?? "");
|
|
37988
|
-
if (from ===
|
|
37989
|
-
if (await isDir(to)) to =
|
|
37990
|
-
if (to !==
|
|
38329
|
+
if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
|
|
38330
|
+
if (await isDir(to)) to = resolve5(to, basename3(from));
|
|
38331
|
+
if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
37991
38332
|
if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
|
|
37992
|
-
await
|
|
37993
|
-
await
|
|
38333
|
+
await mkdir4(dirname5(to), { recursive: true });
|
|
38334
|
+
await rename2(from, to);
|
|
37994
38335
|
return { ok: true, path: to };
|
|
37995
38336
|
}
|
|
37996
38337
|
case "delete": {
|
|
37997
38338
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37998
38339
|
if (!target) return escaped(req.path ?? "");
|
|
37999
|
-
if (target ===
|
|
38000
|
-
await
|
|
38340
|
+
if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
38341
|
+
await rm2(target, { recursive: true, force: true });
|
|
38001
38342
|
return { ok: true, path: target };
|
|
38002
38343
|
}
|
|
38003
38344
|
default:
|
|
@@ -38009,12 +38350,12 @@ async function mutateFs(root, req) {
|
|
|
38009
38350
|
}
|
|
38010
38351
|
|
|
38011
38352
|
// src/core/fs-transfer.ts
|
|
38012
|
-
import { mkdir as
|
|
38013
|
-
import { basename as basename4, dirname as
|
|
38353
|
+
import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
38354
|
+
import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
|
|
38014
38355
|
function resolveInRoot4(root, relPath) {
|
|
38015
38356
|
if (!root) return null;
|
|
38016
|
-
const base =
|
|
38017
|
-
const target =
|
|
38357
|
+
const base = resolve6(root);
|
|
38358
|
+
const target = resolve6(base, relPath || ".");
|
|
38018
38359
|
return target === base || target.startsWith(base + sep6) ? target : null;
|
|
38019
38360
|
}
|
|
38020
38361
|
var MIME_BY_EXT = {
|
|
@@ -38053,8 +38394,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
38053
38394
|
return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
38054
38395
|
}
|
|
38055
38396
|
try {
|
|
38056
|
-
await
|
|
38057
|
-
await
|
|
38397
|
+
await mkdir5(dirname6(target), { recursive: true });
|
|
38398
|
+
await writeFile5(target, buf);
|
|
38058
38399
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
38059
38400
|
} catch (err) {
|
|
38060
38401
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -38129,14 +38470,14 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
38129
38470
|
|
|
38130
38471
|
// src/core/git-diff.ts
|
|
38131
38472
|
import { readFile as readFile5 } from "fs/promises";
|
|
38132
|
-
import { execFile as
|
|
38133
|
-
import { basename as basename5, dirname as
|
|
38134
|
-
import { promisify as
|
|
38135
|
-
var
|
|
38473
|
+
import { execFile as execFile6 } from "child_process";
|
|
38474
|
+
import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
|
|
38475
|
+
import { promisify as promisify6 } from "util";
|
|
38476
|
+
var run6 = promisify6(execFile6);
|
|
38136
38477
|
function resolveInRoot5(root, relPath) {
|
|
38137
38478
|
if (!root) return null;
|
|
38138
|
-
const base =
|
|
38139
|
-
const target =
|
|
38479
|
+
const base = resolve7(root);
|
|
38480
|
+
const target = resolve7(base, relPath || ".");
|
|
38140
38481
|
return target === base || target.startsWith(base + sep7) ? target : null;
|
|
38141
38482
|
}
|
|
38142
38483
|
function looksBinary(buf) {
|
|
@@ -38151,7 +38492,7 @@ async function gitDiff(root, path) {
|
|
|
38151
38492
|
if (!target) {
|
|
38152
38493
|
return { path, oldContent: "", newContent: "", isBinary: false };
|
|
38153
38494
|
}
|
|
38154
|
-
const dir2 =
|
|
38495
|
+
const dir2 = dirname7(target);
|
|
38155
38496
|
const base = basename5(target);
|
|
38156
38497
|
let newBuf = null;
|
|
38157
38498
|
try {
|
|
@@ -38160,7 +38501,7 @@ async function gitDiff(root, path) {
|
|
|
38160
38501
|
}
|
|
38161
38502
|
let oldBuf = null;
|
|
38162
38503
|
try {
|
|
38163
|
-
const { stdout } = await
|
|
38504
|
+
const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
|
|
38164
38505
|
cwd: dir2,
|
|
38165
38506
|
timeout: 15e3,
|
|
38166
38507
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -38190,12 +38531,12 @@ async function gitDiff(root, path) {
|
|
|
38190
38531
|
}
|
|
38191
38532
|
|
|
38192
38533
|
// src/core/git-env.ts
|
|
38193
|
-
import { execFile as
|
|
38194
|
-
import { promisify as
|
|
38195
|
-
var
|
|
38534
|
+
import { execFile as execFile7 } from "child_process";
|
|
38535
|
+
import { promisify as promisify7 } from "util";
|
|
38536
|
+
var run7 = promisify7(execFile7);
|
|
38196
38537
|
async function hasBinary(bin, args = ["--version"]) {
|
|
38197
38538
|
try {
|
|
38198
|
-
await
|
|
38539
|
+
await run7(bin, args, { timeout: 5e3 });
|
|
38199
38540
|
return true;
|
|
38200
38541
|
} catch {
|
|
38201
38542
|
return false;
|
|
@@ -38208,7 +38549,7 @@ async function checkGitEnv(provider) {
|
|
|
38208
38549
|
let account = null;
|
|
38209
38550
|
if (installed) {
|
|
38210
38551
|
try {
|
|
38211
|
-
const { stdout, stderr } = await
|
|
38552
|
+
const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
|
|
38212
38553
|
const out = `${stdout}
|
|
38213
38554
|
${stderr}`;
|
|
38214
38555
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -38221,14 +38562,14 @@ ${stderr}`;
|
|
|
38221
38562
|
}
|
|
38222
38563
|
|
|
38223
38564
|
// 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
|
|
38565
|
+
import { execFile as execFile8 } from "child_process";
|
|
38566
|
+
import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
|
|
38567
|
+
import { join as join18 } from "path";
|
|
38568
|
+
import { promisify as promisify8 } from "util";
|
|
38569
|
+
var run8 = promisify8(execFile8);
|
|
38229
38570
|
function keyPaths(profileDir2) {
|
|
38230
|
-
const dir2 =
|
|
38231
|
-
const key =
|
|
38571
|
+
const dir2 = join18(profileDir2, ".ssh");
|
|
38572
|
+
const key = join18(dir2, "id_4pm");
|
|
38232
38573
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
38233
38574
|
}
|
|
38234
38575
|
async function readPublicKey(pub) {
|
|
@@ -38240,14 +38581,14 @@ async function readPublicKey(pub) {
|
|
|
38240
38581
|
}
|
|
38241
38582
|
async function fingerprintOf(pub) {
|
|
38242
38583
|
try {
|
|
38243
|
-
const { stdout } = await
|
|
38584
|
+
const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
38244
38585
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
38245
38586
|
} catch {
|
|
38246
38587
|
return null;
|
|
38247
38588
|
}
|
|
38248
38589
|
}
|
|
38249
38590
|
async function configureGit(key) {
|
|
38250
|
-
await
|
|
38591
|
+
await run8(
|
|
38251
38592
|
"git",
|
|
38252
38593
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
38253
38594
|
{ timeout: 5e3 }
|
|
@@ -38255,10 +38596,10 @@ async function configureGit(key) {
|
|
|
38255
38596
|
}
|
|
38256
38597
|
async function generate(profileDir2) {
|
|
38257
38598
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
38258
|
-
await
|
|
38259
|
-
await
|
|
38260
|
-
await
|
|
38261
|
-
await
|
|
38599
|
+
await mkdir6(dir2, { recursive: true });
|
|
38600
|
+
await rm3(key, { force: true });
|
|
38601
|
+
await rm3(pub, { force: true });
|
|
38602
|
+
await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
38262
38603
|
timeout: 15e3
|
|
38263
38604
|
});
|
|
38264
38605
|
await configureGit(key);
|
|
@@ -38266,9 +38607,9 @@ async function generate(profileDir2) {
|
|
|
38266
38607
|
}
|
|
38267
38608
|
async function remove(profileDir2) {
|
|
38268
38609
|
const { key, pub } = keyPaths(profileDir2);
|
|
38269
|
-
await
|
|
38270
|
-
await
|
|
38271
|
-
await
|
|
38610
|
+
await rm3(key, { force: true });
|
|
38611
|
+
await rm3(pub, { force: true });
|
|
38612
|
+
await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
38272
38613
|
() => void 0
|
|
38273
38614
|
);
|
|
38274
38615
|
return { publicKey: null, fingerprint: null };
|
|
@@ -38289,22 +38630,22 @@ async function manageSshKey(op, profileDir2) {
|
|
|
38289
38630
|
}
|
|
38290
38631
|
|
|
38291
38632
|
// src/core/git-history.ts
|
|
38292
|
-
import { readdir as
|
|
38293
|
-
import { execFile as
|
|
38294
|
-
import { resolve as
|
|
38295
|
-
import { promisify as
|
|
38296
|
-
var
|
|
38633
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
38634
|
+
import { execFile as execFile9 } from "child_process";
|
|
38635
|
+
import { resolve as resolve8, sep as sep8 } from "path";
|
|
38636
|
+
import { promisify as promisify9 } from "util";
|
|
38637
|
+
var run9 = promisify9(execFile9);
|
|
38297
38638
|
var FS = "";
|
|
38298
38639
|
var RS = "";
|
|
38299
38640
|
function resolveRepo(root, repo) {
|
|
38300
38641
|
if (!root) return null;
|
|
38301
|
-
const base =
|
|
38302
|
-
const requested =
|
|
38642
|
+
const base = resolve8(root);
|
|
38643
|
+
const requested = resolve8(base, repo && repo.trim() ? repo : ".");
|
|
38303
38644
|
return requested === base || requested.startsWith(base + sep8) ? requested : base;
|
|
38304
38645
|
}
|
|
38305
38646
|
async function git(cwd2, args) {
|
|
38306
38647
|
try {
|
|
38307
|
-
const { stdout } = await
|
|
38648
|
+
const { stdout } = await run9("git", args, {
|
|
38308
38649
|
cwd: cwd2,
|
|
38309
38650
|
timeout: 15e3,
|
|
38310
38651
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -38316,7 +38657,7 @@ async function git(cwd2, args) {
|
|
|
38316
38657
|
}
|
|
38317
38658
|
async function gitRepos(root) {
|
|
38318
38659
|
if (!root) return { repos: [] };
|
|
38319
|
-
const base =
|
|
38660
|
+
const base = resolve8(root);
|
|
38320
38661
|
const found = /* @__PURE__ */ new Set();
|
|
38321
38662
|
const repos = [];
|
|
38322
38663
|
const add = async (dir2) => {
|
|
@@ -38330,16 +38671,16 @@ async function gitRepos(root) {
|
|
|
38330
38671
|
};
|
|
38331
38672
|
await add(base);
|
|
38332
38673
|
try {
|
|
38333
|
-
const lvl1 = await
|
|
38674
|
+
const lvl1 = await readdir4(base, { withFileTypes: true });
|
|
38334
38675
|
for (const d of lvl1) {
|
|
38335
38676
|
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
38336
|
-
const p1 =
|
|
38677
|
+
const p1 = resolve8(base, d.name);
|
|
38337
38678
|
await add(p1);
|
|
38338
38679
|
try {
|
|
38339
|
-
const lvl2 = await
|
|
38680
|
+
const lvl2 = await readdir4(p1, { withFileTypes: true });
|
|
38340
38681
|
for (const d2 of lvl2) {
|
|
38341
38682
|
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
38342
|
-
await add(
|
|
38683
|
+
await add(resolve8(p1, d2.name));
|
|
38343
38684
|
}
|
|
38344
38685
|
} catch {
|
|
38345
38686
|
}
|
|
@@ -38457,79 +38798,7 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
38457
38798
|
}
|
|
38458
38799
|
|
|
38459
38800
|
// 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
|
-
}
|
|
38801
|
+
import { join as join19 } from "path";
|
|
38533
38802
|
|
|
38534
38803
|
// src/core/outbound-review.ts
|
|
38535
38804
|
function compilePatterns(sources, fallback) {
|
|
@@ -38605,9 +38874,13 @@ async function evaluateReview(payload) {
|
|
|
38605
38874
|
// src/core/ws-client/handlers/misc.ts
|
|
38606
38875
|
function handleMiscChannels(ctx, message, payload) {
|
|
38607
38876
|
switch (message.channel) {
|
|
38877
|
+
case WsChannels.CLI_UPDATE: {
|
|
38878
|
+
ctx.updateCliNow();
|
|
38879
|
+
return true;
|
|
38880
|
+
}
|
|
38608
38881
|
case WsChannels.LOG_READ: {
|
|
38609
38882
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
38610
|
-
const lines = readRecentLogLines(
|
|
38883
|
+
const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
|
|
38611
38884
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
38612
38885
|
return true;
|
|
38613
38886
|
}
|
|
@@ -38640,161 +38913,6 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
38640
38913
|
// src/core/ws-client/handlers/project.ts
|
|
38641
38914
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
|
|
38642
38915
|
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
38916
|
function handleProjectChannels(ctx, message, payload) {
|
|
38799
38917
|
switch (message.channel) {
|
|
38800
38918
|
case WsChannels.PHYSIC_SYNC: {
|
|
@@ -38815,6 +38933,7 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
38815
38933
|
ctx.physicRoot = ctx.physicFolderPath(sync.newName);
|
|
38816
38934
|
ctx.bus.setProject(sync.newName);
|
|
38817
38935
|
ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
|
|
38936
|
+
void ctx.cloneServingRepos().catch(() => void 0);
|
|
38818
38937
|
return true;
|
|
38819
38938
|
}
|
|
38820
38939
|
case WsChannels.PHYSIC_DELETE: {
|
|
@@ -39088,7 +39207,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
39088
39207
|
}
|
|
39089
39208
|
|
|
39090
39209
|
// src/core/agents.ts
|
|
39091
|
-
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as
|
|
39210
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
|
|
39092
39211
|
import { join as join21 } from "path";
|
|
39093
39212
|
var AGENTS_REL = ".claude/agents";
|
|
39094
39213
|
var SKILLS_REL = ".claude/skills";
|
|
@@ -39113,7 +39232,7 @@ async function listAgents(root) {
|
|
|
39113
39232
|
const subagents = [];
|
|
39114
39233
|
const skills = [];
|
|
39115
39234
|
try {
|
|
39116
|
-
for (const e of await
|
|
39235
|
+
for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
|
|
39117
39236
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
39118
39237
|
const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
|
|
39119
39238
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
@@ -39121,7 +39240,7 @@ async function listAgents(root) {
|
|
|
39121
39240
|
} catch {
|
|
39122
39241
|
}
|
|
39123
39242
|
try {
|
|
39124
|
-
for (const e of await
|
|
39243
|
+
for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
|
|
39125
39244
|
if (!e.isDirectory()) continue;
|
|
39126
39245
|
const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
39127
39246
|
skills.push({ name: e.name, description: fm.description });
|
|
@@ -39151,8 +39270,8 @@ async function writeAgent(root, req) {
|
|
|
39151
39270
|
if (!n) return { ok: false, error: "invalid name" };
|
|
39152
39271
|
try {
|
|
39153
39272
|
if (req.action === "delete") {
|
|
39154
|
-
if (req.kind === "subagent") await
|
|
39155
|
-
else await
|
|
39273
|
+
if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
|
|
39274
|
+
else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
39156
39275
|
return { ok: true };
|
|
39157
39276
|
}
|
|
39158
39277
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
@@ -39273,7 +39392,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
39273
39392
|
|
|
39274
39393
|
// src/core/packages.ts
|
|
39275
39394
|
import { createHash as createHash4 } from "crypto";
|
|
39276
|
-
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as
|
|
39395
|
+
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
|
|
39277
39396
|
import { dirname as dirname9, join as join24, relative } from "path";
|
|
39278
39397
|
var AGENTS_REL2 = ".claude/agents";
|
|
39279
39398
|
var SKILLS_REL2 = ".claude/skills";
|
|
@@ -39293,7 +39412,7 @@ async function listFilesRel(dir2) {
|
|
|
39293
39412
|
async function walk2(d) {
|
|
39294
39413
|
let entries2;
|
|
39295
39414
|
try {
|
|
39296
|
-
entries2 = await
|
|
39415
|
+
entries2 = await readdir6(d, { withFileTypes: true });
|
|
39297
39416
|
} catch {
|
|
39298
39417
|
return;
|
|
39299
39418
|
}
|
|
@@ -39446,11 +39565,11 @@ async function removePackage(root, slug) {
|
|
|
39446
39565
|
const entry = manifest[n];
|
|
39447
39566
|
if (!entry) return { ok: false, error: "not installed" };
|
|
39448
39567
|
if (entry.kind === "subagent") {
|
|
39449
|
-
await
|
|
39450
|
-
await
|
|
39451
|
-
await
|
|
39568
|
+
await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
39569
|
+
await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
39570
|
+
await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
39452
39571
|
} else {
|
|
39453
|
-
await
|
|
39572
|
+
await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
39454
39573
|
}
|
|
39455
39574
|
delete manifest[n];
|
|
39456
39575
|
await writeManifest(root, manifest);
|
|
@@ -39461,7 +39580,7 @@ async function removePackage(root, slug) {
|
|
|
39461
39580
|
}
|
|
39462
39581
|
|
|
39463
39582
|
// src/core/graph.ts
|
|
39464
|
-
import { readFile as readFile11, readdir as
|
|
39583
|
+
import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
|
|
39465
39584
|
import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
39466
39585
|
var MAX_FILES = 2e3;
|
|
39467
39586
|
var MAX_NODES = 3e3;
|
|
@@ -39474,7 +39593,7 @@ async function walk(root, accept) {
|
|
|
39474
39593
|
const dir2 = stack.pop();
|
|
39475
39594
|
let entries2;
|
|
39476
39595
|
try {
|
|
39477
|
-
entries2 = await
|
|
39596
|
+
entries2 = await readdir7(dir2, { withFileTypes: true });
|
|
39478
39597
|
} catch {
|
|
39479
39598
|
continue;
|
|
39480
39599
|
}
|
|
@@ -40170,6 +40289,9 @@ var WsClient = class {
|
|
|
40170
40289
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
40171
40290
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
40172
40291
|
physicRoot = null;
|
|
40292
|
+
/** Declared repos of the served project (ADR-0289), from each ws_token — cloned into the physic
|
|
40293
|
+
* root on connect (and after a PHYSIC_SYNC creates the folder) when any is missing its `.git`. */
|
|
40294
|
+
servingRepos = [];
|
|
40173
40295
|
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
40174
40296
|
* Push uses the account already logged in with gh/glab on the worker. */
|
|
40175
40297
|
machineUsername = "";
|
|
@@ -40287,9 +40409,24 @@ var WsClient = class {
|
|
|
40287
40409
|
setConsoleWatching: (on) => self.setConsoleWatching(on),
|
|
40288
40410
|
setMetricsWatching: (on) => self.setMetricsWatching(on),
|
|
40289
40411
|
ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
|
|
40290
|
-
physicFolderPath: (projectName) => self.physicFolderPath(projectName)
|
|
40412
|
+
physicFolderPath: (projectName) => self.physicFolderPath(projectName),
|
|
40413
|
+
cloneServingRepos: () => self.ensureServingReposCloned(),
|
|
40414
|
+
updateCliNow: () => self.updateScheduler.updateNow()
|
|
40291
40415
|
};
|
|
40292
40416
|
}
|
|
40417
|
+
/**
|
|
40418
|
+
* Clone any declared repo missing from the served physic root (ADR-0289) — idempotent (skips a
|
|
40419
|
+
* repo that already has `.git`). Best-effort: a clone failure never disrupts the session (the web
|
|
40420
|
+
* still surfaces "not provisioned"). No-op when idle (no physic root) or no repos declared.
|
|
40421
|
+
*/
|
|
40422
|
+
async ensureServingReposCloned() {
|
|
40423
|
+
if (!this.physicRoot || this.servingRepos.length === 0) return;
|
|
40424
|
+
try {
|
|
40425
|
+
await ensureReposCloned(this.physicRoot, this.servingRepos, (_step, message) => this.bus.log(message));
|
|
40426
|
+
} catch (err) {
|
|
40427
|
+
logger.warn("repos.clone.failed", { error: err instanceof Error ? err.message : String(err) });
|
|
40428
|
+
}
|
|
40429
|
+
}
|
|
40293
40430
|
/**
|
|
40294
40431
|
* Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
|
|
40295
40432
|
* DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
|
|
@@ -40473,6 +40610,8 @@ var WsClient = class {
|
|
|
40473
40610
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
40474
40611
|
applyGitAuth(token.gitAuth ?? null);
|
|
40475
40612
|
this.machineUsername = token.machineUsername ?? "";
|
|
40613
|
+
this.servingRepos = token.repos ?? [];
|
|
40614
|
+
void this.ensureServingReposCloned();
|
|
40476
40615
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
40477
40616
|
this.credential.scope = token.scope;
|
|
40478
40617
|
writeCredential(this.context.profileDir, this.credential);
|