project-athena 0.1.3 → 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 +1032 -299
- 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 +5 -3
- 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
|
@@ -9,6 +9,7 @@ var PACKAGE_NAME = "project-athena";
|
|
|
9
9
|
var AUTHOR = "Sabarivasan";
|
|
10
10
|
var NPM_URL = `https://www.npmjs.com/package/${PACKAGE_NAME}`;
|
|
11
11
|
var REPO_URL = "https://github.com/Sabari-Vasan-SM/athena";
|
|
12
|
+
var WEBSITE_URL = "https://athena.sabari.me";
|
|
12
13
|
function readVersion() {
|
|
13
14
|
try {
|
|
14
15
|
const require2 = createRequire(import.meta.url);
|
|
@@ -150,7 +151,7 @@ function relativeTime(iso) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
// src/cli/commands/init.ts
|
|
153
|
-
import
|
|
154
|
+
import path26 from "path";
|
|
154
155
|
|
|
155
156
|
// src/core/state/state.ts
|
|
156
157
|
import { promises as fs2 } from "fs";
|
|
@@ -354,8 +355,8 @@ function diffFileIndex(prev, next) {
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
// src/agents/registry.ts
|
|
357
|
-
import { promises as
|
|
358
|
-
import
|
|
358
|
+
import { promises as fs10 } from "fs";
|
|
359
|
+
import path15 from "path";
|
|
359
360
|
|
|
360
361
|
// src/core/util/paths.ts
|
|
361
362
|
import path4 from "path";
|
|
@@ -366,9 +367,9 @@ function resolveInside(root, candidate) {
|
|
|
366
367
|
if (candidate.includes("\0")) return null;
|
|
367
368
|
const resolvedRoot = path4.resolve(root);
|
|
368
369
|
const resolved = path4.resolve(resolvedRoot, candidate);
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
if (
|
|
370
|
+
const rel2 = path4.relative(resolvedRoot, resolved);
|
|
371
|
+
if (rel2 === "") return resolved;
|
|
372
|
+
if (rel2.startsWith("..") || path4.isAbsolute(rel2)) return null;
|
|
372
373
|
return resolved;
|
|
373
374
|
}
|
|
374
375
|
function dirOf(posixPath) {
|
|
@@ -577,18 +578,18 @@ async function readJsonFile(file) {
|
|
|
577
578
|
function isAthenaEntry(entry) {
|
|
578
579
|
return JSON.stringify(entry ?? "").includes(ATHENA_MARK);
|
|
579
580
|
}
|
|
580
|
-
async function planJsonChange(root,
|
|
581
|
-
const file = path5.join(root,
|
|
581
|
+
async function planJsonChange(root, rel2, mutate2) {
|
|
582
|
+
const file = path5.join(root, rel2);
|
|
582
583
|
const { data, existed, malformed } = await readJsonFile(file);
|
|
583
|
-
if (malformed) throw new Error(`${
|
|
584
|
+
if (malformed) throw new Error(`${rel2} is not valid JSON \u2014 fix it before configuring hooks`);
|
|
584
585
|
const before = JSON.stringify(data);
|
|
585
586
|
mutate2(data);
|
|
586
587
|
const content = `${JSON.stringify(data, null, 2)}
|
|
587
588
|
`;
|
|
588
|
-
return { path:
|
|
589
|
+
return { path: rel2, ownership: "block", action: !existed ? "create" : JSON.stringify(data) === before ? "unchanged" : "update", content };
|
|
589
590
|
}
|
|
590
|
-
async function removeJsonHooks(root,
|
|
591
|
-
const file = path5.join(root,
|
|
591
|
+
async function removeJsonHooks(root, rel2, strip2, isEmpty) {
|
|
592
|
+
const file = path5.join(root, rel2);
|
|
592
593
|
const { data, existed, malformed } = await readJsonFile(file);
|
|
593
594
|
if (!existed || malformed) return false;
|
|
594
595
|
const before = JSON.stringify(data);
|
|
@@ -609,7 +610,7 @@ function hookScript(agent, platform = process.platform) {
|
|
|
609
610
|
if (platform === "win32") {
|
|
610
611
|
return {
|
|
611
612
|
file: "athena-hook.cmd",
|
|
612
|
-
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"),
|
|
613
614
|
mode: 420
|
|
614
615
|
};
|
|
615
616
|
}
|
|
@@ -636,20 +637,20 @@ function stripMcpServers(data) {
|
|
|
636
637
|
|
|
637
638
|
// src/agents/common/files.ts
|
|
638
639
|
var OWNED_HEADER = "<!-- Generated by Athena. This file is owned by Athena and may be rewritten by `athena init`/`athena agents`. -->";
|
|
639
|
-
async function planBlock(root,
|
|
640
|
-
const existing = await readTextIfExists(path6.join(root,
|
|
640
|
+
async function planBlock(root, rel2, body) {
|
|
641
|
+
const existing = await readTextIfExists(path6.join(root, rel2));
|
|
641
642
|
const content = upsertIntegrationBlock(existing, body);
|
|
642
|
-
return { path:
|
|
643
|
+
return { path: rel2, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
|
|
643
644
|
}
|
|
644
|
-
async function planOwned(root,
|
|
645
|
-
const existing = await readTextIfExists(path6.join(root,
|
|
645
|
+
async function planOwned(root, rel2, content) {
|
|
646
|
+
const existing = await readTextIfExists(path6.join(root, rel2));
|
|
646
647
|
if (existing !== null && !existing.includes("Generated by Athena")) {
|
|
647
|
-
return { path:
|
|
648
|
+
return { path: rel2, ownership: "owned", action: "unchanged", content: existing };
|
|
648
649
|
}
|
|
649
|
-
return { path:
|
|
650
|
+
return { path: rel2, ownership: "owned", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
|
|
650
651
|
}
|
|
651
|
-
async function removeBlockFrom(root,
|
|
652
|
-
const file = path6.join(root,
|
|
652
|
+
async function removeBlockFrom(root, rel2) {
|
|
653
|
+
const file = path6.join(root, rel2);
|
|
653
654
|
const existing = await readTextIfExists(file);
|
|
654
655
|
if (existing === null || !existing.includes(INTEGRATION_START)) return false;
|
|
655
656
|
const next = removeIntegrationBlock(existing);
|
|
@@ -657,36 +658,40 @@ async function removeBlockFrom(root, rel) {
|
|
|
657
658
|
else await fs4.writeFile(file, next, "utf8");
|
|
658
659
|
return true;
|
|
659
660
|
}
|
|
660
|
-
async function removeOwned(root,
|
|
661
|
-
const file = path6.join(root,
|
|
661
|
+
async function removeOwned(root, rel2) {
|
|
662
|
+
const file = path6.join(root, rel2);
|
|
662
663
|
const existing = await readTextIfExists(file);
|
|
663
664
|
if (existing === null || !existing.includes("Generated by Athena")) return false;
|
|
664
665
|
await fs4.rm(file);
|
|
665
666
|
return true;
|
|
666
667
|
}
|
|
667
|
-
async function checkBlock(root,
|
|
668
|
-
const text = await readTextIfExists(path6.join(root,
|
|
669
|
-
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\`)` };
|
|
670
671
|
const s = text.indexOf(INTEGRATION_START);
|
|
671
672
|
const e = text.indexOf(INTEGRATION_END);
|
|
672
|
-
if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${
|
|
673
|
-
if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${
|
|
674
|
-
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}` };
|
|
675
676
|
}
|
|
676
677
|
async function userEvidence(root, spec) {
|
|
677
678
|
const found = [];
|
|
678
|
-
const owned = new Set(
|
|
679
|
-
for (const
|
|
680
|
-
|
|
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);
|
|
681
683
|
}
|
|
682
|
-
for (const
|
|
683
|
-
|
|
684
|
+
for (const rel2 of spec.athenaOwnedJson ?? []) {
|
|
685
|
+
if (await jsonOnlyAthena(path6.join(root, rel2))) owned.add(rel2);
|
|
686
|
+
}
|
|
687
|
+
for (const rel2 of spec.files ?? []) {
|
|
688
|
+
const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
|
|
684
689
|
if (text === null) continue;
|
|
685
690
|
if (text.includes(OWNED_HEADER)) continue;
|
|
686
|
-
if (removeIntegrationBlock(text).trim()) found.push(
|
|
691
|
+
if (removeIntegrationBlock(text).trim()) found.push(rel2);
|
|
687
692
|
}
|
|
688
|
-
for (const
|
|
689
|
-
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);
|
|
690
695
|
}
|
|
691
696
|
return found;
|
|
692
697
|
}
|
|
@@ -699,8 +704,8 @@ async function hasNonAthenaEntries(dir, root, owned) {
|
|
|
699
704
|
}
|
|
700
705
|
for (const e of entries) {
|
|
701
706
|
const abs = path6.join(dir, e.name);
|
|
702
|
-
const
|
|
703
|
-
if (owned.has(
|
|
707
|
+
const rel2 = path6.relative(root, abs).split(path6.sep).join("/");
|
|
708
|
+
if (owned.has(rel2)) continue;
|
|
704
709
|
if (e.isDirectory()) {
|
|
705
710
|
if (await hasNonAthenaEntries(abs, root, owned)) return true;
|
|
706
711
|
} else return true;
|
|
@@ -717,12 +722,14 @@ async function jsonOnlyAthena(file) {
|
|
|
717
722
|
return false;
|
|
718
723
|
}
|
|
719
724
|
const record = data;
|
|
720
|
-
const servers = record?.mcpServers;
|
|
721
725
|
let hadAthenaMcp = false;
|
|
722
|
-
|
|
723
|
-
|
|
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;
|
|
724
731
|
delete servers.athena;
|
|
725
|
-
if (Object.keys(servers).length === 0) delete record
|
|
732
|
+
if (Object.keys(servers).length === 0) delete record[key];
|
|
726
733
|
}
|
|
727
734
|
if (!text.includes(ATHENA_MARK) && !hadAthenaMcp) return false;
|
|
728
735
|
const strip2 = (value) => {
|
|
@@ -752,8 +759,9 @@ async function jsonOnlyAthena(file) {
|
|
|
752
759
|
import crypto2 from "crypto";
|
|
753
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;
|
|
754
761
|
var REVIEW_COMMAND = /\b(lint|eslint|ruff|clippy|tsc|typecheck|type-check|mypy|flake8|rubocop|golangci-lint|audit)\b/i;
|
|
755
|
-
var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace"]);
|
|
756
|
-
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"]);
|
|
757
765
|
function truncate(s, n = 160) {
|
|
758
766
|
const one = s.replace(/\s+/g, " ").trim();
|
|
759
767
|
return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
|
|
@@ -779,18 +787,26 @@ function filesFrom(input, payload) {
|
|
|
779
787
|
}
|
|
780
788
|
return [...new Set(out2)];
|
|
781
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
|
+
}
|
|
782
798
|
function classifyTool(tool, command, files) {
|
|
783
799
|
const t = tool.toLowerCase().replace(/^mcp__/, "");
|
|
784
800
|
const where = files.length ? ` ${files.slice(0, 2).join(", ")}${files.length > 2 ? ` +${files.length - 2}` : ""}` : "";
|
|
785
801
|
if (EDIT_TOOLS.has(t)) return { kind: "edit", state: "CODING", message: `Editing${where || " files"}` };
|
|
786
|
-
if (READ_TOOLS.has(t)) return { kind: t
|
|
787
|
-
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")) {
|
|
788
804
|
if (command && TEST_COMMAND.test(command)) return { kind: "command", state: "TESTING", message: `Running tests: ${command}` };
|
|
789
805
|
if (command && REVIEW_COMMAND.test(command)) return { kind: "command", state: "REVIEWING", message: `Running checks: ${command}` };
|
|
790
806
|
return { kind: "command", state: "CODING", message: command ? `Running: ${command}` : "Running a shell command" };
|
|
791
807
|
}
|
|
792
808
|
if (t === "task" || t === "agent") return { kind: "subagent", state: "PLANNING", message: "Delegating to a subagent" };
|
|
793
|
-
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" };
|
|
794
810
|
return { kind: "tool", state: "CODING", message: `Using ${tool}` };
|
|
795
811
|
}
|
|
796
812
|
function normalizeHookEvent(agent, hookName, payload) {
|
|
@@ -799,22 +815,23 @@ function normalizeHookEvent(agent, hookName, payload) {
|
|
|
799
815
|
const session = asString(payload.session_id) ?? asString(payload.conversation_id) ?? null;
|
|
800
816
|
const toolInput = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : null;
|
|
801
817
|
const tool = asString(payload.tool_name) ?? asString(payload.tool) ?? null;
|
|
818
|
+
const isPatch = tool?.toLowerCase() === "apply_patch";
|
|
802
819
|
const rawCommand = asString(toolInput?.command) ?? asString(payload.command);
|
|
803
|
-
const command = rawCommand ? truncate(rawCommand) : null;
|
|
804
|
-
const files = filesFrom(toolInput, payload);
|
|
820
|
+
const command = rawCommand && !isPatch ? truncate(rawCommand) : null;
|
|
821
|
+
const files = isPatch && rawCommand ? patchFiles(rawCommand) : filesFrom(toolInput, payload);
|
|
805
822
|
let c2 = null;
|
|
806
823
|
if (h === "sessionstart" || h === "workspaceopen") c2 = { kind: "session-start", state: "IDLE", message: "Session started" };
|
|
807
824
|
else if (h === "sessionend") c2 = { kind: "session-end", state: "IDLE", message: "Session ended" };
|
|
808
|
-
else if (h === "userpromptsubmit" || h === "beforesubmitprompt") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
|
|
809
|
-
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" };
|
|
810
827
|
else if (h === "subagentstart") c2 = { kind: "subagent", state: "PLANNING", message: "Subagent started" };
|
|
811
828
|
else if (h === "subagentstop") c2 = { kind: "subagent", state: "CODING", message: "Subagent finished" };
|
|
812
829
|
else if (h === "beforeshellexecution") c2 = classifyTool("bash", command, files);
|
|
813
830
|
else if (h === "aftershellexecution") return null;
|
|
814
831
|
else if (h === "beforereadfile" || h === "beforetabfileread") c2 = classifyTool("read", null, files);
|
|
815
832
|
else if (h === "afterfileedit" || h === "aftertabfileedit") c2 = classifyTool("edit", null, files);
|
|
816
|
-
else if (h === "pretooluse" || h === "permissionrequest") c2 = tool ? classifyTool(tool, command, files) : null;
|
|
817
|
-
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;
|
|
818
835
|
else if (h === "pretoolusefailure") return null;
|
|
819
836
|
else c2 = { kind: "other", state: "CODING", message: `Hook: ${hook}` };
|
|
820
837
|
if (!c2) return null;
|
|
@@ -833,7 +850,10 @@ function normalizeHookEvent(agent, hookName, payload) {
|
|
|
833
850
|
};
|
|
834
851
|
}
|
|
835
852
|
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
|
|
853
|
+
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
|
|
836
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"];
|
|
837
857
|
|
|
838
858
|
// src/agents/claude-code/adapter.ts
|
|
839
859
|
var SETTINGS = ".claude/settings.json";
|
|
@@ -989,7 +1009,7 @@ var antigravityAdapter = {
|
|
|
989
1009
|
capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: true },
|
|
990
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.',
|
|
991
1011
|
async detectPresence(root) {
|
|
992
|
-
const evidence = await userEvidence(root, {
|
|
1012
|
+
const evidence = await userEvidence(root, { dirs: [".agents", ".agent"], athenaOwned: [RULE_FILE2] });
|
|
993
1013
|
return { detectedInProject: evidence.length > 0, evidence };
|
|
994
1014
|
},
|
|
995
1015
|
async plan(ctx) {
|
|
@@ -1011,6 +1031,450 @@ var antigravityAdapter = {
|
|
|
1011
1031
|
}
|
|
1012
1032
|
};
|
|
1013
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
|
+
|
|
1014
1478
|
// src/agents/agents-md/adapter.ts
|
|
1015
1479
|
var agentsMdAdapter = {
|
|
1016
1480
|
id: "agents-md",
|
|
@@ -1033,9 +1497,23 @@ var agentsMdAdapter = {
|
|
|
1033
1497
|
};
|
|
1034
1498
|
|
|
1035
1499
|
// src/agents/registry.ts
|
|
1036
|
-
var ADAPTERS = [claudeCodeAdapter, cursorAdapter, antigravityAdapter, agentsMdAdapter];
|
|
1500
|
+
var ADAPTERS = [claudeCodeAdapter, cursorAdapter, codexAdapter, copilotAdapter, geminiCliAdapter, antigravityAdapter, windsurfAdapter, clineAdapter, agentsMdAdapter];
|
|
1037
1501
|
function resolveAdapters(names2) {
|
|
1038
|
-
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
|
+
};
|
|
1039
1517
|
const out2 = [];
|
|
1040
1518
|
for (const raw of names2) {
|
|
1041
1519
|
const n = raw.trim().toLowerCase();
|
|
@@ -1053,11 +1531,11 @@ async function applyChanges(root, changes) {
|
|
|
1053
1531
|
if (c2.action === "unchanged") continue;
|
|
1054
1532
|
const abs = resolveInside(root, c2.path);
|
|
1055
1533
|
if (!abs) throw new Error(`Refusing to write outside the project: ${c2.path}`);
|
|
1056
|
-
const st = await
|
|
1534
|
+
const st = await fs10.lstat(abs).catch(() => null);
|
|
1057
1535
|
if (st?.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${c2.path}`);
|
|
1058
|
-
await
|
|
1536
|
+
await fs10.mkdir(path15.dirname(abs), { recursive: true });
|
|
1059
1537
|
await writeFileAtomic(abs, c2.content);
|
|
1060
|
-
if (abs.endsWith(".sh")) await
|
|
1538
|
+
if (abs.endsWith(".sh")) await fs10.chmod(abs, 493).catch(() => {
|
|
1061
1539
|
});
|
|
1062
1540
|
applied.push(c2);
|
|
1063
1541
|
}
|
|
@@ -1065,22 +1543,22 @@ async function applyChanges(root, changes) {
|
|
|
1065
1543
|
}
|
|
1066
1544
|
|
|
1067
1545
|
// src/services/pipeline.ts
|
|
1068
|
-
import { promises as
|
|
1069
|
-
import
|
|
1546
|
+
import { promises as fs16 } from "fs";
|
|
1547
|
+
import path23 from "path";
|
|
1070
1548
|
import crypto4 from "crypto";
|
|
1071
1549
|
|
|
1072
1550
|
// src/core/analyzer/analyze.ts
|
|
1073
|
-
import
|
|
1074
|
-
import { promises as
|
|
1551
|
+
import path20 from "path";
|
|
1552
|
+
import { promises as fs15 } from "fs";
|
|
1075
1553
|
|
|
1076
1554
|
// src/core/fs/walker.ts
|
|
1077
|
-
import { promises as
|
|
1078
|
-
import
|
|
1555
|
+
import { promises as fs12 } from "fs";
|
|
1556
|
+
import path17 from "path";
|
|
1079
1557
|
import crypto3 from "crypto";
|
|
1080
1558
|
|
|
1081
1559
|
// src/core/fs/ignore.ts
|
|
1082
|
-
import { promises as
|
|
1083
|
-
import
|
|
1560
|
+
import { promises as fs11 } from "fs";
|
|
1561
|
+
import path16 from "path";
|
|
1084
1562
|
import ignoreFactory from "ignore";
|
|
1085
1563
|
var IgnoreMatcher = class _IgnoreMatcher {
|
|
1086
1564
|
base;
|
|
@@ -1093,9 +1571,9 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1093
1571
|
}
|
|
1094
1572
|
static async load(root, config) {
|
|
1095
1573
|
const m = new _IgnoreMatcher(config);
|
|
1096
|
-
const exclude = await
|
|
1574
|
+
const exclude = await fs11.readFile(path16.join(root, ".git", "info", "exclude"), "utf8").catch(() => null);
|
|
1097
1575
|
if (exclude) m.setGitignore("", exclude);
|
|
1098
|
-
const rootGi = await
|
|
1576
|
+
const rootGi = await fs11.readFile(path16.join(root, ".gitignore"), "utf8").catch(() => null);
|
|
1099
1577
|
if (rootGi) m.setGitignore("", rootGi, true);
|
|
1100
1578
|
return m;
|
|
1101
1579
|
}
|
|
@@ -1108,16 +1586,16 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1108
1586
|
this.scoped.delete(dir);
|
|
1109
1587
|
}
|
|
1110
1588
|
/** Is the repo-relative POSIX path ignored? `isDir` enables directory-only patterns ("build/"). */
|
|
1111
|
-
ignores(
|
|
1112
|
-
if (!
|
|
1113
|
-
const test = isDir ? `${
|
|
1589
|
+
ignores(rel2, isDir = false) {
|
|
1590
|
+
if (!rel2 || rel2 === ".") return false;
|
|
1591
|
+
const test = isDir ? `${rel2}/` : rel2;
|
|
1114
1592
|
if (this.include?.ignores(test)) return false;
|
|
1115
1593
|
if (this.base.ignores(test)) return true;
|
|
1116
1594
|
for (const [dir, ig] of this.scoped) {
|
|
1117
1595
|
if (dir === "") {
|
|
1118
1596
|
if (ig.ignores(test)) return true;
|
|
1119
|
-
} else if (
|
|
1120
|
-
const sub =
|
|
1597
|
+
} else if (rel2.startsWith(`${dir}/`)) {
|
|
1598
|
+
const sub = rel2.slice(dir.length + 1);
|
|
1121
1599
|
if (ig.ignores(isDir ? `${sub}/` : sub)) return true;
|
|
1122
1600
|
}
|
|
1123
1601
|
}
|
|
@@ -1128,41 +1606,41 @@ var IgnoreMatcher = class _IgnoreMatcher {
|
|
|
1128
1606
|
// src/core/fs/walker.ts
|
|
1129
1607
|
var SNIFF_BYTES = 8e3;
|
|
1130
1608
|
async function walkProject(root, opts) {
|
|
1131
|
-
const rootReal = await
|
|
1609
|
+
const rootReal = await fs12.realpath(root);
|
|
1132
1610
|
const matcher = opts.matcher ?? await IgnoreMatcher.load(rootReal, opts.config);
|
|
1133
1611
|
const result = { files: [], skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0, truncated: false, warnings: [] };
|
|
1134
1612
|
const visitedDirs = /* @__PURE__ */ new Set([rootReal]);
|
|
1135
1613
|
const pending = [];
|
|
1136
|
-
const isIgnored = (
|
|
1614
|
+
const isIgnored = (rel2, isDir) => matcher.ignores(rel2, isDir);
|
|
1137
1615
|
const stack = [rootReal];
|
|
1138
1616
|
while (stack.length) {
|
|
1139
1617
|
opts.signal?.throwIfAborted();
|
|
1140
1618
|
const dir = stack.pop();
|
|
1141
1619
|
let entries;
|
|
1142
1620
|
try {
|
|
1143
|
-
entries = await
|
|
1621
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
1144
1622
|
} catch (err) {
|
|
1145
1623
|
result.skippedUnreadable++;
|
|
1146
|
-
result.warnings.push(`Cannot read directory ${toPosix(
|
|
1624
|
+
result.warnings.push(`Cannot read directory ${toPosix(path17.relative(rootReal, dir)) || "."}: ${err.code ?? "error"}`);
|
|
1147
1625
|
continue;
|
|
1148
1626
|
}
|
|
1149
1627
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1150
|
-
const dirRel = toPosix(
|
|
1628
|
+
const dirRel = toPosix(path17.relative(rootReal, dir));
|
|
1151
1629
|
if (dirRel && entries.some((e) => e.name === ".gitignore" && e.isFile())) {
|
|
1152
|
-
const gi = await
|
|
1630
|
+
const gi = await fs12.readFile(path17.join(dir, ".gitignore"), "utf8").catch(() => null);
|
|
1153
1631
|
if (gi !== null) matcher.setGitignore(dirRel, gi);
|
|
1154
1632
|
}
|
|
1155
1633
|
for (const ent of entries) {
|
|
1156
|
-
const abs =
|
|
1157
|
-
const
|
|
1634
|
+
const abs = path17.join(dir, ent.name);
|
|
1635
|
+
const rel2 = toPosix(path17.relative(rootReal, abs));
|
|
1158
1636
|
let isDir = ent.isDirectory();
|
|
1159
1637
|
let isFile = ent.isFile();
|
|
1160
1638
|
if (ent.isSymbolicLink()) {
|
|
1161
1639
|
try {
|
|
1162
|
-
const target = await
|
|
1163
|
-
const relTarget =
|
|
1164
|
-
if (relTarget.startsWith("..") ||
|
|
1165
|
-
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);
|
|
1166
1644
|
isDir = st.isDirectory();
|
|
1167
1645
|
isFile = st.isFile();
|
|
1168
1646
|
if (isDir) {
|
|
@@ -1173,18 +1651,18 @@ async function walkProject(root, opts) {
|
|
|
1173
1651
|
}
|
|
1174
1652
|
}
|
|
1175
1653
|
if (isDir) {
|
|
1176
|
-
if (isIgnored(
|
|
1177
|
-
const real = await
|
|
1654
|
+
if (isIgnored(rel2, true)) continue;
|
|
1655
|
+
const real = await fs12.realpath(abs).catch(() => abs);
|
|
1178
1656
|
if (visitedDirs.has(real)) continue;
|
|
1179
1657
|
visitedDirs.add(real);
|
|
1180
1658
|
stack.push(abs);
|
|
1181
1659
|
} else if (isFile) {
|
|
1182
|
-
if (isIgnored(
|
|
1660
|
+
if (isIgnored(rel2, false)) continue;
|
|
1183
1661
|
if (result.files.length + pending.length >= opts.config.maxFiles) {
|
|
1184
1662
|
result.truncated = true;
|
|
1185
1663
|
break;
|
|
1186
1664
|
}
|
|
1187
|
-
pending.push(
|
|
1665
|
+
pending.push(rel2);
|
|
1188
1666
|
}
|
|
1189
1667
|
}
|
|
1190
1668
|
if (result.truncated) break;
|
|
@@ -1198,9 +1676,9 @@ async function walkProject(root, opts) {
|
|
|
1198
1676
|
return result;
|
|
1199
1677
|
async function flush() {
|
|
1200
1678
|
const batch = pending.splice(0, pending.length);
|
|
1201
|
-
await mapLimit(batch, 32, async (
|
|
1679
|
+
await mapLimit(batch, 32, async (rel2) => {
|
|
1202
1680
|
opts.signal?.throwIfAborted();
|
|
1203
|
-
const entry = await indexFile(rootReal,
|
|
1681
|
+
const entry = await indexFile(rootReal, rel2, opts.config.maxFileBytes, opts.reuse?.[rel2]);
|
|
1204
1682
|
if (!entry) {
|
|
1205
1683
|
result.skippedUnreadable++;
|
|
1206
1684
|
return;
|
|
@@ -1212,21 +1690,21 @@ async function walkProject(root, opts) {
|
|
|
1212
1690
|
opts.onProgress?.(result.files.length);
|
|
1213
1691
|
}
|
|
1214
1692
|
}
|
|
1215
|
-
async function indexFile(root,
|
|
1216
|
-
const abs =
|
|
1693
|
+
async function indexFile(root, rel2, maxBytes, prev) {
|
|
1694
|
+
const abs = path17.join(root, rel2);
|
|
1217
1695
|
try {
|
|
1218
|
-
const st = await
|
|
1696
|
+
const st = await fs12.stat(abs);
|
|
1219
1697
|
if (prev && prev.s === st.size && prev.m === Math.floor(st.mtimeMs)) {
|
|
1220
|
-
return { path:
|
|
1698
|
+
return { path: rel2, size: st.size, mtimeMs: prev.m, hash: prev.h, binary: prev.b === 1, large: prev.h.startsWith("meta:") };
|
|
1221
1699
|
}
|
|
1222
1700
|
if (st.size > maxBytes) {
|
|
1223
|
-
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 };
|
|
1224
1702
|
}
|
|
1225
|
-
const buf = await
|
|
1703
|
+
const buf = await fs12.readFile(abs);
|
|
1226
1704
|
const sniff = buf.subarray(0, SNIFF_BYTES);
|
|
1227
1705
|
const binary = sniff.includes(0);
|
|
1228
1706
|
return {
|
|
1229
|
-
path:
|
|
1707
|
+
path: rel2,
|
|
1230
1708
|
size: st.size,
|
|
1231
1709
|
mtimeMs: Math.floor(st.mtimeMs),
|
|
1232
1710
|
hash: crypto3.createHash("sha256").update(buf).digest("hex"),
|
|
@@ -1454,8 +1932,8 @@ function emptyModel(name, root) {
|
|
|
1454
1932
|
}
|
|
1455
1933
|
|
|
1456
1934
|
// src/core/analyzer/context.ts
|
|
1457
|
-
import { promises as
|
|
1458
|
-
import
|
|
1935
|
+
import { promises as fs13 } from "fs";
|
|
1936
|
+
import path18 from "path";
|
|
1459
1937
|
function createContext(root, config, files, model, signal) {
|
|
1460
1938
|
const byPath = new Map(files.map((f) => [f.path, f]));
|
|
1461
1939
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -1465,19 +1943,19 @@ function createContext(root, config, files, model, signal) {
|
|
|
1465
1943
|
files,
|
|
1466
1944
|
model,
|
|
1467
1945
|
signal,
|
|
1468
|
-
has: (
|
|
1469
|
-
async read(
|
|
1470
|
-
if (cache.has(
|
|
1471
|
-
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);
|
|
1472
1950
|
if (!entry || entry.binary || entry.large) return null;
|
|
1473
1951
|
signal?.throwIfAborted();
|
|
1474
1952
|
let text = null;
|
|
1475
1953
|
try {
|
|
1476
|
-
text = await
|
|
1954
|
+
text = await fs13.readFile(path18.join(root, rel2), "utf8");
|
|
1477
1955
|
} catch {
|
|
1478
1956
|
text = null;
|
|
1479
1957
|
}
|
|
1480
|
-
if (!text || text.length < 256e3) cache.set(
|
|
1958
|
+
if (!text || text.length < 256e3) cache.set(rel2, text);
|
|
1481
1959
|
return text;
|
|
1482
1960
|
},
|
|
1483
1961
|
find(match) {
|
|
@@ -1577,7 +2055,7 @@ var structureDetector = {
|
|
|
1577
2055
|
};
|
|
1578
2056
|
|
|
1579
2057
|
// src/core/analyzer/detectors/manifests.ts
|
|
1580
|
-
import { parse as
|
|
2058
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
1581
2059
|
import { parse as parseYaml } from "yaml";
|
|
1582
2060
|
import ignoreFactory2 from "ignore";
|
|
1583
2061
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -1632,7 +2110,7 @@ async function parsePyproject(ctx, p) {
|
|
|
1632
2110
|
if (!text) return null;
|
|
1633
2111
|
let t;
|
|
1634
2112
|
try {
|
|
1635
|
-
t =
|
|
2113
|
+
t = parseToml2(text);
|
|
1636
2114
|
} catch {
|
|
1637
2115
|
ctx.warn(`Could not parse ${p} (invalid TOML)`);
|
|
1638
2116
|
return null;
|
|
@@ -1702,7 +2180,7 @@ async function parsePipfile(ctx, p) {
|
|
|
1702
2180
|
const text = await ctx.read(p);
|
|
1703
2181
|
if (!text) return null;
|
|
1704
2182
|
try {
|
|
1705
|
-
const t =
|
|
2183
|
+
const t = parseToml2(text);
|
|
1706
2184
|
return { manifest: { path: p, ecosystem: "python", dependencies: keys(t.packages).map((k) => k.toLowerCase()), devDependencies: keys(t["dev-packages"]).map((k) => k.toLowerCase()) } };
|
|
1707
2185
|
} catch {
|
|
1708
2186
|
ctx.warn(`Could not parse ${p}`);
|
|
@@ -1735,7 +2213,7 @@ async function parseCargo(ctx, p) {
|
|
|
1735
2213
|
if (!text) return null;
|
|
1736
2214
|
let t;
|
|
1737
2215
|
try {
|
|
1738
|
-
t =
|
|
2216
|
+
t = parseToml2(text);
|
|
1739
2217
|
} catch {
|
|
1740
2218
|
ctx.warn(`Could not parse ${p} (invalid TOML)`);
|
|
1741
2219
|
return null;
|
|
@@ -2461,8 +2939,8 @@ var dependenciesDetector = {
|
|
|
2461
2939
|
};
|
|
2462
2940
|
|
|
2463
2941
|
// src/core/analyzer/detectors/env.ts
|
|
2464
|
-
import { promises as
|
|
2465
|
-
import
|
|
2942
|
+
import { promises as fs14 } from "fs";
|
|
2943
|
+
import path19 from "path";
|
|
2466
2944
|
|
|
2467
2945
|
// src/core/security/secrets.ts
|
|
2468
2946
|
var SECRET_PATTERNS = [
|
|
@@ -2619,7 +3097,7 @@ var envDetector = {
|
|
|
2619
3097
|
for (const f of ctx.find(/(^|\/)\.env(\.[a-z.]+)?$/)) if (!TEMPLATE_ENV.test(f.path)) present.add(f.path);
|
|
2620
3098
|
for (const name of REAL_ENV_NAMES) {
|
|
2621
3099
|
try {
|
|
2622
|
-
const st = await
|
|
3100
|
+
const st = await fs14.stat(path19.join(ctx.root, name));
|
|
2623
3101
|
if (st.isFile()) present.add(name);
|
|
2624
3102
|
} catch {
|
|
2625
3103
|
}
|
|
@@ -2902,8 +3380,8 @@ async function prisma(ctx) {
|
|
|
2902
3380
|
if (!fm) continue;
|
|
2903
3381
|
const attributes = [...fm[3].matchAll(/@[\w.]+(\([^)]*\))?/g)].map((a) => a[0]);
|
|
2904
3382
|
fields.push({ name: fm[1], type: fm[2], attributes });
|
|
2905
|
-
const
|
|
2906
|
-
if (
|
|
3383
|
+
const rel2 = attributes.find((a) => a.startsWith("@relation"));
|
|
3384
|
+
if (rel2) relations.push(`${fm[1]} \u2192 ${fm[2].replace(/[[\]?]/g, "")}`);
|
|
2907
3385
|
}
|
|
2908
3386
|
const lineNo = text.slice(0, m.index).split("\n").length;
|
|
2909
3387
|
out2.push({ name: m[2], kind: "model", fields, indexes, relations, file: f.path, provenance: detected("code", [{ file: f.path, line: lineNo }]) });
|
|
@@ -3380,11 +3858,11 @@ async function gitInfo(cwd) {
|
|
|
3380
3858
|
const counts = /* @__PURE__ */ new Map();
|
|
3381
3859
|
for (const line2 of log.stdout.split("\n")) {
|
|
3382
3860
|
const p = line2.trim();
|
|
3383
|
-
if (!p) continue;
|
|
3861
|
+
if (!p || p === ".athena" || p.startsWith(".athena/")) continue;
|
|
3384
3862
|
const dir = p.includes("/") ? p.split("/").slice(0, 2).join("/") : ".";
|
|
3385
3863
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
3386
3864
|
}
|
|
3387
|
-
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 }));
|
|
3388
3866
|
}
|
|
3389
3867
|
return info2;
|
|
3390
3868
|
}
|
|
@@ -3442,8 +3920,8 @@ function isSafeRef(ref) {
|
|
|
3442
3920
|
async function headMovement(cwd, from, to, limit = 20) {
|
|
3443
3921
|
if (!/^[0-9a-f]{7,64}$/i.test(from) || !/^[0-9a-f]{7,64}$/i.test(to)) return null;
|
|
3444
3922
|
if (from === to) return { commits: [], diverged: false, truncated: false };
|
|
3445
|
-
const
|
|
3446
|
-
if (!
|
|
3923
|
+
const exists3 = await git(cwd, ["cat-file", "-e", `${from}^{commit}`]);
|
|
3924
|
+
if (!exists3.ok) return { commits: [], diverged: true, truncated: false };
|
|
3447
3925
|
const ancestor = await git(cwd, ["merge-base", "--is-ancestor", from, to]);
|
|
3448
3926
|
const log = await git(cwd, ["log", `-n${limit + 1}`, "--format=%H%x1f%s", `${from}..${to}`]);
|
|
3449
3927
|
if (!log.ok) return null;
|
|
@@ -3470,10 +3948,10 @@ var DETECTORS = [
|
|
|
3470
3948
|
];
|
|
3471
3949
|
async function analyzeProject(rootInput, opts = {}) {
|
|
3472
3950
|
const started = Date.now();
|
|
3473
|
-
const root = await
|
|
3951
|
+
const root = await fs15.realpath(path20.resolve(rootInput));
|
|
3474
3952
|
const loaded = opts.config ? { config: opts.config } : await loadConfig(root);
|
|
3475
3953
|
const config = loaded.config;
|
|
3476
|
-
const model = emptyModel(
|
|
3954
|
+
const model = emptyModel(path20.basename(root), root);
|
|
3477
3955
|
if ("warning" in loaded && loaded.warning) model.warnings.push(loaded.warning);
|
|
3478
3956
|
opts.onStage?.("scan");
|
|
3479
3957
|
const walk2 = await walkProject(root, { config, signal: opts.signal, reuse: opts.reuse });
|
|
@@ -3523,7 +4001,7 @@ function redactDeep(value) {
|
|
|
3523
4001
|
}
|
|
3524
4002
|
|
|
3525
4003
|
// src/core/knowledge/generate.ts
|
|
3526
|
-
import
|
|
4004
|
+
import path22 from "path";
|
|
3527
4005
|
|
|
3528
4006
|
// src/core/knowledge/md.ts
|
|
3529
4007
|
function esc(s) {
|
|
@@ -4134,7 +4612,7 @@ function seedRules(m) {
|
|
|
4134
4612
|
|
|
4135
4613
|
// src/core/model/security-scan.ts
|
|
4136
4614
|
import { z as z5 } from "zod";
|
|
4137
|
-
import
|
|
4615
|
+
import path21 from "path";
|
|
4138
4616
|
var SCAN_FILE = "security-scan.json";
|
|
4139
4617
|
var Severity = z5.enum(["critical", "high", "moderate", "low", "unknown"]);
|
|
4140
4618
|
var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
|
|
@@ -4166,11 +4644,11 @@ var SecurityScan = z5.object({
|
|
|
4166
4644
|
});
|
|
4167
4645
|
var loadScan = (root) => loadScanFromDir(athenaDir(root));
|
|
4168
4646
|
async function saveScan(root, scan) {
|
|
4169
|
-
await writeFileAtomic(
|
|
4647
|
+
await writeFileAtomic(path21.join(athenaDir(root), SCAN_FILE), `${JSON.stringify(scan, null, 2)}
|
|
4170
4648
|
`);
|
|
4171
4649
|
}
|
|
4172
4650
|
async function loadScanFromDir(dir) {
|
|
4173
|
-
const raw = await readTextIfExists(
|
|
4651
|
+
const raw = await readTextIfExists(path21.join(dir, SCAN_FILE));
|
|
4174
4652
|
if (!raw) return null;
|
|
4175
4653
|
try {
|
|
4176
4654
|
const parsed = SecurityScan.safeParse(JSON.parse(raw));
|
|
@@ -4233,7 +4711,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4233
4711
|
for (const id of ids) {
|
|
4234
4712
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
4235
4713
|
const sections = renderSections(id, model, extras);
|
|
4236
|
-
const before = await readTextIfExists(
|
|
4714
|
+
const before = await readTextIfExists(path22.join(athenaDir2, doc.file));
|
|
4237
4715
|
if (before === null) {
|
|
4238
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: [] });
|
|
4239
4717
|
continue;
|
|
@@ -4255,7 +4733,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4255
4733
|
async function writePlannedKnowledge(athenaDir2, plans) {
|
|
4256
4734
|
const reports = [];
|
|
4257
4735
|
for (const p of plans) {
|
|
4258
|
-
if (p.status !== "unchanged") await writeFileAtomic(
|
|
4736
|
+
if (p.status !== "unchanged") await writeFileAtomic(path22.join(athenaDir2, p.file), p.after);
|
|
4259
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) });
|
|
4260
4738
|
}
|
|
4261
4739
|
return reports;
|
|
@@ -4264,7 +4742,7 @@ async function writeKnowledge(athenaDir2, model, opts = {}) {
|
|
|
4264
4742
|
return writePlannedKnowledge(athenaDir2, await planKnowledge(athenaDir2, model, opts));
|
|
4265
4743
|
}
|
|
4266
4744
|
async function ensureRules(athenaDir2, model) {
|
|
4267
|
-
const file =
|
|
4745
|
+
const file = path22.join(athenaDir2, "rules.md");
|
|
4268
4746
|
const existing = await readTextIfExists(file);
|
|
4269
4747
|
if (existing !== null) return { id: "rules", file: "rules.md", status: "unchanged", blocks: [], preservedModified: [], contentHash: sha256(existing).slice(0, 16) };
|
|
4270
4748
|
const text = seedRules(model);
|
|
@@ -4303,11 +4781,11 @@ ${missing.join("\n")}
|
|
|
4303
4781
|
}
|
|
4304
4782
|
async function indexWithIntegrations(root, files, written) {
|
|
4305
4783
|
const index = buildFileIndex(files);
|
|
4306
|
-
for (const
|
|
4784
|
+
for (const rel2 of written) {
|
|
4307
4785
|
try {
|
|
4308
|
-
const abs =
|
|
4309
|
-
const [st, buf] = await Promise.all([
|
|
4310
|
-
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) };
|
|
4311
4789
|
} catch {
|
|
4312
4790
|
}
|
|
4313
4791
|
}
|
|
@@ -4357,7 +4835,7 @@ async function runPipeline(opts) {
|
|
|
4357
4835
|
const prev = await readState(finalDir);
|
|
4358
4836
|
if (prev.kind === "corrupted" && !opts.dryRun) {
|
|
4359
4837
|
const backup = await backupCorruptedState(finalDir);
|
|
4360
|
-
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.`);
|
|
4361
4839
|
}
|
|
4362
4840
|
const prevState = prev.kind === "ok" ? prev.state : null;
|
|
4363
4841
|
opts.onStage?.("analyze");
|
|
@@ -4370,21 +4848,21 @@ async function runPipeline(opts) {
|
|
|
4370
4848
|
return { analysis, docs: [], agentChanges: agentChanges2, state: null, notes };
|
|
4371
4849
|
}
|
|
4372
4850
|
const fresh = !await exists(finalDir);
|
|
4373
|
-
const workDir = fresh ?
|
|
4851
|
+
const workDir = fresh ? path23.join(root, `${ATHENA_DIR}.tmp-${crypto4.randomBytes(4).toString("hex")}`) : finalDir;
|
|
4374
4852
|
let docs;
|
|
4375
4853
|
try {
|
|
4376
4854
|
opts.onStage?.("write");
|
|
4377
|
-
await
|
|
4855
|
+
await fs16.mkdir(workDir, { recursive: true });
|
|
4378
4856
|
const previousBlocks = prevState ? Object.fromEntries(Object.entries(prevState.documents).map(([k, v]) => [k, v.blocks])) : void 0;
|
|
4379
4857
|
docs = await writeKnowledge(workDir, analysis.model, { force: opts.force, previousBlocks });
|
|
4380
4858
|
docs.push(await ensureRules(workDir, analysis.model));
|
|
4381
|
-
await writeFileAtomic(
|
|
4859
|
+
await writeFileAtomic(path23.join(workDir, "model.json"), `${JSON.stringify(analysis.model, null, 2)}
|
|
4382
4860
|
`);
|
|
4383
|
-
await ensureGitignoreEntries(
|
|
4861
|
+
await ensureGitignoreEntries(path23.join(workDir, ".gitignore"));
|
|
4384
4862
|
opts.signal?.throwIfAborted();
|
|
4385
|
-
if (fresh) await
|
|
4863
|
+
if (fresh) await fs16.rename(workDir, finalDir);
|
|
4386
4864
|
} catch (err) {
|
|
4387
|
-
if (fresh) await
|
|
4865
|
+
if (fresh) await fs16.rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
4388
4866
|
});
|
|
4389
4867
|
throw err;
|
|
4390
4868
|
}
|
|
@@ -4413,21 +4891,21 @@ async function runPipeline(opts) {
|
|
|
4413
4891
|
}
|
|
4414
4892
|
|
|
4415
4893
|
// src/cli/context.ts
|
|
4416
|
-
import { promises as
|
|
4417
|
-
import
|
|
4894
|
+
import { promises as fs18 } from "fs";
|
|
4895
|
+
import path25 from "path";
|
|
4418
4896
|
|
|
4419
4897
|
// src/services/project.ts
|
|
4420
|
-
import { promises as
|
|
4421
|
-
import
|
|
4898
|
+
import { promises as fs17 } from "fs";
|
|
4899
|
+
import path24 from "path";
|
|
4422
4900
|
async function findProjectRoot(start) {
|
|
4423
4901
|
let dir = start;
|
|
4424
4902
|
for (; ; ) {
|
|
4425
4903
|
try {
|
|
4426
|
-
const st = await
|
|
4904
|
+
const st = await fs17.stat(path24.join(dir, ATHENA_DIR));
|
|
4427
4905
|
if (st.isDirectory()) return dir;
|
|
4428
4906
|
} catch {
|
|
4429
4907
|
}
|
|
4430
|
-
const parent =
|
|
4908
|
+
const parent = path24.dirname(dir);
|
|
4431
4909
|
if (parent === dir) return null;
|
|
4432
4910
|
dir = parent;
|
|
4433
4911
|
}
|
|
@@ -4435,11 +4913,11 @@ async function findProjectRoot(start) {
|
|
|
4435
4913
|
|
|
4436
4914
|
// src/cli/context.ts
|
|
4437
4915
|
async function resolveCwd(opts) {
|
|
4438
|
-
const dir =
|
|
4916
|
+
const dir = path25.resolve(opts.cwd ?? process.cwd());
|
|
4439
4917
|
try {
|
|
4440
|
-
const st = await
|
|
4918
|
+
const st = await fs18.stat(dir);
|
|
4441
4919
|
if (!st.isDirectory()) throw new AthenaError(`Not a directory: ${dir}`);
|
|
4442
|
-
return await
|
|
4920
|
+
return await fs18.realpath(dir);
|
|
4443
4921
|
} catch (err) {
|
|
4444
4922
|
if (err instanceof AthenaError) throw err;
|
|
4445
4923
|
throw new AthenaError(`Directory not accessible: ${dir}`, "Check the path and your permissions.");
|
|
@@ -4611,6 +5089,19 @@ function wrapCode(text, w) {
|
|
|
4611
5089
|
}).join("")
|
|
4612
5090
|
);
|
|
4613
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
|
+
}
|
|
4614
5105
|
function numbered(steps) {
|
|
4615
5106
|
return (w) => steps.flatMap((s, i) => {
|
|
4616
5107
|
const badge = paint(PALETTE.green, ` ${i + 1} `, true);
|
|
@@ -4737,9 +5228,9 @@ function logoBlock(w) {
|
|
|
4737
5228
|
}
|
|
4738
5229
|
function linksBlock() {
|
|
4739
5230
|
return [
|
|
4740
|
-
pc4.italic('"Turn any codebase'),
|
|
4741
|
-
pc4.italic(' into clear knowledge."'),
|
|
5231
|
+
pc4.italic('"Turn any codebase into clear knowledge."'),
|
|
4742
5232
|
"",
|
|
5233
|
+
`${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
|
|
4743
5234
|
`${paint(PALETTE.pink, "npm", true)} ${pc4.dim(`v${ATHENA_VERSION}`)}`,
|
|
4744
5235
|
paint(PALETTE.blue, NPM_URL),
|
|
4745
5236
|
paint(PALETTE.violet, "github", true),
|
|
@@ -4772,23 +5263,27 @@ function header() {
|
|
|
4772
5263
|
return [...box(content, Math.min(total, fit.w + 4), { color: PALETTE.blue }), ""];
|
|
4773
5264
|
}
|
|
4774
5265
|
function compactLinks() {
|
|
4775
|
-
return [
|
|
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
|
+
];
|
|
4776
5271
|
}
|
|
4777
5272
|
function footer(title, subtitle, tone = "success") {
|
|
4778
5273
|
const total = termWidth();
|
|
4779
5274
|
const color = tone === "success" ? PALETTE.green : PALETTE.amber;
|
|
4780
5275
|
const mark = tone === "success" ? "\u2713" : "!";
|
|
4781
|
-
const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}
|
|
5276
|
+
const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}`, ""];
|
|
4782
5277
|
const links = compactLinks();
|
|
4783
|
-
const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}
|
|
5278
|
+
const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}`, ""];
|
|
4784
5279
|
const statusW = Math.max(...status2.map(width));
|
|
4785
5280
|
const linksW = 8 + LINKS_WIDTH;
|
|
4786
5281
|
const wide = statusW + 4 + linksW + 3 + 1 + 3 + 14 + 4;
|
|
4787
|
-
const content = wide <= total ? columns([status2, links, divider2(), credit], [statusW, linksW, 1, 14], 3) : total >= linksW + 4 ? [...status2, "", ...links, credit.join(" ")] : [...status2, pc4.dim(REPO_URL)];
|
|
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)];
|
|
4788
5283
|
const w = wide <= total ? wide : Math.min(total, Math.max(statusW, linksW) + 4);
|
|
4789
5284
|
return box(content, w, { color });
|
|
4790
5285
|
}
|
|
4791
|
-
var divider2 = () => [pc4.dim(VR), pc4.dim(VR)];
|
|
5286
|
+
var divider2 = () => [pc4.dim(VR), pc4.dim(VR), pc4.dim(VR)];
|
|
4792
5287
|
|
|
4793
5288
|
// src/cli/ui/stages.ts
|
|
4794
5289
|
import pc5 from "picocolors";
|
|
@@ -4913,7 +5408,7 @@ function parseAgentsOption(value, disabled) {
|
|
|
4913
5408
|
}
|
|
4914
5409
|
async function initCommand(opts) {
|
|
4915
5410
|
const root = await resolveCwd(opts);
|
|
4916
|
-
const existing = await readState(
|
|
5411
|
+
const existing = await readState(path26.join(root, ".athena"));
|
|
4917
5412
|
if (existing.kind === "ok") {
|
|
4918
5413
|
throw new AthenaError("Athena is already initialized in this project.", "Run `athena analyze` to refresh knowledge, or `athena clean` to start over.");
|
|
4919
5414
|
}
|
|
@@ -5018,7 +5513,7 @@ function printSummary(result, dryRun, mode) {
|
|
|
5018
5513
|
"Run `athena open` to browse and edit in the web UI",
|
|
5019
5514
|
configured ? "Your agents now read `.athena/` \u2014 watch with `athena activity`" : "Connect an agent: `athena agents add claude-code`",
|
|
5020
5515
|
"After code changes run `athena sync` to keep docs current",
|
|
5021
|
-
"
|
|
5516
|
+
"Guides and docs: https://athena.sabari.me \xB7 star us on GitHub \u2605"
|
|
5022
5517
|
] : ["Review changes with `git diff .athena/`", "Run `athena open` to browse in the web UI", "Use `athena sync --watch` to stay current"];
|
|
5023
5518
|
for (const l of panels([
|
|
5024
5519
|
{ title: "Project Overview", icon: "\u25C6", color: C.blue, body: rows(overview) },
|
|
@@ -5037,7 +5532,8 @@ function printSummary(result, dryRun, mode) {
|
|
|
5037
5532
|
}),
|
|
5038
5533
|
...mode === "init" && !dryRun ? (() => {
|
|
5039
5534
|
const unconfigured = ADAPTERS.filter((a) => !result.agentChanges.some((c2) => c2.adapter.id === a.id));
|
|
5040
|
-
|
|
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)))] : [];
|
|
5041
5537
|
})() : [],
|
|
5042
5538
|
...dryRun ? result.agentChanges.flatMap(({ adapter, changes }) => changes.map((ch) => () => bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`))) : [],
|
|
5043
5539
|
...result.notes.map((n) => () => warn(n))
|
|
@@ -5084,7 +5580,7 @@ async function analyzeCommand(opts) {
|
|
|
5084
5580
|
}
|
|
5085
5581
|
|
|
5086
5582
|
// src/services/status.ts
|
|
5087
|
-
import
|
|
5583
|
+
import path27 from "path";
|
|
5088
5584
|
|
|
5089
5585
|
// src/core/impact/impact.ts
|
|
5090
5586
|
var RULES = [
|
|
@@ -5128,7 +5624,7 @@ async function buildStatus(root, signal) {
|
|
|
5128
5624
|
const state = st.state;
|
|
5129
5625
|
const documents = [];
|
|
5130
5626
|
for (const d of KNOWLEDGE_DOCS) {
|
|
5131
|
-
const text = await readTextIfExists(
|
|
5627
|
+
const text = await readTextIfExists(path27.join(dir, d.file));
|
|
5132
5628
|
documents.push({
|
|
5133
5629
|
file: d.file,
|
|
5134
5630
|
title: d.title,
|
|
@@ -5207,8 +5703,8 @@ async function statusCommand(opts) {
|
|
|
5207
5703
|
}
|
|
5208
5704
|
|
|
5209
5705
|
// src/services/doctor.ts
|
|
5210
|
-
import { constants, promises as
|
|
5211
|
-
import
|
|
5706
|
+
import { constants, promises as fs22 } from "fs";
|
|
5707
|
+
import path31 from "path";
|
|
5212
5708
|
|
|
5213
5709
|
// src/core/knowledge/rules.ts
|
|
5214
5710
|
var RULE_RE = /^([-*+])\s+(?:\[(disabled)\]\s+)?(.+?)\s*$/;
|
|
@@ -5302,11 +5798,11 @@ function relativeTime2(iso, now = Date.now()) {
|
|
|
5302
5798
|
}
|
|
5303
5799
|
|
|
5304
5800
|
// src/services/instance.ts
|
|
5305
|
-
import { promises as
|
|
5306
|
-
import
|
|
5801
|
+
import { promises as fs19 } from "fs";
|
|
5802
|
+
import path28 from "path";
|
|
5307
5803
|
var INSTANCE_FILE = ".server.json";
|
|
5308
5804
|
async function findRunningInstance(root) {
|
|
5309
|
-
const file =
|
|
5805
|
+
const file = path28.join(athenaDir(root), INSTANCE_FILE);
|
|
5310
5806
|
const raw = await readTextIfExists(file);
|
|
5311
5807
|
if (!raw) return null;
|
|
5312
5808
|
let info2;
|
|
@@ -5319,7 +5815,7 @@ async function findRunningInstance(root) {
|
|
|
5319
5815
|
try {
|
|
5320
5816
|
process.kill(info2.pid, 0);
|
|
5321
5817
|
} catch {
|
|
5322
|
-
await
|
|
5818
|
+
await fs19.rm(file, { force: true }).catch(() => {
|
|
5323
5819
|
});
|
|
5324
5820
|
return null;
|
|
5325
5821
|
}
|
|
@@ -5334,19 +5830,19 @@ async function findRunningInstance(root) {
|
|
|
5334
5830
|
}
|
|
5335
5831
|
|
|
5336
5832
|
// src/services/which.ts
|
|
5337
|
-
import { promises as
|
|
5338
|
-
import
|
|
5833
|
+
import { promises as fs20 } from "fs";
|
|
5834
|
+
import path29 from "path";
|
|
5339
5835
|
async function whichExecutable(command) {
|
|
5340
5836
|
if (command.includes("/") || command.includes("\\")) {
|
|
5341
|
-
return await
|
|
5837
|
+
return await fs20.access(command).then(() => true).catch(() => false) ? command : null;
|
|
5342
5838
|
}
|
|
5343
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
5839
|
+
const dirs = (process.env.PATH ?? "").split(path29.delimiter).filter(Boolean);
|
|
5344
5840
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
5345
5841
|
for (const dir of dirs) {
|
|
5346
5842
|
for (const ext of exts) {
|
|
5347
|
-
const candidate =
|
|
5843
|
+
const candidate = path29.join(dir, command + ext.toLowerCase());
|
|
5348
5844
|
try {
|
|
5349
|
-
const st = await
|
|
5845
|
+
const st = await fs20.stat(candidate);
|
|
5350
5846
|
if (st.isFile()) return candidate;
|
|
5351
5847
|
} catch {
|
|
5352
5848
|
}
|
|
@@ -5356,41 +5852,41 @@ async function whichExecutable(command) {
|
|
|
5356
5852
|
}
|
|
5357
5853
|
|
|
5358
5854
|
// src/services/agent-activity.ts
|
|
5359
|
-
import { promises as
|
|
5360
|
-
import
|
|
5855
|
+
import { promises as fs21 } from "fs";
|
|
5856
|
+
import path30 from "path";
|
|
5361
5857
|
var ACTIVITY_FILE = ".agent-events.jsonl";
|
|
5362
5858
|
var MAX_LINES = 2e3;
|
|
5363
5859
|
var MAX_BYTES = 512 * 1024;
|
|
5364
5860
|
function activityFile(root) {
|
|
5365
|
-
return
|
|
5861
|
+
return path30.join(athenaDir(root), ACTIVITY_FILE);
|
|
5366
5862
|
}
|
|
5367
5863
|
function sanitizeEvent(root, event) {
|
|
5368
|
-
const
|
|
5369
|
-
const abs =
|
|
5370
|
-
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));
|
|
5371
5867
|
return !r || r.startsWith("..") ? toPosix(p) : r;
|
|
5372
5868
|
};
|
|
5373
|
-
const stripRoot = (text) => text.split(`${root}${
|
|
5869
|
+
const stripRoot = (text) => text.split(`${root}${path30.sep}`).join("").split(toPosix(root) + "/").join("");
|
|
5374
5870
|
return {
|
|
5375
5871
|
...event,
|
|
5376
5872
|
message: redact(stripRoot(event.message)),
|
|
5377
5873
|
command: event.command ? redact(stripRoot(event.command)) : null,
|
|
5378
|
-
files: event.files.map(
|
|
5874
|
+
files: event.files.map(rel2)
|
|
5379
5875
|
};
|
|
5380
5876
|
}
|
|
5381
5877
|
async function appendEvent(root, event) {
|
|
5382
5878
|
const file = activityFile(root);
|
|
5383
|
-
await
|
|
5384
|
-
await
|
|
5879
|
+
await fs21.mkdir(path30.dirname(file), { recursive: true });
|
|
5880
|
+
await fs21.appendFile(file, `${JSON.stringify(sanitizeEvent(root, event))}
|
|
5385
5881
|
`);
|
|
5386
5882
|
await rotateIfNeeded(file);
|
|
5387
5883
|
}
|
|
5388
5884
|
async function rotateIfNeeded(file) {
|
|
5389
5885
|
try {
|
|
5390
|
-
const st = await
|
|
5886
|
+
const st = await fs21.stat(file);
|
|
5391
5887
|
if (st.size < MAX_BYTES) return;
|
|
5392
|
-
const lines = (await
|
|
5393
|
-
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")}
|
|
5394
5890
|
`);
|
|
5395
5891
|
} catch {
|
|
5396
5892
|
}
|
|
@@ -5409,7 +5905,7 @@ function parseEventLines(text) {
|
|
|
5409
5905
|
}
|
|
5410
5906
|
async function readRecentEvents(root, limit = 200) {
|
|
5411
5907
|
try {
|
|
5412
|
-
const text = await
|
|
5908
|
+
const text = await fs21.readFile(activityFile(root), "utf8");
|
|
5413
5909
|
return parseEventLines(text).slice(-limit);
|
|
5414
5910
|
} catch {
|
|
5415
5911
|
return [];
|
|
@@ -5446,7 +5942,7 @@ async function runDoctor(cwd) {
|
|
|
5446
5942
|
add({ area: "Project", level: "ok", message: root });
|
|
5447
5943
|
const dir = athenaDir(root);
|
|
5448
5944
|
try {
|
|
5449
|
-
await
|
|
5945
|
+
await fs22.access(dir, constants.R_OK | constants.W_OK);
|
|
5450
5946
|
add({ area: "Knowledge directory", level: "ok", message: ".athena/ is readable and writable" });
|
|
5451
5947
|
} catch {
|
|
5452
5948
|
add({ area: "Knowledge directory", level: "error", message: ".athena/ is not writable", hint: "Check file permissions." });
|
|
@@ -5460,13 +5956,13 @@ async function runDoctor(cwd) {
|
|
|
5460
5956
|
const missing = [];
|
|
5461
5957
|
const malformed = [];
|
|
5462
5958
|
for (const d of KNOWLEDGE_DOCS) {
|
|
5463
|
-
const text = await readTextIfExists(
|
|
5959
|
+
const text = await readTextIfExists(path31.join(dir, d.file));
|
|
5464
5960
|
if (text === null) missing.push(d.file);
|
|
5465
5961
|
else if (d.id !== "rules" && (text.match(/athena:generated:start/g)?.length ?? 0) !== parseBlocks(text).length) malformed.push(d.file);
|
|
5466
5962
|
}
|
|
5467
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 });
|
|
5468
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." });
|
|
5469
|
-
const rulesText = await readTextIfExists(
|
|
5965
|
+
const rulesText = await readTextIfExists(path31.join(dir, "rules.md"));
|
|
5470
5966
|
if (rulesText !== null) {
|
|
5471
5967
|
const rules = listRules(parseRules(rulesText));
|
|
5472
5968
|
const enabled = rules.filter((r) => r.enabled).length;
|
|
@@ -5525,10 +6021,10 @@ async function doctorCommand(opts) {
|
|
|
5525
6021
|
}
|
|
5526
6022
|
|
|
5527
6023
|
// src/services/rules.ts
|
|
5528
|
-
import
|
|
6024
|
+
import path33 from "path";
|
|
5529
6025
|
|
|
5530
6026
|
// src/services/knowledge.ts
|
|
5531
|
-
import
|
|
6027
|
+
import path32 from "path";
|
|
5532
6028
|
var MAX_DOC_BYTES = 1e6;
|
|
5533
6029
|
function resolveDoc(id) {
|
|
5534
6030
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
@@ -5572,12 +6068,12 @@ async function meta(root, id, text) {
|
|
|
5572
6068
|
}
|
|
5573
6069
|
async function listDocs(root) {
|
|
5574
6070
|
const out2 = [];
|
|
5575
|
-
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))));
|
|
5576
6072
|
return out2;
|
|
5577
6073
|
}
|
|
5578
6074
|
async function readDoc(root, id) {
|
|
5579
6075
|
const doc = resolveDoc(id);
|
|
5580
|
-
const text = await readTextIfExists(
|
|
6076
|
+
const text = await readTextIfExists(path32.join(athenaDir(root), doc.file));
|
|
5581
6077
|
if (text === null) throw notFound(`${doc.file} does not exist. Run \`athena analyze\` to regenerate it.`);
|
|
5582
6078
|
return { ...await meta(root, doc.id, text), content: text, segments: segmentDocument(text) };
|
|
5583
6079
|
}
|
|
@@ -5585,7 +6081,7 @@ async function saveDoc(root, id, content, baseHash) {
|
|
|
5585
6081
|
const doc = resolveDoc(id);
|
|
5586
6082
|
if (typeof content !== "string") throw new AthenaError("content must be a string");
|
|
5587
6083
|
if (Buffer.byteLength(content) > MAX_DOC_BYTES) throw new AthenaError(`Document exceeds ${MAX_DOC_BYTES} bytes`, void 0, 1, "unprocessable");
|
|
5588
|
-
const file =
|
|
6084
|
+
const file = path32.join(athenaDir(root), doc.file);
|
|
5589
6085
|
const current = await readTextIfExists(file);
|
|
5590
6086
|
const currentHash = current === null ? null : contentHash(current);
|
|
5591
6087
|
if (currentHash !== baseHash) {
|
|
@@ -5611,7 +6107,7 @@ async function searchDocs(root, query, limit = 50) {
|
|
|
5611
6107
|
if (q.length < 2) return [];
|
|
5612
6108
|
const hits = [];
|
|
5613
6109
|
for (const d of KNOWLEDGE_DOCS) {
|
|
5614
|
-
const text = await readTextIfExists(
|
|
6110
|
+
const text = await readTextIfExists(path32.join(athenaDir(root), d.file));
|
|
5615
6111
|
if (!text) continue;
|
|
5616
6112
|
const lines = text.split("\n");
|
|
5617
6113
|
for (let i = 0; i < lines.length && hits.length < limit; i++) {
|
|
@@ -5625,14 +6121,14 @@ async function searchDocs(root, query, limit = 50) {
|
|
|
5625
6121
|
}
|
|
5626
6122
|
async function docHistory(root, id, limit = 50) {
|
|
5627
6123
|
const doc = resolveDoc(id);
|
|
5628
|
-
const
|
|
5629
|
-
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]);
|
|
5630
6126
|
if (!r.ok) return { available: false, reason: "Git history is not available (not a Git repository, or git is not installed).", entries: [] };
|
|
5631
6127
|
const entries = r.stdout.split("\n").filter(Boolean).map((l) => {
|
|
5632
6128
|
const [sha, date, subject] = l.split("");
|
|
5633
6129
|
return { sha, date, subject: subject ?? "" };
|
|
5634
6130
|
});
|
|
5635
|
-
if (!entries.length) return { available: true, reason: `${
|
|
6131
|
+
if (!entries.length) return { available: true, reason: `${rel2} has not been committed yet.`, entries };
|
|
5636
6132
|
return { available: true, entries };
|
|
5637
6133
|
}
|
|
5638
6134
|
async function docAtRevision(root, id, sha) {
|
|
@@ -5646,7 +6142,7 @@ async function docAtRevision(root, id, sha) {
|
|
|
5646
6142
|
|
|
5647
6143
|
// src/services/rules.ts
|
|
5648
6144
|
async function load(root) {
|
|
5649
|
-
const file =
|
|
6145
|
+
const file = path33.join(athenaDir(root), "rules.md");
|
|
5650
6146
|
const text = await readTextIfExists(file);
|
|
5651
6147
|
if (text === null) throw notFound(".athena/rules.md not found. Run `athena analyze` to recreate it.");
|
|
5652
6148
|
return { file, text, doc: parseRules(text) };
|
|
@@ -5724,7 +6220,7 @@ var rulesEditCommand = (n, text, opts) => mutate(opts, (d) => updateRule(d, pars
|
|
|
5724
6220
|
var rulesRemoveCommand = (n, opts) => mutate(opts, (d) => removeRule(d, parseIndex(n)), `Rule ${n} removed.`);
|
|
5725
6221
|
|
|
5726
6222
|
// src/cli/commands/clean.ts
|
|
5727
|
-
import { promises as
|
|
6223
|
+
import { promises as fs23 } from "fs";
|
|
5728
6224
|
import { createInterface } from "readline/promises";
|
|
5729
6225
|
async function confirm(question) {
|
|
5730
6226
|
if (!process.stdin.isTTY) return false;
|
|
@@ -5749,7 +6245,7 @@ async function cleanCommand(opts) {
|
|
|
5749
6245
|
}
|
|
5750
6246
|
const touched = [];
|
|
5751
6247
|
if (!opts.keepAgents) for (const a of ADAPTERS) touched.push(...await a.remove(root));
|
|
5752
|
-
await
|
|
6248
|
+
await fs23.rm(athenaDir(root), { recursive: true, force: true });
|
|
5753
6249
|
if (isJson()) json({ ok: true, removed: [".athena/", ...touched] });
|
|
5754
6250
|
else {
|
|
5755
6251
|
ok("Removed .athena/");
|
|
@@ -5761,14 +6257,14 @@ async function cleanCommand(opts) {
|
|
|
5761
6257
|
import { execFile as execFile3 } from "child_process";
|
|
5762
6258
|
|
|
5763
6259
|
// src/server/instance.ts
|
|
5764
|
-
import { promises as
|
|
5765
|
-
import
|
|
6260
|
+
import { promises as fs28 } from "fs";
|
|
6261
|
+
import path43 from "path";
|
|
5766
6262
|
import { fileURLToPath } from "url";
|
|
5767
6263
|
|
|
5768
6264
|
// src/server/app.ts
|
|
5769
|
-
import { promises as
|
|
6265
|
+
import { promises as fs27, watch } from "fs";
|
|
5770
6266
|
import crypto7 from "crypto";
|
|
5771
|
-
import
|
|
6267
|
+
import path42 from "path";
|
|
5772
6268
|
import Fastify from "fastify";
|
|
5773
6269
|
|
|
5774
6270
|
// src/services/agents.ts
|
|
@@ -5824,23 +6320,32 @@ async function configureAgents(root, names2) {
|
|
|
5824
6320
|
async function removeAgents(root, names2) {
|
|
5825
6321
|
const st = await readState(athenaDir(root));
|
|
5826
6322
|
const removed = [];
|
|
5827
|
-
|
|
6323
|
+
const targets = resolve(names2);
|
|
6324
|
+
for (const a of targets) {
|
|
5828
6325
|
removed.push(...await a.remove(root));
|
|
5829
6326
|
if (st.kind === "ok") delete st.state.agents[a.id];
|
|
5830
6327
|
}
|
|
5831
|
-
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
|
+
}
|
|
5832
6337
|
return removed;
|
|
5833
6338
|
}
|
|
5834
6339
|
|
|
5835
6340
|
// src/services/overview.ts
|
|
5836
|
-
import
|
|
6341
|
+
import path34 from "path";
|
|
5837
6342
|
async function getOverview(root) {
|
|
5838
6343
|
const st = await readState(athenaDir(root));
|
|
5839
6344
|
if (st.kind === "missing") throw new AthenaError("Athena is not initialized in this project.", "Run `athena init`.", 3);
|
|
5840
6345
|
if (st.kind === "corrupted") throw new AthenaError(`.athena/state.json is corrupted (${st.reason}).`, "Run `athena analyze` to rebuild it.");
|
|
5841
6346
|
const state = st.state;
|
|
5842
6347
|
let model = null;
|
|
5843
|
-
const raw = await readTextIfExists(
|
|
6348
|
+
const raw = await readTextIfExists(path34.join(athenaDir(root), "model.json"));
|
|
5844
6349
|
if (raw) {
|
|
5845
6350
|
try {
|
|
5846
6351
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -5881,8 +6386,8 @@ async function getOverview(root) {
|
|
|
5881
6386
|
}
|
|
5882
6387
|
|
|
5883
6388
|
// src/services/sync.ts
|
|
5884
|
-
import { promises as
|
|
5885
|
-
import
|
|
6389
|
+
import { promises as fs24 } from "fs";
|
|
6390
|
+
import path35 from "path";
|
|
5886
6391
|
import { createTwoFilesPatch } from "diff";
|
|
5887
6392
|
|
|
5888
6393
|
// src/core/impact/model-diff.ts
|
|
@@ -6068,7 +6573,7 @@ function detectRenames(prev, next, added, deleted) {
|
|
|
6068
6573
|
return { added: remainingAdded, modified: [], deleted: deleted.filter((d) => !usedDeleted.has(d)), renamed };
|
|
6069
6574
|
}
|
|
6070
6575
|
async function readPreviousModel(dir) {
|
|
6071
|
-
const raw = await readTextIfExists(
|
|
6576
|
+
const raw = await readTextIfExists(path35.join(dir, "model.json"));
|
|
6072
6577
|
if (!raw) return null;
|
|
6073
6578
|
try {
|
|
6074
6579
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -6078,7 +6583,7 @@ async function readPreviousModel(dir) {
|
|
|
6078
6583
|
}
|
|
6079
6584
|
}
|
|
6080
6585
|
async function readIgnored(dir) {
|
|
6081
|
-
const raw = await readTextIfExists(
|
|
6586
|
+
const raw = await readTextIfExists(path35.join(dir, IGNORE_FILE));
|
|
6082
6587
|
if (!raw) return null;
|
|
6083
6588
|
try {
|
|
6084
6589
|
const j = JSON.parse(raw);
|
|
@@ -6091,7 +6596,7 @@ async function planSync(root, opts = {}) {
|
|
|
6091
6596
|
const started = Date.now();
|
|
6092
6597
|
const dir = athenaDir(root);
|
|
6093
6598
|
const st = await readState(dir);
|
|
6094
|
-
if (st.kind === "missing" && !await readTextIfExists(
|
|
6599
|
+
if (st.kind === "missing" && !await readTextIfExists(path35.join(dir, "rules.md"))) {
|
|
6095
6600
|
throw new AthenaError("Athena is not initialized in this project.", "Run `athena init` first.", 3);
|
|
6096
6601
|
}
|
|
6097
6602
|
const prevState = st.kind === "ok" ? st.state : null;
|
|
@@ -6180,7 +6685,7 @@ async function applySync(root, plan) {
|
|
|
6180
6685
|
if (!inner) throw new AthenaError("This sync plan is no longer available.", "Create a new plan and try again.", 1, "conflict");
|
|
6181
6686
|
const dir = athenaDir(root);
|
|
6182
6687
|
for (const d of plan.documents) {
|
|
6183
|
-
const current = hashOf(await readTextIfExists(
|
|
6688
|
+
const current = hashOf(await readTextIfExists(path35.join(dir, d.file)));
|
|
6184
6689
|
if (current !== d.baseHash) {
|
|
6185
6690
|
throw conflict(`${d.file} changed after the sync plan was created.`, "Review the new plan before applying.", { file: d.file });
|
|
6186
6691
|
}
|
|
@@ -6188,10 +6693,10 @@ async function applySync(root, plan) {
|
|
|
6188
6693
|
const st = await readState(dir);
|
|
6189
6694
|
if (st.kind === "corrupted") await backupCorruptedState(dir);
|
|
6190
6695
|
const prevState = st.kind === "ok" ? st.state : inner.prevState;
|
|
6191
|
-
await
|
|
6696
|
+
await fs24.mkdir(dir, { recursive: true });
|
|
6192
6697
|
const docs = await writePlannedKnowledge(dir, inner.plans);
|
|
6193
6698
|
docs.push(await ensureRules(dir, inner.analysis.model));
|
|
6194
|
-
await writeFileAtomic(
|
|
6699
|
+
await writeFileAtomic(path35.join(dir, "model.json"), `${JSON.stringify(inner.analysis.model, null, 2)}
|
|
6195
6700
|
`);
|
|
6196
6701
|
const managedFiles = Object.values(prevState?.agents ?? {}).flatMap((a) => a.files);
|
|
6197
6702
|
const state = composeState({
|
|
@@ -6202,7 +6707,7 @@ async function applySync(root, plan) {
|
|
|
6202
6707
|
fileIndex: await indexWithIntegrations(root, inner.analysis.files, managedFiles)
|
|
6203
6708
|
});
|
|
6204
6709
|
await writeState(dir, state);
|
|
6205
|
-
await
|
|
6710
|
+
await fs24.rm(path35.join(dir, IGNORE_FILE), { force: true });
|
|
6206
6711
|
internals.delete(plan);
|
|
6207
6712
|
return {
|
|
6208
6713
|
applied: plan.documents.map((d) => d.file),
|
|
@@ -6212,7 +6717,7 @@ async function applySync(root, plan) {
|
|
|
6212
6717
|
}
|
|
6213
6718
|
async function ignorePlan(root, planId) {
|
|
6214
6719
|
if (!/^[a-f0-9]{16}$/.test(planId)) throw new AthenaError("Invalid plan id");
|
|
6215
|
-
await writeFileAtomic(
|
|
6720
|
+
await writeFileAtomic(path35.join(athenaDir(root), IGNORE_FILE), `${JSON.stringify({ planId, ignoredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
6216
6721
|
`);
|
|
6217
6722
|
}
|
|
6218
6723
|
function fileChangeCount(plan) {
|
|
@@ -6226,6 +6731,10 @@ function summarizePlan(plan) {
|
|
|
6226
6731
|
const { documents, ...rest } = plan;
|
|
6227
6732
|
return { ...rest, documents: documents.map(({ diff: _diff, ...d }) => d) };
|
|
6228
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
|
+
}
|
|
6229
6738
|
|
|
6230
6739
|
// src/services/security.ts
|
|
6231
6740
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -6445,7 +6954,7 @@ async function runSecurityScan(root, model, opts = {}) {
|
|
|
6445
6954
|
}
|
|
6446
6955
|
|
|
6447
6956
|
// src/services/review.ts
|
|
6448
|
-
import
|
|
6957
|
+
import path36 from "path";
|
|
6449
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)$/;
|
|
6450
6959
|
var SOURCE_FILE = /\.(m|c)?(t|j)sx?$|\.(py|go|rs|java|kt|cs|php|rb|dart|ex|swift|vue|svelte)$/;
|
|
6451
6960
|
var ROUTE_FILE = /(^|\/)(routes?|controllers?|handlers?|api|endpoints?|resolvers?)\/|(^|\/)(urls\.py|routes\.rb)$|(^|\/)app\/(.+\/)?route\.(t|j)sx?$/i;
|
|
@@ -6503,7 +7012,7 @@ async function reviewChanges(root, opts = {}) {
|
|
|
6503
7012
|
const untracked = changedFiles.filter((f) => f.status === "untracked");
|
|
6504
7013
|
let extra = "";
|
|
6505
7014
|
for (const f of untracked.slice(0, 200)) {
|
|
6506
|
-
const text = await readTextIfExists(
|
|
7015
|
+
const text = await readTextIfExists(path36.join(root, f.path));
|
|
6507
7016
|
if (text && text.length < 512 * 1024) extra += `+++ b/${f.path}
|
|
6508
7017
|
@@ -0,0 +1,${text.split("\n").length} @@
|
|
6509
7018
|
${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
@@ -6512,7 +7021,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6512
7021
|
const added = addedLines(patch + extra);
|
|
6513
7022
|
let untrackedAdded = 0;
|
|
6514
7023
|
for (const f of untracked) {
|
|
6515
|
-
const text = await readTextIfExists(
|
|
7024
|
+
const text = await readTextIfExists(path36.join(root, f.path));
|
|
6516
7025
|
if (text) untrackedAdded += text.split("\n").filter((l) => l.length > 0).length;
|
|
6517
7026
|
}
|
|
6518
7027
|
const numstat = (await git(root, [...diffArgs, "--numstat", "--", "."])).stdout;
|
|
@@ -6542,7 +7051,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6542
7051
|
const envFiles = paths.filter((p) => ENV_FILE.test(p));
|
|
6543
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." });
|
|
6544
7053
|
for (const p of paths.filter((x) => MANIFEST_FILE.test(x))) {
|
|
6545
|
-
const now = await readTextIfExists(
|
|
7054
|
+
const now = await readTextIfExists(path36.join(root, p));
|
|
6546
7055
|
const before = (await git(root, ["show", `${opts.base ?? "HEAD"}:${p}`])).stdout;
|
|
6547
7056
|
if (!now || !before) continue;
|
|
6548
7057
|
const newDeps = [...depsOf(p, now)].filter((d) => !depsOf(p, before).has(d));
|
|
@@ -6561,7 +7070,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6561
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`." });
|
|
6562
7071
|
const large = [];
|
|
6563
7072
|
for (const f of changedFiles.filter((x) => x.status === "added" || x.status === "untracked")) {
|
|
6564
|
-
const text = await readTextIfExists(
|
|
7073
|
+
const text = await readTextIfExists(path36.join(root, f.path)).catch(() => null);
|
|
6565
7074
|
if (text && Buffer.byteLength(text) > LARGE_ADDED_BYTES) large.push(f.path);
|
|
6566
7075
|
}
|
|
6567
7076
|
if (large.length) add({ level: "warning", check: "large-files", message: "Large files added", files: large, hint: "Consider whether these belong in Git." });
|
|
@@ -6579,9 +7088,9 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
|
|
|
6579
7088
|
knowledgeInSync = await opts.checkSync(root).catch(() => null);
|
|
6580
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." });
|
|
6581
7090
|
}
|
|
6582
|
-
const rulesText = await readTextIfExists(
|
|
7091
|
+
const rulesText = await readTextIfExists(path36.join(athenaDir(root), "rules.md"));
|
|
6583
7092
|
const rules = rulesText ? listRules(parseRules(rulesText)).filter((r) => r.enabled) : [];
|
|
6584
|
-
const reviewDoc = await readTextIfExists(
|
|
7093
|
+
const reviewDoc = await readTextIfExists(path36.join(athenaDir(root), "code-review.md"));
|
|
6585
7094
|
const checklist = reviewDoc ? [...reviewDoc.matchAll(/^- \[ \] (.+)$/gm)].map((m) => m[1].trim()) : [];
|
|
6586
7095
|
return { base, changedFiles, findings, rules, checklist, knowledgeInSync, stats: { added: addedCount + untrackedAdded, removed: removedCount, files: changedFiles.length } };
|
|
6587
7096
|
}
|
|
@@ -6590,11 +7099,11 @@ function hasBlockers(result) {
|
|
|
6590
7099
|
}
|
|
6591
7100
|
|
|
6592
7101
|
// src/services/context.ts
|
|
6593
|
-
import
|
|
7102
|
+
import path38 from "path";
|
|
6594
7103
|
|
|
6595
7104
|
// src/core/graph/graph.ts
|
|
6596
7105
|
import { z as z6 } from "zod";
|
|
6597
|
-
import
|
|
7106
|
+
import path37 from "path";
|
|
6598
7107
|
var NodeKind = z6.enum(["package", "file", "route", "entity", "framework", "command", "infra", "doc"]);
|
|
6599
7108
|
var EdgeKind = z6.enum(["contains", "imports", "handles", "defines", "depends_on", "documents", "runs"]);
|
|
6600
7109
|
var GraphNode = z6.object({
|
|
@@ -6618,7 +7127,7 @@ var IMPORT_RE = /^\s*(?:import\s[^'"]*from\s*['"]([^'"]+)['"]|import\s*['"]([^'"
|
|
|
6618
7127
|
var PY_IMPORT_RE = /^\s*(?:from\s+([.\w]+)\s+import|import\s+([.\w]+))/gm;
|
|
6619
7128
|
function resolveImport(fromFile, spec, fileSet) {
|
|
6620
7129
|
if (!spec.startsWith(".")) return null;
|
|
6621
|
-
const base = toPosix(
|
|
7130
|
+
const base = toPosix(path37.posix.normalize(path37.posix.join(path37.posix.dirname(fromFile), spec)));
|
|
6622
7131
|
const candidates = [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".py", ".vue", ".svelte"].flatMap((ext) => [`${base}${ext}`, `${base}/index${ext}`, `${base}/__init__${ext}`])];
|
|
6623
7132
|
return candidates.find((c2) => fileSet.has(c2)) ?? null;
|
|
6624
7133
|
}
|
|
@@ -6634,7 +7143,7 @@ async function buildGraph(model, opts) {
|
|
|
6634
7143
|
if (from === to || !nodes.has(from) || !nodes.has(to)) return;
|
|
6635
7144
|
if (!edges.some((e) => e.from === from && e.to === to && e.kind === kind)) edges.push({ from, to, kind });
|
|
6636
7145
|
};
|
|
6637
|
-
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: {} });
|
|
6638
7147
|
for (const pkg of model.workspace.packages) {
|
|
6639
7148
|
addNode({ id: nodeId("package", pkg.name), kind: "package", name: pkg.name, path: pkg.path, meta: { kind: pkg.kind, ecosystem: pkg.ecosystem } });
|
|
6640
7149
|
}
|
|
@@ -6653,8 +7162,8 @@ async function buildGraph(model, opts) {
|
|
|
6653
7162
|
for (const e of model.dbEntities) {
|
|
6654
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 } });
|
|
6655
7164
|
addEdge(id, fileNode(e.file), "defines");
|
|
6656
|
-
for (const
|
|
6657
|
-
const target =
|
|
7165
|
+
for (const rel2 of e.relations) {
|
|
7166
|
+
const target = rel2.split("\u2192")[1]?.trim();
|
|
6658
7167
|
const other = target ? model.dbEntities.find((x) => x.name.toLowerCase() === target.toLowerCase()) : void 0;
|
|
6659
7168
|
if (other) addEdge(id, nodeId("entity", `${other.name}@${other.file}`), "depends_on");
|
|
6660
7169
|
}
|
|
@@ -6704,11 +7213,11 @@ async function buildGraph(model, opts) {
|
|
|
6704
7213
|
};
|
|
6705
7214
|
}
|
|
6706
7215
|
async function saveGraph(root, graph) {
|
|
6707
|
-
await writeFileAtomic(
|
|
7216
|
+
await writeFileAtomic(path37.join(athenaDir(root), GRAPH_FILE), `${JSON.stringify(graph, null, 2)}
|
|
6708
7217
|
`);
|
|
6709
7218
|
}
|
|
6710
7219
|
async function loadGraph(root) {
|
|
6711
|
-
const raw = await readTextIfExists(
|
|
7220
|
+
const raw = await readTextIfExists(path37.join(athenaDir(root), GRAPH_FILE));
|
|
6712
7221
|
if (!raw) return null;
|
|
6713
7222
|
try {
|
|
6714
7223
|
const parsed = ProjectGraph.safeParse(JSON.parse(raw));
|
|
@@ -6905,7 +7414,7 @@ function formatContext(ctx) {
|
|
|
6905
7414
|
|
|
6906
7415
|
// src/services/context.ts
|
|
6907
7416
|
async function loadModel(root) {
|
|
6908
|
-
const raw = await readTextIfExists(
|
|
7417
|
+
const raw = await readTextIfExists(path38.join(athenaDir(root), "model.json"));
|
|
6909
7418
|
if (!raw) return null;
|
|
6910
7419
|
try {
|
|
6911
7420
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -6921,7 +7430,7 @@ async function refreshGraph(root, opts = {}) {
|
|
|
6921
7430
|
const graph = await buildGraph(analysis.model, {
|
|
6922
7431
|
files,
|
|
6923
7432
|
signal: opts.signal,
|
|
6924
|
-
read: async (
|
|
7433
|
+
read: async (rel2) => readTextIfExists(path38.join(root, rel2)).catch(() => null)
|
|
6925
7434
|
});
|
|
6926
7435
|
await saveGraph(root, graph);
|
|
6927
7436
|
return graph;
|
|
@@ -6937,14 +7446,14 @@ async function getRelevantContext(root, task, opts = {}) {
|
|
|
6937
7446
|
const documents = [];
|
|
6938
7447
|
for (const d of KNOWLEDGE_DOCS) {
|
|
6939
7448
|
if (d.id === "rules") continue;
|
|
6940
|
-
const text = await readTextIfExists(
|
|
7449
|
+
const text = await readTextIfExists(path38.join(dir, d.file));
|
|
6941
7450
|
if (text) documents.push({ id: d.id, file: d.file, sections: documentSections(text) });
|
|
6942
7451
|
}
|
|
6943
7452
|
if (!documents.length) throw new AthenaError("No knowledge documents found.", "Run `athena init` first.", 3);
|
|
6944
7453
|
return selectContext({
|
|
6945
7454
|
task,
|
|
6946
7455
|
documents,
|
|
6947
|
-
rulesMarkdown: await readTextIfExists(
|
|
7456
|
+
rulesMarkdown: await readTextIfExists(path38.join(dir, "rules.md")),
|
|
6948
7457
|
graph: await getGraph(root, { build: opts.buildGraph, signal: opts.signal }),
|
|
6949
7458
|
maxChars: opts.maxChars
|
|
6950
7459
|
});
|
|
@@ -6955,7 +7464,7 @@ async function graphSummary(root) {
|
|
|
6955
7464
|
}
|
|
6956
7465
|
|
|
6957
7466
|
// src/services/ai.ts
|
|
6958
|
-
import
|
|
7467
|
+
import path39 from "path";
|
|
6959
7468
|
|
|
6960
7469
|
// src/ai/provider.ts
|
|
6961
7470
|
import { z as z7 } from "zod";
|
|
@@ -7145,7 +7654,7 @@ async function buildEnrichPayload(root, opts = {}) {
|
|
|
7145
7654
|
for (const id of wanted) {
|
|
7146
7655
|
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
7147
7656
|
if (!doc) throw new AthenaError(`Unknown document: ${id}`, `Available: ${KNOWLEDGE_DOCS.map((d) => d.id).join(", ")}`);
|
|
7148
|
-
const text = await readTextIfExists(
|
|
7657
|
+
const text = await readTextIfExists(path39.join(athenaDir(root), doc.file));
|
|
7149
7658
|
if (!text) continue;
|
|
7150
7659
|
included.push(doc.file);
|
|
7151
7660
|
parts.push(`--- ${doc.file} ---
|
|
@@ -7198,14 +7707,14 @@ async function enrich(root, opts = {}) {
|
|
|
7198
7707
|
body,
|
|
7199
7708
|
""
|
|
7200
7709
|
].join("\n");
|
|
7201
|
-
const file =
|
|
7710
|
+
const file = path39.join(athenaDir(root), SUGGESTIONS_FILE);
|
|
7202
7711
|
await writeFileAtomic(file, markdown);
|
|
7203
7712
|
return { markdown, file: SUGGESTIONS_FILE, provider: result.provider, model: result.model, chars: payload.chars, documents: payload.documents };
|
|
7204
7713
|
}
|
|
7205
7714
|
|
|
7206
7715
|
// src/services/watch.ts
|
|
7207
|
-
import { promises as
|
|
7208
|
-
import
|
|
7716
|
+
import { promises as fs25 } from "fs";
|
|
7717
|
+
import path40 from "path";
|
|
7209
7718
|
import { watch as chokidarWatch } from "chokidar";
|
|
7210
7719
|
async function gitDir(root) {
|
|
7211
7720
|
const r = await git(root, ["rev-parse", "--absolute-git-dir"]);
|
|
@@ -7223,7 +7732,7 @@ async function watchProject(root, opts) {
|
|
|
7223
7732
|
let rerun = false;
|
|
7224
7733
|
let latest = null;
|
|
7225
7734
|
let closed = false;
|
|
7226
|
-
const toRel = (abs) => toPosix(
|
|
7735
|
+
const toRel = (abs) => toPosix(path40.relative(root, abs));
|
|
7227
7736
|
const run3 = async () => {
|
|
7228
7737
|
if (closed) return null;
|
|
7229
7738
|
if (running) {
|
|
@@ -7282,14 +7791,14 @@ async function watchProject(root, opts) {
|
|
|
7282
7791
|
}, wait);
|
|
7283
7792
|
};
|
|
7284
7793
|
const onPath = (abs) => {
|
|
7285
|
-
const
|
|
7286
|
-
if (!
|
|
7287
|
-
if (
|
|
7288
|
-
const dir =
|
|
7289
|
-
void
|
|
7290
|
-
}
|
|
7291
|
-
pending.add(
|
|
7292
|
-
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] });
|
|
7293
7802
|
schedule();
|
|
7294
7803
|
};
|
|
7295
7804
|
const watcher = chokidarWatch(root, {
|
|
@@ -7297,12 +7806,12 @@ async function watchProject(root, opts) {
|
|
|
7297
7806
|
persistent: true,
|
|
7298
7807
|
followSymlinks: false,
|
|
7299
7808
|
ignored: (abs, stats) => {
|
|
7300
|
-
const
|
|
7301
|
-
if (!
|
|
7302
|
-
if (
|
|
7303
|
-
if (
|
|
7304
|
-
if (stats) return matcher.ignores(
|
|
7305
|
-
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);
|
|
7306
7815
|
}
|
|
7307
7816
|
});
|
|
7308
7817
|
const ready = new Promise((resolve2) => watcher.once("ready", () => resolve2()));
|
|
@@ -7313,7 +7822,7 @@ async function watchProject(root, opts) {
|
|
|
7313
7822
|
let lastHead = null;
|
|
7314
7823
|
if (gd) {
|
|
7315
7824
|
lastHead = (await git(root, ["rev-parse", "HEAD"])).stdout.trim() || null;
|
|
7316
|
-
headWatcher = chokidarWatch([
|
|
7825
|
+
headWatcher = chokidarWatch([path40.join(gd, "HEAD"), path40.join(gd, "refs", "heads")], { ignoreInitial: true, persistent: true, depth: 5 });
|
|
7317
7826
|
headWatcher.on("all", () => {
|
|
7318
7827
|
void git(root, ["rev-parse", "HEAD"]).then((r) => {
|
|
7319
7828
|
const head = r.ok ? r.stdout.trim() : null;
|
|
@@ -7465,8 +7974,8 @@ function allowedOriginsFor(host, port) {
|
|
|
7465
7974
|
}
|
|
7466
7975
|
|
|
7467
7976
|
// src/server/static.ts
|
|
7468
|
-
import { promises as
|
|
7469
|
-
import
|
|
7977
|
+
import { promises as fs26 } from "fs";
|
|
7978
|
+
import path41 from "path";
|
|
7470
7979
|
var TYPES = {
|
|
7471
7980
|
".html": "text/html; charset=utf-8",
|
|
7472
7981
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7482,20 +7991,20 @@ var TYPES = {
|
|
|
7482
7991
|
};
|
|
7483
7992
|
async function buildAssetMap(webDir) {
|
|
7484
7993
|
try {
|
|
7485
|
-
await
|
|
7994
|
+
await fs26.access(path41.join(webDir, "index.html"));
|
|
7486
7995
|
} catch {
|
|
7487
7996
|
return null;
|
|
7488
7997
|
}
|
|
7489
7998
|
const map = /* @__PURE__ */ new Map();
|
|
7490
7999
|
async function walk2(dir) {
|
|
7491
|
-
for (const ent of await
|
|
7492
|
-
const abs =
|
|
8000
|
+
for (const ent of await fs26.readdir(dir, { withFileTypes: true })) {
|
|
8001
|
+
const abs = path41.join(dir, ent.name);
|
|
7493
8002
|
if (ent.isDirectory()) await walk2(abs);
|
|
7494
8003
|
else if (ent.isFile()) {
|
|
7495
|
-
const ext =
|
|
8004
|
+
const ext = path41.extname(ent.name).toLowerCase();
|
|
7496
8005
|
const type = TYPES[ext];
|
|
7497
8006
|
if (!type) continue;
|
|
7498
|
-
const urlPath = `/${
|
|
8007
|
+
const urlPath = `/${path41.relative(webDir, abs).split(path41.sep).join("/")}`;
|
|
7499
8008
|
map.set(urlPath, { abs, type, immutable: urlPath.startsWith("/assets/") });
|
|
7500
8009
|
}
|
|
7501
8010
|
}
|
|
@@ -7714,11 +8223,11 @@ async function createServer(opts) {
|
|
|
7714
8223
|
const readNewAgentEvents = async () => {
|
|
7715
8224
|
try {
|
|
7716
8225
|
const file = activityFile(root);
|
|
7717
|
-
const st = await
|
|
8226
|
+
const st = await fs27.stat(file).catch(() => null);
|
|
7718
8227
|
if (!st) return;
|
|
7719
8228
|
if (st.size < activityOffset) activityOffset = 0;
|
|
7720
8229
|
if (st.size === activityOffset) return;
|
|
7721
|
-
const handle = await
|
|
8230
|
+
const handle = await fs27.open(file, "r");
|
|
7722
8231
|
try {
|
|
7723
8232
|
const length = st.size - activityOffset;
|
|
7724
8233
|
const buf = Buffer.alloc(length);
|
|
@@ -7763,7 +8272,7 @@ async function createServer(opts) {
|
|
|
7763
8272
|
try {
|
|
7764
8273
|
const result = await applySync(root, plan);
|
|
7765
8274
|
for (const d of plan.documents) {
|
|
7766
|
-
const text = await
|
|
8275
|
+
const text = await fs27.readFile(path42.join(athenaDir(root), d.file), "utf8").catch(() => null);
|
|
7767
8276
|
if (text !== null) recentWrites.set(d.file, contentHash(text));
|
|
7768
8277
|
}
|
|
7769
8278
|
currentPlan = null;
|
|
@@ -7788,7 +8297,7 @@ async function createServer(opts) {
|
|
|
7788
8297
|
});
|
|
7789
8298
|
let scanRunning = false;
|
|
7790
8299
|
const projectModel = async () => {
|
|
7791
|
-
const raw = await
|
|
8300
|
+
const raw = await fs27.readFile(path42.join(athenaDir(root), "model.json"), "utf8").catch(() => null);
|
|
7792
8301
|
if (raw) {
|
|
7793
8302
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
7794
8303
|
if (parsed.success) return parsed.data;
|
|
@@ -7828,7 +8337,7 @@ async function createServer(opts) {
|
|
|
7828
8337
|
);
|
|
7829
8338
|
app.get("/api/review", async (req) => {
|
|
7830
8339
|
const base = typeof req.query.base === "string" && req.query.base ? req.query.base : void 0;
|
|
7831
|
-
return reviewChanges(root, { base, checkSync: async (r) => (await planSync(r)).
|
|
8340
|
+
return reviewChanges(root, { base, checkSync: async (r) => staleForCheck(await planSync(r)).length === 0 });
|
|
7832
8341
|
});
|
|
7833
8342
|
let graphBuilding = false;
|
|
7834
8343
|
app.get("/api/context", async (req) => {
|
|
@@ -7886,7 +8395,7 @@ data: ${JSON.stringify(data)}
|
|
|
7886
8395
|
const sendAsset = async (reply, urlPath) => {
|
|
7887
8396
|
if (!assets) return reply.code(503).type("text/html; charset=utf-8").send(MISSING_UI_HTML);
|
|
7888
8397
|
const asset = assets.get(urlPath) ?? assets.get("/index.html");
|
|
7889
|
-
const body = await
|
|
8398
|
+
const body = await fs27.readFile(asset.abs);
|
|
7890
8399
|
reply.header("Cache-Control", asset.immutable ? "public, max-age=31536000, immutable" : "no-cache");
|
|
7891
8400
|
return reply.type(asset.type).send(body);
|
|
7892
8401
|
};
|
|
@@ -7921,7 +8430,7 @@ data: ${JSON.stringify(data)}
|
|
|
7921
8430
|
name,
|
|
7922
8431
|
setTimeout(async () => {
|
|
7923
8432
|
pending.delete(name);
|
|
7924
|
-
const text = await
|
|
8433
|
+
const text = await fs27.readFile(path42.join(dir, name), "utf8").catch(() => null);
|
|
7925
8434
|
const hash = text === null ? null : contentHash(text);
|
|
7926
8435
|
if (hash && recentWrites.get(name) === hash) return;
|
|
7927
8436
|
invalidateStatus();
|
|
@@ -7937,7 +8446,7 @@ data: ${JSON.stringify(data)}
|
|
|
7937
8446
|
{
|
|
7938
8447
|
const history = await readRecentEvents(root, 200);
|
|
7939
8448
|
for (const e of history) applyAgentEvent(e, false);
|
|
7940
|
-
activityOffset = await
|
|
8449
|
+
activityOffset = await fs27.stat(activityFile(root)).then((st) => st.size).catch(() => 0);
|
|
7941
8450
|
}
|
|
7942
8451
|
if (opts.watch) {
|
|
7943
8452
|
try {
|
|
@@ -8027,8 +8536,8 @@ async function findAvailablePort(host, start = DEFAULT_PORT, attempts = 100) {
|
|
|
8027
8536
|
|
|
8028
8537
|
// src/server/instance.ts
|
|
8029
8538
|
function defaultWebDir() {
|
|
8030
|
-
const here =
|
|
8031
|
-
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");
|
|
8032
8541
|
}
|
|
8033
8542
|
function browserUrl(info2) {
|
|
8034
8543
|
const host = info2.host.includes(":") ? `[${info2.host}]` : info2.host;
|
|
@@ -8044,8 +8553,8 @@ async function startServer(opts) {
|
|
|
8044
8553
|
const server = await createServer({ root: opts.root, token, host, port, webDir: opts.webDir ?? defaultWebDir(), allowRemote: opts.allowRemote, watch: opts.watch ?? true });
|
|
8045
8554
|
await server.app.listen({ host, port });
|
|
8046
8555
|
const info2 = { pid: process.pid, host, port, instanceId: server.instanceId, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
8047
|
-
const file =
|
|
8048
|
-
await
|
|
8556
|
+
const file = path43.join(athenaDir(opts.root), INSTANCE_FILE);
|
|
8557
|
+
await fs28.writeFile(file, `${JSON.stringify(info2, null, 2)}
|
|
8049
8558
|
`, { mode: 384 });
|
|
8050
8559
|
await ensureGitignored(opts.root, INSTANCE_FILE);
|
|
8051
8560
|
return {
|
|
@@ -8054,16 +8563,16 @@ async function startServer(opts) {
|
|
|
8054
8563
|
url: browserUrl(info2),
|
|
8055
8564
|
async close() {
|
|
8056
8565
|
const current = await readTextIfExists(file);
|
|
8057
|
-
if (current && current.includes(server.instanceId)) await
|
|
8566
|
+
if (current && current.includes(server.instanceId)) await fs28.rm(file, { force: true });
|
|
8058
8567
|
await server.close();
|
|
8059
8568
|
}
|
|
8060
8569
|
};
|
|
8061
8570
|
}
|
|
8062
8571
|
async function ensureGitignored(root, entry) {
|
|
8063
|
-
const file =
|
|
8572
|
+
const file = path43.join(athenaDir(root), ".gitignore");
|
|
8064
8573
|
const text = await readTextIfExists(file) ?? "";
|
|
8065
8574
|
if (text.split(/\r?\n/).includes(entry)) return;
|
|
8066
|
-
await
|
|
8575
|
+
await fs28.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
|
|
8067
8576
|
`);
|
|
8068
8577
|
}
|
|
8069
8578
|
|
|
@@ -8191,9 +8700,15 @@ async function syncCommand(opts) {
|
|
|
8191
8700
|
sp.stop();
|
|
8192
8701
|
throw err;
|
|
8193
8702
|
}
|
|
8703
|
+
const stale = staleForCheck(plan);
|
|
8704
|
+
const inSync = stale.length === 0;
|
|
8194
8705
|
if (isJson() && (opts.dryRun || opts.check)) {
|
|
8195
|
-
json({
|
|
8196
|
-
|
|
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;
|
|
8197
8712
|
}
|
|
8198
8713
|
if (!isJson() && !isQuiet()) {
|
|
8199
8714
|
heading("Athena Sync");
|
|
@@ -8204,8 +8719,12 @@ async function syncCommand(opts) {
|
|
|
8204
8719
|
line(`Proposed: ${plan.documents.map((d) => d.file).join(", ")}`);
|
|
8205
8720
|
}
|
|
8206
8721
|
if (opts.check) {
|
|
8207
|
-
if (!
|
|
8208
|
-
|
|
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;
|
|
8209
8728
|
}
|
|
8210
8729
|
if (opts.dryRun) {
|
|
8211
8730
|
if (!isJson()) line(dim("Dry run \u2014 nothing was written."));
|
|
@@ -8242,7 +8761,7 @@ async function syncCommand(opts) {
|
|
|
8242
8761
|
}
|
|
8243
8762
|
|
|
8244
8763
|
// src/cli/commands/event.ts
|
|
8245
|
-
import
|
|
8764
|
+
import path44 from "path";
|
|
8246
8765
|
var STDIN_TIMEOUT_MS = 2e3;
|
|
8247
8766
|
var MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
8248
8767
|
async function readStdin() {
|
|
@@ -8288,7 +8807,7 @@ async function eventCommand(opts) {
|
|
|
8288
8807
|
let root = null;
|
|
8289
8808
|
for (const c2 of cwdCandidates) {
|
|
8290
8809
|
if (!c2) continue;
|
|
8291
|
-
root = await findProjectRoot(
|
|
8810
|
+
root = await findProjectRoot(path44.resolve(c2));
|
|
8292
8811
|
if (root) break;
|
|
8293
8812
|
}
|
|
8294
8813
|
if (!root) return;
|
|
@@ -8326,7 +8845,7 @@ async function activityCommand(opts) {
|
|
|
8326
8845
|
const agents = await listAgents(root);
|
|
8327
8846
|
const reporting = agents.filter((a) => a.activityObservation === "hooks");
|
|
8328
8847
|
line(dim("No agent activity recorded yet."));
|
|
8329
|
-
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."));
|
|
8330
8849
|
return;
|
|
8331
8850
|
}
|
|
8332
8851
|
for (const e of events) {
|
|
@@ -8342,7 +8861,7 @@ async function activityCommand(opts) {
|
|
|
8342
8861
|
}
|
|
8343
8862
|
|
|
8344
8863
|
// src/cli/commands/security.ts
|
|
8345
|
-
import
|
|
8864
|
+
import path45 from "path";
|
|
8346
8865
|
var TONE = {
|
|
8347
8866
|
critical: c.red,
|
|
8348
8867
|
high: c.red,
|
|
@@ -8351,7 +8870,7 @@ var TONE = {
|
|
|
8351
8870
|
unknown: c.dim
|
|
8352
8871
|
};
|
|
8353
8872
|
async function loadModel2(root, signal) {
|
|
8354
|
-
const raw = await readTextIfExists(
|
|
8873
|
+
const raw = await readTextIfExists(path45.join(athenaDir(root), "model.json"));
|
|
8355
8874
|
if (raw) {
|
|
8356
8875
|
try {
|
|
8357
8876
|
const parsed = ProjectModel.safeParse(JSON.parse(raw));
|
|
@@ -8449,7 +8968,7 @@ async function reviewCommand(opts) {
|
|
|
8449
8968
|
result = await reviewChanges(root, {
|
|
8450
8969
|
base: opts.base,
|
|
8451
8970
|
signal: opts.signal,
|
|
8452
|
-
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
|
|
8453
8972
|
});
|
|
8454
8973
|
sp.stop();
|
|
8455
8974
|
} catch (err) {
|
|
@@ -8656,13 +9175,13 @@ async function aiEnrichCommand(opts) {
|
|
|
8656
9175
|
}
|
|
8657
9176
|
|
|
8658
9177
|
// src/mcp/server.ts
|
|
8659
|
-
import
|
|
9178
|
+
import path46 from "path";
|
|
8660
9179
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8661
9180
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8662
9181
|
import { z as z8 } from "zod";
|
|
8663
9182
|
var textResult = (text) => ({ content: [{ type: "text", text }] });
|
|
8664
9183
|
async function docText(root, file) {
|
|
8665
|
-
const text = await readTextIfExists(
|
|
9184
|
+
const text = await readTextIfExists(path46.join(athenaDir(root), file));
|
|
8666
9185
|
return text ?? `${file} does not exist yet. Run \`athena init\` or \`athena analyze\` in this project.`;
|
|
8667
9186
|
}
|
|
8668
9187
|
function createMcpServer(opts) {
|
|
@@ -8723,7 +9242,7 @@ _Last dependency scan: ${scan.scannedAt}._` : "\n\n_No dependency audit has been
|
|
|
8723
9242
|
"get_project_rules",
|
|
8724
9243
|
{ title: "Project rules", description: "Developer-defined rules this project expects agents to follow. Disabled rules are excluded.", inputSchema: {} },
|
|
8725
9244
|
async () => {
|
|
8726
|
-
const text = await readTextIfExists(
|
|
9245
|
+
const text = await readTextIfExists(path46.join(athenaDir(root), "rules.md"));
|
|
8727
9246
|
if (!text) return textResult("No rules.md found. Run `athena init` in this project.");
|
|
8728
9247
|
const rules = listRules(parseRules(text)).filter((r) => r.enabled);
|
|
8729
9248
|
return textResult(rules.length ? ["# Project rules (all apply)", "", ...rules.map((r) => `- [${r.section}] ${r.text}`)].join("\n") : "rules.md contains no enabled rules.");
|
|
@@ -8914,6 +9433,216 @@ async function watchCommand(opts) {
|
|
|
8914
9433
|
await watcher.close();
|
|
8915
9434
|
}
|
|
8916
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
|
+
|
|
8917
9646
|
// src/cli/commands/agents.ts
|
|
8918
9647
|
async function agentsListCommand(opts) {
|
|
8919
9648
|
const root = await requireProjectRoot(opts);
|
|
@@ -8979,7 +9708,7 @@ function run2(fn) {
|
|
|
8979
9708
|
function buildProgram() {
|
|
8980
9709
|
const program = new Command2();
|
|
8981
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 });
|
|
8982
|
-
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(
|
|
8983
9712
|
run2(async (cmdOpts, cmd) => {
|
|
8984
9713
|
const g = globals(cmd);
|
|
8985
9714
|
await initCommand({ ...g, agents: typeof cmdOpts.agents === "string" ? cmdOpts.agents : void 0, noAgents: cmdOpts.agents === false, dryRun: cmdOpts.dryRun, signal: controller.signal });
|
|
@@ -9008,8 +9737,12 @@ function buildProgram() {
|
|
|
9008
9737
|
rules.command("remove <number>").description("Delete a rule").action(run2(async (n, _o, cmd) => rulesRemoveCommand(n, globals(cmd))));
|
|
9009
9738
|
const agents = program.command("agents").description("Manage AI agent integrations");
|
|
9010
9739
|
agents.command("list", { isDefault: true }).description("Show supported agents and integration status").action(run2(async (_o, cmd) => agentsListCommand(globals(cmd))));
|
|
9011
|
-
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))));
|
|
9012
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))));
|
|
9013
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 })));
|
|
9014
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 })));
|
|
9015
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(
|