@curiousnerd/keel 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/data/capability-buckets.json +15 -0
  4. package/dist/analyze/docDrift.d.ts +9 -0
  5. package/dist/analyze/docDrift.js +116 -0
  6. package/dist/analyze/docDrift.js.map +1 -0
  7. package/dist/analyze/drift.d.ts +4 -0
  8. package/dist/analyze/drift.js +134 -0
  9. package/dist/analyze/drift.js.map +1 -0
  10. package/dist/analyze/duplication.d.ts +7 -0
  11. package/dist/analyze/duplication.js +46 -0
  12. package/dist/analyze/duplication.js.map +1 -0
  13. package/dist/analyze/index.d.ts +10 -0
  14. package/dist/analyze/index.js +28 -0
  15. package/dist/analyze/index.js.map +1 -0
  16. package/dist/analyze/libConflicts.d.ts +9 -0
  17. package/dist/analyze/libConflicts.js +36 -0
  18. package/dist/analyze/libConflicts.js.map +1 -0
  19. package/dist/analyze/nearDup.d.ts +11 -0
  20. package/dist/analyze/nearDup.js +67 -0
  21. package/dist/analyze/nearDup.js.map +1 -0
  22. package/dist/analyze/score.d.ts +6 -0
  23. package/dist/analyze/score.js +39 -0
  24. package/dist/analyze/score.js.map +1 -0
  25. package/dist/analyze/shared.d.ts +19 -0
  26. package/dist/analyze/shared.js +53 -0
  27. package/dist/analyze/shared.js.map +1 -0
  28. package/dist/cache/hashCache.d.ts +19 -0
  29. package/dist/cache/hashCache.js +49 -0
  30. package/dist/cache/hashCache.js.map +1 -0
  31. package/dist/claims/parseBlock.d.ts +4 -0
  32. package/dist/claims/parseBlock.js +66 -0
  33. package/dist/claims/parseBlock.js.map +1 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +136 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config.d.ts +32 -0
  38. package/dist/config.js +37 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/extract/imports.d.ts +12 -0
  41. package/dist/extract/imports.js +74 -0
  42. package/dist/extract/imports.js.map +1 -0
  43. package/dist/extract/index.d.ts +24 -0
  44. package/dist/extract/index.js +117 -0
  45. package/dist/extract/index.js.map +1 -0
  46. package/dist/extract/language.d.ts +3 -0
  47. package/dist/extract/language.js +13 -0
  48. package/dist/extract/language.js.map +1 -0
  49. package/dist/extract/naming.d.ts +11 -0
  50. package/dist/extract/naming.js +57 -0
  51. package/dist/extract/naming.js.map +1 -0
  52. package/dist/extract/packageJson.d.ts +3 -0
  53. package/dist/extract/packageJson.js +43 -0
  54. package/dist/extract/packageJson.js.map +1 -0
  55. package/dist/extract/python.d.ts +11 -0
  56. package/dist/extract/python.js +244 -0
  57. package/dist/extract/python.js.map +1 -0
  58. package/dist/extract/scan.d.ts +12 -0
  59. package/dist/extract/scan.js +16 -0
  60. package/dist/extract/scan.js.map +1 -0
  61. package/dist/extract/symbols.d.ts +9 -0
  62. package/dist/extract/symbols.js +120 -0
  63. package/dist/extract/symbols.js.map +1 -0
  64. package/dist/extract/walk.d.ts +10 -0
  65. package/dist/extract/walk.js +115 -0
  66. package/dist/extract/walk.js.map +1 -0
  67. package/dist/llm/cache.d.ts +17 -0
  68. package/dist/llm/cache.js +50 -0
  69. package/dist/llm/cache.js.map +1 -0
  70. package/dist/llm/claimsFromDocs.d.ts +16 -0
  71. package/dist/llm/claimsFromDocs.js +95 -0
  72. package/dist/llm/claimsFromDocs.js.map +1 -0
  73. package/dist/llm/explain.d.ts +10 -0
  74. package/dist/llm/explain.js +63 -0
  75. package/dist/llm/explain.js.map +1 -0
  76. package/dist/llm/improve.d.ts +9 -0
  77. package/dist/llm/improve.js +37 -0
  78. package/dist/llm/improve.js.map +1 -0
  79. package/dist/llm/provider.d.ts +24 -0
  80. package/dist/llm/provider.js +210 -0
  81. package/dist/llm/provider.js.map +1 -0
  82. package/dist/mcp/server.d.ts +7 -0
  83. package/dist/mcp/server.js +43 -0
  84. package/dist/mcp/server.js.map +1 -0
  85. package/dist/mcp/tools.d.ts +9 -0
  86. package/dist/mcp/tools.js +173 -0
  87. package/dist/mcp/tools.js.map +1 -0
  88. package/dist/report/json.d.ts +3 -0
  89. package/dist/report/json.js +5 -0
  90. package/dist/report/json.js.map +1 -0
  91. package/dist/report/markdown.d.ts +9 -0
  92. package/dist/report/markdown.js +97 -0
  93. package/dist/report/markdown.js.map +1 -0
  94. package/dist/report/text.d.ts +11 -0
  95. package/dist/report/text.js +76 -0
  96. package/dist/report/text.js.map +1 -0
  97. package/dist/suppress.d.ts +22 -0
  98. package/dist/suppress.js +80 -0
  99. package/dist/suppress.js.map +1 -0
  100. package/dist/types.d.ts +144 -0
  101. package/dist/types.js +9 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/util/fingerprint.d.ts +12 -0
  104. package/dist/util/fingerprint.js +60 -0
  105. package/dist/util/fingerprint.js.map +1 -0
  106. package/dist/util/hash.d.ts +4 -0
  107. package/dist/util/hash.js +15 -0
  108. package/dist/util/hash.js.map +1 -0
  109. package/package.json +58 -0
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** Per-file cap so a giant README can't blow up the prompt. */
4
+ const MAX_CHARS_PER_DOC = 6000;
5
+ /** Map an extracted JSON key to the claim key the drift engine understands. */
6
+ const KEY_MAP = {
7
+ packagemanager: "package manager",
8
+ database: "database",
9
+ db: "database",
10
+ naming: "naming",
11
+ };
12
+ function gatherDocs(root, config) {
13
+ const names = [...new Set([...config.claims.sources, "README.md"])];
14
+ const parts = [];
15
+ for (const name of names) {
16
+ const path = join(root, name);
17
+ if (!existsSync(path))
18
+ continue;
19
+ try {
20
+ const text = readFileSync(path, "utf8").slice(0, MAX_CHARS_PER_DOC);
21
+ if (text.trim())
22
+ parts.push(`### ${name}\n${text}`);
23
+ }
24
+ catch {
25
+ // unreadable — skip
26
+ }
27
+ }
28
+ return parts.join("\n\n");
29
+ }
30
+ function buildPrompt(docs) {
31
+ return [
32
+ "From the project documentation below, extract the technology choices the project SAYS it uses.",
33
+ "Return ONLY a JSON object containing any of these keys that are clearly stated (omit the rest):",
34
+ '{"validation":"","database":"","orm":"","http":"","state":"","test":"","packageManager":"","naming":""}',
35
+ 'Use the canonical tool name as the value (e.g. "Zod", "PostgreSQL", "Prisma", "pnpm", "camelCase").',
36
+ "Do not guess or infer from filenames — only include what the prose explicitly states. No markdown.",
37
+ "",
38
+ "Documentation:",
39
+ docs,
40
+ ].join("\n");
41
+ }
42
+ function parseClaims(raw, source) {
43
+ const start = raw.indexOf("{");
44
+ const end = raw.lastIndexOf("}");
45
+ if (start === -1 || end <= start)
46
+ return [];
47
+ let obj;
48
+ try {
49
+ obj = JSON.parse(raw.slice(start, end + 1));
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ const claims = [];
55
+ for (const [rawKey, rawValue] of Object.entries(obj)) {
56
+ if (typeof rawValue !== "string" || rawValue.trim() === "")
57
+ continue;
58
+ const key = KEY_MAP[rawKey.toLowerCase()] ?? rawKey.toLowerCase();
59
+ claims.push({ key, value: rawValue.trim(), source, line: 1 });
60
+ }
61
+ return claims;
62
+ }
63
+ /**
64
+ * Tier 2 of doc/knowledge drift: use the LLM to extract checkable stack claims
65
+ * from freeform documentation prose. The LLM only turns English into claims —
66
+ * the deterministic drift engine still does the verifying. Returns [] on any
67
+ * failure or when no provider is available. Cached by prompt.
68
+ */
69
+ export async function extractClaimsFromDocs(root, config, provider, cache) {
70
+ if (provider.name === "off")
71
+ return [];
72
+ const docs = gatherDocs(root, config);
73
+ if (!docs.trim())
74
+ return [];
75
+ const prompt = buildPrompt(docs);
76
+ let raw = cache.get(provider.name, prompt);
77
+ if (raw === undefined) {
78
+ const response = await provider.complete(prompt);
79
+ if (response === null)
80
+ return [];
81
+ raw = response;
82
+ cache.set(provider.name, prompt, raw);
83
+ }
84
+ return parseClaims(raw, "docs (inferred)");
85
+ }
86
+ /**
87
+ * Add inferred stack claims that the structured block didn't already cover.
88
+ * Structured claims always win (a hand-written `## Stack` beats prose inference).
89
+ */
90
+ export function mergeInferredClaims(existing, inferred) {
91
+ const haveKeys = new Set(existing.map((c) => c.key.toLowerCase()));
92
+ const added = inferred.filter((c) => !haveKeys.has(c.key.toLowerCase()));
93
+ return [...existing, ...added];
94
+ }
95
+ //# sourceMappingURL=claimsFromDocs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claimsFromDocs.js","sourceRoot":"","sources":["../../src/llm/claimsFromDocs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Finding } from "../types.js";
2
+ import type { LLMProvider } from "./provider.js";
3
+ import type { LLMCache } from "./cache.js";
4
+ /**
5
+ * Attach plain-language `explanation`s to findings via one batched LLM call.
6
+ * Advisory only — never changes the score. Cached by prompt; on any failure
7
+ * the findings are left untouched (templated `detail` remains the fallback).
8
+ * Returns the number of findings that got an explanation.
9
+ */
10
+ export declare function explainFindings(findings: Finding[], provider: LLMProvider, cache: LLMCache): Promise<number>;
@@ -0,0 +1,63 @@
1
+ /** Build one batched prompt covering all findings (explanations are short). */
2
+ function buildPrompt(findings) {
3
+ const lines = findings.map((f, i) => `${i + 1}. [${f.kind.toUpperCase()}] ${f.title}`);
4
+ return [
5
+ "You are explaining code-quality findings from a static-analysis tool called keel to a developer.",
6
+ "For each finding, write ONE short, plain-language sentence: what it means and what to do about it.",
7
+ "Be concrete and specific. No preamble, no restating the finding verbatim.",
8
+ 'Return ONLY a JSON array, one object per finding: [{"i": 1, "explanation": "..."}]. No markdown.',
9
+ "",
10
+ "Findings:",
11
+ ...lines,
12
+ ].join("\n");
13
+ }
14
+ /** Extract the JSON array from a model response that may have stray text around it. */
15
+ function parseExplanations(raw) {
16
+ const out = new Map();
17
+ const start = raw.indexOf("[");
18
+ const end = raw.lastIndexOf("]");
19
+ if (start === -1 || end <= start)
20
+ return out;
21
+ try {
22
+ const parsed = JSON.parse(raw.slice(start, end + 1));
23
+ for (const item of parsed) {
24
+ if (typeof item.i === "number" && typeof item.explanation === "string") {
25
+ out.set(item.i, item.explanation.trim());
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ // unparseable — leave empty, caller degrades to templated output
31
+ }
32
+ return out;
33
+ }
34
+ /**
35
+ * Attach plain-language `explanation`s to findings via one batched LLM call.
36
+ * Advisory only — never changes the score. Cached by prompt; on any failure
37
+ * the findings are left untouched (templated `detail` remains the fallback).
38
+ * Returns the number of findings that got an explanation.
39
+ */
40
+ export async function explainFindings(findings, provider, cache) {
41
+ if (findings.length === 0 || provider.name === "off")
42
+ return 0;
43
+ const prompt = buildPrompt(findings);
44
+ let raw = cache.get(provider.name, prompt);
45
+ if (raw === undefined) {
46
+ const response = await provider.complete(prompt);
47
+ if (response === null)
48
+ return 0;
49
+ raw = response;
50
+ cache.set(provider.name, prompt, raw);
51
+ }
52
+ const byIndex = parseExplanations(raw);
53
+ let attached = 0;
54
+ findings.forEach((finding, i) => {
55
+ const explanation = byIndex.get(i + 1);
56
+ if (explanation) {
57
+ finding.explanation = explanation;
58
+ attached += 1;
59
+ }
60
+ });
61
+ return attached;
62
+ }
63
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/llm/explain.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,SAAS,WAAW,CAAC,QAAmB;IACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACvF,OAAO;QACL,kGAAkG;QAClG,oGAAoG;QACpG,2EAA2E;QAC3E,kGAAkG;QAClG,EAAE;QACF,WAAW;QACX,GAAG,KAAK;KACT,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,uFAAuF;AACvF,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,GAAG,CAAC;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAgD,CAAC;QACpG,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,OAAO,IAAI,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;gBACvE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAmB,EAAE,QAAqB,EAAE,KAAe;IAC/F,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,CAAC,CAAC;IAE/D,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,CAAC,CAAC;QAChC,GAAG,GAAG,QAAQ,CAAC;QACf,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE;QAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACvC,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;YAClC,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Report } from "../types.js";
2
+ import type { LLMProvider } from "./provider.js";
3
+ import type { LLMCache } from "./cache.js";
4
+ /**
5
+ * Generate a plain-language "how to raise your score" plan from the findings.
6
+ * Advisory only (never changes the score). Cached; returns null on any failure
7
+ * so callers fall back to the deterministic improvementSummary.
8
+ */
9
+ export declare function suggestImprovements(report: Report, provider: LLMProvider, cache: LLMCache): Promise<string | null>;
@@ -0,0 +1,37 @@
1
+ import { sortFindings } from "../report/text.js";
2
+ /** How many findings to feed the model — bounded to keep the prompt small. */
3
+ const MAX_FINDINGS_IN_PROMPT = 40;
4
+ function buildPrompt(report) {
5
+ const top = sortFindings(report.findings).slice(0, MAX_FINDINGS_IN_PROMPT);
6
+ const lines = top.map((f) => `- [${f.kind}] ${f.title}`);
7
+ return [
8
+ `A static-analysis tool ("keel") scored this codebase ${report.score}/100.`,
9
+ `Penalties — drift: ${report.breakdown.drift}, library conflicts: ${report.breakdown.conflict}, duplication: ${report.breakdown.duplication}.`,
10
+ "Write a short, prioritized, actionable plan (most impactful first) for raising this score.",
11
+ "Be specific to the findings below; group related ones; suggest concrete refactors. Markdown bullets only, max ~8 items.",
12
+ "Respond with ONLY the plan itself — no preamble, no meta-commentary about your process.",
13
+ "",
14
+ "Findings:",
15
+ ...lines,
16
+ ].join("\n");
17
+ }
18
+ /**
19
+ * Generate a plain-language "how to raise your score" plan from the findings.
20
+ * Advisory only (never changes the score). Cached; returns null on any failure
21
+ * so callers fall back to the deterministic improvementSummary.
22
+ */
23
+ export async function suggestImprovements(report, provider, cache) {
24
+ if (provider.name === "off" || report.findings.length === 0)
25
+ return null;
26
+ const prompt = buildPrompt(report);
27
+ let raw = cache.get(provider.name, prompt);
28
+ if (raw === undefined) {
29
+ const response = await provider.complete(prompt);
30
+ if (response === null)
31
+ return null;
32
+ raw = response;
33
+ cache.set(provider.name, prompt, raw);
34
+ }
35
+ return raw.trim() || null;
36
+ }
37
+ //# sourceMappingURL=improve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"improve.js","sourceRoot":"","sources":["../../src/llm/improve.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,8EAA8E;AAC9E,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAElC,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC;IAC3E,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,wDAAwD,MAAM,CAAC,KAAK,OAAO;QAC3E,sBAAsB,MAAM,CAAC,SAAS,CAAC,KAAK,wBAAwB,MAAM,CAAC,SAAS,CAAC,QAAQ,kBAAkB,MAAM,CAAC,SAAS,CAAC,WAAW,GAAG;QAC9I,4FAA4F;QAC5F,yHAAyH;QACzH,yFAAyF;QACzF,EAAE;QACF,WAAW;QACX,GAAG,KAAK;KACT,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAc,EAAE,QAAqB,EAAE,KAAe;IAC9F,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzE,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACnC,GAAG,GAAG,QAAQ,CAAC;QACf,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { KeelConfig } from "../config.js";
2
+ /**
3
+ * The single seam every LLM feature goes through. An implementation either
4
+ * shells out to an already-authenticated agent CLI, calls a hosted API, or is
5
+ * the null provider. The deterministic core never depends on any of this.
6
+ */
7
+ export interface LLMProvider {
8
+ /** Short label printed in the run summary, e.g. "claude-cli". */
9
+ readonly name: string;
10
+ /** Return the model's text for `prompt`, or null on any failure (fail soft). */
11
+ complete(prompt: string): Promise<string | null>;
12
+ }
13
+ /** No LLM available — callers fall back to templated output. */
14
+ export declare class NullProvider implements LLMProvider {
15
+ readonly name = "off";
16
+ complete(): Promise<string | null>;
17
+ }
18
+ /**
19
+ * Resolve which provider to use. CLI-first by default (the audience already has
20
+ * one authenticated, so it's zero-setup and zero marginal cost), with the
21
+ * hosted API as an opt-in escape hatch. Returns NullProvider when nothing is
22
+ * available so callers degrade to templated output rather than failing.
23
+ */
24
+ export declare function resolveProvider(config: KeelConfig): Promise<LLMProvider>;
@@ -0,0 +1,210 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /** Default request size for hosted-API calls (explanations are short). */
6
+ const API_MAX_TOKENS = 2048;
7
+ /** Hard cap so a hung CLI/API call can't block a run forever. */
8
+ const TIMEOUT_MS = 60_000;
9
+ /** Run a command with an args array (no shell), feeding `stdin`, with a timeout. */
10
+ function runProcess(cmd, args, stdin = "") {
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
13
+ let stdout = "";
14
+ let stderr = "";
15
+ const timer = setTimeout(() => child.kill("SIGKILL"), TIMEOUT_MS);
16
+ child.stdout.on("data", (d) => (stdout += d));
17
+ child.stderr.on("data", (d) => (stderr += d));
18
+ child.on("error", (err) => {
19
+ clearTimeout(timer);
20
+ reject(err);
21
+ });
22
+ child.on("close", (code) => {
23
+ clearTimeout(timer);
24
+ resolve({ code, stdout, stderr });
25
+ });
26
+ if (stdin)
27
+ child.stdin.write(stdin);
28
+ child.stdin.end();
29
+ });
30
+ }
31
+ /** Is `bin` on PATH and runnable? (probes `bin --version`). */
32
+ async function cliAvailable(bin) {
33
+ try {
34
+ const { code } = await runProcess(bin, ["--version"]);
35
+ return code === 0;
36
+ }
37
+ catch {
38
+ return false; // ENOENT etc.
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Providers
43
+ // ---------------------------------------------------------------------------
44
+ /** Shells out to Claude Code's headless print mode (`claude -p`). */
45
+ class ClaudeCliProvider {
46
+ model;
47
+ name = "claude-cli";
48
+ constructor(model) {
49
+ this.model = model;
50
+ }
51
+ async complete(prompt) {
52
+ const args = ["-p", "--output-format", "text"];
53
+ if (this.model)
54
+ args.push("--model", this.model);
55
+ try {
56
+ const { code, stdout } = await runProcess("claude", args, prompt);
57
+ if (code !== 0)
58
+ return null;
59
+ const text = stdout.trim();
60
+ return text.length > 0 ? text : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ }
67
+ /** Shells out to Codex's non-interactive mode, reading the clean final message. */
68
+ class CodexCliProvider {
69
+ model;
70
+ name = "codex-cli";
71
+ constructor(model) {
72
+ this.model = model;
73
+ }
74
+ async complete(prompt) {
75
+ const dir = mkdtempSync(join(tmpdir(), "keel-codex-"));
76
+ const outFile = join(dir, "message.txt");
77
+ const args = ["exec", "--output-last-message", outFile, "--color", "never"];
78
+ if (this.model)
79
+ args.push("-m", this.model);
80
+ args.push(prompt);
81
+ try {
82
+ const { code } = await runProcess("codex", args);
83
+ if (code !== 0)
84
+ return null;
85
+ const text = readFileSync(outFile, "utf8").trim();
86
+ return text.length > 0 ? text : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ finally {
92
+ rmSync(dir, { recursive: true, force: true });
93
+ }
94
+ }
95
+ }
96
+ /** Calls the Anthropic Messages API with ANTHROPIC_API_KEY. */
97
+ class AnthropicApiProvider {
98
+ apiKey;
99
+ model;
100
+ name = "anthropic-api";
101
+ constructor(apiKey, model) {
102
+ this.apiKey = apiKey;
103
+ this.model = model;
104
+ }
105
+ async complete(prompt) {
106
+ try {
107
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
108
+ method: "POST",
109
+ headers: {
110
+ "x-api-key": this.apiKey,
111
+ "anthropic-version": "2023-06-01",
112
+ "content-type": "application/json",
113
+ },
114
+ body: JSON.stringify({
115
+ model: this.model || "claude-haiku-4-5",
116
+ max_tokens: API_MAX_TOKENS,
117
+ messages: [{ role: "user", content: prompt }],
118
+ }),
119
+ signal: AbortSignal.timeout(TIMEOUT_MS),
120
+ });
121
+ if (!res.ok)
122
+ return null;
123
+ const data = (await res.json());
124
+ const text = (data.content ?? [])
125
+ .filter((b) => b.type === "text")
126
+ .map((b) => b.text ?? "")
127
+ .join("")
128
+ .trim();
129
+ return text.length > 0 ? text : null;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ }
136
+ /** Calls the OpenAI Chat Completions API with OPENAI_API_KEY (escape hatch). */
137
+ class OpenAiApiProvider {
138
+ apiKey;
139
+ model;
140
+ name = "openai-api";
141
+ constructor(apiKey, model) {
142
+ this.apiKey = apiKey;
143
+ this.model = model;
144
+ }
145
+ async complete(prompt) {
146
+ try {
147
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
148
+ method: "POST",
149
+ headers: { authorization: `Bearer ${this.apiKey}`, "content-type": "application/json" },
150
+ body: JSON.stringify({
151
+ model: this.model || "gpt-4o-mini",
152
+ max_tokens: API_MAX_TOKENS,
153
+ messages: [{ role: "user", content: prompt }],
154
+ }),
155
+ signal: AbortSignal.timeout(TIMEOUT_MS),
156
+ });
157
+ if (!res.ok)
158
+ return null;
159
+ const data = (await res.json());
160
+ const text = data.choices?.[0]?.message?.content?.trim() ?? "";
161
+ return text.length > 0 ? text : null;
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
167
+ }
168
+ /** No LLM available — callers fall back to templated output. */
169
+ export class NullProvider {
170
+ name = "off";
171
+ async complete() {
172
+ return null;
173
+ }
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Resolution
177
+ // ---------------------------------------------------------------------------
178
+ function apiProviderFromEnv(model) {
179
+ if (process.env.ANTHROPIC_API_KEY)
180
+ return new AnthropicApiProvider(process.env.ANTHROPIC_API_KEY, model);
181
+ if (process.env.OPENAI_API_KEY)
182
+ return new OpenAiApiProvider(process.env.OPENAI_API_KEY, model);
183
+ return null;
184
+ }
185
+ /**
186
+ * Resolve which provider to use. CLI-first by default (the audience already has
187
+ * one authenticated, so it's zero-setup and zero marginal cost), with the
188
+ * hosted API as an opt-in escape hatch. Returns NullProvider when nothing is
189
+ * available so callers degrade to templated output rather than failing.
190
+ */
191
+ export async function resolveProvider(config) {
192
+ const { provider, model } = config.llm;
193
+ // Explicit choice.
194
+ if (provider === "claude")
195
+ return new ClaudeCliProvider(model);
196
+ if (provider === "codex")
197
+ return new CodexCliProvider(model);
198
+ if (provider === "api")
199
+ return apiProviderFromEnv(model) ?? new NullProvider();
200
+ if (provider === "off")
201
+ return new NullProvider();
202
+ // Auto: prefer the local CLI (included in the user's subscription) over a
203
+ // metered API key, then fall back to the API, then to null.
204
+ if (await cliAvailable("claude"))
205
+ return new ClaudeCliProvider(model);
206
+ if (await cliAvailable("codex"))
207
+ return new CodexCliProvider(model);
208
+ return apiProviderFromEnv(model) ?? new NullProvider();
209
+ }
210
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Run keel as an MCP server over stdio. The agent calls these tools *before* it
3
+ * writes, so duplication/drift is prevented at generation time, not caught
4
+ * after. Every answer is derived from freshly-extracted deterministic Facts
5
+ * (the file-hash cache makes per-call re-extraction near-instant) — no LLM.
6
+ */
7
+ export declare function startMcpServer(root: string): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { loadConfig } from "../config.js";
5
+ import { loadFacts } from "../extract/scan.js";
6
+ import { parseClaims } from "../claims/parseBlock.js";
7
+ import { loadCapabilityBuckets } from "../analyze/shared.js";
8
+ import { checkBeforeWrite, getConventions, reportDrift } from "./tools.js";
9
+ const text = (s) => ({ content: [{ type: "text", text: s }] });
10
+ /**
11
+ * Run keel as an MCP server over stdio. The agent calls these tools *before* it
12
+ * writes, so duplication/drift is prevented at generation time, not caught
13
+ * after. Every answer is derived from freshly-extracted deterministic Facts
14
+ * (the file-hash cache makes per-call re-extraction near-instant) — no LLM.
15
+ */
16
+ export async function startMcpServer(root) {
17
+ const config = loadConfig(root);
18
+ const buckets = loadCapabilityBuckets();
19
+ const server = new McpServer({ name: "keel", version: "0.0.0" });
20
+ server.registerTool("check_before_write", {
21
+ description: "BEFORE writing new code, check whether this project already has a utility or library for it — so you reuse instead of reinventing, and match the existing library choice. Pass a short description of what you're about to write.",
22
+ inputSchema: { intent: z.string().describe("What you're about to write, e.g. 'a function to format dates'") },
23
+ }, async ({ intent }) => {
24
+ const { facts } = await loadFacts(root, config);
25
+ return text(checkBeforeWrite(facts, intent, buckets));
26
+ });
27
+ server.registerTool("get_conventions", {
28
+ description: "Get the verified, current conventions for an area of the codebase (naming style, libraries in use, language, package manager) — derived from the actual code, not a possibly-stale doc.",
29
+ inputSchema: { path: z.string().describe("The file or directory you're working in, e.g. 'src/api'") },
30
+ }, async ({ path }) => {
31
+ const { facts } = await loadFacts(root, config);
32
+ return text(getConventions(facts, path, buckets));
33
+ });
34
+ server.registerTool("report_drift", {
35
+ description: "List the project's current drift and library-conflict findings, so you can fix them or avoid adding to them.",
36
+ }, async () => {
37
+ const { facts } = await loadFacts(root, config);
38
+ const claims = parseClaims(root, config);
39
+ return text(reportDrift(facts, claims, config));
40
+ });
41
+ await server.connect(new StdioServerTransport());
42
+ }
43
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,qBAAqB,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAEjE,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,WAAW,EACT,mOAAmO;QACrO,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+DAA+D,CAAC,EAAE;KAC9G,EACD,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;QACnB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,WAAW,EACT,yLAAyL;QAC3L,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yDAAyD,CAAC,EAAE;KACtG,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACjB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EACT,8GAA8G;KACjH,EACD,KAAK,IAAI,EAAE;QACT,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC,CACF,CAAC;IAEF,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { KeelConfig } from "../config.js";
2
+ import type { Claims, Facts } from "../types.js";
3
+ import { type CapabilityBuckets } from "../analyze/shared.js";
4
+ /** Suggest existing utilities + libraries to reuse before the agent writes new code. */
5
+ export declare function checkBeforeWrite(facts: Facts, intent: string, buckets?: CapabilityBuckets): string;
6
+ /** Report the verified, current conventions for an area of the codebase. */
7
+ export declare function getConventions(facts: Facts, path: string, buckets?: CapabilityBuckets): string;
8
+ /** Current deterministic drift + library-conflict findings (the "you're inconsistent" signals). */
9
+ export declare function reportDrift(facts: Facts, claims: Claims, config: KeelConfig): string;