@4pm/cli 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +419 -239
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32753,7 +32753,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32753
32753
|
|
|
32754
32754
|
// src/core/ws-client.ts
|
|
32755
32755
|
init_wrapper();
|
|
32756
|
-
import { randomUUID as
|
|
32756
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
32757
32757
|
import { basename as basename7, join as join32 } from "path";
|
|
32758
32758
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
|
|
32759
32759
|
|
|
@@ -37564,116 +37564,12 @@ var userCardsQuerySchema = external_exports.object({
|
|
|
37564
37564
|
// src/core/scaffold.ts
|
|
37565
37565
|
import { cp, mkdir, mkdtemp, rm, writeFile } from "fs/promises";
|
|
37566
37566
|
import { existsSync as existsSync4 } from "fs";
|
|
37567
|
-
import { execFile
|
|
37567
|
+
import { execFile } from "child_process";
|
|
37568
37568
|
import { tmpdir as tmpdir2 } from "os";
|
|
37569
|
-
import { dirname as dirname3, join as join6, resolve
|
|
37569
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
37570
37570
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
37571
|
-
import { promisify as promisify2 } from "util";
|
|
37572
|
-
|
|
37573
|
-
// src/core/ai-assist.ts
|
|
37574
|
-
import { execFile } from "child_process";
|
|
37575
37571
|
import { promisify } from "util";
|
|
37576
|
-
|
|
37577
|
-
// src/utils/agent-deny.ts
|
|
37578
|
-
import { isAbsolute, resolve } from "path";
|
|
37579
|
-
var HOME_SECRET_PATTERNS = [
|
|
37580
|
-
"~/.4pm/**",
|
|
37581
|
-
"~/.ssh/**",
|
|
37582
|
-
"~/.config/gh/**",
|
|
37583
|
-
"~/.git-credentials",
|
|
37584
|
-
"~/.claude*",
|
|
37585
|
-
"~/.claude*/**"
|
|
37586
|
-
];
|
|
37587
|
-
function absDirPattern(dir2) {
|
|
37588
|
-
const abs = (isAbsolute(dir2) ? dir2 : resolve(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
37589
|
-
return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
|
|
37590
|
-
}
|
|
37591
|
-
function agentDenyRules(profileDirs2 = []) {
|
|
37592
|
-
const patterns = new Set(HOME_SECRET_PATTERNS);
|
|
37593
|
-
for (const dir2 of profileDirs2) {
|
|
37594
|
-
if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
|
|
37595
|
-
}
|
|
37596
|
-
const rules = [];
|
|
37597
|
-
for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
|
|
37598
|
-
return rules;
|
|
37599
|
-
}
|
|
37600
|
-
function denySettingsArgs(cmd, profileDirs2 = []) {
|
|
37601
|
-
if (!cmd.includes("claude")) return [];
|
|
37602
|
-
return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
|
|
37603
|
-
}
|
|
37604
|
-
|
|
37605
|
-
// src/core/ai-assist.ts
|
|
37606
37572
|
var run = promisify(execFile);
|
|
37607
|
-
function aiCli() {
|
|
37608
|
-
return process.env.AI_CLI || "claude";
|
|
37609
|
-
}
|
|
37610
|
-
function aiArgs(cli, prompt) {
|
|
37611
|
-
if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
|
|
37612
|
-
if (cli === "codex") return ["exec", "--json", prompt];
|
|
37613
|
-
return ["-p", prompt];
|
|
37614
|
-
}
|
|
37615
|
-
function estimateTokens(prompt, output) {
|
|
37616
|
-
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
37617
|
-
}
|
|
37618
|
-
function parseAi(cli, stdout, prompt) {
|
|
37619
|
-
if (cli === "claude") {
|
|
37620
|
-
try {
|
|
37621
|
-
const j2 = JSON.parse(stdout);
|
|
37622
|
-
const text2 = (j2.result ?? "").trim();
|
|
37623
|
-
const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
|
|
37624
|
-
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
|
|
37625
|
-
} catch {
|
|
37626
|
-
}
|
|
37627
|
-
}
|
|
37628
|
-
if (cli === "codex") {
|
|
37629
|
-
try {
|
|
37630
|
-
let text2 = "";
|
|
37631
|
-
let tokens = 0;
|
|
37632
|
-
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
37633
|
-
const obj = JSON.parse(line);
|
|
37634
|
-
if (obj.text) text2 = obj.text;
|
|
37635
|
-
else if (obj.message) text2 = obj.message;
|
|
37636
|
-
if (obj.usage) {
|
|
37637
|
-
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
37638
|
-
}
|
|
37639
|
-
}
|
|
37640
|
-
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
|
|
37641
|
-
} catch {
|
|
37642
|
-
}
|
|
37643
|
-
}
|
|
37644
|
-
const text = stdout.trim();
|
|
37645
|
-
return { text, tokens: estimateTokens(prompt, text) };
|
|
37646
|
-
}
|
|
37647
|
-
async function runAi(prompt) {
|
|
37648
|
-
const cli = aiCli();
|
|
37649
|
-
const { stdout } = await run(cli, aiArgs(cli, prompt), {
|
|
37650
|
-
timeout: 12e4,
|
|
37651
|
-
maxBuffer: 8 * 1024 * 1024
|
|
37652
|
-
});
|
|
37653
|
-
return parseAi(cli, stdout, prompt);
|
|
37654
|
-
}
|
|
37655
|
-
async function aiGenerate(prompt) {
|
|
37656
|
-
const { text } = await runAi(prompt);
|
|
37657
|
-
return text;
|
|
37658
|
-
}
|
|
37659
|
-
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
37660
|
-
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:
|
|
37661
|
-
${input}`;
|
|
37662
|
-
const { text, tokens } = await runAi(prompt);
|
|
37663
|
-
const start = text.indexOf("{");
|
|
37664
|
-
const end = text.lastIndexOf("}");
|
|
37665
|
-
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
37666
|
-
try {
|
|
37667
|
-
const j2 = JSON.parse(text.slice(start, end + 1));
|
|
37668
|
-
const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
|
|
37669
|
-
return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
|
|
37670
|
-
} catch {
|
|
37671
|
-
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
37672
|
-
}
|
|
37673
|
-
}
|
|
37674
|
-
|
|
37675
|
-
// src/core/scaffold.ts
|
|
37676
|
-
var run2 = promisify2(execFile2);
|
|
37677
37573
|
function reposOf(spec) {
|
|
37678
37574
|
const repos = spec?.repos;
|
|
37679
37575
|
return Array.isArray(repos) ? repos : [];
|
|
@@ -37695,7 +37591,7 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37695
37591
|
if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
|
|
37696
37592
|
emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
|
|
37697
37593
|
try {
|
|
37698
|
-
await
|
|
37594
|
+
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37699
37595
|
} catch {
|
|
37700
37596
|
}
|
|
37701
37597
|
continue;
|
|
@@ -37709,11 +37605,11 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37709
37605
|
const b2 = (sub.branch ?? "").trim();
|
|
37710
37606
|
if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
|
|
37711
37607
|
try {
|
|
37712
|
-
await
|
|
37713
|
-
await
|
|
37608
|
+
await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37609
|
+
await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
|
|
37714
37610
|
try {
|
|
37715
|
-
await
|
|
37716
|
-
await
|
|
37611
|
+
await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
|
|
37612
|
+
await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
|
|
37717
37613
|
} catch (pushErr) {
|
|
37718
37614
|
const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
37719
37615
|
emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
|
|
@@ -37728,13 +37624,13 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37728
37624
|
}
|
|
37729
37625
|
}
|
|
37730
37626
|
try {
|
|
37731
|
-
await
|
|
37627
|
+
await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37732
37628
|
added++;
|
|
37733
37629
|
} catch (err) {
|
|
37734
37630
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37735
37631
|
if (/already exists|already registered|in the index/i.test(msg)) {
|
|
37736
37632
|
try {
|
|
37737
|
-
await
|
|
37633
|
+
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37738
37634
|
added++;
|
|
37739
37635
|
} catch {
|
|
37740
37636
|
}
|
|
@@ -37753,18 +37649,18 @@ async function commitAndPushSubmodules(root, submodules, emit) {
|
|
|
37753
37649
|
];
|
|
37754
37650
|
if (addPaths.length === 0) return;
|
|
37755
37651
|
emit("submodule", "Committing .gitmodules\u2026");
|
|
37756
|
-
await
|
|
37757
|
-
const staged = (await
|
|
37652
|
+
await run("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
|
|
37653
|
+
const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37758
37654
|
if (!staged) return;
|
|
37759
37655
|
const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
|
|
37760
37656
|
try {
|
|
37761
|
-
await
|
|
37657
|
+
await run("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37762
37658
|
} catch {
|
|
37763
|
-
await
|
|
37659
|
+
await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37764
37660
|
}
|
|
37765
37661
|
emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
|
|
37766
37662
|
try {
|
|
37767
|
-
await
|
|
37663
|
+
await run("git", ["push"], { cwd: root, timeout: 12e4 });
|
|
37768
37664
|
} catch (err) {
|
|
37769
37665
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37770
37666
|
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
@@ -37777,22 +37673,22 @@ function providerSlug(url) {
|
|
|
37777
37673
|
}
|
|
37778
37674
|
async function commitAndOpenPr(root, repo, emit) {
|
|
37779
37675
|
emit("commit", "Committing the scaffolded project\u2026");
|
|
37780
|
-
await
|
|
37781
|
-
const staged = (await
|
|
37676
|
+
await run("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
|
|
37677
|
+
const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37782
37678
|
if (!staged) {
|
|
37783
37679
|
emit("commit", "Nothing to commit \u2014 skipping the pull request.");
|
|
37784
37680
|
return;
|
|
37785
37681
|
}
|
|
37786
37682
|
const commitArgs = ["commit", "-m", "chore: scaffold project (4PM)"];
|
|
37787
37683
|
try {
|
|
37788
|
-
await
|
|
37684
|
+
await run("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37789
37685
|
} catch {
|
|
37790
|
-
await
|
|
37686
|
+
await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37791
37687
|
}
|
|
37792
37688
|
const branch = (repo.branch ?? "").trim();
|
|
37793
37689
|
emit("push", "Pushing the scaffold branch\u2026");
|
|
37794
37690
|
try {
|
|
37795
|
-
await
|
|
37691
|
+
await run("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
|
|
37796
37692
|
} catch (err) {
|
|
37797
37693
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37798
37694
|
emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 push it from the Git tab when credentials are available.`);
|
|
@@ -37806,7 +37702,7 @@ async function commitAndOpenPr(root, repo, emit) {
|
|
|
37806
37702
|
emit("pr", `Opening a pull request via ${provider}\u2026`);
|
|
37807
37703
|
try {
|
|
37808
37704
|
const args = provider === "glab" ? ["mr", "create", "--fill", ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...slug ? ["-R", slug] : []];
|
|
37809
|
-
const { stdout } = await
|
|
37705
|
+
const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
|
|
37810
37706
|
const link2 = stdout.trim().split(/\s+/).find((s3) => /^https?:\/\//.test(s3)) ?? stdout.trim().split("\n").pop() ?? "";
|
|
37811
37707
|
emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
|
|
37812
37708
|
} catch (err) {
|
|
@@ -37820,7 +37716,7 @@ function cloneArgs(url, dest, branch) {
|
|
|
37820
37716
|
}
|
|
37821
37717
|
async function remoteHasBranch(url, branch) {
|
|
37822
37718
|
try {
|
|
37823
|
-
const { stdout } = await
|
|
37719
|
+
const { stdout } = await run("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
|
|
37824
37720
|
return stdout.trim().length > 0;
|
|
37825
37721
|
} catch {
|
|
37826
37722
|
return false;
|
|
@@ -37831,9 +37727,9 @@ async function ensureRemoteBranch(url, branch, emit) {
|
|
|
37831
37727
|
const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
|
|
37832
37728
|
try {
|
|
37833
37729
|
emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
|
|
37834
|
-
await
|
|
37835
|
-
await
|
|
37836
|
-
await
|
|
37730
|
+
await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
|
|
37731
|
+
await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
|
|
37732
|
+
await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
|
|
37837
37733
|
return true;
|
|
37838
37734
|
} catch (err) {
|
|
37839
37735
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37846,9 +37742,9 @@ async function ensureRemoteBranch(url, branch, emit) {
|
|
|
37846
37742
|
async function updateRepo(dir2, branch, emit) {
|
|
37847
37743
|
const b2 = (branch ?? "").trim();
|
|
37848
37744
|
emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
|
|
37849
|
-
await
|
|
37850
|
-
if (b2) await
|
|
37851
|
-
await
|
|
37745
|
+
await run("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
37746
|
+
if (b2) await run("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
|
|
37747
|
+
await run("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
37852
37748
|
}
|
|
37853
37749
|
async function provisionRepo(target, repo, emit, opts = {}) {
|
|
37854
37750
|
const mode = opts.mode ?? "clone";
|
|
@@ -37856,7 +37752,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37856
37752
|
if (!existsSync4(join6(target, ".git"))) {
|
|
37857
37753
|
emit("git", "Initializing repository\u2026");
|
|
37858
37754
|
await mkdir(target, { recursive: true });
|
|
37859
|
-
await
|
|
37755
|
+
await run("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
37860
37756
|
}
|
|
37861
37757
|
return;
|
|
37862
37758
|
}
|
|
@@ -37866,7 +37762,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37866
37762
|
} else if (mode === "force") {
|
|
37867
37763
|
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37868
37764
|
await rm(target, { recursive: true, force: true });
|
|
37869
|
-
await
|
|
37765
|
+
await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37870
37766
|
} else {
|
|
37871
37767
|
emit("git", "Repository already present \u2014 skipping clone.");
|
|
37872
37768
|
}
|
|
@@ -37875,10 +37771,10 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37875
37771
|
const b2 = (repo.branch ?? "").trim();
|
|
37876
37772
|
if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
|
|
37877
37773
|
emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
|
|
37878
|
-
await
|
|
37879
|
-
await
|
|
37774
|
+
await run("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
37775
|
+
await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
|
|
37880
37776
|
try {
|
|
37881
|
-
await
|
|
37777
|
+
await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
|
|
37882
37778
|
emit("git", `Created and pushed branch "${b2}".`);
|
|
37883
37779
|
} catch (err) {
|
|
37884
37780
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37887,7 +37783,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37887
37783
|
return;
|
|
37888
37784
|
}
|
|
37889
37785
|
emit("git", `Cloning ${repo.url}\u2026`);
|
|
37890
|
-
await
|
|
37786
|
+
await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37891
37787
|
}
|
|
37892
37788
|
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
37893
37789
|
const list = repos;
|
|
@@ -37900,27 +37796,27 @@ async function ensureReposCloned(physicRoot, repos, emit) {
|
|
|
37900
37796
|
function sampleDir() {
|
|
37901
37797
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
37902
37798
|
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
37903
|
-
const bundled =
|
|
37904
|
-
for (const candidate of [bundled,
|
|
37799
|
+
const bundled = resolve(here, "project-sample");
|
|
37800
|
+
for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
|
|
37905
37801
|
if (existsSync4(candidate)) return candidate;
|
|
37906
37802
|
}
|
|
37907
37803
|
return bundled;
|
|
37908
37804
|
}
|
|
37909
|
-
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
37805
|
+
async function scaffoldProject(payload, profileDir2, onProgress, ai2) {
|
|
37910
37806
|
let lastStep = "start";
|
|
37911
37807
|
const emit = (step, message) => {
|
|
37912
37808
|
lastStep = step;
|
|
37913
37809
|
onProgress?.({ projectId: payload.projectId, step, message });
|
|
37914
37810
|
};
|
|
37915
37811
|
try {
|
|
37916
|
-
const root = process.env.SCAFFOLD_ROOT ?
|
|
37812
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37917
37813
|
const target = join6(root, payload.projectName);
|
|
37918
37814
|
await mkdir(target, { recursive: true });
|
|
37919
37815
|
const repo = singleRepo(reposOf(payload.spec));
|
|
37920
37816
|
if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
|
|
37921
37817
|
emit("git", "Cloning repository\u2026");
|
|
37922
37818
|
await provisionRepo(target, repo, emit, { createMissingBranch: true });
|
|
37923
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true });
|
|
37819
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
|
|
37924
37820
|
await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
|
|
37925
37821
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37926
37822
|
await writeTemplateMarker(target);
|
|
@@ -37939,7 +37835,7 @@ async function scaffoldRepo(dir2, label, spec, emit, opts = {}) {
|
|
|
37939
37835
|
if (spec) {
|
|
37940
37836
|
emit("spec", `Writing project.spec.json into ${label}\u2026`);
|
|
37941
37837
|
await writeFile(join6(dir2, "project.spec.json"), JSON.stringify(spec, null, 2), "utf8");
|
|
37942
|
-
await aiInit(dir2, spec, emit);
|
|
37838
|
+
await aiInit(dir2, spec, emit, opts.ai);
|
|
37943
37839
|
}
|
|
37944
37840
|
}
|
|
37945
37841
|
async function resetTemplateManagedClaude(dir2, label, emit) {
|
|
@@ -37965,7 +37861,7 @@ function readSpecField(spec, key) {
|
|
|
37965
37861
|
}
|
|
37966
37862
|
return typeof spec[key] === "string" ? spec[key] : "";
|
|
37967
37863
|
}
|
|
37968
|
-
async function aiInit(target, spec, emit) {
|
|
37864
|
+
async function aiInit(target, spec, emit, ai2) {
|
|
37969
37865
|
const guideFile = readSpecField(spec, "ai_guide_file") === "AGENT.md" ? "AGENT.md" : "CLAUDE.md";
|
|
37970
37866
|
const guideInstructions = readSpecField(spec, "ai_guide_instructions").trim();
|
|
37971
37867
|
emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
|
|
@@ -37985,25 +37881,47 @@ ${(sa.description || "").trim()}
|
|
|
37985
37881
|
}
|
|
37986
37882
|
}
|
|
37987
37883
|
const specJson = JSON.stringify(spec);
|
|
37988
|
-
|
|
37989
|
-
|
|
37990
|
-
|
|
37884
|
+
let generate3 = null;
|
|
37885
|
+
let releaseSlot = () => void 0;
|
|
37886
|
+
if (ai2) {
|
|
37887
|
+
const slot = await ai2.acquireSlot(
|
|
37888
|
+
(q2) => emit("ai-queued", `Waiting for an AI run slot \u2014 ${q2.position} ahead (${q2.running}/${q2.limit} parallel runs in use)\u2026`)
|
|
37889
|
+
);
|
|
37890
|
+
if (slot.kind === "granted") {
|
|
37891
|
+
releaseSlot = () => slot.handle.release();
|
|
37892
|
+
generate3 = (prompt, label) => ai2.generate(prompt, target, label);
|
|
37893
|
+
if (slot.queued) emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
|
|
37894
|
+
} else {
|
|
37895
|
+
emit("ai-skipped", "No AI run slot freed up in time \u2014 AI init skipped; fallback content written.");
|
|
37896
|
+
}
|
|
37897
|
+
}
|
|
37898
|
+
try {
|
|
37899
|
+
await generateFile(
|
|
37900
|
+
generate3,
|
|
37901
|
+
join6(target, "README.md"),
|
|
37902
|
+
"README.md",
|
|
37903
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
37991
37904
|
${specJson}`
|
|
37992
|
-
|
|
37993
|
-
|
|
37994
|
-
|
|
37995
|
-
|
|
37905
|
+
);
|
|
37906
|
+
await generateFile(
|
|
37907
|
+
generate3,
|
|
37908
|
+
join6(target, guideFile),
|
|
37909
|
+
guideFile,
|
|
37910
|
+
`Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
37996
37911
|
${specJson}` + (guideInstructions ? `
|
|
37997
37912
|
Also incorporate these additional instructions/content:
|
|
37998
37913
|
${guideInstructions}
|
|
37999
37914
|
` : ""),
|
|
38000
|
-
|
|
38001
|
-
|
|
37915
|
+
guideInstructions
|
|
37916
|
+
);
|
|
37917
|
+
} finally {
|
|
37918
|
+
releaseSlot();
|
|
37919
|
+
}
|
|
38002
37920
|
}
|
|
38003
|
-
async function generateFile(path, prompt, fallback) {
|
|
37921
|
+
async function generateFile(generate3, path, label, prompt, fallback) {
|
|
38004
37922
|
try {
|
|
38005
|
-
const text = await
|
|
38006
|
-
if (text.trim()) {
|
|
37923
|
+
const text = generate3 ? await generate3(prompt, label) : null;
|
|
37924
|
+
if (text && text.trim()) {
|
|
38007
37925
|
await writeFile(path, text.trim() + "\n", "utf8");
|
|
38008
37926
|
return;
|
|
38009
37927
|
}
|
|
@@ -38011,21 +37929,21 @@ async function generateFile(path, prompt, fallback) {
|
|
|
38011
37929
|
}
|
|
38012
37930
|
if (fallback && fallback.trim()) await writeFile(path, fallback.trim() + "\n", "utf8");
|
|
38013
37931
|
}
|
|
38014
|
-
async function addProject(payload, profileDir2, onProgress) {
|
|
37932
|
+
async function addProject(payload, profileDir2, onProgress, ai2) {
|
|
38015
37933
|
let lastStep = "start";
|
|
38016
37934
|
const emit = (step, message) => {
|
|
38017
37935
|
lastStep = step;
|
|
38018
37936
|
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38019
37937
|
};
|
|
38020
37938
|
try {
|
|
38021
|
-
const root = process.env.SCAFFOLD_ROOT ?
|
|
37939
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
38022
37940
|
const target = join6(root, payload.projectName);
|
|
38023
37941
|
await mkdir(target, { recursive: true });
|
|
38024
37942
|
const repo = singleRepo(payload.repos ?? []);
|
|
38025
37943
|
const mode = payload.mode ?? "clone";
|
|
38026
37944
|
if (repo) await provisionRepo(target, repo, emit, { mode });
|
|
38027
37945
|
if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
|
|
38028
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37946
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
|
|
38029
37947
|
}
|
|
38030
37948
|
await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
|
|
38031
37949
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
@@ -38432,6 +38350,9 @@ var WsChannels = {
|
|
|
38432
38350
|
CLI_UPDATE_RESULT: "cli.update-result",
|
|
38433
38351
|
// command lifecycle
|
|
38434
38352
|
COMMAND_ANNOUNCE: "command.announce",
|
|
38353
|
+
// server → cli (reply): stop an AI run (ADR-0362) — queued ⇒ leave the run-slot queue, running ⇒ kill
|
|
38354
|
+
// the AI process with no failover; the cli then ends the stream with `command.output {done, cancelled}`.
|
|
38355
|
+
COMMAND_CANCEL: "command.cancel",
|
|
38435
38356
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
38436
38357
|
// transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
|
|
38437
38358
|
// whether a web viewer is attached) so an unwatched cli syncs nothing.
|
|
@@ -38551,6 +38472,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
38551
38472
|
var RUN_SLOT_RENEW_MS = 45e3;
|
|
38552
38473
|
var RUN_SLOT_RETRY_MS = 3e3;
|
|
38553
38474
|
var RUN_SLOT_MAX_WAIT_MS = 30 * 6e4;
|
|
38475
|
+
var COMMAND_CANCELLED_EXIT_CODE = 130;
|
|
38554
38476
|
|
|
38555
38477
|
// src/core/update-scheduler.ts
|
|
38556
38478
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -38696,7 +38618,7 @@ function classifyToolFailure(code, out) {
|
|
|
38696
38618
|
}
|
|
38697
38619
|
return { reason: "other", retryable: false };
|
|
38698
38620
|
}
|
|
38699
|
-
function
|
|
38621
|
+
function run2(cmd, args, onLine, timeoutMs = 18e4) {
|
|
38700
38622
|
return new Promise((resolve12) => {
|
|
38701
38623
|
let out = "";
|
|
38702
38624
|
let buf = "";
|
|
@@ -38736,13 +38658,13 @@ function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
38736
38658
|
});
|
|
38737
38659
|
}
|
|
38738
38660
|
async function detectOne(cmd, versionArg) {
|
|
38739
|
-
const { code, out } = await
|
|
38661
|
+
const { code, out } = await run2(cmd, [versionArg], void 0, 15e3);
|
|
38740
38662
|
if (code !== 0) return null;
|
|
38741
38663
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
38742
38664
|
return first ?? "";
|
|
38743
38665
|
}
|
|
38744
38666
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
38745
|
-
const { out } = await
|
|
38667
|
+
const { out } = await run2("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
38746
38668
|
if (!out.trim()) return [];
|
|
38747
38669
|
let deps = {};
|
|
38748
38670
|
try {
|
|
@@ -38818,7 +38740,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
38818
38740
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
38819
38741
|
}
|
|
38820
38742
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
38821
|
-
const { code, out } = await
|
|
38743
|
+
const { code, out } = await run2(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
38822
38744
|
if (code === 0) return { ok: true, exitCode: code };
|
|
38823
38745
|
const detail = extractToolError(out);
|
|
38824
38746
|
return { ok: false, exitCode: code, error: detail ? `${manager} exited ${code}: ${detail}` : `${manager} exited ${code}` };
|
|
@@ -38851,7 +38773,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
38851
38773
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
38852
38774
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
38853
38775
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
38854
|
-
const { code, out } = await
|
|
38776
|
+
const { code, out } = await run2(manager, args, onLine, timeoutMs);
|
|
38855
38777
|
if (code === 0) return { ok: true };
|
|
38856
38778
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
38857
38779
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -39135,6 +39057,36 @@ function clearPinnedCredential(profileDir2) {
|
|
|
39135
39057
|
import { readFileSync as readFileSync8 } from "fs";
|
|
39136
39058
|
import { homedir as homedir3 } from "os";
|
|
39137
39059
|
import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
39060
|
+
|
|
39061
|
+
// src/utils/agent-deny.ts
|
|
39062
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
39063
|
+
var HOME_SECRET_PATTERNS = [
|
|
39064
|
+
"~/.4pm/**",
|
|
39065
|
+
"~/.ssh/**",
|
|
39066
|
+
"~/.config/gh/**",
|
|
39067
|
+
"~/.git-credentials",
|
|
39068
|
+
"~/.claude*",
|
|
39069
|
+
"~/.claude*/**"
|
|
39070
|
+
];
|
|
39071
|
+
function absDirPattern(dir2) {
|
|
39072
|
+
const abs = (isAbsolute(dir2) ? dir2 : resolve2(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
39073
|
+
return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
|
|
39074
|
+
}
|
|
39075
|
+
function agentDenyRules(profileDirs2 = []) {
|
|
39076
|
+
const patterns = new Set(HOME_SECRET_PATTERNS);
|
|
39077
|
+
for (const dir2 of profileDirs2) {
|
|
39078
|
+
if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
|
|
39079
|
+
}
|
|
39080
|
+
const rules = [];
|
|
39081
|
+
for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
|
|
39082
|
+
return rules;
|
|
39083
|
+
}
|
|
39084
|
+
function denySettingsArgs(cmd, profileDirs2 = []) {
|
|
39085
|
+
if (!cmd.includes("claude")) return [];
|
|
39086
|
+
return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
|
|
39087
|
+
}
|
|
39088
|
+
|
|
39089
|
+
// src/utils/ai-cli.ts
|
|
39138
39090
|
var REQUIRED_AI_ARGS = {
|
|
39139
39091
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
39140
39092
|
codex: ["exec", "--json"],
|
|
@@ -39811,12 +39763,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
39811
39763
|
|
|
39812
39764
|
// src/core/support-answer.ts
|
|
39813
39765
|
import { spawn as spawn4 } from "child_process";
|
|
39814
|
-
import { execFile as
|
|
39766
|
+
import { execFile as execFile2 } from "child_process";
|
|
39815
39767
|
import { createHash as createHash3 } from "crypto";
|
|
39816
39768
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
39817
39769
|
import { join as join14 } from "path";
|
|
39818
39770
|
import { tmpdir as tmpdir3 } from "os";
|
|
39819
|
-
import { promisify as
|
|
39771
|
+
import { promisify as promisify2 } from "util";
|
|
39820
39772
|
|
|
39821
39773
|
// src/core/ai-stream.ts
|
|
39822
39774
|
function assistantText(ev) {
|
|
@@ -39921,7 +39873,7 @@ function aiProviderOf(cmd) {
|
|
|
39921
39873
|
if (cmd.includes("antigravity")) return "antigravity";
|
|
39922
39874
|
return "claude";
|
|
39923
39875
|
}
|
|
39924
|
-
function
|
|
39876
|
+
function estimateTokens(text) {
|
|
39925
39877
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
39926
39878
|
}
|
|
39927
39879
|
|
|
@@ -39977,7 +39929,7 @@ function gitAuthArgs(user, password) {
|
|
|
39977
39929
|
|
|
39978
39930
|
// src/core/support-answer.ts
|
|
39979
39931
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
39980
|
-
var execFileAsync =
|
|
39932
|
+
var execFileAsync = promisify2(execFile2);
|
|
39981
39933
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
39982
39934
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
39983
39935
|
var LEGAL_DRAFT_TIMEOUT_MS = 54e4;
|
|
@@ -40274,7 +40226,7 @@ async function runSupportAnswer(req, ai2, profileDir2) {
|
|
|
40274
40226
|
if (!text) return { body: "", error: "empty answer" };
|
|
40275
40227
|
const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
|
|
40276
40228
|
if (!body) return { body: "", error: "empty answer" };
|
|
40277
|
-
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
40229
|
+
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
|
|
40278
40230
|
return {
|
|
40279
40231
|
body,
|
|
40280
40232
|
...moderation ? { moderation } : {},
|
|
@@ -40323,9 +40275,9 @@ function reportToolResult(tool, ok, message) {
|
|
|
40323
40275
|
// src/core/autonomous.ts
|
|
40324
40276
|
import { readFile as readFile4, writeFile as writeFile4, chmod } from "fs/promises";
|
|
40325
40277
|
import { readFileSync as readFileSync14 } from "fs";
|
|
40326
|
-
import { execFile as
|
|
40278
|
+
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
40327
40279
|
import { join as join17 } from "path";
|
|
40328
|
-
import { promisify as
|
|
40280
|
+
import { promisify as promisify3 } from "util";
|
|
40329
40281
|
|
|
40330
40282
|
// src/core/autonomous-config.ts
|
|
40331
40283
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -40436,7 +40388,7 @@ function pushRecord(h, rec) {
|
|
|
40436
40388
|
}
|
|
40437
40389
|
|
|
40438
40390
|
// src/core/autonomous.ts
|
|
40439
|
-
var
|
|
40391
|
+
var run3 = promisify3(execFile3);
|
|
40440
40392
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
40441
40393
|
var AUTHORS_REL = ".claude/.autonomous.authors.json";
|
|
40442
40394
|
var TICK_REL = ".claude/hooks/autonomous-tick.sh";
|
|
@@ -40500,7 +40452,7 @@ function tickScript(root) {
|
|
|
40500
40452
|
}
|
|
40501
40453
|
async function crontabList() {
|
|
40502
40454
|
try {
|
|
40503
|
-
const { stdout } = await
|
|
40455
|
+
const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
|
|
40504
40456
|
return stdout;
|
|
40505
40457
|
} catch {
|
|
40506
40458
|
return "";
|
|
@@ -40988,9 +40940,20 @@ async function runCommand(dispatch, emitRaw, opts) {
|
|
|
40988
40940
|
hardKillTimer.unref?.();
|
|
40989
40941
|
}, timeoutMs) : null;
|
|
40990
40942
|
timeoutTimer?.unref?.();
|
|
40943
|
+
let cancelled = false;
|
|
40944
|
+
const onAbort = () => {
|
|
40945
|
+
if (cancelled) return;
|
|
40946
|
+
cancelled = true;
|
|
40947
|
+
child.kill("SIGTERM");
|
|
40948
|
+
hardKillTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
40949
|
+
hardKillTimer.unref?.();
|
|
40950
|
+
};
|
|
40951
|
+
if (opts?.signal?.aborted) onAbort();
|
|
40952
|
+
else opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
40991
40953
|
const clearTimers = () => {
|
|
40992
40954
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
40993
40955
|
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
40956
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
40994
40957
|
};
|
|
40995
40958
|
const batcher = new OutputBatcher({
|
|
40996
40959
|
onFlush: (chunk, truncated) => {
|
|
@@ -41017,9 +40980,10 @@ async function runCommand(dispatch, emitRaw, opts) {
|
|
|
41017
40980
|
seq: seq++,
|
|
41018
40981
|
chunk: "",
|
|
41019
40982
|
done: true,
|
|
41020
|
-
// 124 (the conventional timeout code) when we killed it
|
|
41021
|
-
// server settles the command as failed and the web surfaces the
|
|
41022
|
-
|
|
40983
|
+
// 130 when the run was stopped (ADR-0362); 124 (the conventional timeout code) when we killed it
|
|
40984
|
+
// for exceeding the limit, so the server settles the command as failed and the web surfaces the
|
|
40985
|
+
// note instead of spinning.
|
|
40986
|
+
exitCode: cancelled ? COMMAND_CANCELLED_EXIT_CODE : timedOut ? 124 : exitCode ?? -1
|
|
41023
40987
|
});
|
|
41024
40988
|
resolve12();
|
|
41025
40989
|
});
|
|
@@ -41055,7 +41019,7 @@ function isOutOfCredits(text) {
|
|
|
41055
41019
|
text
|
|
41056
41020
|
);
|
|
41057
41021
|
}
|
|
41058
|
-
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
|
|
41022
|
+
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs, signal) {
|
|
41059
41023
|
return new Promise((resolve12) => {
|
|
41060
41024
|
runCommand(
|
|
41061
41025
|
{ commandId, cmd, args, path: cwd2, env: env3 },
|
|
@@ -41063,16 +41027,17 @@ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs)
|
|
|
41063
41027
|
if (out.chunk) onChunk(out.chunk);
|
|
41064
41028
|
if (out.done) resolve12(out.exitCode ?? -1);
|
|
41065
41029
|
},
|
|
41066
|
-
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
|
|
41030
|
+
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {}, ...signal ? { signal } : {} }
|
|
41067
41031
|
).catch(() => resolve12(-1));
|
|
41068
41032
|
});
|
|
41069
41033
|
}
|
|
41070
|
-
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
41034
|
+
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0, signal) {
|
|
41071
41035
|
const total = plan.attempts.length;
|
|
41072
41036
|
let finalExit = -1;
|
|
41073
41037
|
let lastCmd = plan.attempts[0]?.cmd ?? "claude";
|
|
41074
41038
|
let lastCaptured = "";
|
|
41075
41039
|
for (let i = 0; i < total; i++) {
|
|
41040
|
+
if (signal?.aborted) return cancelledResult();
|
|
41076
41041
|
const attempt = plan.attempts[i];
|
|
41077
41042
|
lastCmd = attempt.cmd;
|
|
41078
41043
|
if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
|
|
@@ -41085,17 +41050,18 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
41085
41050
|
handlers.onChunk(text);
|
|
41086
41051
|
}
|
|
41087
41052
|
};
|
|
41088
|
-
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
|
|
41053
|
+
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs, signal);
|
|
41089
41054
|
const tail = parser.flush();
|
|
41090
41055
|
if (tail) {
|
|
41091
41056
|
captured += tail;
|
|
41092
41057
|
handlers.onChunk(tail);
|
|
41093
41058
|
}
|
|
41059
|
+
if (signal?.aborted) return cancelledResult();
|
|
41094
41060
|
lastCaptured = captured;
|
|
41095
41061
|
const apiErr = parser.apiError();
|
|
41096
41062
|
if (finalExit === 0 && !apiErr.isError) {
|
|
41097
41063
|
const usage = parser.usage();
|
|
41098
|
-
if (usage.tokens === 0) usage.tokens =
|
|
41064
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
41099
41065
|
reportToolResult(attempt.cmd, true);
|
|
41100
41066
|
return {
|
|
41101
41067
|
exitCode: 0,
|
|
@@ -41114,6 +41080,17 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
41114
41080
|
reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
|
|
41115
41081
|
return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
|
|
41116
41082
|
}
|
|
41083
|
+
function cancelledResult() {
|
|
41084
|
+
return {
|
|
41085
|
+
exitCode: COMMAND_CANCELLED_EXIT_CODE,
|
|
41086
|
+
workedDir: null,
|
|
41087
|
+
workedKey: null,
|
|
41088
|
+
workedCmd: null,
|
|
41089
|
+
usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 },
|
|
41090
|
+
sessionId: "",
|
|
41091
|
+
cancelled: true
|
|
41092
|
+
};
|
|
41093
|
+
}
|
|
41117
41094
|
function summarizeAiFailure(captured, exitCode) {
|
|
41118
41095
|
if (isAuthFailure(captured)) return "Not logged in";
|
|
41119
41096
|
if (isOutOfCredits(captured)) return "Out of usage credits";
|
|
@@ -41642,21 +41619,30 @@ async function runMemoryCompaction(ai2, cwd2, input) {
|
|
|
41642
41619
|
}
|
|
41643
41620
|
|
|
41644
41621
|
// src/core/git-commit-identity.ts
|
|
41645
|
-
import { execFile as
|
|
41646
|
-
import { promisify as
|
|
41647
|
-
var
|
|
41622
|
+
import { execFile as execFile4 } from "child_process";
|
|
41623
|
+
import { promisify as promisify4 } from "util";
|
|
41624
|
+
var run4 = promisify4(execFile4);
|
|
41648
41625
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
41649
41626
|
if (!machineUsername) return;
|
|
41650
41627
|
try {
|
|
41651
|
-
await
|
|
41628
|
+
await run4("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
41652
41629
|
} catch {
|
|
41653
41630
|
}
|
|
41654
41631
|
}
|
|
41655
41632
|
|
|
41656
41633
|
// src/core/ws-client/run-slot.ts
|
|
41657
41634
|
var NO_SLOT = { release: () => void 0 };
|
|
41658
|
-
function sleep2(ms2) {
|
|
41659
|
-
return new Promise((resolve12) =>
|
|
41635
|
+
function sleep2(ms2, signal) {
|
|
41636
|
+
return new Promise((resolve12) => {
|
|
41637
|
+
if (signal?.aborted) return resolve12();
|
|
41638
|
+
const timer = setTimeout(done, ms2);
|
|
41639
|
+
function done() {
|
|
41640
|
+
clearTimeout(timer);
|
|
41641
|
+
signal?.removeEventListener("abort", done);
|
|
41642
|
+
resolve12();
|
|
41643
|
+
}
|
|
41644
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
41645
|
+
});
|
|
41660
41646
|
}
|
|
41661
41647
|
function holdLease(ctx, lease) {
|
|
41662
41648
|
const timer = setInterval(() => {
|
|
@@ -41673,22 +41659,28 @@ function holdLease(ctx, lease) {
|
|
|
41673
41659
|
}
|
|
41674
41660
|
};
|
|
41675
41661
|
}
|
|
41676
|
-
async function acquireRunSlot(ctx, onQueued) {
|
|
41662
|
+
async function acquireRunSlot(ctx, onQueued, signal) {
|
|
41677
41663
|
const startedAt = Date.now();
|
|
41678
41664
|
let ticket;
|
|
41679
41665
|
let lastPosition = -1;
|
|
41680
41666
|
let limit = 0;
|
|
41681
41667
|
for (; ; ) {
|
|
41668
|
+
if (signal?.aborted) return leaveQueue(ctx, ticket);
|
|
41682
41669
|
let reply;
|
|
41683
41670
|
try {
|
|
41684
41671
|
reply = await ctx.request(WsChannels.RUN_SLOT, { op: "acquire", ...ticket ? { ticket } : {} });
|
|
41685
41672
|
} catch (err) {
|
|
41686
41673
|
logger.warn("run.slot.unavailable", { error: err.message });
|
|
41687
|
-
return { kind: "granted", handle: NO_SLOT };
|
|
41674
|
+
return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
|
|
41688
41675
|
}
|
|
41689
|
-
if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT };
|
|
41676
|
+
if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
|
|
41690
41677
|
if (reply.granted) {
|
|
41691
|
-
|
|
41678
|
+
const handle = reply.lease ? holdLease(ctx, reply.lease) : NO_SLOT;
|
|
41679
|
+
if (signal?.aborted) {
|
|
41680
|
+
handle.release();
|
|
41681
|
+
return { kind: "cancelled" };
|
|
41682
|
+
}
|
|
41683
|
+
return { kind: "granted", handle, queued: ticket !== void 0 };
|
|
41692
41684
|
}
|
|
41693
41685
|
ticket = reply.ticket;
|
|
41694
41686
|
limit = reply.limit;
|
|
@@ -41697,13 +41689,31 @@ async function acquireRunSlot(ctx, onQueued) {
|
|
|
41697
41689
|
onQueued({ position: reply.position, limit: reply.limit, running: reply.running });
|
|
41698
41690
|
}
|
|
41699
41691
|
if (Date.now() - startedAt >= RUN_SLOT_MAX_WAIT_MS) return { kind: "timeout", limit };
|
|
41700
|
-
await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS);
|
|
41692
|
+
await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS, signal);
|
|
41701
41693
|
}
|
|
41702
41694
|
}
|
|
41695
|
+
function leaveQueue(ctx, ticket) {
|
|
41696
|
+
if (ticket) ctx.request(WsChannels.RUN_SLOT, { op: "leave", ticket }).catch(() => void 0);
|
|
41697
|
+
return { kind: "cancelled" };
|
|
41698
|
+
}
|
|
41703
41699
|
|
|
41704
41700
|
// src/core/ws-client/command-dispatch.ts
|
|
41705
41701
|
var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
|
|
41702
|
+
var activeRuns = /* @__PURE__ */ new Map();
|
|
41703
|
+
function cancelActiveRun(commandId) {
|
|
41704
|
+
const run11 = activeRuns.get(commandId);
|
|
41705
|
+
if (!run11) return { ok: false, state: "unknown" };
|
|
41706
|
+
run11.controller.abort();
|
|
41707
|
+
return { ok: true, state: run11.state };
|
|
41708
|
+
}
|
|
41706
41709
|
function handleCommandChannels(ctx, message, payload) {
|
|
41710
|
+
if (message.channel === WsChannels.COMMAND_CANCEL) {
|
|
41711
|
+
const commandId = typeof payload.commandId === "string" ? payload.commandId : "";
|
|
41712
|
+
const reply = cancelActiveRun(commandId);
|
|
41713
|
+
logger.info("command.cancel", { commandId, ...reply });
|
|
41714
|
+
ctx.send(WsChannels.COMMAND_CANCEL, reply, message.id);
|
|
41715
|
+
return true;
|
|
41716
|
+
}
|
|
41707
41717
|
if (message.channel !== WsChannels.COMMAND_DISPATCH) return false;
|
|
41708
41718
|
const dispatch = payload;
|
|
41709
41719
|
if (ctx.hasHandledCommand(dispatch.commandId)) {
|
|
@@ -41790,6 +41800,15 @@ async function runLocalCommand(ctx, input) {
|
|
|
41790
41800
|
await runAiPrompt(ctx, prompt, commandId, "local");
|
|
41791
41801
|
}
|
|
41792
41802
|
async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
41803
|
+
const run11 = { controller: new AbortController(), state: "queued" };
|
|
41804
|
+
activeRuns.set(commandId, run11);
|
|
41805
|
+
try {
|
|
41806
|
+
await runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot, images, aiConfig, readOnly, bypass);
|
|
41807
|
+
} finally {
|
|
41808
|
+
activeRuns.delete(commandId);
|
|
41809
|
+
}
|
|
41810
|
+
}
|
|
41811
|
+
async function runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
41793
41812
|
const config = readProfileConfig(ctx.profileDir);
|
|
41794
41813
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
41795
41814
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -41797,7 +41816,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
41797
41816
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
41798
41817
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
41799
41818
|
if (perPromptLimit > 0) {
|
|
41800
|
-
const estimated =
|
|
41819
|
+
const estimated = estimateTokens(prompt);
|
|
41801
41820
|
if (estimated > perPromptLimit) {
|
|
41802
41821
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
41803
41822
|
return;
|
|
@@ -41954,24 +41973,39 @@ ${guardedPrompt}`;
|
|
|
41954
41973
|
}
|
|
41955
41974
|
};
|
|
41956
41975
|
let result;
|
|
41957
|
-
let
|
|
41976
|
+
let neverStarted = false;
|
|
41958
41977
|
try {
|
|
41959
|
-
const slot = await acquireRunSlot(
|
|
41960
|
-
|
|
41961
|
-
|
|
41962
|
-
|
|
41963
|
-
|
|
41964
|
-
|
|
41978
|
+
const slot = await acquireRunSlot(
|
|
41979
|
+
ctx,
|
|
41980
|
+
({ position, limit, running: running2 }) => {
|
|
41981
|
+
const text = `\u23F3 queued \u2014 ${position} ahead, ${running2}/${limit} parallel runs in use (plan limit)`;
|
|
41982
|
+
ctx.bus.push({ source: origin, kind: "log", text });
|
|
41983
|
+
ctx.send(WsChannels.COMMAND_OUTPUT, {
|
|
41984
|
+
commandId,
|
|
41985
|
+
seq: seq++,
|
|
41986
|
+
chunk: `${text}
|
|
41987
|
+
`,
|
|
41988
|
+
log: true,
|
|
41989
|
+
queue: { position, limit, running: running2 }
|
|
41990
|
+
});
|
|
41991
|
+
},
|
|
41992
|
+
run11.controller.signal
|
|
41993
|
+
);
|
|
41965
41994
|
if (slot.kind === "timeout") {
|
|
41966
|
-
|
|
41995
|
+
neverStarted = true;
|
|
41967
41996
|
const text = `RUN_QUEUE_TIMEOUT: waited 30 min for a free run slot (plan limit ${slot.limit} parallel runs) \u2014 not run`;
|
|
41968
41997
|
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
41969
41998
|
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
41970
41999
|
`, log: true });
|
|
41971
42000
|
result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
|
|
42001
|
+
} else if (slot.kind === "cancelled") {
|
|
42002
|
+
neverStarted = true;
|
|
42003
|
+
result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
|
|
41972
42004
|
} else {
|
|
42005
|
+
if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
|
|
42006
|
+
run11.state = "running";
|
|
41973
42007
|
try {
|
|
41974
|
-
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs);
|
|
42008
|
+
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run11.controller.signal);
|
|
41975
42009
|
} finally {
|
|
41976
42010
|
slot.handle.release();
|
|
41977
42011
|
}
|
|
@@ -41979,6 +42013,12 @@ ${guardedPrompt}`;
|
|
|
41979
42013
|
} finally {
|
|
41980
42014
|
ctx.bus.endBusy(plan.cmd);
|
|
41981
42015
|
}
|
|
42016
|
+
if (result.cancelled) {
|
|
42017
|
+
const text = "\u25A0 stopped";
|
|
42018
|
+
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
42019
|
+
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
42020
|
+
`, log: true });
|
|
42021
|
+
}
|
|
41982
42022
|
const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
|
|
41983
42023
|
if (result.workedDir) {
|
|
41984
42024
|
if (unified && result.workedKey) {
|
|
@@ -41993,14 +42033,16 @@ ${guardedPrompt}`;
|
|
|
41993
42033
|
seq: seq++,
|
|
41994
42034
|
chunk: "",
|
|
41995
42035
|
done: true,
|
|
41996
|
-
exitCode: result.exitCode
|
|
42036
|
+
exitCode: result.exitCode,
|
|
42037
|
+
// A stopped run (ADR-0362): the server settles it `cancelled` and the web applies nothing.
|
|
42038
|
+
...result.cancelled ? { cancelled: true } : {}
|
|
41997
42039
|
});
|
|
41998
42040
|
finishCommand(commandId, result.exitCode, result.usage);
|
|
41999
42041
|
ctx.send(WsChannels.COMMAND_HISTORY, {
|
|
42000
42042
|
commandId,
|
|
42001
42043
|
cmd: plan.cmd,
|
|
42002
42044
|
args: markerArgs,
|
|
42003
|
-
status: result.exitCode === 0 ? "done" : "failed",
|
|
42045
|
+
status: result.cancelled ? "cancelled" : result.exitCode === 0 ? "done" : "failed",
|
|
42004
42046
|
exitCode: result.exitCode,
|
|
42005
42047
|
tokens: result.usage.tokens,
|
|
42006
42048
|
// The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
|
|
@@ -42036,13 +42078,13 @@ ${guardedPrompt}`;
|
|
|
42036
42078
|
});
|
|
42037
42079
|
ctx.bus.addTokens?.(result.usage.tokens);
|
|
42038
42080
|
}
|
|
42039
|
-
if (!
|
|
42081
|
+
if (!neverStarted) ctx.reportUsage(events, profileLabel);
|
|
42040
42082
|
void ctx.pollUsage();
|
|
42041
42083
|
ctx.bus.push({
|
|
42042
42084
|
source: origin,
|
|
42043
42085
|
kind: "exit",
|
|
42044
|
-
text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
|
|
42045
|
-
level: result.exitCode === 0 ? "info" : "error",
|
|
42086
|
+
text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : result.cancelled ? "\u25A0 stopped \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
|
|
42087
|
+
level: result.exitCode === 0 ? "info" : result.cancelled ? "warn" : "error",
|
|
42046
42088
|
durationMs: Date.now() - aiStartedMs
|
|
42047
42089
|
});
|
|
42048
42090
|
if (memCfg.enabled && result.exitCode === 0) {
|
|
@@ -42788,10 +42830,10 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
42788
42830
|
|
|
42789
42831
|
// src/core/git-diff.ts
|
|
42790
42832
|
import { readFile as readFile8 } from "fs/promises";
|
|
42791
|
-
import { execFile as
|
|
42833
|
+
import { execFile as execFile5 } from "child_process";
|
|
42792
42834
|
import { basename as basename5, dirname as dirname8, resolve as resolve9, sep as sep7 } from "path";
|
|
42793
|
-
import { promisify as
|
|
42794
|
-
var
|
|
42835
|
+
import { promisify as promisify5 } from "util";
|
|
42836
|
+
var run5 = promisify5(execFile5);
|
|
42795
42837
|
function resolveInRoot5(root, relPath) {
|
|
42796
42838
|
if (!root) return null;
|
|
42797
42839
|
const base = resolve9(root);
|
|
@@ -42819,7 +42861,7 @@ async function gitDiff(root, path) {
|
|
|
42819
42861
|
}
|
|
42820
42862
|
let oldBuf = null;
|
|
42821
42863
|
try {
|
|
42822
|
-
const { stdout } = await
|
|
42864
|
+
const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
|
|
42823
42865
|
cwd: dir2,
|
|
42824
42866
|
timeout: 15e3,
|
|
42825
42867
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -42849,12 +42891,12 @@ async function gitDiff(root, path) {
|
|
|
42849
42891
|
}
|
|
42850
42892
|
|
|
42851
42893
|
// src/core/git-env.ts
|
|
42852
|
-
import { execFile as
|
|
42853
|
-
import { promisify as
|
|
42854
|
-
var
|
|
42894
|
+
import { execFile as execFile6 } from "child_process";
|
|
42895
|
+
import { promisify as promisify6 } from "util";
|
|
42896
|
+
var run6 = promisify6(execFile6);
|
|
42855
42897
|
async function hasBinary(bin, args = ["--version"]) {
|
|
42856
42898
|
try {
|
|
42857
|
-
await
|
|
42899
|
+
await run6(bin, args, { timeout: 5e3 });
|
|
42858
42900
|
return true;
|
|
42859
42901
|
} catch {
|
|
42860
42902
|
return false;
|
|
@@ -42867,7 +42909,7 @@ async function checkGitEnv(provider) {
|
|
|
42867
42909
|
let account = null;
|
|
42868
42910
|
if (installed) {
|
|
42869
42911
|
try {
|
|
42870
|
-
const { stdout, stderr } = await
|
|
42912
|
+
const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
|
|
42871
42913
|
const out = `${stdout}
|
|
42872
42914
|
${stderr}`;
|
|
42873
42915
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -42880,11 +42922,11 @@ ${stderr}`;
|
|
|
42880
42922
|
}
|
|
42881
42923
|
|
|
42882
42924
|
// src/core/git-ssh-key.ts
|
|
42883
|
-
import { execFile as
|
|
42925
|
+
import { execFile as execFile7 } from "child_process";
|
|
42884
42926
|
import { readFile as readFile9, rm as rm4, mkdir as mkdir7 } from "fs/promises";
|
|
42885
42927
|
import { join as join21 } from "path";
|
|
42886
|
-
import { promisify as
|
|
42887
|
-
var
|
|
42928
|
+
import { promisify as promisify7 } from "util";
|
|
42929
|
+
var run7 = promisify7(execFile7);
|
|
42888
42930
|
function keyPaths(profileDir2) {
|
|
42889
42931
|
const dir2 = join21(profileDir2, ".ssh");
|
|
42890
42932
|
const key = join21(dir2, "id_4pm");
|
|
@@ -42899,14 +42941,14 @@ async function readPublicKey(pub) {
|
|
|
42899
42941
|
}
|
|
42900
42942
|
async function fingerprintOf(pub) {
|
|
42901
42943
|
try {
|
|
42902
|
-
const { stdout } = await
|
|
42944
|
+
const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
42903
42945
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
42904
42946
|
} catch {
|
|
42905
42947
|
return null;
|
|
42906
42948
|
}
|
|
42907
42949
|
}
|
|
42908
42950
|
async function configureGit(key) {
|
|
42909
|
-
await
|
|
42951
|
+
await run7(
|
|
42910
42952
|
"git",
|
|
42911
42953
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
42912
42954
|
{ timeout: 5e3 }
|
|
@@ -42917,7 +42959,7 @@ async function generate(profileDir2) {
|
|
|
42917
42959
|
await mkdir7(dir2, { recursive: true });
|
|
42918
42960
|
await rm4(key, { force: true });
|
|
42919
42961
|
await rm4(pub, { force: true });
|
|
42920
|
-
await
|
|
42962
|
+
await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
42921
42963
|
timeout: 15e3
|
|
42922
42964
|
});
|
|
42923
42965
|
await configureGit(key);
|
|
@@ -42927,7 +42969,7 @@ async function remove(profileDir2) {
|
|
|
42927
42969
|
const { key, pub } = keyPaths(profileDir2);
|
|
42928
42970
|
await rm4(key, { force: true });
|
|
42929
42971
|
await rm4(pub, { force: true });
|
|
42930
|
-
await
|
|
42972
|
+
await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
42931
42973
|
() => void 0
|
|
42932
42974
|
);
|
|
42933
42975
|
return { publicKey: null, fingerprint: null };
|
|
@@ -42948,10 +42990,10 @@ async function manageSshKey(op, profileDir2) {
|
|
|
42948
42990
|
}
|
|
42949
42991
|
|
|
42950
42992
|
// src/core/git-history.ts
|
|
42951
|
-
import { execFile as
|
|
42993
|
+
import { execFile as execFile8 } from "child_process";
|
|
42952
42994
|
import { resolve as resolve10, sep as sep8 } from "path";
|
|
42953
|
-
import { promisify as
|
|
42954
|
-
var
|
|
42995
|
+
import { promisify as promisify8 } from "util";
|
|
42996
|
+
var run8 = promisify8(execFile8);
|
|
42955
42997
|
var FS = "";
|
|
42956
42998
|
var RS = "";
|
|
42957
42999
|
function resolveRepo(root, repo) {
|
|
@@ -42962,7 +43004,7 @@ function resolveRepo(root, repo) {
|
|
|
42962
43004
|
}
|
|
42963
43005
|
async function git(cwd2, args) {
|
|
42964
43006
|
try {
|
|
42965
|
-
const { stdout } = await
|
|
43007
|
+
const { stdout } = await run8("git", args, {
|
|
42966
43008
|
cwd: cwd2,
|
|
42967
43009
|
timeout: 15e3,
|
|
42968
43010
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -43104,6 +43146,74 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
43104
43146
|
// src/core/ws-client/handlers/misc.ts
|
|
43105
43147
|
import { join as join22 } from "path";
|
|
43106
43148
|
|
|
43149
|
+
// src/core/ai-assist.ts
|
|
43150
|
+
import { execFile as execFile9 } from "child_process";
|
|
43151
|
+
import { promisify as promisify9 } from "util";
|
|
43152
|
+
var run9 = promisify9(execFile9);
|
|
43153
|
+
function aiCli() {
|
|
43154
|
+
return process.env.AI_CLI || "claude";
|
|
43155
|
+
}
|
|
43156
|
+
function aiArgs(cli, prompt) {
|
|
43157
|
+
if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
|
|
43158
|
+
if (cli === "codex") return ["exec", "--json", prompt];
|
|
43159
|
+
return ["-p", prompt];
|
|
43160
|
+
}
|
|
43161
|
+
function estimateTokens2(prompt, output) {
|
|
43162
|
+
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
43163
|
+
}
|
|
43164
|
+
function parseAi(cli, stdout, prompt) {
|
|
43165
|
+
if (cli === "claude") {
|
|
43166
|
+
try {
|
|
43167
|
+
const j2 = JSON.parse(stdout);
|
|
43168
|
+
const text2 = (j2.result ?? "").trim();
|
|
43169
|
+
const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
|
|
43170
|
+
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
|
|
43171
|
+
} catch {
|
|
43172
|
+
}
|
|
43173
|
+
}
|
|
43174
|
+
if (cli === "codex") {
|
|
43175
|
+
try {
|
|
43176
|
+
let text2 = "";
|
|
43177
|
+
let tokens = 0;
|
|
43178
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
43179
|
+
const obj = JSON.parse(line);
|
|
43180
|
+
if (obj.text) text2 = obj.text;
|
|
43181
|
+
else if (obj.message) text2 = obj.message;
|
|
43182
|
+
if (obj.usage) {
|
|
43183
|
+
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
43184
|
+
}
|
|
43185
|
+
}
|
|
43186
|
+
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
|
|
43187
|
+
} catch {
|
|
43188
|
+
}
|
|
43189
|
+
}
|
|
43190
|
+
const text = stdout.trim();
|
|
43191
|
+
return { text, tokens: estimateTokens2(prompt, text) };
|
|
43192
|
+
}
|
|
43193
|
+
async function runAi(prompt) {
|
|
43194
|
+
const cli = aiCli();
|
|
43195
|
+
const { stdout } = await run9(cli, aiArgs(cli, prompt), {
|
|
43196
|
+
timeout: 12e4,
|
|
43197
|
+
maxBuffer: 8 * 1024 * 1024
|
|
43198
|
+
});
|
|
43199
|
+
return parseAi(cli, stdout, prompt);
|
|
43200
|
+
}
|
|
43201
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
43202
|
+
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:
|
|
43203
|
+
${input}`;
|
|
43204
|
+
const { text, tokens } = await runAi(prompt);
|
|
43205
|
+
const start = text.indexOf("{");
|
|
43206
|
+
const end = text.lastIndexOf("}");
|
|
43207
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
43208
|
+
try {
|
|
43209
|
+
const j2 = JSON.parse(text.slice(start, end + 1));
|
|
43210
|
+
const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
|
|
43211
|
+
return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
|
|
43212
|
+
} catch {
|
|
43213
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
43214
|
+
}
|
|
43215
|
+
}
|
|
43216
|
+
|
|
43107
43217
|
// src/core/outbound-review.ts
|
|
43108
43218
|
function compilePatterns(sources, fallback) {
|
|
43109
43219
|
const list = sources ?? fallback;
|
|
@@ -43217,6 +43327,72 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
43217
43327
|
// src/core/ws-client/handlers/project.ts
|
|
43218
43328
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
|
|
43219
43329
|
import { join as join23 } from "path";
|
|
43330
|
+
|
|
43331
|
+
// src/core/ai-task.ts
|
|
43332
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
43333
|
+
function createAiTaskRunner(ctx) {
|
|
43334
|
+
return {
|
|
43335
|
+
acquireSlot: (onQueued) => acquireRunSlot(ctx, onQueued),
|
|
43336
|
+
generate: (prompt, cwd2, label) => generate2(ctx, prompt, cwd2, label)
|
|
43337
|
+
};
|
|
43338
|
+
}
|
|
43339
|
+
async function generate2(ctx, prompt, cwd2, label) {
|
|
43340
|
+
const config = readProfileConfig(ctx.profileDir);
|
|
43341
|
+
const cmd = config.aiCli || "claude";
|
|
43342
|
+
const unified = isUnifiedConfig(config);
|
|
43343
|
+
const hint = unified ? {
|
|
43344
|
+
credential: getPinnedCredential(ctx.profileDir) ?? ((config.aiFailoverMode ?? "remember") === "remember" ? getWorkingCredential(ctx.profileDir) : null)
|
|
43345
|
+
} : { dir: getWorkingProfile(ctx.profileDir, cmd) };
|
|
43346
|
+
const plan = planAiRun(prompt, config, hint, /* @__PURE__ */ new Map(), true);
|
|
43347
|
+
const timeoutSec = config.projectAiRunTimeoutSec || config.aiRunTimeoutSec || 0;
|
|
43348
|
+
ctx.bus.push({ source: "system", kind: "aireq", text: `${plan.cmd} \u2039 ${label}` });
|
|
43349
|
+
let text = "";
|
|
43350
|
+
const handlers = {
|
|
43351
|
+
onChunk: (chunk) => {
|
|
43352
|
+
text += chunk;
|
|
43353
|
+
},
|
|
43354
|
+
onAttemptStart: (profile, index, total, attemptCmd) => {
|
|
43355
|
+
ctx.bus.push({ source: "system", kind: "log", text: `\u2192 trying ${attemptCmd} profile "${profile}" (${index + 1}/${total})\u2026` });
|
|
43356
|
+
if (index > 0) text = "";
|
|
43357
|
+
},
|
|
43358
|
+
onAttemptFail: (profile) => {
|
|
43359
|
+
ctx.bus.push({ source: "system", kind: "log", text: `profile "${profile}" failed \u2014 trying next`, level: "warn" });
|
|
43360
|
+
}
|
|
43361
|
+
};
|
|
43362
|
+
ctx.bus.startBusy(plan.cmd);
|
|
43363
|
+
let result;
|
|
43364
|
+
try {
|
|
43365
|
+
result = await runAiFailover(plan, `ai-task-${randomUUID2()}`, cwd2, handlers, timeoutSec * 1e3);
|
|
43366
|
+
} finally {
|
|
43367
|
+
ctx.bus.endBusy(plan.cmd);
|
|
43368
|
+
}
|
|
43369
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
43370
|
+
const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
|
|
43371
|
+
if (result.exitCode === 0 && result.usage.tokens > 0) {
|
|
43372
|
+
events.push({
|
|
43373
|
+
metric: UsageMetric.AI_TOKENS,
|
|
43374
|
+
amount: result.usage.tokens,
|
|
43375
|
+
occurredAt,
|
|
43376
|
+
inputTokens: result.usage.input,
|
|
43377
|
+
outputTokens: result.usage.output,
|
|
43378
|
+
cacheReadTokens: result.usage.cacheRead,
|
|
43379
|
+
cacheCreationTokens: result.usage.cacheCreation,
|
|
43380
|
+
provider: aiProviderOf(result.workedCmd ?? plan.cmd),
|
|
43381
|
+
authMode: resolveClaudeAuthMode(config)
|
|
43382
|
+
});
|
|
43383
|
+
}
|
|
43384
|
+
ctx.reportUsage(events, result.workedDir ? profileDisplayLabel(result.workedDir) : null);
|
|
43385
|
+
const ok = result.exitCode === 0 && text.trim().length > 0;
|
|
43386
|
+
ctx.bus.push({
|
|
43387
|
+
source: "system",
|
|
43388
|
+
kind: ok ? "aires" : "log",
|
|
43389
|
+
text: ok ? `${plan.cmd} \u203A ${label} \u2713` : `${label}: AI run failed (exit ${result.exitCode}) \u2014 using the fallback`,
|
|
43390
|
+
...ok ? {} : { level: "warn" }
|
|
43391
|
+
});
|
|
43392
|
+
return ok ? text.trim() : null;
|
|
43393
|
+
}
|
|
43394
|
+
|
|
43395
|
+
// src/core/ws-client/handlers/project.ts
|
|
43220
43396
|
function handleProjectChannels(ctx, message, payload) {
|
|
43221
43397
|
switch (message.channel) {
|
|
43222
43398
|
case WsChannels.PHYSIC_SYNC: {
|
|
@@ -43274,14 +43450,18 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
43274
43450
|
void scaffoldProject(
|
|
43275
43451
|
payload,
|
|
43276
43452
|
ctx.profileDir,
|
|
43277
|
-
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
|
|
43453
|
+
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
|
|
43454
|
+
// AI init on the standard AI path + one org run slot (ADR-0362).
|
|
43455
|
+
createAiTaskRunner(ctx)
|
|
43278
43456
|
).then((reply) => ctx.send(WsChannels.PROJECT_CREATE, reply, message.id));
|
|
43279
43457
|
return true;
|
|
43280
43458
|
case WsChannels.PROJECT_ADD:
|
|
43281
43459
|
void addProject(
|
|
43282
43460
|
payload,
|
|
43283
43461
|
ctx.profileDir,
|
|
43284
|
-
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
|
|
43462
|
+
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
|
|
43463
|
+
// An add-with-scaffold runs AI init too (ADR-0362).
|
|
43464
|
+
createAiTaskRunner(ctx)
|
|
43285
43465
|
).then((reply) => ctx.send(WsChannels.PROJECT_ADD, reply, message.id));
|
|
43286
43466
|
return true;
|
|
43287
43467
|
default:
|
|
@@ -43364,7 +43544,7 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
|
|
|
43364
43544
|
// src/core/faq-compose.ts
|
|
43365
43545
|
import { spawn as spawn9, execFile as execFile10 } from "child_process";
|
|
43366
43546
|
import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
|
|
43367
|
-
import { randomUUID as
|
|
43547
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
43368
43548
|
import { join as join24 } from "path";
|
|
43369
43549
|
import { tmpdir as tmpdir4 } from "os";
|
|
43370
43550
|
import { promisify as promisify10 } from "util";
|
|
@@ -43486,7 +43666,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
|
43486
43666
|
var MAX_OUTPUT_CHARS = 8e3;
|
|
43487
43667
|
async function runFaqCompose(req, ai2) {
|
|
43488
43668
|
if (!req.repo.token) return { prUrl: "", error: "no write token" };
|
|
43489
|
-
const workDir = join24(tmpdir4(), "4pm-faq-sync",
|
|
43669
|
+
const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID3());
|
|
43490
43670
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
43491
43671
|
const auth = gitAuthArgs("x-access-token", req.repo.token);
|
|
43492
43672
|
try {
|
|
@@ -43501,7 +43681,7 @@ async function runFaqCompose(req, ai2) {
|
|
|
43501
43681
|
await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
|
|
43502
43682
|
const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
|
|
43503
43683
|
if (!staged.stdout.trim()) {
|
|
43504
|
-
return { prUrl: "", output, perTicket, tokens: usage.tokens ||
|
|
43684
|
+
return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
43505
43685
|
}
|
|
43506
43686
|
await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
|
|
43507
43687
|
await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
|
|
@@ -45290,7 +45470,7 @@ var WsClient = class {
|
|
|
45290
45470
|
}
|
|
45291
45471
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
45292
45472
|
const envelope = {
|
|
45293
|
-
id:
|
|
45473
|
+
id: randomUUID4(),
|
|
45294
45474
|
replyTo,
|
|
45295
45475
|
channel,
|
|
45296
45476
|
wsToken: this.wsToken,
|
|
@@ -45311,7 +45491,7 @@ var WsClient = class {
|
|
|
45311
45491
|
}
|
|
45312
45492
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
45313
45493
|
const envelope = {
|
|
45314
|
-
id:
|
|
45494
|
+
id: randomUUID4(),
|
|
45315
45495
|
replyTo: null,
|
|
45316
45496
|
channel,
|
|
45317
45497
|
wsToken: this.wsToken,
|
|
@@ -45703,7 +45883,7 @@ function releaseInstanceLock(profileDir2) {
|
|
|
45703
45883
|
|
|
45704
45884
|
// src/core/session-bus.ts
|
|
45705
45885
|
import { EventEmitter } from "events";
|
|
45706
|
-
import { randomUUID as
|
|
45886
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
45707
45887
|
var REPLAY_CAP = 500;
|
|
45708
45888
|
var SessionBus = class {
|
|
45709
45889
|
emitter = new EventEmitter();
|
|
@@ -45734,7 +45914,7 @@ var SessionBus = class {
|
|
|
45734
45914
|
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
45735
45915
|
push(input) {
|
|
45736
45916
|
const entry = {
|
|
45737
|
-
id:
|
|
45917
|
+
id: randomUUID5(),
|
|
45738
45918
|
level: "info",
|
|
45739
45919
|
...input,
|
|
45740
45920
|
ts: Date.now()
|
|
@@ -48127,7 +48307,7 @@ var src_default = Yoga;
|
|
|
48127
48307
|
|
|
48128
48308
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
48129
48309
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
48130
|
-
var
|
|
48310
|
+
var import_constants12 = __toESM(require_constants2(), 1);
|
|
48131
48311
|
import process3 from "process";
|
|
48132
48312
|
|
|
48133
48313
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -49369,7 +49549,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
49369
49549
|
scheduleTimeout: setTimeout,
|
|
49370
49550
|
cancelTimeout: clearTimeout,
|
|
49371
49551
|
noTimeout: -1,
|
|
49372
|
-
getCurrentEventPriority: () =>
|
|
49552
|
+
getCurrentEventPriority: () => import_constants12.DefaultEventPriority,
|
|
49373
49553
|
beforeActiveInstanceBlur() {
|
|
49374
49554
|
},
|
|
49375
49555
|
afterActiveInstanceBlur() {
|