pinata-security-cli 0.4.0 → 0.4.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/README.md +37 -7
- package/dist/cli/index.js +232 -20
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/categories/definitions/security/sql-injection.yml +29 -0
package/README.md
CHANGED
|
@@ -90,7 +90,8 @@ dist/
|
|
|
90
90
|
--dry-run # Preview generated tests without running
|
|
91
91
|
--confidence <level> # high (default), medium, low
|
|
92
92
|
--output <format> # terminal, json, sarif, junit, markdown
|
|
93
|
-
--
|
|
93
|
+
--output-file <path> # Write results to file (for SARIF upload)
|
|
94
|
+
--domains <domains> # security, data, concurrency, etc.
|
|
94
95
|
--severity <level> # critical, high, medium, low
|
|
95
96
|
--exclude <dirs> # Comma-separated directories to skip
|
|
96
97
|
```
|
|
@@ -149,21 +150,50 @@ pinata analyze . --execute --dry-run
|
|
|
149
150
|
|
|
150
151
|
## CI/CD Integration
|
|
151
152
|
|
|
152
|
-
**GitHub
|
|
153
|
+
**GitHub Action (recommended)**
|
|
154
|
+
|
|
153
155
|
```yaml
|
|
154
156
|
name: Security Scan
|
|
155
157
|
on: [push, pull_request]
|
|
156
158
|
|
|
157
159
|
jobs:
|
|
158
|
-
|
|
160
|
+
security:
|
|
159
161
|
runs-on: ubuntu-latest
|
|
162
|
+
permissions:
|
|
163
|
+
contents: read
|
|
164
|
+
security-events: write
|
|
160
165
|
steps:
|
|
161
166
|
- uses: actions/checkout@v4
|
|
162
|
-
-
|
|
163
|
-
run: npx --yes pinata-security-cli@latest analyze . --output sarif > results.sarif
|
|
164
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
167
|
+
- uses: christiancattaneo/pinata-security@v1
|
|
165
168
|
with:
|
|
166
|
-
|
|
169
|
+
confidence: high
|
|
170
|
+
sarif-output: pinata.sarif
|
|
171
|
+
# Optional: AI verification
|
|
172
|
+
# with:
|
|
173
|
+
# verify: true
|
|
174
|
+
# env:
|
|
175
|
+
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Action inputs:**
|
|
179
|
+
- `path` - Directory to scan (default: `.`)
|
|
180
|
+
- `confidence` - high, medium, low (default: `high`)
|
|
181
|
+
- `domains` - Comma-separated domains to scan
|
|
182
|
+
- `verify` - Enable AI verification (default: `false`)
|
|
183
|
+
- `fail-on-gaps` - Fail if gaps found (default: `true`)
|
|
184
|
+
- `sarif-output` - Path for SARIF file (auto-uploads to GitHub Security)
|
|
185
|
+
|
|
186
|
+
**Action outputs:**
|
|
187
|
+
- `score` - Pinata score (0-100)
|
|
188
|
+
- `gaps` - Number of gaps found
|
|
189
|
+
- `sarif-file` - Path to SARIF file
|
|
190
|
+
|
|
191
|
+
**Manual workflow (any CI)**
|
|
192
|
+
```yaml
|
|
193
|
+
- run: npx --yes pinata-security-cli@latest analyze . --output sarif --output-file results.sarif
|
|
194
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
195
|
+
with:
|
|
196
|
+
sarif_file: results.sarif
|
|
167
197
|
```
|
|
168
198
|
|
|
169
199
|
**GitLab CI**
|
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
4
4
|
import path, { dirname, resolve, join, basename, relative, extname } from 'path';
|
|
5
|
-
import { existsSync,
|
|
5
|
+
import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
|
|
6
6
|
import { homedir, tmpdir } from 'os';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
8
|
import { useState } from 'react';
|
|
@@ -2553,6 +2553,174 @@ var init_tui = __esm({
|
|
|
2553
2553
|
init_App();
|
|
2554
2554
|
}
|
|
2555
2555
|
});
|
|
2556
|
+
|
|
2557
|
+
// src/feedback/types.ts
|
|
2558
|
+
function suggestConfidence(precision) {
|
|
2559
|
+
if (precision >= CONFIDENCE_THRESHOLDS.high) return "high";
|
|
2560
|
+
if (precision >= CONFIDENCE_THRESHOLDS.medium) return "medium";
|
|
2561
|
+
return "low";
|
|
2562
|
+
}
|
|
2563
|
+
var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
|
|
2564
|
+
var init_types3 = __esm({
|
|
2565
|
+
"src/feedback/types.ts"() {
|
|
2566
|
+
EMPTY_FEEDBACK_STATE = {
|
|
2567
|
+
version: 1,
|
|
2568
|
+
patterns: {},
|
|
2569
|
+
totalScans: 0,
|
|
2570
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2571
|
+
};
|
|
2572
|
+
CONFIDENCE_THRESHOLDS = {
|
|
2573
|
+
/** Precision >= 0.7 → high confidence */
|
|
2574
|
+
high: 0.7,
|
|
2575
|
+
/** Precision >= 0.4 → medium confidence */
|
|
2576
|
+
medium: 0.4,
|
|
2577
|
+
/** Precision < 0.4 → low confidence */
|
|
2578
|
+
low: 0
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
async function loadFeedback() {
|
|
2583
|
+
try {
|
|
2584
|
+
const content = await readFile(FEEDBACK_FILE, "utf-8");
|
|
2585
|
+
const state = JSON.parse(content);
|
|
2586
|
+
if (state.version !== 1) {
|
|
2587
|
+
console.warn("Feedback version mismatch, resetting...");
|
|
2588
|
+
return { ...EMPTY_FEEDBACK_STATE };
|
|
2589
|
+
}
|
|
2590
|
+
return state;
|
|
2591
|
+
} catch {
|
|
2592
|
+
return { ...EMPTY_FEEDBACK_STATE };
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
async function saveFeedback(state) {
|
|
2596
|
+
try {
|
|
2597
|
+
await mkdir(FEEDBACK_DIR, { recursive: true });
|
|
2598
|
+
await writeFile(FEEDBACK_FILE, JSON.stringify(state, null, 2));
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
console.warn(`Failed to save feedback: ${error instanceof Error ? error.message : String(error)}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function applyUpdates(state, updates) {
|
|
2604
|
+
const newState = { ...state };
|
|
2605
|
+
newState.patterns = { ...state.patterns };
|
|
2606
|
+
for (const update of updates) {
|
|
2607
|
+
const existing = newState.patterns[update.patternId];
|
|
2608
|
+
const pattern = existing ?? {
|
|
2609
|
+
patternId: update.patternId,
|
|
2610
|
+
categoryId: update.categoryId,
|
|
2611
|
+
totalMatches: 0,
|
|
2612
|
+
confirmedCount: 0,
|
|
2613
|
+
unconfirmedCount: 0,
|
|
2614
|
+
aiDismissedCount: 0,
|
|
2615
|
+
aiVerifiedCount: 0,
|
|
2616
|
+
precision: 0,
|
|
2617
|
+
suggestedConfidence: "medium",
|
|
2618
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2619
|
+
};
|
|
2620
|
+
switch (update.outcome) {
|
|
2621
|
+
case "matched":
|
|
2622
|
+
pattern.totalMatches++;
|
|
2623
|
+
break;
|
|
2624
|
+
case "confirmed":
|
|
2625
|
+
pattern.confirmedCount++;
|
|
2626
|
+
break;
|
|
2627
|
+
case "unconfirmed":
|
|
2628
|
+
pattern.unconfirmedCount++;
|
|
2629
|
+
break;
|
|
2630
|
+
case "ai_verified":
|
|
2631
|
+
pattern.aiVerifiedCount++;
|
|
2632
|
+
break;
|
|
2633
|
+
case "ai_dismissed":
|
|
2634
|
+
pattern.aiDismissedCount++;
|
|
2635
|
+
break;
|
|
2636
|
+
}
|
|
2637
|
+
const total = pattern.confirmedCount + pattern.unconfirmedCount;
|
|
2638
|
+
pattern.precision = total > 0 ? pattern.confirmedCount / total : 0.5;
|
|
2639
|
+
pattern.suggestedConfidence = suggestConfidence(pattern.precision);
|
|
2640
|
+
pattern.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2641
|
+
newState.patterns[update.patternId] = pattern;
|
|
2642
|
+
}
|
|
2643
|
+
newState.totalScans++;
|
|
2644
|
+
newState.lastScanAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2645
|
+
return newState;
|
|
2646
|
+
}
|
|
2647
|
+
function getConfidenceAdjustment(state, patternId) {
|
|
2648
|
+
const pattern = state.patterns[patternId];
|
|
2649
|
+
if (!pattern) return null;
|
|
2650
|
+
const totalExecutions = pattern.confirmedCount + pattern.unconfirmedCount;
|
|
2651
|
+
if (totalExecutions < 5) return null;
|
|
2652
|
+
return pattern.suggestedConfidence;
|
|
2653
|
+
}
|
|
2654
|
+
function getLowPrecisionPatterns(state, threshold = 0.3) {
|
|
2655
|
+
return Object.values(state.patterns).filter((p) => {
|
|
2656
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
2657
|
+
return total >= 5 && p.precision < threshold;
|
|
2658
|
+
}).sort((a, b) => a.precision - b.precision);
|
|
2659
|
+
}
|
|
2660
|
+
function getHighPrecisionPatterns(state, threshold = 0.8) {
|
|
2661
|
+
return Object.values(state.patterns).filter((p) => {
|
|
2662
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
2663
|
+
return total >= 5 && p.precision >= threshold;
|
|
2664
|
+
}).sort((a, b) => b.precision - a.precision);
|
|
2665
|
+
}
|
|
2666
|
+
function generateReport(state) {
|
|
2667
|
+
const lines = [
|
|
2668
|
+
"# Pinata Feedback Report",
|
|
2669
|
+
"",
|
|
2670
|
+
`Total scans: ${state.totalScans}`,
|
|
2671
|
+
`Last scan: ${state.lastScanAt}`,
|
|
2672
|
+
`Patterns tracked: ${Object.keys(state.patterns).length}`,
|
|
2673
|
+
""
|
|
2674
|
+
];
|
|
2675
|
+
const lowPrecision = getLowPrecisionPatterns(state);
|
|
2676
|
+
if (lowPrecision.length > 0) {
|
|
2677
|
+
lines.push("## Low Precision Patterns (potential false positive sources)");
|
|
2678
|
+
lines.push("");
|
|
2679
|
+
for (const p of lowPrecision.slice(0, 10)) {
|
|
2680
|
+
lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
|
|
2681
|
+
}
|
|
2682
|
+
lines.push("");
|
|
2683
|
+
}
|
|
2684
|
+
const highPrecision = getHighPrecisionPatterns(state);
|
|
2685
|
+
if (highPrecision.length > 0) {
|
|
2686
|
+
lines.push("## High Precision Patterns");
|
|
2687
|
+
lines.push("");
|
|
2688
|
+
for (const p of highPrecision.slice(0, 10)) {
|
|
2689
|
+
lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
|
|
2690
|
+
}
|
|
2691
|
+
lines.push("");
|
|
2692
|
+
}
|
|
2693
|
+
return lines.join("\n");
|
|
2694
|
+
}
|
|
2695
|
+
var FEEDBACK_DIR, FEEDBACK_FILE;
|
|
2696
|
+
var init_store = __esm({
|
|
2697
|
+
"src/feedback/store.ts"() {
|
|
2698
|
+
init_types3();
|
|
2699
|
+
FEEDBACK_DIR = join(homedir(), ".pinata");
|
|
2700
|
+
FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
// src/feedback/index.ts
|
|
2705
|
+
var feedback_exports = {};
|
|
2706
|
+
__export(feedback_exports, {
|
|
2707
|
+
CONFIDENCE_THRESHOLDS: () => CONFIDENCE_THRESHOLDS,
|
|
2708
|
+
EMPTY_FEEDBACK_STATE: () => EMPTY_FEEDBACK_STATE,
|
|
2709
|
+
applyUpdates: () => applyUpdates,
|
|
2710
|
+
generateReport: () => generateReport,
|
|
2711
|
+
getConfidenceAdjustment: () => getConfidenceAdjustment,
|
|
2712
|
+
getHighPrecisionPatterns: () => getHighPrecisionPatterns,
|
|
2713
|
+
getLowPrecisionPatterns: () => getLowPrecisionPatterns,
|
|
2714
|
+
loadFeedback: () => loadFeedback,
|
|
2715
|
+
saveFeedback: () => saveFeedback,
|
|
2716
|
+
suggestConfidence: () => suggestConfidence
|
|
2717
|
+
});
|
|
2718
|
+
var init_feedback = __esm({
|
|
2719
|
+
"src/feedback/index.ts"() {
|
|
2720
|
+
init_types3();
|
|
2721
|
+
init_store();
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2556
2724
|
var RiskDomainSchema = z.enum([
|
|
2557
2725
|
"security",
|
|
2558
2726
|
"data",
|
|
@@ -5492,9 +5660,9 @@ var AIService = class {
|
|
|
5492
5660
|
getApiKeyFromConfig(provider) {
|
|
5493
5661
|
try {
|
|
5494
5662
|
const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
|
|
5495
|
-
const { homedir:
|
|
5496
|
-
const { join:
|
|
5497
|
-
const configPath =
|
|
5663
|
+
const { homedir: homedir3 } = __require("os");
|
|
5664
|
+
const { join: join5 } = __require("path");
|
|
5665
|
+
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
5498
5666
|
if (!existsSync4(configPath)) {
|
|
5499
5667
|
return "";
|
|
5500
5668
|
}
|
|
@@ -6882,7 +7050,7 @@ function getDefinitionsPath() {
|
|
|
6882
7050
|
}
|
|
6883
7051
|
var program = new Command();
|
|
6884
7052
|
program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
|
|
6885
|
-
program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
|
|
7053
|
+
program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
|
|
6886
7054
|
const isQuiet = Boolean(options["quiet"]);
|
|
6887
7055
|
const isVerbose = Boolean(options["verbose"]);
|
|
6888
7056
|
if (isQuiet) {
|
|
@@ -7005,12 +7173,12 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
7005
7173
|
const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
|
|
7006
7174
|
try {
|
|
7007
7175
|
const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
|
|
7008
|
-
const { readFile:
|
|
7176
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
7009
7177
|
const apiKey = getApiKey2(provider);
|
|
7010
7178
|
const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
|
|
7011
7179
|
const { verified, dismissed, stats } = await verifier.verifyAll(
|
|
7012
7180
|
scanResult.data.gaps,
|
|
7013
|
-
async (path2) =>
|
|
7181
|
+
async (path2) => readFile6(path2, "utf-8")
|
|
7014
7182
|
);
|
|
7015
7183
|
scanResult.data.gaps = verified;
|
|
7016
7184
|
const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
|
|
@@ -7047,7 +7215,7 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
7047
7215
|
const isDryRun = Boolean(options["dryRun"]);
|
|
7048
7216
|
if (shouldExecute && scanResult.data.gaps.length > 0) {
|
|
7049
7217
|
const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
|
|
7050
|
-
const { readFile:
|
|
7218
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
7051
7219
|
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
7052
7220
|
if (testableGaps.length === 0) {
|
|
7053
7221
|
console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
|
|
@@ -7063,7 +7231,7 @@ Dynamic execution unavailable: ${initResult.error}`));
|
|
|
7063
7231
|
for (const gap of testableGaps) {
|
|
7064
7232
|
if (!fileContents.has(gap.filePath)) {
|
|
7065
7233
|
try {
|
|
7066
|
-
fileContents.set(gap.filePath, await
|
|
7234
|
+
fileContents.set(gap.filePath, await readFile6(gap.filePath, "utf-8"));
|
|
7067
7235
|
} catch {
|
|
7068
7236
|
}
|
|
7069
7237
|
}
|
|
@@ -7090,7 +7258,14 @@ Dynamic execution unavailable: ${initResult.error}`));
|
|
|
7090
7258
|
logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
|
|
7091
7259
|
}
|
|
7092
7260
|
const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
|
|
7093
|
-
|
|
7261
|
+
const outputFile = options["outputFile"];
|
|
7262
|
+
if (outputFile) {
|
|
7263
|
+
const outputPath = resolve(outputFile);
|
|
7264
|
+
writeFileSync(outputPath, output, "utf-8");
|
|
7265
|
+
logger.info(`Results written to: ${outputPath}`);
|
|
7266
|
+
} else {
|
|
7267
|
+
console.log(output);
|
|
7268
|
+
}
|
|
7094
7269
|
if (isVerbose && scanResult.data.warnings.length > 0) {
|
|
7095
7270
|
console.error("\nWarnings:");
|
|
7096
7271
|
for (const warning of scanResult.data.warnings) {
|
|
@@ -7497,8 +7672,8 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
7497
7672
|
let vulnerableCode = [...codeSnippets];
|
|
7498
7673
|
if (filePath) {
|
|
7499
7674
|
try {
|
|
7500
|
-
const { readFile:
|
|
7501
|
-
const content = await
|
|
7675
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
7676
|
+
const content = await readFile6(filePath, "utf-8");
|
|
7502
7677
|
vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
|
|
7503
7678
|
} catch (error) {
|
|
7504
7679
|
console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
|
|
@@ -7797,16 +7972,16 @@ thresholds:
|
|
|
7797
7972
|
high: 5
|
|
7798
7973
|
medium: 20
|
|
7799
7974
|
`;
|
|
7800
|
-
const { writeFile: writeFileAsync, mkdir:
|
|
7975
|
+
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
7801
7976
|
try {
|
|
7802
7977
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
7803
7978
|
console.log(chalk5.green("Created .pinata.yml"));
|
|
7804
|
-
await
|
|
7979
|
+
await mkdir4(cacheDir, { recursive: true });
|
|
7805
7980
|
console.log(chalk5.green("Created .pinata/ directory"));
|
|
7806
7981
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
7807
7982
|
if (existsSync(gitignorePath)) {
|
|
7808
|
-
const { readFile:
|
|
7809
|
-
const gitignore = await
|
|
7983
|
+
const { readFile: readFile6, appendFile } = await import('fs/promises');
|
|
7984
|
+
const gitignore = await readFile6(gitignorePath, "utf8");
|
|
7810
7985
|
if (!gitignore.includes(".pinata/")) {
|
|
7811
7986
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
7812
7987
|
console.log(chalk5.green("Added .pinata/ to .gitignore"));
|
|
@@ -7950,6 +8125,43 @@ Warnings (${warnings.length}):`));
|
|
|
7950
8125
|
process.exit(1);
|
|
7951
8126
|
}
|
|
7952
8127
|
});
|
|
8128
|
+
program.command("feedback").description("View pattern performance feedback (Layer 6)").option("--reset", "Reset all feedback data").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").action(async (options) => {
|
|
8129
|
+
const { loadFeedback: loadFeedback2, saveFeedback: saveFeedback2, generateReport: generateReport2, EMPTY_FEEDBACK_STATE: EMPTY_FEEDBACK_STATE2 } = await Promise.resolve().then(() => (init_feedback(), feedback_exports));
|
|
8130
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
8131
|
+
const shouldReset = Boolean(options["reset"]);
|
|
8132
|
+
if (shouldReset) {
|
|
8133
|
+
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
8134
|
+
console.log(chalk5.green("Feedback data reset."));
|
|
8135
|
+
return;
|
|
8136
|
+
}
|
|
8137
|
+
const state = await loadFeedback2();
|
|
8138
|
+
if (outputFormat === "json") {
|
|
8139
|
+
console.log(JSON.stringify(state, null, 2));
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8142
|
+
if (outputFormat === "markdown") {
|
|
8143
|
+
console.log(generateReport2(state));
|
|
8144
|
+
return;
|
|
8145
|
+
}
|
|
8146
|
+
console.log(chalk5.bold("\nPinata Feedback Report\n"));
|
|
8147
|
+
console.log(`Total scans: ${state.totalScans}`);
|
|
8148
|
+
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
8149
|
+
if (state.totalScans === 0) {
|
|
8150
|
+
console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
8154
|
+
if (patterns.length > 0) {
|
|
8155
|
+
console.log(chalk5.bold("\nPattern Performance:"));
|
|
8156
|
+
for (const p of patterns.slice(0, 15)) {
|
|
8157
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
8158
|
+
const precisionPct = (p.precision * 100).toFixed(0);
|
|
8159
|
+
const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
|
|
8160
|
+
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
console.log();
|
|
8164
|
+
});
|
|
7953
8165
|
var config = program.command("config").description("Manage AI provider configuration");
|
|
7954
8166
|
config.command("set <key> <value>").description("Set a configuration value").addHelpText("after", `
|
|
7955
8167
|
Available keys:
|
|
@@ -8078,9 +8290,9 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
8078
8290
|
}
|
|
8079
8291
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
8080
8292
|
const authPath = resolve(configDir, "auth.json");
|
|
8081
|
-
const { mkdir:
|
|
8293
|
+
const { mkdir: mkdir4, writeFile: writeFileAsync } = await import('fs/promises');
|
|
8082
8294
|
try {
|
|
8083
|
-
await
|
|
8295
|
+
await mkdir4(configDir, { recursive: true });
|
|
8084
8296
|
const maskedKey = `****${apiKey.slice(-8)}`;
|
|
8085
8297
|
const authData = {
|
|
8086
8298
|
configured: true,
|
|
@@ -8133,8 +8345,8 @@ auth.command("status").description("Check authentication status").action(async (
|
|
|
8133
8345
|
process.exit(0);
|
|
8134
8346
|
}
|
|
8135
8347
|
try {
|
|
8136
|
-
const { readFile:
|
|
8137
|
-
const authData = JSON.parse(await
|
|
8348
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
8349
|
+
const authData = JSON.parse(await readFile6(authPath, "utf8"));
|
|
8138
8350
|
console.log(chalk5.green("Authenticated"));
|
|
8139
8351
|
console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
|
|
8140
8352
|
console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
|