@4pm/cli 1.5.42 → 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 +559 -418
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28635,6 +28635,37 @@ function isValidRoleSet(roles) {
|
|
|
28635
28635
|
return true;
|
|
28636
28636
|
}
|
|
28637
28637
|
|
|
28638
|
+
// vendor/@4pm/constants/worker-pool.ts
|
|
28639
|
+
var AiJob = {
|
|
28640
|
+
/** Spec field/section/subagent suggest (one-shot). */
|
|
28641
|
+
SPEC_SUGGEST: "spec-suggest",
|
|
28642
|
+
/** Spec review — advisory (one-shot). */
|
|
28643
|
+
SPEC_REVIEW: "spec-review",
|
|
28644
|
+
/** Spec compose + create verdict (one-shot). */
|
|
28645
|
+
SPEC_COMPOSE: "spec-compose",
|
|
28646
|
+
/** Generators: WBS/est/proposal/feasibility/report (one-shot). */
|
|
28647
|
+
DOC_GENERATE: "doc-generate",
|
|
28648
|
+
/** Console prompt — full agent. */
|
|
28649
|
+
CONSOLE_AGENT: "console-agent",
|
|
28650
|
+
/** Git merge / AI conflict resolve — full agent. */
|
|
28651
|
+
GIT_MERGE: "git-merge",
|
|
28652
|
+
/** Graph add-link/add-code/delete-unused advice (one-shot). */
|
|
28653
|
+
GRAPH_SUGGEST: "graph-suggest",
|
|
28654
|
+
/** Subagent/skill content generate (one-shot). */
|
|
28655
|
+
AGENTS_GENERATE: "agents-generate",
|
|
28656
|
+
/** Template "Analyze impact" report — read-only agent. */
|
|
28657
|
+
ANALYZE_IMPACT: "analyze-impact",
|
|
28658
|
+
/** Template "Update" → branch + PR — write-capable agent. */
|
|
28659
|
+
TEMPLATE_UPDATE: "template-update",
|
|
28660
|
+
/** Autonomous-mode self-spawn — full agent. */
|
|
28661
|
+
AUTONOMOUS: "autonomous",
|
|
28662
|
+
/** Memo → community post compose (vision) — full agent. */
|
|
28663
|
+
MEMO_COMPOSE: "memo-compose",
|
|
28664
|
+
/** Outbound input review — the reviewer cli (one-shot). */
|
|
28665
|
+
OUTBOUND_REVIEW: "outbound-review"
|
|
28666
|
+
};
|
|
28667
|
+
var AI_JOBS = Object.values(AiJob);
|
|
28668
|
+
|
|
28638
28669
|
// vendor/@4pm/constants/worker-tools.ts
|
|
28639
28670
|
var WORKER_TOOL_CATALOG = [
|
|
28640
28671
|
{ id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false },
|
|
@@ -29636,7 +29667,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
29636
29667
|
init_wrapper();
|
|
29637
29668
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
29638
29669
|
import { basename as basename7, join as join28 } from "path";
|
|
29639
|
-
import { existsSync as
|
|
29670
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
29640
29671
|
|
|
29641
29672
|
// vendor/@4pm/utils/helpers.ts
|
|
29642
29673
|
function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
@@ -33828,7 +33859,15 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
33828
33859
|
* "AI settings" modal (per-user localStorage), layered over the profile default by the cli.
|
|
33829
33860
|
* Only meaningful with `ai:true`.
|
|
33830
33861
|
*/
|
|
33831
|
-
aiConfig: aiRunConfigSchema.optional()
|
|
33862
|
+
aiConfig: aiRunConfigSchema.optional(),
|
|
33863
|
+
/**
|
|
33864
|
+
* AI job (ADR-0284): which of the 13 AI functions this dispatch is — the caller stamps it (the
|
|
33865
|
+
* end user never picks). With `pick:"idle"` it drives the job-aware routing over the project's
|
|
33866
|
+
* worker-routing table (pools before lone machines → job priority → member index, with
|
|
33867
|
+
* exhaustion failover). Absent ⇒ the flat idle pool (backward compatible). Ignored with an
|
|
33868
|
+
* explicit `machineLinkId`.
|
|
33869
|
+
*/
|
|
33870
|
+
job: external_exports.enum(AI_JOBS).optional()
|
|
33832
33871
|
}).superRefine((v, ctx) => {
|
|
33833
33872
|
if (v.machineLinkId == null === (v.pick == null)) {
|
|
33834
33873
|
ctx.addIssue({
|
|
@@ -34328,9 +34367,240 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
34328
34367
|
projectId: external_exports.string().uuid().optional()
|
|
34329
34368
|
});
|
|
34330
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
|
+
|
|
34331
34601
|
// src/core/worker-metrics.ts
|
|
34332
34602
|
import { statfs, readFile } from "fs/promises";
|
|
34333
|
-
import { existsSync as
|
|
34603
|
+
import { existsSync as existsSync5 } from "fs";
|
|
34334
34604
|
import { totalmem, freemem, cpus, platform, arch } from "os";
|
|
34335
34605
|
var CGROUP = "/sys/fs/cgroup";
|
|
34336
34606
|
var MB = 1024 * 1024;
|
|
@@ -34380,7 +34650,7 @@ function parseMaybeMax(text) {
|
|
|
34380
34650
|
return Number.isFinite(n) ? n : null;
|
|
34381
34651
|
}
|
|
34382
34652
|
function createWorkerMetricsSampler(diskPath) {
|
|
34383
|
-
const cgroupV2 =
|
|
34653
|
+
const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
|
|
34384
34654
|
let cgPrev = null;
|
|
34385
34655
|
let osPrev = null;
|
|
34386
34656
|
async function cgroupCpuCount() {
|
|
@@ -34695,6 +34965,10 @@ var WsChannels = {
|
|
|
34695
34965
|
// serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
|
|
34696
34966
|
// reconnect. `ws_token` seeds these on connect; this channel only carries updates.
|
|
34697
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",
|
|
34698
34972
|
// command lifecycle
|
|
34699
34973
|
COMMAND_ANNOUNCE: "command.announce",
|
|
34700
34974
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
@@ -34808,9 +35082,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
34808
35082
|
import { spawn as spawn2 } from "child_process";
|
|
34809
35083
|
|
|
34810
35084
|
// src/common/logger/logger.ts
|
|
34811
|
-
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
35085
|
+
import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
|
|
34812
35086
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
34813
|
-
import { join as
|
|
35087
|
+
import { join as join7 } from "path";
|
|
34814
35088
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
34815
35089
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
34816
35090
|
var DEFAULT_RETENTION_DAYS = 14;
|
|
@@ -34859,8 +35133,8 @@ var CliLogger = class {
|
|
|
34859
35133
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
34860
35134
|
const entry = { timestamp, level, ...this.base, event, ...fields };
|
|
34861
35135
|
const line = JSON.stringify(entry) + "\n";
|
|
34862
|
-
const path =
|
|
34863
|
-
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) => {
|
|
34864
35138
|
console.warn(`Failed to write cli log: ${err}`);
|
|
34865
35139
|
});
|
|
34866
35140
|
}
|
|
@@ -34874,7 +35148,7 @@ var CliLogger = class {
|
|
|
34874
35148
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
34875
35149
|
if (!match) continue;
|
|
34876
35150
|
if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
|
|
34877
|
-
await unlink(
|
|
35151
|
+
await unlink(join7(dir2, file)).catch(() => void 0);
|
|
34878
35152
|
}
|
|
34879
35153
|
}
|
|
34880
35154
|
}
|
|
@@ -34885,7 +35159,7 @@ function readRecentLogLines(dir2, limit) {
|
|
|
34885
35159
|
const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
|
|
34886
35160
|
const latest = files[files.length - 1];
|
|
34887
35161
|
if (!latest) return [];
|
|
34888
|
-
const lines = readFileSync6(
|
|
35162
|
+
const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
|
|
34889
35163
|
return lines.slice(-limit);
|
|
34890
35164
|
} catch {
|
|
34891
35165
|
return [];
|
|
@@ -34898,8 +35172,8 @@ function readLogUpload(dir2) {
|
|
|
34898
35172
|
const latest = files[files.length - 1];
|
|
34899
35173
|
if (!latest) return null;
|
|
34900
35174
|
let totalBytes = 0;
|
|
34901
|
-
for (const f of files) totalBytes += statSync(
|
|
34902
|
-
let content = readFileSync6(
|
|
35175
|
+
for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
|
|
35176
|
+
let content = readFileSync6(join7(dir2, latest), "utf8");
|
|
34903
35177
|
if (content.length > LOG_UPLOAD_CONTENT_MAX) {
|
|
34904
35178
|
content = `\u2026[truncated]
|
|
34905
35179
|
${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
@@ -34942,7 +35216,7 @@ function classifyToolFailure(code, out) {
|
|
|
34942
35216
|
}
|
|
34943
35217
|
return { reason: "other", retryable: false };
|
|
34944
35218
|
}
|
|
34945
|
-
function
|
|
35219
|
+
function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
34946
35220
|
return new Promise((resolve10) => {
|
|
34947
35221
|
let out = "";
|
|
34948
35222
|
let buf = "";
|
|
@@ -34982,13 +35256,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
34982
35256
|
});
|
|
34983
35257
|
}
|
|
34984
35258
|
async function detectOne(cmd, versionArg) {
|
|
34985
|
-
const { code, out } = await
|
|
35259
|
+
const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
|
|
34986
35260
|
if (code !== 0) return null;
|
|
34987
35261
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
34988
35262
|
return first ?? "";
|
|
34989
35263
|
}
|
|
34990
35264
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
34991
|
-
const { out } = await
|
|
35265
|
+
const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
34992
35266
|
if (!out.trim()) return [];
|
|
34993
35267
|
let deps = {};
|
|
34994
35268
|
try {
|
|
@@ -35064,7 +35338,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
35064
35338
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
35065
35339
|
}
|
|
35066
35340
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
35067
|
-
const { code } = await
|
|
35341
|
+
const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
35068
35342
|
return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
|
|
35069
35343
|
}
|
|
35070
35344
|
function setToolAutoUpdate(profileDir2, name, enabled) {
|
|
@@ -35095,7 +35369,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
35095
35369
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
35096
35370
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
35097
35371
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
35098
|
-
const { code, out } = await
|
|
35372
|
+
const { code, out } = await run3(manager, args, onLine, timeoutMs);
|
|
35099
35373
|
if (code === 0) return { ok: true };
|
|
35100
35374
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
35101
35375
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -35161,6 +35435,9 @@ var UpdateScheduler = class {
|
|
|
35161
35435
|
lastDoneDateKey = null;
|
|
35162
35436
|
/** Date key marked due but deferred because the cli was busy (stays set past the hour). */
|
|
35163
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;
|
|
35164
35441
|
/** Guards against a second update while one is in flight. */
|
|
35165
35442
|
updating = false;
|
|
35166
35443
|
/** Apply the latest policy (called on each ws_token) and start the tick timer once. */
|
|
@@ -35171,6 +35448,25 @@ var UpdateScheduler = class {
|
|
|
35171
35448
|
this.timer.unref?.();
|
|
35172
35449
|
}
|
|
35173
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
|
+
}
|
|
35174
35470
|
/** Stop the scheduler when the session ends (logout / replaced / stopped). */
|
|
35175
35471
|
stop() {
|
|
35176
35472
|
if (this.timer) {
|
|
@@ -35203,12 +35499,19 @@ var UpdateScheduler = class {
|
|
|
35203
35499
|
return null;
|
|
35204
35500
|
}
|
|
35205
35501
|
}
|
|
35206
|
-
/** One evaluation:
|
|
35502
|
+
/** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
|
|
35207
35503
|
async tick() {
|
|
35208
|
-
|
|
35209
|
-
if (!p || !p.autoUpdateDaily || this.updating) return;
|
|
35504
|
+
if (this.updating) return;
|
|
35210
35505
|
if (CLI_VERSION.startsWith("0.0.0")) return;
|
|
35211
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;
|
|
35212
35515
|
const clock = this.orgClock();
|
|
35213
35516
|
if (!clock) return;
|
|
35214
35517
|
if (clock.dateKey === this.lastDoneDateKey) return;
|
|
@@ -35266,14 +35569,14 @@ var UpdateScheduler = class {
|
|
|
35266
35569
|
};
|
|
35267
35570
|
|
|
35268
35571
|
// src/core/ai-profile-state.ts
|
|
35269
|
-
import { existsSync as
|
|
35270
|
-
import { join as
|
|
35572
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35573
|
+
import { join as join8 } from "path";
|
|
35271
35574
|
function statePath(profileDir2) {
|
|
35272
|
-
return
|
|
35575
|
+
return join8(profileDir2, "ai-profiles.json");
|
|
35273
35576
|
}
|
|
35274
35577
|
function readState(profileDir2) {
|
|
35275
35578
|
const path = statePath(profileDir2);
|
|
35276
|
-
if (!
|
|
35579
|
+
if (!existsSync6(path)) return {};
|
|
35277
35580
|
try {
|
|
35278
35581
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
35279
35582
|
} catch {
|
|
@@ -35331,7 +35634,7 @@ function clearPinnedCredential(profileDir2) {
|
|
|
35331
35634
|
// src/utils/ai-cli.ts
|
|
35332
35635
|
import { readFileSync as readFileSync8 } from "fs";
|
|
35333
35636
|
import { homedir as homedir3 } from "os";
|
|
35334
|
-
import { basename, isAbsolute, join as
|
|
35637
|
+
import { basename, isAbsolute, join as join9 } from "path";
|
|
35335
35638
|
var REQUIRED_AI_ARGS = {
|
|
35336
35639
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
35337
35640
|
codex: ["exec", "--json"],
|
|
@@ -35468,9 +35771,9 @@ function claudeCredentialKeys(config) {
|
|
|
35468
35771
|
return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
|
|
35469
35772
|
}
|
|
35470
35773
|
function resolveHomePath(value) {
|
|
35471
|
-
if (value.startsWith("~")) return
|
|
35774
|
+
if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
|
|
35472
35775
|
if (isAbsolute(value)) return value;
|
|
35473
|
-
return
|
|
35776
|
+
return join9(homedir3(), value);
|
|
35474
35777
|
}
|
|
35475
35778
|
function profileDirs(profiles) {
|
|
35476
35779
|
if (!profiles) return [];
|
|
@@ -35516,11 +35819,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
|
|
|
35516
35819
|
}
|
|
35517
35820
|
function claudeHomeDirs(config) {
|
|
35518
35821
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
35519
|
-
return dirs.length > 0 ? dirs : [
|
|
35822
|
+
return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
|
|
35520
35823
|
}
|
|
35521
35824
|
function profileDisplayLabel(dir2) {
|
|
35522
35825
|
try {
|
|
35523
|
-
const raw = JSON.parse(readFileSync8(
|
|
35826
|
+
const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
|
|
35524
35827
|
const email = raw.oauthAccount?.emailAddress;
|
|
35525
35828
|
if (email && email.trim()) return email;
|
|
35526
35829
|
} catch {
|
|
@@ -35622,11 +35925,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
|
|
|
35622
35925
|
// src/core/claude-usage.ts
|
|
35623
35926
|
import { spawn as spawn3 } from "child_process";
|
|
35624
35927
|
import { readFileSync as readFileSync9 } from "fs";
|
|
35625
|
-
import { join as
|
|
35928
|
+
import { join as join10 } from "path";
|
|
35626
35929
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
35627
35930
|
function readCreds(dir2) {
|
|
35628
35931
|
try {
|
|
35629
|
-
const raw = JSON.parse(readFileSync9(
|
|
35932
|
+
const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
|
|
35630
35933
|
const oauth = raw.claudeAiOauth ?? raw;
|
|
35631
35934
|
return oauth.accessToken ? oauth : null;
|
|
35632
35935
|
} catch {
|
|
@@ -35735,8 +36038,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
|
|
|
35735
36038
|
}
|
|
35736
36039
|
|
|
35737
36040
|
// src/core/command-history.ts
|
|
35738
|
-
import { existsSync as
|
|
35739
|
-
import { join as
|
|
36041
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
36042
|
+
import { join as join11 } from "path";
|
|
35740
36043
|
var MAX_ENTRIES = 1e3;
|
|
35741
36044
|
var filePath = null;
|
|
35742
36045
|
var entries = [];
|
|
@@ -35798,8 +36101,8 @@ async function uploadHistory(cfg) {
|
|
|
35798
36101
|
}
|
|
35799
36102
|
}
|
|
35800
36103
|
function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
|
|
35801
|
-
filePath =
|
|
35802
|
-
if (
|
|
36104
|
+
filePath = join11(profileDir2, "command-history.json");
|
|
36105
|
+
if (existsSync7(filePath)) {
|
|
35803
36106
|
try {
|
|
35804
36107
|
entries = JSON.parse(readFileSync10(filePath, "utf8"));
|
|
35805
36108
|
} catch {
|
|
@@ -35855,16 +36158,16 @@ function finishCommand(commandId, exitCode, usage) {
|
|
|
35855
36158
|
}
|
|
35856
36159
|
|
|
35857
36160
|
// src/core/command-images.ts
|
|
35858
|
-
import { existsSync as
|
|
35859
|
-
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";
|
|
35860
36163
|
function attachmentsBaseDir(physicRoot) {
|
|
35861
|
-
return
|
|
36164
|
+
return join12(physicRoot, ".4pm", "attachments");
|
|
35862
36165
|
}
|
|
35863
36166
|
var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
35864
36167
|
async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
35865
36168
|
const map = /* @__PURE__ */ new Map();
|
|
35866
36169
|
if (!images.length) return map;
|
|
35867
|
-
const dir2 =
|
|
36170
|
+
const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
|
|
35868
36171
|
mkdirSync3(dir2, { recursive: true });
|
|
35869
36172
|
let n = 0;
|
|
35870
36173
|
for (const img of images) {
|
|
@@ -35876,7 +36179,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
|
35876
36179
|
continue;
|
|
35877
36180
|
}
|
|
35878
36181
|
const ext = commandImageExt(reply.mime ?? img.mime);
|
|
35879
|
-
const file =
|
|
36182
|
+
const file = join12(dir2, `img-${n}.${ext}`);
|
|
35880
36183
|
writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
|
|
35881
36184
|
map.set(img.placeholder, file);
|
|
35882
36185
|
} catch (err) {
|
|
@@ -35898,12 +36201,12 @@ function rewriteImagePlaceholders(prompt, paths) {
|
|
|
35898
36201
|
}
|
|
35899
36202
|
function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
35900
36203
|
const base = attachmentsBaseDir(physicRoot);
|
|
35901
|
-
if (!
|
|
36204
|
+
if (!existsSync8(base)) return 0;
|
|
35902
36205
|
const cutoff = Date.now() - maxAgeMs;
|
|
35903
36206
|
let removed = 0;
|
|
35904
36207
|
try {
|
|
35905
36208
|
for (const entry of readdirSync3(base)) {
|
|
35906
|
-
const dir2 =
|
|
36209
|
+
const dir2 = join12(base, entry);
|
|
35907
36210
|
try {
|
|
35908
36211
|
if (statSync2(dir2).mtimeMs < cutoff) {
|
|
35909
36212
|
rmSync3(dir2, { recursive: true, force: true });
|
|
@@ -35920,18 +36223,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
|
|
|
35920
36223
|
// src/core/command-output-store.ts
|
|
35921
36224
|
import {
|
|
35922
36225
|
appendFileSync,
|
|
35923
|
-
existsSync as
|
|
36226
|
+
existsSync as existsSync9,
|
|
35924
36227
|
mkdirSync as mkdirSync4,
|
|
35925
36228
|
readFileSync as readFileSync11,
|
|
35926
36229
|
readdirSync as readdirSync4,
|
|
35927
36230
|
rmSync as rmSync4,
|
|
35928
36231
|
statSync as statSync3
|
|
35929
36232
|
} from "fs";
|
|
35930
|
-
import { join as
|
|
36233
|
+
import { join as join13 } from "path";
|
|
35931
36234
|
var KEEP_FILES = 200;
|
|
35932
36235
|
var dir = null;
|
|
35933
36236
|
function initCommandOutput(profileDir2) {
|
|
35934
|
-
dir =
|
|
36237
|
+
dir = join13(profileDir2, "command-output");
|
|
35935
36238
|
try {
|
|
35936
36239
|
mkdirSync4(dir, { recursive: true });
|
|
35937
36240
|
prune();
|
|
@@ -35941,14 +36244,14 @@ function initCommandOutput(profileDir2) {
|
|
|
35941
36244
|
function appendCommandOutput(commandId, text) {
|
|
35942
36245
|
if (!dir || !text) return;
|
|
35943
36246
|
try {
|
|
35944
|
-
appendFileSync(
|
|
36247
|
+
appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
|
|
35945
36248
|
} catch {
|
|
35946
36249
|
}
|
|
35947
36250
|
}
|
|
35948
36251
|
function readCommandOutput(commandId) {
|
|
35949
36252
|
if (!dir) return null;
|
|
35950
|
-
const path =
|
|
35951
|
-
if (!
|
|
36253
|
+
const path = join13(dir, `${commandId}.log`);
|
|
36254
|
+
if (!existsSync9(path)) return null;
|
|
35952
36255
|
try {
|
|
35953
36256
|
return readFileSync11(path, "utf8");
|
|
35954
36257
|
} catch {
|
|
@@ -35958,8 +36261,8 @@ function readCommandOutput(commandId) {
|
|
|
35958
36261
|
function prune() {
|
|
35959
36262
|
if (!dir) return;
|
|
35960
36263
|
try {
|
|
35961
|
-
const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(
|
|
35962
|
-
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 });
|
|
35963
36266
|
} catch {
|
|
35964
36267
|
}
|
|
35965
36268
|
}
|
|
@@ -35968,7 +36271,7 @@ function pruneCommandOutputByAge(days) {
|
|
|
35968
36271
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
35969
36272
|
try {
|
|
35970
36273
|
for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
|
|
35971
|
-
const p =
|
|
36274
|
+
const p = join13(dir, f);
|
|
35972
36275
|
if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
|
|
35973
36276
|
}
|
|
35974
36277
|
} catch {
|
|
@@ -35977,12 +36280,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
35977
36280
|
|
|
35978
36281
|
// src/core/support-answer.ts
|
|
35979
36282
|
import { spawn as spawn4 } from "child_process";
|
|
35980
|
-
import { execFile } from "child_process";
|
|
36283
|
+
import { execFile as execFile3 } from "child_process";
|
|
35981
36284
|
import { createHash as createHash3 } from "crypto";
|
|
35982
|
-
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";
|
|
35983
36286
|
import { randomUUID } from "crypto";
|
|
35984
|
-
import { join as
|
|
35985
|
-
import { promisify } from "util";
|
|
36287
|
+
import { join as join14 } from "path";
|
|
36288
|
+
import { promisify as promisify3 } from "util";
|
|
35986
36289
|
|
|
35987
36290
|
// src/core/ai-stream.ts
|
|
35988
36291
|
function assistantText(ev) {
|
|
@@ -36077,22 +36380,22 @@ function createAiStreamParser(cli) {
|
|
|
36077
36380
|
apiError: () => ({ ...apiError })
|
|
36078
36381
|
};
|
|
36079
36382
|
}
|
|
36080
|
-
function
|
|
36383
|
+
function estimateTokens2(text) {
|
|
36081
36384
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
36082
36385
|
}
|
|
36083
36386
|
|
|
36084
36387
|
// src/core/support-answer.ts
|
|
36085
36388
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
36086
|
-
var execFileAsync =
|
|
36389
|
+
var execFileAsync = promisify3(execFile3);
|
|
36087
36390
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
36088
36391
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
36089
36392
|
var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
36090
36393
|
function kbRoot(profileDir2) {
|
|
36091
|
-
return
|
|
36394
|
+
return join14(profileDir2, "support-kb");
|
|
36092
36395
|
}
|
|
36093
36396
|
function repoDir(profileDir2, url, branch) {
|
|
36094
36397
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
36095
|
-
return
|
|
36398
|
+
return join14(kbRoot(profileDir2), key);
|
|
36096
36399
|
}
|
|
36097
36400
|
function authedUrl(url, token) {
|
|
36098
36401
|
if (!token) return url;
|
|
@@ -36101,7 +36404,7 @@ function authedUrl(url, token) {
|
|
|
36101
36404
|
}
|
|
36102
36405
|
async function ensureRepo(profileDir2, repo) {
|
|
36103
36406
|
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
36104
|
-
if (
|
|
36407
|
+
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
36105
36408
|
const url = authedUrl(repo.url, repo.token);
|
|
36106
36409
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36107
36410
|
mkdirSync5(kbRoot(profileDir2), { recursive: true });
|
|
@@ -36112,11 +36415,11 @@ async function ensureRepo(profileDir2, repo) {
|
|
|
36112
36415
|
}
|
|
36113
36416
|
async function refreshSupportKb(profileDir2) {
|
|
36114
36417
|
const root = kbRoot(profileDir2);
|
|
36115
|
-
if (!
|
|
36418
|
+
if (!existsSync10(root)) return;
|
|
36116
36419
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
36117
36420
|
for (const entry of readdirSync5(root)) {
|
|
36118
|
-
const dir2 =
|
|
36119
|
-
if (!
|
|
36421
|
+
const dir2 = join14(root, entry);
|
|
36422
|
+
if (!existsSync10(join14(dir2, ".git"))) continue;
|
|
36120
36423
|
try {
|
|
36121
36424
|
await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
|
|
36122
36425
|
} catch (err) {
|
|
@@ -36131,7 +36434,7 @@ function collectDocs(dir2) {
|
|
|
36131
36434
|
if (total >= MAX_CONTEXT_CHARS) return;
|
|
36132
36435
|
for (const entry of readdirSync5(current2)) {
|
|
36133
36436
|
if (entry === ".git" || entry === "node_modules") continue;
|
|
36134
|
-
const full =
|
|
36437
|
+
const full = join14(current2, entry);
|
|
36135
36438
|
const st = statSync4(full);
|
|
36136
36439
|
if (st.isDirectory()) {
|
|
36137
36440
|
walk2(full);
|
|
@@ -36196,11 +36499,11 @@ function imageExt(mime) {
|
|
|
36196
36499
|
}
|
|
36197
36500
|
}
|
|
36198
36501
|
function materializeImages2(profileDir2, images, question) {
|
|
36199
|
-
const dir2 =
|
|
36502
|
+
const dir2 = join14(profileDir2, "help-images", randomUUID());
|
|
36200
36503
|
mkdirSync5(dir2, { recursive: true });
|
|
36201
36504
|
let rewritten = question;
|
|
36202
36505
|
images.forEach((img, i) => {
|
|
36203
|
-
const file =
|
|
36506
|
+
const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
|
|
36204
36507
|
writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
|
|
36205
36508
|
if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
|
|
36206
36509
|
});
|
|
@@ -36295,7 +36598,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
36295
36598
|
if (!text) return { body: "", error: "empty answer" };
|
|
36296
36599
|
const { body, moderation } = parseModeratedAnswer(text);
|
|
36297
36600
|
if (!body) return { body: "", error: "empty answer" };
|
|
36298
|
-
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
36601
|
+
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
|
|
36299
36602
|
return {
|
|
36300
36603
|
body,
|
|
36301
36604
|
...moderation ? { moderation } : {},
|
|
@@ -36342,12 +36645,12 @@ function reportToolResult(tool, ok, message) {
|
|
|
36342
36645
|
}
|
|
36343
36646
|
|
|
36344
36647
|
// src/core/autonomous.ts
|
|
36345
|
-
import { readFile as readFile2, writeFile, chmod } from "fs/promises";
|
|
36648
|
+
import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
|
|
36346
36649
|
import { readFileSync as readFileSync13 } from "fs";
|
|
36347
|
-
import { execFile as
|
|
36348
|
-
import { join as
|
|
36349
|
-
import { promisify as
|
|
36350
|
-
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);
|
|
36351
36654
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
36352
36655
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
36353
36656
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
@@ -36380,11 +36683,11 @@ function tableCell(value) {
|
|
|
36380
36683
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
36381
36684
|
}
|
|
36382
36685
|
function tickScript(root) {
|
|
36383
|
-
return
|
|
36686
|
+
return join15(root, TICK_REL);
|
|
36384
36687
|
}
|
|
36385
36688
|
async function crontabList() {
|
|
36386
36689
|
try {
|
|
36387
|
-
const { stdout } = await
|
|
36690
|
+
const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
|
|
36388
36691
|
return stdout;
|
|
36389
36692
|
} catch {
|
|
36390
36693
|
return "";
|
|
@@ -36403,8 +36706,8 @@ async function cronInstalled(root) {
|
|
|
36403
36706
|
return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
|
|
36404
36707
|
}
|
|
36405
36708
|
async function getAutonomousStatus(root) {
|
|
36406
|
-
const settings = parseJson(await readText(
|
|
36407
|
-
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))) ?? {};
|
|
36408
36711
|
const records = Array.isArray(hist.records) ? hist.records : [];
|
|
36409
36712
|
const last = records[records.length - 1];
|
|
36410
36713
|
const ticks = hist.ticks ?? {};
|
|
@@ -36421,9 +36724,9 @@ async function getAutonomousStatus(root) {
|
|
|
36421
36724
|
}
|
|
36422
36725
|
function isAutonomousRunning(root) {
|
|
36423
36726
|
try {
|
|
36424
|
-
const s = JSON.parse(readFileSync13(
|
|
36727
|
+
const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
|
|
36425
36728
|
if (s.paused === true) return false;
|
|
36426
|
-
const h = JSON.parse(readFileSync13(
|
|
36729
|
+
const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
|
|
36427
36730
|
const ts = h.records?.[h.records.length - 1]?.ts;
|
|
36428
36731
|
if (!ts) return false;
|
|
36429
36732
|
const t2 = Date.parse(ts.replace(" ", "T"));
|
|
@@ -36434,10 +36737,10 @@ function isAutonomousRunning(root) {
|
|
|
36434
36737
|
}
|
|
36435
36738
|
async function readAutonomous(root) {
|
|
36436
36739
|
const [settings, approvals, status, ...books] = await Promise.all([
|
|
36437
|
-
readText(
|
|
36438
|
-
readText(
|
|
36740
|
+
readText(join15(root, SETTINGS_REL)),
|
|
36741
|
+
readText(join15(root, APPROVALS_REL), "{}"),
|
|
36439
36742
|
getAutonomousStatus(root),
|
|
36440
|
-
...Object.values(BOOK_FILES).map((f) => readText(
|
|
36743
|
+
...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
|
|
36441
36744
|
]);
|
|
36442
36745
|
const keys = Object.keys(BOOK_FILES);
|
|
36443
36746
|
const bookMap = {};
|
|
@@ -36446,7 +36749,7 @@ async function readAutonomous(root) {
|
|
|
36446
36749
|
}
|
|
36447
36750
|
async function readAutonomousLogs(root, date) {
|
|
36448
36751
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
36449
|
-
const text = await readText(
|
|
36752
|
+
const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
36450
36753
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
36451
36754
|
return { date: day, lines };
|
|
36452
36755
|
}
|
|
@@ -36470,22 +36773,22 @@ async function writeAutonomous(root, req, by) {
|
|
|
36470
36773
|
switch (req.kind) {
|
|
36471
36774
|
case "settings": {
|
|
36472
36775
|
if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
|
|
36473
|
-
await
|
|
36776
|
+
await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
|
|
36474
36777
|
break;
|
|
36475
36778
|
}
|
|
36476
36779
|
case "approvals": {
|
|
36477
|
-
const map = parseJson(await readText(
|
|
36780
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
36478
36781
|
if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36479
36782
|
else delete map[req.taskId];
|
|
36480
|
-
await
|
|
36783
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
36481
36784
|
break;
|
|
36482
36785
|
}
|
|
36483
36786
|
case "userTodo": {
|
|
36484
|
-
const cur = await readText(
|
|
36787
|
+
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
36485
36788
|
const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
36486
36789
|
const row = `| | ${tableCell(req.content)} | ${trace} |
|
|
36487
36790
|
`;
|
|
36488
|
-
await
|
|
36791
|
+
await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
36489
36792
|
break;
|
|
36490
36793
|
}
|
|
36491
36794
|
case "cron": {
|
|
@@ -36501,7 +36804,7 @@ async function writeAutonomous(root, req, by) {
|
|
|
36501
36804
|
}
|
|
36502
36805
|
|
|
36503
36806
|
// src/core/network-probe.ts
|
|
36504
|
-
import { existsSync as
|
|
36807
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
36505
36808
|
import { connect } from "net";
|
|
36506
36809
|
var CANARY_TARGETS = [
|
|
36507
36810
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -36510,7 +36813,7 @@ var CANARY_TARGETS = [
|
|
|
36510
36813
|
var PROBE_TIMEOUT_MS = 2500;
|
|
36511
36814
|
function detectContainerized() {
|
|
36512
36815
|
try {
|
|
36513
|
-
if (
|
|
36816
|
+
if (existsSync11("/.dockerenv")) return true;
|
|
36514
36817
|
const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
|
|
36515
36818
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
36516
36819
|
} catch {
|
|
@@ -36580,7 +36883,7 @@ async function probeNetwork() {
|
|
|
36580
36883
|
// src/core/git-auth.ts
|
|
36581
36884
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
36582
36885
|
import { homedir as homedir4 } from "os";
|
|
36583
|
-
import { join as
|
|
36886
|
+
import { join as join16 } from "path";
|
|
36584
36887
|
function tokenTarget(method) {
|
|
36585
36888
|
if (method === "gitlab-group-token") {
|
|
36586
36889
|
return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
|
|
@@ -36598,7 +36901,7 @@ function applyGitAuth(method) {
|
|
|
36598
36901
|
try {
|
|
36599
36902
|
const line = `https://${target.user}:${token}@${target.host}
|
|
36600
36903
|
`;
|
|
36601
|
-
writeFileSync8(
|
|
36904
|
+
writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
|
|
36602
36905
|
return true;
|
|
36603
36906
|
} catch {
|
|
36604
36907
|
return false;
|
|
@@ -36783,7 +37086,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
36783
37086
|
const apiErr = parser.apiError();
|
|
36784
37087
|
if (finalExit === 0 && !apiErr.isError) {
|
|
36785
37088
|
const usage = parser.usage();
|
|
36786
|
-
if (usage.tokens === 0) usage.tokens =
|
|
37089
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
|
|
36787
37090
|
reportToolResult(attempt.cmd, true);
|
|
36788
37091
|
return {
|
|
36789
37092
|
exitCode: 0,
|
|
@@ -36835,8 +37138,8 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
36835
37138
|
}
|
|
36836
37139
|
|
|
36837
37140
|
// src/ui/slash-commands.ts
|
|
36838
|
-
import { existsSync as
|
|
36839
|
-
import { join as
|
|
37141
|
+
import { existsSync as existsSync12 } from "fs";
|
|
37142
|
+
import { join as join17 } from "path";
|
|
36840
37143
|
var CONFIG_KEYS = [
|
|
36841
37144
|
"aiCli",
|
|
36842
37145
|
"claudeHome",
|
|
@@ -36848,7 +37151,7 @@ var CONFIG_KEYS = [
|
|
|
36848
37151
|
"aiEnv"
|
|
36849
37152
|
];
|
|
36850
37153
|
function configPath(profileDir2) {
|
|
36851
|
-
return
|
|
37154
|
+
return join17(profileDir2, "config.json");
|
|
36852
37155
|
}
|
|
36853
37156
|
function parseConfigAssignment(key, valueTokens) {
|
|
36854
37157
|
const value = valueTokens.join(" ").trim();
|
|
@@ -36898,7 +37201,7 @@ function runConfig(ctx) {
|
|
|
36898
37201
|
ctx.print(t("slash.config.wroteDefault", { verb, path }));
|
|
36899
37202
|
ctx.print(t("slash.config.addClaudeHint"));
|
|
36900
37203
|
};
|
|
36901
|
-
if (
|
|
37204
|
+
if (existsSync12(path)) {
|
|
36902
37205
|
ctx.confirm(
|
|
36903
37206
|
t("slash.config.replaceConfirm", { path }),
|
|
36904
37207
|
() => writeDefaults(t("slash.config.verbReplaced"))
|
|
@@ -36983,7 +37286,7 @@ function runOutput(ctx) {
|
|
|
36983
37286
|
}
|
|
36984
37287
|
function runLogs(ctx) {
|
|
36985
37288
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
36986
|
-
const dir2 =
|
|
37289
|
+
const dir2 = join17(ctx.info.profileDir, "logs");
|
|
36987
37290
|
const lines = readRecentLogLines(dir2, n);
|
|
36988
37291
|
if (lines.length === 0) {
|
|
36989
37292
|
ctx.print(t("slash.logs.none"));
|
|
@@ -37298,13 +37601,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
|
|
|
37298
37601
|
}
|
|
37299
37602
|
|
|
37300
37603
|
// src/core/git-commit-identity.ts
|
|
37301
|
-
import { execFile as
|
|
37302
|
-
import { promisify as
|
|
37303
|
-
var
|
|
37604
|
+
import { execFile as execFile5 } from "child_process";
|
|
37605
|
+
import { promisify as promisify5 } from "util";
|
|
37606
|
+
var run5 = promisify5(execFile5);
|
|
37304
37607
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
37305
37608
|
if (!machineUsername) return;
|
|
37306
37609
|
try {
|
|
37307
|
-
await
|
|
37610
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
37308
37611
|
} catch {
|
|
37309
37612
|
}
|
|
37310
37613
|
}
|
|
@@ -37403,7 +37706,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
37403
37706
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
37404
37707
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
37405
37708
|
if (perPromptLimit > 0) {
|
|
37406
|
-
const estimated =
|
|
37709
|
+
const estimated = estimateTokens2(prompt);
|
|
37407
37710
|
if (estimated > perPromptLimit) {
|
|
37408
37711
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
37409
37712
|
return;
|
|
@@ -37821,11 +38124,11 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
37821
38124
|
|
|
37822
38125
|
// src/core/fs-browse.ts
|
|
37823
38126
|
import { readdir as readdir2 } from "fs/promises";
|
|
37824
|
-
import { resolve, sep as sep2 } from "path";
|
|
38127
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
37825
38128
|
async function listDir(path, root) {
|
|
37826
38129
|
if (!root) return { path: "", entries: [] };
|
|
37827
|
-
const base =
|
|
37828
|
-
const requested =
|
|
38130
|
+
const base = resolve2(root);
|
|
38131
|
+
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
37829
38132
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
37830
38133
|
try {
|
|
37831
38134
|
const dirents = await readdir2(target, { withFileTypes: true });
|
|
@@ -37844,12 +38147,12 @@ async function listDir(path, root) {
|
|
|
37844
38147
|
|
|
37845
38148
|
// src/core/fs-read.ts
|
|
37846
38149
|
import { readFile as readFile3 } from "fs/promises";
|
|
37847
|
-
import { resolve as
|
|
38150
|
+
import { resolve as resolve3, sep as sep3 } from "path";
|
|
37848
38151
|
var MAX_BYTES = 256 * 1024;
|
|
37849
38152
|
function resolveInRoot(root, relPath) {
|
|
37850
38153
|
if (!root) return null;
|
|
37851
|
-
const base =
|
|
37852
|
-
const target =
|
|
38154
|
+
const base = resolve3(root);
|
|
38155
|
+
const target = resolve3(base, relPath || ".");
|
|
37853
38156
|
return target === base || target.startsWith(base + sep3) ? target : null;
|
|
37854
38157
|
}
|
|
37855
38158
|
async function readWorkerFile(root, path) {
|
|
@@ -37868,13 +38171,13 @@ async function readWorkerFile(root, path) {
|
|
|
37868
38171
|
}
|
|
37869
38172
|
|
|
37870
38173
|
// src/core/fs-write.ts
|
|
37871
|
-
import { mkdir as
|
|
37872
|
-
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";
|
|
37873
38176
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
37874
38177
|
function resolveInRoot2(root, relPath) {
|
|
37875
38178
|
if (!root) return null;
|
|
37876
|
-
const base =
|
|
37877
|
-
const target =
|
|
38179
|
+
const base = resolve4(root);
|
|
38180
|
+
const target = resolve4(base, relPath || ".");
|
|
37878
38181
|
return target === base || target.startsWith(base + sep4) ? target : null;
|
|
37879
38182
|
}
|
|
37880
38183
|
async function writeWorkerFile(root, relPath, content) {
|
|
@@ -37887,8 +38190,8 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37887
38190
|
return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
|
|
37888
38191
|
}
|
|
37889
38192
|
try {
|
|
37890
|
-
await
|
|
37891
|
-
await
|
|
38193
|
+
await mkdir3(dirname4(target), { recursive: true });
|
|
38194
|
+
await writeFile3(target, content, "utf8");
|
|
37892
38195
|
return { ok: true, path: target, bytes };
|
|
37893
38196
|
} catch (err) {
|
|
37894
38197
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -37896,12 +38199,12 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
37896
38199
|
}
|
|
37897
38200
|
|
|
37898
38201
|
// src/core/fs-mutate.ts
|
|
37899
|
-
import { mkdir as
|
|
37900
|
-
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";
|
|
37901
38204
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
37902
38205
|
function resolveInRoot3(root, rel) {
|
|
37903
|
-
const base =
|
|
37904
|
-
const target =
|
|
38206
|
+
const base = resolve5(root);
|
|
38207
|
+
const target = resolve5(base, rel || ".");
|
|
37905
38208
|
return target === base || target.startsWith(base + sep5) ? target : null;
|
|
37906
38209
|
}
|
|
37907
38210
|
async function isDir(p) {
|
|
@@ -37926,38 +38229,38 @@ async function mutateFs(root, req) {
|
|
|
37926
38229
|
switch (req.op) {
|
|
37927
38230
|
case "mkdir": {
|
|
37928
38231
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37929
|
-
if (!target || target ===
|
|
37930
|
-
await
|
|
38232
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
38233
|
+
await mkdir4(target, { recursive: true });
|
|
37931
38234
|
return { ok: true, path: target };
|
|
37932
38235
|
}
|
|
37933
38236
|
case "create": {
|
|
37934
38237
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37935
|
-
if (!target || target ===
|
|
38238
|
+
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
37936
38239
|
if (await exists(target)) return { ok: false, path: target, error: "already exists" };
|
|
37937
38240
|
const content = req.content ?? "";
|
|
37938
38241
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
|
|
37939
38242
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
37940
38243
|
}
|
|
37941
|
-
await
|
|
37942
|
-
await
|
|
38244
|
+
await mkdir4(dirname5(target), { recursive: true });
|
|
38245
|
+
await writeFile4(target, content, "utf8");
|
|
37943
38246
|
return { ok: true, path: target };
|
|
37944
38247
|
}
|
|
37945
38248
|
case "move": {
|
|
37946
38249
|
const from = resolveInRoot3(root, req.from ?? "");
|
|
37947
38250
|
let to = resolveInRoot3(root, req.to ?? "");
|
|
37948
38251
|
if (!from || !to) return escaped(req.from ?? req.to ?? "");
|
|
37949
|
-
if (from ===
|
|
37950
|
-
if (await isDir(to)) to =
|
|
37951
|
-
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 ?? "");
|
|
37952
38255
|
if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
|
|
37953
|
-
await
|
|
38256
|
+
await mkdir4(dirname5(to), { recursive: true });
|
|
37954
38257
|
await rename(from, to);
|
|
37955
38258
|
return { ok: true, path: to };
|
|
37956
38259
|
}
|
|
37957
38260
|
case "delete": {
|
|
37958
38261
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
37959
38262
|
if (!target) return escaped(req.path ?? "");
|
|
37960
|
-
if (target ===
|
|
38263
|
+
if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
37961
38264
|
await rm(target, { recursive: true, force: true });
|
|
37962
38265
|
return { ok: true, path: target };
|
|
37963
38266
|
}
|
|
@@ -37970,12 +38273,12 @@ async function mutateFs(root, req) {
|
|
|
37970
38273
|
}
|
|
37971
38274
|
|
|
37972
38275
|
// src/core/fs-transfer.ts
|
|
37973
|
-
import { mkdir as
|
|
37974
|
-
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";
|
|
37975
38278
|
function resolveInRoot4(root, relPath) {
|
|
37976
38279
|
if (!root) return null;
|
|
37977
|
-
const base =
|
|
37978
|
-
const target =
|
|
38280
|
+
const base = resolve6(root);
|
|
38281
|
+
const target = resolve6(base, relPath || ".");
|
|
37979
38282
|
return target === base || target.startsWith(base + sep6) ? target : null;
|
|
37980
38283
|
}
|
|
37981
38284
|
var MIME_BY_EXT = {
|
|
@@ -38014,8 +38317,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
38014
38317
|
return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
38015
38318
|
}
|
|
38016
38319
|
try {
|
|
38017
|
-
await
|
|
38018
|
-
await
|
|
38320
|
+
await mkdir5(dirname6(target), { recursive: true });
|
|
38321
|
+
await writeFile5(target, buf);
|
|
38019
38322
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
38020
38323
|
} catch (err) {
|
|
38021
38324
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -38090,48 +38393,73 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
38090
38393
|
|
|
38091
38394
|
// src/core/git-diff.ts
|
|
38092
38395
|
import { readFile as readFile5 } from "fs/promises";
|
|
38093
|
-
import { execFile as
|
|
38094
|
-
import { basename as basename5, dirname as
|
|
38095
|
-
import { promisify as
|
|
38096
|
-
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);
|
|
38097
38400
|
function resolveInRoot5(root, relPath) {
|
|
38098
38401
|
if (!root) return null;
|
|
38099
|
-
const base =
|
|
38100
|
-
const target =
|
|
38402
|
+
const base = resolve7(root);
|
|
38403
|
+
const target = resolve7(base, relPath || ".");
|
|
38101
38404
|
return target === base || target.startsWith(base + sep7) ? target : null;
|
|
38102
38405
|
}
|
|
38406
|
+
function looksBinary(buf) {
|
|
38407
|
+
return buf.subarray(0, 8e3).includes(0);
|
|
38408
|
+
}
|
|
38409
|
+
function cappedBase64(buf) {
|
|
38410
|
+
if (!buf || buf.byteLength > FS_TRANSFER_MAX_BYTES) return "";
|
|
38411
|
+
return buf.toString("base64");
|
|
38412
|
+
}
|
|
38103
38413
|
async function gitDiff(root, path) {
|
|
38104
38414
|
const target = resolveInRoot5(root, path);
|
|
38105
38415
|
if (!target) {
|
|
38106
|
-
return { path, oldContent: "", newContent: "" };
|
|
38416
|
+
return { path, oldContent: "", newContent: "", isBinary: false };
|
|
38107
38417
|
}
|
|
38108
|
-
const dir2 =
|
|
38418
|
+
const dir2 = dirname7(target);
|
|
38109
38419
|
const base = basename5(target);
|
|
38110
|
-
let
|
|
38420
|
+
let newBuf = null;
|
|
38111
38421
|
try {
|
|
38112
|
-
|
|
38422
|
+
newBuf = await readFile5(target);
|
|
38113
38423
|
} catch {
|
|
38114
38424
|
}
|
|
38115
|
-
let
|
|
38425
|
+
let oldBuf = null;
|
|
38116
38426
|
try {
|
|
38117
|
-
const { stdout } = await
|
|
38427
|
+
const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
|
|
38118
38428
|
cwd: dir2,
|
|
38119
38429
|
timeout: 15e3,
|
|
38120
|
-
maxBuffer:
|
|
38430
|
+
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
38431
|
+
encoding: "buffer"
|
|
38121
38432
|
});
|
|
38122
|
-
|
|
38433
|
+
oldBuf = stdout;
|
|
38123
38434
|
} catch {
|
|
38124
38435
|
}
|
|
38125
|
-
|
|
38436
|
+
const sample = newBuf ?? oldBuf;
|
|
38437
|
+
if (sample && looksBinary(sample)) {
|
|
38438
|
+
return {
|
|
38439
|
+
path: target,
|
|
38440
|
+
oldContent: "",
|
|
38441
|
+
newContent: "",
|
|
38442
|
+
isBinary: true,
|
|
38443
|
+
contentType: guessMime(target),
|
|
38444
|
+
oldContentBase64: cappedBase64(oldBuf),
|
|
38445
|
+
newContentBase64: cappedBase64(newBuf)
|
|
38446
|
+
};
|
|
38447
|
+
}
|
|
38448
|
+
return {
|
|
38449
|
+
path: target,
|
|
38450
|
+
oldContent: oldBuf ? oldBuf.toString("utf8") : "",
|
|
38451
|
+
newContent: newBuf ? newBuf.toString("utf8") : "",
|
|
38452
|
+
isBinary: false
|
|
38453
|
+
};
|
|
38126
38454
|
}
|
|
38127
38455
|
|
|
38128
38456
|
// src/core/git-env.ts
|
|
38129
|
-
import { execFile as
|
|
38130
|
-
import { promisify as
|
|
38131
|
-
var
|
|
38457
|
+
import { execFile as execFile7 } from "child_process";
|
|
38458
|
+
import { promisify as promisify7 } from "util";
|
|
38459
|
+
var run7 = promisify7(execFile7);
|
|
38132
38460
|
async function hasBinary(bin, args = ["--version"]) {
|
|
38133
38461
|
try {
|
|
38134
|
-
await
|
|
38462
|
+
await run7(bin, args, { timeout: 5e3 });
|
|
38135
38463
|
return true;
|
|
38136
38464
|
} catch {
|
|
38137
38465
|
return false;
|
|
@@ -38144,7 +38472,7 @@ async function checkGitEnv(provider) {
|
|
|
38144
38472
|
let account = null;
|
|
38145
38473
|
if (installed) {
|
|
38146
38474
|
try {
|
|
38147
|
-
const { stdout, stderr } = await
|
|
38475
|
+
const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
|
|
38148
38476
|
const out = `${stdout}
|
|
38149
38477
|
${stderr}`;
|
|
38150
38478
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -38157,14 +38485,14 @@ ${stderr}`;
|
|
|
38157
38485
|
}
|
|
38158
38486
|
|
|
38159
38487
|
// src/core/git-ssh-key.ts
|
|
38160
|
-
import { execFile as
|
|
38161
|
-
import { readFile as readFile6, rm as rm2, mkdir as
|
|
38162
|
-
import { join as
|
|
38163
|
-
import { promisify as
|
|
38164
|
-
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);
|
|
38165
38493
|
function keyPaths(profileDir2) {
|
|
38166
|
-
const dir2 =
|
|
38167
|
-
const key =
|
|
38494
|
+
const dir2 = join18(profileDir2, ".ssh");
|
|
38495
|
+
const key = join18(dir2, "id_4pm");
|
|
38168
38496
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
38169
38497
|
}
|
|
38170
38498
|
async function readPublicKey(pub) {
|
|
@@ -38176,14 +38504,14 @@ async function readPublicKey(pub) {
|
|
|
38176
38504
|
}
|
|
38177
38505
|
async function fingerprintOf(pub) {
|
|
38178
38506
|
try {
|
|
38179
|
-
const { stdout } = await
|
|
38507
|
+
const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
38180
38508
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
38181
38509
|
} catch {
|
|
38182
38510
|
return null;
|
|
38183
38511
|
}
|
|
38184
38512
|
}
|
|
38185
38513
|
async function configureGit(key) {
|
|
38186
|
-
await
|
|
38514
|
+
await run8(
|
|
38187
38515
|
"git",
|
|
38188
38516
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
38189
38517
|
{ timeout: 5e3 }
|
|
@@ -38191,10 +38519,10 @@ async function configureGit(key) {
|
|
|
38191
38519
|
}
|
|
38192
38520
|
async function generate(profileDir2) {
|
|
38193
38521
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
38194
|
-
await
|
|
38522
|
+
await mkdir6(dir2, { recursive: true });
|
|
38195
38523
|
await rm2(key, { force: true });
|
|
38196
38524
|
await rm2(pub, { force: true });
|
|
38197
|
-
await
|
|
38525
|
+
await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
38198
38526
|
timeout: 15e3
|
|
38199
38527
|
});
|
|
38200
38528
|
await configureGit(key);
|
|
@@ -38204,7 +38532,7 @@ async function remove(profileDir2) {
|
|
|
38204
38532
|
const { key, pub } = keyPaths(profileDir2);
|
|
38205
38533
|
await rm2(key, { force: true });
|
|
38206
38534
|
await rm2(pub, { force: true });
|
|
38207
|
-
await
|
|
38535
|
+
await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
38208
38536
|
() => void 0
|
|
38209
38537
|
);
|
|
38210
38538
|
return { publicKey: null, fingerprint: null };
|
|
@@ -38226,21 +38554,21 @@ async function manageSshKey(op, profileDir2) {
|
|
|
38226
38554
|
|
|
38227
38555
|
// src/core/git-history.ts
|
|
38228
38556
|
import { readdir as readdir3 } from "fs/promises";
|
|
38229
|
-
import { execFile as
|
|
38230
|
-
import { resolve as
|
|
38231
|
-
import { promisify as
|
|
38232
|
-
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);
|
|
38233
38561
|
var FS = "";
|
|
38234
38562
|
var RS = "";
|
|
38235
38563
|
function resolveRepo(root, repo) {
|
|
38236
38564
|
if (!root) return null;
|
|
38237
|
-
const base =
|
|
38238
|
-
const requested =
|
|
38565
|
+
const base = resolve8(root);
|
|
38566
|
+
const requested = resolve8(base, repo && repo.trim() ? repo : ".");
|
|
38239
38567
|
return requested === base || requested.startsWith(base + sep8) ? requested : base;
|
|
38240
38568
|
}
|
|
38241
38569
|
async function git(cwd2, args) {
|
|
38242
38570
|
try {
|
|
38243
|
-
const { stdout } = await
|
|
38571
|
+
const { stdout } = await run9("git", args, {
|
|
38244
38572
|
cwd: cwd2,
|
|
38245
38573
|
timeout: 15e3,
|
|
38246
38574
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -38252,7 +38580,7 @@ async function git(cwd2, args) {
|
|
|
38252
38580
|
}
|
|
38253
38581
|
async function gitRepos(root) {
|
|
38254
38582
|
if (!root) return { repos: [] };
|
|
38255
|
-
const base =
|
|
38583
|
+
const base = resolve8(root);
|
|
38256
38584
|
const found = /* @__PURE__ */ new Set();
|
|
38257
38585
|
const repos = [];
|
|
38258
38586
|
const add = async (dir2) => {
|
|
@@ -38269,13 +38597,13 @@ async function gitRepos(root) {
|
|
|
38269
38597
|
const lvl1 = await readdir3(base, { withFileTypes: true });
|
|
38270
38598
|
for (const d of lvl1) {
|
|
38271
38599
|
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
38272
|
-
const p1 =
|
|
38600
|
+
const p1 = resolve8(base, d.name);
|
|
38273
38601
|
await add(p1);
|
|
38274
38602
|
try {
|
|
38275
38603
|
const lvl2 = await readdir3(p1, { withFileTypes: true });
|
|
38276
38604
|
for (const d2 of lvl2) {
|
|
38277
38605
|
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
38278
|
-
await add(
|
|
38606
|
+
await add(resolve8(p1, d2.name));
|
|
38279
38607
|
}
|
|
38280
38608
|
} catch {
|
|
38281
38609
|
}
|
|
@@ -38393,79 +38721,7 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
38393
38721
|
}
|
|
38394
38722
|
|
|
38395
38723
|
// src/core/ws-client/handlers/misc.ts
|
|
38396
|
-
import { join as
|
|
38397
|
-
|
|
38398
|
-
// src/core/ai-assist.ts
|
|
38399
|
-
import { execFile as execFile8 } from "child_process";
|
|
38400
|
-
import { promisify as promisify8 } from "util";
|
|
38401
|
-
var run8 = promisify8(execFile8);
|
|
38402
|
-
function aiCli() {
|
|
38403
|
-
return process.env.AI_CLI || "claude";
|
|
38404
|
-
}
|
|
38405
|
-
function aiArgs(cli, prompt) {
|
|
38406
|
-
if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
|
|
38407
|
-
if (cli === "codex") return ["exec", "--json", prompt];
|
|
38408
|
-
return ["-p", prompt];
|
|
38409
|
-
}
|
|
38410
|
-
function estimateTokens2(prompt, output) {
|
|
38411
|
-
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
38412
|
-
}
|
|
38413
|
-
function parseAi(cli, stdout, prompt) {
|
|
38414
|
-
if (cli === "claude") {
|
|
38415
|
-
try {
|
|
38416
|
-
const j = JSON.parse(stdout);
|
|
38417
|
-
const text2 = (j.result ?? "").trim();
|
|
38418
|
-
const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
|
|
38419
|
-
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
|
|
38420
|
-
} catch {
|
|
38421
|
-
}
|
|
38422
|
-
}
|
|
38423
|
-
if (cli === "codex") {
|
|
38424
|
-
try {
|
|
38425
|
-
let text2 = "";
|
|
38426
|
-
let tokens = 0;
|
|
38427
|
-
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
38428
|
-
const obj = JSON.parse(line);
|
|
38429
|
-
if (obj.text) text2 = obj.text;
|
|
38430
|
-
else if (obj.message) text2 = obj.message;
|
|
38431
|
-
if (obj.usage) {
|
|
38432
|
-
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
38433
|
-
}
|
|
38434
|
-
}
|
|
38435
|
-
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
|
|
38436
|
-
} catch {
|
|
38437
|
-
}
|
|
38438
|
-
}
|
|
38439
|
-
const text = stdout.trim();
|
|
38440
|
-
return { text, tokens: estimateTokens2(prompt, text) };
|
|
38441
|
-
}
|
|
38442
|
-
async function runAi(prompt) {
|
|
38443
|
-
const cli = aiCli();
|
|
38444
|
-
const { stdout } = await run8(cli, aiArgs(cli, prompt), {
|
|
38445
|
-
timeout: 12e4,
|
|
38446
|
-
maxBuffer: 8 * 1024 * 1024
|
|
38447
|
-
});
|
|
38448
|
-
return parseAi(cli, stdout, prompt);
|
|
38449
|
-
}
|
|
38450
|
-
async function aiGenerate(prompt) {
|
|
38451
|
-
const { text } = await runAi(prompt);
|
|
38452
|
-
return text;
|
|
38453
|
-
}
|
|
38454
|
-
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
38455
|
-
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:
|
|
38456
|
-
${input}`;
|
|
38457
|
-
const { text, tokens } = await runAi(prompt);
|
|
38458
|
-
const start = text.indexOf("{");
|
|
38459
|
-
const end = text.lastIndexOf("}");
|
|
38460
|
-
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
38461
|
-
try {
|
|
38462
|
-
const j = JSON.parse(text.slice(start, end + 1));
|
|
38463
|
-
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
38464
|
-
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
38465
|
-
} catch {
|
|
38466
|
-
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
38467
|
-
}
|
|
38468
|
-
}
|
|
38724
|
+
import { join as join19 } from "path";
|
|
38469
38725
|
|
|
38470
38726
|
// src/core/outbound-review.ts
|
|
38471
38727
|
function compilePatterns(sources, fallback) {
|
|
@@ -38541,9 +38797,13 @@ async function evaluateReview(payload) {
|
|
|
38541
38797
|
// src/core/ws-client/handlers/misc.ts
|
|
38542
38798
|
function handleMiscChannels(ctx, message, payload) {
|
|
38543
38799
|
switch (message.channel) {
|
|
38800
|
+
case WsChannels.CLI_UPDATE: {
|
|
38801
|
+
ctx.updateCliNow();
|
|
38802
|
+
return true;
|
|
38803
|
+
}
|
|
38544
38804
|
case WsChannels.LOG_READ: {
|
|
38545
38805
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
38546
|
-
const lines = readRecentLogLines(
|
|
38806
|
+
const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
|
|
38547
38807
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
38548
38808
|
return true;
|
|
38549
38809
|
}
|
|
@@ -38574,168 +38834,29 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
38574
38834
|
}
|
|
38575
38835
|
|
|
38576
38836
|
// src/core/ws-client/handlers/project.ts
|
|
38577
|
-
import { mkdirSync as mkdirSync6, rmSync as rmSync6 } from "fs";
|
|
38837
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
|
|
38578
38838
|
import { join as join20 } from "path";
|
|
38579
|
-
|
|
38580
|
-
// src/core/scaffold.ts
|
|
38581
|
-
import { cp, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
|
|
38582
|
-
import { existsSync as existsSync12 } from "fs";
|
|
38583
|
-
import { execFile as execFile9 } from "child_process";
|
|
38584
|
-
import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
|
|
38585
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
38586
|
-
import { promisify as promisify9 } from "util";
|
|
38587
|
-
var run9 = promisify9(execFile9);
|
|
38588
|
-
function reposOf(spec) {
|
|
38589
|
-
const repos = spec?.repos;
|
|
38590
|
-
return Array.isArray(repos) ? repos : [];
|
|
38591
|
-
}
|
|
38592
|
-
function subDirName(repo) {
|
|
38593
|
-
const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
|
|
38594
|
-
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
38595
|
-
}
|
|
38596
|
-
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
38597
|
-
for (const repo of repos) {
|
|
38598
|
-
if (repo.primary) {
|
|
38599
|
-
if (opts.clonePrimary && repo.url) {
|
|
38600
|
-
emit("git", `Cloning ${repo.url}\u2026`);
|
|
38601
|
-
await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
38602
|
-
continue;
|
|
38603
|
-
}
|
|
38604
|
-
emit("git", "Initializing the primary repo\u2026");
|
|
38605
|
-
await run9("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
38606
|
-
if (repo.url) {
|
|
38607
|
-
await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
38608
|
-
() => void 0
|
|
38609
|
-
);
|
|
38610
|
-
}
|
|
38611
|
-
continue;
|
|
38612
|
-
}
|
|
38613
|
-
const dir2 = join19(target, subDirName(repo));
|
|
38614
|
-
if (repo.url) {
|
|
38615
|
-
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
38616
|
-
await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
38617
|
-
} else {
|
|
38618
|
-
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
38619
|
-
await mkdir6(dir2, { recursive: true });
|
|
38620
|
-
await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
38621
|
-
}
|
|
38622
|
-
}
|
|
38623
|
-
}
|
|
38624
|
-
function sampleDir() {
|
|
38625
|
-
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
38626
|
-
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
38627
|
-
const bundled = resolve8(here, "project-sample");
|
|
38628
|
-
for (const candidate of [bundled, resolve8(here, "../project-sample"), resolve8(here, "../../project-sample")]) {
|
|
38629
|
-
if (existsSync12(candidate)) return candidate;
|
|
38630
|
-
}
|
|
38631
|
-
return bundled;
|
|
38632
|
-
}
|
|
38633
|
-
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
38634
|
-
let lastStep = "start";
|
|
38635
|
-
const emit = (step, message) => {
|
|
38636
|
-
lastStep = step;
|
|
38637
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38638
|
-
};
|
|
38639
|
-
try {
|
|
38640
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38641
|
-
const target = join19(root, payload.projectName);
|
|
38642
|
-
await mkdir6(target, { recursive: true });
|
|
38643
|
-
emit("copy", "Copying the sample template\u2026");
|
|
38644
|
-
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
38645
|
-
if (payload.spec) {
|
|
38646
|
-
emit("spec", "Writing project.spec.json\u2026");
|
|
38647
|
-
await writeFile5(
|
|
38648
|
-
join19(target, "project.spec.json"),
|
|
38649
|
-
JSON.stringify(payload.spec, null, 2),
|
|
38650
|
-
"utf8"
|
|
38651
|
-
);
|
|
38652
|
-
}
|
|
38653
|
-
const repos = reposOf(payload.spec);
|
|
38654
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
38655
|
-
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
38656
|
-
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
38657
|
-
await writeTemplateMarker(target);
|
|
38658
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
38659
|
-
return { ok: true, path: target };
|
|
38660
|
-
} catch (err) {
|
|
38661
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38662
|
-
}
|
|
38663
|
-
}
|
|
38664
|
-
async function writeTemplateMarker(target) {
|
|
38665
|
-
const dir2 = join19(target, ".4pm");
|
|
38666
|
-
await mkdir6(dir2, { recursive: true });
|
|
38667
|
-
const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
38668
|
-
await writeFile5(join19(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
38669
|
-
}
|
|
38670
|
-
async function aiInit(target, spec, emit) {
|
|
38671
|
-
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
38672
|
-
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
38673
|
-
if (subagents.length > 0) {
|
|
38674
|
-
await mkdir6(join19(target, ".claude", "agents"), { recursive: true });
|
|
38675
|
-
for (const sa of subagents) {
|
|
38676
|
-
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
38677
|
-
if (!name) continue;
|
|
38678
|
-
const body = `---
|
|
38679
|
-
name: ${name}
|
|
38680
|
-
---
|
|
38681
|
-
|
|
38682
|
-
${(sa.description || "").trim()}
|
|
38683
|
-
`;
|
|
38684
|
-
await writeFile5(join19(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
38685
|
-
}
|
|
38686
|
-
}
|
|
38687
|
-
const specJson = JSON.stringify(spec);
|
|
38688
|
-
await generateFile(
|
|
38689
|
-
join19(target, "README.md"),
|
|
38690
|
-
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
38691
|
-
${specJson}`
|
|
38692
|
-
);
|
|
38693
|
-
await generateFile(
|
|
38694
|
-
join19(target, "CLAUDE.md"),
|
|
38695
|
-
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
38696
|
-
${specJson}`
|
|
38697
|
-
);
|
|
38698
|
-
}
|
|
38699
|
-
async function generateFile(path, prompt) {
|
|
38700
|
-
try {
|
|
38701
|
-
const text = await aiGenerate(prompt);
|
|
38702
|
-
if (text.trim()) await writeFile5(path, text.trim() + "\n", "utf8");
|
|
38703
|
-
} catch {
|
|
38704
|
-
}
|
|
38705
|
-
}
|
|
38706
|
-
async function addProject(payload, profileDir2, onProgress) {
|
|
38707
|
-
let lastStep = "start";
|
|
38708
|
-
const emit = (step, message) => {
|
|
38709
|
-
lastStep = step;
|
|
38710
|
-
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38711
|
-
};
|
|
38712
|
-
try {
|
|
38713
|
-
const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
|
|
38714
|
-
const target = join19(root, payload.projectName);
|
|
38715
|
-
await mkdir6(target, { recursive: true });
|
|
38716
|
-
const repos = payload.repos ?? [];
|
|
38717
|
-
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
|
|
38718
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
38719
|
-
return { ok: true, path: target };
|
|
38720
|
-
} catch (err) {
|
|
38721
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
38722
|
-
}
|
|
38723
|
-
}
|
|
38724
|
-
|
|
38725
|
-
// src/core/ws-client/handlers/project.ts
|
|
38726
38839
|
function handleProjectChannels(ctx, message, payload) {
|
|
38727
38840
|
switch (message.channel) {
|
|
38728
38841
|
case WsChannels.PHYSIC_SYNC: {
|
|
38729
38842
|
const sync = payload;
|
|
38730
|
-
|
|
38731
|
-
|
|
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);
|
|
38732
38848
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
38733
38849
|
}
|
|
38734
|
-
if (
|
|
38735
|
-
|
|
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
|
+
}
|
|
38736
38856
|
ctx.physicRoot = ctx.physicFolderPath(sync.newName);
|
|
38737
38857
|
ctx.bus.setProject(sync.newName);
|
|
38738
38858
|
ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
|
|
38859
|
+
void ctx.cloneServingRepos().catch(() => void 0);
|
|
38739
38860
|
return true;
|
|
38740
38861
|
}
|
|
38741
38862
|
case WsChannels.PHYSIC_DELETE: {
|
|
@@ -39523,7 +39644,7 @@ async function buildGraph(root, mode) {
|
|
|
39523
39644
|
|
|
39524
39645
|
// src/core/rag.ts
|
|
39525
39646
|
import { readFile as readFile12, writeFile as writeFile10, statfs as statfs2, mkdir as mkdir10 } from "fs/promises";
|
|
39526
|
-
import { existsSync as
|
|
39647
|
+
import { existsSync as existsSync14 } from "fs";
|
|
39527
39648
|
import { execFile as execFile10, spawn as spawn9 } from "child_process";
|
|
39528
39649
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
39529
39650
|
import { join as join26 } from "path";
|
|
@@ -39616,7 +39737,7 @@ async function ragStatus(root) {
|
|
|
39616
39737
|
const modelPresent = modelName.length > 0;
|
|
39617
39738
|
return {
|
|
39618
39739
|
installed: fastembed && sqliteVec && modelPresent,
|
|
39619
|
-
installing:
|
|
39740
|
+
installing: existsSync14(join26(root, RAG_REL, ".installing")),
|
|
39620
39741
|
python: { found: py.found, version: py.version },
|
|
39621
39742
|
deps: { pip, fastembed, sqliteVec },
|
|
39622
39743
|
model: { present: modelPresent, name: modelName },
|
|
@@ -39627,7 +39748,7 @@ async function ragStatus(root) {
|
|
|
39627
39748
|
};
|
|
39628
39749
|
}
|
|
39629
39750
|
async function readIndexMeta(root) {
|
|
39630
|
-
const indexing =
|
|
39751
|
+
const indexing = existsSync14(join26(root, RAG_REL, ".indexing"));
|
|
39631
39752
|
try {
|
|
39632
39753
|
const m = JSON.parse(await readFile12(join26(root, RAG_REL, ".index.json"), "utf8"));
|
|
39633
39754
|
return {
|
|
@@ -39702,8 +39823,8 @@ async function ensureEngine(root) {
|
|
|
39702
39823
|
async function ragReindex(root) {
|
|
39703
39824
|
const py = await detectPython();
|
|
39704
39825
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
39705
|
-
if (!
|
|
39706
|
-
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 };
|
|
39707
39828
|
await ensureEngine(root);
|
|
39708
39829
|
const script = `
|
|
39709
39830
|
set +e
|
|
@@ -39723,7 +39844,7 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
39723
39844
|
async function ragQuery(root, queryText, k = 8) {
|
|
39724
39845
|
const py = await detectPython();
|
|
39725
39846
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
39726
|
-
if (!
|
|
39847
|
+
if (!existsSync14(join26(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
39727
39848
|
await ensureEngine(root);
|
|
39728
39849
|
try {
|
|
39729
39850
|
const { stdout } = await run10(
|
|
@@ -39743,7 +39864,7 @@ async function ragInstall(root, model) {
|
|
|
39743
39864
|
}
|
|
39744
39865
|
const py = await detectPython();
|
|
39745
39866
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
39746
|
-
if (
|
|
39867
|
+
if (existsSync14(join26(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
39747
39868
|
try {
|
|
39748
39869
|
await mkdir10(join26(root, RAG_REL), { recursive: true });
|
|
39749
39870
|
} catch {
|
|
@@ -39772,7 +39893,7 @@ rm -f "${RAG_REL}/.installing"
|
|
|
39772
39893
|
}
|
|
39773
39894
|
|
|
39774
39895
|
// src/core/config-sync.ts
|
|
39775
|
-
import { existsSync as
|
|
39896
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
|
|
39776
39897
|
import { join as join27 } from "path";
|
|
39777
39898
|
var SERVER_MANAGED_KEYS = [
|
|
39778
39899
|
"physicPath",
|
|
@@ -39791,7 +39912,7 @@ var SERVER_MANAGED_KEYS = [
|
|
|
39791
39912
|
];
|
|
39792
39913
|
function readConfigText(profileDir2) {
|
|
39793
39914
|
const path = join27(profileDir2, "config.json");
|
|
39794
|
-
if (
|
|
39915
|
+
if (existsSync15(path)) {
|
|
39795
39916
|
try {
|
|
39796
39917
|
return readFileSync15(path, "utf8");
|
|
39797
39918
|
} catch {
|
|
@@ -40091,6 +40212,9 @@ var WsClient = class {
|
|
|
40091
40212
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
40092
40213
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
40093
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 = [];
|
|
40094
40218
|
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
40095
40219
|
* Push uses the account already logged in with gh/glab on the worker. */
|
|
40096
40220
|
machineUsername = "";
|
|
@@ -40208,9 +40332,24 @@ var WsClient = class {
|
|
|
40208
40332
|
setConsoleWatching: (on) => self.setConsoleWatching(on),
|
|
40209
40333
|
setMetricsWatching: (on) => self.setMetricsWatching(on),
|
|
40210
40334
|
ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
|
|
40211
|
-
physicFolderPath: (projectName) => self.physicFolderPath(projectName)
|
|
40335
|
+
physicFolderPath: (projectName) => self.physicFolderPath(projectName),
|
|
40336
|
+
cloneServingRepos: () => self.ensureServingReposCloned(),
|
|
40337
|
+
updateCliNow: () => self.updateScheduler.updateNow()
|
|
40212
40338
|
};
|
|
40213
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
|
+
}
|
|
40214
40353
|
/**
|
|
40215
40354
|
* Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
|
|
40216
40355
|
* DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
|
|
@@ -40394,6 +40533,8 @@ var WsClient = class {
|
|
|
40394
40533
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
40395
40534
|
applyGitAuth(token.gitAuth ?? null);
|
|
40396
40535
|
this.machineUsername = token.machineUsername ?? "";
|
|
40536
|
+
this.servingRepos = token.repos ?? [];
|
|
40537
|
+
void this.ensureServingReposCloned();
|
|
40397
40538
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
40398
40539
|
this.credential.scope = token.scope;
|
|
40399
40540
|
writeCredential(this.context.profileDir, this.credential);
|
|
@@ -40644,7 +40785,7 @@ var WsClient = class {
|
|
|
40644
40785
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
40645
40786
|
ensurePhysicFolderPath(folder) {
|
|
40646
40787
|
try {
|
|
40647
|
-
if (!
|
|
40788
|
+
if (!existsSync16(folder)) {
|
|
40648
40789
|
mkdirSync7(folder, { recursive: true });
|
|
40649
40790
|
this.bus.log(t("project.createdFolder", { folder }));
|
|
40650
40791
|
}
|
|
@@ -40674,7 +40815,7 @@ var WsClient = class {
|
|
|
40674
40815
|
projects: [],
|
|
40675
40816
|
version: CLI_VERSION,
|
|
40676
40817
|
physicPath,
|
|
40677
|
-
physicPathExists: physicPath ?
|
|
40818
|
+
physicPathExists: physicPath ? existsSync16(physicPath) : void 0,
|
|
40678
40819
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
40679
40820
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
|
|
40680
40821
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
@@ -43309,7 +43450,7 @@ var src_default = Yoga;
|
|
|
43309
43450
|
|
|
43310
43451
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
43311
43452
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
43312
|
-
var
|
|
43453
|
+
var import_constants11 = __toESM(require_constants2(), 1);
|
|
43313
43454
|
import process3 from "process";
|
|
43314
43455
|
|
|
43315
43456
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -44551,7 +44692,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
44551
44692
|
scheduleTimeout: setTimeout,
|
|
44552
44693
|
cancelTimeout: clearTimeout,
|
|
44553
44694
|
noTimeout: -1,
|
|
44554
|
-
getCurrentEventPriority: () =>
|
|
44695
|
+
getCurrentEventPriority: () => import_constants11.DefaultEventPriority,
|
|
44555
44696
|
beforeActiveInstanceBlur() {
|
|
44556
44697
|
},
|
|
44557
44698
|
afterActiveInstanceBlur() {
|
|
@@ -46993,7 +47134,7 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
46993
47134
|
var import_react26 = __toESM(require_react(), 1);
|
|
46994
47135
|
|
|
46995
47136
|
// src/core/input-history.ts
|
|
46996
|
-
import { existsSync as
|
|
47137
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
46997
47138
|
import { join as join31 } from "path";
|
|
46998
47139
|
var MAX_HISTORY = 200;
|
|
46999
47140
|
function historyPath(profileDir2) {
|
|
@@ -47001,7 +47142,7 @@ function historyPath(profileDir2) {
|
|
|
47001
47142
|
}
|
|
47002
47143
|
function loadInputHistory(profileDir2) {
|
|
47003
47144
|
const path = historyPath(profileDir2);
|
|
47004
|
-
if (!
|
|
47145
|
+
if (!existsSync18(path)) return [];
|
|
47005
47146
|
try {
|
|
47006
47147
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
47007
47148
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|