project-athena 0.2.0 → 0.3.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 +76 -0
- package/README.md +19 -11
- package/dist/activity-P4UUFHWV.js +77 -0
- package/dist/agents-3HTSQGL2.js +69 -0
- package/dist/ai-ASL4FFQ7.js +96 -0
- package/dist/analyze-F3YOILNL.js +49 -0
- package/dist/chunk-2DY4PUR4.js +465 -0
- package/dist/chunk-2R6PXY6Z.js +33 -0
- package/dist/chunk-2RS6TA3D.js +908 -0
- package/dist/chunk-32PRS2MG.js +169 -0
- package/dist/chunk-3IS2X2Z7.js +89 -0
- package/dist/chunk-65TSRDQ3.js +124 -0
- package/dist/chunk-673EPYYH.js +105 -0
- package/dist/chunk-AD4OZCU2.js +34 -0
- package/dist/chunk-CXUTYCZX.js +238 -0
- package/dist/chunk-DOQMAZL5.js +436 -0
- package/dist/chunk-EF6GHOIN.js +343 -0
- package/dist/chunk-ELKFRTZT.js +221 -0
- package/dist/chunk-EUPETUWA.js +14 -0
- package/dist/chunk-FQEGEVLY.js +38 -0
- package/dist/chunk-GHFRQAYT.js +112 -0
- package/dist/chunk-GR4DL2VM.js +47 -0
- package/dist/chunk-INT3IGMQ.js +111 -0
- package/dist/chunk-IQQBAWSM.js +90 -0
- package/dist/chunk-JCGDR4UV.js +182 -0
- package/dist/chunk-JY6BFIJJ.js +95 -0
- package/dist/chunk-KBO7XRBW.js +26 -0
- package/dist/chunk-LEMPCNAY.js +209 -0
- package/dist/chunk-LMOFZNTQ.js +379 -0
- package/dist/chunk-OLPSEQS5.js +264 -0
- package/dist/chunk-PANVPNYF.js +2892 -0
- package/dist/chunk-PDFQ5BFG.js +378 -0
- package/dist/chunk-PR53DT7I.js +179 -0
- package/dist/chunk-PVCIGUIT.js +64 -0
- package/dist/chunk-RXNRMIZP.js +390 -0
- package/dist/chunk-S7T6VX56.js +61 -0
- package/dist/chunk-SHKXELUX.js +295 -0
- package/dist/chunk-SJOOXO34.js +904 -0
- package/dist/chunk-TGJUUB44.js +399 -0
- package/dist/chunk-VSYBC3VY.js +28 -0
- package/dist/chunk-XB66PZ6Z.js +182 -0
- package/dist/chunk-XFN7UZCS.js +35 -0
- package/dist/chunk-XW3MLHI2.js +32 -0
- package/dist/chunk-Z6QWMJLK.js +124 -0
- package/dist/chunk-ZB7LC6FP.js +221 -0
- package/dist/clean-S7PKCOLM.js +23 -0
- package/dist/cli.js +18 -9795
- package/dist/context-AH5MFHRG.js +147 -0
- package/dist/doctor-RSJMLE2P.js +62 -0
- package/dist/event-TNT6CQZR.js +101 -0
- package/dist/git-hook-YZNZGAAP.js +252 -0
- package/dist/init-2V34DVRQ.js +37 -0
- package/dist/mcp-T2GRTEX5.js +255 -0
- package/dist/open-2QSZKKUG.js +1259 -0
- package/dist/program-V2USHGXN.js +148 -0
- package/dist/review-R3R2J5BJ.js +115 -0
- package/dist/rules-4FXJUCXB.js +83 -0
- package/dist/security-PI4LXCOB.js +175 -0
- package/dist/staged-check-HHIBKKMR.js +500 -0
- package/dist/status-6M73WX5I.js +75 -0
- package/dist/sync-3ZNJDSSU.js +216 -0
- package/dist/watch-USN5CQIV.js +121 -0
- package/dist/web/assets/{Editor-CL6A82dQ.js → Editor-BI56-HX5.js} +1 -1
- package/dist/web/assets/{abnfDiagram-O67JEVCF-BuFfjTfx.js → abnfDiagram-O67JEVCF-y_9px2ob.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-DyxkwYiW.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-BTsv87pA.js → architectureDiagram-NJMV4G6O-lB6fcVzU.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-CNXqyKIx.js → blockDiagram-BEXU5L5S-DcM5pEjM.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-kksFXQoJ.js → c4Diagram-YGBWAQC7-CJgqxTY2.js} +1 -1
- package/dist/web/assets/channel-BvDD7AMO.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-BY99XYBs.js → chunk-3FUC2YCW-B8l_VuWR.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-h-PRThMP.js → chunk-5DYCD2WN-CZecgNhf.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-BIxfyBUj.js → chunk-742MDFTN-BeZ7P5jB.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-DZtctDdk.js → chunk-7INBJB4K-IKy0pr5m.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-BX9fUUbL.js → chunk-7M6MHVWA-Dg_QqJEX.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-X1s3e81n.js → chunk-7PRAP22T-DqQNS8oq.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-Bh8eluU-.js → chunk-GTNCS2PH-x4sM51nJ.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-D1KATAi0.js → chunk-GWA4HPMP-CE5GcwVN.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-CEErv5kP.js → chunk-MBY4JIJT-DA_D1hjF.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-BXiH4aPa.js → chunk-NETBCI7D-CeJ3XBZ2.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-Dede62yi.js → chunk-O7XYJQB3-BH2gESvO.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-KZ9aWRTa.js → chunk-UA2S7LBM-BWr0xCux.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-xf-3L2zU.js → chunk-WEXAMYUT-B4QwUZPM.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-DLpRdZ8n.js → chunk-Z7XXMR3K-BxcA-y5B.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-BdJ0_J-U.js → chunk-ZIGJFQKS-BZYvP63G.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-C5VzGDGq.js → classDiagram-v2-NBCMYWYE-BS3P_NXb.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-BqOgbE7U.js → cynefin-OW5HDTMX-BALD4Pve.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-CCC_feG5.js → cynefinDiagram-VND7K2PF-BM2y4W6d.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-SLP-94sd.js → dagre-6A5THRUB-PTfOrIdF.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-DpHwEBYM.js → diagram-22UHCM2B-CpXEHhUl.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-iGFwuMEk.js → diagram-3UASUU5V-DPHfXzr6.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-Csx9hj0z.js → diagram-ATOU4E4O-DMBNXvAC.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-yGKrLRO5.js → diagram-CDSNMT55-CDLe1M7G.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-mDpCormD.js → diagram-MLGK6HIB-D6L8oE7j.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-BJHdatrF.js → diagram-MPIPVDR6-CP-S7rrQ.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-ClJbCBBu.js → ebnfDiagram-ZINNZB2B-CkUc63TK.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-C5tFZMYm.js → elk-276RUBZZ-C1CAJLH_.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-u7_UFZot.js → erDiagram-OPXOYQCR-DXBnaTHm.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-cLIzvgDp.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-D7Zq63VQ.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-B4hrLcLf.js → ganttDiagram-FUAMR5RP-BVm8SiGg.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-Dxx-Bl-U.js → gitGraph-4MIJSDKK-Dm7xBrW0.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-DKjDV5Kr.js → gitGraphDiagram-X574FWY7-D_PIKKhx.js} +1 -1
- package/dist/web/assets/index-CeOIBpu2.js +45 -0
- package/dist/web/assets/{info-A6RAGUB7-CgqIdgQA.js → info-A6RAGUB7-CLGrx8pn.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-D-qjWkwy.js → infoDiagram-VRGFBTTK-BlBjaKm7.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-CzpqV6WN.js → ishikawaDiagram-OU5B5YK6-Kxe7GGS3.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-BBnOUDzw.js → journeyDiagram-ZHPQQLJL-wCMoHK4M.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-Di9sTFB1.js → kanban-definition-PNTS6WVX-BxdCz9Ko.js} +1 -1
- package/dist/web/assets/{line-B8ehGiwF.js → line-Zf11FnWF.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DWsT24e5.js → mermaid-parser.core-BvFFklev.js} +3 -3
- package/dist/web/assets/{mermaid.core-DzYpaVzI.js → mermaid.core-Cd_Ssti9.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-CxagBsH3.js → mindmap-definition-NLK3R4M7-BGPQl-GY.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-CmxbS0qf.js → packet-AYTQ26CC-6lI3cEX_.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-_3jnD3q8.js → pegDiagram-GJSIUBJH-BPIjlSAj.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-DppCWOqP.js → pie-WAS4IAKB-D6Dzj3Ja.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-DTVThTqz.js → pieDiagram-5QR66LMP-DtA-vZmv.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-BJ_OFnht.js → quadrantDiagram-O4NWA36T-EettyqQn.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-B6quSMjs.js → radar-RG4KPBEZ-xmRc3Dl8.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CErU7RG3.js → railroad-74A4TZTK-BLB7dhOn.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-DBFym6g8.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-CkG28ig8.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-Df_Ifaad.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-Zu0N_D-D.js → railroadDiagram-XR7U4H2S-PBw4We6b.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-Bl6CN3ul.js → requirementDiagram-PLB6GJNP-DjQpaUFA.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DdpKzZV8.js → sankeyDiagram-IPEJSGJF-CBW-_AoF.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BwUqrl9F.js → sequenceDiagram-PO4LG4MO-DEoPYqY9.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-C3vZGpAi.js → stateDiagram-v2-GCMORJYK-UZq5rPud.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-BjoMa2HW.js → swimlanes-2SLR337P-CACfuXJR.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CDSrqdgx.js +8 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-0apcf2l2.js → timeline-definition-EJHVYXUP-CYJl7mP-.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BOPWzyg3.js → treeView-Q6P3EWNA-DtZ5nkPQ.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-CC6rU6dD.js → treemap-WGGIJYW6-m49GDM-M.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-BFclVNBG.js → usecaseDiagram-POWQR4AR-C2Vg20UK.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-D2XPv-zb.js → vennDiagram-UO4OBE2U-C07v5OGI.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-Btz6ID8P.js → wardley-WFR3VGLG-C9Z7EZtz.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DFYPvGpS.js → wardleyDiagram-VNRHLVJA-DdA8CcSC.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-DMkQD-sV.js → xychartDiagram-PMCCYNJV-C9EZw07b.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +5 -2
- package/dist/web/assets/architecture-7GRP2DOG-CGSEE8po.js +0 -1
- package/dist/web/assets/channel-Dneho6u-.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-CA-vzJb3.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-Ca_1cbbS.js +0 -1
- package/dist/web/assets/index-s6lwoj84.js +0 -45
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CWsEvTRR.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cyq9ELNn.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-BX1hI1hV.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BGNz1HGG.js +0 -8
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readLocalConfig
|
|
4
|
+
} from "./chunk-PVCIGUIT.js";
|
|
5
|
+
import {
|
|
6
|
+
redact,
|
|
7
|
+
scanText
|
|
8
|
+
} from "./chunk-JCGDR4UV.js";
|
|
9
|
+
import {
|
|
10
|
+
KNOWLEDGE_DOCS
|
|
11
|
+
} from "./chunk-FQEGEVLY.js";
|
|
12
|
+
import {
|
|
13
|
+
athenaDir,
|
|
14
|
+
loadConfig
|
|
15
|
+
} from "./chunk-2DY4PUR4.js";
|
|
16
|
+
import {
|
|
17
|
+
readTextIfExists,
|
|
18
|
+
writeFileAtomic
|
|
19
|
+
} from "./chunk-IQQBAWSM.js";
|
|
20
|
+
import {
|
|
21
|
+
AthenaError
|
|
22
|
+
} from "./chunk-AD4OZCU2.js";
|
|
23
|
+
|
|
24
|
+
// src/services/ai.ts
|
|
25
|
+
import os from "os";
|
|
26
|
+
import path from "path";
|
|
27
|
+
import { z as z2 } from "zod";
|
|
28
|
+
|
|
29
|
+
// src/ai/provider.ts
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
var AiConfig = z.object({
|
|
32
|
+
provider: z.enum(["anthropic", "openai", "google", "ollama"]).default("anthropic"),
|
|
33
|
+
model: z.string().optional(),
|
|
34
|
+
/** cloud: a hosted API · local: a model on this machine (Ollama). */
|
|
35
|
+
mode: z.enum(["cloud", "local"]).default("cloud"),
|
|
36
|
+
/** Must be true (or --consent passed) before any project content leaves the machine. */
|
|
37
|
+
consent: z.boolean().default(false),
|
|
38
|
+
/** Upper bound on characters of knowledge sent in one request. */
|
|
39
|
+
maxChars: z.number().int().min(500).max(2e5).default(24e3),
|
|
40
|
+
/**
|
|
41
|
+
* Ollama base URL, or a custom compatible endpoint. Only ever set from a trusted,
|
|
42
|
+
* machine-local source (see resolveAiConfig in src/services/ai.ts) — never from the
|
|
43
|
+
* committed .athena/config.json, which anyone with push access controls.
|
|
44
|
+
*/
|
|
45
|
+
baseUrl: z.string().optional(),
|
|
46
|
+
/** Where baseUrl came from. Providers refuse a baseUrl without a trusted source. */
|
|
47
|
+
baseUrlSource: z.enum(["env", "local", "user"]).optional()
|
|
48
|
+
});
|
|
49
|
+
var AiError = class extends Error {
|
|
50
|
+
constructor(message, hint) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.hint = hint;
|
|
53
|
+
this.name = "AiError";
|
|
54
|
+
}
|
|
55
|
+
hint;
|
|
56
|
+
};
|
|
57
|
+
function isLoopbackUrl(url) {
|
|
58
|
+
try {
|
|
59
|
+
const host = new URL(url).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
60
|
+
return host === "localhost" || host === "::1" || host === "127.0.0.1";
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function providerBase(config, fallback) {
|
|
66
|
+
if (!config.baseUrl) return fallback;
|
|
67
|
+
if (!config.baseUrlSource) {
|
|
68
|
+
throw new AiError("Refusing to use an AI base URL that did not come from a trusted source.", 'Set ATHENA_AI_BASE_URL, or "ai.baseUrl" in .athena/local.json or your user config.');
|
|
69
|
+
}
|
|
70
|
+
return config.baseUrl.replace(/\/+$/, "");
|
|
71
|
+
}
|
|
72
|
+
async function postJson(url, body, headers, req) {
|
|
73
|
+
const timeout = AbortSignal.timeout(req.timeoutMs ?? 12e4);
|
|
74
|
+
const signal = req.signal ? AbortSignal.any([req.signal, timeout]) : timeout;
|
|
75
|
+
let res;
|
|
76
|
+
try {
|
|
77
|
+
res = await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body), signal });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new AiError(`Could not reach ${new URL(url).host}: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const detail = text.slice(0, 300);
|
|
84
|
+
throw new AiError(`${new URL(url).host} returned ${res.status}`, detail || void 0);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new AiError("Provider returned a non-JSON response");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function envKey(name) {
|
|
93
|
+
return name ? process.env[name]?.trim() || void 0 : void 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/ai/providers.ts
|
|
97
|
+
var str = (v) => typeof v === "string" ? v : "";
|
|
98
|
+
function keyAvailability(provider, endpoint) {
|
|
99
|
+
const key = envKey(provider.keyEnvVar);
|
|
100
|
+
return key ? { ok: true, endpoint, keyEnvVar: provider.keyEnvVar } : { ok: false, reason: `${provider.keyEnvVar} is not set`, endpoint, keyEnvVar: provider.keyEnvVar };
|
|
101
|
+
}
|
|
102
|
+
var anthropicProvider = {
|
|
103
|
+
id: "anthropic",
|
|
104
|
+
name: "Anthropic (Claude)",
|
|
105
|
+
remote: true,
|
|
106
|
+
defaultModel: "claude-sonnet-5",
|
|
107
|
+
keyEnvVar: "ANTHROPIC_API_KEY",
|
|
108
|
+
async available(config) {
|
|
109
|
+
return keyAvailability(anthropicProvider, providerBase(config, "https://api.anthropic.com"));
|
|
110
|
+
},
|
|
111
|
+
async complete(config, req) {
|
|
112
|
+
const key = envKey(anthropicProvider.keyEnvVar);
|
|
113
|
+
if (!key) throw new AiError("ANTHROPIC_API_KEY is not set");
|
|
114
|
+
const model = config.model ?? anthropicProvider.defaultModel;
|
|
115
|
+
const json = await postJson(
|
|
116
|
+
`${providerBase(config, "https://api.anthropic.com")}/v1/messages`,
|
|
117
|
+
{ model, max_tokens: req.maxTokens ?? 2e3, system: req.system, messages: [{ role: "user", content: req.prompt }] },
|
|
118
|
+
{ "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
119
|
+
req
|
|
120
|
+
);
|
|
121
|
+
const content = Array.isArray(json.content) ? json.content : [];
|
|
122
|
+
const text = content.map((c) => c && typeof c === "object" ? str(c.text) : "").join("");
|
|
123
|
+
const usage = json.usage ?? {};
|
|
124
|
+
return { text, model: str(json.model) || model, provider: "anthropic", usage: { inputTokens: usage.input_tokens, outputTokens: usage.output_tokens } };
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var openaiProvider = {
|
|
128
|
+
id: "openai",
|
|
129
|
+
name: "OpenAI",
|
|
130
|
+
remote: true,
|
|
131
|
+
defaultModel: "gpt-4.1-mini",
|
|
132
|
+
keyEnvVar: "OPENAI_API_KEY",
|
|
133
|
+
async available(config) {
|
|
134
|
+
return keyAvailability(openaiProvider, providerBase(config, "https://api.openai.com"));
|
|
135
|
+
},
|
|
136
|
+
async complete(config, req) {
|
|
137
|
+
const key = envKey(openaiProvider.keyEnvVar);
|
|
138
|
+
if (!key) throw new AiError("OPENAI_API_KEY is not set");
|
|
139
|
+
const model = config.model ?? openaiProvider.defaultModel;
|
|
140
|
+
const json = await postJson(
|
|
141
|
+
`${providerBase(config, "https://api.openai.com")}/v1/chat/completions`,
|
|
142
|
+
{ model, max_completion_tokens: req.maxTokens ?? 2e3, messages: [{ role: "system", content: req.system }, { role: "user", content: req.prompt }] },
|
|
143
|
+
{ authorization: `Bearer ${key}` },
|
|
144
|
+
req
|
|
145
|
+
);
|
|
146
|
+
const choices = Array.isArray(json.choices) ? json.choices : [];
|
|
147
|
+
const first = choices[0] ?? {};
|
|
148
|
+
const message = first.message ?? {};
|
|
149
|
+
const usage = json.usage ?? {};
|
|
150
|
+
return { text: str(message.content), model: str(json.model) || model, provider: "openai", usage: { inputTokens: usage.prompt_tokens, outputTokens: usage.completion_tokens } };
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var googleProvider = {
|
|
154
|
+
id: "google",
|
|
155
|
+
name: "Google (Gemini)",
|
|
156
|
+
remote: true,
|
|
157
|
+
defaultModel: "gemini-2.5-flash",
|
|
158
|
+
keyEnvVar: "GEMINI_API_KEY",
|
|
159
|
+
async available(config) {
|
|
160
|
+
const key = envKey("GEMINI_API_KEY") ?? envKey("GOOGLE_API_KEY");
|
|
161
|
+
const endpoint = providerBase(config, "https://generativelanguage.googleapis.com");
|
|
162
|
+
return key ? { ok: true, endpoint, keyEnvVar: "GEMINI_API_KEY" } : { ok: false, reason: "GEMINI_API_KEY (or GOOGLE_API_KEY) is not set", endpoint, keyEnvVar: "GEMINI_API_KEY" };
|
|
163
|
+
},
|
|
164
|
+
async complete(config, req) {
|
|
165
|
+
const key = envKey("GEMINI_API_KEY") ?? envKey("GOOGLE_API_KEY");
|
|
166
|
+
if (!key) throw new AiError("GEMINI_API_KEY is not set");
|
|
167
|
+
const model = config.model ?? googleProvider.defaultModel;
|
|
168
|
+
const json = await postJson(
|
|
169
|
+
`${providerBase(config, "https://generativelanguage.googleapis.com")}/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
170
|
+
{ systemInstruction: { parts: [{ text: req.system }] }, contents: [{ role: "user", parts: [{ text: req.prompt }] }], generationConfig: { maxOutputTokens: req.maxTokens ?? 2e3 } },
|
|
171
|
+
{ "x-goog-api-key": key },
|
|
172
|
+
req
|
|
173
|
+
);
|
|
174
|
+
const candidates = Array.isArray(json.candidates) ? json.candidates : [];
|
|
175
|
+
const parts = candidates[0]?.content?.parts;
|
|
176
|
+
const text = Array.isArray(parts) ? parts.map((p) => str(p.text)).join("") : "";
|
|
177
|
+
return { text, model, provider: "google" };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var ollamaProvider = {
|
|
181
|
+
id: "ollama",
|
|
182
|
+
name: "Ollama (local)",
|
|
183
|
+
remote: false,
|
|
184
|
+
defaultModel: "llama3.1",
|
|
185
|
+
async available(config) {
|
|
186
|
+
const endpoint = ollamaEndpoint(config);
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(`${endpoint}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
189
|
+
if (!res.ok) return { ok: false, reason: `Ollama responded ${res.status}`, endpoint };
|
|
190
|
+
const json = await res.json();
|
|
191
|
+
const models = (json.models ?? []).map((m) => m.name).filter(Boolean);
|
|
192
|
+
return models.length ? { ok: true, endpoint } : { ok: false, reason: "Ollama is running but has no models pulled (try `ollama pull llama3.1`)", endpoint };
|
|
193
|
+
} catch {
|
|
194
|
+
return { ok: false, reason: "Ollama is not reachable (start it with `ollama serve`)", endpoint };
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async complete(config, req) {
|
|
198
|
+
const endpoint = ollamaEndpoint(config);
|
|
199
|
+
const model = config.model ?? ollamaProvider.defaultModel;
|
|
200
|
+
const json = await postJson(
|
|
201
|
+
`${endpoint}/api/chat`,
|
|
202
|
+
{ model, stream: false, options: { num_predict: req.maxTokens ?? 2e3 }, messages: [{ role: "system", content: req.system }, { role: "user", content: req.prompt }] },
|
|
203
|
+
{},
|
|
204
|
+
req
|
|
205
|
+
);
|
|
206
|
+
const message = json.message ?? {};
|
|
207
|
+
return { text: str(message.content), model, provider: "ollama" };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function ollamaEndpoint(config) {
|
|
211
|
+
const env = process.env.OLLAMA_HOST?.trim();
|
|
212
|
+
const fromEnv = env ? /^https?:\/\//i.test(env) ? env : `http://${env}` : void 0;
|
|
213
|
+
return providerBase(config, fromEnv ?? "http://127.0.0.1:11434").replace(/\/+$/, "");
|
|
214
|
+
}
|
|
215
|
+
function isRemote(provider, config) {
|
|
216
|
+
if (provider.id === "ollama") return !isLoopbackUrl(ollamaEndpoint(config));
|
|
217
|
+
return provider.remote;
|
|
218
|
+
}
|
|
219
|
+
var PROVIDERS = [anthropicProvider, openaiProvider, googleProvider, ollamaProvider];
|
|
220
|
+
function getProvider(id) {
|
|
221
|
+
const p = PROVIDERS.find((x) => x.id === id);
|
|
222
|
+
if (!p) throw new AiError(`Unknown AI provider: ${id}`, `Supported: ${PROVIDERS.map((x) => x.id).join(", ")}`);
|
|
223
|
+
return p;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/services/ai.ts
|
|
227
|
+
var SUGGESTIONS_FILE = "ai-suggestions.md";
|
|
228
|
+
var TrustedAi = z2.object({ baseUrl: z2.string().optional(), consent: z2.boolean().optional() });
|
|
229
|
+
function userConfigPath(env = process.env, platform = process.platform) {
|
|
230
|
+
if (platform === "win32") {
|
|
231
|
+
const base2 = env.APPDATA?.trim() || path.join(os.homedir(), "AppData", "Roaming");
|
|
232
|
+
return path.win32.join(base2, "athena", "config.json");
|
|
233
|
+
}
|
|
234
|
+
const base = env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
|
|
235
|
+
return path.join(base, "athena", "config.json");
|
|
236
|
+
}
|
|
237
|
+
async function readUserAi() {
|
|
238
|
+
const text = await readTextIfExists(userConfigPath()).catch(() => null);
|
|
239
|
+
if (!text) return {};
|
|
240
|
+
try {
|
|
241
|
+
const parsed = TrustedAi.safeParse(JSON.parse(text)?.ai ?? {});
|
|
242
|
+
return parsed.success ? parsed.data : {};
|
|
243
|
+
} catch {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function envConsent() {
|
|
248
|
+
const v = process.env.ATHENA_AI_CONSENT?.trim().toLowerCase();
|
|
249
|
+
if (!v) return void 0;
|
|
250
|
+
if (v === "1" || v === "true") return true;
|
|
251
|
+
if (v === "0" || v === "false") return false;
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
function validBaseUrl(raw) {
|
|
255
|
+
const v = raw?.trim();
|
|
256
|
+
if (!v) return void 0;
|
|
257
|
+
try {
|
|
258
|
+
const u = new URL(v);
|
|
259
|
+
return u.protocol === "http:" || u.protocol === "https:" ? v.replace(/\/+$/, "") : void 0;
|
|
260
|
+
} catch {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
var MOVE_AI_HINT = 'Set ATHENA_AI_BASE_URL / ATHENA_AI_CONSENT=1, or put them under "ai" in .athena/local.json (gitignored) or ~/.config/athena/config.json.';
|
|
265
|
+
async function committedAiWarning(root) {
|
|
266
|
+
const { config } = await loadConfig(root);
|
|
267
|
+
const ignored = [config.ai?.baseUrl !== void 0 ? "ai.baseUrl" : "", config.ai?.consent !== void 0 ? "ai.consent" : ""].filter(Boolean);
|
|
268
|
+
if (!ignored.length) return void 0;
|
|
269
|
+
return `Ignoring ${ignored.join(" and ")} in .athena/config.json: that file is committed, so anyone who can push to the repository could redirect your API key and knowledge. ${MOVE_AI_HINT}`;
|
|
270
|
+
}
|
|
271
|
+
async function resolveAiConfig(root) {
|
|
272
|
+
const { config } = await loadConfig(root);
|
|
273
|
+
const warnings = [];
|
|
274
|
+
const committedWarning = await committedAiWarning(root);
|
|
275
|
+
if (committedWarning) warnings.push(committedWarning);
|
|
276
|
+
const { baseUrl: _ignoredUrl, consent: _ignoredConsent, ...committed } = config.ai ?? {};
|
|
277
|
+
const local = (await readLocalConfig(root)).ai ?? {};
|
|
278
|
+
const user = await readUserAi();
|
|
279
|
+
let baseUrl;
|
|
280
|
+
let baseUrlSource;
|
|
281
|
+
const candidates = [
|
|
282
|
+
["env", process.env.ATHENA_AI_BASE_URL, "ATHENA_AI_BASE_URL"],
|
|
283
|
+
["local", local.baseUrl, ".athena/local.json"],
|
|
284
|
+
["user", user.baseUrl, userConfigPath()]
|
|
285
|
+
];
|
|
286
|
+
for (const [source, raw, where] of candidates) {
|
|
287
|
+
if (!raw?.trim()) continue;
|
|
288
|
+
const url = validBaseUrl(raw);
|
|
289
|
+
if (!url) {
|
|
290
|
+
warnings.push(`Ignoring the AI base URL from ${where}: it must be an http(s) URL.`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
baseUrl = url;
|
|
294
|
+
baseUrlSource = source;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
let consent = false;
|
|
298
|
+
let consentSource;
|
|
299
|
+
for (const [source, value] of [["env", envConsent()], ["local", local.consent], ["user", user.consent]]) {
|
|
300
|
+
if (value === void 0) continue;
|
|
301
|
+
consent = value;
|
|
302
|
+
consentSource = value ? source : void 0;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
const cfg = AiConfig.parse({ ...committed, consent, ...baseUrl ? { baseUrl, baseUrlSource } : {} });
|
|
306
|
+
return { config: cfg, consentSource, warnings };
|
|
307
|
+
}
|
|
308
|
+
async function aiConfig(root) {
|
|
309
|
+
return (await resolveAiConfig(root)).config;
|
|
310
|
+
}
|
|
311
|
+
async function aiStatus(root) {
|
|
312
|
+
const { config: cfg, consentSource, warnings } = await resolveAiConfig(root);
|
|
313
|
+
const providers = [];
|
|
314
|
+
for (const p of PROVIDERS) providers.push({ id: p.id, name: p.name, remote: isRemote(p, cfg), defaultModel: p.defaultModel, keyEnvVar: p.keyEnvVar, availability: await p.available(cfg), selected: p.id === cfg.provider });
|
|
315
|
+
return { configured: cfg, providers, consent: cfg.consent, consentSource, warnings };
|
|
316
|
+
}
|
|
317
|
+
async function buildEnrichPayload(root, opts = {}) {
|
|
318
|
+
const { config: cfg, warnings } = await resolveAiConfig(root);
|
|
319
|
+
const provider = getProvider(cfg.provider);
|
|
320
|
+
const wanted = opts.docs?.length ? opts.docs : ["project", "architecture", "database", "api"];
|
|
321
|
+
const parts = [];
|
|
322
|
+
const included = [];
|
|
323
|
+
for (const id of wanted) {
|
|
324
|
+
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
325
|
+
if (!doc) throw new AthenaError(`Unknown document: ${id}`, `Available: ${KNOWLEDGE_DOCS.map((d) => d.id).join(", ")}`);
|
|
326
|
+
const text = await readTextIfExists(path.join(athenaDir(root), doc.file));
|
|
327
|
+
if (!text) continue;
|
|
328
|
+
included.push(doc.file);
|
|
329
|
+
parts.push(`--- ${doc.file} ---
|
|
330
|
+
${text.replace(/<!--[\s\S]*?-->/g, "").trim()}`);
|
|
331
|
+
}
|
|
332
|
+
if (!included.length) throw new AthenaError("No knowledge documents to send.", "Run `athena init` first.", 3);
|
|
333
|
+
let content = redact(parts.join("\n\n"));
|
|
334
|
+
if (content.length > cfg.maxChars) content = `${content.slice(0, cfg.maxChars)}
|
|
335
|
+
|
|
336
|
+
[truncated to ${cfg.maxChars} characters]`;
|
|
337
|
+
const leftover = scanText(content);
|
|
338
|
+
if (leftover.length) throw new AthenaError("Refusing to send: the knowledge still contains values that look like secrets.", "Fix them in .athena/*.md first.");
|
|
339
|
+
const availability = await provider.available(cfg);
|
|
340
|
+
return { content, chars: content.length, documents: included, provider, endpoint: availability.endpoint, remote: isRemote(provider, cfg), model: cfg.model ?? provider.defaultModel, warnings };
|
|
341
|
+
}
|
|
342
|
+
var SYSTEM = [
|
|
343
|
+
"You help maintain a project knowledge base for AI coding agents.",
|
|
344
|
+
"You will be given generated documentation about a software project.",
|
|
345
|
+
"Suggest concise, high-value additions a developer might record: the project purpose, architectural decisions worth documenting, risky areas, and candidate project rules.",
|
|
346
|
+
"Rules: never invent facts that are not supported by the provided material; where you are unsure, say what is unknown and what would confirm it; do not restate the documents; no code.",
|
|
347
|
+
'Output GitHub-flavoured Markdown with these sections: "## Suggested project summary", "## Questions Athena could not answer", "## Candidate rules" (as a bullet list).'
|
|
348
|
+
].join(" ");
|
|
349
|
+
async function enrich(root, opts = {}) {
|
|
350
|
+
const cfg = await aiConfig(root);
|
|
351
|
+
const payload = await buildEnrichPayload(root, { docs: opts.docs });
|
|
352
|
+
const availability = await payload.provider.available(cfg);
|
|
353
|
+
if (!availability.ok) throw new AthenaError(`${payload.provider.name} is not available: ${availability.reason}`, payload.provider.keyEnvVar ? `Set ${payload.provider.keyEnvVar}, or choose another provider in .athena/config.json.` : void 0);
|
|
354
|
+
if (payload.remote && !cfg.consent && !opts.consent) {
|
|
355
|
+
throw new AthenaError(
|
|
356
|
+
`Sending project knowledge to ${payload.endpoint} requires consent.`,
|
|
357
|
+
'Re-run with `athena ai enrich --consent`, set ATHENA_AI_CONSENT=1, or set "ai": { "consent": true } in .athena/local.json (machine-local, gitignored). Use a local Ollama to keep everything on this machine.'
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
let result;
|
|
361
|
+
try {
|
|
362
|
+
result = await payload.provider.complete(cfg, { system: SYSTEM, prompt: payload.content, maxTokens: 2e3, signal: opts.signal });
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err instanceof AiError) throw new AthenaError(err.message, err.hint);
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
const body = redact(result.text.trim());
|
|
368
|
+
if (!body) throw new AthenaError("The provider returned an empty response.");
|
|
369
|
+
const markdown = [
|
|
370
|
+
"# AI suggestions (INFERRED \u2014 not project knowledge)",
|
|
371
|
+
"",
|
|
372
|
+
`> Generated ${(/* @__PURE__ */ new Date()).toISOString()} by ${result.provider} (${result.model}) from ${payload.documents.join(", ")}.`,
|
|
373
|
+
">",
|
|
374
|
+
"> **Status: INFERRED.** Nothing here was verified against the code. Move anything useful into a document's Developer Notes or `rules.md` yourself; Athena will not do it for you.",
|
|
375
|
+
"",
|
|
376
|
+
body,
|
|
377
|
+
""
|
|
378
|
+
].join("\n");
|
|
379
|
+
const file = path.join(athenaDir(root), SUGGESTIONS_FILE);
|
|
380
|
+
await writeFileAtomic(file, markdown);
|
|
381
|
+
return { markdown, file: SUGGESTIONS_FILE, provider: result.provider, model: result.model, chars: payload.chars, documents: payload.documents };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
committedAiWarning,
|
|
386
|
+
aiStatus,
|
|
387
|
+
buildEnrichPayload,
|
|
388
|
+
enrich
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=chunk-RXNRMIZP.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
c,
|
|
4
|
+
dim,
|
|
5
|
+
isJson,
|
|
6
|
+
json,
|
|
7
|
+
line,
|
|
8
|
+
ok
|
|
9
|
+
} from "./chunk-Z6QWMJLK.js";
|
|
10
|
+
import {
|
|
11
|
+
requireProjectRoot
|
|
12
|
+
} from "./chunk-XFN7UZCS.js";
|
|
13
|
+
import {
|
|
14
|
+
ADAPTERS
|
|
15
|
+
} from "./chunk-2RS6TA3D.js";
|
|
16
|
+
import {
|
|
17
|
+
athenaDir
|
|
18
|
+
} from "./chunk-2DY4PUR4.js";
|
|
19
|
+
import {
|
|
20
|
+
AthenaError
|
|
21
|
+
} from "./chunk-AD4OZCU2.js";
|
|
22
|
+
|
|
23
|
+
// src/cli/commands/clean.ts
|
|
24
|
+
import { promises as fs } from "fs";
|
|
25
|
+
import { createInterface } from "readline/promises";
|
|
26
|
+
async function confirm(question) {
|
|
27
|
+
if (!process.stdin.isTTY) return false;
|
|
28
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
try {
|
|
30
|
+
const answer = await rl.question(`${question} ${dim("[y/N]")} `);
|
|
31
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
32
|
+
} finally {
|
|
33
|
+
rl.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function cleanCommand(opts) {
|
|
37
|
+
const root = await requireProjectRoot(opts);
|
|
38
|
+
if (!opts.yes) {
|
|
39
|
+
if (!process.stdin.isTTY) throw new AthenaError("Refusing to delete .athena/ without confirmation.", "Re-run with --yes in non-interactive environments.");
|
|
40
|
+
line(`This removes ${c.bold(".athena/")} (including rules.md and your Developer Notes)${opts.keepAgents ? "" : " and Athena blocks/files in agent configurations"}.`);
|
|
41
|
+
line(dim("If .athena/ is committed to Git you can restore it from history."));
|
|
42
|
+
if (!await confirm("Continue?")) {
|
|
43
|
+
line("Cancelled.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const touched = [];
|
|
48
|
+
if (!opts.keepAgents) for (const a of ADAPTERS) touched.push(...await a.remove(root));
|
|
49
|
+
await fs.rm(athenaDir(root), { recursive: true, force: true });
|
|
50
|
+
if (isJson()) json({ ok: true, removed: [".athena/", ...touched] });
|
|
51
|
+
else {
|
|
52
|
+
ok("Removed .athena/");
|
|
53
|
+
for (const t of touched) ok(`Removed Athena integration from ${t}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
confirm,
|
|
59
|
+
cleanCommand
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=chunk-S7T6VX56.js.map
|