@4pm/cli 1.7.0 → 1.8.4-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 +485 -408
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29667,7 +29667,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
29667
29667
|
init_wrapper();
|
|
29668
29668
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
29669
29669
|
import { basename as basename7, join as join28 } from "path";
|
|
29670
|
-
import { existsSync as
|
|
29670
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
29671
29671
|
|
|
29672
29672
|
// vendor/@4pm/utils/helpers.ts
|
|
29673
29673
|
function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
@@ -34367,9 +34367,240 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
34367
34367
|
projectId: external_exports.string().uuid().optional()
|
|
34368
34368
|
});
|
|
34369
34369
|
|
|
34370
|
+
// src/core/scaffold.ts
|
|
34371
|
+
import { cp, mkdir, writeFile } from "fs/promises";
|
|
34372
|
+
import { existsSync as existsSync4 } from "fs";
|
|
34373
|
+
import { execFile as execFile2 } from "child_process";
|
|
34374
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
34375
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
34376
|
+
import { promisify as promisify2 } from "util";
|
|
34377
|
+
|
|
34378
|
+
// src/core/ai-assist.ts
|
|
34379
|
+
import { execFile } from "child_process";
|
|
34380
|
+
import { promisify } from "util";
|
|
34381
|
+
var run = promisify(execFile);
|
|
34382
|
+
function aiCli() {
|
|
34383
|
+
return process.env.AI_CLI || "claude";
|
|
34384
|
+
}
|
|
34385
|
+
function aiArgs(cli, prompt) {
|
|
34386
|
+
if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
|
|
34387
|
+
if (cli === "codex") return ["exec", "--json", prompt];
|
|
34388
|
+
return ["-p", prompt];
|
|
34389
|
+
}
|
|
34390
|
+
function estimateTokens(prompt, output) {
|
|
34391
|
+
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
34392
|
+
}
|
|
34393
|
+
function parseAi(cli, stdout, prompt) {
|
|
34394
|
+
if (cli === "claude") {
|
|
34395
|
+
try {
|
|
34396
|
+
const j = JSON.parse(stdout);
|
|
34397
|
+
const text2 = (j.result ?? "").trim();
|
|
34398
|
+
const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
|
|
34399
|
+
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
|
|
34400
|
+
} catch {
|
|
34401
|
+
}
|
|
34402
|
+
}
|
|
34403
|
+
if (cli === "codex") {
|
|
34404
|
+
try {
|
|
34405
|
+
let text2 = "";
|
|
34406
|
+
let tokens = 0;
|
|
34407
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
34408
|
+
const obj = JSON.parse(line);
|
|
34409
|
+
if (obj.text) text2 = obj.text;
|
|
34410
|
+
else if (obj.message) text2 = obj.message;
|
|
34411
|
+
if (obj.usage) {
|
|
34412
|
+
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
34413
|
+
}
|
|
34414
|
+
}
|
|
34415
|
+
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
|
|
34416
|
+
} catch {
|
|
34417
|
+
}
|
|
34418
|
+
}
|
|
34419
|
+
const text = stdout.trim();
|
|
34420
|
+
return { text, tokens: estimateTokens(prompt, text) };
|
|
34421
|
+
}
|
|
34422
|
+
async function runAi(prompt) {
|
|
34423
|
+
const cli = aiCli();
|
|
34424
|
+
const { stdout } = await run(cli, aiArgs(cli, prompt), {
|
|
34425
|
+
timeout: 12e4,
|
|
34426
|
+
maxBuffer: 8 * 1024 * 1024
|
|
34427
|
+
});
|
|
34428
|
+
return parseAi(cli, stdout, prompt);
|
|
34429
|
+
}
|
|
34430
|
+
async function aiGenerate(prompt) {
|
|
34431
|
+
const { text } = await runAi(prompt);
|
|
34432
|
+
return text;
|
|
34433
|
+
}
|
|
34434
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
34435
|
+
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:
|
|
34436
|
+
${input}`;
|
|
34437
|
+
const { text, tokens } = await runAi(prompt);
|
|
34438
|
+
const start = text.indexOf("{");
|
|
34439
|
+
const end = text.lastIndexOf("}");
|
|
34440
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34441
|
+
try {
|
|
34442
|
+
const j = JSON.parse(text.slice(start, end + 1));
|
|
34443
|
+
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
34444
|
+
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
34445
|
+
} catch {
|
|
34446
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
34447
|
+
}
|
|
34448
|
+
}
|
|
34449
|
+
|
|
34450
|
+
// src/core/scaffold.ts
|
|
34451
|
+
var run2 = promisify2(execFile2);
|
|
34452
|
+
function reposOf(spec) {
|
|
34453
|
+
const repos = spec?.repos;
|
|
34454
|
+
return Array.isArray(repos) ? repos : [];
|
|
34455
|
+
}
|
|
34456
|
+
function subDirName(repo) {
|
|
34457
|
+
const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
|
|
34458
|
+
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
34459
|
+
}
|
|
34460
|
+
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
34461
|
+
for (const repo of repos) {
|
|
34462
|
+
if (repo.primary) {
|
|
34463
|
+
if (opts.clonePrimary && repo.url) {
|
|
34464
|
+
if (existsSync4(join6(target, ".git"))) {
|
|
34465
|
+
emit("git", `Primary repo already present \u2014 skipping clone.`);
|
|
34466
|
+
continue;
|
|
34467
|
+
}
|
|
34468
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
34469
|
+
await run2("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
34470
|
+
continue;
|
|
34471
|
+
}
|
|
34472
|
+
emit("git", "Initializing the primary repo\u2026");
|
|
34473
|
+
await run2("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
34474
|
+
if (repo.url) {
|
|
34475
|
+
await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
34476
|
+
() => void 0
|
|
34477
|
+
);
|
|
34478
|
+
}
|
|
34479
|
+
continue;
|
|
34480
|
+
}
|
|
34481
|
+
const dir2 = join6(target, subDirName(repo));
|
|
34482
|
+
if (repo.url) {
|
|
34483
|
+
if (existsSync4(join6(dir2, ".git"))) {
|
|
34484
|
+
emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
|
|
34485
|
+
continue;
|
|
34486
|
+
}
|
|
34487
|
+
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
34488
|
+
await run2("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
34489
|
+
} else {
|
|
34490
|
+
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
34491
|
+
await mkdir(dir2, { recursive: true });
|
|
34492
|
+
await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
34493
|
+
}
|
|
34494
|
+
}
|
|
34495
|
+
}
|
|
34496
|
+
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
34497
|
+
if (repos.length === 0) return;
|
|
34498
|
+
await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
|
|
34499
|
+
}
|
|
34500
|
+
function sampleDir() {
|
|
34501
|
+
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
34502
|
+
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
34503
|
+
const bundled = resolve(here, "project-sample");
|
|
34504
|
+
for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
|
|
34505
|
+
if (existsSync4(candidate)) return candidate;
|
|
34506
|
+
}
|
|
34507
|
+
return bundled;
|
|
34508
|
+
}
|
|
34509
|
+
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
34510
|
+
let lastStep = "start";
|
|
34511
|
+
const emit = (step, message) => {
|
|
34512
|
+
lastStep = step;
|
|
34513
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34514
|
+
};
|
|
34515
|
+
try {
|
|
34516
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34517
|
+
const target = join6(root, payload.projectName);
|
|
34518
|
+
await mkdir(target, { recursive: true });
|
|
34519
|
+
emit("copy", "Copying the sample template\u2026");
|
|
34520
|
+
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
34521
|
+
if (payload.spec) {
|
|
34522
|
+
emit("spec", "Writing project.spec.json\u2026");
|
|
34523
|
+
await writeFile(
|
|
34524
|
+
join6(target, "project.spec.json"),
|
|
34525
|
+
JSON.stringify(payload.spec, null, 2),
|
|
34526
|
+
"utf8"
|
|
34527
|
+
);
|
|
34528
|
+
}
|
|
34529
|
+
const repos = reposOf(payload.spec);
|
|
34530
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
34531
|
+
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
34532
|
+
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
34533
|
+
await writeTemplateMarker(target);
|
|
34534
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
34535
|
+
return { ok: true, path: target };
|
|
34536
|
+
} catch (err) {
|
|
34537
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
34538
|
+
}
|
|
34539
|
+
}
|
|
34540
|
+
async function writeTemplateMarker(target) {
|
|
34541
|
+
const dir2 = join6(target, ".4pm");
|
|
34542
|
+
await mkdir(dir2, { recursive: true });
|
|
34543
|
+
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
34544
|
+
await writeFile(join6(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
34545
|
+
}
|
|
34546
|
+
async function aiInit(target, spec, emit) {
|
|
34547
|
+
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
34548
|
+
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
34549
|
+
if (subagents.length > 0) {
|
|
34550
|
+
await mkdir(join6(target, ".claude", "agents"), { recursive: true });
|
|
34551
|
+
for (const sa of subagents) {
|
|
34552
|
+
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34553
|
+
if (!name) continue;
|
|
34554
|
+
const body = `---
|
|
34555
|
+
name: ${name}
|
|
34556
|
+
---
|
|
34557
|
+
|
|
34558
|
+
${(sa.description || "").trim()}
|
|
34559
|
+
`;
|
|
34560
|
+
await writeFile(join6(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
34561
|
+
}
|
|
34562
|
+
}
|
|
34563
|
+
const specJson = JSON.stringify(spec);
|
|
34564
|
+
await generateFile(
|
|
34565
|
+
join6(target, "README.md"),
|
|
34566
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
34567
|
+
${specJson}`
|
|
34568
|
+
);
|
|
34569
|
+
await generateFile(
|
|
34570
|
+
join6(target, "CLAUDE.md"),
|
|
34571
|
+
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
34572
|
+
${specJson}`
|
|
34573
|
+
);
|
|
34574
|
+
}
|
|
34575
|
+
async function generateFile(path, prompt) {
|
|
34576
|
+
try {
|
|
34577
|
+
const text = await aiGenerate(prompt);
|
|
34578
|
+
if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
|
|
34579
|
+
} catch {
|
|
34580
|
+
}
|
|
34581
|
+
}
|
|
34582
|
+
async function addProject(payload, profileDir2, onProgress) {
|
|
34583
|
+
let lastStep = "start";
|
|
34584
|
+
const emit = (step, message) => {
|
|
34585
|
+
lastStep = step;
|
|
34586
|
+
onProgress?.({ projectId: payload.projectId, step, message });
|
|
34587
|
+
};
|
|
34588
|
+
try {
|
|
34589
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
34590
|
+
const target = join6(root, payload.projectName);
|
|
34591
|
+
await mkdir(target, { recursive: true });
|
|
34592
|
+
const repos = payload.repos ?? [];
|
|
34593
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
|
|
34594
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
34595
|
+
return { ok: true, path: target };
|
|
34596
|
+
} catch (err) {
|
|
34597
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
34598
|
+
}
|
|
34599
|
+
}
|
|
34600
|
+
|
|
34370
34601
|
// src/core/worker-metrics.ts
|
|
34371
34602
|
import { statfs, readFile } from "fs/promises";
|
|
34372
|
-
import { existsSync as
|
|
34603
|
+
import { existsSync as existsSync5 } from "fs";
|
|
34373
34604
|
import { totalmem, freemem, cpus, platform, arch } from "os";
|
|
34374
34605
|
var CGROUP = "/sys/fs/cgroup";
|
|
34375
34606
|
var MB = 1024 * 1024;
|
|
@@ -34419,7 +34650,7 @@ function parseMaybeMax(text) {
|
|
|
34419
34650
|
return Number.isFinite(n) ? n : null;
|
|
34420
34651
|
}
|
|
34421
34652
|
function createWorkerMetricsSampler(diskPath) {
|
|
34422
|
-
const cgroupV2 =
|
|
34653
|
+
const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
|
|
34423
34654
|
let cgPrev = null;
|
|
34424
34655
|
let osPrev = null;
|
|
34425
34656
|
async function cgroupCpuCount() {
|
|
@@ -34734,6 +34965,10 @@ var WsChannels = {
|
|
|
34734
34965
|
// serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
|
|
34735
34966
|
// reconnect. `ws_token` seeds these on connect; this channel only carries updates.
|
|
34736
34967
|
PROJECT_TOKENS: "project.tokens",
|
|
34968
|
+
// server → cli (ADR-0289): update the cli to the latest version now (idle-aware self-update +
|
|
34969
|
+
// re-exec). Sent by the manual operator "Update" action and the update-on-connect hook when the
|
|
34970
|
+
// connecting cli is outdated. No payload; honours the machine's local autoUpdate opt-out.
|
|
34971
|
+
CLI_UPDATE: "cli.update",
|
|
34737
34972
|
// command lifecycle
|
|
34738
34973
|
COMMAND_ANNOUNCE: "command.announce",
|
|
34739
34974
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
@@ -34847,9 +35082,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
34847
35082
|
import { spawn as spawn2 } from "child_process";
|
|
34848
35083
|
|
|
34849
35084
|
// src/common/logger/logger.ts
|
|
34850
|
-
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
35085
|
+
import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
|
|
34851
35086
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
34852
|
-
import { join as
|
|
35087
|
+
import { join as join7 } from "path";
|
|
34853
35088
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
34854
35089
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
34855
35090
|
var DEFAULT_RETENTION_DAYS = 14;
|
|
@@ -34898,8 +35133,8 @@ var CliLogger = class {
|
|
|
34898
35133
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
34899
35134
|
const entry = { timestamp, level, ...this.base, event, ...fields };
|
|
34900
35135
|
const line = JSON.stringify(entry) + "\n";
|
|
34901
|
-
const path =
|
|
34902
|
-
this.queue = this.queue.then(() =>
|
|
35136
|
+
const path = join7(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
|
|
35137
|
+
this.queue = this.queue.then(() => mkdir2(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
|
|
34903
35138
|
console.warn(`Failed to write cli log: ${err}`);
|
|
34904
35139
|
});
|
|
34905
35140
|
}
|
|
@@ -34913,7 +35148,7 @@ var CliLogger = class {
|
|
|
34913
35148
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
34914
35149
|
if (!match) continue;
|
|
34915
35150
|
if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
|
|
34916
|
-
await unlink(
|
|
35151
|
+
await unlink(join7(dir2, file)).catch(() => void 0);
|
|
34917
35152
|
}
|
|
34918
35153
|
}
|
|
34919
35154
|
}
|
|
@@ -34924,7 +35159,7 @@ function readRecentLogLines(dir2, limit) {
|
|
|
34924
35159
|
const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
|
|
34925
35160
|
const latest = files[files.length - 1];
|
|
34926
35161
|
if (!latest) return [];
|
|
34927
|
-
const lines = readFileSync6(
|
|
35162
|
+
const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
|
|
34928
35163
|
return lines.slice(-limit);
|
|
34929
35164
|
} catch {
|
|
34930
35165
|
return [];
|
|
@@ -34937,8 +35172,8 @@ function readLogUpload(dir2) {
|
|
|
34937
35172
|
const latest = files[files.length - 1];
|
|
34938
35173
|
if (!latest) return null;
|
|
34939
35174
|
let totalBytes = 0;
|
|
34940
|
-
for (const f of files) totalBytes += statSync(
|
|
34941
|
-
let content = readFileSync6(
|
|
35175
|
+
for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
|
|
35176
|
+
let content = readFileSync6(join7(dir2, latest), "utf8");
|
|
34942
35177
|
if (content.length > LOG_UPLOAD_CONTENT_MAX) {
|
|
34943
35178
|
content = `\u2026[truncated]
|
|
34944
35179
|
${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
@@ -34981,7 +35216,7 @@ function classifyToolFailure(code, out) {
|
|
|
34981
35216
|
}
|
|
34982
35217
|
return { reason: "other", retryable: false };
|
|
34983
35218
|
}
|
|
34984
|
-
function
|
|
35219
|
+
function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
34985
35220
|
return new Promise((resolve10) => {
|
|
34986
35221
|
let out = "";
|
|
34987
35222
|
let buf = "";
|
|
@@ -35021,13 +35256,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
35021
35256
|
});
|
|
35022
35257
|
}
|
|
35023
35258
|
async function detectOne(cmd, versionArg) {
|
|
35024
|
-
const { code, out } = await
|
|
35259
|
+
const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
|
|
35025
35260
|
if (code !== 0) return null;
|
|
35026
35261
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
35027
35262
|
return first ?? "";
|
|
35028
35263
|
}
|
|
35029
35264
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
35030
|
-
const { out } = await
|
|
35265
|
+
const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
35031
35266
|
if (!out.trim()) return [];
|
|
35032
35267
|
let deps = {};
|
|
35033
35268
|
try {
|
|
@@ -35103,7 +35338,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
35103
35338
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
35104
35339
|
}
|
|
35105
35340
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
35106
|
-
const { code } = await
|
|
35341
|
+
const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
35107
35342
|
return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
|
|
35108
35343
|
}
|
|
35109
35344
|
function setToolAutoUpdate(profileDir2, name, enabled) {
|
|
@@ -35134,7 +35369,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
35134
35369
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
35135
35370
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
35136
35371
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
35137
|
-
const { code, out } = await
|
|
35372
|
+
const { code, out } = await run3(manager, args, onLine, timeoutMs);
|
|
35138
35373
|
if (code === 0) return { ok: true };
|
|
35139
35374
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
35140
35375
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -35200,6 +35435,9 @@ var UpdateScheduler = class {
|
|
|
35200
35435
|
lastDoneDateKey = null;
|
|
35201
35436
|
/** Date key marked due but deferred because the cli was busy (stays set past the hour). */
|
|
35202
35437
|
pendingDateKey = null;
|
|
35438
|
+
/** A push/connect-triggered update requested now (ADR-0289) — runs when idle, independent of the
|
|
35439
|
+
* daily policy; cleared once it runs. */
|
|
35440
|
+
forcedPending = false;
|
|
35203
35441
|
/** Guards against a second update while one is in flight. */
|
|
35204
35442
|
updating = false;
|
|
35205
35443
|
/** Apply the latest policy (called on each ws_token) and start the tick timer once. */
|
|
@@ -35210,6 +35448,25 @@ var UpdateScheduler = class {
|
|
|
35210
35448
|
this.timer.unref?.();
|
|
35211
35449
|
}
|
|
35212
35450
|
}
|
|
35451
|
+
/**
|
|
35452
|
+
* Request an update to latest NOW (ADR-0289) — the server's `cli.update` push or the
|
|
35453
|
+
* update-on-connect hook. Idle-aware like the daily tick (defers while a command runs) and
|
|
35454
|
+
* honours the local `autoUpdate:false` opt-out, but is independent of the daily org policy. Also
|
|
35455
|
+
* ensures the tick timer is running so the request is served even before the first ws_token.
|
|
35456
|
+
*/
|
|
35457
|
+
updateNow() {
|
|
35458
|
+
if (this.optedOutLocally()) {
|
|
35459
|
+
logger.info("update.push.optedOut");
|
|
35460
|
+
return;
|
|
35461
|
+
}
|
|
35462
|
+
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35463
|
+
this.forcedPending = true;
|
|
35464
|
+
if (!this.timer) {
|
|
35465
|
+
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
35466
|
+
this.timer.unref?.();
|
|
35467
|
+
}
|
|
35468
|
+
void this.tick();
|
|
35469
|
+
}
|
|
35213
35470
|
/** Stop the scheduler when the session ends (logout / replaced / stopped). */
|
|
35214
35471
|
stop() {
|
|
35215
35472
|
if (this.timer) {
|
|
@@ -35242,12 +35499,19 @@ var UpdateScheduler = class {
|
|
|
35242
35499
|
return null;
|
|
35243
35500
|
}
|
|
35244
35501
|
}
|
|
35245
|
-
/** One evaluation:
|
|
35502
|
+
/** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
|
|
35246
35503
|
async tick() {
|
|
35247
|
-
|
|
35248
|
-
if (!p || !p.autoUpdateDaily || this.updating) return;
|
|
35504
|
+
if (this.updating) return;
|
|
35249
35505
|
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35250
35506
|
if (this.optedOutLocally()) return;
|
|
35507
|
+
if (this.forcedPending) {
|
|
35508
|
+
if (this.bus.busy !== null) return;
|
|
35509
|
+
this.forcedPending = false;
|
|
35510
|
+
await this.runUpdate();
|
|
35511
|
+
return;
|
|
35512
|
+
}
|
|
35513
|
+
const p = this.policy;
|
|
35514
|
+
if (!p || !p.autoUpdateDaily) return;
|
|
35251
35515
|
const clock = this.orgClock();
|
|
35252
35516
|
if (!clock) return;
|
|
35253
35517
|
if (clock.dateKey === this.lastDoneDateKey) return;
|
|
@@ -35305,14 +35569,14 @@ var UpdateScheduler = class {
|
|
|
35305
35569
|
};
|
|
35306
35570
|
|
|
35307
35571
|
// src/core/ai-profile-state.ts
|
|
35308
|
-
import { existsSync as
|
|
35309
|
-
import { join as
|
|
35572
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35573
|
+
import { join as join8 } from "path";
|
|
35310
35574
|
function statePath(profileDir2) {
|
|
35311
|
-
return
|
|
35575
|
+
return join8(profileDir2, "ai-profiles.json");
|
|
35312
35576
|
}
|
|
35313
35577
|
function readState(profileDir2) {
|
|
35314
35578
|
const path = statePath(profileDir2);
|
|
35315
|
-
if (!
|
|
35579
|
+
if (!existsSync6(path)) return {};
|
|
35316
35580
|
try {
|
|
35317
35581
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
35318
35582
|
} catch {
|
|
@@ -35370,7 +35634,7 @@ function clearPinnedCredential(profileDir2) {
|
|
|
35370
35634
|
// src/utils/ai-cli.ts
|
|
35371
35635
|
import { readFileSync as readFileSync8 } from "fs";
|
|
35372
35636
|
import { homedir as homedir3 } from "os";
|
|
35373
|
-
import { basename, isAbsolute, join as
|
|
35637
|
+
import { basename, isAbsolute, join as join9 } from "path";
|
|
35374
35638
|
var REQUIRED_AI_ARGS = {
|
|
35375
35639
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
35376
35640
|
codex: ["exec", "--json"],
|
|
@@ -35507,9 +35771,9 @@ function claudeCredentialKeys(config) {
|
|
|
35507
35771
|
return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
|
|
35508
35772
|
}
|
|
35509
35773
|
function resolveHomePath(value) {
|
|
35510
|
-
if (value.startsWith("~")) return
|
|
35774
|
+
if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
|
|
35511
35775
|
if (isAbsolute(value)) return value;
|
|
35512
|
-
return
|
|
35776
|
+
return join9(homedir3(), value);
|
|
35513
35777
|
}
|
|
35514
35778
|
function profileDirs(profiles) {
|
|
35515
35779
|
if (!profiles) return [];
|
|
@@ -35555,11 +35819,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
|
|
|
35555
35819
|
}
|
|
35556
35820
|
function claudeHomeDirs(config) {
|
|
35557
35821
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
35558
|
-
return dirs.length > 0 ? dirs : [
|
|
35822
|
+
return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
|
|
35559
35823
|
}
|
|
35560
35824
|
function profileDisplayLabel(dir2) {
|
|
35561
35825
|
try {
|
|
35562
|
-
const raw = JSON.parse(readFileSync8(
|
|
35826
|
+
const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
|
|
35563
35827
|
const email = raw.oauthAccount?.emailAddress;
|
|
35564
35828
|
if (email && email.trim()) return email;
|
|
35565
35829
|
} catch {
|
|
@@ -35661,11 +35925,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
|
|
|
35661
35925
|
// src/core/claude-usage.ts
|
|
35662
35926
|
import { spawn as spawn3 } from "child_process";
|
|
35663
35927
|
import { readFileSync as readFileSync9 } from "fs";
|
|
35664
|
-
import { join as
|
|
35928
|
+
import { join as join10 } from "path";
|
|
35665
35929
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
35666
35930
|
function readCreds(dir2) {
|
|
35667
35931
|
try {
|
|
35668
|
-
const raw = JSON.parse(readFileSync9(
|
|
35932
|
+
const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
|
|
35669
35933
|
const oauth = raw.claudeAiOauth ?? raw;
|
|
35670
35934
|
return oauth.accessToken ? oauth : null;
|
|
35671
35935
|
} catch {
|
|
@@ -35774,8 +36038,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
|
|
|
35774
36038
|
}
|
|
35775
36039
|
|
|
35776
36040
|
// src/core/command-history.ts
|
|
35777
|
-
import { existsSync as
|
|
35778
|
-
import { join as
|
|
36041
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
36042
|
+
import { join as join11 } from "path";
|
|
35779
36043
|
var MAX_ENTRIES = 1e3;
|
|
35780
36044
|
var filePath = null;
|
|
35781
36045
|
var entries = [];
|
|
@@ -35837,8 +36101,8 @@ async function uploadHistory(cfg) {
|
|
|
35837
36101
|
}
|
|
35838
36102
|
}
|
|
35839
36103
|
function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
|
|
35840
|
-
filePath =
|
|
35841
|
-
if (
|
|
36104
|
+
filePath = join11(profileDir2, "command-history.json");
|
|
36105
|
+
if (existsSync7(filePath)) {
|
|
35842
36106
|
try {
|
|
35843
36107
|
entries = JSON.parse(readFileSync10(filePath, "utf8"));
|
|
35844
36108
|
} catch {
|
|
@@ -35894,16 +36158,16 @@ function finishCommand(commandId, exitCode, usage) {
|
|
|
35894
36158
|
}
|
|
35895
36159
|
|
|
35896
36160
|
// src/core/command-images.ts
|
|
35897
|
-
import { existsSync as
|
|
35898
|
-
import { join as
|
|
36161
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
36162
|
+
import { join as join12 } from "path";
|
|
35899
36163
|
function attachmentsBaseDir(physicRoot) {
|
|
35900
|
-
return
|
|
36164
|
+
return join12(physicRoot, ".4pm", "attachments");
|
|
35901
36165
|
}
|
|
35902
36166
|
var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
35903
36167
|
async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
35904
36168
|
const map = /* @__PURE__ */ new Map();
|
|
35905
36169
|
if (!images.length) return map;
|
|
35906
|
-
const dir2 =
|
|
36170
|
+
const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
|
|
35907
36171
|
mkdirSync3(dir2, { recursive: true });
|
|
35908
36172
|
let n = 0;
|
|
35909
36173
|
for (const img of images) {
|
|
@@ -35915,7 +36179,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
|
35915
36179
|
continue;
|
|
35916
36180
|
}
|
|
35917
36181
|
const ext = commandImageExt(reply.mime ?? img.mime);
|
|
35918
|
-
const file =
|
|
36182
|
+
const file = join12(dir2, `img-${n}.${ext}`);
|
|
35919
36183
|
writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
|
|
35920
36184
|
map.set(img.placeholder, file);
|
|
35921
36185
|
} catch (err) {
|
|
@@ -35937,12 +36201,12 @@ function rewriteImagePlaceholders(prompt, paths) {
|
|
|
35937
36201
|
}
|
|
35938
36202
|
function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
35939
36203
|
const base = attachmentsBaseDir(physicRoot);
|
|
35940
|
-
if (!
|
|
36204
|
+
if (!existsSync8(base)) return 0;
|
|
35941
36205
|
const cutoff = Date.now() - maxAgeMs;
|
|
35942
36206
|
let removed = 0;
|
|
35943
36207
|
try {
|
|
35944
36208
|
for (const entry of readdirSync3(base)) {
|
|
35945
|
-
const dir2 =
|
|
36209
|
+
const dir2 = join12(base, entry);
|
|
35946
36210
|
try {
|
|
35947
36211
|
if (statSync2(dir2).mtimeMs < cutoff) {
|
|
35948
36212
|
rmSync3(dir2, { recursive: true, force: true });
|
|
@@ -35959,18 +36223,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
|
35959
36223
|
// src/core/command-output-store.ts
|
|
35960
36224
|
import {
|
|
35961
36225
|
appendFileSync,
|
|
35962
|
-
existsSync as
|
|
36226
|
+
existsSync as existsSync9,
|
|
35963
36227
|
mkdirSync as mkdirSync4,
|
|
35964
36228
|
readFileSync as readFileSync11,
|
|
35965
36229
|
readdirSync as readdirSync4,
|
|
35966
36230
|
rmSync as rmSync4,
|
|
35967
36231
|
statSync as statSync3
|
|
35968
36232
|
} from "fs";
|
|
35969
|
-
import { join as
|
|
36233
|
+
import { join as join13 } from "path";
|
|
35970
36234
|
var KEEP_FILES = 200;
|
|
35971
36235
|
var dir = null;
|
|
35972
36236
|
function initCommandOutput(profileDir2) {
|
|
35973
|
-
dir =
|
|
36237
|
+
dir = join13(profileDir2, "command-output");
|
|
35974
36238
|
try {
|
|
35975
36239
|
mkdirSync4(dir, { recursive: true });
|
|
35976
36240
|
prune();
|
|
@@ -35980,14 +36244,14 @@ function initCommandOutput(profileDir2) {
|
|
|
35980
36244
|
function appendCommandOutput(commandId, text) {
|
|
35981
36245
|
if (!dir || !text) return;
|
|
35982
36246
|
try {
|
|
35983
|
-
appendFileSync(
|
|
36247
|
+
appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
|
|
35984
36248
|
} catch {
|
|
35985
36249
|
}
|
|
35986
36250
|
}
|
|
35987
36251
|
function readCommandOutput(commandId) {
|
|
35988
36252
|
if (!dir) return null;
|
|
35989
|
-
const path =
|
|
35990
|
-
if (!
|
|
36253
|
+
const path = join13(dir, `${commandId}.log`);
|
|
36254
|
+
if (!existsSync9(path)) return null;
|
|
35991
36255
|
try {
|
|
35992
36256
|
return readFileSync11(path, "utf8");
|
|
35993
36257
|
} catch {
|
|
@@ -35997,8 +36261,8 @@ function readCommandOutput(commandId) {
|
|
|
35997
36261
|
function prune() {
|
|
35998
36262
|
if (!dir) return;
|
|
35999
36263
|
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(
|
|
36264
|
+
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);
|
|
36265
|
+
for (const { f } of files.slice(KEEP_FILES)) rmSync4(join13(dir, f), { force: true });
|
|
36002
36266
|
} catch {
|
|
36003
36267
|
}
|
|
36004
36268
|
}
|
|
@@ -36007,7 +36271,7 @@ function pruneCommandOutputByAge(days) {
|
|
|
36007
36271
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
36008
36272
|
try {
|
|
36009
36273
|
for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
|
|
36010
|
-
const p =
|
|
36274
|
+
const p = join13(dir, f);
|
|
36011
36275
|
if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
|
|
36012
36276
|
}
|
|
36013
36277
|
} catch {
|
|
@@ -36016,12 +36280,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
36016
36280
|
|
|
36017
36281
|
// src/core/support-answer.ts
|
|
36018
36282
|
import { spawn as spawn4 } from "child_process";
|
|
36019
|
-
import { execFile } from "child_process";
|
|
36283
|
+
import { execFile as execFile3 } from "child_process";
|
|
36020
36284
|
import { createHash as createHash3 } from "crypto";
|
|
36021
|
-
import { existsSync as
|
|
36285
|
+
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
36286
|
import { randomUUID } from "crypto";
|
|
36023
|
-
import { join as
|
|
36024
|
-
import { promisify } from "util";
|
|
36287
|
+
import { join as join14 } from "path";
|
|
36288
|
+
import { promisify as promisify3 } from "util";
|
|
36025
36289
|
|
|
36026
36290
|
// src/core/ai-stream.ts
|
|
36027
36291
|
function assistantText(ev) {
|
|
@@ -36116,22 +36380,22 @@ function createAiStreamParser(cli) {
|
|
|
36116
36380
|
apiError: () => ({ ...apiError })
|
|
36117
36381
|
};
|
|
36118
36382
|
}
|
|
36119
|
-
function
|
|
36383
|
+
function estimateTokens2(text) {
|
|
36120
36384
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
36121
36385
|
}
|
|
36122
36386
|
|
|
36123
36387
|
// src/core/support-answer.ts
|
|
36124
36388
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
36125
|
-
var execFileAsync =
|
|
36389
|
+
var execFileAsync = promisify3(execFile3);
|
|
36126
36390
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
36127
36391
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
36128
36392
|
var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
36129
36393
|
function kbRoot(profileDir2) {
|
|
36130
|
-
return
|
|
36394
|
+
return join14(profileDir2, "support-kb");
|
|
36131
36395
|
}
|
|
36132
36396
|
function repoDir(profileDir2, url, branch) {
|
|
36133
36397
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
36134
|
-
return
|
|
36398
|
+
return join14(kbRoot(profileDir2), key);
|
|
36135
36399
|
}
|
|
36136
36400
|
function authedUrl(url, token) {
|
|
36137
36401
|
if (!token) return url;
|
|
@@ -36140,7 +36404,7 @@ function authedUrl(url, token) {
|
|
|
36140
36404
|
}
|
|
36141
36405
|
async function ensureRepo(profileDir2, repo) {
|
|
36142
36406
|
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
36143
|
-
if (
|
|
36407
|
+
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
36144
36408
|
const url = authedUrl(repo.url, repo.token);
|
|
36145
36409
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36146
36410
|
mkdirSync5(kbRoot(profileDir2), { recursive: true });
|
|
@@ -36151,11 +36415,11 @@ async function ensureRepo(profileDir2, repo) {
|
|
|
36151
36415
|
}
|
|
36152
36416
|
async function refreshSupportKb(profileDir2) {
|
|
36153
36417
|
const root = kbRoot(profileDir2);
|
|
36154
|
-
if (!
|
|
36418
|
+
if (!existsSync10(root)) return;
|
|
36155
36419
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36156
36420
|
for (const entry of readdirSync5(root)) {
|
|
36157
|
-
const dir2 =
|
|
36158
|
-
if (!
|
|
36421
|
+
const dir2 = join14(root, entry);
|
|
36422
|
+
if (!existsSync10(join14(dir2, ".git"))) continue;
|
|
36159
36423
|
try {
|
|
36160
36424
|
await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
|
|
36161
36425
|
} catch (err) {
|
|
@@ -36170,7 +36434,7 @@ function collectDocs(dir2) {
|
|
|
36170
36434
|
if (total >= MAX_CONTEXT_CHARS) return;
|
|
36171
36435
|
for (const entry of readdirSync5(current2)) {
|
|
36172
36436
|
if (entry === ".git" || entry === "node_modules") continue;
|
|
36173
|
-
const full =
|
|
36437
|
+
const full = join14(current2, entry);
|
|
36174
36438
|
const st = statSync4(full);
|
|
36175
36439
|
if (st.isDirectory()) {
|
|
36176
36440
|
walk2(full);
|
|
@@ -36235,11 +36499,11 @@ function imageExt(mime) {
|
|
|
36235
36499
|
}
|
|
36236
36500
|
}
|
|
36237
36501
|
function materializeImages2(profileDir2, images, question) {
|
|
36238
|
-
const dir2 =
|
|
36502
|
+
const dir2 = join14(profileDir2, "help-images", randomUUID());
|
|
36239
36503
|
mkdirSync5(dir2, { recursive: true });
|
|
36240
36504
|
let rewritten = question;
|
|
36241
36505
|
images.forEach((img, i) => {
|
|
36242
|
-
const file =
|
|
36506
|
+
const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
|
|
36243
36507
|
writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
|
|
36244
36508
|
if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
|
|
36245
36509
|
});
|
|
@@ -36334,7 +36598,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
36334
36598
|
if (!text) return { body: "", error: "empty answer" };
|
|
36335
36599
|
const { body, moderation } = parseModeratedAnswer(text);
|
|
36336
36600
|
if (!body) return { body: "", error: "empty answer" };
|
|
36337
|
-
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
36601
|
+
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
|
|
36338
36602
|
return {
|
|
36339
36603
|
body,
|
|
36340
36604
|
...moderation ? { moderation } : {},
|
|
@@ -36381,12 +36645,12 @@ function reportToolResult(tool, ok, message) {
|
|
|
36381
36645
|
}
|
|
36382
36646
|
|
|
36383
36647
|
// src/core/autonomous.ts
|
|
36384
|
-
import { readFile as readFile2, writeFile, chmod } from "fs/promises";
|
|
36648
|
+
import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
|
|
36385
36649
|
import { readFileSync as readFileSync13 } from "fs";
|
|
36386
|
-
import { execFile as
|
|
36387
|
-
import { join as
|
|
36388
|
-
import { promisify as
|
|
36389
|
-
var
|
|
36650
|
+
import { execFile as execFile4, spawn as spawn5 } from "child_process";
|
|
36651
|
+
import { join as join15 } from "path";
|
|
36652
|
+
import { promisify as promisify4 } from "util";
|
|
36653
|
+
var run4 = promisify4(execFile4);
|
|
36390
36654
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
36391
36655
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
36392
36656
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
@@ -36419,11 +36683,11 @@ function tableCell(value) {
|
|
|
36419
36683
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
36420
36684
|
}
|
|
36421
36685
|
function tickScript(root) {
|
|
36422
|
-
return
|
|
36686
|
+
return join15(root, TICK_REL);
|
|
36423
36687
|
}
|
|
36424
36688
|
async function crontabList() {
|
|
36425
36689
|
try {
|
|
36426
|
-
const { stdout } = await
|
|
36690
|
+
const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
|
|
36427
36691
|
return stdout;
|
|
36428
36692
|
} catch {
|
|
36429
36693
|
return "";
|
|
@@ -36442,8 +36706,8 @@ async function cronInstalled(root) {
|
|
|
36442
36706
|
return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
|
|
36443
36707
|
}
|
|
36444
36708
|
async function getAutonomousStatus(root) {
|
|
36445
|
-
const settings = parseJson(await readText(
|
|
36446
|
-
const hist = parseJson(await readText(
|
|
36709
|
+
const settings = parseJson(await readText(join15(root, SETTINGS_REL))) ?? {};
|
|
36710
|
+
const hist = parseJson(await readText(join15(root, HISTORIES_REL))) ?? {};
|
|
36447
36711
|
const records = Array.isArray(hist.records) ? hist.records : [];
|
|
36448
36712
|
const last = records[records.length - 1];
|
|
36449
36713
|
const ticks = hist.ticks ?? {};
|
|
@@ -36460,9 +36724,9 @@ async function getAutonomousStatus(root) {
|
|
|
36460
36724
|
}
|
|
36461
36725
|
function isAutonomousRunning(root) {
|
|
36462
36726
|
try {
|
|
36463
|
-
const s = JSON.parse(readFileSync13(
|
|
36727
|
+
const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
|
|
36464
36728
|
if (s.paused === true) return false;
|
|
36465
|
-
const h = JSON.parse(readFileSync13(
|
|
36729
|
+
const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
|
|
36466
36730
|
const ts = h.records?.[h.records.length - 1]?.ts;
|
|
36467
36731
|
if (!ts) return false;
|
|
36468
36732
|
const t2 = Date.parse(ts.replace(" ", "T"));
|
|
@@ -36473,10 +36737,10 @@ function isAutonomousRunning(root) {
|
|
|
36473
36737
|
}
|
|
36474
36738
|
async function readAutonomous(root) {
|
|
36475
36739
|
const [settings, approvals, status, ...books] = await Promise.all([
|
|
36476
|
-
readText(
|
|
36477
|
-
readText(
|
|
36740
|
+
readText(join15(root, SETTINGS_REL)),
|
|
36741
|
+
readText(join15(root, APPROVALS_REL), "{}"),
|
|
36478
36742
|
getAutonomousStatus(root),
|
|
36479
|
-
...Object.values(BOOK_FILES).map((f) => readText(
|
|
36743
|
+
...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
|
|
36480
36744
|
]);
|
|
36481
36745
|
const keys = Object.keys(BOOK_FILES);
|
|
36482
36746
|
const bookMap = {};
|
|
@@ -36485,7 +36749,7 @@ async function readAutonomous(root) {
|
|
|
36485
36749
|
}
|
|
36486
36750
|
async function readAutonomousLogs(root, date) {
|
|
36487
36751
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
36488
|
-
const text = await readText(
|
|
36752
|
+
const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
36489
36753
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
36490
36754
|
return { date: day, lines };
|
|
36491
36755
|
}
|
|
@@ -36509,22 +36773,22 @@ async function writeAutonomous(root, req, by) {
|
|
|
36509
36773
|
switch (req.kind) {
|
|
36510
36774
|
case "settings": {
|
|
36511
36775
|
if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
|
|
36512
|
-
await
|
|
36776
|
+
await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
|
|
36513
36777
|
break;
|
|
36514
36778
|
}
|
|
36515
36779
|
case "approvals": {
|
|
36516
|
-
const map = parseJson(await readText(
|
|
36780
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
36517
36781
|
if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36518
36782
|
else delete map[req.taskId];
|
|
36519
|
-
await
|
|
36783
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
36520
36784
|
break;
|
|
36521
36785
|
}
|
|
36522
36786
|
case "userTodo": {
|
|
36523
|
-
const cur = await readText(
|
|
36787
|
+
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
36524
36788
|
const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
36525
36789
|
const row = `| | ${tableCell(req.content)} | ${trace} |
|
|
36526
36790
|
`;
|
|
36527
|
-
await
|
|
36791
|
+
await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
36528
36792
|
break;
|
|
36529
36793
|
}
|
|
36530
36794
|
case "cron": {
|
|
@@ -36540,7 +36804,7 @@ async function writeAutonomous(root, req, by) {
|
|
|
36540
36804
|
}
|
|
36541
36805
|
|
|
36542
36806
|
// src/core/network-probe.ts
|
|
36543
|
-
import { existsSync as
|
|
36807
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
36544
36808
|
import { connect } from "net";
|
|
36545
36809
|
var CANARY_TARGETS = [
|
|
36546
36810
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -36549,7 +36813,7 @@ var CANARY_TARGETS = [
|
|
|
36549
36813
|
var PROBE_TIMEOUT_MS = 2500;
|
|
36550
36814
|
function detectContainerized() {
|
|
36551
36815
|
try {
|
|
36552
|
-
if (
|
|
36816
|
+
if (existsSync11("/.dockerenv")) return true;
|
|
36553
36817
|
const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
|
|
36554
36818
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
36555
36819
|
} catch {
|
|
@@ -36619,7 +36883,7 @@ async function probeNetwork() {
|
|
|
36619
36883
|
// src/core/git-auth.ts
|
|
36620
36884
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
36621
36885
|
import { homedir as homedir4 } from "os";
|
|
36622
|
-
import { join as
|
|
36886
|
+
import { join as join16 } from "path";
|
|
36623
36887
|
function tokenTarget(method) {
|
|
36624
36888
|
if (method === "gitlab-group-token") {
|
|
36625
36889
|
return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
|
|
@@ -36637,7 +36901,7 @@ function applyGitAuth(method) {
|
|
|
36637
36901
|
try {
|
|
36638
36902
|
const line = `https://${target.user}:${token}@${target.host}
|
|
36639
36903
|
`;
|
|
36640
|
-
writeFileSync8(
|
|
36904
|
+
writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
|
|
36641
36905
|
return true;
|
|
36642
36906
|
} catch {
|
|
36643
36907
|
return false;
|
|
@@ -36822,7 +37086,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
36822
37086
|
const apiErr = parser.apiError();
|
|
36823
37087
|
if (finalExit === 0 && !apiErr.isError) {
|
|
36824
37088
|
const usage = parser.usage();
|
|
36825
|
-
if (usage.tokens === 0) usage.tokens =
|
|
37089
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
|
|
36826
37090
|
reportToolResult(attempt.cmd, true);
|
|
36827
37091
|
return {
|
|
36828
37092
|
exitCode: 0,
|
|
@@ -36874,8 +37138,8 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
36874
37138
|
}
|
|
36875
37139
|
|
|
36876
37140
|
// src/ui/slash-commands.ts
|
|
36877
|
-
import { existsSync as
|
|
36878
|
-
import { join as
|
|
37141
|
+
import { existsSync as existsSync12 } from "fs";
|
|
37142
|
+
import { join as join17 } from "path";
|
|
36879
37143
|
var CONFIG_KEYS = [
|
|
36880
37144
|
"aiCli",
|
|
36881
37145
|
"claudeHome",
|
|
@@ -36887,7 +37151,7 @@ var CONFIG_KEYS = [
|
|
|
36887
37151
|
"aiEnv"
|
|
36888
37152
|
];
|
|
36889
37153
|
function configPath(profileDir2) {
|
|
36890
|
-
return
|
|
37154
|
+
return join17(profileDir2, "config.json");
|
|
36891
37155
|
}
|
|
36892
37156
|
function parseConfigAssignment(key, valueTokens) {
|
|
36893
37157
|
const value = valueTokens.join(" ").trim();
|
|
@@ -36937,7 +37201,7 @@ function runConfig(ctx) {
|
|
|
36937
37201
|
ctx.print(t("slash.config.wroteDefault", { verb, path }));
|
|
36938
37202
|
ctx.print(t("slash.config.addClaudeHint"));
|
|
36939
37203
|
};
|
|
36940
|
-
if (
|
|
37204
|
+
if (existsSync12(path)) {
|
|
36941
37205
|
ctx.confirm(
|
|
36942
37206
|
t("slash.config.replaceConfirm", { path }),
|
|
36943
37207
|
() => writeDefaults(t("slash.config.verbReplaced"))
|
|
@@ -37022,7 +37286,7 @@ function runOutput(ctx) {
|
|
|
37022
37286
|
}
|
|
37023
37287
|
function runLogs(ctx) {
|
|
37024
37288
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
37025
|
-
const dir2 =
|
|
37289
|
+
const dir2 = join17(ctx.info.profileDir, "logs");
|
|
37026
37290
|
const lines = readRecentLogLines(dir2, n);
|
|
37027
37291
|
if (lines.length === 0) {
|
|
37028
37292
|
ctx.print(t("slash.logs.none"));
|
|
@@ -37337,13 +37601,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
|
|
|
37337
37601
|
}
|
|
37338
37602
|
|
|
37339
37603
|
// src/core/git-commit-identity.ts
|
|
37340
|
-
import { execFile as
|
|
37341
|
-
import { promisify as
|
|
37342
|
-
var
|
|
37604
|
+
import { execFile as execFile5 } from "child_process";
|
|
37605
|
+
import { promisify as promisify5 } from "util";
|
|
37606
|
+
var run5 = promisify5(execFile5);
|
|
37343
37607
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
37344
37608
|
if (!machineUsername) return;
|
|
37345
37609
|
try {
|
|
37346
|
-
await
|
|
37610
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
37347
37611
|
} catch {
|
|
37348
37612
|
}
|
|
37349
37613
|
}
|
|
@@ -37442,7 +37706,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
37442
37706
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
37443
37707
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
37444
37708
|
if (perPromptLimit > 0) {
|
|
37445
|
-
const estimated =
|
|
37709
|
+
const estimated = estimateTokens2(prompt);
|
|
37446
37710
|
if (estimated > perPromptLimit) {
|
|
37447
37711
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
37448
37712
|
return;
|
|
@@ -37860,11 +38124,11 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
37860
38124
|
|
|
37861
38125
|
// src/core/fs-browse.ts
|
|
37862
38126
|
import { readdir as readdir2 } from "fs/promises";
|
|
37863
|
-
import { resolve, sep as sep2 } from "path";
|
|
38127
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
37864
38128
|
async function listDir(path, root) {
|
|
37865
38129
|
if (!root) return { path: "", entries: [] };
|
|
37866
|
-
const base =
|
|
37867
|
-
const requested =
|
|
38130
|
+
const base = resolve2(root);
|
|
38131
|
+
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
37868
38132
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
37869
38133
|
try {
|
|
37870
38134
|
const dirents = await readdir2(target, { withFileTypes: true });
|
|
@@ -37883,12 +38147,12 @@ async function listDir(path, root) {
|
|
|
37883
38147
|
|
|
37884
38148
|
// src/core/fs-read.ts
|
|
37885
38149
|
import { readFile as readFile3 } from "fs/promises";
|
|
37886
|
-
import { resolve as
|
|
38150
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
37887
38151
|
var MAX_BYTES = 256 * 1024;
|
|
37888
38152
|
function resolveInRoot(root, relPath) {
|
|
37889
38153
|
if (!root) return null;
|
|
37890
|
-
const base =
|
|
37891
|
-
const target =
|
|
38154
|
+
const base = resolve3(root);
|
|
38155
|
+
const target = resolve3(base, relPath || ".");
|
|
37892
38156
|
return target === base || target.startsWith(base + sep3) ? target : null;
|
|
37893
38157
|
}
|
|
37894
38158
|
async function readWorkerFile(root, path) {
|
|
@@ -37907,13 +38171,13 @@ async function readWorkerFile(root, path) {
|
|
|
37907
38171
|
}
|
|
37908
38172
|
|
|
37909
38173
|
// src/core/fs-write.ts
|
|
37910
|
-
import { mkdir as
|
|
37911
|
-
import { dirname as
|
|
38174
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
38175
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
37912
38176
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
37913
38177
|
function resolveInRoot2(root, relPath) {
|
|
37914
38178
|
if (!root) return null;
|
|
37915
|
-
const base =
|
|
37916
|
-
const target =
|
|
38179
|
+
const base = resolve4(root);
|
|
38180
|
+
const target = resolve4(base, relPath || ".");
|
|
37917
38181
|
return target === base || target.startsWith(base + sep4) ? target : null;
|
|
37918
38182
|
}
|
|
37919
38183
|
async function writeWorkerFile(root, relPath, content) {
|
|
@@ -37926,8 +38190,8 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37926
38190
|
return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
|
|
37927
38191
|
}
|
|
37928
38192
|
try {
|
|
37929
|
-
await
|
|
37930
|
-
await
|
|
38193
|
+
await mkdir3(dirname4(target), { recursive: true });
|
|
38194
|
+
await writeFile3(target, content, "utf8");
|
|
37931
38195
|
return { ok: true, path: target, bytes };
|
|
37932
38196
|
} catch (err) {
|
|
37933
38197
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -37935,12 +38199,12 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37935
38199
|
}
|
|
37936
38200
|
|
|
37937
38201
|
// src/core/fs-mutate.ts
|
|
37938
|
-
import { mkdir as
|
|
37939
|
-
import { basename as basename3, dirname as
|
|
38202
|
+
import { mkdir as mkdir4, writeFile as writeFile4, rename, rm, stat } from "fs/promises";
|
|
38203
|
+
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
37940
38204
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
37941
38205
|
function resolveInRoot3(root, rel) {
|
|
37942
|
-
const base =
|
|
37943
|
-
const target =
|
|
38206
|
+
const base = resolve5(root);
|
|
38207
|
+
const target = resolve5(base, rel || ".");
|
|
37944
38208
|
return target === base || target.startsWith(base + sep5) ? target : null;
|
|
37945
38209
|
}
|
|
37946
38210
|
async function isDir(p) {
|
|
@@ -37965,38 +38229,38 @@ async function mutateFs(root, req) {
|
|
|
37965
38229
|
switch (req.op) {
|
|
37966
38230
|
case "mkdir": {
|
|
37967
38231
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37968
|
-
if (!target || target ===
|
|
37969
|
-
await
|
|
38232
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
38233
|
+
await mkdir4(target, { recursive: true });
|
|
37970
38234
|
return { ok: true, path: target };
|
|
37971
38235
|
}
|
|
37972
38236
|
case "create": {
|
|
37973
38237
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37974
|
-
if (!target || target ===
|
|
38238
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
37975
38239
|
if (await exists(target)) return { ok: false, path: target, error: "already exists" };
|
|
37976
38240
|
const content = req.content ?? "";
|
|
37977
38241
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
|
|
37978
38242
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
37979
38243
|
}
|
|
37980
|
-
await
|
|
37981
|
-
await
|
|
38244
|
+
await mkdir4(dirname5(target), { recursive: true });
|
|
38245
|
+
await writeFile4(target, content, "utf8");
|
|
37982
38246
|
return { ok: true, path: target };
|
|
37983
38247
|
}
|
|
37984
38248
|
case "move": {
|
|
37985
38249
|
const from = resolveInRoot3(root, req.from ?? "");
|
|
37986
38250
|
let to = resolveInRoot3(root, req.to ?? "");
|
|
37987
38251
|
if (!from || !to) return escaped(req.from ?? req.to ?? "");
|
|
37988
|
-
if (from ===
|
|
37989
|
-
if (await isDir(to)) to =
|
|
37990
|
-
if (to !==
|
|
38252
|
+
if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
|
|
38253
|
+
if (await isDir(to)) to = resolve5(to, basename3(from));
|
|
38254
|
+
if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
37991
38255
|
if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
|
|
37992
|
-
await
|
|
38256
|
+
await mkdir4(dirname5(to), { recursive: true });
|
|
37993
38257
|
await rename(from, to);
|
|
37994
38258
|
return { ok: true, path: to };
|
|
37995
38259
|
}
|
|
37996
38260
|
case "delete": {
|
|
37997
38261
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37998
38262
|
if (!target) return escaped(req.path ?? "");
|
|
37999
|
-
if (target ===
|
|
38263
|
+
if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
38000
38264
|
await rm(target, { recursive: true, force: true });
|
|
38001
38265
|
return { ok: true, path: target };
|
|
38002
38266
|
}
|
|
@@ -38009,12 +38273,12 @@ async function mutateFs(root, req) {
|
|
|
38009
38273
|
}
|
|
38010
38274
|
|
|
38011
38275
|
// src/core/fs-transfer.ts
|
|
38012
|
-
import { mkdir as
|
|
38013
|
-
import { basename as basename4, dirname as
|
|
38276
|
+
import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
38277
|
+
import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
|
|
38014
38278
|
function resolveInRoot4(root, relPath) {
|
|
38015
38279
|
if (!root) return null;
|
|
38016
|
-
const base =
|
|
38017
|
-
const target =
|
|
38280
|
+
const base = resolve6(root);
|
|
38281
|
+
const target = resolve6(base, relPath || ".");
|
|
38018
38282
|
return target === base || target.startsWith(base + sep6) ? target : null;
|
|
38019
38283
|
}
|
|
38020
38284
|
var MIME_BY_EXT = {
|
|
@@ -38053,8 +38317,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
38053
38317
|
return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
38054
38318
|
}
|
|
38055
38319
|
try {
|
|
38056
|
-
await
|
|
38057
|
-
await
|
|
38320
|
+
await mkdir5(dirname6(target), { recursive: true });
|
|
38321
|
+
await writeFile5(target, buf);
|
|
38058
38322
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
38059
38323
|
} catch (err) {
|
|
38060
38324
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -38129,14 +38393,14 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
38129
38393
|
|
|
38130
38394
|
// src/core/git-diff.ts
|
|
38131
38395
|
import { readFile as readFile5 } from "fs/promises";
|
|
38132
|
-
import { execFile as
|
|
38133
|
-
import { basename as basename5, dirname as
|
|
38134
|
-
import { promisify as
|
|
38135
|
-
var
|
|
38396
|
+
import { execFile as execFile6 } from "child_process";
|
|
38397
|
+
import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
|
|
38398
|
+
import { promisify as promisify6 } from "util";
|
|
38399
|
+
var run6 = promisify6(execFile6);
|
|
38136
38400
|
function resolveInRoot5(root, relPath) {
|
|
38137
38401
|
if (!root) return null;
|
|
38138
|
-
const base =
|
|
38139
|
-
const target =
|
|
38402
|
+
const base = resolve7(root);
|
|
38403
|
+
const target = resolve7(base, relPath || ".");
|
|
38140
38404
|
return target === base || target.startsWith(base + sep7) ? target : null;
|
|
38141
38405
|
}
|
|
38142
38406
|
function looksBinary(buf) {
|
|
@@ -38151,7 +38415,7 @@ async function gitDiff(root, path) {
|
|
|
38151
38415
|
if (!target) {
|
|
38152
38416
|
return { path, oldContent: "", newContent: "", isBinary: false };
|
|
38153
38417
|
}
|
|
38154
|
-
const dir2 =
|
|
38418
|
+
const dir2 = dirname7(target);
|
|
38155
38419
|
const base = basename5(target);
|
|
38156
38420
|
let newBuf = null;
|
|
38157
38421
|
try {
|
|
@@ -38160,7 +38424,7 @@ async function gitDiff(root, path) {
|
|
|
38160
38424
|
}
|
|
38161
38425
|
let oldBuf = null;
|
|
38162
38426
|
try {
|
|
38163
|
-
const { stdout } = await
|
|
38427
|
+
const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
|
|
38164
38428
|
cwd: dir2,
|
|
38165
38429
|
timeout: 15e3,
|
|
38166
38430
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -38190,12 +38454,12 @@ async function gitDiff(root, path) {
|
|
|
38190
38454
|
}
|
|
38191
38455
|
|
|
38192
38456
|
// src/core/git-env.ts
|
|
38193
|
-
import { execFile as
|
|
38194
|
-
import { promisify as
|
|
38195
|
-
var
|
|
38457
|
+
import { execFile as execFile7 } from "child_process";
|
|
38458
|
+
import { promisify as promisify7 } from "util";
|
|
38459
|
+
var run7 = promisify7(execFile7);
|
|
38196
38460
|
async function hasBinary(bin, args = ["--version"]) {
|
|
38197
38461
|
try {
|
|
38198
|
-
await
|
|
38462
|
+
await run7(bin, args, { timeout: 5e3 });
|
|
38199
38463
|
return true;
|
|
38200
38464
|
} catch {
|
|
38201
38465
|
return false;
|
|
@@ -38208,7 +38472,7 @@ async function checkGitEnv(provider) {
|
|
|
38208
38472
|
let account = null;
|
|
38209
38473
|
if (installed) {
|
|
38210
38474
|
try {
|
|
38211
|
-
const { stdout, stderr } = await
|
|
38475
|
+
const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
|
|
38212
38476
|
const out = `${stdout}
|
|
38213
38477
|
${stderr}`;
|
|
38214
38478
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -38221,14 +38485,14 @@ ${stderr}`;
|
|
|
38221
38485
|
}
|
|
38222
38486
|
|
|
38223
38487
|
// src/core/git-ssh-key.ts
|
|
38224
|
-
import { execFile as
|
|
38225
|
-
import { readFile as readFile6, rm as rm2, mkdir as
|
|
38226
|
-
import { join as
|
|
38227
|
-
import { promisify as
|
|
38228
|
-
var
|
|
38488
|
+
import { execFile as execFile8 } from "child_process";
|
|
38489
|
+
import { readFile as readFile6, rm as rm2, mkdir as mkdir6 } from "fs/promises";
|
|
38490
|
+
import { join as join18 } from "path";
|
|
38491
|
+
import { promisify as promisify8 } from "util";
|
|
38492
|
+
var run8 = promisify8(execFile8);
|
|
38229
38493
|
function keyPaths(profileDir2) {
|
|
38230
|
-
const dir2 =
|
|
38231
|
-
const key =
|
|
38494
|
+
const dir2 = join18(profileDir2, ".ssh");
|
|
38495
|
+
const key = join18(dir2, "id_4pm");
|
|
38232
38496
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
38233
38497
|
}
|
|
38234
38498
|
async function readPublicKey(pub) {
|
|
@@ -38240,14 +38504,14 @@ async function readPublicKey(pub) {
|
|
|
38240
38504
|
}
|
|
38241
38505
|
async function fingerprintOf(pub) {
|
|
38242
38506
|
try {
|
|
38243
|
-
const { stdout } = await
|
|
38507
|
+
const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
38244
38508
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
38245
38509
|
} catch {
|
|
38246
38510
|
return null;
|
|
38247
38511
|
}
|
|
38248
38512
|
}
|
|
38249
38513
|
async function configureGit(key) {
|
|
38250
|
-
await
|
|
38514
|
+
await run8(
|
|
38251
38515
|
"git",
|
|
38252
38516
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
38253
38517
|
{ timeout: 5e3 }
|
|
@@ -38255,10 +38519,10 @@ async function configureGit(key) {
|
|
|
38255
38519
|
}
|
|
38256
38520
|
async function generate(profileDir2) {
|
|
38257
38521
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
38258
|
-
await
|
|
38522
|
+
await mkdir6(dir2, { recursive: true });
|
|
38259
38523
|
await rm2(key, { force: true });
|
|
38260
38524
|
await rm2(pub, { force: true });
|
|
38261
|
-
await
|
|
38525
|
+
await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
38262
38526
|
timeout: 15e3
|
|
38263
38527
|
});
|
|
38264
38528
|
await configureGit(key);
|
|
@@ -38268,7 +38532,7 @@ async function remove(profileDir2) {
|
|
|
38268
38532
|
const { key, pub } = keyPaths(profileDir2);
|
|
38269
38533
|
await rm2(key, { force: true });
|
|
38270
38534
|
await rm2(pub, { force: true });
|
|
38271
|
-
await
|
|
38535
|
+
await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
38272
38536
|
() => void 0
|
|
38273
38537
|
);
|
|
38274
38538
|
return { publicKey: null, fingerprint: null };
|
|
@@ -38290,21 +38554,21 @@ async function manageSshKey(op, profileDir2) {
|
|
|
38290
38554
|
|
|
38291
38555
|
// src/core/git-history.ts
|
|
38292
38556
|
import { readdir as readdir3 } from "fs/promises";
|
|
38293
|
-
import { execFile as
|
|
38294
|
-
import { resolve as
|
|
38295
|
-
import { promisify as
|
|
38296
|
-
var
|
|
38557
|
+
import { execFile as execFile9 } from "child_process";
|
|
38558
|
+
import { resolve as resolve8, sep as sep8 } from "path";
|
|
38559
|
+
import { promisify as promisify9 } from "util";
|
|
38560
|
+
var run9 = promisify9(execFile9);
|
|
38297
38561
|
var FS = "";
|
|
38298
38562
|
var RS = "";
|
|
38299
38563
|
function resolveRepo(root, repo) {
|
|
38300
38564
|
if (!root) return null;
|
|
38301
|
-
const base =
|
|
38302
|
-
const requested =
|
|
38565
|
+
const base = resolve8(root);
|
|
38566
|
+
const requested = resolve8(base, repo && repo.trim() ? repo : ".");
|
|
38303
38567
|
return requested === base || requested.startsWith(base + sep8) ? requested : base;
|
|
38304
38568
|
}
|
|
38305
38569
|
async function git(cwd2, args) {
|
|
38306
38570
|
try {
|
|
38307
|
-
const { stdout } = await
|
|
38571
|
+
const { stdout } = await run9("git", args, {
|
|
38308
38572
|
cwd: cwd2,
|
|
38309
38573
|
timeout: 15e3,
|
|
38310
38574
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -38316,7 +38580,7 @@ async function git(cwd2, args) {
|
|
|
38316
38580
|
}
|
|
38317
38581
|
async function gitRepos(root) {
|
|
38318
38582
|
if (!root) return { repos: [] };
|
|
38319
|
-
const base =
|
|
38583
|
+
const base = resolve8(root);
|
|
38320
38584
|
const found = /* @__PURE__ */ new Set();
|
|
38321
38585
|
const repos = [];
|
|
38322
38586
|
const add = async (dir2) => {
|
|
@@ -38333,13 +38597,13 @@ async function gitRepos(root) {
|
|
|
38333
38597
|
const lvl1 = await readdir3(base, { withFileTypes: true });
|
|
38334
38598
|
for (const d of lvl1) {
|
|
38335
38599
|
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
38336
|
-
const p1 =
|
|
38600
|
+
const p1 = resolve8(base, d.name);
|
|
38337
38601
|
await add(p1);
|
|
38338
38602
|
try {
|
|
38339
38603
|
const lvl2 = await readdir3(p1, { withFileTypes: true });
|
|
38340
38604
|
for (const d2 of lvl2) {
|
|
38341
38605
|
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
38342
|
-
await add(
|
|
38606
|
+
await add(resolve8(p1, d2.name));
|
|
38343
38607
|
}
|
|
38344
38608
|
} catch {
|
|
38345
38609
|
}
|
|
@@ -38457,79 +38721,7 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
38457
38721
|
}
|
|
38458
38722
|
|
|
38459
38723
|
// 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
|
-
}
|
|
38724
|
+
import { join as join19 } from "path";
|
|
38533
38725
|
|
|
38534
38726
|
// src/core/outbound-review.ts
|
|
38535
38727
|
function compilePatterns(sources, fallback) {
|
|
@@ -38605,9 +38797,13 @@ async function evaluateReview(payload) {
|
|
|
38605
38797
|
// src/core/ws-client/handlers/misc.ts
|
|
38606
38798
|
function handleMiscChannels(ctx, message, payload) {
|
|
38607
38799
|
switch (message.channel) {
|
|
38800
|
+
case WsChannels.CLI_UPDATE: {
|
|
38801
|
+
ctx.updateCliNow();
|
|
38802
|
+
return true;
|
|
38803
|
+
}
|
|
38608
38804
|
case WsChannels.LOG_READ: {
|
|
38609
38805
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
38610
|
-
const lines = readRecentLogLines(
|
|
38806
|
+
const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
|
|
38611
38807
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
38612
38808
|
return true;
|
|
38613
38809
|
}
|
|
@@ -38638,168 +38834,29 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
38638
38834
|
}
|
|
38639
38835
|
|
|
38640
38836
|
// src/core/ws-client/handlers/project.ts
|
|
38641
|
-
import { mkdirSync as mkdirSync6, rmSync as rmSync6 } from "fs";
|
|
38837
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
|
|
38642
38838
|
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
|
-
emit("git", `Cloning ${repo.url}\u2026`);
|
|
38665
|
-
await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
38666
|
-
continue;
|
|
38667
|
-
}
|
|
38668
|
-
emit("git", "Initializing the primary repo\u2026");
|
|
38669
|
-
await run9("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
38670
|
-
if (repo.url) {
|
|
38671
|
-
await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
38672
|
-
() => void 0
|
|
38673
|
-
);
|
|
38674
|
-
}
|
|
38675
|
-
continue;
|
|
38676
|
-
}
|
|
38677
|
-
const dir2 = join19(target, subDirName(repo));
|
|
38678
|
-
if (repo.url) {
|
|
38679
|
-
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
38680
|
-
await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
38681
|
-
} else {
|
|
38682
|
-
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
38683
|
-
await mkdir6(dir2, { recursive: true });
|
|
38684
|
-
await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
38685
|
-
}
|
|
38686
|
-
}
|
|
38687
|
-
}
|
|
38688
|
-
function sampleDir() {
|
|
38689
|
-
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
38690
|
-
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
38691
|
-
const bundled = resolve8(here, "project-sample");
|
|
38692
|
-
for (const candidate of [bundled, resolve8(here, "../project-sample"), resolve8(here, "../../project-sample")]) {
|
|
38693
|
-
if (existsSync12(candidate)) return candidate;
|
|
38694
|
-
}
|
|
38695
|
-
return bundled;
|
|
38696
|
-
}
|
|
38697
|
-
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
38698
|
-
let lastStep = "start";
|
|
38699
|
-
const emit = (step, message) => {
|
|
38700
|
-
lastStep = step;
|
|
38701
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38702
|
-
};
|
|
38703
|
-
try {
|
|
38704
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38705
|
-
const target = join19(root, payload.projectName);
|
|
38706
|
-
await mkdir6(target, { recursive: true });
|
|
38707
|
-
emit("copy", "Copying the sample template\u2026");
|
|
38708
|
-
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
38709
|
-
if (payload.spec) {
|
|
38710
|
-
emit("spec", "Writing project.spec.json\u2026");
|
|
38711
|
-
await writeFile5(
|
|
38712
|
-
join19(target, "project.spec.json"),
|
|
38713
|
-
JSON.stringify(payload.spec, null, 2),
|
|
38714
|
-
"utf8"
|
|
38715
|
-
);
|
|
38716
|
-
}
|
|
38717
|
-
const repos = reposOf(payload.spec);
|
|
38718
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
38719
|
-
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
38720
|
-
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
38721
|
-
await writeTemplateMarker(target);
|
|
38722
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
38723
|
-
return { ok: true, path: target };
|
|
38724
|
-
} catch (err) {
|
|
38725
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38726
|
-
}
|
|
38727
|
-
}
|
|
38728
|
-
async function writeTemplateMarker(target) {
|
|
38729
|
-
const dir2 = join19(target, ".4pm");
|
|
38730
|
-
await mkdir6(dir2, { recursive: true });
|
|
38731
|
-
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
38732
|
-
await writeFile5(join19(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
38733
|
-
}
|
|
38734
|
-
async function aiInit(target, spec, emit) {
|
|
38735
|
-
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
38736
|
-
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
38737
|
-
if (subagents.length > 0) {
|
|
38738
|
-
await mkdir6(join19(target, ".claude", "agents"), { recursive: true });
|
|
38739
|
-
for (const sa of subagents) {
|
|
38740
|
-
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
38741
|
-
if (!name) continue;
|
|
38742
|
-
const body = `---
|
|
38743
|
-
name: ${name}
|
|
38744
|
-
---
|
|
38745
|
-
|
|
38746
|
-
${(sa.description || "").trim()}
|
|
38747
|
-
`;
|
|
38748
|
-
await writeFile5(join19(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
38749
|
-
}
|
|
38750
|
-
}
|
|
38751
|
-
const specJson = JSON.stringify(spec);
|
|
38752
|
-
await generateFile(
|
|
38753
|
-
join19(target, "README.md"),
|
|
38754
|
-
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
38755
|
-
${specJson}`
|
|
38756
|
-
);
|
|
38757
|
-
await generateFile(
|
|
38758
|
-
join19(target, "CLAUDE.md"),
|
|
38759
|
-
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
38760
|
-
${specJson}`
|
|
38761
|
-
);
|
|
38762
|
-
}
|
|
38763
|
-
async function generateFile(path, prompt) {
|
|
38764
|
-
try {
|
|
38765
|
-
const text = await aiGenerate(prompt);
|
|
38766
|
-
if (text.trim()) await writeFile5(path, text.trim() + "\n", "utf8");
|
|
38767
|
-
} catch {
|
|
38768
|
-
}
|
|
38769
|
-
}
|
|
38770
|
-
async function addProject(payload, profileDir2, onProgress) {
|
|
38771
|
-
let lastStep = "start";
|
|
38772
|
-
const emit = (step, message) => {
|
|
38773
|
-
lastStep = step;
|
|
38774
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38775
|
-
};
|
|
38776
|
-
try {
|
|
38777
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38778
|
-
const target = join19(root, payload.projectName);
|
|
38779
|
-
await mkdir6(target, { recursive: true });
|
|
38780
|
-
const repos = payload.repos ?? [];
|
|
38781
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
|
|
38782
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
38783
|
-
return { ok: true, path: target };
|
|
38784
|
-
} catch (err) {
|
|
38785
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38786
|
-
}
|
|
38787
|
-
}
|
|
38788
|
-
|
|
38789
|
-
// src/core/ws-client/handlers/project.ts
|
|
38790
38839
|
function handleProjectChannels(ctx, message, payload) {
|
|
38791
38840
|
switch (message.channel) {
|
|
38792
38841
|
case WsChannels.PHYSIC_SYNC: {
|
|
38793
38842
|
const sync = payload;
|
|
38794
|
-
|
|
38795
|
-
|
|
38843
|
+
const oldName = sync.oldName;
|
|
38844
|
+
const oldPath = oldName ? join20(ctx.profileDir, oldName) : null;
|
|
38845
|
+
const newPath = join20(ctx.profileDir, sync.newName);
|
|
38846
|
+
if (oldName) {
|
|
38847
|
+
const oldRoot = ctx.physicFolderPath(oldName);
|
|
38796
38848
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
38797
38849
|
}
|
|
38798
|
-
if (
|
|
38799
|
-
|
|
38850
|
+
if (oldPath && oldName !== sync.newName && existsSync13(oldPath) && !existsSync13(newPath)) {
|
|
38851
|
+
renameSync(oldPath, newPath);
|
|
38852
|
+
} else {
|
|
38853
|
+
if (oldPath && oldName !== sync.newName && existsSync13(oldPath)) rmSync6(oldPath, { recursive: true, force: true });
|
|
38854
|
+
mkdirSync6(newPath, { recursive: true });
|
|
38855
|
+
}
|
|
38800
38856
|
ctx.physicRoot = ctx.physicFolderPath(sync.newName);
|
|
38801
38857
|
ctx.bus.setProject(sync.newName);
|
|
38802
38858
|
ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
|
|
38859
|
+
void ctx.cloneServingRepos().catch(() => void 0);
|
|
38803
38860
|
return true;
|
|
38804
38861
|
}
|
|
38805
38862
|
case WsChannels.PHYSIC_DELETE: {
|
|
@@ -39587,7 +39644,7 @@ async function buildGraph(root, mode) {
|
|
|
39587
39644
|
|
|
39588
39645
|
// src/core/rag.ts
|
|
39589
39646
|
import { readFile as readFile12, writeFile as writeFile10, statfs as statfs2, mkdir as mkdir10 } from "fs/promises";
|
|
39590
|
-
import { existsSync as
|
|
39647
|
+
import { existsSync as existsSync14 } from "fs";
|
|
39591
39648
|
import { execFile as execFile10, spawn as spawn9 } from "child_process";
|
|
39592
39649
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
39593
39650
|
import { join as join26 } from "path";
|
|
@@ -39680,7 +39737,7 @@ async function ragStatus(root) {
|
|
|
39680
39737
|
const modelPresent = modelName.length > 0;
|
|
39681
39738
|
return {
|
|
39682
39739
|
installed: fastembed && sqliteVec && modelPresent,
|
|
39683
|
-
installing:
|
|
39740
|
+
installing: existsSync14(join26(root, RAG_REL, ".installing")),
|
|
39684
39741
|
python: { found: py.found, version: py.version },
|
|
39685
39742
|
deps: { pip, fastembed, sqliteVec },
|
|
39686
39743
|
model: { present: modelPresent, name: modelName },
|
|
@@ -39691,7 +39748,7 @@ async function ragStatus(root) {
|
|
|
39691
39748
|
};
|
|
39692
39749
|
}
|
|
39693
39750
|
async function readIndexMeta(root) {
|
|
39694
|
-
const indexing =
|
|
39751
|
+
const indexing = existsSync14(join26(root, RAG_REL, ".indexing"));
|
|
39695
39752
|
try {
|
|
39696
39753
|
const m = JSON.parse(await readFile12(join26(root, RAG_REL, ".index.json"), "utf8"));
|
|
39697
39754
|
return {
|
|
@@ -39766,8 +39823,8 @@ async function ensureEngine(root) {
|
|
|
39766
39823
|
async function ragReindex(root) {
|
|
39767
39824
|
const py = await detectPython();
|
|
39768
39825
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
39769
|
-
if (!
|
|
39770
|
-
if (
|
|
39826
|
+
if (!existsSync14(join26(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
|
|
39827
|
+
if (existsSync14(join26(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
|
|
39771
39828
|
await ensureEngine(root);
|
|
39772
39829
|
const script = `
|
|
39773
39830
|
set +e
|
|
@@ -39787,7 +39844,7 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
39787
39844
|
async function ragQuery(root, queryText, k = 8) {
|
|
39788
39845
|
const py = await detectPython();
|
|
39789
39846
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
39790
|
-
if (!
|
|
39847
|
+
if (!existsSync14(join26(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
39791
39848
|
await ensureEngine(root);
|
|
39792
39849
|
try {
|
|
39793
39850
|
const { stdout } = await run10(
|
|
@@ -39807,7 +39864,7 @@ async function ragInstall(root, model) {
|
|
|
39807
39864
|
}
|
|
39808
39865
|
const py = await detectPython();
|
|
39809
39866
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
39810
|
-
if (
|
|
39867
|
+
if (existsSync14(join26(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
39811
39868
|
try {
|
|
39812
39869
|
await mkdir10(join26(root, RAG_REL), { recursive: true });
|
|
39813
39870
|
} catch {
|
|
@@ -39836,7 +39893,7 @@ rm -f "${RAG_REL}/.installing"
|
|
|
39836
39893
|
}
|
|
39837
39894
|
|
|
39838
39895
|
// src/core/config-sync.ts
|
|
39839
|
-
import { existsSync as
|
|
39896
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
|
|
39840
39897
|
import { join as join27 } from "path";
|
|
39841
39898
|
var SERVER_MANAGED_KEYS = [
|
|
39842
39899
|
"physicPath",
|
|
@@ -39855,7 +39912,7 @@ var SERVER_MANAGED_KEYS = [
|
|
|
39855
39912
|
];
|
|
39856
39913
|
function readConfigText(profileDir2) {
|
|
39857
39914
|
const path = join27(profileDir2, "config.json");
|
|
39858
|
-
if (
|
|
39915
|
+
if (existsSync15(path)) {
|
|
39859
39916
|
try {
|
|
39860
39917
|
return readFileSync15(path, "utf8");
|
|
39861
39918
|
} catch {
|
|
@@ -40155,6 +40212,9 @@ var WsClient = class {
|
|
|
40155
40212
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
40156
40213
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
40157
40214
|
physicRoot = null;
|
|
40215
|
+
/** Declared repos of the served project (ADR-0289), from each ws_token — cloned into the physic
|
|
40216
|
+
* root on connect (and after a PHYSIC_SYNC creates the folder) when any is missing its `.git`. */
|
|
40217
|
+
servingRepos = [];
|
|
40158
40218
|
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
40159
40219
|
* Push uses the account already logged in with gh/glab on the worker. */
|
|
40160
40220
|
machineUsername = "";
|
|
@@ -40272,9 +40332,24 @@ var WsClient = class {
|
|
|
40272
40332
|
setConsoleWatching: (on) => self.setConsoleWatching(on),
|
|
40273
40333
|
setMetricsWatching: (on) => self.setMetricsWatching(on),
|
|
40274
40334
|
ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
|
|
40275
|
-
physicFolderPath: (projectName) => self.physicFolderPath(projectName)
|
|
40335
|
+
physicFolderPath: (projectName) => self.physicFolderPath(projectName),
|
|
40336
|
+
cloneServingRepos: () => self.ensureServingReposCloned(),
|
|
40337
|
+
updateCliNow: () => self.updateScheduler.updateNow()
|
|
40276
40338
|
};
|
|
40277
40339
|
}
|
|
40340
|
+
/**
|
|
40341
|
+
* Clone any declared repo missing from the served physic root (ADR-0289) — idempotent (skips a
|
|
40342
|
+
* repo that already has `.git`). Best-effort: a clone failure never disrupts the session (the web
|
|
40343
|
+
* still surfaces "not provisioned"). No-op when idle (no physic root) or no repos declared.
|
|
40344
|
+
*/
|
|
40345
|
+
async ensureServingReposCloned() {
|
|
40346
|
+
if (!this.physicRoot || this.servingRepos.length === 0) return;
|
|
40347
|
+
try {
|
|
40348
|
+
await ensureReposCloned(this.physicRoot, this.servingRepos, (_step, message) => this.bus.log(message));
|
|
40349
|
+
} catch (err) {
|
|
40350
|
+
logger.warn("repos.clone.failed", { error: err instanceof Error ? err.message : String(err) });
|
|
40351
|
+
}
|
|
40352
|
+
}
|
|
40278
40353
|
/**
|
|
40279
40354
|
* Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
|
|
40280
40355
|
* DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
|
|
@@ -40458,6 +40533,8 @@ var WsClient = class {
|
|
|
40458
40533
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
40459
40534
|
applyGitAuth(token.gitAuth ?? null);
|
|
40460
40535
|
this.machineUsername = token.machineUsername ?? "";
|
|
40536
|
+
this.servingRepos = token.repos ?? [];
|
|
40537
|
+
void this.ensureServingReposCloned();
|
|
40461
40538
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
40462
40539
|
this.credential.scope = token.scope;
|
|
40463
40540
|
writeCredential(this.context.profileDir, this.credential);
|
|
@@ -40708,7 +40785,7 @@ var WsClient = class {
|
|
|
40708
40785
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
40709
40786
|
ensurePhysicFolderPath(folder) {
|
|
40710
40787
|
try {
|
|
40711
|
-
if (!
|
|
40788
|
+
if (!existsSync16(folder)) {
|
|
40712
40789
|
mkdirSync7(folder, { recursive: true });
|
|
40713
40790
|
this.bus.log(t("project.createdFolder", { folder }));
|
|
40714
40791
|
}
|
|
@@ -40738,7 +40815,7 @@ var WsClient = class {
|
|
|
40738
40815
|
projects: [],
|
|
40739
40816
|
version: CLI_VERSION,
|
|
40740
40817
|
physicPath,
|
|
40741
|
-
physicPathExists: physicPath ?
|
|
40818
|
+
physicPathExists: physicPath ? existsSync16(physicPath) : void 0,
|
|
40742
40819
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
40743
40820
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
|
|
40744
40821
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
@@ -47057,7 +47134,7 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
47057
47134
|
var import_react26 = __toESM(require_react(), 1);
|
|
47058
47135
|
|
|
47059
47136
|
// src/core/input-history.ts
|
|
47060
|
-
import { existsSync as
|
|
47137
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
47061
47138
|
import { join as join31 } from "path";
|
|
47062
47139
|
var MAX_HISTORY = 200;
|
|
47063
47140
|
function historyPath(profileDir2) {
|
|
@@ -47065,7 +47142,7 @@ function historyPath(profileDir2) {
|
|
|
47065
47142
|
}
|
|
47066
47143
|
function loadInputHistory(profileDir2) {
|
|
47067
47144
|
const path = historyPath(profileDir2);
|
|
47068
|
-
if (!
|
|
47145
|
+
if (!existsSync18(path)) return [];
|
|
47069
47146
|
try {
|
|
47070
47147
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
47071
47148
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|