@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.
Files changed (2) hide show
  1. package/dist/cli.js +451 -185
  2. 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 import_path2 = require("path");
41
+ var import_path3 = require("path");
31
42
 
32
43
  // src/audit/loader.ts
33
- var import_path = require("path");
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, import_path.join)(targetDir, name));
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 = read("AGENTS.md") ?? read("agents.md") ?? read(".agents.md");
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, import_path.join)(targetDir, "PROGRESS.md")) ?? statMtime((0, import_path.join)(targetDir, "progress.md"));
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, import_path.join)(targetDir, "src")),
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/scorer.ts
99
- function scoreIdentity(files) {
100
- const gaps = [];
101
- if (!files.agentsMd) {
102
- return {
103
- score: 0,
104
- gaps: ["No AGENTS.md \u2014 agents have no entry point for this repository"]
105
- };
106
- }
107
- let score = 20;
108
- const wordCount = files.agentsMd.split(/\s+/).filter(Boolean).length;
109
- if (wordCount >= 50) {
110
- score += 20;
111
- } else {
112
- gaps.push(`AGENTS.md description too short (${wordCount} words, need 50+)`);
113
- }
114
- if (/^##\s+stack/im.test(files.agentsMd)) {
115
- score += 20;
116
- } else {
117
- gaps.push("No ## Stack section in AGENTS.md");
118
- }
119
- const hasVersion = typeof files.packageJson?.["version"] === "string" || /v?\d+\.\d+\.\d+/.test(files.agentsMd);
120
- if (hasVersion) {
121
- score += 20;
122
- } else {
123
- gaps.push("No version number found in AGENTS.md or package.json");
124
- }
125
- if (/^##\s+repo\s+structure/im.test(files.agentsMd) || /src\//m.test(files.agentsMd)) {
126
- score += 20;
127
- } else {
128
- gaps.push("No repo structure section in AGENTS.md");
129
- }
130
- return { score, gaps };
131
- }
132
- function scoreVerification(files) {
133
- const gaps = [];
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 scoreConstraints(files) {
218
- const gaps = [];
219
- let score = 0;
220
- const hasSection = files.constraintsMd != null || /^##\s+constraints/im.test(files.agentsMd ?? "");
221
- const constraintContent = files.constraintsMd ?? files.agentsMd ?? "";
222
- if (hasSection) {
223
- score += 30;
224
- } else {
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 subsystemLine(name, score, gaps) {
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, xref) {
469
+ function collectCriticalGaps(scored) {
407
470
  const gaps = [];
408
- for (const [, value] of Object.entries(scored)) {
409
- if (typeof value === "object" && "score" in value && "gaps" in value) {
410
- const sub = value;
411
- if (sub.score < 50) {
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 xref.checks) {
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, xref, opts) {
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: xref,
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, xref);
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 targetDir = (0, import_path2.resolve)(target);
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 scored = scoreRepo(files);
466
- const xref = crossRef(files);
467
- report(scored, xref, { json: opts.json });
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.0",
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
- "commander": "^12.0.0"
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",