@sicilianwildcat/aiready 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +451 -185
- package/package.json +6 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/cli.ts
|
|
@@ -24,16 +34,18 @@ __export(cli_exports, {
|
|
|
24
34
|
program: () => program
|
|
25
35
|
});
|
|
26
36
|
module.exports = __toCommonJS(cli_exports);
|
|
37
|
+
var import_config = require("dotenv/config");
|
|
27
38
|
var import_commander = require("commander");
|
|
28
39
|
|
|
29
40
|
// src/audit/index.ts
|
|
30
|
-
var
|
|
41
|
+
var import_path3 = require("path");
|
|
31
42
|
|
|
32
43
|
// src/audit/loader.ts
|
|
33
|
-
var
|
|
44
|
+
var import_path2 = require("path");
|
|
34
45
|
|
|
35
46
|
// src/utils/fs.ts
|
|
36
47
|
var import_fs = require("fs");
|
|
48
|
+
var import_path = require("path");
|
|
37
49
|
function readFile(filePath) {
|
|
38
50
|
try {
|
|
39
51
|
return (0, import_fs.readFileSync)(filePath, "utf8");
|
|
@@ -41,6 +53,9 @@ function readFile(filePath) {
|
|
|
41
53
|
return null;
|
|
42
54
|
}
|
|
43
55
|
}
|
|
56
|
+
function exists(filePath) {
|
|
57
|
+
return (0, import_fs.existsSync)(filePath);
|
|
58
|
+
}
|
|
44
59
|
function listDirs(dirPath) {
|
|
45
60
|
try {
|
|
46
61
|
return (0, import_fs.readdirSync)(dirPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
@@ -62,10 +77,64 @@ function statMtime(filePath) {
|
|
|
62
77
|
return null;
|
|
63
78
|
}
|
|
64
79
|
}
|
|
80
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
81
|
+
"node_modules",
|
|
82
|
+
".git",
|
|
83
|
+
"dist",
|
|
84
|
+
"build",
|
|
85
|
+
"coverage",
|
|
86
|
+
".next",
|
|
87
|
+
".nuxt",
|
|
88
|
+
"__pycache__",
|
|
89
|
+
".cache",
|
|
90
|
+
"vendor",
|
|
91
|
+
"tmp",
|
|
92
|
+
".tmp"
|
|
93
|
+
]);
|
|
94
|
+
function walkMdFiles(dirPath, relBase = "", depth = 0) {
|
|
95
|
+
if (depth > 3) return [];
|
|
96
|
+
const results = [];
|
|
97
|
+
try {
|
|
98
|
+
const entries = (0, import_fs.readdirSync)(dirPath, { withFileTypes: true });
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
101
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
102
|
+
const fullPath = (0, import_path.join)(dirPath, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
results.push(...walkMdFiles(fullPath, relPath, depth + 1));
|
|
105
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
106
|
+
const content = readFile(fullPath);
|
|
107
|
+
if (content !== null) {
|
|
108
|
+
results.push({ relPath, name: entry.name, fullContent: content });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
65
116
|
|
|
66
117
|
// src/audit/loader.ts
|
|
118
|
+
var AGENT_ENTRY_CANDIDATES = [
|
|
119
|
+
"AGENTS.md",
|
|
120
|
+
"CLAUDE.md",
|
|
121
|
+
"AGENT.md",
|
|
122
|
+
".cursorrules",
|
|
123
|
+
".windsurfrules",
|
|
124
|
+
".github/copilot-instructions.md",
|
|
125
|
+
"COPILOT.md"
|
|
126
|
+
];
|
|
127
|
+
function loadAgentEntry(targetDir) {
|
|
128
|
+
for (const rel of AGENT_ENTRY_CANDIDATES) {
|
|
129
|
+
const filePath = (0, import_path2.join)(targetDir, rel);
|
|
130
|
+
if (exists(filePath)) {
|
|
131
|
+
return readFile(filePath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
67
136
|
function loadRepo(targetDir) {
|
|
68
|
-
const read = (name) => readFile((0,
|
|
137
|
+
const read = (name) => readFile((0, import_path2.join)(targetDir, name));
|
|
69
138
|
const packageJsonRaw = read("package.json");
|
|
70
139
|
let packageJson = null;
|
|
71
140
|
if (packageJsonRaw) {
|
|
@@ -74,13 +143,21 @@ function loadRepo(targetDir) {
|
|
|
74
143
|
} catch {
|
|
75
144
|
}
|
|
76
145
|
}
|
|
77
|
-
const agentsMd =
|
|
146
|
+
const agentsMd = loadAgentEntry(targetDir);
|
|
78
147
|
const architectureMd = read("ARCHITECTURE.md") ?? read("architecture.md");
|
|
79
148
|
const constraintsMd = read("CONSTRAINTS.md") ?? read("constraints.md");
|
|
80
149
|
const progressMd = read("PROGRESS.md") ?? read("progress.md");
|
|
81
150
|
const sessionHandoffMd = read("SESSION-HANDOFF.md") ?? read("session-handoff.md");
|
|
82
|
-
const progressMdModifiedAt = statMtime((0,
|
|
151
|
+
const progressMdModifiedAt = statMtime((0, import_path2.join)(targetDir, "PROGRESS.md")) ?? statMtime((0, import_path2.join)(targetDir, "progress.md"));
|
|
152
|
+
const rawMdFiles = walkMdFiles(targetDir);
|
|
153
|
+
const mdFiles = rawMdFiles.map(({ relPath, name, fullContent }) => ({
|
|
154
|
+
path: relPath,
|
|
155
|
+
name,
|
|
156
|
+
preview: fullContent.slice(0, 200),
|
|
157
|
+
fullContent
|
|
158
|
+
}));
|
|
83
159
|
return {
|
|
160
|
+
mdFiles,
|
|
84
161
|
agentsMd,
|
|
85
162
|
architectureMd,
|
|
86
163
|
constraintsMd,
|
|
@@ -88,164 +165,59 @@ function loadRepo(targetDir) {
|
|
|
88
165
|
sessionHandoffMd,
|
|
89
166
|
packageJsonRaw,
|
|
90
167
|
packageJson,
|
|
91
|
-
srcDirs: listDirs((0,
|
|
168
|
+
srcDirs: listDirs((0, import_path2.join)(targetDir, "src")),
|
|
92
169
|
rootFiles: listFiles(targetDir),
|
|
93
170
|
progressMdModifiedAt,
|
|
94
171
|
targetDir
|
|
95
172
|
};
|
|
96
173
|
}
|
|
97
174
|
|
|
98
|
-
// src/audit/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let score = 0;
|
|
135
|
-
const hasVerificationSection = files.agentsMd != null && (/^##\s+verification/im.test(files.agentsMd) || /```(?:bash|sh)/.test(files.agentsMd));
|
|
136
|
-
if (hasVerificationSection) {
|
|
137
|
-
score += 30;
|
|
138
|
-
} else {
|
|
139
|
-
gaps.push("No verification commands section in AGENTS.md");
|
|
140
|
-
}
|
|
141
|
-
const npmCommandMatches = files.agentsMd?.match(/npm\s+(?:run\s+\w+|test|install|ci)/g) ?? [];
|
|
142
|
-
if (npmCommandMatches.length >= 2) {
|
|
143
|
-
score += 20;
|
|
144
|
-
} else {
|
|
145
|
-
gaps.push("Fewer than 2 verification commands documented in AGENTS.md");
|
|
146
|
-
}
|
|
147
|
-
const scripts = files.packageJson?.["scripts"];
|
|
148
|
-
const scriptNames = scripts != null && typeof scripts === "object" ? Object.keys(scripts) : [];
|
|
149
|
-
if (scriptNames.length >= 2) {
|
|
150
|
-
score += 30;
|
|
151
|
-
} else {
|
|
152
|
-
gaps.push("package.json missing scripts (need at least 2)");
|
|
153
|
-
}
|
|
154
|
-
if (scriptNames.includes("build") && scriptNames.includes("test")) {
|
|
155
|
-
score += 20;
|
|
156
|
-
} else {
|
|
157
|
-
gaps.push("package.json missing build or test script");
|
|
158
|
-
}
|
|
159
|
-
return { score, gaps };
|
|
160
|
-
}
|
|
161
|
-
function scoreState(files) {
|
|
162
|
-
const gaps = [];
|
|
163
|
-
let score = 0;
|
|
164
|
-
if (files.progressMd) {
|
|
165
|
-
score += 30;
|
|
166
|
-
if (/^##\s+(?:completed|in\s+progress|current)/im.test(files.progressMd)) {
|
|
167
|
-
score += 20;
|
|
168
|
-
} else {
|
|
169
|
-
gaps.push("PROGRESS.md lacks structured sections (## Completed / ## In progress)");
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
gaps.push("No PROGRESS.md \u2014 agents cannot resume sessions without project state");
|
|
173
|
-
}
|
|
174
|
-
if (files.sessionHandoffMd) {
|
|
175
|
-
score += 30;
|
|
176
|
-
} else {
|
|
177
|
-
gaps.push("No SESSION-HANDOFF.md \u2014 agents start blind each session");
|
|
178
|
-
}
|
|
179
|
-
if (files.progressMdModifiedAt) {
|
|
180
|
-
const ageMs = Date.now() - files.progressMdModifiedAt.getTime();
|
|
181
|
-
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
182
|
-
if (ageDays <= 7) {
|
|
183
|
-
score += 20;
|
|
184
|
-
} else {
|
|
185
|
-
gaps.push(`PROGRESS.md last updated ${Math.round(ageDays)} days ago (threshold: 7)`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return { score, gaps };
|
|
189
|
-
}
|
|
190
|
-
function scoreMemory(files) {
|
|
191
|
-
const gaps = [];
|
|
192
|
-
if (!files.architectureMd) {
|
|
193
|
-
return {
|
|
194
|
-
score: 0,
|
|
195
|
-
gaps: ["No ARCHITECTURE.md \u2014 agents cannot navigate the codebase without exploring"]
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
let score = 30;
|
|
199
|
-
const annotatedLines = (files.architectureMd.match(/←/g) ?? []).length;
|
|
200
|
-
if (annotatedLines >= 3) {
|
|
201
|
-
score += 30;
|
|
202
|
-
} else {
|
|
203
|
-
gaps.push("ARCHITECTURE.md has few module annotations (add \u2190 comments to explain modules)");
|
|
204
|
-
}
|
|
205
|
-
if (files.architectureMd.length > 300) {
|
|
206
|
-
score += 20;
|
|
207
|
-
} else {
|
|
208
|
-
gaps.push("ARCHITECTURE.md too short \u2014 add module responsibilities and data flow");
|
|
209
|
-
}
|
|
210
|
-
if (/src\//m.test(files.architectureMd)) {
|
|
211
|
-
score += 20;
|
|
212
|
-
} else {
|
|
213
|
-
gaps.push("ARCHITECTURE.md does not reference src/ directory structure");
|
|
175
|
+
// src/audit/mapper.ts
|
|
176
|
+
var MAPPER_SYSTEM = `You are classifying repository markdown files by AI agent harness subsystem.
|
|
177
|
+
|
|
178
|
+
There are exactly 5 subsystems:
|
|
179
|
+
- identity: File describes what the project is, its purpose, stack, version, or high-level structure
|
|
180
|
+
- verification: File contains commands to build/test/validate work, CI configuration, or runbooks
|
|
181
|
+
- state: File tracks current progress, session state, what is done, blocked, or next
|
|
182
|
+
- memory: File maps the architecture, module responsibilities, file structure, or code navigation
|
|
183
|
+
- constraints: File defines rules agents must follow, using MUST or MUST NOT language
|
|
184
|
+
|
|
185
|
+
For each file provided (path and first 200 characters), return which subsystems it belongs to.
|
|
186
|
+
A file can belong to multiple subsystems. Omit files with no harness relevance.
|
|
187
|
+
|
|
188
|
+
Return ONLY valid JSON with no explanation:
|
|
189
|
+
{"mappings":[{"path":"file.md","subsystems":["identity"]}]}`;
|
|
190
|
+
var VALID_SUBSYSTEMS = /* @__PURE__ */ new Set([
|
|
191
|
+
"identity",
|
|
192
|
+
"verification",
|
|
193
|
+
"state",
|
|
194
|
+
"memory",
|
|
195
|
+
"constraints"
|
|
196
|
+
]);
|
|
197
|
+
function parseMapperResponse(text) {
|
|
198
|
+
try {
|
|
199
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
200
|
+
if (!jsonMatch) return [];
|
|
201
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
202
|
+
if (!Array.isArray(parsed.mappings)) return [];
|
|
203
|
+
return parsed.mappings.filter((m) => typeof m.path === "string" && Array.isArray(m.subsystems)).map((m) => ({
|
|
204
|
+
path: m.path,
|
|
205
|
+
subsystems: m.subsystems.filter(
|
|
206
|
+
(s) => VALID_SUBSYSTEMS.has(s)
|
|
207
|
+
)
|
|
208
|
+
})).filter((m) => m.subsystems.length > 0);
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
214
211
|
}
|
|
215
|
-
return { score, gaps };
|
|
216
212
|
}
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
gaps.push("No CONSTRAINTS.md and no ## Constraints section in AGENTS.md");
|
|
226
|
-
}
|
|
227
|
-
if (/\bMUST\b/.test(constraintContent)) {
|
|
228
|
-
score += 40;
|
|
229
|
-
} else {
|
|
230
|
-
gaps.push("No MUST language in constraints \u2014 use imperative MUST / MUST NOT");
|
|
231
|
-
}
|
|
232
|
-
if (/\bMUST NOT\b/.test(constraintContent)) {
|
|
233
|
-
score += 30;
|
|
234
|
-
} else {
|
|
235
|
-
gaps.push("No MUST NOT language in constraints");
|
|
236
|
-
}
|
|
237
|
-
return { score, gaps };
|
|
238
|
-
}
|
|
239
|
-
function scoreRepo(files) {
|
|
240
|
-
const identity = scoreIdentity(files);
|
|
241
|
-
const verification = scoreVerification(files);
|
|
242
|
-
const state = scoreState(files);
|
|
243
|
-
const memory = scoreMemory(files);
|
|
244
|
-
const constraints = scoreConstraints(files);
|
|
245
|
-
const overall = Math.round(
|
|
246
|
-
(identity.score + verification.score + state.score + memory.score + constraints.score) / 5
|
|
247
|
-
);
|
|
248
|
-
return { identity, verification, state, memory, constraints, overall };
|
|
213
|
+
async function mapFiles(mdFiles, provider) {
|
|
214
|
+
if (mdFiles.length === 0) return [];
|
|
215
|
+
const fileList = mdFiles.map((f) => `- path: ${f.path}
|
|
216
|
+
preview: ${JSON.stringify(f.preview)}`).join("\n");
|
|
217
|
+
const text = await provider.chat(MAPPER_SYSTEM, `Classify these repository files:
|
|
218
|
+
|
|
219
|
+
${fileList}`, { fast: true });
|
|
220
|
+
return parseMapperResponse(text);
|
|
249
221
|
}
|
|
250
222
|
|
|
251
223
|
// src/audit/cross-ref.ts
|
|
@@ -372,15 +344,106 @@ function crossRef(files) {
|
|
|
372
344
|
};
|
|
373
345
|
}
|
|
374
346
|
|
|
347
|
+
// src/audit/scorer.ts
|
|
348
|
+
var SCORER_SYSTEM = `You are scoring an AI agent harness for a software repository.
|
|
349
|
+
|
|
350
|
+
Score each of the 5 subsystems from 0 to 100 based on the file contents provided.
|
|
351
|
+
|
|
352
|
+
Scoring criteria:
|
|
353
|
+
- identity (0-100): Does an agent know what this project is? (purpose, stack, version, structure)
|
|
354
|
+
Score 0 if no relevant files. Score 30-60 for partial coverage. Score 70-90 for good coverage. Score 91-100 for comprehensive coverage including stack, version, and structure.
|
|
355
|
+
- verification (0-100): Can an agent confirm work is correct? (build/test commands documented and present)
|
|
356
|
+
Score 0 if no verification commands. Score 50-70 if commands mentioned. Score 80-100 if commands are specific, runnable, and cross-referenced with scripts.
|
|
357
|
+
- state (0-100): Can an agent resume sessions? (progress tracking, session handoffs)
|
|
358
|
+
Score 0 if no state files. Score 30-50 for minimal tracking. Score 60-80 for structured progress + handoff. Score 90-100 for structured, fresh, and complete state.
|
|
359
|
+
- memory (0-100): Can an agent navigate without exploring? (architecture doc, module map with annotations)
|
|
360
|
+
Score 0 if no architecture doc. Score 30-50 for basic architecture. Score 70-90 for annotated module map. Score 91-100 for detailed map with \u2190 annotations and data flow.
|
|
361
|
+
- constraints (0-100): Does an agent know what it must never do? (MUST/MUST NOT rules)
|
|
362
|
+
Score 0 if no constraints. Score 30-50 for general rules. Score 70-90 for MUST language. Score 91-100 for explicit MUST NOT rules covering critical failure modes.
|
|
363
|
+
|
|
364
|
+
Return ONLY valid JSON with no explanation:
|
|
365
|
+
{"identity":{"score":85,"gaps":["one specific gap"]},"verification":{"score":70,"gaps":[]},"state":{"score":60,"gaps":["gap"]},"memory":{"score":90,"gaps":[]},"constraints":{"score":100,"gaps":[]}}`;
|
|
366
|
+
var SUBSYSTEMS = ["identity", "verification", "state", "memory", "constraints"];
|
|
367
|
+
function parseScorerResponse(text) {
|
|
368
|
+
try {
|
|
369
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
370
|
+
if (!jsonMatch) return {};
|
|
371
|
+
return JSON.parse(jsonMatch[0]);
|
|
372
|
+
} catch {
|
|
373
|
+
return {};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function emptyScore() {
|
|
377
|
+
return { score: 0, gaps: ["No files mapped to this subsystem"], files: [] };
|
|
378
|
+
}
|
|
379
|
+
function buildSubsystemContent(subsystem, mdFiles, mappings) {
|
|
380
|
+
const mappedPaths = mappings.filter((m) => m.subsystems.includes(subsystem)).map((m) => m.path);
|
|
381
|
+
if (mappedPaths.length === 0) return { files: [], content: "" };
|
|
382
|
+
const fileMap = new Map(mdFiles.map((f) => [f.path, f.fullContent]));
|
|
383
|
+
const sections = mappedPaths.map((p) => {
|
|
384
|
+
const content = fileMap.get(p) ?? "";
|
|
385
|
+
return `=== ${p} ===
|
|
386
|
+
${content}`;
|
|
387
|
+
});
|
|
388
|
+
return { files: mappedPaths, content: sections.join("\n\n") };
|
|
389
|
+
}
|
|
390
|
+
async function scoreRepo(files, mappings, provider) {
|
|
391
|
+
const subsystemContents = SUBSYSTEMS.map((s) => ({
|
|
392
|
+
subsystem: s,
|
|
393
|
+
...buildSubsystemContent(s, files.mdFiles, mappings)
|
|
394
|
+
}));
|
|
395
|
+
const contentSections = subsystemContents.map(({ subsystem, content }) => {
|
|
396
|
+
if (!content) return `### ${subsystem}
|
|
397
|
+
(no files mapped)`;
|
|
398
|
+
return `### ${subsystem}
|
|
399
|
+
${content}`;
|
|
400
|
+
}).join("\n\n");
|
|
401
|
+
const text = await provider.chat(
|
|
402
|
+
SCORER_SYSTEM,
|
|
403
|
+
`Score this repository's AI harness across all 5 subsystems:
|
|
404
|
+
|
|
405
|
+
${contentSections}`,
|
|
406
|
+
{ fast: false }
|
|
407
|
+
);
|
|
408
|
+
const llmScores = parseScorerResponse(text);
|
|
409
|
+
function toSubsystemScore(key) {
|
|
410
|
+
const entry = llmScores[key];
|
|
411
|
+
const { files: subsysFiles } = subsystemContents.find((s) => s.subsystem === key) ?? { files: [] };
|
|
412
|
+
if (!entry) return emptyScore();
|
|
413
|
+
return {
|
|
414
|
+
score: Math.min(100, Math.max(0, Math.round(entry.score))),
|
|
415
|
+
gaps: Array.isArray(entry.gaps) ? entry.gaps.filter((g) => typeof g === "string") : [],
|
|
416
|
+
files: subsysFiles
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const identity = toSubsystemScore("identity");
|
|
420
|
+
const verification = toSubsystemScore("verification");
|
|
421
|
+
const state = toSubsystemScore("state");
|
|
422
|
+
const memory = toSubsystemScore("memory");
|
|
423
|
+
const constraints = toSubsystemScore("constraints");
|
|
424
|
+
const overall = Math.round(
|
|
425
|
+
(identity.score + verification.score + state.score + memory.score + constraints.score) / 5
|
|
426
|
+
);
|
|
427
|
+
const xref = crossRef(files);
|
|
428
|
+
return { identity, verification, state, memory, constraints, overall, crossRef: xref };
|
|
429
|
+
}
|
|
430
|
+
|
|
375
431
|
// src/audit/reporter.ts
|
|
376
432
|
function bar(score) {
|
|
377
433
|
const filled = Math.round(score / 10);
|
|
378
434
|
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
379
435
|
}
|
|
380
|
-
function
|
|
436
|
+
function abbreviateFiles(files) {
|
|
437
|
+
if (files.length === 0) return "(no files)";
|
|
438
|
+
const names = files.map((p) => p.split("/").pop() ?? p);
|
|
439
|
+
if (names.length <= 3) return names.join(", ");
|
|
440
|
+
return `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
|
|
441
|
+
}
|
|
442
|
+
function subsystemLine(name, score, gaps, files) {
|
|
381
443
|
const scoreStr = String(score).padStart(3);
|
|
444
|
+
const filePart = abbreviateFiles(files).padEnd(30);
|
|
382
445
|
const summary = gaps.length > 0 ? gaps[0] : "All checks passed";
|
|
383
|
-
return `${name.padEnd(14)} ${bar(score)} ${scoreStr} ${summary}`;
|
|
446
|
+
return `${name.padEnd(14)} ${bar(score)} ${scoreStr} ${filePart} ${summary}`;
|
|
384
447
|
}
|
|
385
448
|
function getRecommendation(scored) {
|
|
386
449
|
const subsystems = [
|
|
@@ -403,35 +466,33 @@ function getRecommendation(scored) {
|
|
|
403
466
|
}
|
|
404
467
|
return messages[lowest.name] ?? "Run `npx aiready init` to generate missing artifacts.";
|
|
405
468
|
}
|
|
406
|
-
function collectCriticalGaps(scored
|
|
469
|
+
function collectCriticalGaps(scored) {
|
|
407
470
|
const gaps = [];
|
|
408
|
-
for (const [,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
gaps.push(...sub.gaps.slice(0, 1));
|
|
413
|
-
}
|
|
471
|
+
for (const key of ["identity", "verification", "state", "memory", "constraints"]) {
|
|
472
|
+
const sub = scored[key];
|
|
473
|
+
if (sub.score < 50 && sub.gaps.length > 0) {
|
|
474
|
+
gaps.push(sub.gaps[0]);
|
|
414
475
|
}
|
|
415
476
|
}
|
|
416
|
-
for (const check of
|
|
477
|
+
for (const check of scored.crossRef.checks) {
|
|
417
478
|
if (!check.passed) {
|
|
418
479
|
gaps.push(check.detail);
|
|
419
480
|
}
|
|
420
481
|
}
|
|
421
482
|
return gaps;
|
|
422
483
|
}
|
|
423
|
-
function report(scored,
|
|
484
|
+
function report(scored, opts) {
|
|
424
485
|
if (opts.json) {
|
|
425
486
|
const out = {
|
|
426
487
|
overall: scored.overall,
|
|
427
488
|
subsystems: {
|
|
428
|
-
identity: scored.identity,
|
|
429
|
-
verification: scored.verification,
|
|
430
|
-
state: scored.state,
|
|
431
|
-
memory: scored.memory,
|
|
432
|
-
constraints: scored.constraints
|
|
489
|
+
identity: { score: scored.identity.score, gaps: scored.identity.gaps, files: scored.identity.files },
|
|
490
|
+
verification: { score: scored.verification.score, gaps: scored.verification.gaps, files: scored.verification.files },
|
|
491
|
+
state: { score: scored.state.score, gaps: scored.state.gaps, files: scored.state.files },
|
|
492
|
+
memory: { score: scored.memory.score, gaps: scored.memory.gaps, files: scored.memory.files },
|
|
493
|
+
constraints: { score: scored.constraints.score, gaps: scored.constraints.gaps, files: scored.constraints.files }
|
|
433
494
|
},
|
|
434
|
-
crossReference:
|
|
495
|
+
crossReference: scored.crossRef,
|
|
435
496
|
recommendation: getRecommendation(scored)
|
|
436
497
|
};
|
|
437
498
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
@@ -440,12 +501,12 @@ function report(scored, xref, opts) {
|
|
|
440
501
|
const lines = [];
|
|
441
502
|
lines.push(`AI Readiness: ${scored.overall}/100`);
|
|
442
503
|
lines.push("");
|
|
443
|
-
lines.push(subsystemLine("identity", scored.identity.score, scored.identity.gaps));
|
|
444
|
-
lines.push(subsystemLine("verification", scored.verification.score, scored.verification.gaps));
|
|
445
|
-
lines.push(subsystemLine("state", scored.state.score, scored.state.gaps));
|
|
446
|
-
lines.push(subsystemLine("memory", scored.memory.score, scored.memory.gaps));
|
|
447
|
-
lines.push(subsystemLine("constraints", scored.constraints.score, scored.constraints.gaps));
|
|
448
|
-
const criticalGaps = collectCriticalGaps(scored
|
|
504
|
+
lines.push(subsystemLine("identity", scored.identity.score, scored.identity.gaps, scored.identity.files));
|
|
505
|
+
lines.push(subsystemLine("verification", scored.verification.score, scored.verification.gaps, scored.verification.files));
|
|
506
|
+
lines.push(subsystemLine("state", scored.state.score, scored.state.gaps, scored.state.files));
|
|
507
|
+
lines.push(subsystemLine("memory", scored.memory.score, scored.memory.gaps, scored.memory.files));
|
|
508
|
+
lines.push(subsystemLine("constraints", scored.constraints.score, scored.constraints.gaps, scored.constraints.files));
|
|
509
|
+
const criticalGaps = collectCriticalGaps(scored);
|
|
449
510
|
if (criticalGaps.length > 0) {
|
|
450
511
|
lines.push("");
|
|
451
512
|
lines.push("Critical gaps:");
|
|
@@ -458,13 +519,212 @@ function report(scored, xref, opts) {
|
|
|
458
519
|
process.stdout.write(lines.join("\n") + "\n");
|
|
459
520
|
}
|
|
460
521
|
|
|
522
|
+
// src/utils/prompt.ts
|
|
523
|
+
var import_prompts = require("@inquirer/prompts");
|
|
524
|
+
|
|
525
|
+
// src/utils/llm.ts
|
|
526
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
527
|
+
var import_openai = __toESM(require("openai"));
|
|
528
|
+
|
|
529
|
+
// src/utils/models.ts
|
|
530
|
+
var ANTHROPIC_MODELS = [
|
|
531
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 \u2014 fast, cheapest" },
|
|
532
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 \u2014 balanced (recommended)" },
|
|
533
|
+
{ id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 \u2014 balanced" },
|
|
534
|
+
{ id: "claude-opus-4-8", label: "Claude Opus 4.8 \u2014 most capable" },
|
|
535
|
+
{ id: "claude-opus-4-20250514", label: "Claude Opus 4 \u2014 most capable" }
|
|
536
|
+
];
|
|
537
|
+
var OPENAI_FALLBACK_MODELS = [
|
|
538
|
+
{ id: "gpt-4o-mini", label: "GPT-4o Mini" },
|
|
539
|
+
{ id: "gpt-4o", label: "GPT-4o" },
|
|
540
|
+
{ id: "gpt-4-turbo", label: "GPT-4 Turbo" },
|
|
541
|
+
{ id: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
// src/utils/llm.ts
|
|
545
|
+
var ANTHROPIC_FAST = "claude-haiku-4-5-20251001";
|
|
546
|
+
var ANTHROPIC_QUALITY = "claude-sonnet-4-6";
|
|
547
|
+
var AnthropicProvider = class {
|
|
548
|
+
client;
|
|
549
|
+
modelId;
|
|
550
|
+
constructor(apiKey, modelId) {
|
|
551
|
+
this.client = new import_sdk.default({ apiKey });
|
|
552
|
+
this.modelId = modelId;
|
|
553
|
+
}
|
|
554
|
+
async chat(system, user, opts) {
|
|
555
|
+
const model = this.modelId ?? (opts?.fast ? ANTHROPIC_FAST : ANTHROPIC_QUALITY);
|
|
556
|
+
const maxTokens = opts?.fast ? 1024 : 2048;
|
|
557
|
+
const response = await this.client.messages.create({
|
|
558
|
+
model,
|
|
559
|
+
max_tokens: maxTokens,
|
|
560
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
561
|
+
messages: [{ role: "user", content: user }]
|
|
562
|
+
});
|
|
563
|
+
const block = response.content.find((b) => b.type === "text");
|
|
564
|
+
return block && block.type === "text" ? block.text : "";
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var OPENAI_FAST = "gpt-4o-mini";
|
|
568
|
+
var OPENAI_QUALITY = "gpt-4o";
|
|
569
|
+
var OpenAIProvider = class {
|
|
570
|
+
client;
|
|
571
|
+
modelId;
|
|
572
|
+
constructor(apiKey, modelId) {
|
|
573
|
+
const baseURL = process.env["OPENAI_BASE_URL"];
|
|
574
|
+
this.client = new import_openai.default({ apiKey, ...baseURL ? { baseURL } : {} });
|
|
575
|
+
this.modelId = modelId;
|
|
576
|
+
}
|
|
577
|
+
async chat(system, user, opts) {
|
|
578
|
+
const model = this.modelId ?? (opts?.fast ? OPENAI_FAST : OPENAI_QUALITY);
|
|
579
|
+
const response = await this.client.chat.completions.create({
|
|
580
|
+
model,
|
|
581
|
+
messages: [
|
|
582
|
+
{ role: "system", content: system },
|
|
583
|
+
{ role: "user", content: user }
|
|
584
|
+
]
|
|
585
|
+
});
|
|
586
|
+
return response.choices[0]?.message?.content ?? "";
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var OllamaProvider = class {
|
|
590
|
+
client;
|
|
591
|
+
model;
|
|
592
|
+
constructor() {
|
|
593
|
+
const host = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
594
|
+
this.model = process.env["OLLAMA_MODEL"] ?? "llama3";
|
|
595
|
+
this.client = new import_openai.default({
|
|
596
|
+
apiKey: "ollama",
|
|
597
|
+
baseURL: `${host}/v1`
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async chat(system, user) {
|
|
601
|
+
const response = await this.client.chat.completions.create({
|
|
602
|
+
model: this.model,
|
|
603
|
+
messages: [
|
|
604
|
+
{ role: "system", content: system },
|
|
605
|
+
{ role: "user", content: user }
|
|
606
|
+
]
|
|
607
|
+
});
|
|
608
|
+
return response.choices[0]?.message?.content ?? "";
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
async function listOpenAIModels(apiKey) {
|
|
612
|
+
try {
|
|
613
|
+
const baseURL = process.env["OPENAI_BASE_URL"];
|
|
614
|
+
const client = new import_openai.default({ apiKey, ...baseURL ? { baseURL } : {} });
|
|
615
|
+
const response = await client.models.list();
|
|
616
|
+
const chatModels = response.data.filter((m) => m.id.startsWith("gpt-") && !m.id.includes(":")).sort((a, b) => b.created - a.created).map((m) => ({ id: m.id, label: m.id }));
|
|
617
|
+
return chatModels.length > 0 ? chatModels : OPENAI_FALLBACK_MODELS;
|
|
618
|
+
} catch {
|
|
619
|
+
return OPENAI_FALLBACK_MODELS;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function createProvider(name, apiKey, modelId) {
|
|
623
|
+
switch (name) {
|
|
624
|
+
case "anthropic":
|
|
625
|
+
if (!apiKey) throw new Error("API key required for Anthropic provider");
|
|
626
|
+
return new AnthropicProvider(apiKey, modelId);
|
|
627
|
+
case "openai":
|
|
628
|
+
if (!apiKey) throw new Error("API key required for OpenAI provider");
|
|
629
|
+
return new OpenAIProvider(apiKey, modelId);
|
|
630
|
+
case "ollama":
|
|
631
|
+
return new OllamaProvider();
|
|
632
|
+
default: {
|
|
633
|
+
const _exhaustive = name;
|
|
634
|
+
throw new Error(`Unknown provider: ${String(_exhaustive)}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/utils/prompt.ts
|
|
640
|
+
var ENV_KEY_NAMES = {
|
|
641
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
642
|
+
openai: "OPENAI_API_KEY",
|
|
643
|
+
ollama: null
|
|
644
|
+
};
|
|
645
|
+
var GET_KEY_URLS = {
|
|
646
|
+
anthropic: "https://console.anthropic.com",
|
|
647
|
+
openai: "https://platform.openai.com/api-keys"
|
|
648
|
+
};
|
|
649
|
+
function exitMissingKey(provider, envVar) {
|
|
650
|
+
const url = GET_KEY_URLS[provider];
|
|
651
|
+
process.stderr.write(`ERROR: ${envVar} not set
|
|
652
|
+
`);
|
|
653
|
+
process.stderr.write(`WHY: aiready needs an API key to analyse your repository with ${provider}
|
|
654
|
+
`);
|
|
655
|
+
process.stderr.write(`FIX: Add it to your .env file:
|
|
656
|
+
`);
|
|
657
|
+
process.stderr.write(` ${envVar}=your_key_here
|
|
658
|
+
`);
|
|
659
|
+
if (url) {
|
|
660
|
+
process.stderr.write(` Get your key at: ${url}
|
|
661
|
+
`);
|
|
662
|
+
}
|
|
663
|
+
process.stderr.write(`
|
|
664
|
+
`);
|
|
665
|
+
process.stderr.write(` If you use Cursor Pro or Claude Code without your own API key,
|
|
666
|
+
`);
|
|
667
|
+
process.stderr.write(` use Ollama instead (free, runs locally): https://ollama.com
|
|
668
|
+
`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
function resolveProvider(flag) {
|
|
672
|
+
const valid = ["anthropic", "openai", "ollama"];
|
|
673
|
+
const lower = flag.toLowerCase();
|
|
674
|
+
if (!valid.includes(lower)) {
|
|
675
|
+
process.stderr.write(`ERROR: Unknown provider "${flag}". Valid options: anthropic, openai, ollama
|
|
676
|
+
`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
return lower;
|
|
680
|
+
}
|
|
681
|
+
async function promptProvider() {
|
|
682
|
+
return (0, import_prompts.select)({
|
|
683
|
+
message: "Select LLM provider:",
|
|
684
|
+
choices: [
|
|
685
|
+
{ name: "Anthropic", value: "anthropic" },
|
|
686
|
+
{ name: "OpenAI", value: "openai" },
|
|
687
|
+
{ name: "Ollama (local \u2014 no API key needed)", value: "ollama" }
|
|
688
|
+
]
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async function promptModelId(provider, apiKey) {
|
|
692
|
+
if (provider === "ollama") {
|
|
693
|
+
return process.env["OLLAMA_MODEL"] ?? "llama3";
|
|
694
|
+
}
|
|
695
|
+
if (provider === "anthropic") {
|
|
696
|
+
return (0, import_prompts.select)({
|
|
697
|
+
message: "Select model:",
|
|
698
|
+
choices: ANTHROPIC_MODELS.map((m) => ({ name: m.label, value: m.id }))
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const models = await listOpenAIModels(apiKey);
|
|
702
|
+
return (0, import_prompts.select)({
|
|
703
|
+
message: "Select model:",
|
|
704
|
+
choices: models.map((m) => ({ name: m.label, value: m.id }))
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async function selectAuditConfig(flags) {
|
|
708
|
+
const provider = flags.provider ? resolveProvider(flags.provider) : await promptProvider();
|
|
709
|
+
const envVarName = ENV_KEY_NAMES[provider];
|
|
710
|
+
let apiKey;
|
|
711
|
+
if (envVarName) {
|
|
712
|
+
apiKey = process.env[envVarName];
|
|
713
|
+
if (!apiKey) exitMissingKey(provider, envVarName);
|
|
714
|
+
}
|
|
715
|
+
const modelId = flags.model ?? await promptModelId(provider, apiKey);
|
|
716
|
+
return { provider, modelId, apiKey };
|
|
717
|
+
}
|
|
718
|
+
|
|
461
719
|
// src/audit/index.ts
|
|
462
|
-
function runAudit(target, opts) {
|
|
463
|
-
const
|
|
720
|
+
async function runAudit(target, opts) {
|
|
721
|
+
const config = await selectAuditConfig({ provider: opts.provider, model: opts.model });
|
|
722
|
+
const provider = createProvider(config.provider, config.apiKey, config.modelId);
|
|
723
|
+
const targetDir = (0, import_path3.resolve)(target);
|
|
464
724
|
const files = loadRepo(targetDir);
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
report(scored,
|
|
725
|
+
const mappings = await mapFiles(files.mdFiles, provider);
|
|
726
|
+
const scored = await scoreRepo(files, mappings, provider);
|
|
727
|
+
report(scored, { json: opts.json });
|
|
468
728
|
if (scored.overall < opts.minScore) {
|
|
469
729
|
process.exit(1);
|
|
470
730
|
}
|
|
@@ -473,10 +733,16 @@ function runAudit(target, opts) {
|
|
|
473
733
|
// src/cli.ts
|
|
474
734
|
var program = new import_commander.Command();
|
|
475
735
|
program.name("aiready").description("Audit repositories for AI agent readiness").version("0.1.0");
|
|
476
|
-
program.command("audit").description("Score a repository against the 5 AI-readiness subsystems").option("-t, --target <dir>", "target directory to audit", ".").option("--json", "output results as JSON", false).option("--min-score <n>", "exit 1 if overall score is below this threshold", "70").action((opts) => {
|
|
736
|
+
program.command("audit").description("Score a repository against the 5 AI-readiness subsystems").option("-t, --target <dir>", "target directory to audit", ".").option("--json", "output results as JSON", false).option("--min-score <n>", "exit 1 if overall score is below this threshold", "70").option("--provider <name>", "LLM provider: anthropic | openai | ollama (skips prompt)").option("--model <id>", "model ID (e.g. claude-sonnet-4-6, gpt-4o) \u2014 skips prompt").action((opts) => {
|
|
477
737
|
runAudit(opts.target, {
|
|
478
738
|
json: opts.json,
|
|
479
|
-
minScore: parseInt(opts.minScore, 10)
|
|
739
|
+
minScore: parseInt(opts.minScore, 10),
|
|
740
|
+
provider: opts.provider,
|
|
741
|
+
model: opts.model
|
|
742
|
+
}).catch((err) => {
|
|
743
|
+
process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}
|
|
744
|
+
`);
|
|
745
|
+
process.exit(1);
|
|
480
746
|
});
|
|
481
747
|
});
|
|
482
748
|
if (require.main === module) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sicilianwildcat/aiready",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Audit repositories for AI agent readiness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "harikrishn4a",
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"dev": "tsup --watch"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"
|
|
42
|
+
"@anthropic-ai/sdk": "^0.100.1",
|
|
43
|
+
"@inquirer/prompts": "^7.10.1",
|
|
44
|
+
"commander": "^12.0.0",
|
|
45
|
+
"dotenv": "^17.4.2",
|
|
46
|
+
"openai": "^6.42.0"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@types/node": "^20.0.0",
|