@sicilianwildcat/aiready 0.1.2 → 0.1.4

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 +197 -35
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -133,6 +133,59 @@ function loadAgentEntry(targetDir) {
133
133
  }
134
134
  return null;
135
135
  }
136
+ var GRAPHIFY_DIRECT_PATHS = ["graphify-out/graph.json", ".graphify/graph.json"];
137
+ function findGraphifyOutput(target) {
138
+ for (const p of GRAPHIFY_DIRECT_PATHS) {
139
+ const full = (0, import_path2.join)(target, p);
140
+ if (exists(full)) return full;
141
+ }
142
+ const datedBase = (0, import_path2.join)(target, "graphify-out");
143
+ if (exists(datedBase)) {
144
+ const subdirs = listDirs(datedBase).sort().reverse();
145
+ for (const subdir of subdirs) {
146
+ const candidate = (0, import_path2.join)(datedBase, subdir, "graph.json");
147
+ if (exists(candidate)) return candidate;
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+ function loadFromGraph(target, graphPath) {
153
+ const raw = readFile(graphPath);
154
+ if (!raw) return [];
155
+ let graph;
156
+ try {
157
+ graph = JSON.parse(raw);
158
+ } catch {
159
+ return [];
160
+ }
161
+ const nodeDegree = /* @__PURE__ */ new Map();
162
+ for (const link of graph.links ?? []) {
163
+ nodeDegree.set(link.source, (nodeDegree.get(link.source) ?? 0) + 1);
164
+ nodeDegree.set(link.target, (nodeDegree.get(link.target) ?? 0) + 1);
165
+ }
166
+ const fileDegree = /* @__PURE__ */ new Map();
167
+ for (const node of graph.nodes ?? []) {
168
+ if (node.file_type !== "document") continue;
169
+ if (!node.source_file?.endsWith(".md")) continue;
170
+ const current = fileDegree.get(node.source_file) ?? 0;
171
+ fileDegree.set(node.source_file, current + (nodeDegree.get(node.id) ?? 0));
172
+ }
173
+ const topFiles = [...fileDegree.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([filePath]) => filePath);
174
+ const repoFiles = [];
175
+ for (const filePath of topFiles) {
176
+ const full = (0, import_path2.join)(target, filePath);
177
+ if (!exists(full)) continue;
178
+ const content = readFile(full);
179
+ if (content === null) continue;
180
+ repoFiles.push({
181
+ path: filePath,
182
+ name: filePath.split("/").pop() ?? filePath,
183
+ preview: content.slice(0, 200),
184
+ fullContent: content
185
+ });
186
+ }
187
+ return repoFiles;
188
+ }
136
189
  function loadRepo(targetDir) {
137
190
  const read = (name) => readFile((0, import_path2.join)(targetDir, name));
138
191
  const packageJsonRaw = read("package.json");
@@ -149,15 +202,27 @@ function loadRepo(targetDir) {
149
202
  const progressMd = read("PROGRESS.md") ?? read("progress.md");
150
203
  const sessionHandoffMd = read("SESSION-HANDOFF.md") ?? read("session-handoff.md");
151
204
  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
- }));
205
+ const graphPath = findGraphifyOutput(targetDir);
206
+ let mdFiles;
207
+ let usedGraphify = false;
208
+ if (graphPath) {
209
+ mdFiles = loadFromGraph(targetDir, graphPath);
210
+ usedGraphify = true;
211
+ const rel = graphPath.startsWith(targetDir) ? graphPath.slice(targetDir.length + 1) : graphPath;
212
+ console.log(` Using Graphify knowledge graph (${rel})`);
213
+ console.log(` Top ${mdFiles.length} files by centrality selected`);
214
+ } else {
215
+ const rawMdFiles = walkMdFiles(targetDir);
216
+ mdFiles = rawMdFiles.map(({ relPath, name, fullContent }) => ({
217
+ path: relPath,
218
+ name,
219
+ preview: fullContent.slice(0, 200),
220
+ fullContent
221
+ }));
222
+ }
159
223
  return {
160
224
  mdFiles,
225
+ usedGraphify,
161
226
  agentsMd,
162
227
  architectureMd,
163
228
  constraintsMd,
@@ -173,6 +238,19 @@ function loadRepo(targetDir) {
173
238
  }
174
239
 
175
240
  // src/audit/mapper.ts
241
+ var TRIAGE_SYSTEM = `You are a repository analyst. Given a list of file paths,
242
+ identify which files could plausibly serve as harness artifacts for AI
243
+ coding agents.
244
+
245
+ Harness artifacts include: agent instructions, project progress tracking,
246
+ architecture documentation, constraints or rules, session state,
247
+ design decisions.
248
+
249
+ Exclude: changelogs, license files, generated API docs, package readmes,
250
+ dependency documentation, test fixtures, lock files.
251
+
252
+ Respond with valid JSON only:
253
+ { "relevant": ["path/to/file.md", ...] }`;
176
254
  var MAPPER_SYSTEM = `You are classifying repository markdown files by AI agent harness subsystem.
177
255
 
178
256
  There are exactly 5 subsystems:
@@ -194,6 +272,20 @@ var VALID_SUBSYSTEMS = /* @__PURE__ */ new Set([
194
272
  "memory",
195
273
  "constraints"
196
274
  ]);
275
+ function firstLines(content, lineCount) {
276
+ return content.split("\n").slice(0, lineCount).join("\n");
277
+ }
278
+ function parseTriageResponse(text) {
279
+ try {
280
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
281
+ if (!jsonMatch) return [];
282
+ const parsed = JSON.parse(jsonMatch[0]);
283
+ if (!Array.isArray(parsed.relevant)) return [];
284
+ return parsed.relevant.filter((p) => typeof p === "string");
285
+ } catch {
286
+ return [];
287
+ }
288
+ }
197
289
  function parseMapperResponse(text) {
198
290
  try {
199
291
  const jsonMatch = text.match(/\{[\s\S]*\}/);
@@ -210,15 +302,41 @@ function parseMapperResponse(text) {
210
302
  return [];
211
303
  }
212
304
  }
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:
305
+ async function triageByFilename(mdFiles, provider) {
306
+ const fileList = mdFiles.map((f) => `- ${f.path}`).join("\n");
307
+ const text = await provider.chat(
308
+ TRIAGE_SYSTEM,
309
+ `Identify harness-relevant files from these paths:
218
310
 
219
- ${fileList}`, { fast: true });
311
+ ${fileList}`,
312
+ { fast: true }
313
+ );
314
+ return parseTriageResponse(text);
315
+ }
316
+ async function classifyFiles(relevantFiles, provider) {
317
+ const fileList = relevantFiles.map((f) => `- path: ${f.path}
318
+ preview: ${JSON.stringify(firstLines(f.fullContent, 50))}`).join("\n");
319
+ const text = await provider.chat(
320
+ MAPPER_SYSTEM,
321
+ `Classify these repository files:
322
+
323
+ ${fileList}`,
324
+ { fast: true }
325
+ );
220
326
  return parseMapperResponse(text);
221
327
  }
328
+ async function mapFiles(mdFiles, provider, usedGraphify = false) {
329
+ if (mdFiles.length === 0) return [];
330
+ if (usedGraphify) {
331
+ return classifyFiles(mdFiles, provider);
332
+ }
333
+ const relevantPaths = await triageByFilename(mdFiles, provider);
334
+ if (relevantPaths.length === 0) return [];
335
+ const pathSet = new Set(relevantPaths);
336
+ const relevantFiles = mdFiles.filter((f) => pathSet.has(f.path));
337
+ if (relevantFiles.length === 0) return [];
338
+ return classifyFiles(relevantFiles, provider);
339
+ }
222
340
 
223
341
  // src/audit/cross-ref.ts
224
342
  function extractNpmRunCommands(content) {
@@ -244,23 +362,28 @@ function extractDocumentedModules(content) {
244
362
  }
245
363
  return [...new Set(modules)];
246
364
  }
247
- function checkCommands(files) {
248
- if (!files.agentsMd) {
365
+ function checkCommands(files, mappings, mdFileMap) {
366
+ let contentToCheck = files.agentsMd;
367
+ if (!contentToCheck && mappings.length > 0) {
368
+ const mapped = mappings.filter((m) => m.subsystems.includes("identity") || m.subsystems.includes("verification")).map((m) => mdFileMap.get(m.path)).filter((c) => c !== void 0).join("\n\n");
369
+ if (mapped) contentToCheck = mapped;
370
+ }
371
+ if (!contentToCheck) {
249
372
  return [
250
373
  {
251
374
  name: "commands-in-agents-exist-in-package",
252
375
  passed: false,
253
- detail: "No AGENTS.md to extract commands from"
376
+ detail: "No agent harness file found to extract commands from"
254
377
  }
255
378
  ];
256
379
  }
257
- const commands = extractNpmRunCommands(files.agentsMd);
380
+ const commands = extractNpmRunCommands(contentToCheck);
258
381
  if (commands.length === 0) {
259
382
  return [
260
383
  {
261
384
  name: "commands-in-agents-exist-in-package",
262
385
  passed: true,
263
- detail: "No npm run commands found in AGENTS.md bash blocks"
386
+ detail: "No npm run commands found in harness bash blocks"
264
387
  }
265
388
  ];
266
389
  }
@@ -272,30 +395,35 @@ function checkCommands(files) {
272
395
  {
273
396
  name: "commands-in-agents-exist-in-package",
274
397
  passed: true,
275
- detail: `All ${commands.length} npm run command(s) in AGENTS.md found in package.json scripts`
398
+ detail: `All ${commands.length} npm run command(s) in harness found in package.json scripts`
276
399
  }
277
400
  ];
278
401
  }
279
402
  return missing.map((cmd) => ({
280
403
  name: "commands-in-agents-exist-in-package",
281
404
  passed: false,
282
- detail: `\`npm run ${cmd}\` in AGENTS.md not found in package.json scripts`
405
+ detail: `\`npm run ${cmd}\` in harness not found in package.json scripts`
283
406
  }));
284
407
  }
285
- function checkModules(files) {
286
- if (!files.architectureMd) {
408
+ function checkModules(files, mappings, mdFileMap) {
409
+ let contentToCheck = files.architectureMd;
410
+ if (!contentToCheck && mappings.length > 0) {
411
+ const mapped = mappings.filter((m) => m.subsystems.includes("memory")).map((m) => mdFileMap.get(m.path)).filter((c) => c !== void 0).join("\n\n");
412
+ if (mapped) contentToCheck = mapped;
413
+ }
414
+ if (!contentToCheck) {
287
415
  return {
288
416
  name: "architecture-modules-match-src",
289
417
  passed: false,
290
- detail: "No ARCHITECTURE.md to extract documented modules from"
418
+ detail: "No architecture file found to extract documented modules from"
291
419
  };
292
420
  }
293
- const documented = extractDocumentedModules(files.architectureMd);
421
+ const documented = extractDocumentedModules(contentToCheck);
294
422
  if (documented.length === 0) {
295
423
  return {
296
424
  name: "architecture-modules-match-src",
297
425
  passed: true,
298
- detail: "No annotated module directories found in ARCHITECTURE.md"
426
+ detail: "No annotated module directories found in architecture file"
299
427
  };
300
428
  }
301
429
  const missing = documented.filter((mod) => !files.srcDirs.includes(mod));
@@ -309,7 +437,7 @@ function checkModules(files) {
309
437
  return {
310
438
  name: "architecture-modules-match-src",
311
439
  passed: false,
312
- detail: `Module(s) in ARCHITECTURE.md not found in src/: ${missing.join(", ")}`
440
+ detail: `Module(s) in architecture file not found in src/: ${missing.join(", ")}`
313
441
  };
314
442
  }
315
443
  function checkProgressFreshness(files) {
@@ -334,11 +462,12 @@ function checkProgressFreshness(files) {
334
462
  detail: `PROGRESS.md last updated ${Math.round(ageDays)} days ago (threshold: 7)`
335
463
  };
336
464
  }
337
- function crossRef(files) {
465
+ function crossRef(files, mappings = []) {
466
+ const mdFileMap = new Map(files.mdFiles.map((f) => [f.path, f.fullContent]));
338
467
  return {
339
468
  checks: [
340
- ...checkCommands(files),
341
- checkModules(files),
469
+ ...checkCommands(files, mappings, mdFileMap),
470
+ checkModules(files, mappings, mdFileMap),
342
471
  checkProgressFreshness(files)
343
472
  ]
344
473
  };
@@ -424,7 +553,7 @@ ${contentSections}`,
424
553
  const overall = Math.round(
425
554
  (identity.score + verification.score + state.score + memory.score + constraints.score) / 5
426
555
  );
427
- const xref = crossRef(files);
556
+ const xref = crossRef(files, mappings);
428
557
  return { identity, verification, state, memory, constraints, overall, crossRef: xref };
429
558
  }
430
559
 
@@ -485,6 +614,7 @@ function report(scored, opts) {
485
614
  if (opts.json) {
486
615
  const out = {
487
616
  overall: scored.overall,
617
+ ...opts.tokenUsage !== void 0 ? { token_usage: opts.tokenUsage } : {},
488
618
  subsystems: {
489
619
  identity: { score: scored.identity.score, gaps: scored.identity.gaps, files: scored.identity.files },
490
620
  verification: { score: scored.verification.score, gaps: scored.verification.gaps, files: scored.verification.files },
@@ -541,16 +671,28 @@ var OPENAI_FALLBACK_MODELS = [
541
671
  { id: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }
542
672
  ];
543
673
 
674
+ // src/utils/tokens.ts
675
+ function estimateTokens(text) {
676
+ return Math.ceil(text.length / 4);
677
+ }
678
+
544
679
  // src/utils/llm.ts
680
+ function estimateChatTokens(system, user, response) {
681
+ return estimateTokens(system) + estimateTokens(user) + estimateTokens(response);
682
+ }
545
683
  var ANTHROPIC_FAST = "claude-haiku-4-5-20251001";
546
684
  var ANTHROPIC_QUALITY = "claude-sonnet-4-6";
547
685
  var AnthropicProvider = class {
548
686
  client;
549
687
  modelId;
688
+ totalTokens = 0;
550
689
  constructor(apiKey, modelId) {
551
690
  this.client = new import_sdk.default({ apiKey });
552
691
  this.modelId = modelId;
553
692
  }
693
+ getTotalTokens() {
694
+ return this.totalTokens;
695
+ }
554
696
  async chat(system, user, opts) {
555
697
  const model = this.modelId ?? (opts?.fast ? ANTHROPIC_FAST : ANTHROPIC_QUALITY);
556
698
  const maxTokens = opts?.fast ? 1024 : 2048;
@@ -561,7 +703,9 @@ var AnthropicProvider = class {
561
703
  messages: [{ role: "user", content: user }]
562
704
  });
563
705
  const block = response.content.find((b) => b.type === "text");
564
- return block && block.type === "text" ? block.text : "";
706
+ const text = block && block.type === "text" ? block.text : "";
707
+ this.totalTokens += estimateChatTokens(system, user, text);
708
+ return text;
565
709
  }
566
710
  };
567
711
  var OPENAI_FAST = "gpt-4o-mini";
@@ -569,11 +713,15 @@ var OPENAI_QUALITY = "gpt-4o";
569
713
  var OpenAIProvider = class {
570
714
  client;
571
715
  modelId;
716
+ totalTokens = 0;
572
717
  constructor(apiKey, modelId) {
573
718
  const baseURL = process.env["OPENAI_BASE_URL"];
574
719
  this.client = new import_openai.default({ apiKey, ...baseURL ? { baseURL } : {} });
575
720
  this.modelId = modelId;
576
721
  }
722
+ getTotalTokens() {
723
+ return this.totalTokens;
724
+ }
577
725
  async chat(system, user, opts) {
578
726
  const model = this.modelId ?? (opts?.fast ? OPENAI_FAST : OPENAI_QUALITY);
579
727
  const response = await this.client.chat.completions.create({
@@ -583,12 +731,15 @@ var OpenAIProvider = class {
583
731
  { role: "user", content: user }
584
732
  ]
585
733
  });
586
- return response.choices[0]?.message?.content ?? "";
734
+ const text = response.choices[0]?.message?.content ?? "";
735
+ this.totalTokens += estimateChatTokens(system, user, text);
736
+ return text;
587
737
  }
588
738
  };
589
739
  var OllamaProvider = class {
590
740
  client;
591
741
  model;
742
+ totalTokens = 0;
592
743
  constructor() {
593
744
  const host = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
594
745
  this.model = process.env["OLLAMA_MODEL"] ?? "llama3";
@@ -597,6 +748,9 @@ var OllamaProvider = class {
597
748
  baseURL: `${host}/v1`
598
749
  });
599
750
  }
751
+ getTotalTokens() {
752
+ return this.totalTokens;
753
+ }
600
754
  async chat(system, user) {
601
755
  const response = await this.client.chat.completions.create({
602
756
  model: this.model,
@@ -605,7 +759,9 @@ var OllamaProvider = class {
605
759
  { role: "user", content: user }
606
760
  ]
607
761
  });
608
- return response.choices[0]?.message?.content ?? "";
762
+ const text = response.choices[0]?.message?.content ?? "";
763
+ this.totalTokens += estimateChatTokens(system, user, text);
764
+ return text;
609
765
  }
610
766
  };
611
767
  var NON_CHAT_MODEL_SUBSTRINGS = [
@@ -747,9 +903,15 @@ async function runAudit(target, opts) {
747
903
  const provider = createProvider(config.provider, config.apiKey, config.modelId);
748
904
  const targetDir = (0, import_path3.resolve)(target);
749
905
  const files = loadRepo(targetDir);
750
- const mappings = await mapFiles(files.mdFiles, provider);
906
+ const mappings = await mapFiles(files.mdFiles, provider, files.usedGraphify);
751
907
  const scored = await scoreRepo(files, mappings, provider);
752
- report(scored, { json: opts.json });
908
+ const totalTokens = provider.getTotalTokens();
909
+ report(scored, { json: opts.json, tokenUsage: totalTokens });
910
+ if (!opts.json) {
911
+ const display = totalTokens >= 1e3 ? `~${Math.ceil(totalTokens / 1e3)}k` : `~${totalTokens}`;
912
+ console.log(`
913
+ Tokens used: ${display}`);
914
+ }
753
915
  if (scored.overall < opts.minScore) {
754
916
  process.exit(1);
755
917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sicilianwildcat/aiready",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Audit repositories for AI agent readiness",
5
5
  "license": "MIT",
6
6
  "author": "harikrishn4a",