Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node

import { scan } from '../src/scanner.mjs';
import { score } from '../src/scorer.mjs';
import { report, reportInteractive, reportJson, createSpinner } from '../src/reporter.mjs';
import { score, scoreAllTools } from '../src/scorer.mjs';
import { report, reportInteractive, reportJson, reportTools, reportToolsInteractive, reportToolsJson, createSpinner } from '../src/reporter.mjs';
import { resolve, basename } from 'path';

// ── Parse args ────────────────────────────────────────────
Expand All @@ -14,6 +14,7 @@ const flags = {
version: false,
noInteractive: false,
badge: false,
tools: false,
};
let targetPath = '.';

Expand All @@ -24,6 +25,7 @@ for (const arg of args) {
else if (arg === '--version') flags.version = true;
else if (arg === '--no-interactive' || arg === '--ci') flags.noInteractive = true;
else if (arg === 'badge') flags.badge = true;
else if (arg === '--tools' || arg === '-t') flags.tools = true;
else if (!arg.startsWith('-')) targetPath = arg;
}

Expand All @@ -44,6 +46,7 @@ if (flags.help) {

Options
--json Output results as JSON
--tools, -t Show per-tool readiness scores
--verbose, -v Show all recommendations (including low-priority)
--no-interactive Disable animated output (auto-detected in CI / pipes)
--ci Alias for --no-interactive
Expand All @@ -54,6 +57,7 @@ if (flags.help) {
$ check-ai # audit current directory
$ check-ai ./my-project # audit a specific repo
$ check-ai --json # machine-readable output
$ check-ai --tools # show per-tool breakdown
$ check-ai . --verbose # include nice-to-have suggestions
$ check-ai badge # output a Markdown badge
`);
Expand Down Expand Up @@ -83,7 +87,7 @@ if (interactive) {
spinner.start(`Auditing ${repoName} …`);
} else if (!flags.json && !flags.badge) {
console.log('');
console.log(` πŸ” Auditing ${repoName} …`);
console.log(` [SEARCH] Auditing ${repoName} …`);
}

// Progress callback for interactive mode
Expand All @@ -106,19 +110,32 @@ const onProgress = interactive
const findings = await scan(targetDir, onProgress);
const result = score(findings);

// ── Per-tool scoring (if requested) ───────────────────────
const toolResults = flags.tools ? scoreAllTools(findings) : null;

if (flags.badge) {
const badgeColor = result.color === 'green' ? 'brightgreen' : result.color === 'yellow' ? 'yellow' : 'red';
const label = 'AI Ready';
const message = `${result.grade} ${result.normalized}/10`;
const url = `https://img.shields.io/badge/${encodeURIComponent(label)}-${encodeURIComponent(message)}-${badgeColor}`;
console.log(`[![AI Ready](${url})](https://github.com/f/check-ai)`);
} else if (flags.json) {
reportJson(result, findings);
if (flags.tools && toolResults) {
reportToolsJson(toolResults);
} else {
reportJson(result, findings);
}
} else if (interactive) {
await reportInteractive(result, findings, { verbose: flags.verbose });
if (flags.tools && toolResults) {
await reportToolsInteractive(toolResults, { verbose: flags.verbose });
}
} else {
console.log('');
report(result, findings, { verbose: flags.verbose });
if (flags.tools && toolResults) {
reportTools(toolResults, { verbose: flags.verbose });
}
}

// Exit with non-zero if score is below threshold (useful in CI)
Expand Down
50 changes: 50 additions & 0 deletions src/audits/agent-configs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const TOOL_SIGNALS = [
'.augment-guidelines.md',
'.qodo',
'.kiro',
// CLIO - Perl-based AI coding assistant
'.clio',
'.clio/instructions.md',
];

export const checks = [
Expand Down Expand Up @@ -410,6 +413,53 @@ export const checks = [
type: 'any',
description: 'Qodo AI code quality configuration',
},

// CLIO (Command Line Intelligence Orchestrator)
{
id: 'clio-dir',
label: '.clio/',
section,
weight: 0,
paths: ['.clio'],
type: 'dir',
description: 'CLIO AI assistant configuration directory',
},
{
id: 'clio-instructions',
label: '.clio/instructions.md',
section,
weight: 0,
paths: ['.clio/instructions.md'],
type: 'file',
description: 'CLIO project-specific agent instructions',
},
{
id: 'clio-ltm',
label: '.clio/ltm.json',
section,
weight: 0,
paths: ['.clio/ltm.json'],
type: 'file',
description: 'CLIO long-term memory (discoveries, solutions, patterns)',
},
{
id: 'clio-memory',
label: '.clio/memory/',
section,
weight: 0,
paths: ['.clio/memory'],
type: 'dir',
description: 'CLIO session memory storage',
},
{
id: 'clio-sessions',
label: '.clio/sessions/',
section,
weight: 0,
paths: ['.clio/sessions'],
type: 'dir',
description: 'CLIO session state persistence',
},
];

/**
Expand Down
15 changes: 15 additions & 0 deletions src/audits/ai-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,21 @@ export function analyze(rootDir, ctx) {
}
}

// Check for self-contained AI tools (projects that ARE AI tools)
// These implement their own AI integration rather than using external SDKs
const selfContainedSignals = [
'.clio/instructions.md', // CLIO - Perl AI assistant
'lib/CLIO', // CLIO source code
];

for (const signal of selfContainedSignals) {
const fullPath = ctx.join(rootDir, signal);
if (ctx.existsSync(fullPath)) {
found.push('native-ai-tool');
break;
}
}

return {
found: found.length > 0,
matches: found,
Expand Down
3 changes: 3 additions & 0 deletions src/audits/grounding-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export const checks = [
'docs/conventions.md',
'docs/development.md',
'CODING_GUIDELINES.md',
'docs/DEVELOPER_GUIDE.md',
'docs/developer_guide.md',
'DEVELOPER_GUIDE.md',
],
type: 'file',
description: 'Coding conventions and development setup guide for agents',
Expand Down
4 changes: 4 additions & 0 deletions src/audits/testing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const checks = [
'e2e',
'cypress',
'playwright',
't',
],
type: 'dir',
description: 'Tests catch agent-introduced regressions',
Expand All @@ -46,6 +47,9 @@ export const checks = [
'pytest.ini',
'conftest.py',
'.rspec',
't/test.pl',
'tests/test.pl',
'test.pl',
],
type: 'file',
description: 'Configured test runner for automated verification',
Expand Down
133 changes: 133 additions & 0 deletions src/reporter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,136 @@ export function reportJson(result, findings) {

console.log(JSON.stringify(output, null, 2));
}


// ───────────────────────────────────────────────────────────────────────
// Per-Tool Report
// ───────────────────────────────────────────────────────────────────────

function toolBar(percentage, width = 16) {
const filled = Math.round((percentage / 100) * width);
const empty = width - filled;
const color = percentage >= 70 ? GREEN : percentage >= 40 ? YELLOW : RED;
return `${color}${'β–ˆ'.repeat(filled)}${DIM}${'β–‘'.repeat(empty)}${RESET}`;
}

export function reportTools(toolResults, opts = {}) {
const { toolScores } = toolResults;
const tools = Object.values(toolScores);

if (tools.length === 0) {
console.log(` ${DIM}No configured tools detected.${RESET}`);
console.log('');
return;
}

console.log(` ${BOLD}Per-Tool Readiness${RESET}`);
console.log('');

// Sort by percentage descending
tools.sort((a, b) => b.percentage - a.percentage);

for (const tool of tools) {
const pctColor = tool.percentage >= 70 ? GREEN : tool.percentage >= 40 ? YELLOW : RED;
const icon = tool.icon || '[DONE]';

console.log(
` ${icon} ${BOLD}${tool.name.padEnd(14)}${RESET} ${toolBar(tool.percentage)} ` +
`${pctColor}${tool.percentage.toString().padStart(3)}%${RESET} ` +
`${DIM}(${tool.earned}/${tool.max})${RESET}`
);

// Show required check status if verbose
if (opts.verbose && tool.required.total > 0) {
for (const check of tool.required.checks) {
const status = check.found ? `${GREEN}${RESET}` : `${RED}${RESET}`;
console.log(` ${status} ${DIM}${check.label} (required)${RESET}`);
}
}
}

console.log('');
}

export async function reportToolsInteractive(toolResults, opts = {}) {
const { toolScores } = toolResults;
const tools = Object.values(toolScores);

if (tools.length === 0) {
console.log(` ${DIM}No configured tools detected.${RESET}`);
console.log('');
return;
}

console.log(` ${BOLD}Per-Tool Readiness${RESET}`);
console.log('');

// Sort by percentage descending
tools.sort((a, b) => b.percentage - a.percentage);

for (const tool of tools) {
const pctColor = tool.percentage >= 70 ? GREEN : tool.percentage >= 40 ? YELLOW : RED;
const icon = tool.icon || '[DONE]';

// Animate bar fill
w(` ${icon} ${BOLD}${tool.name.padEnd(14)}${RESET} `);
await animateToolBar(tool.percentage, 16, 8);
w(` ${pctColor}${tool.percentage.toString().padStart(3)}%${RESET} `);
w(`${DIM}(${tool.earned}/${tool.max})${RESET}\n`);

// Show required check status if verbose
if (opts.verbose && tool.required.total > 0) {
for (const check of tool.required.checks) {
await sleep(20);
const status = check.found ? `${GREEN}${RESET}` : `${RED}${RESET}`;
console.log(` ${status} ${DIM}${check.label} (required)${RESET}`);
}
}

await sleep(50);
}

console.log('');
}

async function animateToolBar(percentage, width = 16, stepMs = 8) {
const target = Math.round((percentage / 100) * width);
const color = percentage >= 70 ? GREEN : percentage >= 40 ? YELLOW : RED;

for (let i = 0; i <= target; i++) {
const empty = width - i;
w(`\r${color}${'β–ˆ'.repeat(i)}${DIM}${'β–‘'.repeat(empty)}${RESET}`);
await sleep(stepMs);
}
// Final position without \r
const final = target;
const empty = width - final;
w(`${color}${'β–ˆ'.repeat(final)}${DIM}${'β–‘'.repeat(empty)}${RESET}`);
}

export function reportToolsJson(toolResults) {
const output = {
configuredTools: toolResults.configuredTools,
tools: {},
};

for (const [key, tool] of Object.entries(toolResults.toolScores)) {
output.tools[key] = {
name: tool.name,
percentage: tool.percentage,
score: tool.normalized,
grade: tool.grade,
points: { earned: tool.earned, max: tool.max },
required: {
passed: tool.required.passed,
total: tool.required.total,
},
valuable: {
passed: tool.valuable.passed,
total: tool.valuable.total,
},
};
}

console.log(JSON.stringify(output, null, 2));
}
Loading