project-athena 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +99 -4
- package/dist/cli.js +1585 -395
- package/dist/web/assets/{Editor-DmFaONG7.js → Editor-CL6A82dQ.js} +1 -1
- package/dist/web/assets/{abnfDiagram-O67JEVCF-BEaDGLKl.js → abnfDiagram-O67JEVCF-BuFfjTfx.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-CGSEE8po.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-Cr_YqU4C.js → architectureDiagram-NJMV4G6O-BTsv87pA.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-NA64lNxM.js → blockDiagram-BEXU5L5S-CNXqyKIx.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-C9OZTI8T.js → c4Diagram-YGBWAQC7-kksFXQoJ.js} +1 -1
- package/dist/web/assets/channel-Dneho6u-.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-PivwegVV.js → chunk-3FUC2YCW-BY99XYBs.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-DH_dVKgx.js → chunk-5DYCD2WN-h-PRThMP.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-DTR6iTOw.js → chunk-742MDFTN-BIxfyBUj.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-XeJOzIDQ.js → chunk-7INBJB4K-DZtctDdk.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-Bn-cXg0l.js → chunk-7M6MHVWA-BX9fUUbL.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-CPe5zfLc.js → chunk-7PRAP22T-X1s3e81n.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-DSn-mpqy.js → chunk-GTNCS2PH-Bh8eluU-.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-DE4DJkth.js → chunk-GWA4HPMP-D1KATAi0.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-9uaOqseW.js → chunk-MBY4JIJT-CEErv5kP.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-bxqe7jPe.js → chunk-NETBCI7D-BXiH4aPa.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-CjZQSnt8.js → chunk-O7XYJQB3-Dede62yi.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-BfYbPxwY.js → chunk-UA2S7LBM-KZ9aWRTa.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-CT8bG5L7.js → chunk-WEXAMYUT-xf-3L2zU.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-CHWuLoFG.js → chunk-Z7XXMR3K-DLpRdZ8n.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-C6SOqBs6.js → chunk-ZIGJFQKS-BdJ0_J-U.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CrSxkUNF.js → classDiagram-v2-NBCMYWYE-C5VzGDGq.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-CMbTYwga.js → cynefin-OW5HDTMX-BqOgbE7U.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-B8wb03CF.js → cynefinDiagram-VND7K2PF-CCC_feG5.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-Bz-BHtjW.js → dagre-6A5THRUB-SLP-94sd.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-C9M-mcex.js → diagram-22UHCM2B-DpHwEBYM.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-q18RgATq.js → diagram-3UASUU5V-iGFwuMEk.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-zywdDXPc.js → diagram-ATOU4E4O-Csx9hj0z.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-BGMdofKO.js → diagram-CDSNMT55-yGKrLRO5.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-BdT0J6Xl.js → diagram-MLGK6HIB-mDpCormD.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-BXdFmdOp.js → diagram-MPIPVDR6-BJHdatrF.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-B7Mz8zd0.js → ebnfDiagram-ZINNZB2B-ClJbCBBu.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-CQ4lVw-r.js → elk-276RUBZZ-C5tFZMYm.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-DmmZBLvC.js → erDiagram-OPXOYQCR-u7_UFZot.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-CA-vzJb3.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-Ca_1cbbS.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-B0egPzgH.js → ganttDiagram-FUAMR5RP-B4hrLcLf.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-BdShI4JY.js → gitGraph-4MIJSDKK-Dxx-Bl-U.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-jwG6nUmP.js → gitGraphDiagram-X574FWY7-DKjDV5Kr.js} +1 -1
- package/dist/web/assets/{index-BSiW3yI1.js → index-s6lwoj84.js} +2 -2
- package/dist/web/assets/{info-A6RAGUB7-Hm3KO-lX.js → info-A6RAGUB7-CgqIdgQA.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-CdcggvNW.js → infoDiagram-VRGFBTTK-D-qjWkwy.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-C8x3tV6X.js → ishikawaDiagram-OU5B5YK6-CzpqV6WN.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CLXIkiwl.js → journeyDiagram-ZHPQQLJL-BBnOUDzw.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-P94klDAy.js → kanban-definition-PNTS6WVX-Di9sTFB1.js} +1 -1
- package/dist/web/assets/{line-DtGZCXZs.js → line-B8ehGiwF.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Du2mcMMx.js → mermaid-parser.core-DWsT24e5.js} +3 -3
- package/dist/web/assets/{mermaid.core-B4zgSyXF.js → mermaid.core-DzYpaVzI.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-C0EF7lWr.js → mindmap-definition-NLK3R4M7-CxagBsH3.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-SJ1tKR5t.js → packet-AYTQ26CC-CmxbS0qf.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-Dek_S7Z0.js → pegDiagram-GJSIUBJH-_3jnD3q8.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-C0wgWLu4.js → pie-WAS4IAKB-DppCWOqP.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-Dy1jV_P-.js → pieDiagram-5QR66LMP-DTVThTqz.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-Df6Jl4OG.js → quadrantDiagram-O4NWA36T-BJ_OFnht.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BCtk017a.js → radar-RG4KPBEZ-B6quSMjs.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-_ZAooQok.js → railroad-74A4TZTK-CErU7RG3.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CWsEvTRR.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cyq9ELNn.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-BX1hI1hV.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-DNraioKj.js → railroadDiagram-XR7U4H2S-Zu0N_D-D.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-C09s8xl6.js → requirementDiagram-PLB6GJNP-Bl6CN3ul.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-cwVwdash.js → sankeyDiagram-IPEJSGJF-DdpKzZV8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-Uz6m2m-d.js → sequenceDiagram-PO4LG4MO-BwUqrl9F.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-DASY_MN7.js → stateDiagram-v2-GCMORJYK-C3vZGpAi.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-DjbxDj0Z.js → swimlanes-2SLR337P-BjoMa2HW.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BGNz1HGG.js +8 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-Dffxj5AG.js → timeline-definition-EJHVYXUP-0apcf2l2.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-CXAvuG6A.js → treeView-Q6P3EWNA-BOPWzyg3.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-D_BSE2GX.js → treemap-WGGIJYW6-CC6rU6dD.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-CKcfbpDB.js → usecaseDiagram-POWQR4AR-BFclVNBG.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-DkJknm_I.js → vennDiagram-UO4OBE2U-D2XPv-zb.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-Drx6WB-t.js → wardley-WFR3VGLG-Btz6ID8P.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-fdjd5am3.js → wardleyDiagram-VNRHLVJA-DFYPvGpS.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-DCWTWMv0.js → xychartDiagram-PMCCYNJV-DMkQD-sV.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +12 -2
- package/dist/cli.js.map +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-CHNxu4zI.js +0 -1
- package/dist/web/assets/channel-D_T5GGJX.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BgPhBkRf.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-C3wNriu7.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-Bacbbg7g.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cb23oOsU.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-EPh5LyCw.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-Bv90z5my.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,10 @@ import { Command as Command2, Option } from "commander";
|
|
|
6
6
|
// src/services/version.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
var PACKAGE_NAME = "project-athena";
|
|
9
|
+
var AUTHOR = "Sabarivasan";
|
|
10
|
+
var NPM_URL = `https://www.npmjs.com/package/${PACKAGE_NAME}`;
|
|
11
|
+
var REPO_URL = "https://github.com/Sabari-Vasan-SM/athena";
|
|
12
|
+
var WEBSITE_URL = "https://athena.sabari.me";
|
|
9
13
|
function readVersion() {
|
|
10
14
|
try {
|
|
11
15
|
const require2 = createRequire(import.meta.url);
|
|
@@ -91,33 +95,6 @@ function json(value) {
|
|
|
91
95
|
out.write(`${JSON.stringify(value, null, 2)}
|
|
92
96
|
`);
|
|
93
97
|
}
|
|
94
|
-
function banner() {
|
|
95
|
-
if (jsonMode || quiet) return;
|
|
96
|
-
const title = "ATHENA";
|
|
97
|
-
const sub = "Project Intelligence for AI Coding";
|
|
98
|
-
const w = sub.length + 4;
|
|
99
|
-
const pad = (s) => {
|
|
100
|
-
const left = Math.floor((w - s.length) / 2);
|
|
101
|
-
return " ".repeat(left) + s + " ".repeat(w - s.length - left);
|
|
102
|
-
};
|
|
103
|
-
if (unicode) {
|
|
104
|
-
line(c.dim(`\u256D${"\u2500".repeat(w)}\u256E`));
|
|
105
|
-
line(`${c.dim("\u2502")}${c.bold(c.cyan(pad(title)))}${c.dim("\u2502")}`);
|
|
106
|
-
line(`${c.dim("\u2502")}${c.dim(pad(sub))}${c.dim("\u2502")}`);
|
|
107
|
-
line(c.dim(`\u2570${"\u2500".repeat(w)}\u256F`));
|
|
108
|
-
} else {
|
|
109
|
-
line(c.bold(c.cyan(title)));
|
|
110
|
-
line(c.dim(sub));
|
|
111
|
-
}
|
|
112
|
-
line();
|
|
113
|
-
}
|
|
114
|
-
function tree(rootLabel, items) {
|
|
115
|
-
line(rootLabel);
|
|
116
|
-
items.forEach((item, i) => {
|
|
117
|
-
const last = i === items.length - 1;
|
|
118
|
-
line(`${c.dim(unicode ? last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : last ? "`-- " : "|-- ")}${item}`);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
98
|
var FRAMES = unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
|
|
122
99
|
function spinner(text) {
|
|
123
100
|
let current = text;
|
|
@@ -174,7 +151,7 @@ function relativeTime(iso) {
|
|
|
174
151
|
}
|
|
175
152
|
|
|
176
153
|
// src/cli/commands/init.ts
|
|
177
|
-
import
|
|
154
|
+
import path26 from "path";
|
|
178
155
|
|
|
179
156
|
// src/core/state/state.ts
|
|
180
157
|
import { promises as fs2 } from "fs";
|
|
@@ -378,8 +355,8 @@ function diffFileIndex(prev, next) {
|
|
|
378
355
|
}
|
|
379
356
|
|
|
380
357
|
// src/agents/registry.ts
|
|
381
|
-
import { promises as
|
|
382
|
-
import
|
|
358
|
+
import { promises as fs10 } from "fs";
|
|
359
|
+
import path15 from "path";
|
|
383
360
|
|
|
384
361
|
// src/core/util/paths.ts
|
|
385
362
|
import path4 from "path";
|
|
@@ -390,9 +367,9 @@ function resolveInside(root, candidate) {
|
|
|
390
367
|
if (candidate.includes("\0")) return null;
|
|
391
368
|
const resolvedRoot = path4.resolve(root);
|
|
392
369
|
const resolved = path4.resolve(resolvedRoot, candidate);
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
if (
|
|
370
|
+
const rel2 = path4.relative(resolvedRoot, resolved);
|
|
371
|
+
if (rel2 === "") return resolved;
|
|
372
|
+
if (rel2.startsWith("..") || path4.isAbsolute(rel2)) return null;
|
|
396
373
|
return resolved;
|
|
397
374
|
}
|
|
398
375
|
function dirOf(posixPath) {
|
|
@@ -601,22 +578,22 @@ async function readJsonFile(file) {
|
|
|
601
578
|
function isAthenaEntry(entry) {
|
|
602
579
|
return JSON.stringify(entry ?? "").includes(ATHENA_MARK);
|
|
603
580
|
}
|
|
604
|
-
async function planJsonChange(root,
|
|
605
|
-
const file = path5.join(root,
|
|
581
|
+
async function planJsonChange(root, rel2, mutate2) {
|
|
582
|
+
const file = path5.join(root, rel2);
|
|
606
583
|
const { data, existed, malformed } = await readJsonFile(file);
|
|
607
|
-
if (malformed) throw new Error(`${
|
|
584
|
+
if (malformed) throw new Error(`${rel2} is not valid JSON \u2014 fix it before configuring hooks`);
|
|
608
585
|
const before = JSON.stringify(data);
|
|
609
586
|
mutate2(data);
|
|
610
587
|
const content = `${JSON.stringify(data, null, 2)}
|
|
611
588
|
`;
|
|
612
|
-
return { path:
|
|
589
|
+
return { path: rel2, ownership: "block", action: !existed ? "create" : JSON.stringify(data) === before ? "unchanged" : "update", content };
|
|
613
590
|
}
|
|
614
|
-
async function removeJsonHooks(root,
|
|
615
|
-
const file = path5.join(root,
|
|
591
|
+
async function removeJsonHooks(root, rel2, strip2, isEmpty) {
|
|
592
|
+
const file = path5.join(root, rel2);
|
|
616
593
|
const { data, existed, malformed } = await readJsonFile(file);
|
|
617
594
|
if (!existed || malformed) return false;
|
|
618
595
|
const before = JSON.stringify(data);
|
|
619
|
-
|
|
596
|
+
strip2(data);
|
|
620
597
|
if (JSON.stringify(data) === before) return false;
|
|
621
598
|
if (isEmpty(data)) await fs3.rm(file, { force: true });
|
|
622
599
|
else await fs3.writeFile(file, `${JSON.stringify(data, null, 2)}
|
|
@@ -633,7 +610,7 @@ function hookScript(agent, platform = process.platform) {
|
|
|
633
610
|
if (platform === "win32") {
|
|
634
611
|
return {
|
|
635
612
|
file: "athena-hook.cmd",
|
|
636
|
-
content: ["@echo off", `${command} event ${ATHENA_MARK} --agent ${agent} --hook %1`, "exit /b 0", ""].join("\r\n"),
|
|
613
|
+
content: ["@echo off", "rem Generated by Athena: forwards agent hook payloads to `athena event`.", `${command} event ${ATHENA_MARK} --agent ${agent} --hook %1`, "exit /b 0", ""].join("\r\n"),
|
|
637
614
|
mode: 420
|
|
638
615
|
};
|
|
639
616
|
}
|
|
@@ -660,20 +637,20 @@ function stripMcpServers(data) {
|
|
|
660
637
|
|
|
661
638
|
// src/agents/common/files.ts
|
|
662
639
|
var OWNED_HEADER = "<!-- Generated by Athena. This file is owned by Athena and may be rewritten by `athena init`/`athena agents`. -->";
|
|
663
|
-
async function planBlock(root,
|
|
664
|
-
const existing = await readTextIfExists(path6.join(root,
|
|
640
|
+
async function planBlock(root, rel2, body) {
|
|
641
|
+
const existing = await readTextIfExists(path6.join(root, rel2));
|
|
665
642
|
const content = upsertIntegrationBlock(existing, body);
|
|
666
|
-
return { path:
|
|
643
|
+
return { path: rel2, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
|
|
667
644
|
}
|
|
668
|
-
async function planOwned(root,
|
|
669
|
-
const existing = await readTextIfExists(path6.join(root,
|
|
645
|
+
async function planOwned(root, rel2, content) {
|
|
646
|
+
const existing = await readTextIfExists(path6.join(root, rel2));
|
|
670
647
|
if (existing !== null && !existing.includes("Generated by Athena")) {
|
|
671
|
-
return { path:
|
|
648
|
+
return { path: rel2, ownership: "owned", action: "unchanged", content: existing };
|
|
672
649
|
}
|
|
673
|
-
return { path:
|
|
650
|
+
return { path: rel2, ownership: "owned", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
|
|
674
651
|
}
|
|
675
|
-
async function removeBlockFrom(root,
|
|
676
|
-
const file = path6.join(root,
|
|
652
|
+
async function removeBlockFrom(root, rel2) {
|
|
653
|
+
const file = path6.join(root, rel2);
|
|
677
654
|
const existing = await readTextIfExists(file);
|
|
678
655
|
if (existing === null || !existing.includes(INTEGRATION_START)) return false;
|
|
679
656
|
const next = removeIntegrationBlock(existing);
|
|
@@ -681,36 +658,40 @@ async function removeBlockFrom(root, rel) {
|
|
|
681
658
|
else await fs4.writeFile(file, next, "utf8");
|
|
682
659
|
return true;
|
|
683
660
|
}
|
|
684
|
-
async function removeOwned(root,
|
|
685
|
-
const file = path6.join(root,
|
|
661
|
+
async function removeOwned(root, rel2) {
|
|
662
|
+
const file = path6.join(root, rel2);
|
|
686
663
|
const existing = await readTextIfExists(file);
|
|
687
664
|
if (existing === null || !existing.includes("Generated by Athena")) return false;
|
|
688
665
|
await fs4.rm(file);
|
|
689
666
|
return true;
|
|
690
667
|
}
|
|
691
|
-
async function checkBlock(root,
|
|
692
|
-
const text = await readTextIfExists(path6.join(root,
|
|
693
|
-
if (text === null) return { ok: false, level: "warn", message: `${label}: ${
|
|
668
|
+
async function checkBlock(root, rel2, label) {
|
|
669
|
+
const text = await readTextIfExists(path6.join(root, rel2));
|
|
670
|
+
if (text === null) return { ok: false, level: "warn", message: `${label}: ${rel2} not found (run \`athena init\` or \`athena analyze\`)` };
|
|
694
671
|
const s = text.indexOf(INTEGRATION_START);
|
|
695
672
|
const e = text.indexOf(INTEGRATION_END);
|
|
696
|
-
if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${
|
|
697
|
-
if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${
|
|
698
|
-
return { ok: true, level: "ok", message: `${label}: ${
|
|
673
|
+
if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${rel2} has no Athena block` };
|
|
674
|
+
if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${rel2} has a malformed Athena block (unmatched markers)` };
|
|
675
|
+
return { ok: true, level: "ok", message: `${label}: ${rel2}` };
|
|
699
676
|
}
|
|
700
677
|
async function userEvidence(root, spec) {
|
|
701
678
|
const found = [];
|
|
702
|
-
const owned = new Set(
|
|
703
|
-
for (const
|
|
704
|
-
|
|
679
|
+
const owned = /* @__PURE__ */ new Set();
|
|
680
|
+
for (const rel2 of spec.athenaOwned ?? []) {
|
|
681
|
+
const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
|
|
682
|
+
if (text !== null && /Generated by Athena|managed by Athena/.test(text)) owned.add(rel2);
|
|
683
|
+
}
|
|
684
|
+
for (const rel2 of spec.athenaOwnedJson ?? []) {
|
|
685
|
+
if (await jsonOnlyAthena(path6.join(root, rel2))) owned.add(rel2);
|
|
705
686
|
}
|
|
706
|
-
for (const
|
|
707
|
-
const text = await readTextIfExists(path6.join(root,
|
|
687
|
+
for (const rel2 of spec.files ?? []) {
|
|
688
|
+
const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
|
|
708
689
|
if (text === null) continue;
|
|
709
690
|
if (text.includes(OWNED_HEADER)) continue;
|
|
710
|
-
if (removeIntegrationBlock(text).trim()) found.push(
|
|
691
|
+
if (removeIntegrationBlock(text).trim()) found.push(rel2);
|
|
711
692
|
}
|
|
712
|
-
for (const
|
|
713
|
-
if (await hasNonAthenaEntries(path6.join(root,
|
|
693
|
+
for (const rel2 of spec.dirs ?? []) {
|
|
694
|
+
if (await hasNonAthenaEntries(path6.join(root, rel2), root, owned)) found.push(rel2);
|
|
714
695
|
}
|
|
715
696
|
return found;
|
|
716
697
|
}
|
|
@@ -723,8 +704,8 @@ async function hasNonAthenaEntries(dir, root, owned) {
|
|
|
723
704
|
}
|
|
724
705
|
for (const e of entries) {
|
|
725
706
|
const abs = path6.join(dir, e.name);
|
|
726
|
-
const
|
|
727
|
-
if (owned.has(
|
|
707
|
+
const rel2 = path6.relative(root, abs).split(path6.sep).join("/");
|
|
708
|
+
if (owned.has(rel2)) continue;
|
|
728
709
|
if (e.isDirectory()) {
|
|
729
710
|
if (await hasNonAthenaEntries(abs, root, owned)) return true;
|
|
730
711
|
} else return true;
|
|
@@ -741,24 +722,26 @@ async function jsonOnlyAthena(file) {
|
|
|
741
722
|
return false;
|
|
742
723
|
}
|
|
743
724
|
const record = data;
|
|
744
|
-
const servers = record?.mcpServers;
|
|
745
725
|
let hadAthenaMcp = false;
|
|
746
|
-
|
|
747
|
-
|
|
726
|
+
for (const key of ["mcpServers", "servers"]) {
|
|
727
|
+
const servers = record?.[key];
|
|
728
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
|
|
729
|
+
if (!("athena" in servers)) continue;
|
|
730
|
+
hadAthenaMcp = true;
|
|
748
731
|
delete servers.athena;
|
|
749
|
-
if (Object.keys(servers).length === 0) delete record
|
|
732
|
+
if (Object.keys(servers).length === 0) delete record[key];
|
|
750
733
|
}
|
|
751
734
|
if (!text.includes(ATHENA_MARK) && !hadAthenaMcp) return false;
|
|
752
|
-
const
|
|
735
|
+
const strip2 = (value) => {
|
|
753
736
|
if (Array.isArray(value)) {
|
|
754
|
-
const kept = value.filter((v) => !JSON.stringify(v ?? "").includes(ATHENA_MARK)).map(
|
|
737
|
+
const kept = value.filter((v) => !JSON.stringify(v ?? "").includes(ATHENA_MARK)).map(strip2);
|
|
755
738
|
return kept.length ? kept : void 0;
|
|
756
739
|
}
|
|
757
740
|
if (value && typeof value === "object") {
|
|
758
741
|
const out2 = {};
|
|
759
742
|
for (const [k, v] of Object.entries(value)) {
|
|
760
743
|
if (JSON.stringify(v ?? "").includes(ATHENA_MARK)) {
|
|
761
|
-
const cleaned =
|
|
744
|
+
const cleaned = strip2(v);
|
|
762
745
|
if (cleaned !== void 0) out2[k] = cleaned;
|
|
763
746
|
continue;
|
|
764
747
|
}
|
|
@@ -769,15 +752,16 @@ async function jsonOnlyAthena(file) {
|
|
|
769
752
|
}
|
|
770
753
|
return value;
|
|
771
754
|
};
|
|
772
|
-
return
|
|
755
|
+
return strip2(data) === void 0;
|
|
773
756
|
}
|
|
774
757
|
|
|
775
758
|
// src/agents/common/hook-events.ts
|
|
776
759
|
import crypto2 from "crypto";
|
|
777
760
|
var TEST_COMMAND = /\b(test|vitest|jest|pytest|go test|cargo test|mocha|rspec|phpunit|gradle test|mvn test|tox|playwright|cypress)\b/i;
|
|
778
761
|
var REVIEW_COMMAND = /\b(lint|eslint|ruff|clippy|tsc|typecheck|type-check|mypy|flake8|rubocop|golangci-lint|audit)\b/i;
|
|
779
|
-
var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace"]);
|
|
780
|
-
var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "create_diff", "str_replace"]);
|
|
762
|
+
var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace", "read_file", "read_many_files", "list_directory", "grep_search", "web_fetch", "google_web_search"]);
|
|
763
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "apply_patch", "create_diff", "str_replace", "write_file", "replace"]);
|
|
764
|
+
var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "glob", "grep_search"]);
|
|
781
765
|
function truncate(s, n = 160) {
|
|
782
766
|
const one = s.replace(/\s+/g, " ").trim();
|
|
783
767
|
return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
|
|
@@ -803,18 +787,26 @@ function filesFrom(input, payload) {
|
|
|
803
787
|
}
|
|
804
788
|
return [...new Set(out2)];
|
|
805
789
|
}
|
|
790
|
+
function patchFiles(patch) {
|
|
791
|
+
const out2 = [];
|
|
792
|
+
for (const m of patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move to: (.+)$/gm)) {
|
|
793
|
+
const f = (m[1] ?? m[2]).trim();
|
|
794
|
+
if (f) out2.push(f);
|
|
795
|
+
}
|
|
796
|
+
return [...new Set(out2)];
|
|
797
|
+
}
|
|
806
798
|
function classifyTool(tool, command, files) {
|
|
807
799
|
const t = tool.toLowerCase().replace(/^mcp__/, "");
|
|
808
800
|
const where = files.length ? ` ${files.slice(0, 2).join(", ")}${files.length > 2 ? ` +${files.length - 2}` : ""}` : "";
|
|
809
801
|
if (EDIT_TOOLS.has(t)) return { kind: "edit", state: "CODING", message: `Editing${where || " files"}` };
|
|
810
|
-
if (READ_TOOLS.has(t)) return { kind: t
|
|
811
|
-
if (t === "bash" || t === "shell" || t.includes("terminal")) {
|
|
802
|
+
if (READ_TOOLS.has(t)) return { kind: SEARCH_TOOLS.has(t) ? "search" : "read", state: "ANALYZING", message: `Reading${where || ` (${tool})`}` };
|
|
803
|
+
if (t === "bash" || t === "shell" || t === "powershell" || t === "run_shell_command" || t.includes("terminal")) {
|
|
812
804
|
if (command && TEST_COMMAND.test(command)) return { kind: "command", state: "TESTING", message: `Running tests: ${command}` };
|
|
813
805
|
if (command && REVIEW_COMMAND.test(command)) return { kind: "command", state: "REVIEWING", message: `Running checks: ${command}` };
|
|
814
806
|
return { kind: "command", state: "CODING", message: command ? `Running: ${command}` : "Running a shell command" };
|
|
815
807
|
}
|
|
816
808
|
if (t === "task" || t === "agent") return { kind: "subagent", state: "PLANNING", message: "Delegating to a subagent" };
|
|
817
|
-
if (t === "todowrite" || t === "exitplanmode" || t === "plan") return { kind: "tool", state: "PLANNING", message: "Updating its plan" };
|
|
809
|
+
if (t === "todowrite" || t === "exitplanmode" || t === "plan" || t === "update_plan" || t === "write_todos" || t === "enter_plan_mode" || t === "exit_plan_mode") return { kind: "tool", state: "PLANNING", message: "Updating its plan" };
|
|
818
810
|
return { kind: "tool", state: "CODING", message: `Using ${tool}` };
|
|
819
811
|
}
|
|
820
812
|
function normalizeHookEvent(agent, hookName, payload) {
|
|
@@ -823,22 +815,23 @@ function normalizeHookEvent(agent, hookName, payload) {
|
|
|
823
815
|
const session = asString(payload.session_id) ?? asString(payload.conversation_id) ?? null;
|
|
824
816
|
const toolInput = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : null;
|
|
825
817
|
const tool = asString(payload.tool_name) ?? asString(payload.tool) ?? null;
|
|
818
|
+
const isPatch = tool?.toLowerCase() === "apply_patch";
|
|
826
819
|
const rawCommand = asString(toolInput?.command) ?? asString(payload.command);
|
|
827
|
-
const command = rawCommand ? truncate(rawCommand) : null;
|
|
828
|
-
const files = filesFrom(toolInput, payload);
|
|
820
|
+
const command = rawCommand && !isPatch ? truncate(rawCommand) : null;
|
|
821
|
+
const files = isPatch && rawCommand ? patchFiles(rawCommand) : filesFrom(toolInput, payload);
|
|
829
822
|
let c2 = null;
|
|
830
823
|
if (h === "sessionstart" || h === "workspaceopen") c2 = { kind: "session-start", state: "IDLE", message: "Session started" };
|
|
831
824
|
else if (h === "sessionend") c2 = { kind: "session-end", state: "IDLE", message: "Session ended" };
|
|
832
|
-
else if (h === "userpromptsubmit" || h === "beforesubmitprompt") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
|
|
833
|
-
else if (h === "stop" || h === "afteragentresponse") c2 = { kind: "stop", state: "SUCCESS", message: "Finished responding" };
|
|
825
|
+
else if (h === "userpromptsubmit" || h === "beforesubmitprompt" || h === "beforeagent") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
|
|
826
|
+
else if (h === "stop" || h === "afteragentresponse" || h === "afteragent") c2 = { kind: "stop", state: "SUCCESS", message: "Finished responding" };
|
|
834
827
|
else if (h === "subagentstart") c2 = { kind: "subagent", state: "PLANNING", message: "Subagent started" };
|
|
835
828
|
else if (h === "subagentstop") c2 = { kind: "subagent", state: "CODING", message: "Subagent finished" };
|
|
836
829
|
else if (h === "beforeshellexecution") c2 = classifyTool("bash", command, files);
|
|
837
830
|
else if (h === "aftershellexecution") return null;
|
|
838
831
|
else if (h === "beforereadfile" || h === "beforetabfileread") c2 = classifyTool("read", null, files);
|
|
839
832
|
else if (h === "afterfileedit" || h === "aftertabfileedit") c2 = classifyTool("edit", null, files);
|
|
840
|
-
else if (h === "pretooluse" || h === "permissionrequest") c2 = tool ? classifyTool(tool, command, files) : null;
|
|
841
|
-
else if (h === "posttooluse" || h === "posttoolusefailure") return null;
|
|
833
|
+
else if (h === "pretooluse" || h === "permissionrequest" || h === "beforetool") c2 = tool ? classifyTool(tool, command, files) : null;
|
|
834
|
+
else if (h === "posttooluse" || h === "posttoolusefailure" || h === "aftertool") return null;
|
|
842
835
|
else if (h === "pretoolusefailure") return null;
|
|
843
836
|
else c2 = { kind: "other", state: "CODING", message: `Hook: ${hook}` };
|
|
844
837
|
if (!c2) return null;
|
|
@@ -857,7 +850,10 @@ function normalizeHookEvent(agent, hookName, payload) {
|
|
|
857
850
|
};
|
|
858
851
|
}
|
|
859
852
|
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
|
|
853
|
+
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
|
|
860
854
|
var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "beforeShellExecution", "afterFileEdit", "stop", "sessionEnd"];
|
|
855
|
+
var COPILOT_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"];
|
|
856
|
+
var GEMINI_HOOK_EVENTS = ["SessionStart", "BeforeAgent", "BeforeTool", "AfterAgent", "SessionEnd"];
|
|
861
857
|
|
|
862
858
|
// src/agents/claude-code/adapter.ts
|
|
863
859
|
var SETTINGS = ".claude/settings.json";
|
|
@@ -1013,7 +1009,7 @@ var antigravityAdapter = {
|
|
|
1013
1009
|
capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: true },
|
|
1014
1010
|
supportNote: 'Uses an Athena-owned workspace rule at .agents/rules/athena.md. Set its activation to "Always On" in Antigravity if it is not applied automatically.',
|
|
1015
1011
|
async detectPresence(root) {
|
|
1016
|
-
const evidence = await userEvidence(root, {
|
|
1012
|
+
const evidence = await userEvidence(root, { dirs: [".agents", ".agent"], athenaOwned: [RULE_FILE2] });
|
|
1017
1013
|
return { detectedInProject: evidence.length > 0, evidence };
|
|
1018
1014
|
},
|
|
1019
1015
|
async plan(ctx) {
|
|
@@ -1035,6 +1031,450 @@ var antigravityAdapter = {
|
|
|
1035
1031
|
}
|
|
1036
1032
|
};
|
|
1037
1033
|
|
|
1034
|
+
// src/agents/codex/adapter.ts
|
|
1035
|
+
import { promises as fs6 } from "fs";
|
|
1036
|
+
import os from "os";
|
|
1037
|
+
import path10 from "path";
|
|
1038
|
+
import { parse as parseToml } from "smol-toml";
|
|
1039
|
+
var INSTRUCTIONS = "AGENTS.md";
|
|
1040
|
+
var CONFIG = ".codex/config.toml";
|
|
1041
|
+
var HOOKS_FILE2 = ".codex/hooks.json";
|
|
1042
|
+
var TOML_START = "# athena:start (managed by Athena; `athena agents remove codex` removes it)";
|
|
1043
|
+
var TOML_END = "# athena:end";
|
|
1044
|
+
var codexAdapter = {
|
|
1045
|
+
id: "codex",
|
|
1046
|
+
displayName: "Codex",
|
|
1047
|
+
capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
|
|
1048
|
+
supportNote: "Uses the Athena block in AGENTS.md, an MCP server entry in .codex/config.toml, and hooks in .codex/hooks.json that report activity. Codex loads .codex/ only for trusted projects.",
|
|
1049
|
+
async detectPresence(root) {
|
|
1050
|
+
const athenaOwned = await tomlOnlyAthena(path10.join(root, CONFIG)) ? [CONFIG] : [];
|
|
1051
|
+
const evidence = await userEvidence(root, { dirs: [".codex"], athenaOwned, athenaOwnedJson: [HOOKS_FILE2] });
|
|
1052
|
+
return { detectedInProject: evidence.length > 0, evidence };
|
|
1053
|
+
},
|
|
1054
|
+
async plan(ctx) {
|
|
1055
|
+
const changes = [await planBlock(ctx.root, INSTRUCTIONS, athenaInstructions({ heading: "##" }))];
|
|
1056
|
+
changes.push(await planConfig(ctx.root));
|
|
1057
|
+
changes.push(
|
|
1058
|
+
await planJsonChange(ctx.root, HOOKS_FILE2, (data) => {
|
|
1059
|
+
const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
|
|
1060
|
+
for (const event of CODEX_HOOK_EVENTS) {
|
|
1061
|
+
const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
|
|
1062
|
+
list.push({ hooks: [{ type: "command", command: codexHookCommand(event), timeout: 10 }] });
|
|
1063
|
+
hooks[event] = list;
|
|
1064
|
+
}
|
|
1065
|
+
data.hooks = hooks;
|
|
1066
|
+
})
|
|
1067
|
+
);
|
|
1068
|
+
return changes;
|
|
1069
|
+
},
|
|
1070
|
+
async remove(root) {
|
|
1071
|
+
const touched = [];
|
|
1072
|
+
if (await removeBlockFrom(root, INSTRUCTIONS)) touched.push(INSTRUCTIONS);
|
|
1073
|
+
if (await removeConfigBlock(root)) touched.push(CONFIG);
|
|
1074
|
+
const removed = await removeJsonHooks(
|
|
1075
|
+
root,
|
|
1076
|
+
HOOKS_FILE2,
|
|
1077
|
+
(data) => {
|
|
1078
|
+
const hooks = data.hooks;
|
|
1079
|
+
if (!hooks) return;
|
|
1080
|
+
for (const event of Object.keys(hooks)) {
|
|
1081
|
+
hooks[event] = (hooks[event] ?? []).filter((e) => !isAthenaEntry(e));
|
|
1082
|
+
if (!hooks[event].length) delete hooks[event];
|
|
1083
|
+
}
|
|
1084
|
+
pruneEmpty(data, "hooks");
|
|
1085
|
+
},
|
|
1086
|
+
(data) => Object.keys(data).length === 0
|
|
1087
|
+
);
|
|
1088
|
+
if (removed) touched.push(HOOKS_FILE2);
|
|
1089
|
+
return touched;
|
|
1090
|
+
},
|
|
1091
|
+
async check(root) {
|
|
1092
|
+
const checks = [await checkBlock(root, INSTRUCTIONS, "Codex")];
|
|
1093
|
+
const config = await readTextIfExists(path10.join(root, CONFIG));
|
|
1094
|
+
checks.push(
|
|
1095
|
+
config?.includes(TOML_START) && config.includes("[mcp_servers.athena]") ? { ok: true, level: "ok", message: `Codex: MCP server registered in ${CONFIG}` } : { ok: false, level: "warn", message: `Codex: no Athena MCP entry in ${CONFIG} \u2014 re-run \`athena agents add codex\`` }
|
|
1096
|
+
);
|
|
1097
|
+
const hooks = await readTextIfExists(path10.join(root, HOOKS_FILE2));
|
|
1098
|
+
if (hooks === null) checks.push({ ok: false, level: "warn", message: `Codex: ${HOOKS_FILE2} not found \u2014 activity reporting is off` });
|
|
1099
|
+
else {
|
|
1100
|
+
const configured = CODEX_HOOK_EVENTS.filter((e) => new RegExp(`"${e}"`).test(hooks) && hooks.includes(ATHENA_MARK));
|
|
1101
|
+
checks.push(
|
|
1102
|
+
configured.length === CODEX_HOOK_EVENTS.length ? { ok: true, level: "ok", message: `Codex: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `Codex: only ${configured.length}/${CODEX_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add codex\`` }
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
checks.push(await trustCheck(root));
|
|
1106
|
+
return checks;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
function codexHookCommand(event) {
|
|
1110
|
+
const cmd = hookCommand();
|
|
1111
|
+
const exe = /[\s"'$`\\]/.test(cmd) ? JSON.stringify(cmd) : cmd;
|
|
1112
|
+
return `${exe} event ${ATHENA_MARK} --agent codex --hook ${event}`;
|
|
1113
|
+
}
|
|
1114
|
+
function mcpBlock() {
|
|
1115
|
+
return [TOML_START, "[mcp_servers.athena]", `command = ${JSON.stringify(hookCommand())}`, 'args = ["mcp"]', TOML_END].join("\n");
|
|
1116
|
+
}
|
|
1117
|
+
function stripTomlBlock(text) {
|
|
1118
|
+
const lines = text.split("\n");
|
|
1119
|
+
const out2 = [];
|
|
1120
|
+
let inside = false;
|
|
1121
|
+
for (const line2 of lines) {
|
|
1122
|
+
if (!inside && line2.startsWith("# athena:start")) inside = true;
|
|
1123
|
+
else if (inside && line2.trim() === TOML_END) inside = false;
|
|
1124
|
+
else if (!inside) out2.push(line2);
|
|
1125
|
+
}
|
|
1126
|
+
return out2.join("\n");
|
|
1127
|
+
}
|
|
1128
|
+
function parseTomlOrThrow(text) {
|
|
1129
|
+
try {
|
|
1130
|
+
return parseToml(text);
|
|
1131
|
+
} catch {
|
|
1132
|
+
throw new Error(`${CONFIG} is not valid TOML \u2014 fix it before configuring Codex`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function planConfig(root) {
|
|
1136
|
+
const existing = await readTextIfExists(path10.join(root, CONFIG));
|
|
1137
|
+
const base = existing === null ? "" : stripTomlBlock(existing);
|
|
1138
|
+
const parsed = parseTomlOrThrow(base);
|
|
1139
|
+
const servers = parsed.mcp_servers;
|
|
1140
|
+
if (servers && "athena" in servers) return { path: CONFIG, ownership: "block", action: "unchanged", content: existing ?? "" };
|
|
1141
|
+
const head = base.trimEnd();
|
|
1142
|
+
const content = `${head ? `${head}
|
|
1143
|
+
|
|
1144
|
+
` : ""}${mcpBlock()}
|
|
1145
|
+
`;
|
|
1146
|
+
return { path: CONFIG, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
|
|
1147
|
+
}
|
|
1148
|
+
async function removeConfigBlock(root) {
|
|
1149
|
+
const file = path10.join(root, CONFIG);
|
|
1150
|
+
const existing = await readTextIfExists(file);
|
|
1151
|
+
if (existing === null || !existing.includes(TOML_START)) return false;
|
|
1152
|
+
const next = stripTomlBlock(existing).trimEnd();
|
|
1153
|
+
if (next.trim() === "") await fs6.rm(file, { force: true });
|
|
1154
|
+
else await fs6.writeFile(file, `${next}
|
|
1155
|
+
`, "utf8");
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
async function tomlOnlyAthena(file) {
|
|
1159
|
+
const text = await readTextIfExists(file).catch(() => null);
|
|
1160
|
+
return text !== null && text.includes(TOML_START) && stripTomlBlock(text).trim() === "";
|
|
1161
|
+
}
|
|
1162
|
+
async function trustCheck(root) {
|
|
1163
|
+
const home = process.env.CODEX_HOME?.trim() || path10.join(os.homedir(), ".codex");
|
|
1164
|
+
const hint = `Codex loads .codex/ only for trusted projects: trust this folder when Codex asks, or add [projects."${root}"] trust_level = "trusted" to ${path10.join(home, "config.toml")}`;
|
|
1165
|
+
const text = await readTextIfExists(path10.join(home, "config.toml")).catch(() => null);
|
|
1166
|
+
if (text === null) return { ok: false, level: "warn", message: `Codex: could not confirm this project is trusted. ${hint}` };
|
|
1167
|
+
let projects = {};
|
|
1168
|
+
try {
|
|
1169
|
+
projects = parseToml(text).projects ?? {};
|
|
1170
|
+
} catch {
|
|
1171
|
+
return { ok: false, level: "warn", message: `Codex: could not read ${path10.join(home, "config.toml")} to confirm trust. ${hint}` };
|
|
1172
|
+
}
|
|
1173
|
+
for (let dir = root; ; dir = path10.dirname(dir)) {
|
|
1174
|
+
const entry = projects[dir];
|
|
1175
|
+
if (entry?.trust_level === "trusted") return { ok: true, level: "ok", message: `Codex: project is trusted${dir === root ? "" : ` (via ${dir})`}` };
|
|
1176
|
+
if (entry?.trust_level === "untrusted") return { ok: false, level: "warn", message: `Codex: ${dir} is marked untrusted, so Codex ignores .codex/. ${hint}` };
|
|
1177
|
+
if (path10.dirname(dir) === dir) break;
|
|
1178
|
+
}
|
|
1179
|
+
return { ok: false, level: "warn", message: `Codex: this project is not trusted yet. ${hint}` };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/agents/copilot/adapter.ts
|
|
1183
|
+
import { promises as fs7 } from "fs";
|
|
1184
|
+
import path11 from "path";
|
|
1185
|
+
var INSTRUCTIONS2 = ".github/copilot-instructions.md";
|
|
1186
|
+
var MCP_FILE3 = ".vscode/mcp.json";
|
|
1187
|
+
var HOOKS_DIR = ".github/hooks";
|
|
1188
|
+
var HOOKS_FILE3 = `${HOOKS_DIR}/athena.json`;
|
|
1189
|
+
var copilotAdapter = {
|
|
1190
|
+
id: "copilot",
|
|
1191
|
+
displayName: "GitHub Copilot",
|
|
1192
|
+
capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
|
|
1193
|
+
supportNote: "Uses the Athena block in .github/copilot-instructions.md, an MCP server entry in .vscode/mcp.json (VS Code), and hooks in .github/hooks/athena.json that report activity (VS Code hooks are in Preview; Copilot CLI reads the same file).",
|
|
1194
|
+
async detectPresence(root) {
|
|
1195
|
+
const evidence = await userEvidence(root, { files: [INSTRUCTIONS2], dirs: [".github/instructions", HOOKS_DIR], athenaOwnedJson: [HOOKS_FILE3] });
|
|
1196
|
+
if (await exists(path11.join(root, MCP_FILE3)) && !await jsonOnlyAthena(path11.join(root, MCP_FILE3))) evidence.push(MCP_FILE3);
|
|
1197
|
+
return { detectedInProject: evidence.length > 0, evidence };
|
|
1198
|
+
},
|
|
1199
|
+
async plan(ctx) {
|
|
1200
|
+
const changes = [await planBlock(ctx.root, INSTRUCTIONS2, athenaInstructions({ heading: "##" }))];
|
|
1201
|
+
changes.push(
|
|
1202
|
+
await planJsonChange(ctx.root, MCP_FILE3, (data) => {
|
|
1203
|
+
const servers = data.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
1204
|
+
servers.athena = { type: "stdio", command: hookCommand(), args: ["mcp"] };
|
|
1205
|
+
data.servers = servers;
|
|
1206
|
+
})
|
|
1207
|
+
);
|
|
1208
|
+
changes.push(
|
|
1209
|
+
await planJsonChange(ctx.root, HOOKS_FILE3, (data) => {
|
|
1210
|
+
data.version = typeof data.version === "number" ? data.version : 1;
|
|
1211
|
+
const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
|
|
1212
|
+
for (const event of COPILOT_HOOK_EVENTS) {
|
|
1213
|
+
const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
|
|
1214
|
+
list.push({ type: "command", bash: posixCommand(event), powershell: powershellCommand(event), timeoutSec: 10 });
|
|
1215
|
+
hooks[event] = list;
|
|
1216
|
+
}
|
|
1217
|
+
data.hooks = hooks;
|
|
1218
|
+
})
|
|
1219
|
+
);
|
|
1220
|
+
return changes;
|
|
1221
|
+
},
|
|
1222
|
+
async remove(root) {
|
|
1223
|
+
const touched = [];
|
|
1224
|
+
if (await removeBlockFrom(root, INSTRUCTIONS2)) touched.push(INSTRUCTIONS2);
|
|
1225
|
+
const mcpRemoved = await removeJsonHooks(
|
|
1226
|
+
root,
|
|
1227
|
+
MCP_FILE3,
|
|
1228
|
+
(data) => {
|
|
1229
|
+
const servers = data.servers;
|
|
1230
|
+
if (!servers || typeof servers !== "object") return;
|
|
1231
|
+
delete servers.athena;
|
|
1232
|
+
pruneEmpty(data, "servers");
|
|
1233
|
+
},
|
|
1234
|
+
(d) => Object.keys(d).length === 0
|
|
1235
|
+
);
|
|
1236
|
+
if (mcpRemoved) touched.push(MCP_FILE3);
|
|
1237
|
+
const hooksRemoved = await removeJsonHooks(
|
|
1238
|
+
root,
|
|
1239
|
+
HOOKS_FILE3,
|
|
1240
|
+
(data) => {
|
|
1241
|
+
const hooks = data.hooks;
|
|
1242
|
+
if (!hooks || typeof hooks !== "object") return;
|
|
1243
|
+
for (const event of Object.keys(hooks)) {
|
|
1244
|
+
if (!Array.isArray(hooks[event])) continue;
|
|
1245
|
+
hooks[event] = hooks[event].filter((e) => !isAthenaEntry(e));
|
|
1246
|
+
if (!hooks[event].length) delete hooks[event];
|
|
1247
|
+
}
|
|
1248
|
+
pruneEmpty(data, "hooks");
|
|
1249
|
+
if (Object.keys(data).length === 1 && "version" in data) delete data.version;
|
|
1250
|
+
},
|
|
1251
|
+
(d) => Object.keys(d).length === 0
|
|
1252
|
+
);
|
|
1253
|
+
if (hooksRemoved) touched.push(HOOKS_FILE3);
|
|
1254
|
+
for (const dir of [HOOKS_DIR, ".vscode"]) await fs7.rmdir(path11.join(root, dir)).catch(() => {
|
|
1255
|
+
});
|
|
1256
|
+
return touched;
|
|
1257
|
+
},
|
|
1258
|
+
async check(root) {
|
|
1259
|
+
const checks = [await checkBlock(root, INSTRUCTIONS2, "GitHub Copilot")];
|
|
1260
|
+
const mcp = await readJsonFile(path11.join(root, MCP_FILE3)).catch(() => null);
|
|
1261
|
+
const servers = mcp?.data.servers;
|
|
1262
|
+
if (mcp?.malformed) checks.push({ ok: false, level: "error", message: `GitHub Copilot: ${MCP_FILE3} is not valid JSON` });
|
|
1263
|
+
else
|
|
1264
|
+
checks.push(
|
|
1265
|
+
servers && typeof servers === "object" && "athena" in servers ? { ok: true, level: "ok", message: `GitHub Copilot: MCP server registered in ${MCP_FILE3} (VS Code)` } : { ok: false, level: "warn", message: `GitHub Copilot: no Athena MCP entry in ${MCP_FILE3} \u2014 re-run \`athena agents add copilot\`` }
|
|
1266
|
+
);
|
|
1267
|
+
const hooks = await readTextIfExists(path11.join(root, HOOKS_FILE3));
|
|
1268
|
+
if (hooks === null) checks.push({ ok: false, level: "warn", message: `GitHub Copilot: ${HOOKS_FILE3} not found \u2014 activity reporting is off` });
|
|
1269
|
+
else {
|
|
1270
|
+
const configured = COPILOT_HOOK_EVENTS.filter((e) => new RegExp(`"${e}"`).test(hooks) && hooks.includes(ATHENA_MARK));
|
|
1271
|
+
checks.push(
|
|
1272
|
+
configured.length === COPILOT_HOOK_EVENTS.length ? { ok: true, level: "ok", message: `GitHub Copilot: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `GitHub Copilot: only ${configured.length}/${COPILOT_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add copilot\`` }
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
return checks;
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
function posixCommand(event) {
|
|
1279
|
+
const cmd = hookCommand();
|
|
1280
|
+
const exe = /[\s"'$`\\]/.test(cmd) ? `'${cmd.replace(/'/g, `'\\''`)}'` : cmd;
|
|
1281
|
+
return `${exe} event ${ATHENA_MARK} --agent copilot --hook ${event} 2>/dev/null || true`;
|
|
1282
|
+
}
|
|
1283
|
+
function powershellCommand(event) {
|
|
1284
|
+
const exe = `'${hookCommand().replace(/'/g, "''")}'`;
|
|
1285
|
+
return `& ${exe} event ${ATHENA_MARK} --agent copilot --hook ${event} 2>$null; exit 0`;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/agents/gemini-cli/adapter.ts
|
|
1289
|
+
import { promises as fs8 } from "fs";
|
|
1290
|
+
import path12 from "path";
|
|
1291
|
+
var CONTEXT = "GEMINI.md";
|
|
1292
|
+
var SETTINGS2 = ".gemini/settings.json";
|
|
1293
|
+
var geminiCliAdapter = {
|
|
1294
|
+
id: "gemini-cli",
|
|
1295
|
+
displayName: "Gemini CLI",
|
|
1296
|
+
capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
|
|
1297
|
+
supportNote: "Uses the Athena block in GEMINI.md, plus an MCP server entry and hooks that report activity in .gemini/settings.json. Gemini CLI ignores .gemini/settings.json in folders it does not trust (when folder trust is enabled).",
|
|
1298
|
+
async detectPresence(root) {
|
|
1299
|
+
const evidence = await userEvidence(root, { files: [CONTEXT], dirs: [".gemini"], athenaOwnedJson: [SETTINGS2] });
|
|
1300
|
+
return { detectedInProject: evidence.length > 0, evidence };
|
|
1301
|
+
},
|
|
1302
|
+
async plan(ctx) {
|
|
1303
|
+
const changes = [await planBlock(ctx.root, CONTEXT, athenaInstructions({ heading: "##" }))];
|
|
1304
|
+
changes.push(
|
|
1305
|
+
await planJsonChange(ctx.root, SETTINGS2, (data) => {
|
|
1306
|
+
mergeMcpServers(data);
|
|
1307
|
+
const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
|
|
1308
|
+
for (const event of GEMINI_HOOK_EVENTS) {
|
|
1309
|
+
const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
|
|
1310
|
+
list.push({
|
|
1311
|
+
...event === "BeforeTool" ? { matcher: "*" } : {},
|
|
1312
|
+
hooks: [{ name: "athena-activity", type: "command", command: geminiHookCommand(event), timeout: 1e4 }]
|
|
1313
|
+
});
|
|
1314
|
+
hooks[event] = list;
|
|
1315
|
+
}
|
|
1316
|
+
data.hooks = hooks;
|
|
1317
|
+
})
|
|
1318
|
+
);
|
|
1319
|
+
return changes;
|
|
1320
|
+
},
|
|
1321
|
+
async remove(root) {
|
|
1322
|
+
const touched = [];
|
|
1323
|
+
if (await removeBlockFrom(root, CONTEXT)) touched.push(CONTEXT);
|
|
1324
|
+
const removed = await removeJsonHooks(
|
|
1325
|
+
root,
|
|
1326
|
+
SETTINGS2,
|
|
1327
|
+
(data) => {
|
|
1328
|
+
stripMcpServers(data);
|
|
1329
|
+
const hooks = data.hooks;
|
|
1330
|
+
if (!hooks || typeof hooks !== "object") return;
|
|
1331
|
+
for (const event of Object.keys(hooks)) {
|
|
1332
|
+
if (!Array.isArray(hooks[event])) continue;
|
|
1333
|
+
hooks[event] = hooks[event].filter((e) => !isAthenaEntry(e));
|
|
1334
|
+
if (!hooks[event].length) delete hooks[event];
|
|
1335
|
+
}
|
|
1336
|
+
pruneEmpty(data, "hooks");
|
|
1337
|
+
},
|
|
1338
|
+
(d) => Object.keys(d).length === 0
|
|
1339
|
+
);
|
|
1340
|
+
if (removed) touched.push(SETTINGS2);
|
|
1341
|
+
await fs8.rmdir(path12.join(root, ".gemini")).catch(() => {
|
|
1342
|
+
});
|
|
1343
|
+
return touched;
|
|
1344
|
+
},
|
|
1345
|
+
async check(root) {
|
|
1346
|
+
const checks = [await checkBlock(root, CONTEXT, "Gemini CLI")];
|
|
1347
|
+
const settings = await readJsonFile(path12.join(root, SETTINGS2)).catch(() => null);
|
|
1348
|
+
if (!settings?.existed) {
|
|
1349
|
+
checks.push({ ok: false, level: "warn", message: `Gemini CLI: ${SETTINGS2} not found \u2014 no MCP server and activity reporting is off` });
|
|
1350
|
+
return checks;
|
|
1351
|
+
}
|
|
1352
|
+
if (settings.malformed) {
|
|
1353
|
+
checks.push({ ok: false, level: "error", message: `Gemini CLI: ${SETTINGS2} is not valid JSON` });
|
|
1354
|
+
return checks;
|
|
1355
|
+
}
|
|
1356
|
+
const servers = settings.data.mcpServers;
|
|
1357
|
+
checks.push(
|
|
1358
|
+
servers && typeof servers === "object" && "athena" in servers ? { ok: true, level: "ok", message: `Gemini CLI: MCP server registered in ${SETTINGS2}` } : { ok: false, level: "warn", message: `Gemini CLI: no Athena MCP entry in ${SETTINGS2} \u2014 re-run \`athena agents add gemini-cli\`` }
|
|
1359
|
+
);
|
|
1360
|
+
const text = await readTextIfExists(path12.join(root, SETTINGS2)) ?? "";
|
|
1361
|
+
const hooks = settings.data.hooks ?? {};
|
|
1362
|
+
const configured = GEMINI_HOOK_EVENTS.filter((e) => isAthenaEntry(hooks[e]));
|
|
1363
|
+
checks.push(
|
|
1364
|
+
configured.length === GEMINI_HOOK_EVENTS.length && text.includes(ATHENA_MARK) ? { ok: true, level: "ok", message: `Gemini CLI: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `Gemini CLI: only ${configured.length}/${GEMINI_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add gemini-cli\`` }
|
|
1365
|
+
);
|
|
1366
|
+
checks.push({ ok: true, level: "warn", message: "Gemini CLI: if folder trust is enabled, trust this folder \u2014 Gemini CLI ignores .gemini/settings.json (MCP, hooks) in untrusted folders" });
|
|
1367
|
+
return checks;
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
function geminiHookCommand(event) {
|
|
1371
|
+
const cmd = hookCommand();
|
|
1372
|
+
const exe = /[\s"'$`\\]/.test(cmd) ? JSON.stringify(cmd) : cmd;
|
|
1373
|
+
return `${exe} event ${ATHENA_MARK} --agent gemini-cli --hook ${event}`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/agents/windsurf/adapter.ts
|
|
1377
|
+
import path13 from "path";
|
|
1378
|
+
var RULE_FILE3 = ".windsurf/rules/athena.md";
|
|
1379
|
+
var WINDSURF_RULE_LIMIT = 12e3;
|
|
1380
|
+
var windsurfAdapter = {
|
|
1381
|
+
id: "windsurf",
|
|
1382
|
+
displayName: "Windsurf",
|
|
1383
|
+
capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: false },
|
|
1384
|
+
supportNote: 'Uses an Athena-owned workspace rule at .windsurf/rules/athena.md (trigger: always_on), which Windsurf and Devin Desktop read. MCP is configured per user in Windsurf, so Athena does not register it: add `{ "mcpServers": { "athena": { "command": "athena", "args": ["mcp"] } } }` to your mcp_config.json yourself. No activity hooks.',
|
|
1385
|
+
async detectPresence(root) {
|
|
1386
|
+
const evidence = await userEvidence(root, { files: [".windsurfrules"], dirs: [".windsurf"], athenaOwned: await generatedByAthena(root, [RULE_FILE3]) });
|
|
1387
|
+
return { detectedInProject: evidence.length > 0, evidence };
|
|
1388
|
+
},
|
|
1389
|
+
async plan(ctx) {
|
|
1390
|
+
const content = ["---", "trigger: always_on", "---", OWNED_HEADER, "", athenaInstructions({ heading: "#" }), ""].join("\n");
|
|
1391
|
+
if (content.length > WINDSURF_RULE_LIMIT) throw new Error(`Windsurf rule exceeds ${WINDSURF_RULE_LIMIT} characters`);
|
|
1392
|
+
return [await planOwned(ctx.root, RULE_FILE3, content)];
|
|
1393
|
+
},
|
|
1394
|
+
async remove(root) {
|
|
1395
|
+
return await removeOwned(root, RULE_FILE3) ? [RULE_FILE3] : [];
|
|
1396
|
+
},
|
|
1397
|
+
async check(root) {
|
|
1398
|
+
const text = await readTextIfExists(path13.join(root, RULE_FILE3));
|
|
1399
|
+
if (text === null) return [{ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} not found` }];
|
|
1400
|
+
if (!text.includes("Generated by Athena")) return [{ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} is a developer file, so Athena did not write its rule there` }];
|
|
1401
|
+
const checks = [];
|
|
1402
|
+
if (text.length > WINDSURF_RULE_LIMIT) checks.push({ ok: false, level: "error", message: `Windsurf: ${RULE_FILE3} exceeds the 12,000 character rule limit` });
|
|
1403
|
+
else if (!/^---\r?\n[\s\S]*?trigger:\s*always_on[\s\S]*?\r?\n---/.test(text)) checks.push({ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} frontmatter was modified (trigger is not always_on)` });
|
|
1404
|
+
else checks.push({ ok: true, level: "ok", message: `Windsurf: ${RULE_FILE3}` });
|
|
1405
|
+
return checks;
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
async function generatedByAthena(root, rels) {
|
|
1409
|
+
const out2 = [];
|
|
1410
|
+
for (const rel2 of rels) {
|
|
1411
|
+
const text = await readTextIfExists(path13.join(root, rel2)).catch(() => null);
|
|
1412
|
+
if (text?.includes("Generated by Athena")) out2.push(rel2);
|
|
1413
|
+
}
|
|
1414
|
+
return out2;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/agents/cline/adapter.ts
|
|
1418
|
+
import { promises as fs9 } from "fs";
|
|
1419
|
+
import path14 from "path";
|
|
1420
|
+
var RULES_DIR = ".clinerules";
|
|
1421
|
+
var RULE_FILE4 = ".clinerules/athena.md";
|
|
1422
|
+
var ALT_RULE_FILE = ".cline/rules/athena.md";
|
|
1423
|
+
var clineAdapter = {
|
|
1424
|
+
id: "cline",
|
|
1425
|
+
displayName: "Cline",
|
|
1426
|
+
capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: false },
|
|
1427
|
+
supportNote: 'Uses an Athena-owned workspace rule at .clinerules/athena.md (or .cline/rules/athena.md when .clinerules is a single file). MCP servers are configured per user in Cline, so Athena does not register it: add an "athena" server with command `athena` and args ["mcp"] in Cline\'s MCP settings yourself. No activity hooks.',
|
|
1428
|
+
async detectPresence(root) {
|
|
1429
|
+
const evidence = await userEvidence(root, { files: [RULES_DIR], dirs: [RULES_DIR, ".cline"], athenaOwned: await generatedByAthena2(root, [RULE_FILE4, ALT_RULE_FILE]) });
|
|
1430
|
+
return { detectedInProject: evidence.length > 0, evidence };
|
|
1431
|
+
},
|
|
1432
|
+
async plan(ctx) {
|
|
1433
|
+
const content = [OWNED_HEADER, "", athenaInstructions({ heading: "#" }), ""].join("\n");
|
|
1434
|
+
return [await planOwned(ctx.root, await rulePath(ctx.root), content)];
|
|
1435
|
+
},
|
|
1436
|
+
async remove(root) {
|
|
1437
|
+
const touched = [];
|
|
1438
|
+
for (const rel2 of [RULE_FILE4, ALT_RULE_FILE]) {
|
|
1439
|
+
if (!await isDirectory(path14.join(root, path14.dirname(rel2)))) continue;
|
|
1440
|
+
if (await removeOwned(root, rel2)) touched.push(rel2);
|
|
1441
|
+
}
|
|
1442
|
+
return touched;
|
|
1443
|
+
},
|
|
1444
|
+
async check(root) {
|
|
1445
|
+
const rel2 = await rulePath(root);
|
|
1446
|
+
const text = await readTextIfExists(path14.join(root, rel2));
|
|
1447
|
+
if (text === null) return [{ ok: false, level: "warn", message: `Cline: ${rel2} not found` }];
|
|
1448
|
+
if (!text.includes("Generated by Athena")) return [{ ok: false, level: "warn", message: `Cline: ${rel2} is a developer file, so Athena did not write its rule there` }];
|
|
1449
|
+
const checks = [{ ok: true, level: "ok", message: `Cline: ${rel2}` }];
|
|
1450
|
+
if (rel2 === ALT_RULE_FILE && !await isDirectory(path14.join(root, RULES_DIR)) && await exists2(path14.join(root, RULES_DIR))) {
|
|
1451
|
+
checks.push({ ok: true, level: "warn", message: `Cline: .clinerules is a single file, so Athena's rule is in ${ALT_RULE_FILE}; older Cline versions that only read .clinerules will not see it` });
|
|
1452
|
+
}
|
|
1453
|
+
return checks;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
async function rulePath(root) {
|
|
1457
|
+
const clinerules = path14.join(root, RULES_DIR);
|
|
1458
|
+
if (await exists2(clinerules) && !await isDirectory(clinerules)) return ALT_RULE_FILE;
|
|
1459
|
+
const alt = await readTextIfExists(path14.join(root, ALT_RULE_FILE)).catch(() => null);
|
|
1460
|
+
if (alt?.includes("Generated by Athena")) return ALT_RULE_FILE;
|
|
1461
|
+
return RULE_FILE4;
|
|
1462
|
+
}
|
|
1463
|
+
async function exists2(p) {
|
|
1464
|
+
return await fs9.lstat(p).catch(() => null) !== null;
|
|
1465
|
+
}
|
|
1466
|
+
async function isDirectory(p) {
|
|
1467
|
+
return (await fs9.lstat(p).catch(() => null))?.isDirectory() ?? false;
|
|
1468
|
+
}
|
|
1469
|
+
async function generatedByAthena2(root, rels) {
|
|
1470
|
+
const out2 = [];
|
|
1471
|
+
for (const rel2 of rels) {
|
|
1472
|
+
const text = await readTextIfExists(path14.join(root, rel2)).catch(() => null);
|
|
1473
|
+
if (text?.includes("Generated by Athena")) out2.push(rel2);
|
|
1474
|
+
}
|
|
1475
|
+
return out2;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1038
1478
|
// src/agents/agents-md/adapter.ts
|
|
1039
1479
|
var agentsMdAdapter = {
|
|
1040
1480
|
id: "agents-md",
|
|
@@ -1057,9 +1497,23 @@ var agentsMdAdapter = {
|
|
|
1057
1497
|
};
|
|
1058
1498
|
|
|
1059
1499
|
// src/agents/registry.ts
|
|
1060
|
-
var ADAPTERS = [claudeCodeAdapter, cursorAdapter, antigravityAdapter, agentsMdAdapter];
|
|
1500
|
+
var ADAPTERS = [claudeCodeAdapter, cursorAdapter, codexAdapter, copilotAdapter, geminiCliAdapter, antigravityAdapter, windsurfAdapter, clineAdapter, agentsMdAdapter];
|
|
1061
1501
|
function resolveAdapters(names2) {
|
|
1062
|
-
const alias = {
|
|
1502
|
+
const alias = {
|
|
1503
|
+
claude: "claude-code",
|
|
1504
|
+
claudecode: "claude-code",
|
|
1505
|
+
agents: "agents-md",
|
|
1506
|
+
"agents.md": "agents-md",
|
|
1507
|
+
gemini: "gemini-cli",
|
|
1508
|
+
geminicli: "gemini-cli",
|
|
1509
|
+
"openai-codex": "codex",
|
|
1510
|
+
"codex-cli": "codex",
|
|
1511
|
+
"github-copilot": "copilot",
|
|
1512
|
+
"gh-copilot": "copilot",
|
|
1513
|
+
"copilot-cli": "copilot",
|
|
1514
|
+
"google-antigravity": "antigravity",
|
|
1515
|
+
codeium: "windsurf"
|
|
1516
|
+
};
|
|
1063
1517
|
const out2 = [];
|
|
1064
1518
|
for (const raw of names2) {
|
|
1065
1519
|
const n = raw.trim().toLowerCase();
|
|
@@ -1077,11 +1531,11 @@ async function applyChanges(root, changes) {
|
|
|
1077
1531
|
if (c2.action === "unchanged") continue;
|
|
1078
1532
|
const abs = resolveInside(root, c2.path);
|
|
1079
1533
|
if (!abs) throw new Error(`Refusing to write outside the project: ${c2.path}`);
|
|
1080
|
-
const st = await
|
|
1534
|
+
const st = await fs10.lstat(abs).catch(() => null);
|
|
1081
1535
|
if (st?.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${c2.path}`);
|
|
1082
|
-
await
|
|
1536
|
+
await fs10.mkdir(path15.dirname(abs), { recursive: true });
|
|
1083
1537
|
await writeFileAtomic(abs, c2.content);
|
|
1084
|
-
if (abs.endsWith(".sh")) await
|
|
1538
|
+
if (abs.endsWith(".sh")) await fs10.chmod(abs, 493).catch(() => {
|
|
1085
1539
|
});
|
|
1086
1540
|
applied.push(c2);
|
|
1087
1541
|
}
|
|
@@ -1089,22 +1543,22 @@ async function applyChanges(root, changes) {
|
|
|
1089
1543
|
}
|
|
1090
1544
|
|
|
1091
1545
|
// src/services/pipeline.ts
|
|
1092
|
-
import { promises as
|
|
1093
|
-
import
|
|
1546
|
+
import { promises as fs16 } from "fs";
|
|
1547
|
+
import path23 from "path";
|
|
1094
1548
|
import crypto4 from "crypto";
|
|
1095
1549
|
|
|
1096
1550
|
// src/core/analyzer/analyze.ts
|
|
1097
|
-
import
|
|
1098
|
-
import { promises as
|
|
1551
|
+
import path20 from "path";
|
|
1552
|
+
import { promises as fs15 } from "fs";
|
|
1099
1553
|
|
|
1100
1554
|
// src/core/fs/walker.ts
|
|
1101
|
-
import { promises as
|
|
1102
|
-
import
|
|
1555
|
+
import { promises as fs12 } from "fs";
|
|
1556
|
+
import path17 from "path";
|
|
1103
1557
|
import crypto3 from "crypto";
|
|
1104
1558
|
|
|
1105
1559
|
// src/core/fs/ignore.ts
|
|
1106
|
-
import { promises as
|
|
1107
|
-
import
|
|
1560
|
+
import { promises as fs11 } from "fs";
|
|
1561
|
+
import path16 from "path";
|
|
1108
1562
|
import ignoreFactory from "ignore";
|
|
1109
1563
|
var IgnoreMatcher = class _IgnoreMatcher {
|
|
1110
1564
|
base;
|
|
@@ -1117,9 +1571,9 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1117
1571
|
}
|
|
1118
1572
|
static async load(root, config) {
|
|
1119
1573
|
const m = new _IgnoreMatcher(config);
|
|
1120
|
-
const exclude = await
|
|
1574
|
+
const exclude = await fs11.readFile(path16.join(root, ".git", "info", "exclude"), "utf8").catch(() => null);
|
|
1121
1575
|
if (exclude) m.setGitignore("", exclude);
|
|
1122
|
-
const rootGi = await
|
|
1576
|
+
const rootGi = await fs11.readFile(path16.join(root, ".gitignore"), "utf8").catch(() => null);
|
|
1123
1577
|
if (rootGi) m.setGitignore("", rootGi, true);
|
|
1124
1578
|
return m;
|
|
1125
1579
|
}
|
|
@@ -1132,16 +1586,16 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1132
1586
|
this.scoped.delete(dir);
|
|
1133
1587
|
}
|
|
1134
1588
|
/** Is the repo-relative POSIX path ignored? `isDir` enables directory-only patterns ("build/"). */
|
|
1135
|
-
ignores(
|
|
1136
|
-
if (!
|
|
1137
|
-
const test = isDir ? `${
|
|
1589
|
+
ignores(rel2, isDir = false) {
|
|
1590
|
+
if (!rel2 || rel2 === ".") return false;
|
|
1591
|
+
const test = isDir ? `${rel2}/` : rel2;
|
|
1138
1592
|
if (this.include?.ignores(test)) return false;
|
|
1139
1593
|
if (this.base.ignores(test)) return true;
|
|
1140
1594
|
for (const [dir, ig] of this.scoped) {
|
|
1141
1595
|
if (dir === "") {
|
|
1142
1596
|
if (ig.ignores(test)) return true;
|
|
1143
|
-
} else if (
|
|
1144
|
-
const sub =
|
|
1597
|
+
} else if (rel2.startsWith(`${dir}/`)) {
|
|
1598
|
+
const sub = rel2.slice(dir.length + 1);
|
|
1145
1599
|
if (ig.ignores(isDir ? `${sub}/` : sub)) return true;
|
|
1146
1600
|
}
|
|
1147
1601
|
}
|
|
@@ -1152,41 +1606,41 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1152
1606
|
// src/core/fs/walker.ts
|
|
1153
1607
|
var SNIFF_BYTES = 8e3;
|
|
1154
1608
|
async function walkProject(root, opts) {
|
|
1155
|
-
const rootReal = await
|
|
1609
|
+
const rootReal = await fs12.realpath(root);
|
|
1156
1610
|
const matcher = opts.matcher ?? await IgnoreMatcher.load(rootReal, opts.config);
|
|
1157
1611
|
const result = { files: [], skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0, truncated: false, warnings: [] };
|
|
1158
1612
|
const visitedDirs = /* @__PURE__ */ new Set([rootReal]);
|
|
1159
1613
|
const pending = [];
|
|
1160
|
-
const isIgnored = (
|
|
1614
|
+
const isIgnored = (rel2, isDir) => matcher.ignores(rel2, isDir);
|
|
1161
1615
|
const stack = [rootReal];
|
|
1162
1616
|
while (stack.length) {
|
|
1163
1617
|
opts.signal?.throwIfAborted();
|
|
1164
1618
|
const dir = stack.pop();
|
|
1165
1619
|
let entries;
|
|
1166
1620
|
try {
|
|
1167
|
-
entries = await
|
|
1621
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
1168
1622
|
} catch (err) {
|
|
1169
1623
|
result.skippedUnreadable++;
|
|
1170
|
-
result.warnings.push(`Cannot read directory ${toPosix(
|
|
1624
|
+
result.warnings.push(`Cannot read directory ${toPosix(path17.relative(rootReal, dir)) || "."}: ${err.code ?? "error"}`);
|
|
1171
1625
|
continue;
|
|
1172
1626
|
}
|
|
1173
1627
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1174
|
-
const dirRel = toPosix(
|
|
1628
|
+
const dirRel = toPosix(path17.relative(rootReal, dir));
|
|
1175
1629
|
if (dirRel && entries.some((e) => e.name === ".gitignore" && e.isFile())) {
|
|
1176
|
-
const gi = await
|
|
1630
|
+
const gi = await fs12.readFile(path17.join(dir, ".gitignore"), "utf8").catch(() => null);
|
|
1177
1631
|
if (gi !== null) matcher.setGitignore(dirRel, gi);
|
|
1178
1632
|
}
|
|
1179
1633
|
for (const ent of entries) {
|
|
1180
|
-
const abs =
|
|
1181
|
-
const
|
|
1634
|
+
const abs = path17.join(dir, ent.name);
|
|
1635
|
+
const rel2 = toPosix(path17.relative(rootReal, abs));
|
|
1182
1636
|
let isDir = ent.isDirectory();
|
|
1183
1637
|
let isFile = ent.isFile();
|
|
1184
1638
|
if (ent.isSymbolicLink()) {
|
|
1185
1639
|
try {
|
|
1186
|
-
const target = await
|
|
1187
|
-
const relTarget =
|
|
1188
|
-
if (relTarget.startsWith("..") ||
|
|
1189
|
-
const st = await
|
|
1640
|
+
const target = await fs12.realpath(abs);
|
|
1641
|
+
const relTarget = path17.relative(rootReal, target);
|
|
1642
|
+
if (relTarget.startsWith("..") || path17.isAbsolute(relTarget)) continue;
|
|
1643
|
+
const st = await fs12.stat(target);
|
|
1190
1644
|
isDir = st.isDirectory();
|
|
1191
1645
|
isFile = st.isFile();
|
|
1192
1646
|
if (isDir) {
|
|
@@ -1197,18 +1651,18 @@ async function walkProject(root, opts) {
|
|
|
1197
1651
|
}
|
|
1198
1652
|
}
|
|
1199
1653
|
if (isDir) {
|
|
1200
|
-
if (isIgnored(
|
|
1201
|
-
const real = await
|
|
1654
|
+
if (isIgnored(rel2, true)) continue;
|
|
1655
|
+
const real = await fs12.realpath(abs).catch(() => abs);
|
|
1202
1656
|
if (visitedDirs.has(real)) continue;
|
|
1203
1657
|
visitedDirs.add(real);
|
|
1204
1658
|
stack.push(abs);
|
|
1205
1659
|
} else if (isFile) {
|
|
1206
|
-
if (isIgnored(
|
|
1660
|
+
if (isIgnored(rel2, false)) continue;
|
|
1207
1661
|
if (result.files.length + pending.length >= opts.config.maxFiles) {
|
|
1208
1662
|
result.truncated = true;
|
|
1209
1663
|
break;
|
|
1210
1664
|
}
|
|
1211
|
-
pending.push(
|
|
1665
|
+
pending.push(rel2);
|
|
1212
1666
|
}
|
|
1213
1667
|
}
|
|
1214
1668
|
if (result.truncated) break;
|
|
@@ -1222,9 +1676,9 @@ async function walkProject(root, opts) {
|
|
|
1222
1676
|
return result;
|
|
1223
1677
|
async function flush() {
|
|
1224
1678
|
const batch = pending.splice(0, pending.length);
|
|
1225
|
-
await mapLimit(batch, 32, async (
|
|
1679
|
+
await mapLimit(batch, 32, async (rel2) => {
|
|
1226
1680
|
opts.signal?.throwIfAborted();
|
|
1227
|
-
const entry = await indexFile(rootReal,
|
|
1681
|
+
const entry = await indexFile(rootReal, rel2, opts.config.maxFileBytes, opts.reuse?.[rel2]);
|
|
1228
1682
|
if (!entry) {
|
|
1229
1683
|
result.skippedUnreadable++;
|
|
1230
1684
|
return;
|
|
@@ -1236,21 +1690,21 @@ async function walkProject(root, opts) {
|
|
|
1236
1690
|
opts.onProgress?.(result.files.length);
|
|
1237
1691
|
}
|
|
1238
1692
|
}
|
|
1239
|
-
async function indexFile(root,
|
|
1240
|
-
const abs =
|
|
1693
|
+
async function indexFile(root, rel2, maxBytes, prev) {
|
|
1694
|
+
const abs = path17.join(root, rel2);
|
|
1241
1695
|
try {
|
|
1242
|
-
const st = await
|
|
1696
|
+
const st = await fs12.stat(abs);
|
|
1243
1697
|
if (prev && prev.s === st.size && prev.m === Math.floor(st.mtimeMs)) {
|
|
1244
|
-
return { path:
|
|
1698
|
+
return { path: rel2, size: st.size, mtimeMs: prev.m, hash: prev.h, binary: prev.b === 1, large: prev.h.startsWith("meta:") };
|
|
1245
1699
|
}
|
|
1246
1700
|
if (st.size > maxBytes) {
|
|
1247
|
-
return { path:
|
|
1701
|
+
return { path: rel2, size: st.size, mtimeMs: Math.floor(st.mtimeMs), hash: `meta:${st.size}:${Math.floor(st.mtimeMs)}`, binary: false, large: true };
|
|
1248
1702
|
}
|
|
1249
|
-
const buf = await
|
|
1703
|
+
const buf = await fs12.readFile(abs);
|
|
1250
1704
|
const sniff = buf.subarray(0, SNIFF_BYTES);
|
|
1251
1705
|
const binary = sniff.includes(0);
|
|
1252
1706
|
return {
|
|
1253
|
-
path:
|
|
1707
|
+
path: rel2,
|
|
1254
1708
|
size: st.size,
|
|
1255
1709
|
mtimeMs: Math.floor(st.mtimeMs),
|
|
1256
1710
|
hash: crypto3.createHash("sha256").update(buf).digest("hex"),
|
|
@@ -1478,8 +1932,8 @@ function emptyModel(name, root) {
|
|
|
1478
1932
|
}
|
|
1479
1933
|
|
|
1480
1934
|
// src/core/analyzer/context.ts
|
|
1481
|
-
import { promises as
|
|
1482
|
-
import
|
|
1935
|
+
import { promises as fs13 } from "fs";
|
|
1936
|
+
import path18 from "path";
|
|
1483
1937
|
function createContext(root, config, files, model, signal) {
|
|
1484
1938
|
const byPath = new Map(files.map((f) => [f.path, f]));
|
|
1485
1939
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -1489,19 +1943,19 @@ function createContext(root, config, files, model, signal) {
|
|
|
1489
1943
|
files,
|
|
1490
1944
|
model,
|
|
1491
1945
|
signal,
|
|
1492
|
-
has: (
|
|
1493
|
-
async read(
|
|
1494
|
-
if (cache.has(
|
|
1495
|
-
const entry = byPath.get(
|
|
1946
|
+
has: (rel2) => byPath.has(rel2),
|
|
1947
|
+
async read(rel2) {
|
|
1948
|
+
if (cache.has(rel2)) return cache.get(rel2);
|
|
1949
|
+
const entry = byPath.get(rel2);
|
|
1496
1950
|
if (!entry || entry.binary || entry.large) return null;
|
|
1497
1951
|
signal?.throwIfAborted();
|
|
1498
1952
|
let text = null;
|
|
1499
1953
|
try {
|
|
1500
|
-
text = await
|
|
1954
|
+
text = await fs13.readFile(path18.join(root, rel2), "utf8");
|
|
1501
1955
|
} catch {
|
|
1502
1956
|
text = null;
|
|
1503
1957
|
}
|
|
1504
|
-
if (!text || text.length < 256e3) cache.set(
|
|
1958
|
+
if (!text || text.length < 256e3) cache.set(rel2, text);
|
|
1505
1959
|
return text;
|
|
1506
1960
|
},
|
|
1507
1961
|
find(match) {
|
|
@@ -1601,7 +2055,7 @@ var structureDetector = {
|
|
|
1601
2055
|
};
|
|
1602
2056
|
|
|
1603
2057
|
// src/core/analyzer/detectors/manifests.ts
|
|
1604
|
-
import { parse as
|
|
2058
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
1605
2059
|
import { parse as parseYaml } from "yaml";
|
|
1606
2060
|
import ignoreFactory2 from "ignore";
|
|
1607
2061
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -1656,7 +2110,7 @@ async function parsePyproject(ctx, p) {
|
|
|
1656
2110
|
if (!text) return null;
|
|
1657
2111
|
let t;
|
|
1658
2112
|
try {
|
|
1659
|
-
t =
|
|
2113
|
+
t = parseToml2(text);
|
|
1660
2114
|
} catch {
|
|
1661
2115
|
ctx.warn(`Could not parse ${p} (invalid TOML)`);
|
|
1662
2116
|
return null;
|
|
@@ -1726,7 +2180,7 @@ async function parsePipfile(ctx, p) {
|
|
|
1726
2180
|
const text = await ctx.read(p);
|
|
1727
2181
|
if (!text) return null;
|
|
1728
2182
|
try {
|
|
1729
|
-
const t =
|
|
2183
|
+
const t = parseToml2(text);
|
|
1730
2184
|
return { manifest: { path: p, ecosystem: "python", dependencies: keys(t.packages).map((k) => k.toLowerCase()), devDependencies: keys(t["dev-packages"]).map((k) => k.toLowerCase()) } };
|
|
1731
2185
|
} catch {
|
|
1732
2186
|
ctx.warn(`Could not parse ${p}`);
|
|
@@ -1759,7 +2213,7 @@ async function parseCargo(ctx, p) {
|
|
|
1759
2213
|
if (!text) return null;
|
|
1760
2214
|
let t;
|
|
1761
2215
|
try {
|
|
1762
|
-
t =
|
|
2216
|
+
t = parseToml2(text);
|
|
1763
2217
|
} catch {
|
|
1764
2218
|
ctx.warn(`Could not parse ${p} (invalid TOML)`);
|
|
1765
2219
|
return null;
|
|
@@ -2485,8 +2939,8 @@ var dependenciesDetector = {
|
|
|
2485
2939
|
};
|
|
2486
2940
|
|
|
2487
2941
|
// src/core/analyzer/detectors/env.ts
|
|
2488
|
-
import { promises as
|
|
2489
|
-
import
|
|
2942
|
+
import { promises as fs14 } from "fs";
|
|
2943
|
+
import path19 from "path";
|
|
2490
2944
|
|
|
2491
2945
|
// src/core/security/secrets.ts
|
|
2492
2946
|
var SECRET_PATTERNS = [
|
|
@@ -2643,7 +3097,7 @@ var envDetector = {
|
|
|
2643
3097
|
for (const f of ctx.find(/(^|\/)\.env(\.[a-z.]+)?$/)) if (!TEMPLATE_ENV.test(f.path)) present.add(f.path);
|
|
2644
3098
|
for (const name of REAL_ENV_NAMES) {
|
|
2645
3099
|
try {
|
|
2646
|
-
const st = await
|
|
3100
|
+
const st = await fs14.stat(path19.join(ctx.root, name));
|
|
2647
3101
|
if (st.isFile()) present.add(name);
|
|
2648
3102
|
} catch {
|
|
2649
3103
|
}
|
|
@@ -2926,8 +3380,8 @@ async function prisma(ctx) {
|
|
|
2926
3380
|
if (!fm) continue;
|
|
2927
3381
|
const attributes = [...fm[3].matchAll(/@[\w.]+(\([^)]*\))?/g)].map((a) => a[0]);
|
|
2928
3382
|
fields.push({ name: fm[1], type: fm[2], attributes });
|
|
2929
|
-
const
|
|
2930
|
-
if (
|
|
3383
|
+
const rel2 = attributes.find((a) => a.startsWith("@relation"));
|
|
3384
|
+
if (rel2) relations.push(`${fm[1]} \u2192 ${fm[2].replace(/[[\]?]/g, "")}`);
|
|
2931
3385
|
}
|
|
2932
3386
|
const lineNo = text.slice(0, m.index).split("\n").length;
|
|
2933
3387
|
out2.push({ name: m[2], kind: "model", fields, indexes, relations, file: f.path, provenance: detected("code", [{ file: f.path, line: lineNo }]) });
|
|
@@ -3404,11 +3858,11 @@ async function gitInfo(cwd) {
|
|
|
3404
3858
|
const counts = /* @__PURE__ */ new Map();
|
|
3405
3859
|
for (const line2 of log.stdout.split("\n")) {
|
|
3406
3860
|
const p = line2.trim();
|
|
3407
|
-
if (!p) continue;
|
|
3861
|
+
if (!p || p === ".athena" || p.startsWith(".athena/")) continue;
|
|
3408
3862
|
const dir = p.includes("/") ? p.split("/").slice(0, 2).join("/") : ".";
|
|
3409
3863
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
3410
3864
|
}
|
|
3411
|
-
info2.hotspots = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([
|
|
3865
|
+
info2.hotspots = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, 10).map(([path49, commits]) => ({ path: path49, commits }));
|
|
3412
3866
|
}
|
|
3413
3867
|
return info2;
|
|
3414
3868
|
}
|
|
@@ -3466,8 +3920,8 @@ function isSafeRef(ref) {
|
|
|
3466
3920
|
async function headMovement(cwd, from, to, limit = 20) {
|
|
3467
3921
|
if (!/^[0-9a-f]{7,64}$/i.test(from) || !/^[0-9a-f]{7,64}$/i.test(to)) return null;
|
|
3468
3922
|
if (from === to) return { commits: [], diverged: false, truncated: false };
|
|
3469
|
-
const
|
|
3470
|
-
if (!
|
|
3923
|
+
const exists3 = await git(cwd, ["cat-file", "-e", `${from}^{commit}`]);
|
|
3924
|
+
if (!exists3.ok) return { commits: [], diverged: true, truncated: false };
|
|
3471
3925
|
const ancestor = await git(cwd, ["merge-base", "--is-ancestor", from, to]);
|
|
3472
3926
|
const log = await git(cwd, ["log", `-n${limit + 1}`, "--format=%H%x1f%s", `${from}..${to}`]);
|
|
3473
3927
|
if (!log.ok) return null;
|
|
@@ -3494,10 +3948,10 @@ var DETECTORS = [
|
|
|
3494
3948
|
];
|
|
3495
3949
|
async function analyzeProject(rootInput, opts = {}) {
|
|
3496
3950
|
const started = Date.now();
|
|
3497
|
-
const root = await
|
|
3951
|
+
const root = await fs15.realpath(path20.resolve(rootInput));
|
|
3498
3952
|
const loaded = opts.config ? { config: opts.config } : await loadConfig(root);
|
|
3499
3953
|
const config = loaded.config;
|
|
3500
|
-
const model = emptyModel(
|
|
3954
|
+
const model = emptyModel(path20.basename(root), root);
|
|
3501
3955
|
if ("warning" in loaded && loaded.warning) model.warnings.push(loaded.warning);
|
|
3502
3956
|
opts.onStage?.("scan");
|
|
3503
3957
|
const walk2 = await walkProject(root, { config, signal: opts.signal, reuse: opts.reuse });
|
|
@@ -3547,7 +4001,7 @@ function redactDeep(value) {
|
|
|
3547
4001
|
}
|
|
3548
4002
|
|
|
3549
4003
|
// src/core/knowledge/generate.ts
|
|
3550
|
-
import
|
|
4004
|
+
import path22 from "path";
|
|
3551
4005
|
|
|
3552
4006
|
// src/core/knowledge/md.ts
|
|
3553
4007
|
function esc(s) {
|
|
@@ -3569,11 +4023,11 @@ function evidenceList(evidence, max = 3) {
|
|
|
3569
4023
|
function status(p) {
|
|
3570
4024
|
return p.status === "UNKNOWN" ? "UNKNOWN" : `${p.status} \xB7 ${p.confidence}`;
|
|
3571
4025
|
}
|
|
3572
|
-
function table(headers,
|
|
3573
|
-
if (!
|
|
4026
|
+
function table(headers, rows2) {
|
|
4027
|
+
if (!rows2.length) return "";
|
|
3574
4028
|
const head = `| ${headers.join(" | ")} |`;
|
|
3575
4029
|
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
3576
|
-
return [head, sep, ...
|
|
4030
|
+
return [head, sep, ...rows2.map((r) => `| ${r.join(" | ")} |`)].join("\n");
|
|
3577
4031
|
}
|
|
3578
4032
|
function unknown(what, hint) {
|
|
3579
4033
|
return `**${what}:** UNKNOWN${hint ? ` \u2014 ${hint}` : ""}`;
|
|
@@ -3917,8 +4371,8 @@ function renderDependencyRisks(m, scan) {
|
|
|
3917
4371
|
const missing = scan.tools.filter((t) => t.status !== "ok");
|
|
3918
4372
|
const findings = ran.flatMap((t) => t.findings.map((f) => ({ ...f, tool: t.tool })));
|
|
3919
4373
|
if (findings.length) {
|
|
3920
|
-
const
|
|
3921
|
-
lines.push(`**Detected by tooling:** ${findings.length} vulnerable dependenc${findings.length === 1 ? "y" : "ies"}.`, "", table(["Package", "Severity", "Advisory", "ID", "Tool", "Fix available"],
|
|
4374
|
+
const rows2 = findings.slice(0, 60).map((f) => [esc(f.package), f.severity.toUpperCase(), esc(f.title.slice(0, 90)), esc(f.id ?? "\u2014"), esc(f.tool), f.fixAvailable ? "yes" : "unknown"]);
|
|
4375
|
+
lines.push(`**Detected by tooling:** ${findings.length} vulnerable dependenc${findings.length === 1 ? "y" : "ies"}.`, "", table(["Package", "Severity", "Advisory", "ID", "Tool", "Fix available"], rows2));
|
|
3922
4376
|
if (findings.length > 60) lines.push("", `_Showing 60 of ${findings.length}. See \`.athena/security-scan.json\`._`);
|
|
3923
4377
|
} else if (ran.length) {
|
|
3924
4378
|
lines.push(`**Detected:** no known vulnerable dependencies reported by ${ran.map((t) => esc(t.tool)).join(", ")} at the time of the scan.`);
|
|
@@ -4158,7 +4612,7 @@ function seedRules(m) {
|
|
|
4158
4612
|
|
|
4159
4613
|
// src/core/model/security-scan.ts
|
|
4160
4614
|
import { z as z5 } from "zod";
|
|
4161
|
-
import
|
|
4615
|
+
import path21 from "path";
|
|
4162
4616
|
var SCAN_FILE = "security-scan.json";
|
|
4163
4617
|
var Severity = z5.enum(["critical", "high", "moderate", "low", "unknown"]);
|
|
4164
4618
|
var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
|
|
@@ -4190,11 +4644,11 @@ var SecurityScan = z5.object({
|
|
|
4190
4644
|
});
|
|
4191
4645
|
var loadScan = (root) => loadScanFromDir(athenaDir(root));
|
|
4192
4646
|
async function saveScan(root, scan) {
|
|
4193
|
-
await writeFileAtomic(
|
|
4647
|
+
await writeFileAtomic(path21.join(athenaDir(root), SCAN_FILE), `${JSON.stringify(scan, null, 2)}
|
|
4194
4648
|
`);
|
|
4195
4649
|
}
|
|
4196
4650
|
async function loadScanFromDir(dir) {
|
|
4197
|
-
const raw = await readTextIfExists(
|
|
4651
|
+
const raw = await readTextIfExists(path21.join(dir, SCAN_FILE));
|
|
4198
4652
|
if (!raw) return null;
|
|
4199
4653
|
try {
|
|
4200
4654
|
const parsed = SecurityScan.safeParse(JSON.parse(raw));
|
|
@@ -4257,7 +4711,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4257
4711
|
for (const id of ids) {
|
|
4258
4712
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
4259
4713
|
const sections = renderSections(id, model, extras);
|
|
4260
|
-
const before = await readTextIfExists(
|
|
4714
|
+
const before = await readTextIfExists(path22.join(athenaDir2, doc.file));
|
|
4261
4715
|
if (before === null) {
|
|
4262
4716
|
plans.push({ id, file: doc.file, status: "created", before, after: composeNewDocument(id, sections), blocks: sections.map((s) => s.id), changedBlocks: sections.map((s) => s.id), preservedModified: [] });
|
|
4263
4717
|
continue;
|
|
@@ -4279,7 +4733,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4279
4733
|
async function writePlannedKnowledge(athenaDir2, plans) {
|
|
4280
4734
|
const reports = [];
|
|
4281
4735
|
for (const p of plans) {
|
|
4282
|
-
if (p.status !== "unchanged") await writeFileAtomic(
|
|
4736
|
+
if (p.status !== "unchanged") await writeFileAtomic(path22.join(athenaDir2, p.file), p.after);
|
|
4283
4737
|
reports.push({ id: p.id, file: p.file, status: p.status, blocks: p.blocks, preservedModified: p.preservedModified, contentHash: sha256(p.after).slice(0, 16) });
|
|
4284
4738
|
}
|
|
4285
4739
|
return reports;
|
|
@@ -4288,7 +4742,7 @@ async function writeKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4288
4742
|
return writePlannedKnowledge(athenaDir2, await planKnowledge(athenaDir2, model, opts));
|
|
4289
4743
|
}
|
|
4290
4744
|
async function ensureRules(athenaDir2, model) {
|
|
4291
|
-
const file =
|
|
4745
|
+
const file = path22.join(athenaDir2, "rules.md");
|
|
4292
4746
|
const existing = await readTextIfExists(file);
|
|
4293
4747
|
if (existing !== null) return { id: "rules", file: "rules.md", status: "unchanged", blocks: [], preservedModified: [], contentHash: sha256(existing).slice(0, 16) };
|
|
4294
4748
|
const text = seedRules(model);
|
|
@@ -4327,11 +4781,11 @@ ${missing.join("\n")}
|
|
|
4327
4781
|
}
|
|
4328
4782
|
async function indexWithIntegrations(root, files, written) {
|
|
4329
4783
|
const index = buildFileIndex(files);
|
|
4330
|
-
for (const
|
|
4784
|
+
for (const rel2 of written) {
|
|
4331
4785
|
try {
|
|
4332
|
-
const abs =
|
|
4333
|
-
const [st, buf] = await Promise.all([
|
|
4334
|
-
index[
|
|
4786
|
+
const abs = path23.join(root, rel2);
|
|
4787
|
+
const [st, buf] = await Promise.all([fs16.stat(abs), fs16.readFile(abs)]);
|
|
4788
|
+
index[rel2] = { h: sha256(buf).slice(0, 16), s: st.size, m: Math.floor(st.mtimeMs) };
|
|
4335
4789
|
} catch {
|
|
4336
4790
|
}
|
|
4337
4791
|
}
|
|
@@ -4381,7 +4835,7 @@ async function runPipeline(opts) {
|
|
|
4381
4835
|
const prev = await readState(finalDir);
|
|
4382
4836
|
if (prev.kind === "corrupted" && !opts.dryRun) {
|
|
4383
4837
|
const backup = await backupCorruptedState(finalDir);
|
|
4384
|
-
notes.push(`state.json was corrupted (${prev.reason}); ${backup ? `moved to ${
|
|
4838
|
+
notes.push(`state.json was corrupted (${prev.reason}); ${backup ? `moved to ${path23.relative(root, backup)}` : "it will be replaced"} and rebuilt.`);
|
|
4385
4839
|
}
|
|
4386
4840
|
const prevState = prev.kind === "ok" ? prev.state : null;
|
|
4387
4841
|
opts.onStage?.("analyze");
|
|
@@ -4394,21 +4848,21 @@ async function runPipeline(opts) {
|
|
|
4394
4848
|
return { analysis, docs: [], agentChanges: agentChanges2, state: null, notes };
|
|
4395
4849
|
}
|
|
4396
4850
|
const fresh = !await exists(finalDir);
|
|
4397
|
-
const workDir = fresh ?
|
|
4851
|
+
const workDir = fresh ? path23.join(root, `${ATHENA_DIR}.tmp-${crypto4.randomBytes(4).toString("hex")}`) : finalDir;
|
|
4398
4852
|
let docs;
|
|
4399
4853
|
try {
|
|
4400
4854
|
opts.onStage?.("write");
|
|
4401
|
-
await
|
|
4855
|
+
await fs16.mkdir(workDir, { recursive: true });
|
|
4402
4856
|
const previousBlocks = prevState ? Object.fromEntries(Object.entries(prevState.documents).map(([k, v]) => [k, v.blocks])) : void 0;
|
|
4403
4857
|
docs = await writeKnowledge(workDir, analysis.model, { force: opts.force, previousBlocks });
|
|
4404
4858
|
docs.push(await ensureRules(workDir, analysis.model));
|
|
4405
|
-
await writeFileAtomic(
|
|
4859
|
+
await writeFileAtomic(path23.join(workDir, "model.json"), `${JSON.stringify(analysis.model, null, 2)}
|
|
4406
4860
|
`);
|
|
4407
|
-
await ensureGitignoreEntries(
|
|
4861
|
+
await ensureGitignoreEntries(path23.join(workDir, ".gitignore"));
|
|
4408
4862
|
opts.signal?.throwIfAborted();
|
|
4409
|
-
if (fresh) await
|
|
4863
|
+
if (fresh) await fs16.rename(workDir, finalDir);
|
|
4410
4864
|
} catch (err) {
|
|
4411
|
-
if (fresh) await
|
|
4865
|
+
if (fresh) await fs16.rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
4412
4866
|
});
|
|
4413
4867
|
throw err;
|
|
4414
4868
|
}
|
|
@@ -4437,21 +4891,21 @@ async function runPipeline(opts) {
|
|
|
4437
4891
|
}
|
|
4438
4892
|
|
|
4439
4893
|
// src/cli/context.ts
|
|
4440
|
-
import { promises as
|
|
4441
|
-
import
|
|
4894
|
+
import { promises as fs18 } from "fs";
|
|
4895
|
+
import path25 from "path";
|
|
4442
4896
|
|
|
4443
4897
|
// src/services/project.ts
|
|
4444
|
-
import { promises as
|
|
4445
|
-
import
|
|
4898
|
+
import { promises as fs17 } from "fs";
|
|
4899
|
+
import path24 from "path";
|
|
4446
4900
|
async function findProjectRoot(start) {
|
|
4447
4901
|
let dir = start;
|
|
4448
4902
|
for (; ; ) {
|
|
4449
4903
|
try {
|
|
4450
|
-
const st = await
|
|
4904
|
+
const st = await fs17.stat(path24.join(dir, ATHENA_DIR));
|
|
4451
4905
|
if (st.isDirectory()) return dir;
|
|
4452
4906
|
} catch {
|
|
4453
4907
|
}
|
|
4454
|
-
const parent =
|
|
4908
|
+
const parent = path24.dirname(dir);
|
|
4455
4909
|
if (parent === dir) return null;
|
|
4456
4910
|
dir = parent;
|
|
4457
4911
|
}
|
|
@@ -4459,11 +4913,11 @@ async function findProjectRoot(start) {
|
|
|
4459
4913
|
|
|
4460
4914
|
// src/cli/context.ts
|
|
4461
4915
|
async function resolveCwd(opts) {
|
|
4462
|
-
const dir =
|
|
4916
|
+
const dir = path25.resolve(opts.cwd ?? process.cwd());
|
|
4463
4917
|
try {
|
|
4464
|
-
const st = await
|
|
4918
|
+
const st = await fs18.stat(dir);
|
|
4465
4919
|
if (!st.isDirectory()) throw new AthenaError(`Not a directory: ${dir}`);
|
|
4466
|
-
return await
|
|
4920
|
+
return await fs18.realpath(dir);
|
|
4467
4921
|
} catch (err) {
|
|
4468
4922
|
if (err instanceof AthenaError) throw err;
|
|
4469
4923
|
throw new AthenaError(`Directory not accessible: ${dir}`, "Check the path and your permissions.");
|
|
@@ -4476,6 +4930,470 @@ async function requireProjectRoot(opts) {
|
|
|
4476
4930
|
return root;
|
|
4477
4931
|
}
|
|
4478
4932
|
|
|
4933
|
+
// src/cli/ui/dashboard.ts
|
|
4934
|
+
import pc3 from "picocolors";
|
|
4935
|
+
|
|
4936
|
+
// src/cli/ui/layout.ts
|
|
4937
|
+
import pc2 from "picocolors";
|
|
4938
|
+
var ANSI = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\]8;;[^\x07]*\x07/g;
|
|
4939
|
+
var strip = (s) => s.replace(ANSI, "");
|
|
4940
|
+
var width = (s) => [...strip(s)].length;
|
|
4941
|
+
function termWidth() {
|
|
4942
|
+
const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
|
|
4943
|
+
return Math.max(40, Math.min(cols, 160));
|
|
4944
|
+
}
|
|
4945
|
+
function truncate2(s, max) {
|
|
4946
|
+
if (width(s) <= max) return s;
|
|
4947
|
+
if (max <= 0) return "";
|
|
4948
|
+
let out2 = "";
|
|
4949
|
+
let visible = 0;
|
|
4950
|
+
let i = 0;
|
|
4951
|
+
while (i < s.length) {
|
|
4952
|
+
ANSI.lastIndex = 0;
|
|
4953
|
+
const rest = s.slice(i);
|
|
4954
|
+
const m = rest.match(new RegExp(`^(?:${ANSI.source})`));
|
|
4955
|
+
if (m) {
|
|
4956
|
+
out2 += m[0];
|
|
4957
|
+
i += m[0].length;
|
|
4958
|
+
continue;
|
|
4959
|
+
}
|
|
4960
|
+
const ch = String.fromCodePoint(s.codePointAt(i));
|
|
4961
|
+
if (visible === max - 1) {
|
|
4962
|
+
out2 += unicode ? "\u2026" : ".";
|
|
4963
|
+
break;
|
|
4964
|
+
}
|
|
4965
|
+
out2 += ch;
|
|
4966
|
+
visible++;
|
|
4967
|
+
i += ch.length;
|
|
4968
|
+
}
|
|
4969
|
+
return pc2.isColorSupported ? `${out2}\x1B[0m` : out2;
|
|
4970
|
+
}
|
|
4971
|
+
var padEnd = (s, w) => {
|
|
4972
|
+
const cut = truncate2(s, w);
|
|
4973
|
+
return cut + " ".repeat(Math.max(0, w - width(cut)));
|
|
4974
|
+
};
|
|
4975
|
+
var center = (s, w) => {
|
|
4976
|
+
const n = width(s);
|
|
4977
|
+
if (n >= w) return truncate2(s, w);
|
|
4978
|
+
const left = Math.floor((w - n) / 2);
|
|
4979
|
+
return " ".repeat(left) + s + " ".repeat(w - n - left);
|
|
4980
|
+
};
|
|
4981
|
+
var truecolor = /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "") || ["iTerm.app", "vscode", "WezTerm", "ghostty", "Hyper"].includes(process.env.TERM_PROGRAM ?? "");
|
|
4982
|
+
var hex = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
|
|
4983
|
+
function ansi256([r, g, b]) {
|
|
4984
|
+
const q = (v) => v < 48 ? 0 : v < 115 ? 1 : Math.round((v - 35) / 40);
|
|
4985
|
+
return 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
4986
|
+
}
|
|
4987
|
+
function paint(color, s, bold = false) {
|
|
4988
|
+
if (!pc2.isColorSupported || !s) return s;
|
|
4989
|
+
const rgb = typeof color === "string" ? hex(color) : color;
|
|
4990
|
+
const fg = truecolor ? `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\x1B[38;5;${ansi256(rgb)}m`;
|
|
4991
|
+
return `${bold ? "\x1B[1m" : ""}${fg}${s}\x1B[0m`;
|
|
4992
|
+
}
|
|
4993
|
+
function cell(ch, fg, bg) {
|
|
4994
|
+
if (!pc2.isColorSupported) return ch;
|
|
4995
|
+
const code2 = (c2, layer) => {
|
|
4996
|
+
const rgb = typeof c2 === "string" ? hex(c2) : c2;
|
|
4997
|
+
return truecolor ? `\x1B[${layer};2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\x1B[${layer};5;${ansi256(rgb)}m`;
|
|
4998
|
+
};
|
|
4999
|
+
return `${fg ? code2(fg, 38) : ""}${bg ? code2(bg, 48) : ""}${ch}\x1B[0m`;
|
|
5000
|
+
}
|
|
5001
|
+
var PALETTE = {
|
|
5002
|
+
green: "#34d399",
|
|
5003
|
+
cyan: "#22d3ee",
|
|
5004
|
+
blue: "#3b82f6",
|
|
5005
|
+
violet: "#a855f7",
|
|
5006
|
+
pink: "#f472b6",
|
|
5007
|
+
amber: "#fbbf24",
|
|
5008
|
+
slate: "#64748b"
|
|
5009
|
+
};
|
|
5010
|
+
var BRAND = [PALETTE.cyan, PALETTE.blue, PALETTE.violet];
|
|
5011
|
+
function mix(stops, t) {
|
|
5012
|
+
const rgb = stops.map(hex);
|
|
5013
|
+
if (rgb.length === 1) return rgb[0];
|
|
5014
|
+
const x = Math.min(0.9999, Math.max(0, t)) * (rgb.length - 1);
|
|
5015
|
+
const i = Math.floor(x);
|
|
5016
|
+
const f = x - i;
|
|
5017
|
+
const [a, b] = [rgb[i], rgb[i + 1]];
|
|
5018
|
+
return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * f));
|
|
5019
|
+
}
|
|
5020
|
+
function gradientBlock(lines, stops = BRAND, bold = true) {
|
|
5021
|
+
const w = Math.max(...lines.map((l) => [...l].length));
|
|
5022
|
+
return lines.map((l) => [...l].map((ch, i) => ch === " " ? ch : paint(mix(stops, i / Math.max(1, w - 1)), ch, bold)).join(""));
|
|
5023
|
+
}
|
|
5024
|
+
var gradient = (s, stops = BRAND, bold = false) => gradientBlock([s], stops, bold)[0];
|
|
5025
|
+
var B = unicode ? { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" } : { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
|
|
5026
|
+
var HR = B.h;
|
|
5027
|
+
var VR = B.v;
|
|
5028
|
+
function box(content, w, opts = {}) {
|
|
5029
|
+
const color = opts.color ?? PALETTE.slate;
|
|
5030
|
+
const padX = opts.padX ?? 1;
|
|
5031
|
+
const inner = w - 2;
|
|
5032
|
+
const edge = (s) => paint(color, s);
|
|
5033
|
+
const title = opts.title ? ` ${opts.title} ` : "";
|
|
5034
|
+
const top = title ? edge(`${B.tl}${B.h}`) + title + edge(B.h.repeat(Math.max(0, inner - 1 - width(title))) + B.tr) : edge(B.tl + B.h.repeat(inner) + B.tr);
|
|
5035
|
+
const body = content.map((l) => edge(B.v) + " ".repeat(padX) + padEnd(l, inner - padX * 2) + " ".repeat(padX) + edge(B.v));
|
|
5036
|
+
return [top, ...body, edge(B.bl + B.h.repeat(inner) + B.br)];
|
|
5037
|
+
}
|
|
5038
|
+
function columns(blocks, widths, gap = 2) {
|
|
5039
|
+
const h = Math.max(...blocks.map((b) => b.length));
|
|
5040
|
+
const rows2 = [];
|
|
5041
|
+
for (let r = 0; r < h; r++) rows2.push(blocks.map((b, i) => padEnd(b[r] ?? "", widths[i])).join(" ".repeat(gap)).trimEnd());
|
|
5042
|
+
return rows2;
|
|
5043
|
+
}
|
|
5044
|
+
function vcenter(block, h) {
|
|
5045
|
+
const top = Math.max(0, Math.floor((h - block.length) / 2));
|
|
5046
|
+
return [...Array(top).fill(""), ...block];
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
// src/cli/ui/dashboard.ts
|
|
5050
|
+
var C = PALETTE;
|
|
5051
|
+
var accent = (s) => paint(PALETTE.amber, s, true);
|
|
5052
|
+
var warnText = (s) => paint(PALETTE.amber, s);
|
|
5053
|
+
function sectionTitle(title) {
|
|
5054
|
+
const total = termWidth();
|
|
5055
|
+
const head = ` ${paint(PALETTE.violet, unicode ? "\u2726" : "*", true)} ${gradient(title, [PALETTE.cyan, PALETTE.violet], true)} `;
|
|
5056
|
+
const rest = Math.max(0, total - width(head) - 2);
|
|
5057
|
+
return [head + paint(PALETTE.blue, HR.repeat(rest))];
|
|
5058
|
+
}
|
|
5059
|
+
function rows(items) {
|
|
5060
|
+
const labelW = Math.max(...items.map(([l]) => l.length)) + 2;
|
|
5061
|
+
return (w) => items.map(([label, value]) => pc3.dim(padEnd(label, labelW)) + padEnd(value ? paint(PALETTE.cyan, value) : pc3.dim("Not detected"), w - labelW));
|
|
5062
|
+
}
|
|
5063
|
+
function fileTree(root, files) {
|
|
5064
|
+
const nameW = Math.max(...files.map((f) => f.name.length)) + 2;
|
|
5065
|
+
return (w) => [
|
|
5066
|
+
pc3.bold(root),
|
|
5067
|
+
...files.map((f, i) => {
|
|
5068
|
+
const branch = unicode ? i === files.length - 1 ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : i === files.length - 1 ? "`-- " : "|-- ";
|
|
5069
|
+
const name = f.highlight ? paint(PALETTE.amber, padEnd(f.name, nameW)) : padEnd(f.name, nameW);
|
|
5070
|
+
return padEnd(pc3.dim(branch) + name + pc3.dim(f.note ? `# ${f.note}` : ""), w);
|
|
5071
|
+
})
|
|
5072
|
+
];
|
|
5073
|
+
}
|
|
5074
|
+
function wrapCode(text, w) {
|
|
5075
|
+
const out2 = [];
|
|
5076
|
+
let cur = "";
|
|
5077
|
+
for (const word of text.split(" ")) {
|
|
5078
|
+
if (cur && cur.length + 1 + word.length > w) {
|
|
5079
|
+
out2.push(cur);
|
|
5080
|
+
cur = word;
|
|
5081
|
+
} else cur = cur ? `${cur} ${word}` : word;
|
|
5082
|
+
}
|
|
5083
|
+
if (cur) out2.push(cur);
|
|
5084
|
+
let inCode = false;
|
|
5085
|
+
return out2.map(
|
|
5086
|
+
(l) => l.split("`").map((part, i) => {
|
|
5087
|
+
if (i > 0) inCode = !inCode;
|
|
5088
|
+
return inCode ? paint(PALETTE.cyan, part) : part;
|
|
5089
|
+
}).join("")
|
|
5090
|
+
);
|
|
5091
|
+
}
|
|
5092
|
+
function wrapLines(text, indent = 2) {
|
|
5093
|
+
const w = Math.max(20, termWidth() - 1);
|
|
5094
|
+
const out2 = [];
|
|
5095
|
+
let cur = "";
|
|
5096
|
+
for (const word of text.split(" ")) {
|
|
5097
|
+
if (cur && cur.length + 1 + word.length > w) {
|
|
5098
|
+
out2.push(cur);
|
|
5099
|
+
cur = " ".repeat(indent) + word;
|
|
5100
|
+
} else cur = cur ? `${cur} ${word}` : word;
|
|
5101
|
+
}
|
|
5102
|
+
if (cur) out2.push(cur);
|
|
5103
|
+
return out2;
|
|
5104
|
+
}
|
|
5105
|
+
function numbered(steps) {
|
|
5106
|
+
return (w) => steps.flatMap((s, i) => {
|
|
5107
|
+
const badge = paint(PALETTE.green, ` ${i + 1} `, true);
|
|
5108
|
+
const [first = "", ...rest] = wrapCode(s, Math.max(10, w - 5));
|
|
5109
|
+
return [`${badge} ${first}`, ...rest.map((r) => ` ${r}`)];
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
function quote(text) {
|
|
5113
|
+
return (w) => {
|
|
5114
|
+
const lines = wrapCode(`\u201C${text}\u201D`, Math.max(10, w - 6)).map((l) => pc3.italic(l));
|
|
5115
|
+
return box([...lines, paint(PALETTE.cyan, `\u2014 Sabarivasan`)], Math.min(w, Math.max(...lines.map(width)) + 6), { color: PALETTE.slate, padX: 2 });
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
function renderBody(body, w) {
|
|
5119
|
+
const parts = Array.isArray(body) && body.length && typeof body[0] !== "string" ? body : [body];
|
|
5120
|
+
return parts.flatMap((p) => typeof p === "function" ? p(w) : p);
|
|
5121
|
+
}
|
|
5122
|
+
function panels(list) {
|
|
5123
|
+
const total = termWidth();
|
|
5124
|
+
const title = (p) => `${paint(p.color, unicode ? p.icon : "*", true)} ${paint(p.color, p.title, true)}`;
|
|
5125
|
+
if (total >= 120) {
|
|
5126
|
+
const gap = 2;
|
|
5127
|
+
const avail = total - gap * (list.length - 1) - 1;
|
|
5128
|
+
const weights = [0.36, 0.32, 0.32];
|
|
5129
|
+
const widths = list.map((_, i) => Math.floor(avail * (weights[i] ?? 1 / list.length)));
|
|
5130
|
+
const bodies = list.map((p, i) => renderBody(p.body, widths[i] - 4));
|
|
5131
|
+
const h = Math.max(...bodies.map((b) => b.length));
|
|
5132
|
+
const boxes = list.map((p, i) => box([title(p), "", ...bodies[i], ...Array(h - bodies[i].length).fill("")], widths[i], { color: p.color, padX: 1 }));
|
|
5133
|
+
return columns(boxes, widths, gap);
|
|
5134
|
+
}
|
|
5135
|
+
const w = Math.min(total - 1, 100);
|
|
5136
|
+
return list.flatMap((p, i) => [...i ? [""] : [], ...box([title(p), "", ...renderBody(p.body, w - 4)], w, { color: p.color })]);
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
// src/cli/ui/brand.ts
|
|
5140
|
+
import pc4 from "picocolors";
|
|
5141
|
+
var LETTERS = {
|
|
5142
|
+
A: [" \u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"],
|
|
5143
|
+
T: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u255A\u2550\u255D "],
|
|
5144
|
+
H: ["\u2588\u2588\u2557 \u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"],
|
|
5145
|
+
E: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u255D ", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],
|
|
5146
|
+
N: ["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551", "\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551", "\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551", "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"]
|
|
5147
|
+
};
|
|
5148
|
+
var LOGO = [0, 1, 2, 3, 4, 5].map((r) => [..."ATHENA"].map((ch) => LETTERS[ch][r]).join(""));
|
|
5149
|
+
var LOGO_WIDTH = width(LOGO[0]);
|
|
5150
|
+
var ROBOT_PIXELS = [
|
|
5151
|
+
"...........AA...........",
|
|
5152
|
+
"..........AAAA..........",
|
|
5153
|
+
"...........KK...........",
|
|
5154
|
+
"....KKKKKKKKKKKKKKKK....",
|
|
5155
|
+
"...KBBBBBBBBBBBBBBBBK...",
|
|
5156
|
+
"..KBBBBBBBBBBBBBBBBBBK..",
|
|
5157
|
+
"..KBBBBWWBBBBBBYYYYBBK..",
|
|
5158
|
+
"YYKBBBWBBWBBBBYYDDYYBKYY",
|
|
5159
|
+
"YYKBBBBBBBBBBBYYDDYYBKYY",
|
|
5160
|
+
"YYKBBBBBBBBBBBBYYYYBBKYY",
|
|
5161
|
+
"YYKBBBBBYYYYYYYYBBBBBKYY",
|
|
5162
|
+
"..KBBBBBBYYYYYYBBBBBBK..",
|
|
5163
|
+
"..KBBBBBBBYYYYBBBBBBBK..",
|
|
5164
|
+
"..KBBBBBBBBBBBBBBBBBBK..",
|
|
5165
|
+
"..KBBBBBBBBBBBBBBBBBBK..",
|
|
5166
|
+
"..KBBBBBKKKKKKKKKKKK....",
|
|
5167
|
+
"...KBBBK................",
|
|
5168
|
+
"....KBK.................",
|
|
5169
|
+
".....K..................",
|
|
5170
|
+
"........................"
|
|
5171
|
+
];
|
|
5172
|
+
var PIXEL_COLORS = { K: "#1e40af", B: "#4ba3e3", Y: "#f5c542", D: "#2e3f9e", A: "#5b7cfa", W: "#0f172a" };
|
|
5173
|
+
var ROBOT_ASCII = [
|
|
5174
|
+
" \u25CF ",
|
|
5175
|
+
" \u2503 ",
|
|
5176
|
+
" \u256D\u2500\u2500\u2500\u2538\u2500\u2500\u2500\u256E ",
|
|
5177
|
+
" \u2590\u2502 \u25E0 \u25C9 \u2502\u258C ",
|
|
5178
|
+
" \u2590\u2502 \u2570\u2500\u256F \u2502\u258C ",
|
|
5179
|
+
" \u2502 \u2502 ",
|
|
5180
|
+
" \u2570\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u256F ",
|
|
5181
|
+
" \u2570\u256F ",
|
|
5182
|
+
" ",
|
|
5183
|
+
" "
|
|
5184
|
+
];
|
|
5185
|
+
var ROBOT_HEIGHT = ROBOT_PIXELS.length / 2;
|
|
5186
|
+
var ROBOT_WIDTH = pc4.isColorSupported ? ROBOT_PIXELS[0].length : ROBOT_ASCII[0].length;
|
|
5187
|
+
var BUBBLE = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 Analyze \u2502", "\u2502 Understand \u2502", "\u2502 Document \u2502", "\u2502 Empower \u2726 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
5188
|
+
var BUBBLE_WIDTH = 14;
|
|
5189
|
+
function paintRobot() {
|
|
5190
|
+
if (!pc4.isColorSupported) return ROBOT_ASCII;
|
|
5191
|
+
const color = (p) => PIXEL_COLORS[p] ?? null;
|
|
5192
|
+
const out2 = [];
|
|
5193
|
+
for (let r = 0; r < ROBOT_PIXELS.length; r += 2) {
|
|
5194
|
+
const top = ROBOT_PIXELS[r];
|
|
5195
|
+
const bottom = ROBOT_PIXELS[r + 1];
|
|
5196
|
+
let row = "";
|
|
5197
|
+
for (let x = 0; x < top.length; x++) {
|
|
5198
|
+
const [t, b] = [color(top[x]), color(bottom[x])];
|
|
5199
|
+
if (!t && !b) row += " ";
|
|
5200
|
+
else if (t === b) row += cell("\u2588", t, null);
|
|
5201
|
+
else if (!b) row += cell("\u2580", t, null);
|
|
5202
|
+
else if (!t) row += cell("\u2584", b, null);
|
|
5203
|
+
else row += cell("\u2580", t, b);
|
|
5204
|
+
}
|
|
5205
|
+
out2.push(row);
|
|
5206
|
+
}
|
|
5207
|
+
return out2;
|
|
5208
|
+
}
|
|
5209
|
+
function paintBubble() {
|
|
5210
|
+
return BUBBLE.map((row) => {
|
|
5211
|
+
const inner = row.slice(1, -1);
|
|
5212
|
+
const edge = (s) => paint(PALETTE.slate, s);
|
|
5213
|
+
if (row.startsWith("\u256D") || row.startsWith("\u2570")) return edge(row);
|
|
5214
|
+
return edge("\u2502") + inner.replace("\u2726", paint(PALETTE.amber, "\u2726")).replace(/[A-Za-z]+/, (w) => paint(PALETTE.cyan, w)) + edge("\u2502");
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
function logoBlock(w) {
|
|
5218
|
+
const rule = (label) => {
|
|
5219
|
+
const side = Math.max(2, Math.floor((w - width(label) - 2) / 2));
|
|
5220
|
+
return `${pc4.dim(HR.repeat(side))} ${label} ${pc4.dim(HR.repeat(side))}`;
|
|
5221
|
+
};
|
|
5222
|
+
return [
|
|
5223
|
+
...gradientBlock(LOGO),
|
|
5224
|
+
"",
|
|
5225
|
+
center(gradient("Project Intelligence for AI Coding", [PALETTE.blue, PALETTE.violet], true), w),
|
|
5226
|
+
center(rule(`${pc4.dim("developed by")} ${paint(PALETTE.cyan, AUTHOR, true)}`), w)
|
|
5227
|
+
];
|
|
5228
|
+
}
|
|
5229
|
+
function linksBlock() {
|
|
5230
|
+
return [
|
|
5231
|
+
pc4.italic('"Turn any codebase into clear knowledge."'),
|
|
5232
|
+
"",
|
|
5233
|
+
`${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
|
|
5234
|
+
`${paint(PALETTE.pink, "npm", true)} ${pc4.dim(`v${ATHENA_VERSION}`)}`,
|
|
5235
|
+
paint(PALETTE.blue, NPM_URL),
|
|
5236
|
+
paint(PALETTE.violet, "github", true),
|
|
5237
|
+
paint(PALETTE.blue, REPO_URL)
|
|
5238
|
+
];
|
|
5239
|
+
}
|
|
5240
|
+
var LINKS_WIDTH = Math.max(NPM_URL.length, REPO_URL.length);
|
|
5241
|
+
function printHeader() {
|
|
5242
|
+
if (isJson() || isQuiet()) return;
|
|
5243
|
+
for (const l of header()) line(l);
|
|
5244
|
+
}
|
|
5245
|
+
function header() {
|
|
5246
|
+
if (!unicode) return [pc4.bold(pc4.cyan("ATHENA")) + pc4.dim(` Project Intelligence for AI Coding \xB7 v${ATHENA_VERSION}`), ""];
|
|
5247
|
+
const total = termWidth();
|
|
5248
|
+
const logoW = LOGO_WIDTH;
|
|
5249
|
+
const divider = (h) => Array.from({ length: h }, () => pc4.dim(VR));
|
|
5250
|
+
const robotBubble = columns([paintRobot(), vcenter(paintBubble(), ROBOT_HEIGHT - 2)], [ROBOT_WIDTH, BUBBLE_WIDTH], 1);
|
|
5251
|
+
const logo = vcenter(logoBlock(logoW), ROBOT_HEIGHT);
|
|
5252
|
+
const links = vcenter(linksBlock(), ROBOT_HEIGHT);
|
|
5253
|
+
const H = ROBOT_HEIGHT;
|
|
5254
|
+
const layouts = [
|
|
5255
|
+
{ w: ROBOT_WIDTH + 1 + BUBBLE_WIDTH + 3 + logoW + 3 + 1 + 3 + LINKS_WIDTH, build: () => columns([robotBubble, logo, divider(H), links], [ROBOT_WIDTH + 1 + BUBBLE_WIDTH, logoW, 1, LINKS_WIDTH], 3) },
|
|
5256
|
+
{ w: ROBOT_WIDTH + 3 + logoW + 3 + 1 + 3 + LINKS_WIDTH, build: () => columns([paintRobot(), logo, divider(H), links], [ROBOT_WIDTH, logoW, 1, LINKS_WIDTH], 3) },
|
|
5257
|
+
{ w: ROBOT_WIDTH + 3 + logoW, build: () => [...columns([paintRobot(), logo], [ROBOT_WIDTH, logoW], 3), "", ...compactLinks()] },
|
|
5258
|
+
{ w: logoW, build: () => [...logoBlock(logoW), "", ...compactLinks()] }
|
|
5259
|
+
];
|
|
5260
|
+
const fit = layouts.find((l) => l.w + 4 <= total);
|
|
5261
|
+
if (!fit) return [pc4.bold(gradient("ATHENA", BRAND, true)) + pc4.dim(` Project Intelligence for AI Coding \xB7 v${ATHENA_VERSION}`), pc4.dim(REPO_URL), ""];
|
|
5262
|
+
const content = fit.build();
|
|
5263
|
+
return [...box(content, Math.min(total, fit.w + 4), { color: PALETTE.blue }), ""];
|
|
5264
|
+
}
|
|
5265
|
+
function compactLinks() {
|
|
5266
|
+
return [
|
|
5267
|
+
`${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
|
|
5268
|
+
`${paint(PALETTE.pink, "npm", true)} ${paint(PALETTE.blue, NPM_URL)}`,
|
|
5269
|
+
`${paint(PALETTE.violet, "github", true)} ${paint(PALETTE.blue, REPO_URL)}`
|
|
5270
|
+
];
|
|
5271
|
+
}
|
|
5272
|
+
function footer(title, subtitle, tone = "success") {
|
|
5273
|
+
const total = termWidth();
|
|
5274
|
+
const color = tone === "success" ? PALETTE.green : PALETTE.amber;
|
|
5275
|
+
const mark = tone === "success" ? "\u2713" : "!";
|
|
5276
|
+
const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}`, ""];
|
|
5277
|
+
const links = compactLinks();
|
|
5278
|
+
const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}`, ""];
|
|
5279
|
+
const statusW = Math.max(...status2.map(width));
|
|
5280
|
+
const linksW = 8 + LINKS_WIDTH;
|
|
5281
|
+
const wide = statusW + 4 + linksW + 3 + 1 + 3 + 14 + 4;
|
|
5282
|
+
const content = wide <= total ? columns([status2, links, divider2(), credit], [statusW, linksW, 1, 14], 3) : total >= linksW + 4 ? [...status2.filter(Boolean), "", ...links, credit.filter(Boolean).join(" ")] : [...status2, pc4.dim(REPO_URL)];
|
|
5283
|
+
const w = wide <= total ? wide : Math.min(total, Math.max(statusW, linksW) + 4);
|
|
5284
|
+
return box(content, w, { color });
|
|
5285
|
+
}
|
|
5286
|
+
var divider2 = () => [pc4.dim(VR), pc4.dim(VR), pc4.dim(VR)];
|
|
5287
|
+
|
|
5288
|
+
// src/cli/ui/stages.ts
|
|
5289
|
+
import pc5 from "picocolors";
|
|
5290
|
+
var STAGES = [
|
|
5291
|
+
{ id: "scan", label: "Scanning project structure" },
|
|
5292
|
+
{ id: "detect", label: "Detecting stack, database, API & auth" },
|
|
5293
|
+
{ id: "git", label: "Reading Git metadata" },
|
|
5294
|
+
{ id: "finalize", label: "Redacting secrets & validating" },
|
|
5295
|
+
{ id: "write", label: "Generating documentation" },
|
|
5296
|
+
{ id: "agents", label: "Configuring AI agents" }
|
|
5297
|
+
];
|
|
5298
|
+
var STAGE_ALIASES = { analyze: "scan" };
|
|
5299
|
+
var FRAMES2 = unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
|
|
5300
|
+
var BAR_STOPS = [PALETTE.green, PALETTE.cyan, PALETTE.blue, PALETTE.violet];
|
|
5301
|
+
var StageBoard = class {
|
|
5302
|
+
state = new Map(STAGES.map((s) => [s.id, "pending"]));
|
|
5303
|
+
started = /* @__PURE__ */ new Map();
|
|
5304
|
+
elapsed = /* @__PURE__ */ new Map();
|
|
5305
|
+
results = /* @__PURE__ */ new Map();
|
|
5306
|
+
current = null;
|
|
5307
|
+
frame = 0;
|
|
5308
|
+
drawn = 0;
|
|
5309
|
+
timer = null;
|
|
5310
|
+
live = isTTY && !isJson() && !isQuiet();
|
|
5311
|
+
enabled = !isJson() && !isQuiet();
|
|
5312
|
+
restoreCursor = () => process.stdout.write("\x1B[?25h");
|
|
5313
|
+
start() {
|
|
5314
|
+
if (!this.live) return;
|
|
5315
|
+
process.stdout.write("\x1B[?25l");
|
|
5316
|
+
process.once("exit", this.restoreCursor);
|
|
5317
|
+
this.render();
|
|
5318
|
+
this.timer = setInterval(() => {
|
|
5319
|
+
this.frame++;
|
|
5320
|
+
this.render();
|
|
5321
|
+
}, 80);
|
|
5322
|
+
this.timer.unref();
|
|
5323
|
+
}
|
|
5324
|
+
/** Accepts raw pipeline stage names; unknown names are ignored. */
|
|
5325
|
+
enter(raw) {
|
|
5326
|
+
const id = STAGE_ALIASES[raw] ?? raw;
|
|
5327
|
+
if (!this.state.has(id) || this.current === id) return;
|
|
5328
|
+
this.closeCurrent("done");
|
|
5329
|
+
this.current = id;
|
|
5330
|
+
this.state.set(id, "running");
|
|
5331
|
+
this.started.set(id, performance.now());
|
|
5332
|
+
}
|
|
5333
|
+
finish(results) {
|
|
5334
|
+
this.closeCurrent("done");
|
|
5335
|
+
for (const [id, st] of this.state) if (st === "pending") this.state.set(id, "skipped");
|
|
5336
|
+
for (const [id, r] of Object.entries(results)) this.results.set(id, r);
|
|
5337
|
+
this.stop();
|
|
5338
|
+
}
|
|
5339
|
+
fail() {
|
|
5340
|
+
this.closeCurrent("failed");
|
|
5341
|
+
this.stop();
|
|
5342
|
+
}
|
|
5343
|
+
closeCurrent(to) {
|
|
5344
|
+
if (!this.current) return;
|
|
5345
|
+
this.state.set(this.current, to);
|
|
5346
|
+
this.elapsed.set(this.current, performance.now() - this.started.get(this.current));
|
|
5347
|
+
this.current = null;
|
|
5348
|
+
}
|
|
5349
|
+
stop() {
|
|
5350
|
+
if (this.timer) clearInterval(this.timer);
|
|
5351
|
+
this.timer = null;
|
|
5352
|
+
if (!this.enabled) return;
|
|
5353
|
+
if (this.live) {
|
|
5354
|
+
this.render();
|
|
5355
|
+
this.restoreCursor();
|
|
5356
|
+
process.removeListener("exit", this.restoreCursor);
|
|
5357
|
+
} else for (const l of this.lines()) line(l);
|
|
5358
|
+
}
|
|
5359
|
+
render() {
|
|
5360
|
+
const rows2 = this.lines();
|
|
5361
|
+
let s = this.drawn ? `\x1B[${this.drawn}F` : "";
|
|
5362
|
+
for (const r of rows2) s += `\x1B[2K${r}
|
|
5363
|
+
`;
|
|
5364
|
+
process.stdout.write(s);
|
|
5365
|
+
this.drawn = rows2.length;
|
|
5366
|
+
}
|
|
5367
|
+
lines() {
|
|
5368
|
+
const total = termWidth();
|
|
5369
|
+
const showBar = total >= 100;
|
|
5370
|
+
const barW = total >= 130 ? 26 : 18;
|
|
5371
|
+
return STAGES.map((s, i) => {
|
|
5372
|
+
const st = this.state.get(s.id);
|
|
5373
|
+
const icon = st === "done" ? paint(PALETTE.green, sym.ok, true) : st === "running" ? paint(PALETTE.cyan, FRAMES2[this.frame % FRAMES2.length]) : st === "failed" ? pc5.red(sym.err) : pc5.dim(st === "skipped" ? "\u2013" : sym.ring);
|
|
5374
|
+
const label = st === "pending" || st === "skipped" ? pc5.dim(s.label) : s.label;
|
|
5375
|
+
const ms = this.elapsed.get(s.id) ?? (st === "running" ? performance.now() - this.started.get(s.id) : void 0);
|
|
5376
|
+
const time2 = st === "skipped" ? pc5.dim("skipped") : ms === void 0 ? "" : pc5.dim(ms < 1e3 ? `${Math.max(1, Math.round(ms))}ms` : `${(ms / 1e3).toFixed(1)}s`);
|
|
5377
|
+
const r = this.results.get(s.id);
|
|
5378
|
+
const result = r ? r.tone === "warn" ? paint(PALETTE.amber, `${sym.warn} ${r.text}`) : r.tone === "dim" ? pc5.dim(r.text) : `${paint(PALETTE.green, sym.ok)} ${r.text}` : "";
|
|
5379
|
+
const bar = showBar ? `${this.bar(st, barW, i)} ` : "";
|
|
5380
|
+
return truncate2(` ${icon} ${padEnd(label, 40)}${bar}${padEnd(time2, 8)}${result}`, total - 1);
|
|
5381
|
+
});
|
|
5382
|
+
}
|
|
5383
|
+
bar(st, w, row) {
|
|
5384
|
+
const fill = unicode ? "\u2501" : "#";
|
|
5385
|
+
const track = unicode ? "\u2500" : "-";
|
|
5386
|
+
if (st === "done") return paint(mix(BAR_STOPS, row / (STAGES.length - 1)), fill.repeat(w));
|
|
5387
|
+
if (st === "failed") return pc5.red(fill.repeat(w));
|
|
5388
|
+
if (st !== "running") return pc5.dim(track.repeat(w));
|
|
5389
|
+
const seg = 6;
|
|
5390
|
+
const span = w - seg;
|
|
5391
|
+
const t = this.frame % (span * 2);
|
|
5392
|
+
const pos = t <= span ? t : span * 2 - t;
|
|
5393
|
+
return pc5.dim(track.repeat(pos)) + paint(PALETTE.cyan, fill.repeat(seg)) + pc5.dim(track.repeat(w - pos - seg));
|
|
5394
|
+
}
|
|
5395
|
+
};
|
|
5396
|
+
|
|
4479
5397
|
// src/cli/commands/init.ts
|
|
4480
5398
|
function parseAgentsOption(value, disabled) {
|
|
4481
5399
|
if (disabled) return [];
|
|
@@ -4490,12 +5408,12 @@ function parseAgentsOption(value, disabled) {
|
|
|
4490
5408
|
}
|
|
4491
5409
|
async function initCommand(opts) {
|
|
4492
5410
|
const root = await resolveCwd(opts);
|
|
4493
|
-
const existing = await readState(
|
|
5411
|
+
const existing = await readState(path26.join(root, ".athena"));
|
|
4494
5412
|
if (existing.kind === "ok") {
|
|
4495
5413
|
throw new AthenaError("Athena is already initialized in this project.", "Run `athena analyze` to refresh knowledge, or `athena clean` to start over.");
|
|
4496
5414
|
}
|
|
4497
5415
|
const agents = parseAgentsOption(opts.agents, opts.noAgents);
|
|
4498
|
-
|
|
5416
|
+
printHeader();
|
|
4499
5417
|
const result = await runWithProgress(root, { mode: "init", dryRun: opts.dryRun, agents, signal: opts.signal });
|
|
4500
5418
|
if (isJson()) {
|
|
4501
5419
|
json(summarize(result, root, Boolean(opts.dryRun)));
|
|
@@ -4504,8 +5422,8 @@ async function initCommand(opts) {
|
|
|
4504
5422
|
printSummary(result, Boolean(opts.dryRun), "init");
|
|
4505
5423
|
}
|
|
4506
5424
|
async function runWithProgress(root, o) {
|
|
4507
|
-
const
|
|
4508
|
-
|
|
5425
|
+
const board = new StageBoard();
|
|
5426
|
+
board.start();
|
|
4509
5427
|
try {
|
|
4510
5428
|
const result = await runPipeline({
|
|
4511
5429
|
root,
|
|
@@ -4514,22 +5432,50 @@ async function runWithProgress(root, o) {
|
|
|
4514
5432
|
force: o.force,
|
|
4515
5433
|
agents: o.agents,
|
|
4516
5434
|
signal: o.signal,
|
|
4517
|
-
onStage: (stage) =>
|
|
4518
|
-
const labels = { analyze: "Scanning project files...", scan: "Scanning project files...", detect: "Detecting technologies, schema, routes and infrastructure...", git: "Reading Git metadata...", finalize: "Redacting and validating...", write: "Writing knowledge...", agents: "Configuring AI agents..." };
|
|
4519
|
-
sp.update(labels[stage] ?? stage);
|
|
4520
|
-
}
|
|
5435
|
+
onStage: (stage) => board.enter(stage)
|
|
4521
5436
|
});
|
|
4522
|
-
|
|
5437
|
+
board.finish(stageResults(result, o.mode, Boolean(o.dryRun)));
|
|
4523
5438
|
return result;
|
|
4524
5439
|
} catch (err) {
|
|
4525
|
-
|
|
5440
|
+
board.fail();
|
|
4526
5441
|
throw err;
|
|
4527
5442
|
}
|
|
4528
5443
|
}
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
5444
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
5445
|
+
function stageResults(result, mode, dryRun) {
|
|
5446
|
+
const m = result.analysis.model;
|
|
5447
|
+
const skipped = m.stats.skippedBinary + m.stats.skippedLarge + m.stats.skippedUnreadable;
|
|
5448
|
+
const found = [
|
|
5449
|
+
m.frameworks.length && plural(new Set(m.frameworks.map((f) => f.name)).size, "framework"),
|
|
5450
|
+
m.dbEntities.length && plural(m.dbEntities.length, "entity").replace("entitys", "entities"),
|
|
5451
|
+
m.routes.length && plural(m.routes.length, "route"),
|
|
5452
|
+
m.auth.length && "auth"
|
|
5453
|
+
].filter(Boolean);
|
|
5454
|
+
const changed = result.docs.filter((d) => d.status !== "unchanged").length;
|
|
5455
|
+
const agents = result.agentChanges.map((a) => a.adapter.displayName);
|
|
5456
|
+
return {
|
|
5457
|
+
scan: { text: `Found ${plural(m.stats.filesScanned, "file")}${skipped ? ` \xB7 ${skipped} skipped` : ""}` },
|
|
5458
|
+
detect: found.length ? { text: found.join(" \xB7 ") } : { text: "No frameworks detected", tone: "dim" },
|
|
5459
|
+
git: m.git.isRepo ? { text: `On ${m.git.branch ?? "detached HEAD"}${m.git.head ? ` @ ${m.git.head.slice(0, 7)}` : ""}` } : { text: m.git.available ? "Not a Git repository" : "Git not installed", tone: "dim" },
|
|
5460
|
+
finalize: m.security.secrets.length ? { text: `${plural(m.security.secrets.length, "potential secret")} (values redacted)`, tone: "warn" } : { text: "No likely secrets found" },
|
|
5461
|
+
write: dryRun ? { text: "Dry run \u2014 nothing written", tone: "dim" } : { text: mode === "init" ? `${plural(result.docs.length, "document")} generated` : changed ? `${changed} updated \xB7 ${result.docs.length - changed} unchanged` : "Already up to date" },
|
|
5462
|
+
agents: agents.length ? { text: agents.join(", ") } : { text: "None configured", tone: "dim" }
|
|
5463
|
+
};
|
|
4532
5464
|
}
|
|
5465
|
+
var DOC_NOTES = {
|
|
5466
|
+
"project.md": "Project overview",
|
|
5467
|
+
"architecture.md": "System architecture",
|
|
5468
|
+
"database.md": "Database schema",
|
|
5469
|
+
"api.md": "API reference",
|
|
5470
|
+
"auth.md": "Authentication flow",
|
|
5471
|
+
"security.md": "Security analysis",
|
|
5472
|
+
"testing.md": "Testing strategy",
|
|
5473
|
+
"debugging.md": "Debugging guide",
|
|
5474
|
+
"performance.md": "Performance notes",
|
|
5475
|
+
"code-review.md": "Review checklist",
|
|
5476
|
+
"deployment.md": "Deployment guide",
|
|
5477
|
+
"rules.md": "Your project rules"
|
|
5478
|
+
};
|
|
4533
5479
|
function printSummary(result, dryRun, mode) {
|
|
4534
5480
|
const m = result.analysis.model;
|
|
4535
5481
|
if (isQuiet()) {
|
|
@@ -4540,63 +5486,65 @@ function printSummary(result, dryRun, mode) {
|
|
|
4540
5486
|
line(dryRun ? "Dry run: nothing written." : changed.length ? `Updated: ${changed.join(", ")}` : "Knowledge up to date.");
|
|
4541
5487
|
return;
|
|
4542
5488
|
}
|
|
4543
|
-
line();
|
|
4544
5489
|
const uniq = (xs) => [...new Set(xs)];
|
|
4545
|
-
|
|
4546
|
-
detectionLine("Frameworks", m.frameworks.length, uniq(m.frameworks.map((f) => f.name)).slice(0, 5).join(", "));
|
|
4547
|
-
detectionLine("Workspace", m.workspace.isMonorepo ? m.workspace.packages.length : 0, m.workspace.isMonorepo ? `monorepo \xB7 ${m.workspace.packages.length} packages` : void 0);
|
|
4548
|
-
detectionLine("Database", m.databases.length + m.dbEntities.length, `${uniq(m.databases.map((d) => d.name)).slice(0, 3).join(", ")}${m.dbEntities.length ? ` \xB7 ${m.dbEntities.length} entities` : ""}`);
|
|
4549
|
-
detectionLine("API", m.routes.length + m.apiSpecs.length, `${m.routes.length} route${m.routes.length === 1 ? "" : "s"}${m.apiSpecs.length ? ` \xB7 ${m.apiSpecs.length} specs` : ""}`);
|
|
4550
|
-
detectionLine("Authentication", m.auth.length, uniq(m.auth.map((a) => a.name)).slice(0, 3).join(", "));
|
|
4551
|
-
detectionLine("Testing", m.tests.frameworks.length + m.tests.testFileCount, `${uniq(m.tests.frameworks.map((f) => f.name)).slice(0, 3).join(", ")} \xB7 ${m.tests.testFileCount} test files`);
|
|
4552
|
-
detectionLine("Deployment", m.containers.dockerfiles.length + m.ci.length + m.infrastructure.length, [m.containers.dockerfiles.length ? "Docker" : "", m.ci.length ? uniq(m.ci.map((j) => j.system)).join(", ") : "", ...uniq(m.infrastructure.filter((i) => i.kind === "hosting").map((i) => i.name))].filter(Boolean).slice(0, 4).join(", "));
|
|
4553
|
-
if (m.security.secrets.length) warn(`Security \u2014 ${m.security.secrets.length} potential hardcoded secret(s); see .athena/security.md (values not stored)`);
|
|
4554
|
-
else ok(`Security${dim(" \u2014 no likely secrets matched")}`);
|
|
4555
|
-
if (m.warnings.length) {
|
|
4556
|
-
line();
|
|
4557
|
-
for (const w of m.warnings.slice(0, 5)) warn(c.yellow(w));
|
|
4558
|
-
if (m.warnings.length > 5) line(dim(` +${m.warnings.length - 5} more warnings (see .athena/project.md)`));
|
|
4559
|
-
}
|
|
5490
|
+
const list = (xs, n = 4) => uniq(xs).slice(0, n).join(" \xB7 ");
|
|
4560
5491
|
line();
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
line("Would create .athena/ with 12 knowledge files, model.json and state.json.");
|
|
4564
|
-
for (const { adapter, changes } of result.agentChanges) for (const ch of changes) bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`);
|
|
4565
|
-
return;
|
|
4566
|
-
}
|
|
4567
|
-
if (mode === "init") {
|
|
4568
|
-
tree(c.bold(".athena/"), [...result.docs.map((d) => d.file), "model.json", "state.json"]);
|
|
4569
|
-
} else {
|
|
4570
|
-
const changed = result.docs.filter((d) => d.status !== "unchanged");
|
|
4571
|
-
if (changed.length) {
|
|
4572
|
-
heading("Knowledge updated:");
|
|
4573
|
-
for (const d of changed) bullet(`${d.file} ${dim(`(${d.status})`)}`);
|
|
4574
|
-
} else ok("Knowledge already up to date.");
|
|
4575
|
-
}
|
|
4576
|
-
const preserved = result.docs.filter((d) => d.preservedModified.length);
|
|
4577
|
-
if (preserved.length) {
|
|
4578
|
-
line();
|
|
4579
|
-
for (const d of preserved) warn(`${d.file}: kept your edits in ${d.preservedModified.join(", ")} ${dim("(use --force to regenerate)")}`);
|
|
4580
|
-
}
|
|
5492
|
+
for (const l of sectionTitle(dryRun ? "Dry Run Complete" : "Analysis Complete!")) line(l);
|
|
5493
|
+
line(` Analyzed ${accent(String(m.stats.filesScanned))} files in ${accent(`${(result.analysis.durationMs / 1e3).toFixed(1)}s`)}`);
|
|
4581
5494
|
line();
|
|
4582
|
-
|
|
4583
|
-
|
|
5495
|
+
const overview = [
|
|
5496
|
+
["Project", `${m.name}${m.languages[0] ? ` \xB7 ${list(m.languages.map((l) => l.name), 3)}` : ""}`],
|
|
5497
|
+
["Frameworks", list(m.frameworks.map((f) => f.name), 5)],
|
|
5498
|
+
["Workspace", m.workspace.isMonorepo ? `monorepo \xB7 ${plural(m.workspace.packages.length, "package")}` : ""],
|
|
5499
|
+
["Database", m.databases.length + m.dbEntities.length ? [list(m.databases.map((d) => d.name), 3), m.dbEntities.length ? plural(m.dbEntities.length, "entity").replace("entitys", "entities") : ""].filter(Boolean).join(" \xB7 ") : ""],
|
|
5500
|
+
["API", m.routes.length + m.apiSpecs.length ? [m.routes.length ? plural(m.routes.length, "route") : "", m.apiSpecs.length ? plural(m.apiSpecs.length, "spec") : ""].filter(Boolean).join(" \xB7 ") : ""],
|
|
5501
|
+
["Auth", list(m.auth.map((a) => a.name), 3)],
|
|
5502
|
+
["Testing", m.tests.frameworks.length + m.tests.testFileCount ? [list(m.tests.frameworks.map((f) => f.name), 3), plural(m.tests.testFileCount, "test file")].filter(Boolean).join(" \xB7 ") : ""],
|
|
5503
|
+
["Deployment", [m.containers.dockerfiles.length ? "Docker" : "", ...uniq(m.ci.map((j) => j.system)), ...uniq(m.infrastructure.filter((i) => i.kind === "hosting").map((i) => i.name))].filter(Boolean).slice(0, 4).join(" \xB7 ")],
|
|
5504
|
+
["Security", m.security.secrets.length ? warnText(`${plural(m.security.secrets.length, "potential secret")} \u2014 see security.md`) : "No likely secrets matched"]
|
|
5505
|
+
];
|
|
5506
|
+
const files = dryRun ? ["Would create .athena/ with:", ...[...KNOWLEDGE_DOCS.map((d) => d.file), "model.json", "state.json"].map((f) => ` ${f}`)] : fileTree(
|
|
5507
|
+
".athena/",
|
|
5508
|
+
result.docs.map((d) => ({ name: d.file, note: mode === "init" ? DOC_NOTES[d.file] ?? "" : d.status, highlight: mode === "analyze" && d.status !== "unchanged" }))
|
|
5509
|
+
);
|
|
5510
|
+
const configured = result.agentChanges.length > 0;
|
|
5511
|
+
const steps = dryRun ? ["Run `athena init` without --dry-run to write the files"] : mode === "init" ? [
|
|
5512
|
+
"Review `.athena/rules.md` and enable the rules that apply",
|
|
5513
|
+
"Run `athena open` to browse and edit in the web UI",
|
|
5514
|
+
configured ? "Your agents now read `.athena/` \u2014 watch with `athena activity`" : "Connect an agent: `athena agents add claude-code`",
|
|
5515
|
+
"After code changes run `athena sync` to keep docs current",
|
|
5516
|
+
"Guides and docs: https://athena.sabari.me \xB7 star us on GitHub \u2605"
|
|
5517
|
+
] : ["Review changes with `git diff .athena/`", "Run `athena open` to browse in the web UI", "Use `athena sync --watch` to stay current"];
|
|
5518
|
+
for (const l of panels([
|
|
5519
|
+
{ title: "Project Overview", icon: "\u25C6", color: C.blue, body: rows(overview) },
|
|
5520
|
+
{ title: mode === "init" ? "Generated Files" : "Knowledge Files", icon: "\u25A4", color: C.violet, body: files },
|
|
5521
|
+
{ title: "Next Steps", icon: "\u279C", color: C.green, body: mode === "init" && !dryRun ? [numbered(steps), [""], quote("Better documentation leads to better software.")] : numbered(steps) }
|
|
5522
|
+
]))
|
|
5523
|
+
line(l);
|
|
5524
|
+
const preserved = result.docs.filter((d) => d.preservedModified.length);
|
|
5525
|
+
const notes = [
|
|
5526
|
+
...m.warnings.slice(0, 5).map((w) => () => warn(c.yellow(w))),
|
|
5527
|
+
...m.warnings.length > 5 ? [() => line(dim(` +${m.warnings.length - 5} more warnings (see .athena/project.md)`))] : [],
|
|
5528
|
+
...preserved.map((d) => () => warn(`${d.file}: kept your edits in ${d.preservedModified.join(", ")} ${dim("(use --force to regenerate)")}`)),
|
|
5529
|
+
...(dryRun ? [] : result.agentChanges).map(({ adapter, changes }) => () => {
|
|
4584
5530
|
const touched = changes.filter((c2) => c2.action !== "unchanged").map((c2) => `${c2.path} (${c2.action})`);
|
|
4585
5531
|
ok(`${adapter.displayName} integration${dim(` \u2014 ${touched.length ? touched.join(", ") : changes.map((c2) => c2.path).join(", ") + " (unchanged)"}`)}`);
|
|
4586
|
-
}
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
5532
|
+
}),
|
|
5533
|
+
...mode === "init" && !dryRun ? (() => {
|
|
5534
|
+
const unconfigured = ADAPTERS.filter((a) => !result.agentChanges.some((c2) => c2.adapter.id === a.id));
|
|
5535
|
+
const text = `Not configured (not detected in this project): ${unconfigured.map((a) => a.id).join(", ")}. Add one with \`athena agents add <id>\`.`;
|
|
5536
|
+
return unconfigured.length ? [() => wrapLines(text).forEach((l) => line(dim(l)))] : [];
|
|
5537
|
+
})() : [],
|
|
5538
|
+
...dryRun ? result.agentChanges.flatMap(({ adapter, changes }) => changes.map((ch) => () => bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`))) : [],
|
|
5539
|
+
...result.notes.map((n) => () => warn(n))
|
|
5540
|
+
];
|
|
5541
|
+
if (notes.length) {
|
|
5542
|
+
line();
|
|
5543
|
+
for (const print of notes) print();
|
|
4594
5544
|
}
|
|
4595
5545
|
line();
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
line(dim("Next: review .athena/rules.md and enable the rules that apply, then run `athena open` for the web UI."));
|
|
4599
|
-
}
|
|
5546
|
+
const lines = dryRun ? footer("Dry run \u2014 nothing was written.", "Re-run without --dry-run to create .athena/", "warn") : mode === "init" ? footer("Athena is ready!", `Your project intelligence is ready in ${accent(".athena/")}`) : footer("Analysis complete.", `Knowledge in ${accent(".athena/")} is up to date with your code`);
|
|
5547
|
+
for (const l of lines) line(l);
|
|
4600
5548
|
}
|
|
4601
5549
|
function summarize(result, root, dryRun) {
|
|
4602
5550
|
const m = result.analysis.model;
|
|
@@ -4625,13 +5573,14 @@ function summarize(result, root, dryRun) {
|
|
|
4625
5573
|
async function analyzeCommand(opts) {
|
|
4626
5574
|
const root = await requireProjectRoot(opts);
|
|
4627
5575
|
const agents = parseAgentsOption(opts.agents, false);
|
|
5576
|
+
printHeader();
|
|
4628
5577
|
const result = await runWithProgress(root, { mode: "analyze", force: opts.force, agents, signal: opts.signal });
|
|
4629
5578
|
if (isJson()) json(summarize(result, root, false));
|
|
4630
5579
|
else printSummary(result, false, "analyze");
|
|
4631
5580
|
}
|
|
4632
5581
|
|
|
4633
5582
|
// src/services/status.ts
|
|
4634
|
-
import
|
|
5583
|
+
import path27 from "path";
|
|
4635
5584
|
|
|
4636
5585
|
// src/core/impact/impact.ts
|
|
4637
5586
|
var RULES = [
|
|
@@ -4675,7 +5624,7 @@ async function buildStatus(root, signal) {
|
|
|
4675
5624
|
const state = st.state;
|
|
4676
5625
|
const documents = [];
|
|
4677
5626
|
for (const d of KNOWLEDGE_DOCS) {
|
|
4678
|
-
const text = await readTextIfExists(
|
|
5627
|
+
const text = await readTextIfExists(path27.join(dir, d.file));
|
|
4679
5628
|
documents.push({
|
|
4680
5629
|
file: d.file,
|
|
4681
5630
|
title: d.title,
|
|
@@ -4754,8 +5703,8 @@ async function statusCommand(opts) {
|
|
|
4754
5703
|
}
|
|
4755
5704
|
|
|
4756
5705
|
// src/services/doctor.ts
|
|
4757
|
-
import { constants, promises as
|
|
4758
|
-
import
|
|
5706
|
+
import { constants, promises as fs22 } from "fs";
|
|
5707
|
+
import path31 from "path";
|
|
4759
5708
|
|
|
4760
5709
|
// src/core/knowledge/rules.ts
|
|
4761
5710
|
var RULE_RE = /^([-*+])\s+(?:\[(disabled)\]\s+)?(.+?)\s*$/;
|
|
@@ -4849,11 +5798,11 @@ function relativeTime2(iso, now = Date.now()) {
|
|
|
4849
5798
|
}
|
|
4850
5799
|
|
|
4851
5800
|
// src/services/instance.ts
|
|
4852
|
-
import { promises as
|
|
4853
|
-
import
|
|
5801
|
+
import { promises as fs19 } from "fs";
|
|
5802
|
+
import path28 from "path";
|
|
4854
5803
|
var INSTANCE_FILE = ".server.json";
|
|
4855
5804
|
async function findRunningInstance(root) {
|
|
4856
|
-
const file =
|
|
5805
|
+
const file = path28.join(athenaDir(root), INSTANCE_FILE);
|
|
4857
5806
|
const raw = await readTextIfExists(file);
|
|
4858
5807
|
if (!raw) return null;
|
|
4859
5808
|
let info2;
|
|
@@ -4866,7 +5815,7 @@ async function findRunningInstance(root) {
|
|
|
4866
5815
|
try {
|
|
4867
5816
|
process.kill(info2.pid, 0);
|
|
4868
5817
|
} catch {
|
|
4869
|
-
await
|
|
5818
|
+
await fs19.rm(file, { force: true }).catch(() => {
|
|
4870
5819
|
});
|
|
4871
5820
|
return null;
|
|
4872
5821
|
}
|
|
@@ -4881,19 +5830,19 @@ async function findRunningInstance(root) {
|
|
|
4881
5830
|
}
|
|
4882
5831
|
|
|
4883
5832
|
// src/services/which.ts
|
|
4884
|
-
import { promises as
|
|
4885
|
-
import
|
|
5833
|
+
import { promises as fs20 } from "fs";
|
|
5834
|
+
import path29 from "path";
|
|
4886
5835
|
async function whichExecutable(command) {
|
|
4887
5836
|
if (command.includes("/") || command.includes("\\")) {
|
|
4888
|
-
return await
|
|
5837
|
+
return await fs20.access(command).then(() => true).catch(() => false) ? command : null;
|
|
4889
5838
|
}
|
|
4890
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
5839
|
+
const dirs = (process.env.PATH ?? "").split(path29.delimiter).filter(Boolean);
|
|
4891
5840
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
4892
5841
|
for (const dir of dirs) {
|
|
4893
5842
|
for (const ext of exts) {
|
|
4894
|
-
const candidate =
|
|
5843
|
+
const candidate = path29.join(dir, command + ext.toLowerCase());
|
|
4895
5844
|
try {
|
|
4896
|
-
const st = await
|
|
5845
|
+
const st = await fs20.stat(candidate);
|
|
4897
5846
|
if (st.isFile()) return candidate;
|
|
4898
5847
|
} catch {
|
|
4899
5848
|
}
|
|
@@ -4903,41 +5852,41 @@ async function whichExecutable(command) {
|
|
|
4903
5852
|
}
|
|
4904
5853
|
|
|
4905
5854
|
// src/services/agent-activity.ts
|
|
4906
|
-
import { promises as
|
|
4907
|
-
import
|
|
5855
|
+
import { promises as fs21 } from "fs";
|
|
5856
|
+
import path30 from "path";
|
|
4908
5857
|
var ACTIVITY_FILE = ".agent-events.jsonl";
|
|
4909
5858
|
var MAX_LINES = 2e3;
|
|
4910
5859
|
var MAX_BYTES = 512 * 1024;
|
|
4911
5860
|
function activityFile(root) {
|
|
4912
|
-
return
|
|
5861
|
+
return path30.join(athenaDir(root), ACTIVITY_FILE);
|
|
4913
5862
|
}
|
|
4914
5863
|
function sanitizeEvent(root, event) {
|
|
4915
|
-
const
|
|
4916
|
-
const abs =
|
|
4917
|
-
const r = toPosix(
|
|
5864
|
+
const rel2 = (p) => {
|
|
5865
|
+
const abs = path30.isAbsolute(p) ? p : path30.resolve(root, p);
|
|
5866
|
+
const r = toPosix(path30.relative(root, abs));
|
|
4918
5867
|
return !r || r.startsWith("..") ? toPosix(p) : r;
|
|
4919
5868
|
};
|
|
4920
|
-
const stripRoot = (text) => text.split(`${root}${
|
|
5869
|
+
const stripRoot = (text) => text.split(`${root}${path30.sep}`).join("").split(toPosix(root) + "/").join("");
|
|
4921
5870
|
return {
|
|
4922
5871
|
...event,
|
|
4923
5872
|
message: redact(stripRoot(event.message)),
|
|
4924
5873
|
command: event.command ? redact(stripRoot(event.command)) : null,
|
|
4925
|
-
files: event.files.map(
|
|
5874
|
+
files: event.files.map(rel2)
|
|
4926
5875
|
};
|
|
4927
5876
|
}
|
|
4928
5877
|
async function appendEvent(root, event) {
|
|
4929
5878
|
const file = activityFile(root);
|
|
4930
|
-
await
|
|
4931
|
-
await
|
|
5879
|
+
await fs21.mkdir(path30.dirname(file), { recursive: true });
|
|
5880
|
+
await fs21.appendFile(file, `${JSON.stringify(sanitizeEvent(root, event))}
|
|
4932
5881
|
`);
|
|
4933
5882
|
await rotateIfNeeded(file);
|
|
4934
5883
|
}
|
|
4935
5884
|
async function rotateIfNeeded(file) {
|
|
4936
5885
|
try {
|
|
4937
|
-
const st = await
|
|
5886
|
+
const st = await fs21.stat(file);
|
|
4938
5887
|
if (st.size < MAX_BYTES) return;
|
|
4939
|
-
const lines = (await
|
|
4940
|
-
await
|
|
5888
|
+
const lines = (await fs21.readFile(file, "utf8")).split("\n").filter(Boolean);
|
|
5889
|
+
await fs21.writeFile(file, `${lines.slice(-MAX_LINES / 2).join("\n")}
|
|
4941
5890
|
`);
|
|
4942
5891
|
} catch {
|
|
4943
5892
|
}
|
|
@@ -4956,7 +5905,7 @@ function parseEventLines(text) {
|
|
|
4956
5905
|
}
|
|
4957
5906
|
async function readRecentEvents(root, limit = 200) {
|
|
4958
5907
|
try {
|
|
4959
|
-
const text = await
|
|
5908
|
+
const text = await fs21.readFile(activityFile(root), "utf8");
|
|
4960
5909
|
return parseEventLines(text).slice(-limit);
|
|
4961
5910
|
} catch {
|
|
4962
5911
|
return [];
|
|
@@ -4993,7 +5942,7 @@ async function runDoctor(cwd) {
|
|
|
4993
5942
|
add({ area: "Project", level: "ok", message: root });
|
|
4994
5943
|
const dir = athenaDir(root);
|
|
4995
5944
|
try {
|
|
4996
|
-
await
|
|
5945
|
+
await fs22.access(dir, constants.R_OK | constants.W_OK);
|
|
4997
5946
|
add({ area: "Knowledge directory", level: "ok", message: ".athena/ is readable and writable" });
|
|
4998
5947
|
} catch {
|
|
4999
5948
|
add({ area: "Knowledge directory", level: "error", message: ".athena/ is not writable", hint: "Check file permissions." });
|
|
@@ -5007,13 +5956,13 @@ async function runDoctor(cwd) {
|
|
|
5007
5956
|
const missing = [];
|
|
5008
5957
|
const malformed = [];
|
|
5009
5958
|
for (const d of KNOWLEDGE_DOCS) {
|
|
5010
|
-
const text = await readTextIfExists(
|
|
5959
|
+
const text = await readTextIfExists(path31.join(dir, d.file));
|
|
5011
5960
|
if (text === null) missing.push(d.file);
|
|
5012
5961
|
else if (d.id !== "rules" && (text.match(/athena:generated:start/g)?.length ?? 0) !== parseBlocks(text).length) malformed.push(d.file);
|
|
5013
5962
|
}
|
|
5014
5963
|
add({ area: "Knowledge files", level: missing.length ? "error" : "ok", message: missing.length ? `Missing: ${missing.join(", ")}` : `${KNOWLEDGE_DOCS.length} knowledge files present`, hint: missing.length ? "Run `athena analyze` to regenerate missing files." : void 0 });
|
|
5015
5964
|
if (malformed.length) add({ area: "Knowledge files", level: "warn", message: `Unmatched generated-section markers in: ${malformed.join(", ")}`, hint: "Those sections are treated as developer content and will not be refreshed." });
|
|
5016
|
-
const rulesText = await readTextIfExists(
|
|
5965
|
+
const rulesText = await readTextIfExists(path31.join(dir, "rules.md"));
|
|
5017
5966
|
if (rulesText !== null) {
|
|
5018
5967
|
const rules = listRules(parseRules(rulesText));
|
|
5019
5968
|
const enabled = rules.filter((r) => r.enabled).length;
|
|
@@ -5072,10 +6021,10 @@ async function doctorCommand(opts) {
|
|
|
5072
6021
|
}
|
|
5073
6022
|
|
|
5074
6023
|
// src/services/rules.ts
|
|
5075
|
-
import
|
|
6024
|
+
import path33 from "path";
|
|
5076
6025
|
|
|
5077
6026
|
// src/services/knowledge.ts
|
|
5078
|
-
import
|
|
6027
|
+
import path32 from "path";
|
|
5079
6028
|
var MAX_DOC_BYTES = 1e6;
|
|
5080
6029
|
function resolveDoc(id) {
|
|
5081
6030
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
@@ -5119,12 +6068,12 @@ async function meta(root, id, text) {
|
|
|
5119
6068
|
}
|
|
5120
6069
|
async function listDocs(root) {
|
|
5121
6070
|
const out2 = [];
|
|
5122
|
-
for (const d of KNOWLEDGE_DOCS) out2.push(await meta(root, d.id, await readTextIfExists(
|
|
6071
|
+
for (const d of KNOWLEDGE_DOCS) out2.push(await meta(root, d.id, await readTextIfExists(path32.join(athenaDir(root), d.file))));
|
|
5123
6072
|
return out2;
|
|
5124
6073
|
}
|
|
5125
6074
|
async function readDoc(root, id) {
|
|
5126
6075
|
const doc = resolveDoc(id);
|
|
5127
|
-
const text = await readTextIfExists(
|
|
6076
|
+
const text = await readTextIfExists(path32.join(athenaDir(root), doc.file));
|
|
5128
6077
|
if (text === null) throw notFound(`${doc.file} does not exist. Run \`athena analyze\` to regenerate it.`);
|
|
5129
6078
|
return { ...await meta(root, doc.id, text), content: text, segments: segmentDocument(text) };
|
|
5130
6079
|
}
|
|
@@ -5132,7 +6081,7 @@ async function saveDoc(root, id, content, baseHash) {
|
|
|
5132
6081
|
const doc = resolveDoc(id);
|
|
5133
6082
|
if (typeof content !== "string") throw new AthenaError("content must be a string");
|
|
5134
6083
|
if (Buffer.byteLength(content) > MAX_DOC_BYTES) throw new AthenaError(`Document exceeds ${MAX_DOC_BYTES} bytes`, void 0, 1, "unprocessable");
|
|
5135
|
-
const file =
|
|
6084
|
+
const file = path32.join(athenaDir(root), doc.file);
|
|
5136
6085
|
const current = await readTextIfExists(file);
|
|
5137
6086
|
const currentHash = current === null ? null : contentHash(current);
|
|
5138
6087
|
if (currentHash !== baseHash) {
|
|
@@ -5158,7 +6107,7 @@ async function searchDocs(root, query, limit = 50) {
|
|
|
5158
6107
|
if (q.length < 2) return [];
|
|
5159
6108
|
const hits = [];
|
|
5160
6109
|
for (const d of KNOWLEDGE_DOCS) {
|
|
5161
|
-
const text = await readTextIfExists(
|
|
6110
|
+
const text = await readTextIfExists(path32.join(athenaDir(root), d.file));
|
|
5162
6111
|
if (!text) continue;
|
|
5163
6112
|
const lines = text.split("\n");
|
|
5164
6113
|
for (let i = 0; i < lines.length && hits.length < limit; i++) {
|
|
@@ -5172,14 +6121,14 @@ async function searchDocs(root, query, limit = 50) {
|
|
|
5172
6121
|
}
|
|
5173
6122
|
async function docHistory(root, id, limit = 50) {
|
|
5174
6123
|
const doc = resolveDoc(id);
|
|
5175
|
-
const
|
|
5176
|
-
const r = await git(root, ["log", `-n${limit}`, "--follow", "--format=%H%x1f%cI%x1f%s", "--",
|
|
6124
|
+
const rel2 = `.athena/${doc.file}`;
|
|
6125
|
+
const r = await git(root, ["log", `-n${limit}`, "--follow", "--format=%H%x1f%cI%x1f%s", "--", rel2]);
|
|
5177
6126
|
if (!r.ok) return { available: false, reason: "Git history is not available (not a Git repository, or git is not installed).", entries: [] };
|
|
5178
6127
|
const entries = r.stdout.split("\n").filter(Boolean).map((l) => {
|
|
5179
6128
|
const [sha, date, subject] = l.split("");
|
|
5180
6129
|
return { sha, date, subject: subject ?? "" };
|
|
5181
6130
|
});
|
|
5182
|
-
if (!entries.length) return { available: true, reason: `${
|
|
6131
|
+
if (!entries.length) return { available: true, reason: `${rel2} has not been committed yet.`, entries };
|
|
5183
6132
|
return { available: true, entries };
|
|
5184
6133
|
}
|
|
5185
6134
|
async function docAtRevision(root, id, sha) {
|
|
@@ -5193,7 +6142,7 @@ async function docAtRevision(root, id, sha) {
|
|
|
5193
6142
|
|
|
5194
6143
|
// src/services/rules.ts
|
|
5195
6144
|
async function load(root) {
|
|
5196
|
-
const file =
|
|
6145
|
+
const file = path33.join(athenaDir(root), "rules.md");
|
|
5197
6146
|
const text = await readTextIfExists(file);
|
|
5198
6147
|
if (text === null) throw notFound(".athena/rules.md not found. Run `athena analyze` to recreate it.");
|
|
5199
6148
|
return { file, text, doc: parseRules(text) };
|
|
@@ -5271,7 +6220,7 @@ var rulesEditCommand = (n, text, opts) => mutate(opts, (d) => updateRule(d, pars
|
|
|
5271
6220
|
var rulesRemoveCommand = (n, opts) => mutate(opts, (d) => removeRule(d, parseIndex(n)), `Rule ${n} removed.`);
|
|
5272
6221
|
|
|
5273
6222
|
// src/cli/commands/clean.ts
|
|
5274
|
-
import { promises as
|
|
6223
|
+
import { promises as fs23 } from "fs";
|
|
5275
6224
|
import { createInterface } from "readline/promises";
|
|
5276
6225
|
async function confirm(question) {
|
|
5277
6226
|
if (!process.stdin.isTTY) return false;
|
|
@@ -5296,7 +6245,7 @@ async function cleanCommand(opts) {
|
|
|
5296
6245
|
}
|
|
5297
6246
|
const touched = [];
|
|
5298
6247
|
if (!opts.keepAgents) for (const a of ADAPTERS) touched.push(...await a.remove(root));
|
|
5299
|
-
await
|
|
6248
|
+
await fs23.rm(athenaDir(root), { recursive: true, force: true });
|
|
5300
6249
|
if (isJson()) json({ ok: true, removed: [".athena/", ...touched] });
|
|
5301
6250
|
else {
|
|
5302
6251
|
ok("Removed .athena/");
|
|
@@ -5308,14 +6257,14 @@ async function cleanCommand(opts) {
|
|
|
5308
6257
|
import { execFile as execFile3 } from "child_process";
|
|
5309
6258
|
|
|
5310
6259
|
// src/server/instance.ts
|
|
5311
|
-
import { promises as
|
|
5312
|
-
import
|
|
6260
|
+
import { promises as fs28 } from "fs";
|
|
6261
|
+
import path43 from "path";
|
|
5313
6262
|
import { fileURLToPath } from "url";
|
|
5314
6263
|
|
|
5315
6264
|
// src/server/app.ts
|
|
5316
|
-
import { promises as
|
|
6265
|
+
import { promises as fs27, watch } from "fs";
|
|
5317
6266
|
import crypto7 from "crypto";
|
|
5318
|
-
import
|
|
6267
|
+
import path42 from "path";
|
|
5319
6268
|
import Fastify from "fastify";
|
|
5320
6269
|
|
|
5321
6270
|
// src/services/agents.ts
|
|
@@ -5371,23 +6320,32 @@ async function configureAgents(root, names2) {
|
|
|
5371
6320
|
async function removeAgents(root, names2) {
|
|
5372
6321
|
const st = await readState(athenaDir(root));
|
|
5373
6322
|
const removed = [];
|
|
5374
|
-
|
|
6323
|
+
const targets = resolve(names2);
|
|
6324
|
+
for (const a of targets) {
|
|
5375
6325
|
removed.push(...await a.remove(root));
|
|
5376
6326
|
if (st.kind === "ok") delete st.state.agents[a.id];
|
|
5377
6327
|
}
|
|
5378
|
-
if (st.kind === "ok")
|
|
6328
|
+
if (st.kind === "ok") {
|
|
6329
|
+
for (const a of ADAPTERS) {
|
|
6330
|
+
const s = st.state.agents[a.id];
|
|
6331
|
+
if (!s?.configured || targets.includes(a) || !s.files.some((f) => removed.includes(f))) continue;
|
|
6332
|
+
const planned = await a.plan({ root, projectName: st.state.projectName });
|
|
6333
|
+
await applyChanges(root, planned.filter((p) => removed.includes(p.path)));
|
|
6334
|
+
}
|
|
6335
|
+
await writeState(athenaDir(root), st.state);
|
|
6336
|
+
}
|
|
5379
6337
|
return removed;
|
|
5380
6338
|
}
|
|
5381
6339
|
|
|
5382
6340
|
// src/services/overview.ts
|
|
5383
|
-
import
|
|
6341
|
+
import path34 from "path";
|
|
5384
6342
|
async function getOverview(root) {
|
|
5385
6343
|
const st = await readState(athenaDir(root));
|
|
5386
6344
|
if (st.kind === "missing") throw new AthenaError("Athena is not initialized in this project.", "Run `athena init`.", 3);
|
|
5387
6345
|
if (st.kind === "corrupted") throw new AthenaError(`.athena/state.json is corrupted (${st.reason}).`, "Run `athena analyze` to rebuild it.");
|
|
5388
6346
|
const state = st.state;
|
|
5389
6347
|
let model = null;
|
|
5390
|
-
const raw = await readTextIfExists(
|
|
6348
|
+
const raw = await readTextIfExists(path34.join(athenaDir(root), "model.json"));
|
|
5391
6349
|
if (raw) {
|
|
5392
6350
|
try {
|
|
5393
6351
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -5428,8 +6386,8 @@ async function getOverview(root) {
|
|
|
5428
6386
|
}
|
|
5429
6387
|
|
|
5430
6388
|
// src/services/sync.ts
|
|
5431
|
-
import { promises as
|
|
5432
|
-
import
|
|
6389
|
+
import { promises as fs24 } from "fs";
|
|
6390
|
+
import path35 from "path";
|
|
5433
6391
|
import { createTwoFilesPatch } from "diff";
|
|
5434
6392
|
|
|
5435
6393
|
// src/core/impact/model-diff.ts
|
|
@@ -5615,7 +6573,7 @@ function detectRenames(prev, next, added, deleted) {
|
|
|
5615
6573
|
return { added: remainingAdded, modified: [], deleted: deleted.filter((d) => !usedDeleted.has(d)), renamed };
|
|
5616
6574
|
}
|
|
5617
6575
|
async function readPreviousModel(dir) {
|
|
5618
|
-
const raw = await readTextIfExists(
|
|
6576
|
+
const raw = await readTextIfExists(path35.join(dir, "model.json"));
|
|
5619
6577
|
if (!raw) return null;
|
|
5620
6578
|
try {
|
|
5621
6579
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -5625,7 +6583,7 @@ async function readPreviousModel(dir) {
|
|
|
5625
6583
|
}
|
|
5626
6584
|
}
|
|
5627
6585
|
async function readIgnored(dir) {
|
|
5628
|
-
const raw = await readTextIfExists(
|
|
6586
|
+
const raw = await readTextIfExists(path35.join(dir, IGNORE_FILE));
|
|
5629
6587
|
if (!raw) return null;
|
|
5630
6588
|
try {
|
|
5631
6589
|
const j = JSON.parse(raw);
|
|
@@ -5638,7 +6596,7 @@ async function planSync(root, opts = {}) {
|
|
|
5638
6596
|
const started = Date.now();
|
|
5639
6597
|
const dir = athenaDir(root);
|
|
5640
6598
|
const st = await readState(dir);
|
|
5641
|
-
if (st.kind === "missing" && !await readTextIfExists(
|
|
6599
|
+
if (st.kind === "missing" && !await readTextIfExists(path35.join(dir, "rules.md"))) {
|
|
5642
6600
|
throw new AthenaError("Athena is not initialized in this project.", "Run `athena init` first.", 3);
|
|
5643
6601
|
}
|
|
5644
6602
|
const prevState = st.kind === "ok" ? st.state : null;
|
|
@@ -5727,7 +6685,7 @@ async function applySync(root, plan) {
|
|
|
5727
6685
|
if (!inner) throw new AthenaError("This sync plan is no longer available.", "Create a new plan and try again.", 1, "conflict");
|
|
5728
6686
|
const dir = athenaDir(root);
|
|
5729
6687
|
for (const d of plan.documents) {
|
|
5730
|
-
const current = hashOf(await readTextIfExists(
|
|
6688
|
+
const current = hashOf(await readTextIfExists(path35.join(dir, d.file)));
|
|
5731
6689
|
if (current !== d.baseHash) {
|
|
5732
6690
|
throw conflict(`${d.file} changed after the sync plan was created.`, "Review the new plan before applying.", { file: d.file });
|
|
5733
6691
|
}
|
|
@@ -5735,10 +6693,10 @@ async function applySync(root, plan) {
|
|
|
5735
6693
|
const st = await readState(dir);
|
|
5736
6694
|
if (st.kind === "corrupted") await backupCorruptedState(dir);
|
|
5737
6695
|
const prevState = st.kind === "ok" ? st.state : inner.prevState;
|
|
5738
|
-
await
|
|
6696
|
+
await fs24.mkdir(dir, { recursive: true });
|
|
5739
6697
|
const docs = await writePlannedKnowledge(dir, inner.plans);
|
|
5740
6698
|
docs.push(await ensureRules(dir, inner.analysis.model));
|
|
5741
|
-
await writeFileAtomic(
|
|
6699
|
+
await writeFileAtomic(path35.join(dir, "model.json"), `${JSON.stringify(inner.analysis.model, null, 2)}
|
|
5742
6700
|
`);
|
|
5743
6701
|
const managedFiles = Object.values(prevState?.agents ?? {}).flatMap((a) => a.files);
|
|
5744
6702
|
const state = composeState({
|
|
@@ -5749,7 +6707,7 @@ async function applySync(root, plan) {
|
|
|
5749
6707
|
fileIndex: await indexWithIntegrations(root, inner.analysis.files, managedFiles)
|
|
5750
6708
|
});
|
|
5751
6709
|
await writeState(dir, state);
|
|
5752
|
-
await
|
|
6710
|
+
await fs24.rm(path35.join(dir, IGNORE_FILE), { force: true });
|
|
5753
6711
|
internals.delete(plan);
|
|
5754
6712
|
return {
|
|
5755
6713
|
applied: plan.documents.map((d) => d.file),
|
|
@@ -5759,7 +6717,7 @@ async function applySync(root, plan) {
|
|
|
5759
6717
|
}
|
|
5760
6718
|
async function ignorePlan(root, planId) {
|
|
5761
6719
|
if (!/^[a-f0-9]{16}$/.test(planId)) throw new AthenaError("Invalid plan id");
|
|
5762
|
-
await writeFileAtomic(
|
|
6720
|
+
await writeFileAtomic(path35.join(athenaDir(root), IGNORE_FILE), `${JSON.stringify({ planId, ignoredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
5763
6721
|
`);
|
|
5764
6722
|
}
|
|
5765
6723
|
function fileChangeCount(plan) {
|
|
@@ -5773,6 +6731,10 @@ function summarizePlan(plan) {
|
|
|
5773
6731
|
const { documents, ...rest } = plan;
|
|
5774
6732
|
return { ...rest, documents: documents.map(({ diff: _diff, ...d }) => d) };
|
|
5775
6733
|
}
|
|
6734
|
+
var HISTORY_SECTIONS = /* @__PURE__ */ new Set(["hotspots"]);
|
|
6735
|
+
function staleForCheck(plan) {
|
|
6736
|
+
return plan.documents.filter((d) => d.status === "created" || !d.changedSections.length || d.changedSections.some((s) => !HISTORY_SECTIONS.has(s)));
|
|
6737
|
+
}
|
|
5776
6738
|
|
|
5777
6739
|
// src/services/security.ts
|
|
5778
6740
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -5992,7 +6954,7 @@ async function runSecurityScan(root, model, opts = {}) {
|
|
|
5992
6954
|
}
|
|
5993
6955
|
|
|
5994
6956
|
// src/services/review.ts
|
|
5995
|
-
import
|
|
6957
|
+
import path36 from "path";
|
|
5996
6958
|
var TEST_FILE = /(^|\/)(__tests__|tests?|spec|specs|e2e)\/|\.(test|spec|e2e)\.[cm]?[jt]sx?$|(^|\/)test_[^/]+\.py$|_test\.(py|go|exs)$|_spec\.rb$|Tests?\.(java|kt|cs)$/;
|
|
5997
6959
|
var SOURCE_FILE = /\.(m|c)?(t|j)sx?$|\.(py|go|rs|java|kt|cs|php|rb|dart|ex|swift|vue|svelte)$/;
|
|
5998
6960
|
var ROUTE_FILE = /(^|\/)(routes?|controllers?|handlers?|api|endpoints?|resolvers?)\/|(^|\/)(urls\.py|routes\.rb)$|(^|\/)app\/(.+\/)?route\.(t|j)sx?$/i;
|
|
@@ -6050,7 +7012,7 @@ async function reviewChanges(root, opts = {}) {
|
|
|
6050
7012
|
const untracked = changedFiles.filter((f) => f.status === "untracked");
|
|
6051
7013
|
let extra = "";
|
|
6052
7014
|
for (const f of untracked.slice(0, 200)) {
|
|
6053
|
-
const text = await readTextIfExists(
|
|
7015
|
+
const text = await readTextIfExists(path36.join(root, f.path));
|
|
6054
7016
|
if (text && text.length < 512 * 1024) extra += `+++ b/${f.path}
|
|
6055
7017
|
@@ -0,0 +1,${text.split("\n").length} @@
|
|
6056
7018
|
${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
@@ -6059,7 +7021,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6059
7021
|
const added = addedLines(patch + extra);
|
|
6060
7022
|
let untrackedAdded = 0;
|
|
6061
7023
|
for (const f of untracked) {
|
|
6062
|
-
const text = await readTextIfExists(
|
|
7024
|
+
const text = await readTextIfExists(path36.join(root, f.path));
|
|
6063
7025
|
if (text) untrackedAdded += text.split("\n").filter((l) => l.length > 0).length;
|
|
6064
7026
|
}
|
|
6065
7027
|
const numstat = (await git(root, [...diffArgs, "--numstat", "--", "."])).stdout;
|
|
@@ -6089,7 +7051,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6089
7051
|
const envFiles = paths.filter((p) => ENV_FILE.test(p));
|
|
6090
7052
|
if (envFiles.length) add({ level: "blocker", check: "env-file", message: "Environment file included in the change", files: envFiles, hint: "Add it to .gitignore and commit a .env.example with names only." });
|
|
6091
7053
|
for (const p of paths.filter((x) => MANIFEST_FILE.test(x))) {
|
|
6092
|
-
const now = await readTextIfExists(
|
|
7054
|
+
const now = await readTextIfExists(path36.join(root, p));
|
|
6093
7055
|
const before = (await git(root, ["show", `${opts.base ?? "HEAD"}:${p}`])).stdout;
|
|
6094
7056
|
if (!now || !before) continue;
|
|
6095
7057
|
const newDeps = [...depsOf(p, now)].filter((d) => !depsOf(p, before).has(d));
|
|
@@ -6108,7 +7070,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6108
7070
|
if (authFiles.length) add({ level: "warning", check: "auth", message: "Authentication/authorization code changed", files: authFiles.slice(0, 10), hint: "Review against `.athena/auth.md` and `.athena/security.md`." });
|
|
6109
7071
|
const large = [];
|
|
6110
7072
|
for (const f of changedFiles.filter((x) => x.status === "added" || x.status === "untracked")) {
|
|
6111
|
-
const text = await readTextIfExists(
|
|
7073
|
+
const text = await readTextIfExists(path36.join(root, f.path)).catch(() => null);
|
|
6112
7074
|
if (text && Buffer.byteLength(text) > LARGE_ADDED_BYTES) large.push(f.path);
|
|
6113
7075
|
}
|
|
6114
7076
|
if (large.length) add({ level: "warning", check: "large-files", message: "Large files added", files: large, hint: "Consider whether these belong in Git." });
|
|
@@ -6126,9 +7088,9 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6126
7088
|
knowledgeInSync = await opts.checkSync(root).catch(() => null);
|
|
6127
7089
|
if (knowledgeInSync === false) add({ level: "info", check: "knowledge", message: "Athena knowledge is out of date for these changes", files: [], hint: "Run `athena sync` so agents read current context." });
|
|
6128
7090
|
}
|
|
6129
|
-
const rulesText = await readTextIfExists(
|
|
7091
|
+
const rulesText = await readTextIfExists(path36.join(athenaDir(root), "rules.md"));
|
|
6130
7092
|
const rules = rulesText ? listRules(parseRules(rulesText)).filter((r) => r.enabled) : [];
|
|
6131
|
-
const reviewDoc = await readTextIfExists(
|
|
7093
|
+
const reviewDoc = await readTextIfExists(path36.join(athenaDir(root), "code-review.md"));
|
|
6132
7094
|
const checklist = reviewDoc ? [...reviewDoc.matchAll(/^- \[ \] (.+)$/gm)].map((m) => m[1].trim()) : [];
|
|
6133
7095
|
return { base, changedFiles, findings, rules, checklist, knowledgeInSync, stats: { added: addedCount + untrackedAdded, removed: removedCount, files: changedFiles.length } };
|
|
6134
7096
|
}
|
|
@@ -6137,11 +7099,11 @@ function hasBlockers(result) {
|
|
|
6137
7099
|
}
|
|
6138
7100
|
|
|
6139
7101
|
// src/services/context.ts
|
|
6140
|
-
import
|
|
7102
|
+
import path38 from "path";
|
|
6141
7103
|
|
|
6142
7104
|
// src/core/graph/graph.ts
|
|
6143
7105
|
import { z as z6 } from "zod";
|
|
6144
|
-
import
|
|
7106
|
+
import path37 from "path";
|
|
6145
7107
|
var NodeKind = z6.enum(["package", "file", "route", "entity", "framework", "command", "infra", "doc"]);
|
|
6146
7108
|
var EdgeKind = z6.enum(["contains", "imports", "handles", "defines", "depends_on", "documents", "runs"]);
|
|
6147
7109
|
var GraphNode = z6.object({
|
|
@@ -6165,7 +7127,7 @@ var IMPORT_RE = /^\s*(?:import\s[^'"]*from\s*['"]([^'"]+)['"]|import\s*['"]([^'"
|
|
|
6165
7127
|
var PY_IMPORT_RE = /^\s*(?:from\s+([.\w]+)\s+import|import\s+([.\w]+))/gm;
|
|
6166
7128
|
function resolveImport(fromFile, spec, fileSet) {
|
|
6167
7129
|
if (!spec.startsWith(".")) return null;
|
|
6168
|
-
const base = toPosix(
|
|
7130
|
+
const base = toPosix(path37.posix.normalize(path37.posix.join(path37.posix.dirname(fromFile), spec)));
|
|
6169
7131
|
const candidates = [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".py", ".vue", ".svelte"].flatMap((ext) => [`${base}${ext}`, `${base}/index${ext}`, `${base}/__init__${ext}`])];
|
|
6170
7132
|
return candidates.find((c2) => fileSet.has(c2)) ?? null;
|
|
6171
7133
|
}
|
|
@@ -6181,7 +7143,7 @@ async function buildGraph(model, opts) {
|
|
|
6181
7143
|
if (from === to || !nodes.has(from) || !nodes.has(to)) return;
|
|
6182
7144
|
if (!edges.some((e) => e.from === from && e.to === to && e.kind === kind)) edges.push({ from, to, kind });
|
|
6183
7145
|
};
|
|
6184
|
-
const fileNode = (p) => addNode({ id: nodeId("file", p), kind: "file", name:
|
|
7146
|
+
const fileNode = (p) => addNode({ id: nodeId("file", p), kind: "file", name: path37.posix.basename(p), path: p, meta: {} });
|
|
6185
7147
|
for (const pkg of model.workspace.packages) {
|
|
6186
7148
|
addNode({ id: nodeId("package", pkg.name), kind: "package", name: pkg.name, path: pkg.path, meta: { kind: pkg.kind, ecosystem: pkg.ecosystem } });
|
|
6187
7149
|
}
|
|
@@ -6200,8 +7162,8 @@ async function buildGraph(model, opts) {
|
|
|
6200
7162
|
for (const e of model.dbEntities) {
|
|
6201
7163
|
const id = addNode({ id: nodeId("entity", `${e.name}@${e.file}`), kind: "entity", name: e.name, path: e.file, meta: { kind: e.kind, fields: e.fields.length, indexes: e.indexes.length } });
|
|
6202
7164
|
addEdge(id, fileNode(e.file), "defines");
|
|
6203
|
-
for (const
|
|
6204
|
-
const target =
|
|
7165
|
+
for (const rel2 of e.relations) {
|
|
7166
|
+
const target = rel2.split("\u2192")[1]?.trim();
|
|
6205
7167
|
const other = target ? model.dbEntities.find((x) => x.name.toLowerCase() === target.toLowerCase()) : void 0;
|
|
6206
7168
|
if (other) addEdge(id, nodeId("entity", `${other.name}@${other.file}`), "depends_on");
|
|
6207
7169
|
}
|
|
@@ -6251,11 +7213,11 @@ async function buildGraph(model, opts) {
|
|
|
6251
7213
|
};
|
|
6252
7214
|
}
|
|
6253
7215
|
async function saveGraph(root, graph) {
|
|
6254
|
-
await writeFileAtomic(
|
|
7216
|
+
await writeFileAtomic(path37.join(athenaDir(root), GRAPH_FILE), `${JSON.stringify(graph, null, 2)}
|
|
6255
7217
|
`);
|
|
6256
7218
|
}
|
|
6257
7219
|
async function loadGraph(root) {
|
|
6258
|
-
const raw = await readTextIfExists(
|
|
7220
|
+
const raw = await readTextIfExists(path37.join(athenaDir(root), GRAPH_FILE));
|
|
6259
7221
|
if (!raw) return null;
|
|
6260
7222
|
try {
|
|
6261
7223
|
const parsed = ProjectGraph.safeParse(JSON.parse(raw));
|
|
@@ -6452,7 +7414,7 @@ function formatContext(ctx) {
|
|
|
6452
7414
|
|
|
6453
7415
|
// src/services/context.ts
|
|
6454
7416
|
async function loadModel(root) {
|
|
6455
|
-
const raw = await readTextIfExists(
|
|
7417
|
+
const raw = await readTextIfExists(path38.join(athenaDir(root), "model.json"));
|
|
6456
7418
|
if (!raw) return null;
|
|
6457
7419
|
try {
|
|
6458
7420
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -6468,7 +7430,7 @@ async function refreshGraph(root, opts = {}) {
|
|
|
6468
7430
|
const graph = await buildGraph(analysis.model, {
|
|
6469
7431
|
files,
|
|
6470
7432
|
signal: opts.signal,
|
|
6471
|
-
read: async (
|
|
7433
|
+
read: async (rel2) => readTextIfExists(path38.join(root, rel2)).catch(() => null)
|
|
6472
7434
|
});
|
|
6473
7435
|
await saveGraph(root, graph);
|
|
6474
7436
|
return graph;
|
|
@@ -6484,14 +7446,14 @@ async function getRelevantContext(root, task, opts = {}) {
|
|
|
6484
7446
|
const documents = [];
|
|
6485
7447
|
for (const d of KNOWLEDGE_DOCS) {
|
|
6486
7448
|
if (d.id === "rules") continue;
|
|
6487
|
-
const text = await readTextIfExists(
|
|
7449
|
+
const text = await readTextIfExists(path38.join(dir, d.file));
|
|
6488
7450
|
if (text) documents.push({ id: d.id, file: d.file, sections: documentSections(text) });
|
|
6489
7451
|
}
|
|
6490
7452
|
if (!documents.length) throw new AthenaError("No knowledge documents found.", "Run `athena init` first.", 3);
|
|
6491
7453
|
return selectContext({
|
|
6492
7454
|
task,
|
|
6493
7455
|
documents,
|
|
6494
|
-
rulesMarkdown: await readTextIfExists(
|
|
7456
|
+
rulesMarkdown: await readTextIfExists(path38.join(dir, "rules.md")),
|
|
6495
7457
|
graph: await getGraph(root, { build: opts.buildGraph, signal: opts.signal }),
|
|
6496
7458
|
maxChars: opts.maxChars
|
|
6497
7459
|
});
|
|
@@ -6502,7 +7464,7 @@ async function graphSummary(root) {
|
|
|
6502
7464
|
}
|
|
6503
7465
|
|
|
6504
7466
|
// src/services/ai.ts
|
|
6505
|
-
import
|
|
7467
|
+
import path39 from "path";
|
|
6506
7468
|
|
|
6507
7469
|
// src/ai/provider.ts
|
|
6508
7470
|
import { z as z7 } from "zod";
|
|
@@ -6692,7 +7654,7 @@ async function buildEnrichPayload(root, opts = {}) {
|
|
|
6692
7654
|
for (const id of wanted) {
|
|
6693
7655
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
6694
7656
|
if (!doc) throw new AthenaError(`Unknown document: ${id}`, `Available: ${KNOWLEDGE_DOCS.map((d) => d.id).join(", ")}`);
|
|
6695
|
-
const text = await readTextIfExists(
|
|
7657
|
+
const text = await readTextIfExists(path39.join(athenaDir(root), doc.file));
|
|
6696
7658
|
if (!text) continue;
|
|
6697
7659
|
included.push(doc.file);
|
|
6698
7660
|
parts.push(`--- ${doc.file} ---
|
|
@@ -6745,14 +7707,14 @@ async function enrich(root, opts = {}) {
|
|
|
6745
7707
|
body,
|
|
6746
7708
|
""
|
|
6747
7709
|
].join("\n");
|
|
6748
|
-
const file =
|
|
7710
|
+
const file = path39.join(athenaDir(root), SUGGESTIONS_FILE);
|
|
6749
7711
|
await writeFileAtomic(file, markdown);
|
|
6750
7712
|
return { markdown, file: SUGGESTIONS_FILE, provider: result.provider, model: result.model, chars: payload.chars, documents: payload.documents };
|
|
6751
7713
|
}
|
|
6752
7714
|
|
|
6753
7715
|
// src/services/watch.ts
|
|
6754
|
-
import { promises as
|
|
6755
|
-
import
|
|
7716
|
+
import { promises as fs25 } from "fs";
|
|
7717
|
+
import path40 from "path";
|
|
6756
7718
|
import { watch as chokidarWatch } from "chokidar";
|
|
6757
7719
|
async function gitDir(root) {
|
|
6758
7720
|
const r = await git(root, ["rev-parse", "--absolute-git-dir"]);
|
|
@@ -6770,7 +7732,7 @@ async function watchProject(root, opts) {
|
|
|
6770
7732
|
let rerun = false;
|
|
6771
7733
|
let latest = null;
|
|
6772
7734
|
let closed = false;
|
|
6773
|
-
const toRel = (abs) => toPosix(
|
|
7735
|
+
const toRel = (abs) => toPosix(path40.relative(root, abs));
|
|
6774
7736
|
const run3 = async () => {
|
|
6775
7737
|
if (closed) return null;
|
|
6776
7738
|
if (running) {
|
|
@@ -6829,14 +7791,14 @@ async function watchProject(root, opts) {
|
|
|
6829
7791
|
}, wait);
|
|
6830
7792
|
};
|
|
6831
7793
|
const onPath = (abs) => {
|
|
6832
|
-
const
|
|
6833
|
-
if (!
|
|
6834
|
-
if (
|
|
6835
|
-
const dir =
|
|
6836
|
-
void
|
|
6837
|
-
}
|
|
6838
|
-
pending.add(
|
|
6839
|
-
opts.onEvent({ type: "changes", paths: [
|
|
7794
|
+
const rel2 = toRel(abs);
|
|
7795
|
+
if (!rel2 || rel2.startsWith("..")) return;
|
|
7796
|
+
if (rel2 === ".gitignore" || rel2.endsWith("/.gitignore")) {
|
|
7797
|
+
const dir = rel2 === ".gitignore" ? "" : rel2.slice(0, -"/.gitignore".length);
|
|
7798
|
+
void fs25.readFile(abs, "utf8").then((text) => matcher.setGitignore(dir, text)).catch(() => matcher.removeGitignore(dir));
|
|
7799
|
+
}
|
|
7800
|
+
pending.add(rel2);
|
|
7801
|
+
opts.onEvent({ type: "changes", paths: [rel2] });
|
|
6840
7802
|
schedule();
|
|
6841
7803
|
};
|
|
6842
7804
|
const watcher = chokidarWatch(root, {
|
|
@@ -6844,12 +7806,12 @@ async function watchProject(root, opts) {
|
|
|
6844
7806
|
persistent: true,
|
|
6845
7807
|
followSymlinks: false,
|
|
6846
7808
|
ignored: (abs, stats) => {
|
|
6847
|
-
const
|
|
6848
|
-
if (!
|
|
6849
|
-
if (
|
|
6850
|
-
if (
|
|
6851
|
-
if (stats) return matcher.ignores(
|
|
6852
|
-
return matcher.ignores(
|
|
7809
|
+
const rel2 = toRel(abs);
|
|
7810
|
+
if (!rel2 || rel2 === ".") return false;
|
|
7811
|
+
if (rel2.startsWith("..")) return true;
|
|
7812
|
+
if (rel2 === ".git" || rel2.startsWith(".git/")) return true;
|
|
7813
|
+
if (stats) return matcher.ignores(rel2, stats.isDirectory());
|
|
7814
|
+
return matcher.ignores(rel2, false) || matcher.ignores(rel2, true);
|
|
6853
7815
|
}
|
|
6854
7816
|
});
|
|
6855
7817
|
const ready = new Promise((resolve2) => watcher.once("ready", () => resolve2()));
|
|
@@ -6860,7 +7822,7 @@ async function watchProject(root, opts) {
|
|
|
6860
7822
|
let lastHead = null;
|
|
6861
7823
|
if (gd) {
|
|
6862
7824
|
lastHead = (await git(root, ["rev-parse", "HEAD"])).stdout.trim() || null;
|
|
6863
|
-
headWatcher = chokidarWatch([
|
|
7825
|
+
headWatcher = chokidarWatch([path40.join(gd, "HEAD"), path40.join(gd, "refs", "heads")], { ignoreInitial: true, persistent: true, depth: 5 });
|
|
6864
7826
|
headWatcher.on("all", () => {
|
|
6865
7827
|
void git(root, ["rev-parse", "HEAD"]).then((r) => {
|
|
6866
7828
|
const head = r.ok ? r.stdout.trim() : null;
|
|
@@ -7012,8 +7974,8 @@ function allowedOriginsFor(host, port) {
|
|
|
7012
7974
|
}
|
|
7013
7975
|
|
|
7014
7976
|
// src/server/static.ts
|
|
7015
|
-
import { promises as
|
|
7016
|
-
import
|
|
7977
|
+
import { promises as fs26 } from "fs";
|
|
7978
|
+
import path41 from "path";
|
|
7017
7979
|
var TYPES = {
|
|
7018
7980
|
".html": "text/html; charset=utf-8",
|
|
7019
7981
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7029,20 +7991,20 @@ var TYPES = {
|
|
|
7029
7991
|
};
|
|
7030
7992
|
async function buildAssetMap(webDir) {
|
|
7031
7993
|
try {
|
|
7032
|
-
await
|
|
7994
|
+
await fs26.access(path41.join(webDir, "index.html"));
|
|
7033
7995
|
} catch {
|
|
7034
7996
|
return null;
|
|
7035
7997
|
}
|
|
7036
7998
|
const map = /* @__PURE__ */ new Map();
|
|
7037
7999
|
async function walk2(dir) {
|
|
7038
|
-
for (const ent of await
|
|
7039
|
-
const abs =
|
|
8000
|
+
for (const ent of await fs26.readdir(dir, { withFileTypes: true })) {
|
|
8001
|
+
const abs = path41.join(dir, ent.name);
|
|
7040
8002
|
if (ent.isDirectory()) await walk2(abs);
|
|
7041
8003
|
else if (ent.isFile()) {
|
|
7042
|
-
const ext =
|
|
8004
|
+
const ext = path41.extname(ent.name).toLowerCase();
|
|
7043
8005
|
const type = TYPES[ext];
|
|
7044
8006
|
if (!type) continue;
|
|
7045
|
-
const urlPath = `/${
|
|
8007
|
+
const urlPath = `/${path41.relative(webDir, abs).split(path41.sep).join("/")}`;
|
|
7046
8008
|
map.set(urlPath, { abs, type, immutable: urlPath.startsWith("/assets/") });
|
|
7047
8009
|
}
|
|
7048
8010
|
}
|
|
@@ -7261,11 +8223,11 @@ async function createServer(opts) {
|
|
|
7261
8223
|
const readNewAgentEvents = async () => {
|
|
7262
8224
|
try {
|
|
7263
8225
|
const file = activityFile(root);
|
|
7264
|
-
const st = await
|
|
8226
|
+
const st = await fs27.stat(file).catch(() => null);
|
|
7265
8227
|
if (!st) return;
|
|
7266
8228
|
if (st.size < activityOffset) activityOffset = 0;
|
|
7267
8229
|
if (st.size === activityOffset) return;
|
|
7268
|
-
const handle = await
|
|
8230
|
+
const handle = await fs27.open(file, "r");
|
|
7269
8231
|
try {
|
|
7270
8232
|
const length = st.size - activityOffset;
|
|
7271
8233
|
const buf = Buffer.alloc(length);
|
|
@@ -7310,7 +8272,7 @@ async function createServer(opts) {
|
|
|
7310
8272
|
try {
|
|
7311
8273
|
const result = await applySync(root, plan);
|
|
7312
8274
|
for (const d of plan.documents) {
|
|
7313
|
-
const text = await
|
|
8275
|
+
const text = await fs27.readFile(path42.join(athenaDir(root), d.file), "utf8").catch(() => null);
|
|
7314
8276
|
if (text !== null) recentWrites.set(d.file, contentHash(text));
|
|
7315
8277
|
}
|
|
7316
8278
|
currentPlan = null;
|
|
@@ -7335,7 +8297,7 @@ async function createServer(opts) {
|
|
|
7335
8297
|
});
|
|
7336
8298
|
let scanRunning = false;
|
|
7337
8299
|
const projectModel = async () => {
|
|
7338
|
-
const raw = await
|
|
8300
|
+
const raw = await fs27.readFile(path42.join(athenaDir(root), "model.json"), "utf8").catch(() => null);
|
|
7339
8301
|
if (raw) {
|
|
7340
8302
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
7341
8303
|
if (parsed.success) return parsed.data;
|
|
@@ -7375,7 +8337,7 @@ async function createServer(opts) {
|
|
|
7375
8337
|
);
|
|
7376
8338
|
app.get("/api/review", async (req) => {
|
|
7377
8339
|
const base = typeof req.query.base === "string" && req.query.base ? req.query.base : void 0;
|
|
7378
|
-
return reviewChanges(root, { base, checkSync: async (r) => (await planSync(r)).
|
|
8340
|
+
return reviewChanges(root, { base, checkSync: async (r) => staleForCheck(await planSync(r)).length === 0 });
|
|
7379
8341
|
});
|
|
7380
8342
|
let graphBuilding = false;
|
|
7381
8343
|
app.get("/api/context", async (req) => {
|
|
@@ -7433,7 +8395,7 @@ data: ${JSON.stringify(data)}
|
|
|
7433
8395
|
const sendAsset = async (reply, urlPath) => {
|
|
7434
8396
|
if (!assets) return reply.code(503).type("text/html; charset=utf-8").send(MISSING_UI_HTML);
|
|
7435
8397
|
const asset = assets.get(urlPath) ?? assets.get("/index.html");
|
|
7436
|
-
const body = await
|
|
8398
|
+
const body = await fs27.readFile(asset.abs);
|
|
7437
8399
|
reply.header("Cache-Control", asset.immutable ? "public, max-age=31536000, immutable" : "no-cache");
|
|
7438
8400
|
return reply.type(asset.type).send(body);
|
|
7439
8401
|
};
|
|
@@ -7468,7 +8430,7 @@ data: ${JSON.stringify(data)}
|
|
|
7468
8430
|
name,
|
|
7469
8431
|
setTimeout(async () => {
|
|
7470
8432
|
pending.delete(name);
|
|
7471
|
-
const text = await
|
|
8433
|
+
const text = await fs27.readFile(path42.join(dir, name), "utf8").catch(() => null);
|
|
7472
8434
|
const hash = text === null ? null : contentHash(text);
|
|
7473
8435
|
if (hash && recentWrites.get(name) === hash) return;
|
|
7474
8436
|
invalidateStatus();
|
|
@@ -7484,7 +8446,7 @@ data: ${JSON.stringify(data)}
|
|
|
7484
8446
|
{
|
|
7485
8447
|
const history = await readRecentEvents(root, 200);
|
|
7486
8448
|
for (const e of history) applyAgentEvent(e, false);
|
|
7487
|
-
activityOffset = await
|
|
8449
|
+
activityOffset = await fs27.stat(activityFile(root)).then((st) => st.size).catch(() => 0);
|
|
7488
8450
|
}
|
|
7489
8451
|
if (opts.watch) {
|
|
7490
8452
|
try {
|
|
@@ -7574,8 +8536,8 @@ async function findAvailablePort(host, start = DEFAULT_PORT, attempts = 100) {
|
|
|
7574
8536
|
|
|
7575
8537
|
// src/server/instance.ts
|
|
7576
8538
|
function defaultWebDir() {
|
|
7577
|
-
const here =
|
|
7578
|
-
return
|
|
8539
|
+
const here = path43.dirname(fileURLToPath(import.meta.url));
|
|
8540
|
+
return path43.basename(here) === "dist" ? path43.join(here, "web") : path43.resolve(here, "../../dist/web");
|
|
7579
8541
|
}
|
|
7580
8542
|
function browserUrl(info2) {
|
|
7581
8543
|
const host = info2.host.includes(":") ? `[${info2.host}]` : info2.host;
|
|
@@ -7591,8 +8553,8 @@ async function startServer(opts) {
|
|
|
7591
8553
|
const server = await createServer({ root: opts.root, token, host, port, webDir: opts.webDir ?? defaultWebDir(), allowRemote: opts.allowRemote, watch: opts.watch ?? true });
|
|
7592
8554
|
await server.app.listen({ host, port });
|
|
7593
8555
|
const info2 = { pid: process.pid, host, port, instanceId: server.instanceId, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7594
|
-
const file =
|
|
7595
|
-
await
|
|
8556
|
+
const file = path43.join(athenaDir(opts.root), INSTANCE_FILE);
|
|
8557
|
+
await fs28.writeFile(file, `${JSON.stringify(info2, null, 2)}
|
|
7596
8558
|
`, { mode: 384 });
|
|
7597
8559
|
await ensureGitignored(opts.root, INSTANCE_FILE);
|
|
7598
8560
|
return {
|
|
@@ -7601,16 +8563,16 @@ async function startServer(opts) {
|
|
|
7601
8563
|
url: browserUrl(info2),
|
|
7602
8564
|
async close() {
|
|
7603
8565
|
const current = await readTextIfExists(file);
|
|
7604
|
-
if (current && current.includes(server.instanceId)) await
|
|
8566
|
+
if (current && current.includes(server.instanceId)) await fs28.rm(file, { force: true });
|
|
7605
8567
|
await server.close();
|
|
7606
8568
|
}
|
|
7607
8569
|
};
|
|
7608
8570
|
}
|
|
7609
8571
|
async function ensureGitignored(root, entry) {
|
|
7610
|
-
const file =
|
|
8572
|
+
const file = path43.join(athenaDir(root), ".gitignore");
|
|
7611
8573
|
const text = await readTextIfExists(file) ?? "";
|
|
7612
8574
|
if (text.split(/\r?\n/).includes(entry)) return;
|
|
7613
|
-
await
|
|
8575
|
+
await fs28.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
|
|
7614
8576
|
`);
|
|
7615
8577
|
}
|
|
7616
8578
|
|
|
@@ -7654,7 +8616,7 @@ async function openCommand(opts) {
|
|
|
7654
8616
|
}
|
|
7655
8617
|
if (isJson()) json({ running: true, reused: false, url: running.url, port: running.info.port, pid: process.pid });
|
|
7656
8618
|
else {
|
|
7657
|
-
|
|
8619
|
+
printHeader();
|
|
7658
8620
|
line(c.bold(c.green("Athena is running")));
|
|
7659
8621
|
line();
|
|
7660
8622
|
line(`Local: ${c.cyan(running.url)}`);
|
|
@@ -7711,10 +8673,10 @@ function printPlan(plan, opts = {}) {
|
|
|
7711
8673
|
return;
|
|
7712
8674
|
}
|
|
7713
8675
|
heading("Knowledge to update");
|
|
7714
|
-
const
|
|
8676
|
+
const width2 = Math.max(...plan.documents.map((d) => d.file.length));
|
|
7715
8677
|
for (const d of plan.documents) {
|
|
7716
8678
|
const sign = d.status === "created" ? c.green("+") : c.yellow("~");
|
|
7717
|
-
line(` ${sign} ${d.file.padEnd(
|
|
8679
|
+
line(` ${sign} ${d.file.padEnd(width2)} ${c.green(`+${d.additions}`)} ${c.red(`\u2212${d.deletions}`)} ${dim(d.changedSections.join(", "))}`);
|
|
7718
8680
|
for (const r of d.reasons.slice(0, 3)) line(` ${dim(`${sym.arrow} ${r}`)}`);
|
|
7719
8681
|
if (d.preservedSections.length) line(` ${c.yellow(sym.warn)} ${dim(`your edits kept in: ${d.preservedSections.join(", ")}`)}`);
|
|
7720
8682
|
}
|
|
@@ -7738,9 +8700,15 @@ async function syncCommand(opts) {
|
|
|
7738
8700
|
sp.stop();
|
|
7739
8701
|
throw err;
|
|
7740
8702
|
}
|
|
8703
|
+
const stale = staleForCheck(plan);
|
|
8704
|
+
const inSync = stale.length === 0;
|
|
7741
8705
|
if (isJson() && (opts.dryRun || opts.check)) {
|
|
7742
|
-
json({
|
|
7743
|
-
|
|
8706
|
+
json({
|
|
8707
|
+
...summarizePlan(plan),
|
|
8708
|
+
...opts.check ? { inSync, stale: stale.map((d) => d.file) } : {},
|
|
8709
|
+
...opts.diff ? { diffs: Object.fromEntries(plan.documents.map((d) => [d.file, d.diff])) } : {}
|
|
8710
|
+
});
|
|
8711
|
+
return opts.check && !inSync ? 1 : EXIT.OK;
|
|
7744
8712
|
}
|
|
7745
8713
|
if (!isJson() && !isQuiet()) {
|
|
7746
8714
|
heading("Athena Sync");
|
|
@@ -7751,8 +8719,12 @@ async function syncCommand(opts) {
|
|
|
7751
8719
|
line(`Proposed: ${plan.documents.map((d) => d.file).join(", ")}`);
|
|
7752
8720
|
}
|
|
7753
8721
|
if (opts.check) {
|
|
7754
|
-
if (!
|
|
7755
|
-
|
|
8722
|
+
if (!inSync) line(c.yellow("Knowledge is out of date. Run `athena sync` to review and apply."));
|
|
8723
|
+
else {
|
|
8724
|
+
line(c.green("Knowledge is synchronized."));
|
|
8725
|
+
if (!plan.upToDate) info(dim("Only Git-history sections (change hotspots) differ; they are not checked. `athena sync` refreshes them."));
|
|
8726
|
+
}
|
|
8727
|
+
return inSync ? EXIT.OK : 1;
|
|
7756
8728
|
}
|
|
7757
8729
|
if (opts.dryRun) {
|
|
7758
8730
|
if (!isJson()) line(dim("Dry run \u2014 nothing was written."));
|
|
@@ -7789,7 +8761,7 @@ async function syncCommand(opts) {
|
|
|
7789
8761
|
}
|
|
7790
8762
|
|
|
7791
8763
|
// src/cli/commands/event.ts
|
|
7792
|
-
import
|
|
8764
|
+
import path44 from "path";
|
|
7793
8765
|
var STDIN_TIMEOUT_MS = 2e3;
|
|
7794
8766
|
var MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
7795
8767
|
async function readStdin() {
|
|
@@ -7835,7 +8807,7 @@ async function eventCommand(opts) {
|
|
|
7835
8807
|
let root = null;
|
|
7836
8808
|
for (const c2 of cwdCandidates) {
|
|
7837
8809
|
if (!c2) continue;
|
|
7838
|
-
root = await findProjectRoot(
|
|
8810
|
+
root = await findProjectRoot(path44.resolve(c2));
|
|
7839
8811
|
if (root) break;
|
|
7840
8812
|
}
|
|
7841
8813
|
if (!root) return;
|
|
@@ -7873,7 +8845,7 @@ async function activityCommand(opts) {
|
|
|
7873
8845
|
const agents = await listAgents(root);
|
|
7874
8846
|
const reporting = agents.filter((a) => a.activityObservation === "hooks");
|
|
7875
8847
|
line(dim("No agent activity recorded yet."));
|
|
7876
|
-
line(dim(reporting.length ? `Hooks are installed for: ${reporting.map((a) => a.name).join(", ")}. Events appear once an agent runs in this project.` : "Install hooks with `athena agents add claude-code` (or cursor) to record agent activity."));
|
|
8848
|
+
line(dim(reporting.length ? `Hooks are installed for: ${reporting.map((a) => a.name).join(", ")}. Events appear once an agent runs in this project.` : "Install hooks with `athena agents add claude-code` (or cursor, codex, copilot, gemini-cli) to record agent activity."));
|
|
7877
8849
|
return;
|
|
7878
8850
|
}
|
|
7879
8851
|
for (const e of events) {
|
|
@@ -7889,7 +8861,7 @@ async function activityCommand(opts) {
|
|
|
7889
8861
|
}
|
|
7890
8862
|
|
|
7891
8863
|
// src/cli/commands/security.ts
|
|
7892
|
-
import
|
|
8864
|
+
import path45 from "path";
|
|
7893
8865
|
var TONE = {
|
|
7894
8866
|
critical: c.red,
|
|
7895
8867
|
high: c.red,
|
|
@@ -7898,7 +8870,7 @@ var TONE = {
|
|
|
7898
8870
|
unknown: c.dim
|
|
7899
8871
|
};
|
|
7900
8872
|
async function loadModel2(root, signal) {
|
|
7901
|
-
const raw = await readTextIfExists(
|
|
8873
|
+
const raw = await readTextIfExists(path45.join(athenaDir(root), "model.json"));
|
|
7902
8874
|
if (raw) {
|
|
7903
8875
|
try {
|
|
7904
8876
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -7996,7 +8968,7 @@ async function reviewCommand(opts) {
|
|
|
7996
8968
|
result = await reviewChanges(root, {
|
|
7997
8969
|
base: opts.base,
|
|
7998
8970
|
signal: opts.signal,
|
|
7999
|
-
checkSync: opts.noSync ? void 0 : async (r) => (await planSync(r, { signal: opts.signal })).
|
|
8971
|
+
checkSync: opts.noSync ? void 0 : async (r) => staleForCheck(await planSync(r, { signal: opts.signal })).length === 0
|
|
8000
8972
|
});
|
|
8001
8973
|
sp.stop();
|
|
8002
8974
|
} catch (err) {
|
|
@@ -8203,13 +9175,13 @@ async function aiEnrichCommand(opts) {
|
|
|
8203
9175
|
}
|
|
8204
9176
|
|
|
8205
9177
|
// src/mcp/server.ts
|
|
8206
|
-
import
|
|
9178
|
+
import path46 from "path";
|
|
8207
9179
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8208
9180
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8209
9181
|
import { z as z8 } from "zod";
|
|
8210
9182
|
var textResult = (text) => ({ content: [{ type: "text", text }] });
|
|
8211
9183
|
async function docText(root, file) {
|
|
8212
|
-
const text = await readTextIfExists(
|
|
9184
|
+
const text = await readTextIfExists(path46.join(athenaDir(root), file));
|
|
8213
9185
|
return text ?? `${file} does not exist yet. Run \`athena init\` or \`athena analyze\` in this project.`;
|
|
8214
9186
|
}
|
|
8215
9187
|
function createMcpServer(opts) {
|
|
@@ -8270,7 +9242,7 @@ _Last dependency scan: ${scan.scannedAt}._` : "\n\n_No dependency audit has been
|
|
|
8270
9242
|
"get_project_rules",
|
|
8271
9243
|
{ title: "Project rules", description: "Developer-defined rules this project expects agents to follow. Disabled rules are excluded.", inputSchema: {} },
|
|
8272
9244
|
async () => {
|
|
8273
|
-
const text = await readTextIfExists(
|
|
9245
|
+
const text = await readTextIfExists(path46.join(athenaDir(root), "rules.md"));
|
|
8274
9246
|
if (!text) return textResult("No rules.md found. Run `athena init` in this project.");
|
|
8275
9247
|
const rules = listRules(parseRules(text)).filter((r) => r.enabled);
|
|
8276
9248
|
return textResult(rules.length ? ["# Project rules (all apply)", "", ...rules.map((r) => `- [${r.section}] ${r.text}`)].join("\n") : "rules.md contains no enabled rules.");
|
|
@@ -8461,17 +9433,227 @@ async function watchCommand(opts) {
|
|
|
8461
9433
|
await watcher.close();
|
|
8462
9434
|
}
|
|
8463
9435
|
|
|
9436
|
+
// src/cli/commands/git-hook.ts
|
|
9437
|
+
import path48 from "path";
|
|
9438
|
+
|
|
9439
|
+
// src/services/git-hook.ts
|
|
9440
|
+
import { promises as fs29 } from "fs";
|
|
9441
|
+
import path47 from "path";
|
|
9442
|
+
var HOOK_START = "# athena:start";
|
|
9443
|
+
var HOOK_END = "# athena:end";
|
|
9444
|
+
var HOOK_MANAGER_COMMAND = "npx --no-install athena sync --check";
|
|
9445
|
+
var SHELL_SHEBANG = /^#!\s*(?:\/usr\/bin\/env\s+)?(?:\/usr\/local\/bin\/|\/usr\/bin\/|\/bin\/)?(?:sh|bash|dash|zsh|ksh)(?:\s|$)/;
|
|
9446
|
+
var HOOK_MANAGERS = [
|
|
9447
|
+
{ name: "husky", pattern: /husky/i },
|
|
9448
|
+
{ name: "lefthook", pattern: /lefthook/i },
|
|
9449
|
+
{ name: "pre-commit", pattern: /pre-commit\.com|INSTALL_PYTHON|pre_commit/ },
|
|
9450
|
+
{ name: "simple-git-hooks", pattern: /simple-git-hooks/i },
|
|
9451
|
+
{ name: "overcommit", pattern: /overcommit/i }
|
|
9452
|
+
];
|
|
9453
|
+
async function locateHook(projectRoot) {
|
|
9454
|
+
const top = await git(projectRoot, ["rev-parse", "--show-toplevel"]);
|
|
9455
|
+
if (!top.ok || !top.stdout.trim()) throw new AthenaError("`athena git-hook` needs a Git repository.", "Run `git init` first, or use `athena sync --check` in your own tooling.");
|
|
9456
|
+
const topLevel = await fs29.realpath(top.stdout.trim());
|
|
9457
|
+
const hooks = await git(topLevel, ["rev-parse", "--git-path", "hooks"]);
|
|
9458
|
+
if (!hooks.ok || !hooks.stdout.trim()) throw new AthenaError("Could not find the Git hooks directory.");
|
|
9459
|
+
const hooksDir = path47.resolve(topLevel, hooks.stdout.trim());
|
|
9460
|
+
const projectPrefix = path47.relative(topLevel, await fs29.realpath(projectRoot)).split(path47.sep).join("/");
|
|
9461
|
+
return { topLevel, hooksDir, hookFile: path47.join(hooksDir, "pre-commit"), projectPrefix };
|
|
9462
|
+
}
|
|
9463
|
+
function shQuote(s) {
|
|
9464
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
9465
|
+
}
|
|
9466
|
+
function renderHookBlock(opts) {
|
|
9467
|
+
const cmd = opts.command ?? hookCommand();
|
|
9468
|
+
const probe = cmd.split(/\s+/)[0];
|
|
9469
|
+
const cwdArg = opts.projectPrefix ? ` -C ${shQuote(opts.projectPrefix)}` : "";
|
|
9470
|
+
const athena = `${cmd} --no-color${cwdArg}`;
|
|
9471
|
+
const lines = [
|
|
9472
|
+
`${HOOK_START} (added by \`athena git-hook install\`; remove with \`athena git-hook uninstall\`)`,
|
|
9473
|
+
`if command -v ${probe} >/dev/null 2>&1; then`,
|
|
9474
|
+
" athena_rc=0",
|
|
9475
|
+
` athena_out=$(${athena} sync --check --quiet 2>&1) || athena_rc=$?`,
|
|
9476
|
+
' if [ "$athena_rc" -eq 3 ]; then',
|
|
9477
|
+
" echo 'athena: Athena is not initialized in this project; skipping the knowledge check.' >&2",
|
|
9478
|
+
' elif [ "$athena_rc" -ne 0 ]; then',
|
|
9479
|
+
` printf '%s\\n' "$athena_out" >&2`,
|
|
9480
|
+
" echo 'athena: commit blocked: .athena/ knowledge is out of date. Run `athena sync`, stage .athena/ and commit again (or skip once with `git commit --no-verify`).' >&2",
|
|
9481
|
+
" exit 1",
|
|
9482
|
+
" fi"
|
|
9483
|
+
];
|
|
9484
|
+
if (opts.review) {
|
|
9485
|
+
lines.push(
|
|
9486
|
+
" athena_rc=0",
|
|
9487
|
+
` athena_out=$(${athena} review --no-sync 2>&1) || athena_rc=$?`,
|
|
9488
|
+
' if [ "$athena_rc" -ne 0 ] && [ "$athena_rc" -ne 3 ]; then',
|
|
9489
|
+
` printf '%s\\n' "$athena_out" >&2`,
|
|
9490
|
+
" echo 'athena: commit blocked: `athena review` found blockers (possible secrets or env files). Fix them, or skip once with `git commit --no-verify`.' >&2",
|
|
9491
|
+
" exit 1",
|
|
9492
|
+
" fi"
|
|
9493
|
+
);
|
|
9494
|
+
}
|
|
9495
|
+
lines.push("else", ` echo 'athena: \`${probe}\` not found on PATH; skipping Athena pre-commit checks.' >&2`, "fi", HOOK_END);
|
|
9496
|
+
return `${lines.join("\n")}
|
|
9497
|
+
`;
|
|
9498
|
+
}
|
|
9499
|
+
function parseHook(text) {
|
|
9500
|
+
const start = text.indexOf(`${HOOK_START}`);
|
|
9501
|
+
if (start < 0) return null;
|
|
9502
|
+
const lineStart = text.lastIndexOf("\n", start) + 1;
|
|
9503
|
+
const endIdx = text.indexOf(HOOK_END, start);
|
|
9504
|
+
if (endIdx < 0) return null;
|
|
9505
|
+
let blockEnd = endIdx + HOOK_END.length;
|
|
9506
|
+
if (text[blockEnd] === "\n") blockEnd++;
|
|
9507
|
+
return { before: text.slice(0, lineStart), block: text.slice(lineStart, blockEnd), after: text.slice(blockEnd) };
|
|
9508
|
+
}
|
|
9509
|
+
function detectManager(loc, text) {
|
|
9510
|
+
const rel2 = path47.relative(loc.topLevel, loc.hooksDir).split(path47.sep);
|
|
9511
|
+
if (rel2.includes(".husky")) return "husky";
|
|
9512
|
+
if (text) {
|
|
9513
|
+
for (const m of HOOK_MANAGERS) if (m.pattern.test(text)) return m.name;
|
|
9514
|
+
}
|
|
9515
|
+
return null;
|
|
9516
|
+
}
|
|
9517
|
+
var NEW_FILE_HEADER = "#!/bin/sh\n";
|
|
9518
|
+
async function hookStatus(projectRoot) {
|
|
9519
|
+
const loc = await locateHook(projectRoot);
|
|
9520
|
+
const text = await readTextIfExists(loc.hookFile);
|
|
9521
|
+
const parsed = text ? parseHook(text) : null;
|
|
9522
|
+
return {
|
|
9523
|
+
...loc,
|
|
9524
|
+
installed: Boolean(parsed),
|
|
9525
|
+
review: Boolean(parsed?.block.includes(" review ")),
|
|
9526
|
+
ownsFile: Boolean(parsed && `${parsed.before}${parsed.after}`.trim() === NEW_FILE_HEADER.trim()),
|
|
9527
|
+
manager: detectManager(loc, text && parsed ? `${parsed.before}${parsed.after}` : text)
|
|
9528
|
+
};
|
|
9529
|
+
}
|
|
9530
|
+
async function installHook(projectRoot, opts = {}) {
|
|
9531
|
+
const loc = await locateHook(projectRoot);
|
|
9532
|
+
const block = renderHookBlock({ projectPrefix: loc.projectPrefix, review: Boolean(opts.review) });
|
|
9533
|
+
const text = await readTextIfExists(loc.hookFile);
|
|
9534
|
+
let next;
|
|
9535
|
+
let action;
|
|
9536
|
+
const parsed = text ? parseHook(text) : null;
|
|
9537
|
+
if (text === null) {
|
|
9538
|
+
const manager = detectManager(loc, null);
|
|
9539
|
+
if (manager) throw managedError(manager);
|
|
9540
|
+
next = `${NEW_FILE_HEADER}
|
|
9541
|
+
${block}`;
|
|
9542
|
+
action = "created";
|
|
9543
|
+
} else if (parsed) {
|
|
9544
|
+
next = `${parsed.before}${block}${parsed.after}`;
|
|
9545
|
+
action = next === text ? "unchanged" : "updated";
|
|
9546
|
+
} else {
|
|
9547
|
+
const manager = detectManager(loc, text);
|
|
9548
|
+
if (manager) throw managedError(manager);
|
|
9549
|
+
const firstLine = text.split("\n", 1)[0];
|
|
9550
|
+
if (!SHELL_SHEBANG.test(firstLine)) {
|
|
9551
|
+
throw conflict(
|
|
9552
|
+
`${path47.relative(loc.topLevel, loc.hookFile) || loc.hookFile} already exists and is not a shell script, so Athena will not modify it.`,
|
|
9553
|
+
`Add this command to your pre-commit hook yourself: ${HOOK_MANAGER_COMMAND}`
|
|
9554
|
+
);
|
|
9555
|
+
}
|
|
9556
|
+
const rest = text.slice(firstLine.length + 1);
|
|
9557
|
+
next = `${firstLine}
|
|
9558
|
+
${block}
|
|
9559
|
+
${rest}`;
|
|
9560
|
+
action = "added-to-existing";
|
|
9561
|
+
}
|
|
9562
|
+
if (action !== "unchanged") {
|
|
9563
|
+
await fs29.mkdir(loc.hooksDir, { recursive: true });
|
|
9564
|
+
await fs29.writeFile(loc.hookFile, next, "utf8");
|
|
9565
|
+
}
|
|
9566
|
+
await fs29.chmod(loc.hookFile, 493);
|
|
9567
|
+
return { action, status: await hookStatus(projectRoot) };
|
|
9568
|
+
}
|
|
9569
|
+
async function uninstallHook(projectRoot) {
|
|
9570
|
+
const loc = await locateHook(projectRoot);
|
|
9571
|
+
const text = await readTextIfExists(loc.hookFile);
|
|
9572
|
+
const parsed = text ? parseHook(text) : null;
|
|
9573
|
+
if (!parsed) return { removed: false, deletedFile: false, status: await hookStatus(projectRoot) };
|
|
9574
|
+
let rest = parsed.after;
|
|
9575
|
+
if (rest.startsWith("\n")) rest = rest.slice(1);
|
|
9576
|
+
const remaining = `${parsed.before}${rest}`;
|
|
9577
|
+
let deletedFile = false;
|
|
9578
|
+
if (!remaining.trim() || remaining.trim() === NEW_FILE_HEADER.trim()) {
|
|
9579
|
+
await fs29.rm(loc.hookFile, { force: true });
|
|
9580
|
+
deletedFile = true;
|
|
9581
|
+
} else {
|
|
9582
|
+
await fs29.writeFile(loc.hookFile, remaining, "utf8");
|
|
9583
|
+
}
|
|
9584
|
+
return { removed: true, deletedFile, status: await hookStatus(projectRoot) };
|
|
9585
|
+
}
|
|
9586
|
+
function managedError(manager) {
|
|
9587
|
+
return conflict(
|
|
9588
|
+
`Git hooks in this repository are managed by ${manager}, so Athena will not write a hook file.`,
|
|
9589
|
+
`Add this line to your ${manager} pre-commit configuration instead: ${HOOK_MANAGER_COMMAND}`
|
|
9590
|
+
);
|
|
9591
|
+
}
|
|
9592
|
+
|
|
9593
|
+
// src/cli/commands/git-hook.ts
|
|
9594
|
+
var rel = (s) => path48.relative(s.topLevel, s.hookFile).split(path48.sep).join("/") || s.hookFile;
|
|
9595
|
+
function managerTip() {
|
|
9596
|
+
line(dim(`Using husky, lefthook or pre-commit? Add \`${HOOK_MANAGER_COMMAND}\` to its pre-commit config instead.`));
|
|
9597
|
+
}
|
|
9598
|
+
async function gitHookInstallCommand(opts) {
|
|
9599
|
+
const root = await requireProjectRoot(opts);
|
|
9600
|
+
const { action, status: status2 } = await installHook(root, { review: opts.review });
|
|
9601
|
+
if (isJson()) {
|
|
9602
|
+
json({ action, ...status2 });
|
|
9603
|
+
return EXIT.OK;
|
|
9604
|
+
}
|
|
9605
|
+
const what = status2.review ? "knowledge check + review" : "knowledge check";
|
|
9606
|
+
const message = {
|
|
9607
|
+
created: `Installed the pre-commit hook (${what}) at ${rel(status2)}`,
|
|
9608
|
+
updated: `Updated Athena's block in ${rel(status2)} (${what})`,
|
|
9609
|
+
unchanged: `Pre-commit hook already installed at ${rel(status2)} (${what})`,
|
|
9610
|
+
"added-to-existing": `Added Athena's block to your existing ${rel(status2)} (${what}); the rest of the hook is unchanged`
|
|
9611
|
+
};
|
|
9612
|
+
ok(message[action]);
|
|
9613
|
+
info(dim("Commits are blocked when .athena/ knowledge is out of date. Teammates without Athena installed are not blocked."));
|
|
9614
|
+
if (!isQuiet()) managerTip();
|
|
9615
|
+
return EXIT.OK;
|
|
9616
|
+
}
|
|
9617
|
+
async function gitHookUninstallCommand(opts) {
|
|
9618
|
+
const root = await requireProjectRoot(opts);
|
|
9619
|
+
const r = await uninstallHook(root);
|
|
9620
|
+
if (isJson()) {
|
|
9621
|
+
json({ removed: r.removed, deletedFile: r.deletedFile, ...r.status });
|
|
9622
|
+
return EXIT.OK;
|
|
9623
|
+
}
|
|
9624
|
+
if (!r.removed) line(`No Athena pre-commit hook found in ${rel(r.status)}.`);
|
|
9625
|
+
else if (r.deletedFile) ok(`Removed ${rel(r.status)}`);
|
|
9626
|
+
else ok(`Removed Athena's block from ${rel(r.status)}; the rest of the hook is unchanged`);
|
|
9627
|
+
return EXIT.OK;
|
|
9628
|
+
}
|
|
9629
|
+
async function gitHookStatusCommand(opts) {
|
|
9630
|
+
const root = await requireProjectRoot(opts);
|
|
9631
|
+
const s = await hookStatus(root);
|
|
9632
|
+
if (isJson()) {
|
|
9633
|
+
json(s);
|
|
9634
|
+
return EXIT.OK;
|
|
9635
|
+
}
|
|
9636
|
+
if (s.installed) {
|
|
9637
|
+
ok(`Installed in ${rel(s)} (${s.review ? "knowledge check + review" : "knowledge check"}${s.ownsFile ? "" : "; shared with your own hook"})`);
|
|
9638
|
+
} else {
|
|
9639
|
+
line(`Not installed ${dim(`(${rel(s)})`)}`);
|
|
9640
|
+
if (s.manager) line(dim(`Hooks here are managed by ${s.manager}: add \`${HOOK_MANAGER_COMMAND}\` to its pre-commit config.`));
|
|
9641
|
+
else line(dim("Run `athena git-hook install` (add `--review` to also block commits with secrets or env files)."));
|
|
9642
|
+
}
|
|
9643
|
+
return EXIT.OK;
|
|
9644
|
+
}
|
|
9645
|
+
|
|
8464
9646
|
// src/cli/commands/agents.ts
|
|
8465
9647
|
async function agentsListCommand(opts) {
|
|
8466
9648
|
const root = await requireProjectRoot(opts);
|
|
8467
|
-
const
|
|
9649
|
+
const rows2 = await listAgents(root);
|
|
8468
9650
|
if (isJson()) {
|
|
8469
|
-
json(
|
|
9651
|
+
json(rows2);
|
|
8470
9652
|
return;
|
|
8471
9653
|
}
|
|
8472
9654
|
heading("AI Agent Integrations");
|
|
8473
9655
|
line();
|
|
8474
|
-
for (const r of
|
|
9656
|
+
for (const r of rows2) {
|
|
8475
9657
|
const icon = r.configured ? c.green(sym.dot) : c.dim(sym.ring);
|
|
8476
9658
|
line(`${icon} ${c.bold(r.name)} ${dim(`(${r.id})`)} \u2014 ${r.configured ? "configured" : "not configured"}${r.detectedInProject ? dim(` \xB7 detected: ${r.evidence.join(", ")}`) : ""}`);
|
|
8477
9659
|
line(` ${dim(r.note)}`);
|
|
@@ -8504,6 +9686,10 @@ function onSignal() {
|
|
|
8504
9686
|
${c.yellow("Interrupted.")} ${dim(getInterruptMessage())}
|
|
8505
9687
|
`);
|
|
8506
9688
|
}
|
|
9689
|
+
process.stdout.on("error", (err) => {
|
|
9690
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
9691
|
+
throw err;
|
|
9692
|
+
});
|
|
8507
9693
|
process.on("SIGINT", onSignal);
|
|
8508
9694
|
process.on("SIGTERM", onSignal);
|
|
8509
9695
|
var PLANNED = [];
|
|
@@ -8522,7 +9708,7 @@ function run2(fn) {
|
|
|
8522
9708
|
function buildProgram() {
|
|
8523
9709
|
const program = new Command2();
|
|
8524
9710
|
program.name("athena").description("Athena \u2014 local-first project intelligence for AI coding agents").version(ATHENA_VERSION, "-v, --version", "Print the Athena version").addOption(new Option("-C, --cwd <dir>", "Run as if started in <dir>")).option("--json", "Machine-readable JSON output").option("-q, --quiet", "Only print warnings, errors and results").option("--no-color", "Disable colors (also respects NO_COLOR)").showHelpAfterError("(run `athena --help` for usage)").configureHelp({ sortSubcommands: false });
|
|
8525
|
-
program.command("init").description("Analyze the project and create .athena/ knowledge").option("--agents <list>", 'Agents to configure: comma-separated ids (claude-code, cursor, antigravity, agents-md), "all" or "none". Default: detected agents + AGENTS.md').option("--no-agents", "Do not configure any agent integration").option("--dry-run", "Analyze and show what would be written, without writing").action(
|
|
9711
|
+
program.command("init").description("Analyze the project and create .athena/ knowledge").option("--agents <list>", 'Agents to configure: comma-separated ids (claude-code, cursor, codex, copilot, gemini-cli, antigravity, windsurf, cline, agents-md), "all" or "none". Default: detected agents + AGENTS.md').option("--no-agents", "Do not configure any agent integration").option("--dry-run", "Analyze and show what would be written, without writing").action(
|
|
8526
9712
|
run2(async (cmdOpts, cmd) => {
|
|
8527
9713
|
const g = globals(cmd);
|
|
8528
9714
|
await initCommand({ ...g, agents: typeof cmdOpts.agents === "string" ? cmdOpts.agents : void 0, noAgents: cmdOpts.agents === false, dryRun: cmdOpts.dryRun, signal: controller.signal });
|
|
@@ -8551,8 +9737,12 @@ function buildProgram() {
|
|
|
8551
9737
|
rules.command("remove <number>").description("Delete a rule").action(run2(async (n, _o, cmd) => rulesRemoveCommand(n, globals(cmd))));
|
|
8552
9738
|
const agents = program.command("agents").description("Manage AI agent integrations");
|
|
8553
9739
|
agents.command("list", { isDefault: true }).description("Show supported agents and integration status").action(run2(async (_o, cmd) => agentsListCommand(globals(cmd))));
|
|
8554
|
-
agents.command("add <agents...>").description('Configure integrations (claude-code, cursor, antigravity, agents-md, or "all")').action(run2(async (names2, _o, cmd) => agentsAddCommand(names2, globals(cmd))));
|
|
9740
|
+
agents.command("add <agents...>").description('Configure integrations (claude-code, cursor, codex, copilot, gemini-cli, antigravity, windsurf, cline, agents-md, or "all")').action(run2(async (names2, _o, cmd) => agentsAddCommand(names2, globals(cmd))));
|
|
8555
9741
|
agents.command("remove <agents...>").description("Remove Athena-managed integration files/blocks").action(run2(async (names2, _o, cmd) => agentsRemoveCommand(names2, globals(cmd))));
|
|
9742
|
+
const gitHook = program.command("git-hook").description("Install a Git pre-commit hook that blocks commits when knowledge is out of date");
|
|
9743
|
+
gitHook.command("status", { isDefault: true }).description("Show whether the Athena pre-commit hook is installed").action(run2(async (_o, cmd) => gitHookStatusCommand(globals(cmd))));
|
|
9744
|
+
gitHook.command("install").description("Add the Athena pre-commit hook (existing hooks are kept; Athena adds a marked block)").option("--review", "Also run `athena review` and block commits with possible secrets or env files").action(run2(async (o, cmd) => gitHookInstallCommand({ ...globals(cmd), review: o.review })));
|
|
9745
|
+
gitHook.command("uninstall").description("Remove Athena's pre-commit hook or block").action(run2(async (_o, cmd) => gitHookUninstallCommand(globals(cmd))));
|
|
8556
9746
|
program.command("open").description("Start (or reuse) the local web UI and open it in your browser").option("-p, --port <port>", "Port to bind (default: first free port from 7432)").option("--host <host>", "Interface to bind (default: 127.0.0.1)").option("--allow-remote", "Allow binding to a non-loopback interface").option("--no-open", "Do not launch a browser").option("--no-watch", "Do not watch the project for changes").action(run2(async (o, cmd) => openCommand({ ...globals(cmd), ...o, signal: controller.signal })));
|
|
8557
9747
|
program.command("activity").description("Show AI agent activity observed through hooks").option("-n, --limit <count>", "Number of events to show (default 30)").option("--agent <id>", "Only show events from this agent").action(run2(async (o, cmd) => activityCommand({ ...globals(cmd), ...o })));
|
|
8558
9748
|
program.command("event").description("Record an agent hook event (called by AI coding agents, reads JSON on stdin)").option("--agent <id>", "Agent that fired the hook").option("--hook <name>", "Hook event name").option("--athena-hook", "Marker used to identify Athena-managed hook entries").option("--verbose-errors", "Print errors to stderr (hooks stay silent by default)").action(
|