@4pm/cli 1.19.7 → 1.20.0-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +386 -120
- package/dist/project-sample/.claude/settings.json +0 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29519,8 +29519,8 @@ async function runLink(serverUrl, explicitProfile, token = null) {
|
|
|
29519
29519
|
}
|
|
29520
29520
|
|
|
29521
29521
|
// src/commands/start.ts
|
|
29522
|
-
import { spawn as
|
|
29523
|
-
import { join as
|
|
29522
|
+
import { spawn as spawn11 } from "child_process";
|
|
29523
|
+
import { join as join37 } from "path";
|
|
29524
29524
|
|
|
29525
29525
|
// src/core/update.ts
|
|
29526
29526
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -32732,9 +32732,9 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32732
32732
|
|
|
32733
32733
|
// src/core/ws-client.ts
|
|
32734
32734
|
init_wrapper();
|
|
32735
|
-
import { randomUUID as
|
|
32736
|
-
import { basename as basename7, join as
|
|
32737
|
-
import { existsSync as
|
|
32735
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
32736
|
+
import { basename as basename7, join as join32 } from "path";
|
|
32737
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
|
|
32738
32738
|
|
|
32739
32739
|
// vendor/@4pm/utils/helpers.ts
|
|
32740
32740
|
function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
@@ -37191,7 +37191,6 @@ var toolRuleSchema = external_exports.string().min(1).max(500);
|
|
|
37191
37191
|
var agentToolsPermissionsSchema = external_exports.object({
|
|
37192
37192
|
defaultMode: external_exports.enum(["default", "acceptEdits", "plan", "bypassPermissions"]),
|
|
37193
37193
|
allow: external_exports.array(toolRuleSchema).max(500),
|
|
37194
|
-
ask: external_exports.array(toolRuleSchema).max(500),
|
|
37195
37194
|
deny: external_exports.array(toolRuleSchema).max(500)
|
|
37196
37195
|
});
|
|
37197
37196
|
var agentToolsReadQuerySchema = external_exports.object({
|
|
@@ -37681,6 +37680,50 @@ async function commitAndPushSubmodules(root, submodules, emit) {
|
|
|
37681
37680
|
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
37682
37681
|
}
|
|
37683
37682
|
}
|
|
37683
|
+
function providerSlug(url) {
|
|
37684
|
+
const provider = /github\.com/i.test(url) ? "gh" : /gitlab\.com/i.test(url) ? "glab" : null;
|
|
37685
|
+
const m2 = /(?:github|gitlab)\.com[/:]+([^/]+\/.+?)(?:\.git)?\/?$/i.exec(url);
|
|
37686
|
+
return { provider, slug: m2?.[1] ?? "" };
|
|
37687
|
+
}
|
|
37688
|
+
async function commitAndOpenPr(root, repo, emit) {
|
|
37689
|
+
emit("commit", "Committing the scaffolded project\u2026");
|
|
37690
|
+
await run2("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
|
|
37691
|
+
const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37692
|
+
if (!staged) {
|
|
37693
|
+
emit("commit", "Nothing to commit \u2014 skipping the pull request.");
|
|
37694
|
+
return;
|
|
37695
|
+
}
|
|
37696
|
+
const commitArgs = ["commit", "-m", "chore: scaffold project (4PM)"];
|
|
37697
|
+
try {
|
|
37698
|
+
await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37699
|
+
} catch {
|
|
37700
|
+
await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37701
|
+
}
|
|
37702
|
+
const branch = (repo.branch ?? "").trim();
|
|
37703
|
+
emit("push", "Pushing the scaffold branch\u2026");
|
|
37704
|
+
try {
|
|
37705
|
+
await run2("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
|
|
37706
|
+
} catch (err) {
|
|
37707
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37708
|
+
emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 push it from the Git tab when credentials are available.`);
|
|
37709
|
+
return;
|
|
37710
|
+
}
|
|
37711
|
+
const { provider, slug } = providerSlug(repo.url ?? "");
|
|
37712
|
+
if (!provider) {
|
|
37713
|
+
emit("pr", "Unknown git host \u2014 skipping the pull request.");
|
|
37714
|
+
return;
|
|
37715
|
+
}
|
|
37716
|
+
emit("pr", `Opening a pull request via ${provider}\u2026`);
|
|
37717
|
+
try {
|
|
37718
|
+
const args = provider === "glab" ? ["mr", "create", "--fill", ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...slug ? ["-R", slug] : []];
|
|
37719
|
+
const { stdout } = await run2(provider, args, { cwd: root, timeout: 12e4 });
|
|
37720
|
+
const link2 = stdout.trim().split(/\s+/).find((s3) => /^https?:\/\//.test(s3)) ?? stdout.trim().split("\n").pop() ?? "";
|
|
37721
|
+
emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
|
|
37722
|
+
} catch (err) {
|
|
37723
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37724
|
+
emit("pr-failed", `Could not open a pull request (${provider}): ${msg}`);
|
|
37725
|
+
}
|
|
37726
|
+
}
|
|
37684
37727
|
function cloneArgs(url, dest, branch) {
|
|
37685
37728
|
const b2 = (branch ?? "").trim();
|
|
37686
37729
|
return b2 ? ["clone", "-b", b2, url, dest] : ["clone", url, dest];
|
|
@@ -37787,18 +37830,20 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
|
37787
37830
|
if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
|
|
37788
37831
|
emit("git", "Cloning repository\u2026");
|
|
37789
37832
|
await provisionRepo(target, repo, emit, { createMissingBranch: true });
|
|
37790
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37833
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true });
|
|
37791
37834
|
await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
|
|
37792
37835
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37793
37836
|
await writeTemplateMarker(target);
|
|
37837
|
+
await commitAndOpenPr(target, repo, emit);
|
|
37794
37838
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
37795
37839
|
return { ok: true, path: target };
|
|
37796
37840
|
} catch (err) {
|
|
37797
37841
|
return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
|
|
37798
37842
|
}
|
|
37799
37843
|
}
|
|
37800
|
-
async function scaffoldRepo(dir2, label, spec, emit) {
|
|
37844
|
+
async function scaffoldRepo(dir2, label, spec, emit, opts = {}) {
|
|
37801
37845
|
await mkdir(dir2, { recursive: true });
|
|
37846
|
+
if (opts.resetClaude) await resetTemplateManagedClaude(dir2, label, emit);
|
|
37802
37847
|
emit("copy", `Copying the sample template into ${label}\u2026`);
|
|
37803
37848
|
await cp(sampleDir(), dir2, { recursive: true, force: false, errorOnExist: false });
|
|
37804
37849
|
if (spec) {
|
|
@@ -37807,6 +37852,13 @@ async function scaffoldRepo(dir2, label, spec, emit) {
|
|
|
37807
37852
|
await aiInit(dir2, spec, emit);
|
|
37808
37853
|
}
|
|
37809
37854
|
}
|
|
37855
|
+
async function resetTemplateManagedClaude(dir2, label, emit) {
|
|
37856
|
+
const claude = join6(dir2, ".claude");
|
|
37857
|
+
if (!existsSync4(claude)) return;
|
|
37858
|
+
emit("copy", `Resetting template .claude config in ${label}\u2026`);
|
|
37859
|
+
await rm(join6(claude, "settings.json"), { force: true });
|
|
37860
|
+
await rm(join6(claude, "skills"), { recursive: true, force: true });
|
|
37861
|
+
}
|
|
37810
37862
|
async function writeTemplateMarker(target) {
|
|
37811
37863
|
const dir2 = join6(target, ".4pm");
|
|
37812
37864
|
await mkdir(dir2, { recursive: true });
|
|
@@ -37854,15 +37906,20 @@ ${specJson}`
|
|
|
37854
37906
|
${specJson}` + (guideInstructions ? `
|
|
37855
37907
|
Also incorporate these additional instructions/content:
|
|
37856
37908
|
${guideInstructions}
|
|
37857
|
-
` : "")
|
|
37909
|
+
` : ""),
|
|
37910
|
+
guideInstructions
|
|
37858
37911
|
);
|
|
37859
37912
|
}
|
|
37860
|
-
async function generateFile(path, prompt) {
|
|
37913
|
+
async function generateFile(path, prompt, fallback) {
|
|
37861
37914
|
try {
|
|
37862
37915
|
const text = await aiGenerate(prompt);
|
|
37863
|
-
if (text.trim())
|
|
37916
|
+
if (text.trim()) {
|
|
37917
|
+
await writeFile(path, text.trim() + "\n", "utf8");
|
|
37918
|
+
return;
|
|
37919
|
+
}
|
|
37864
37920
|
} catch {
|
|
37865
37921
|
}
|
|
37922
|
+
if (fallback && fallback.trim()) await writeFile(path, fallback.trim() + "\n", "utf8");
|
|
37866
37923
|
}
|
|
37867
37924
|
async function addProject(payload, profileDir2, onProgress) {
|
|
37868
37925
|
let lastStep = "start";
|
|
@@ -38332,6 +38389,12 @@ var WsChannels = {
|
|
|
38332
38389
|
// AI knowledge distill (ADR-0190): server → project cli request–reply. The worker runs the AI CLI
|
|
38333
38390
|
// in the project's working dir with a distillation prompt and replies with the composed markdown.
|
|
38334
38391
|
KNOWLEDGE_COMPOSE: "knowledge.compose",
|
|
38392
|
+
// Ticket → FAQ synthesis (ADR-0333): server (platform-pool dispatcher) → platform-pool cli
|
|
38393
|
+
// request–reply. The dispatcher sends the selected support tickets + the 4pm-faq repo with a
|
|
38394
|
+
// short-lived WRITE token; the worker runs an agent-write that distils the tickets into FAQ
|
|
38395
|
+
// entries, commits, pushes a branch, opens a PR, then wipes the token/clone, and replies with the
|
|
38396
|
+
// PR URL. The write token never persists on the worker (leak-safety on release/reassign).
|
|
38397
|
+
FAQ_COMPOSE: "faq.compose",
|
|
38335
38398
|
// Shared AI memory (ADR-0245): cli → server one-way write-back of the compacted rolling memory for
|
|
38336
38399
|
// the project it serves (persisted per project × machine-user link). The read direction rides
|
|
38337
38400
|
// `ws_token` (seeds the cli cache on connect); this channel only carries updates.
|
|
@@ -42360,7 +42423,8 @@ async function gitRepos(root) {
|
|
|
42360
42423
|
if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
|
|
42361
42424
|
const repos = [];
|
|
42362
42425
|
const rootRemote = (await git(base, ["remote", "get-url", "origin"])).trim();
|
|
42363
|
-
|
|
42426
|
+
const rootBranch = (await git(base, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
42427
|
+
repos.push({ subdir: "", name: "", remote: rootRemote || null, branch: rootBranch || null });
|
|
42364
42428
|
const subOut = await git(base, ["submodule", "status"]);
|
|
42365
42429
|
for (const line of subOut.split("\n")) {
|
|
42366
42430
|
const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
|
|
@@ -42369,7 +42433,8 @@ async function gitRepos(root) {
|
|
|
42369
42433
|
const abs = resolve8(base, subPath);
|
|
42370
42434
|
if (!(abs === base || abs.startsWith(base + sep8))) continue;
|
|
42371
42435
|
const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
|
|
42372
|
-
|
|
42436
|
+
const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
42437
|
+
repos.push({ subdir: subPath, name: subPath, remote: remote || null, branch: branch || null });
|
|
42373
42438
|
}
|
|
42374
42439
|
return { repos };
|
|
42375
42440
|
}
|
|
@@ -42740,6 +42805,187 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
|
|
|
42740
42805
|
return { bodyMarkdown: "", error: lastReason };
|
|
42741
42806
|
}
|
|
42742
42807
|
|
|
42808
|
+
// src/core/faq-compose.ts
|
|
42809
|
+
import { spawn as spawn9, execFile as execFile10 } from "child_process";
|
|
42810
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync as rmSync7 } from "fs";
|
|
42811
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
42812
|
+
import { join as join24 } from "path";
|
|
42813
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
42814
|
+
import { promisify as promisify10 } from "util";
|
|
42815
|
+
var execFileAsync2 = promisify10(execFile10);
|
|
42816
|
+
var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
42817
|
+
var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
42818
|
+
function authedUrl2(url, token) {
|
|
42819
|
+
if (url.startsWith("https://")) return url.replace("https://", `https://x-access-token:${token}@`);
|
|
42820
|
+
return url;
|
|
42821
|
+
}
|
|
42822
|
+
function buildPrompt4(tickets, customPrompt) {
|
|
42823
|
+
const transcript = tickets.map((t2) => {
|
|
42824
|
+
const thread = t2.messages.map((m2) => ` [${m2.author}] ${m2.body}`).join("\n");
|
|
42825
|
+
return `### Ticket ${t2.id} \u2014 ${t2.subject} (${t2.category})
|
|
42826
|
+
${thread}`;
|
|
42827
|
+
}).join("\n\n");
|
|
42828
|
+
const ids = tickets.map((t2) => t2.id);
|
|
42829
|
+
return [
|
|
42830
|
+
"You are maintaining the 4PM product FAQ, stored as markdown in THIS git repository (your CWD).",
|
|
42831
|
+
"From the resolved support tickets below, distil reusable, product-general FAQ entries and add or",
|
|
42832
|
+
"update the appropriate markdown file(s) in this repo (e.g. an FAQ.md, or the closest existing",
|
|
42833
|
+
"topic file \u2014 inspect the repo first and match its structure/style).",
|
|
42834
|
+
"",
|
|
42835
|
+
"Rules:",
|
|
42836
|
+
"- Write generalized Q&A, NOT ticket-specific replies; never include customer names, emails, ids,",
|
|
42837
|
+
" org names or any personal data from the tickets.",
|
|
42838
|
+
"- Merge with existing entries instead of duplicating; keep the existing formatting and headings.",
|
|
42839
|
+
"- If a ticket yields no reusable FAQ value, skip it. If nothing is worth adding, make NO changes.",
|
|
42840
|
+
"- Only edit markdown documentation files; do not touch code, CI, or unrelated files.",
|
|
42841
|
+
"- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working",
|
|
42842
|
+
" tree; the surrounding tooling handles commit + PR.",
|
|
42843
|
+
...customPrompt?.trim() ? ["", "Additional instruction from the operator (follow it too):", customPrompt.trim()] : [],
|
|
42844
|
+
"",
|
|
42845
|
+
"===== RESOLVED TICKETS =====",
|
|
42846
|
+
transcript,
|
|
42847
|
+
"",
|
|
42848
|
+
"===== REQUIRED FINAL OUTPUT =====",
|
|
42849
|
+
"After finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON",
|
|
42850
|
+
"block summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):",
|
|
42851
|
+
"```json",
|
|
42852
|
+
`{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`,
|
|
42853
|
+
"```",
|
|
42854
|
+
"Include exactly one entry per ticket id above; keep each summary to one or two sentences."
|
|
42855
|
+
].join("\n");
|
|
42856
|
+
}
|
|
42857
|
+
function parsePerTicket(text) {
|
|
42858
|
+
const idx = text.lastIndexOf('"perTicket"');
|
|
42859
|
+
if (idx === -1) return [];
|
|
42860
|
+
const open = text.lastIndexOf("{", idx);
|
|
42861
|
+
if (open === -1) return [];
|
|
42862
|
+
let depth = 0;
|
|
42863
|
+
for (let i = open; i < text.length; i++) {
|
|
42864
|
+
if (text[i] === "{") depth++;
|
|
42865
|
+
else if (text[i] === "}") {
|
|
42866
|
+
depth--;
|
|
42867
|
+
if (depth === 0) {
|
|
42868
|
+
try {
|
|
42869
|
+
const obj = JSON.parse(text.slice(open, i + 1));
|
|
42870
|
+
if (Array.isArray(obj.perTicket)) {
|
|
42871
|
+
return obj.perTicket.filter((e) => !!e && typeof e.ticketId === "string").map((e) => ({ ticketId: e.ticketId, summary: String(e.summary ?? "") }));
|
|
42872
|
+
}
|
|
42873
|
+
} catch {
|
|
42874
|
+
return [];
|
|
42875
|
+
}
|
|
42876
|
+
return [];
|
|
42877
|
+
}
|
|
42878
|
+
}
|
|
42879
|
+
}
|
|
42880
|
+
return [];
|
|
42881
|
+
}
|
|
42882
|
+
function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv) {
|
|
42883
|
+
return new Promise((resolve10, reject) => {
|
|
42884
|
+
const isClaude = cmd.includes("claude");
|
|
42885
|
+
const args = [
|
|
42886
|
+
"-p",
|
|
42887
|
+
...profile?.model ? ["--model", profile.model] : [],
|
|
42888
|
+
...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
|
|
42889
|
+
// Write-capable headless agent (ADR-0271): auto-approve tools so it never stalls on a prompt.
|
|
42890
|
+
...isClaude ? ["--permission-mode", "bypassPermissions"] : [],
|
|
42891
|
+
...cmd.includes("codex") ? ["--dangerously-bypass-approvals-and-sandbox"] : []
|
|
42892
|
+
];
|
|
42893
|
+
const env3 = {
|
|
42894
|
+
...process.env,
|
|
42895
|
+
...extraEnv,
|
|
42896
|
+
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
42897
|
+
};
|
|
42898
|
+
const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
42899
|
+
const parser = createAiStreamParser(cmd);
|
|
42900
|
+
let out = "";
|
|
42901
|
+
let err = "";
|
|
42902
|
+
const timer = setTimeout(() => {
|
|
42903
|
+
child.kill("SIGKILL");
|
|
42904
|
+
reject(new Error("faq-compose agent timed out"));
|
|
42905
|
+
}, COMPOSE_TIMEOUT_MS2);
|
|
42906
|
+
timer.unref();
|
|
42907
|
+
child.stdout.on("data", (d) => out += parser.push(d.toString()));
|
|
42908
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
42909
|
+
child.on("error", (e) => {
|
|
42910
|
+
clearTimeout(timer);
|
|
42911
|
+
reject(e);
|
|
42912
|
+
});
|
|
42913
|
+
child.on("close", (code) => {
|
|
42914
|
+
clearTimeout(timer);
|
|
42915
|
+
out += parser.flush();
|
|
42916
|
+
resolve10({ code: code ?? -1, out, err, usage: parser.usage() });
|
|
42917
|
+
});
|
|
42918
|
+
child.stdin.write(prompt);
|
|
42919
|
+
child.stdin.end();
|
|
42920
|
+
});
|
|
42921
|
+
}
|
|
42922
|
+
async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
42923
|
+
const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
|
|
42924
|
+
let lastReason = "no attempt";
|
|
42925
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
42926
|
+
const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env);
|
|
42927
|
+
if (code === 0) return { usage, text: out.trim() };
|
|
42928
|
+
lastReason = `exited ${code}: ${(err || out).slice(0, 500)}`;
|
|
42929
|
+
if (i === attempts.length - 1) break;
|
|
42930
|
+
}
|
|
42931
|
+
throw new Error(`claude ${lastReason}`);
|
|
42932
|
+
}
|
|
42933
|
+
var MAX_OUTPUT_CHARS = 8e3;
|
|
42934
|
+
async function runFaqCompose(req, ai2) {
|
|
42935
|
+
if (!req.repo.token) return { prUrl: "", error: "no write token" };
|
|
42936
|
+
const workDir = join24(tmpdir3(), "4pm-faq-sync", randomUUID3());
|
|
42937
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
42938
|
+
const authed = authedUrl2(req.repo.url, req.repo.token);
|
|
42939
|
+
try {
|
|
42940
|
+
mkdirSync7(workDir, { recursive: true });
|
|
42941
|
+
await execFileAsync2("git", ["clone", "--depth", "1", "--branch", req.repo.branch, authed, workDir], { env: gitEnv });
|
|
42942
|
+
await execFileAsync2("git", ["-C", workDir, "config", "user.name", "4PM FAQ Bot"], { env: gitEnv });
|
|
42943
|
+
await execFileAsync2("git", ["-C", workDir, "config", "user.email", "faq-bot@4pm.app"], { env: gitEnv });
|
|
42944
|
+
await execFileAsync2("git", ["-C", workDir, "checkout", "-b", req.headBranch], { env: gitEnv });
|
|
42945
|
+
const { usage, text } = await runClaudeWithFailover2(buildPrompt4(req.tickets, req.customPrompt), workDir, ai2);
|
|
42946
|
+
const output = text.slice(0, MAX_OUTPUT_CHARS);
|
|
42947
|
+
const perTicket = parsePerTicket(text);
|
|
42948
|
+
await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
|
|
42949
|
+
const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
|
|
42950
|
+
if (!staged.stdout.trim()) {
|
|
42951
|
+
return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens2(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
42952
|
+
}
|
|
42953
|
+
await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
|
|
42954
|
+
await execFileAsync2("git", ["-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
|
|
42955
|
+
let prUrl = "";
|
|
42956
|
+
try {
|
|
42957
|
+
const pr2 = await execFileAsync2(
|
|
42958
|
+
"gh",
|
|
42959
|
+
["pr", "create", "--fill", "--head", req.headBranch, "--base", req.repo.branch],
|
|
42960
|
+
{ cwd: workDir, env: { ...gitEnv, GH_TOKEN: req.repo.token } }
|
|
42961
|
+
);
|
|
42962
|
+
prUrl = pr2.stdout.trim().split("\n").pop() ?? "";
|
|
42963
|
+
} catch (err) {
|
|
42964
|
+
logger.warn("faq.compose.pr.failed", { error: String(err) });
|
|
42965
|
+
}
|
|
42966
|
+
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE2 };
|
|
42967
|
+
return {
|
|
42968
|
+
prUrl,
|
|
42969
|
+
branch: req.headBranch,
|
|
42970
|
+
output,
|
|
42971
|
+
perTicket,
|
|
42972
|
+
tokens: u2.tokens,
|
|
42973
|
+
...usage.tokens > 0 ? { tokensBreakdown: { input: usage.input, output: usage.output, cacheRead: usage.cacheRead, cacheCreation: usage.cacheCreation } } : {},
|
|
42974
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
42975
|
+
};
|
|
42976
|
+
} catch (err) {
|
|
42977
|
+
logger.warn("faq.compose.failed", { error: String(err) });
|
|
42978
|
+
return { prUrl: "", error: String(err) };
|
|
42979
|
+
} finally {
|
|
42980
|
+
if (existsSync14(workDir)) {
|
|
42981
|
+
try {
|
|
42982
|
+
rmSync7(workDir, { recursive: true, force: true });
|
|
42983
|
+
} catch {
|
|
42984
|
+
}
|
|
42985
|
+
}
|
|
42986
|
+
}
|
|
42987
|
+
}
|
|
42988
|
+
|
|
42743
42989
|
// src/core/ws-client/handlers/support.ts
|
|
42744
42990
|
function handleSupportChannels(ctx, message, payload) {
|
|
42745
42991
|
switch (message.channel) {
|
|
@@ -42796,6 +43042,28 @@ function handleSupportChannels(ctx, message, payload) {
|
|
|
42796
43042
|
}).finally(() => ctx.bus.endBusy("knowledge-compose"));
|
|
42797
43043
|
return true;
|
|
42798
43044
|
}
|
|
43045
|
+
case WsChannels.FAQ_COMPOSE: {
|
|
43046
|
+
const freq = payload;
|
|
43047
|
+
const fcfg = readProfileConfig(ctx.profileDir);
|
|
43048
|
+
const fcmd = fcfg.aiCli || "claude";
|
|
43049
|
+
const fai = {
|
|
43050
|
+
cmd: fcmd,
|
|
43051
|
+
profiles: resolveClaudeProfiles(fcfg, getWorkingProfile(ctx.profileDir, fcmd)),
|
|
43052
|
+
env: fcfg.aiEnv
|
|
43053
|
+
};
|
|
43054
|
+
ctx.bus.push({ source: "server", kind: "aireq", text: `${fcmd} \u2039 synthesize FAQ (${freq.tickets.length} tickets)` });
|
|
43055
|
+
ctx.bus.startBusy("faq-compose");
|
|
43056
|
+
void runFaqCompose(freq, fai).then((reply) => {
|
|
43057
|
+
if (reply.error) {
|
|
43058
|
+
ctx.bus.push({ source: "server", kind: "log", text: `faq-compose failed: ${reply.error}`, level: "warn" });
|
|
43059
|
+
} else {
|
|
43060
|
+
ctx.bus.push({ source: "server", kind: "aires", text: `${fcmd} \u203A ${reply.prUrl || "no changes"}` });
|
|
43061
|
+
}
|
|
43062
|
+
reportToolResult(fcmd, !reply.error, reply.error);
|
|
43063
|
+
ctx.send(WsChannels.FAQ_COMPOSE, reply, message.id);
|
|
43064
|
+
}).finally(() => ctx.bus.endBusy("faq-compose"));
|
|
43065
|
+
return true;
|
|
43066
|
+
}
|
|
42799
43067
|
default:
|
|
42800
43068
|
return false;
|
|
42801
43069
|
}
|
|
@@ -42894,7 +43162,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
42894
43162
|
|
|
42895
43163
|
// src/core/agents.ts
|
|
42896
43164
|
import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir8, readdir as readdir4, rm as rm5 } from "fs/promises";
|
|
42897
|
-
import { join as
|
|
43165
|
+
import { join as join25 } from "path";
|
|
42898
43166
|
var AGENTS_REL = ".claude/agents";
|
|
42899
43167
|
var SKILLS_REL = ".claude/skills";
|
|
42900
43168
|
var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
|
|
@@ -42918,17 +43186,17 @@ async function listAgents(root) {
|
|
|
42918
43186
|
const subagents = [];
|
|
42919
43187
|
const skills = [];
|
|
42920
43188
|
try {
|
|
42921
|
-
for (const e of await readdir4(
|
|
43189
|
+
for (const e of await readdir4(join25(root, AGENTS_REL), { withFileTypes: true })) {
|
|
42922
43190
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
42923
|
-
const fm = frontmatter(await readText2(
|
|
43191
|
+
const fm = frontmatter(await readText2(join25(root, AGENTS_REL, e.name)));
|
|
42924
43192
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
42925
43193
|
}
|
|
42926
43194
|
} catch {
|
|
42927
43195
|
}
|
|
42928
43196
|
try {
|
|
42929
|
-
for (const e of await readdir4(
|
|
43197
|
+
for (const e of await readdir4(join25(root, SKILLS_REL), { withFileTypes: true })) {
|
|
42930
43198
|
if (!e.isDirectory()) continue;
|
|
42931
|
-
const fm = frontmatter(await readText2(
|
|
43199
|
+
const fm = frontmatter(await readText2(join25(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
42932
43200
|
skills.push({ name: e.name, description: fm.description });
|
|
42933
43201
|
}
|
|
42934
43202
|
} catch {
|
|
@@ -42939,15 +43207,15 @@ async function listAgents(root) {
|
|
|
42939
43207
|
}
|
|
42940
43208
|
function itemPath(root, kind, name) {
|
|
42941
43209
|
const n = safeName(name);
|
|
42942
|
-
return kind === "subagent" ?
|
|
43210
|
+
return kind === "subagent" ? join25(root, AGENTS_REL, `${n}.md`) : join25(root, SKILLS_REL, n, "SKILL.md");
|
|
42943
43211
|
}
|
|
42944
43212
|
async function readAgent(root, kind, name) {
|
|
42945
43213
|
const content = await readText2(itemPath(root, kind, name));
|
|
42946
43214
|
if (kind !== "subagent") return { content };
|
|
42947
43215
|
const n = safeName(name);
|
|
42948
43216
|
const [short, long] = await Promise.all([
|
|
42949
|
-
readText2(
|
|
42950
|
-
readText2(
|
|
43217
|
+
readText2(join25(root, AGENTS_REL, "short-memory", `${n}.md`)),
|
|
43218
|
+
readText2(join25(root, AGENTS_REL, "long-memory", `${n}.md`))
|
|
42951
43219
|
]);
|
|
42952
43220
|
return { content, memory: { short, long } };
|
|
42953
43221
|
}
|
|
@@ -42957,12 +43225,12 @@ async function writeAgent(root, req) {
|
|
|
42957
43225
|
try {
|
|
42958
43226
|
if (req.action === "delete") {
|
|
42959
43227
|
if (req.kind === "subagent") await rm5(itemPath(root, "subagent", n), { force: true });
|
|
42960
|
-
else await rm5(
|
|
43228
|
+
else await rm5(join25(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
42961
43229
|
return { ok: true };
|
|
42962
43230
|
}
|
|
42963
43231
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
42964
43232
|
const path = itemPath(root, req.kind, n);
|
|
42965
|
-
await mkdir8(
|
|
43233
|
+
await mkdir8(join25(path, ".."), { recursive: true });
|
|
42966
43234
|
await writeFile8(path, req.content, "utf8");
|
|
42967
43235
|
return { ok: true };
|
|
42968
43236
|
} catch (err) {
|
|
@@ -42972,7 +43240,7 @@ async function writeAgent(root, req) {
|
|
|
42972
43240
|
|
|
42973
43241
|
// src/core/secrets.ts
|
|
42974
43242
|
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
42975
|
-
import { join as
|
|
43243
|
+
import { join as join26 } from "path";
|
|
42976
43244
|
var SECURITY_REL = "AI_SECURITY.md";
|
|
42977
43245
|
var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
|
|
42978
43246
|
var SECRETS_REL = "project.secrets.json";
|
|
@@ -42985,7 +43253,7 @@ async function readText3(path, fallback = "") {
|
|
|
42985
43253
|
}
|
|
42986
43254
|
async function readSecretsObject(root) {
|
|
42987
43255
|
try {
|
|
42988
|
-
const v2 = JSON.parse(await readFile11(
|
|
43256
|
+
const v2 = JSON.parse(await readFile11(join26(root, SECRETS_REL), "utf8"));
|
|
42989
43257
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
42990
43258
|
} catch {
|
|
42991
43259
|
return {};
|
|
@@ -43004,8 +43272,8 @@ function placeholderKeys(text) {
|
|
|
43004
43272
|
}
|
|
43005
43273
|
async function readSecrets(root) {
|
|
43006
43274
|
const [security, placeholder, secrets] = await Promise.all([
|
|
43007
|
-
readText3(
|
|
43008
|
-
readText3(
|
|
43275
|
+
readText3(join26(root, SECURITY_REL)),
|
|
43276
|
+
readText3(join26(root, PLACEHOLDER_REL)),
|
|
43009
43277
|
readSecretsObject(root)
|
|
43010
43278
|
]);
|
|
43011
43279
|
const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
|
|
@@ -43018,14 +43286,14 @@ async function readSecrets(root) {
|
|
|
43018
43286
|
async function writeSecrets(root, req) {
|
|
43019
43287
|
try {
|
|
43020
43288
|
if (req.kind === "security") {
|
|
43021
|
-
await writeFile9(
|
|
43289
|
+
await writeFile9(join26(root, SECURITY_REL), req.content, "utf8");
|
|
43022
43290
|
} else if (req.kind === "placeholder") {
|
|
43023
|
-
await writeFile9(
|
|
43291
|
+
await writeFile9(join26(root, PLACEHOLDER_REL), req.content, "utf8");
|
|
43024
43292
|
} else {
|
|
43025
43293
|
const secrets = await readSecretsObject(root);
|
|
43026
43294
|
if (req.kind === "secret") secrets[req.key] = req.value;
|
|
43027
43295
|
else delete secrets[req.key];
|
|
43028
|
-
await writeFile9(
|
|
43296
|
+
await writeFile9(join26(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
|
|
43029
43297
|
}
|
|
43030
43298
|
return { ok: true };
|
|
43031
43299
|
} catch (err) {
|
|
@@ -43035,13 +43303,12 @@ async function writeSecrets(root, req) {
|
|
|
43035
43303
|
|
|
43036
43304
|
// src/core/agent-tools.ts
|
|
43037
43305
|
import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
|
|
43038
|
-
import { dirname as dirname8, join as
|
|
43306
|
+
import { dirname as dirname8, join as join27 } from "path";
|
|
43039
43307
|
var SHARED_REL = ".claude/settings.json";
|
|
43040
43308
|
var LOCAL_REL = ".claude/settings.local.json";
|
|
43041
43309
|
var SECRETS_DENY = "Read(./project.secrets.json)";
|
|
43042
|
-
var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
43043
43310
|
function fileFor(root, scope) {
|
|
43044
|
-
return
|
|
43311
|
+
return join27(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
|
|
43045
43312
|
}
|
|
43046
43313
|
async function readSettingsObject(path) {
|
|
43047
43314
|
try {
|
|
@@ -43053,9 +43320,8 @@ async function readSettingsObject(path) {
|
|
|
43053
43320
|
}
|
|
43054
43321
|
function normalizePermissions(raw) {
|
|
43055
43322
|
const p2 = raw && typeof raw === "object" ? raw : {};
|
|
43056
|
-
const mode = MODES.includes(p2.defaultMode) ? p2.defaultMode : "default";
|
|
43057
43323
|
const list = (v2) => Array.isArray(v2) ? v2.filter((x) => typeof x === "string") : [];
|
|
43058
|
-
return { defaultMode:
|
|
43324
|
+
return { defaultMode: "bypassPermissions", allow: list(p2.allow), deny: list(p2.deny) };
|
|
43059
43325
|
}
|
|
43060
43326
|
async function readAgentTools(root, scope) {
|
|
43061
43327
|
const obj = await readSettingsObject(fileFor(root, scope));
|
|
@@ -43079,7 +43345,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
43079
43345
|
// src/core/packages.ts
|
|
43080
43346
|
import { createHash as createHash4 } from "crypto";
|
|
43081
43347
|
import { readFile as readFile13, writeFile as writeFile11, mkdir as mkdir10, readdir as readdir5, rm as rm6, stat as stat4 } from "fs/promises";
|
|
43082
|
-
import { dirname as dirname9, join as
|
|
43348
|
+
import { dirname as dirname9, join as join28, relative } from "path";
|
|
43083
43349
|
var AGENTS_REL2 = ".claude/agents";
|
|
43084
43350
|
var SKILLS_REL2 = ".claude/skills";
|
|
43085
43351
|
var MANIFEST_REL = ".claude/.4pm-packages.json";
|
|
@@ -43103,7 +43369,7 @@ async function listFilesRel(dir2) {
|
|
|
43103
43369
|
return;
|
|
43104
43370
|
}
|
|
43105
43371
|
for (const e of entries2) {
|
|
43106
|
-
const full =
|
|
43372
|
+
const full = join28(d, e.name);
|
|
43107
43373
|
if (e.isDirectory()) await walk2(full);
|
|
43108
43374
|
else if (e.isFile()) out.push(relative(dir2, full));
|
|
43109
43375
|
}
|
|
@@ -43114,16 +43380,16 @@ async function listFilesRel(dir2) {
|
|
|
43114
43380
|
async function readArtifactFiles(root, kind, slug) {
|
|
43115
43381
|
const files = [];
|
|
43116
43382
|
if (kind === "subagent") {
|
|
43117
|
-
const agent = await readMaybe(
|
|
43383
|
+
const agent = await readMaybe(join28(root, AGENTS_REL2, `${slug}.md`));
|
|
43118
43384
|
if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
|
|
43119
|
-
const short = await readMaybe(
|
|
43385
|
+
const short = await readMaybe(join28(root, AGENTS_REL2, "short-memory", `${slug}.md`));
|
|
43120
43386
|
if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
|
|
43121
|
-
const long = await readMaybe(
|
|
43387
|
+
const long = await readMaybe(join28(root, AGENTS_REL2, "long-memory", `${slug}.md`));
|
|
43122
43388
|
if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
|
|
43123
43389
|
} else {
|
|
43124
|
-
const base =
|
|
43390
|
+
const base = join28(root, SKILLS_REL2, slug);
|
|
43125
43391
|
for (const rel of await listFilesRel(base)) {
|
|
43126
|
-
const buf = await readMaybe(
|
|
43392
|
+
const buf = await readMaybe(join28(base, rel));
|
|
43127
43393
|
if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
|
|
43128
43394
|
}
|
|
43129
43395
|
}
|
|
@@ -43141,7 +43407,7 @@ async function artifactSha256(root, kind, slug) {
|
|
|
43141
43407
|
return h.digest("hex");
|
|
43142
43408
|
}
|
|
43143
43409
|
async function artifactExists(root, kind, slug) {
|
|
43144
|
-
const target = kind === "subagent" ?
|
|
43410
|
+
const target = kind === "subagent" ? join28(root, AGENTS_REL2, `${slug}.md`) : join28(root, SKILLS_REL2, slug);
|
|
43145
43411
|
try {
|
|
43146
43412
|
await stat4(target);
|
|
43147
43413
|
return true;
|
|
@@ -43150,7 +43416,7 @@ async function artifactExists(root, kind, slug) {
|
|
|
43150
43416
|
}
|
|
43151
43417
|
}
|
|
43152
43418
|
async function readManifest(root) {
|
|
43153
|
-
const buf = await readMaybe(
|
|
43419
|
+
const buf = await readMaybe(join28(root, MANIFEST_REL));
|
|
43154
43420
|
if (!buf) return {};
|
|
43155
43421
|
try {
|
|
43156
43422
|
return JSON.parse(buf.toString("utf8"));
|
|
@@ -43159,7 +43425,7 @@ async function readManifest(root) {
|
|
|
43159
43425
|
}
|
|
43160
43426
|
}
|
|
43161
43427
|
async function writeManifest(root, manifest) {
|
|
43162
|
-
const path =
|
|
43428
|
+
const path = join28(root, MANIFEST_REL);
|
|
43163
43429
|
await mkdir10(dirname9(path), { recursive: true });
|
|
43164
43430
|
await writeFile11(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
43165
43431
|
}
|
|
@@ -43168,24 +43434,24 @@ async function writeArtifactFiles(root, kind, slug, files) {
|
|
|
43168
43434
|
if (kind === "subagent") {
|
|
43169
43435
|
const agent = byPath.get("agent.md");
|
|
43170
43436
|
if (agent) {
|
|
43171
|
-
await mkdir10(
|
|
43172
|
-
await writeFile11(
|
|
43437
|
+
await mkdir10(join28(root, AGENTS_REL2), { recursive: true });
|
|
43438
|
+
await writeFile11(join28(root, AGENTS_REL2, `${slug}.md`), agent);
|
|
43173
43439
|
}
|
|
43174
43440
|
const short = byPath.get("short-memory.md");
|
|
43175
43441
|
if (short) {
|
|
43176
|
-
await mkdir10(
|
|
43177
|
-
await writeFile11(
|
|
43442
|
+
await mkdir10(join28(root, AGENTS_REL2, "short-memory"), { recursive: true });
|
|
43443
|
+
await writeFile11(join28(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
|
|
43178
43444
|
}
|
|
43179
43445
|
const long = byPath.get("long-memory.md");
|
|
43180
43446
|
if (long) {
|
|
43181
|
-
await mkdir10(
|
|
43182
|
-
await writeFile11(
|
|
43447
|
+
await mkdir10(join28(root, AGENTS_REL2, "long-memory"), { recursive: true });
|
|
43448
|
+
await writeFile11(join28(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
|
|
43183
43449
|
}
|
|
43184
43450
|
} else {
|
|
43185
|
-
const base =
|
|
43451
|
+
const base = join28(root, SKILLS_REL2, slug);
|
|
43186
43452
|
for (const [rel, buf] of byPath) {
|
|
43187
43453
|
if (rel.includes("..") || rel.startsWith("/")) continue;
|
|
43188
|
-
const dest =
|
|
43454
|
+
const dest = join28(base, rel);
|
|
43189
43455
|
await mkdir10(dirname9(dest), { recursive: true });
|
|
43190
43456
|
await writeFile11(dest, buf);
|
|
43191
43457
|
}
|
|
@@ -43251,11 +43517,11 @@ async function removePackage(root, slug) {
|
|
|
43251
43517
|
const entry = manifest[n];
|
|
43252
43518
|
if (!entry) return { ok: false, error: "not installed" };
|
|
43253
43519
|
if (entry.kind === "subagent") {
|
|
43254
|
-
await rm6(
|
|
43255
|
-
await rm6(
|
|
43256
|
-
await rm6(
|
|
43520
|
+
await rm6(join28(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
43521
|
+
await rm6(join28(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
43522
|
+
await rm6(join28(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
43257
43523
|
} else {
|
|
43258
|
-
await rm6(
|
|
43524
|
+
await rm6(join28(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
43259
43525
|
}
|
|
43260
43526
|
delete manifest[n];
|
|
43261
43527
|
await writeManifest(root, manifest);
|
|
@@ -43267,7 +43533,7 @@ async function removePackage(root, slug) {
|
|
|
43267
43533
|
|
|
43268
43534
|
// src/core/graph.ts
|
|
43269
43535
|
import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
|
|
43270
|
-
import { join as
|
|
43536
|
+
import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
43271
43537
|
var MAX_FILES = 2e3;
|
|
43272
43538
|
var MAX_NODES = 3e3;
|
|
43273
43539
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
|
|
@@ -43287,7 +43553,7 @@ async function walk(root, accept) {
|
|
|
43287
43553
|
if (e.name.startsWith(".") && e.name !== ".claude") {
|
|
43288
43554
|
if (e.isDirectory()) continue;
|
|
43289
43555
|
}
|
|
43290
|
-
const full =
|
|
43556
|
+
const full = join29(dir2, e.name);
|
|
43291
43557
|
if (e.isDirectory()) {
|
|
43292
43558
|
if (!SKIP_DIRS.has(e.name)) stack.push(full);
|
|
43293
43559
|
} else if (accept(full)) {
|
|
@@ -43407,12 +43673,12 @@ async function buildGraph(root, mode) {
|
|
|
43407
43673
|
|
|
43408
43674
|
// src/core/rag.ts
|
|
43409
43675
|
import { readFile as readFile15, writeFile as writeFile12, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
|
|
43410
|
-
import { existsSync as
|
|
43411
|
-
import { execFile as
|
|
43676
|
+
import { existsSync as existsSync15 } from "fs";
|
|
43677
|
+
import { execFile as execFile11, spawn as spawn10 } from "child_process";
|
|
43412
43678
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
43413
|
-
import { join as
|
|
43414
|
-
import { promisify as
|
|
43415
|
-
var run10 =
|
|
43679
|
+
import { join as join30 } from "path";
|
|
43680
|
+
import { promisify as promisify11 } from "util";
|
|
43681
|
+
var run10 = promisify11(execFile11);
|
|
43416
43682
|
var RAG_REL = ".claude/rag";
|
|
43417
43683
|
async function tryRun(cmd, args) {
|
|
43418
43684
|
try {
|
|
@@ -43431,7 +43697,7 @@ async function detectPython() {
|
|
|
43431
43697
|
}
|
|
43432
43698
|
async function installLog(root) {
|
|
43433
43699
|
try {
|
|
43434
|
-
const text = await readFile15(
|
|
43700
|
+
const text = await readFile15(join30(root, RAG_REL, "install.log"), "utf8");
|
|
43435
43701
|
return text.split("\n").filter((l) => l.length > 0).slice(-200);
|
|
43436
43702
|
} catch {
|
|
43437
43703
|
return [];
|
|
@@ -43493,14 +43759,14 @@ async function ragStatus(root) {
|
|
|
43493
43759
|
};
|
|
43494
43760
|
let modelName = "";
|
|
43495
43761
|
try {
|
|
43496
|
-
modelName = (await readFile15(
|
|
43762
|
+
modelName = (await readFile15(join30(root, RAG_REL, ".installed"), "utf8")).trim();
|
|
43497
43763
|
} catch {
|
|
43498
43764
|
modelName = "";
|
|
43499
43765
|
}
|
|
43500
43766
|
const modelPresent = modelName.length > 0;
|
|
43501
43767
|
return {
|
|
43502
43768
|
installed: fastembed && sqliteVec && modelPresent,
|
|
43503
|
-
installing:
|
|
43769
|
+
installing: existsSync15(join30(root, RAG_REL, ".installing")),
|
|
43504
43770
|
python: { found: py.found, version: py.version },
|
|
43505
43771
|
deps: { pip, fastembed, sqliteVec },
|
|
43506
43772
|
model: { present: modelPresent, name: modelName },
|
|
@@ -43511,9 +43777,9 @@ async function ragStatus(root) {
|
|
|
43511
43777
|
};
|
|
43512
43778
|
}
|
|
43513
43779
|
async function readIndexMeta(root) {
|
|
43514
|
-
const indexing =
|
|
43780
|
+
const indexing = existsSync15(join30(root, RAG_REL, ".indexing"));
|
|
43515
43781
|
try {
|
|
43516
|
-
const m2 = JSON.parse(await readFile15(
|
|
43782
|
+
const m2 = JSON.parse(await readFile15(join30(root, RAG_REL, ".index.json"), "utf8"));
|
|
43517
43783
|
return {
|
|
43518
43784
|
present: true,
|
|
43519
43785
|
indexing,
|
|
@@ -43580,14 +43846,14 @@ if __name__ == "__main__":
|
|
|
43580
43846
|
elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
|
|
43581
43847
|
`;
|
|
43582
43848
|
async function ensureEngine(root) {
|
|
43583
|
-
await mkdir11(
|
|
43584
|
-
await writeFile12(
|
|
43849
|
+
await mkdir11(join30(root, RAG_REL), { recursive: true }).catch(() => void 0);
|
|
43850
|
+
await writeFile12(join30(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
|
|
43585
43851
|
}
|
|
43586
43852
|
async function ragReindex(root) {
|
|
43587
43853
|
const py = await detectPython();
|
|
43588
43854
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
43589
|
-
if (!
|
|
43590
|
-
if (
|
|
43855
|
+
if (!existsSync15(join30(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
|
|
43856
|
+
if (existsSync15(join30(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
|
|
43591
43857
|
await ensureEngine(root);
|
|
43592
43858
|
const script = `
|
|
43593
43859
|
set +e
|
|
@@ -43595,7 +43861,7 @@ touch "${RAG_REL}/.indexing"
|
|
|
43595
43861
|
{ echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
|
|
43596
43862
|
rm -f "${RAG_REL}/.indexing"
|
|
43597
43863
|
`;
|
|
43598
|
-
const child =
|
|
43864
|
+
const child = spawn10("bash", ["-c", script], {
|
|
43599
43865
|
cwd: root,
|
|
43600
43866
|
env: { ...process.env, RAG_PY: py.bin },
|
|
43601
43867
|
detached: true,
|
|
@@ -43607,12 +43873,12 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
43607
43873
|
async function ragQuery(root, queryText, k2 = 8) {
|
|
43608
43874
|
const py = await detectPython();
|
|
43609
43875
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
43610
|
-
if (!
|
|
43876
|
+
if (!existsSync15(join30(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
43611
43877
|
await ensureEngine(root);
|
|
43612
43878
|
try {
|
|
43613
43879
|
const { stdout } = await run10(
|
|
43614
43880
|
py.bin,
|
|
43615
|
-
[
|
|
43881
|
+
[join30(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
|
|
43616
43882
|
{ cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
43617
43883
|
);
|
|
43618
43884
|
const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
|
|
@@ -43627,9 +43893,9 @@ async function ragInstall(root, model) {
|
|
|
43627
43893
|
}
|
|
43628
43894
|
const py = await detectPython();
|
|
43629
43895
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
43630
|
-
if (
|
|
43896
|
+
if (existsSync15(join30(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
43631
43897
|
try {
|
|
43632
|
-
await mkdir11(
|
|
43898
|
+
await mkdir11(join30(root, RAG_REL), { recursive: true });
|
|
43633
43899
|
} catch {
|
|
43634
43900
|
}
|
|
43635
43901
|
const script = `
|
|
@@ -43645,7 +43911,7 @@ rm -f "${RAG_REL}/.installed"
|
|
|
43645
43911
|
} >> "${RAG_REL}/install.log" 2>&1
|
|
43646
43912
|
rm -f "${RAG_REL}/.installing"
|
|
43647
43913
|
`;
|
|
43648
|
-
const child =
|
|
43914
|
+
const child = spawn10("bash", ["-c", script], {
|
|
43649
43915
|
cwd: root,
|
|
43650
43916
|
env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
|
|
43651
43917
|
detached: true,
|
|
@@ -43656,8 +43922,8 @@ rm -f "${RAG_REL}/.installing"
|
|
|
43656
43922
|
}
|
|
43657
43923
|
|
|
43658
43924
|
// src/core/config-sync.ts
|
|
43659
|
-
import { existsSync as
|
|
43660
|
-
import { join as
|
|
43925
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
|
|
43926
|
+
import { join as join31 } from "path";
|
|
43661
43927
|
var SERVER_MANAGED_KEYS = [
|
|
43662
43928
|
"physicPath",
|
|
43663
43929
|
"sessionSwitchPct",
|
|
@@ -43674,8 +43940,8 @@ var SERVER_MANAGED_KEYS = [
|
|
|
43674
43940
|
"projectAiMemoryBudgetChars"
|
|
43675
43941
|
];
|
|
43676
43942
|
function readConfigText(profileDir2) {
|
|
43677
|
-
const path =
|
|
43678
|
-
if (
|
|
43943
|
+
const path = join31(profileDir2, "config.json");
|
|
43944
|
+
if (existsSync16(path)) {
|
|
43679
43945
|
try {
|
|
43680
43946
|
return readFileSync16(path, "utf8");
|
|
43681
43947
|
} catch {
|
|
@@ -44464,7 +44730,7 @@ var WsClient = class {
|
|
|
44464
44730
|
}
|
|
44465
44731
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
44466
44732
|
const envelope = {
|
|
44467
|
-
id:
|
|
44733
|
+
id: randomUUID4(),
|
|
44468
44734
|
replyTo,
|
|
44469
44735
|
channel,
|
|
44470
44736
|
wsToken: this.wsToken,
|
|
@@ -44485,7 +44751,7 @@ var WsClient = class {
|
|
|
44485
44751
|
}
|
|
44486
44752
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
44487
44753
|
const envelope = {
|
|
44488
|
-
id:
|
|
44754
|
+
id: randomUUID4(),
|
|
44489
44755
|
replyTo: null,
|
|
44490
44756
|
channel,
|
|
44491
44757
|
wsToken: this.wsToken,
|
|
@@ -44552,8 +44818,8 @@ var WsClient = class {
|
|
|
44552
44818
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
44553
44819
|
ensurePhysicFolderPath(folder) {
|
|
44554
44820
|
try {
|
|
44555
|
-
if (!
|
|
44556
|
-
|
|
44821
|
+
if (!existsSync17(folder)) {
|
|
44822
|
+
mkdirSync8(folder, { recursive: true });
|
|
44557
44823
|
this.bus.log(t("project.createdFolder", { folder }));
|
|
44558
44824
|
}
|
|
44559
44825
|
} catch (err) {
|
|
@@ -44567,7 +44833,7 @@ var WsClient = class {
|
|
|
44567
44833
|
*/
|
|
44568
44834
|
physicFolderPath(projectName) {
|
|
44569
44835
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
44570
|
-
return safe ?
|
|
44836
|
+
return safe ? join32(this.context.profileDir, safe) : null;
|
|
44571
44837
|
}
|
|
44572
44838
|
/**
|
|
44573
44839
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -44582,7 +44848,7 @@ var WsClient = class {
|
|
|
44582
44848
|
projects: [],
|
|
44583
44849
|
version: CLI_VERSION,
|
|
44584
44850
|
physicPath,
|
|
44585
|
-
physicPathExists: physicPath ?
|
|
44851
|
+
physicPathExists: physicPath ? existsSync17(physicPath) : void 0,
|
|
44586
44852
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
44587
44853
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath, this.context.profileDir) : false,
|
|
44588
44854
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
@@ -44676,7 +44942,7 @@ var WsClient = class {
|
|
|
44676
44942
|
*/
|
|
44677
44943
|
uploadLog() {
|
|
44678
44944
|
try {
|
|
44679
|
-
const upload = readLogUpload(
|
|
44945
|
+
const upload = readLogUpload(join32(this.context.profileDir, "logs"));
|
|
44680
44946
|
if (!upload) return;
|
|
44681
44947
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
44682
44948
|
if (sig === this.lastLogSig) return;
|
|
@@ -44838,10 +45104,10 @@ var WsClient = class {
|
|
|
44838
45104
|
};
|
|
44839
45105
|
|
|
44840
45106
|
// src/core/instance-lock.ts
|
|
44841
|
-
import { readFileSync as readFileSync17, rmSync as
|
|
44842
|
-
import { join as
|
|
45107
|
+
import { readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
45108
|
+
import { join as join33 } from "path";
|
|
44843
45109
|
function lockPath(profileDir2) {
|
|
44844
|
-
return
|
|
45110
|
+
return join33(profileDir2, "start.lock");
|
|
44845
45111
|
}
|
|
44846
45112
|
function isAlive(pid) {
|
|
44847
45113
|
try {
|
|
@@ -44867,14 +45133,14 @@ function acquireInstanceLock(profileDir2) {
|
|
|
44867
45133
|
function releaseInstanceLock(profileDir2) {
|
|
44868
45134
|
try {
|
|
44869
45135
|
const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
|
|
44870
|
-
if (pid === process.pid)
|
|
45136
|
+
if (pid === process.pid) rmSync8(lockPath(profileDir2), { force: true });
|
|
44871
45137
|
} catch {
|
|
44872
45138
|
}
|
|
44873
45139
|
}
|
|
44874
45140
|
|
|
44875
45141
|
// src/core/session-bus.ts
|
|
44876
45142
|
import { EventEmitter } from "events";
|
|
44877
|
-
import { randomUUID as
|
|
45143
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
44878
45144
|
var REPLAY_CAP = 500;
|
|
44879
45145
|
var SessionBus = class {
|
|
44880
45146
|
emitter = new EventEmitter();
|
|
@@ -44905,7 +45171,7 @@ var SessionBus = class {
|
|
|
44905
45171
|
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
44906
45172
|
push(input) {
|
|
44907
45173
|
const entry = {
|
|
44908
|
-
id:
|
|
45174
|
+
id: randomUUID5(),
|
|
44909
45175
|
level: "info",
|
|
44910
45176
|
...input,
|
|
44911
45177
|
ts: Date.now()
|
|
@@ -45162,8 +45428,8 @@ var SessionBus = class {
|
|
|
45162
45428
|
|
|
45163
45429
|
// src/core/control-server.ts
|
|
45164
45430
|
import { createServer } from "net";
|
|
45165
|
-
import { chmodSync as chmodSync4, rmSync as
|
|
45166
|
-
import { join as
|
|
45431
|
+
import { chmodSync as chmodSync4, rmSync as rmSync9 } from "fs";
|
|
45432
|
+
import { join as join35 } from "path";
|
|
45167
45433
|
|
|
45168
45434
|
// src/core/control-protocol.ts
|
|
45169
45435
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -45194,9 +45460,9 @@ function createFrameParser() {
|
|
|
45194
45460
|
// src/core/control-token.ts
|
|
45195
45461
|
import { chmodSync as chmodSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
45196
45462
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
45197
|
-
import { join as
|
|
45463
|
+
import { join as join34 } from "path";
|
|
45198
45464
|
function controlTokenPath(profileDir2) {
|
|
45199
|
-
return
|
|
45465
|
+
return join34(profileDir2, CONTROL_TOKEN_FILE);
|
|
45200
45466
|
}
|
|
45201
45467
|
function writeControlToken(profileDir2) {
|
|
45202
45468
|
const token = randomBytes3(32).toString("hex");
|
|
@@ -45225,9 +45491,9 @@ function tokenMatches(expected, given) {
|
|
|
45225
45491
|
|
|
45226
45492
|
// src/core/control-server.ts
|
|
45227
45493
|
function startControlServer(bus, profileDir2, info) {
|
|
45228
|
-
const socketPath =
|
|
45494
|
+
const socketPath = join35(profileDir2, CONTROL_SOCKET_FILE);
|
|
45229
45495
|
try {
|
|
45230
|
-
|
|
45496
|
+
rmSync9(socketPath, { force: true });
|
|
45231
45497
|
} catch {
|
|
45232
45498
|
}
|
|
45233
45499
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -45313,8 +45579,8 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
45313
45579
|
clients.clear();
|
|
45314
45580
|
server.close();
|
|
45315
45581
|
try {
|
|
45316
|
-
|
|
45317
|
-
|
|
45582
|
+
rmSync9(socketPath, { force: true });
|
|
45583
|
+
rmSync9(controlTokenPath(profileDir2), { force: true });
|
|
45318
45584
|
} catch {
|
|
45319
45585
|
}
|
|
45320
45586
|
};
|
|
@@ -50977,15 +51243,15 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
50977
51243
|
var import_react26 = __toESM(require_react(), 1);
|
|
50978
51244
|
|
|
50979
51245
|
// src/core/input-history.ts
|
|
50980
|
-
import { existsSync as
|
|
50981
|
-
import { join as
|
|
51246
|
+
import { existsSync as existsSync19, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
|
|
51247
|
+
import { join as join36 } from "path";
|
|
50982
51248
|
var MAX_HISTORY = 200;
|
|
50983
51249
|
function historyPath(profileDir2) {
|
|
50984
|
-
return
|
|
51250
|
+
return join36(profileDir2, "input-history.json");
|
|
50985
51251
|
}
|
|
50986
51252
|
function loadInputHistory(profileDir2) {
|
|
50987
51253
|
const path = historyPath(profileDir2);
|
|
50988
|
-
if (!
|
|
51254
|
+
if (!existsSync19(path)) return [];
|
|
50989
51255
|
try {
|
|
50990
51256
|
const data = JSON.parse(readFileSync20(path, "utf8"));
|
|
50991
51257
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
@@ -51595,7 +51861,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
51595
51861
|
return;
|
|
51596
51862
|
}
|
|
51597
51863
|
logger.configure({
|
|
51598
|
-
dir:
|
|
51864
|
+
dir: join37(profileDir2, "logs"),
|
|
51599
51865
|
level: resolveLogLevel(),
|
|
51600
51866
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
51601
51867
|
});
|
|
@@ -51615,7 +51881,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
51615
51881
|
if (result.action === "updated") {
|
|
51616
51882
|
logger.info("update.updated", { version: result.version });
|
|
51617
51883
|
console.log(t("start.restarting"));
|
|
51618
|
-
const child =
|
|
51884
|
+
const child = spawn11(process.execPath, process.argv.slice(1), {
|
|
51619
51885
|
stdio: "inherit",
|
|
51620
51886
|
env: { ...process.env, FOURPM_NO_UPDATE: "1" }
|
|
51621
51887
|
});
|
|
@@ -51704,7 +51970,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
51704
51970
|
}
|
|
51705
51971
|
|
|
51706
51972
|
// src/commands/attach.ts
|
|
51707
|
-
import { join as
|
|
51973
|
+
import { join as join38 } from "path";
|
|
51708
51974
|
|
|
51709
51975
|
// src/core/control-client.ts
|
|
51710
51976
|
import { connect as connect2 } from "net";
|
|
@@ -51813,7 +52079,7 @@ async function runAttach(profileDir2, profileName) {
|
|
|
51813
52079
|
process.exitCode = 1;
|
|
51814
52080
|
return;
|
|
51815
52081
|
}
|
|
51816
|
-
const socketPath =
|
|
52082
|
+
const socketPath = join38(profileDir2, CONTROL_SOCKET_FILE);
|
|
51817
52083
|
const bus = new SessionBus();
|
|
51818
52084
|
let conn;
|
|
51819
52085
|
try {
|
|
@@ -51844,11 +52110,11 @@ async function runAttach(profileDir2, profileName) {
|
|
|
51844
52110
|
|
|
51845
52111
|
// src/commands/auto-run.ts
|
|
51846
52112
|
import { connect as connect3 } from "net";
|
|
51847
|
-
import { join as
|
|
52113
|
+
import { join as join39 } from "path";
|
|
51848
52114
|
var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
|
|
51849
52115
|
async function runAutoRun(profileDir2, profileName) {
|
|
51850
52116
|
initI18n(readProfileConfig(profileDir2).locale);
|
|
51851
|
-
const socketPath =
|
|
52117
|
+
const socketPath = join39(profileDir2, CONTROL_SOCKET_FILE);
|
|
51852
52118
|
const token = readControlToken(profileDir2);
|
|
51853
52119
|
const code = await new Promise((resolve10) => {
|
|
51854
52120
|
let settled = false;
|
|
@@ -51904,12 +52170,12 @@ async function runAutoRun(profileDir2, profileName) {
|
|
|
51904
52170
|
}
|
|
51905
52171
|
|
|
51906
52172
|
// src/commands/ai-login.ts
|
|
51907
|
-
import { spawn as
|
|
51908
|
-
import { mkdirSync as
|
|
52173
|
+
import { spawn as spawn12 } from "child_process";
|
|
52174
|
+
import { mkdirSync as mkdirSync9 } from "fs";
|
|
51909
52175
|
import { basename as basename8 } from "path";
|
|
51910
52176
|
function runProviderLogin(cmd, envVar, dir2) {
|
|
51911
52177
|
return new Promise((resolve10) => {
|
|
51912
|
-
const child =
|
|
52178
|
+
const child = spawn12(cmd, [], {
|
|
51913
52179
|
stdio: "inherit",
|
|
51914
52180
|
env: { ...process.env, [envVar]: dir2 }
|
|
51915
52181
|
});
|
|
@@ -51942,7 +52208,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
|
|
|
51942
52208
|
continue;
|
|
51943
52209
|
}
|
|
51944
52210
|
const dir2 = resolveHomePath(cred.profile);
|
|
51945
|
-
|
|
52211
|
+
mkdirSync9(dir2, { recursive: true });
|
|
51946
52212
|
console.log(t("aiLogin.loggingIn", { provider: cred.provider, profile: cred.profile, envVar, dir: dir2 }));
|
|
51947
52213
|
console.log(t("aiLogin.completeSignIn"));
|
|
51948
52214
|
const code = await runProviderLogin(cred.provider, envVar, dir2);
|
|
@@ -51954,7 +52220,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
|
|
|
51954
52220
|
}
|
|
51955
52221
|
|
|
51956
52222
|
// src/commands/unlink.ts
|
|
51957
|
-
import { rmSync as
|
|
52223
|
+
import { rmSync as rmSync10 } from "fs";
|
|
51958
52224
|
async function runUnlink(profileDir2, profileName) {
|
|
51959
52225
|
initI18n(readProfileConfig(profileDir2).locale);
|
|
51960
52226
|
const cred = readCredential(profileDir2, profileName);
|
|
@@ -51968,7 +52234,7 @@ async function runUnlink(profileDir2, profileName) {
|
|
|
51968
52234
|
} catch (err) {
|
|
51969
52235
|
console.log(t("unlink.revokeFailed", { message: err.message }));
|
|
51970
52236
|
}
|
|
51971
|
-
|
|
52237
|
+
rmSync10(profileDir2, { recursive: true, force: true });
|
|
51972
52238
|
clearDefaultProfileIf(profileName);
|
|
51973
52239
|
console.log(t("unlink.deleted"));
|
|
51974
52240
|
}
|