guardlink 1.0.0 → 1.2.0
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/CHANGELOG.md +65 -0
- package/README.md +14 -0
- package/dist/agents/config.d.ts +8 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js +28 -5
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +2 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/launcher.d.ts +14 -0
- package/dist/agents/launcher.d.ts.map +1 -1
- package/dist/agents/launcher.js +126 -1
- package/dist/agents/launcher.js.map +1 -1
- package/dist/agents/prompts.d.ts +2 -2
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +251 -31
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/index.d.ts +34 -1
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +281 -8
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/llm.d.ts +54 -3
- package/dist/analyze/llm.d.ts.map +1 -1
- package/dist/analyze/llm.js +418 -97
- package/dist/analyze/llm.js.map +1 -1
- package/dist/analyze/prompts.d.ts +3 -2
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +227 -111
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyze/tools.d.ts +22 -0
- package/dist/analyze/tools.d.ts.map +1 -0
- package/dist/analyze/tools.js +230 -0
- package/dist/analyze/tools.js.map +1 -0
- package/dist/analyzer/sarif.js +1 -1
- package/dist/cli/index.d.ts +15 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +290 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +5 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +24 -12
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/diagrams.d.ts.map +1 -1
- package/dist/dashboard/diagrams.js +310 -37
- package/dist/dashboard/diagrams.js.map +1 -1
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +197 -64
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/init/picker.d.ts.map +1 -1
- package/dist/init/picker.js +2 -2
- package/dist/init/picker.js.map +1 -1
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +52 -32
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +14 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-line.js +3 -3
- package/dist/parser/parse-line.js.map +1 -1
- package/dist/parser/parse-project.js +1 -1
- package/dist/parser/validate.d.ts +31 -0
- package/dist/parser/validate.d.ts.map +1 -0
- package/dist/parser/validate.js +149 -0
- package/dist/parser/validate.js.map +1 -0
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +64 -0
- package/dist/report/report.js.map +1 -1
- package/dist/tui/commands.d.ts +3 -3
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +390 -206
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/config.d.ts +2 -0
- package/dist/tui/config.d.ts.map +1 -1
- package/dist/tui/config.js.map +1 -1
- package/dist/tui/format.d.ts +7 -0
- package/dist/tui/format.d.ts.map +1 -1
- package/dist/tui/format.js +59 -0
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +32 -19
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input.d.ts +2 -2
- package/dist/tui/input.js +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/tui/commands.js
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
* Returns void. Throws on fatal errors.
|
|
6
6
|
*/
|
|
7
7
|
import { resolve, basename } from 'node:path';
|
|
8
|
-
import { writeFileSync } from 'node:fs';
|
|
9
|
-
import { parseProject } from '../parser/index.js';
|
|
8
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from '../parser/index.js';
|
|
10
10
|
import { initProject, detectProject, promptAgentSelection } from '../init/index.js';
|
|
11
11
|
import { generateReport } from '../report/index.js';
|
|
12
12
|
import { generateDashboardHTML } from '../dashboard/index.js';
|
|
13
13
|
import { computeStats, computeSeverity, computeExposures } from '../dashboard/data.js';
|
|
14
|
-
import { generateThreatReport, serializeModel, listThreatReports, loadThreatReportsForDashboard, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage } from '../analyze/index.js';
|
|
14
|
+
import { generateThreatReport, serializeModel, listThreatReports, loadThreatReportsForDashboard, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage, buildProjectContext, extractCodeSnippets } from '../analyze/index.js';
|
|
15
15
|
import { diffModels, formatDiff, parseAtRef } from '../diff/index.js';
|
|
16
16
|
import { generateSarif } from '../analyzer/index.js';
|
|
17
|
-
import { C, severityBadge, severityText, severityTextPad, severityOrder, computeGrade, gradeColored, readCodeContext, trunc, bar, fileLink, fileLinkTrunc } from './format.js';
|
|
18
|
-
import { resolveLLMConfig, saveTuiConfig } from './config.js';
|
|
19
|
-
import { AGENTS, parseAgentFlag, launchAgent, copyToClipboard, buildAnnotatePrompt } from '../agents/index.js';
|
|
17
|
+
import { C, severityBadge, severityText, severityTextPad, severityOrder, computeGrade, gradeColored, readCodeContext, trunc, bar, fileLink, fileLinkTrunc, cleanCliArtifacts } from './format.js';
|
|
18
|
+
import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js';
|
|
19
|
+
import { AGENTS, parseAgentFlag, launchAgent, launchAgentInline, copyToClipboard, buildAnnotatePrompt } from '../agents/index.js';
|
|
20
20
|
import { describeConfigSource } from '../agents/config.js';
|
|
21
21
|
// ─── Shared context ──────────────────────────────────────────────────
|
|
22
22
|
/** Prompt user to pick an agent interactively (TUI only) */
|
|
@@ -61,19 +61,19 @@ export function cmdHelp() {
|
|
|
61
61
|
['/init [name]', 'Initialize GuardLink in this project'],
|
|
62
62
|
['/parse', 'Parse annotations, build threat model'],
|
|
63
63
|
['/status', 'Risk grade + summary stats'],
|
|
64
|
-
['/scan', 'Find unannotated security-relevant functions'],
|
|
65
64
|
['/validate [--strict]', 'Check for syntax errors + dangling refs'],
|
|
66
65
|
['', ''],
|
|
67
|
-
['/exposures [
|
|
68
|
-
['/show <n>', 'Detail view
|
|
66
|
+
['/exposures [--all]', 'List open exposures by severity (filter: --asset --severity --threat --file)'],
|
|
67
|
+
['/show <n>', 'Detail view + code context for an exposure (from /exposures list)'],
|
|
68
|
+
['/scan', 'Annotation coverage scanner — find unannotated symbols'],
|
|
69
69
|
['/assets', 'Asset tree with threat/control counts'],
|
|
70
70
|
['/files', 'Annotated file tree with exposure counts'],
|
|
71
71
|
['/view <file>', 'Show all annotations in a file with code context'],
|
|
72
72
|
['', ''],
|
|
73
|
-
['/threat-report <fw>', 'AI threat report (stride|dread|pasta|attacker|rapid|general)'],
|
|
73
|
+
['/threat-report <fw>', 'AI threat report (stride|dread|pasta|attacker|rapid|general|custom)'],
|
|
74
74
|
['/threat-reports', 'List saved AI threat reports'],
|
|
75
75
|
['/annotate <prompt>', 'Launch coding agent to annotate codebase'],
|
|
76
|
-
['/model', 'Set AI provider
|
|
76
|
+
['/model', 'Set AI provider (API or CLI agent: Claude Code, Codex, Gemini)'],
|
|
77
77
|
['(freeform text)', 'Chat about your threat model with AI'],
|
|
78
78
|
['', ''],
|
|
79
79
|
['/report', 'Generate markdown + JSON report'],
|
|
@@ -81,6 +81,7 @@ export function cmdHelp() {
|
|
|
81
81
|
['/diff [ref]', 'Compare model against a git ref (default: HEAD~1)'],
|
|
82
82
|
['/sarif [-o file]', 'Export SARIF 2.1.0 for GitHub / VS Code'],
|
|
83
83
|
['', ''],
|
|
84
|
+
['/gal', 'GAL annotation language guide'],
|
|
84
85
|
['/help', 'This help'],
|
|
85
86
|
['/quit', 'Exit'],
|
|
86
87
|
];
|
|
@@ -326,9 +327,7 @@ export function cmdExposures(args, ctx) {
|
|
|
326
327
|
return;
|
|
327
328
|
}
|
|
328
329
|
console.log('');
|
|
329
|
-
// Determine terminal width for adaptive layout
|
|
330
330
|
const termWidth = process.stdout.columns || 100;
|
|
331
|
-
// Manual table (we need colored cells which formatTable can't do directly)
|
|
332
331
|
const header = ` ${C.dim('#'.padEnd(4))}${C.dim('SEVERITY'.padEnd(12))}${C.dim('ASSET'.padEnd(18))}${C.dim('THREAT'.padEnd(20))}${C.dim('FILE'.padEnd(30))}${C.dim('LINE')}`;
|
|
333
332
|
console.log(header);
|
|
334
333
|
console.log(C.dim(' ' + '─'.repeat(Math.min(termWidth - 4, 96))));
|
|
@@ -367,7 +366,6 @@ export function cmdShow(args, ctx) {
|
|
|
367
366
|
}
|
|
368
367
|
console.log(` ${C.cyan('│')} ${C.dim(fileLink(exp.location.file, exp.location.line, ctx.root))}`);
|
|
369
368
|
console.log(` ${C.cyan('│')}`);
|
|
370
|
-
// Code context
|
|
371
369
|
const { lines } = readCodeContext(exp.location.file, exp.location.line, ctx.root);
|
|
372
370
|
for (const l of lines) {
|
|
373
371
|
console.log(` ${C.cyan('│')} ${l}`);
|
|
@@ -375,6 +373,33 @@ export function cmdShow(args, ctx) {
|
|
|
375
373
|
console.log(` ${C.cyan('└')}`);
|
|
376
374
|
console.log('');
|
|
377
375
|
}
|
|
376
|
+
// ─── /scan ───────────────────────────────────────────────────────────
|
|
377
|
+
export function cmdScan(ctx) {
|
|
378
|
+
if (!ctx.model) {
|
|
379
|
+
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const cov = ctx.model.coverage;
|
|
383
|
+
const pct = cov.coverage_percent;
|
|
384
|
+
console.log('');
|
|
385
|
+
console.log(` ${C.bold('Coverage:')} ${cov.annotated_symbols}/${cov.total_symbols} symbols (${pct}%)`);
|
|
386
|
+
const unannotated = cov.unannotated_critical || [];
|
|
387
|
+
if (unannotated.length === 0) {
|
|
388
|
+
console.log(C.green(' All security-relevant symbols are annotated!'));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
console.log(C.warn(` ${unannotated.length} unannotated symbol(s):`));
|
|
392
|
+
console.log('');
|
|
393
|
+
const show = unannotated.slice(0, 25);
|
|
394
|
+
for (const u of show) {
|
|
395
|
+
console.log(` ${C.dim(fileLink(u.file, u.line, ctx.root))} ${u.kind} ${C.bold(u.name)}`);
|
|
396
|
+
}
|
|
397
|
+
if (unannotated.length > 25) {
|
|
398
|
+
console.log(C.dim(` ... and ${unannotated.length - 25} more`));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
console.log('');
|
|
402
|
+
}
|
|
378
403
|
// ─── /assets ─────────────────────────────────────────────────────────
|
|
379
404
|
export function cmdAssets(ctx) {
|
|
380
405
|
if (!ctx.model) {
|
|
@@ -671,33 +696,6 @@ export async function cmdParse(ctx) {
|
|
|
671
696
|
console.log(C.error(` ✗ Parse failed: ${err.message}`));
|
|
672
697
|
}
|
|
673
698
|
}
|
|
674
|
-
// ─── /scan ───────────────────────────────────────────────────────────
|
|
675
|
-
export function cmdScan(ctx) {
|
|
676
|
-
if (!ctx.model) {
|
|
677
|
-
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
const cov = ctx.model.coverage;
|
|
681
|
-
const pct = cov.coverage_percent;
|
|
682
|
-
console.log('');
|
|
683
|
-
console.log(` ${C.bold('Coverage:')} ${cov.annotated_symbols}/${cov.total_symbols} symbols (${pct}%)`);
|
|
684
|
-
const unannotated = cov.unannotated_critical || [];
|
|
685
|
-
if (unannotated.length === 0) {
|
|
686
|
-
console.log(C.green(' All security-relevant symbols are annotated!'));
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
console.log(C.warn(` ${unannotated.length} unannotated symbol(s):`));
|
|
690
|
-
console.log('');
|
|
691
|
-
const show = unannotated.slice(0, 25);
|
|
692
|
-
for (const u of show) {
|
|
693
|
-
console.log(` ${C.dim(fileLink(u.file, u.line, ctx.root))} ${u.kind} ${C.bold(u.name)}`);
|
|
694
|
-
}
|
|
695
|
-
if (unannotated.length > 25) {
|
|
696
|
-
console.log(C.dim(` ... and ${unannotated.length - 25} more`));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
console.log('');
|
|
700
|
-
}
|
|
701
699
|
// ─── /validate ───────────────────────────────────────────────────────
|
|
702
700
|
export async function cmdValidate(ctx) {
|
|
703
701
|
console.log(C.dim(' Checking annotations...'));
|
|
@@ -706,9 +704,13 @@ export async function cmdValidate(ctx) {
|
|
|
706
704
|
ctx.model = model;
|
|
707
705
|
// Dangling refs
|
|
708
706
|
const danglingDiags = findDanglingRefs(model);
|
|
709
|
-
|
|
707
|
+
// Check for @accepts without @audit (governance concern)
|
|
708
|
+
const acceptAuditDiags = findAcceptedWithoutAudit(model);
|
|
709
|
+
const allDiags = [...diagnostics, ...danglingDiags, ...acceptAuditDiags];
|
|
710
710
|
// Unmitigated exposures
|
|
711
711
|
const unmitigated = findUnmitigatedExposures(model);
|
|
712
|
+
// Accepted-but-unmitigated exposures
|
|
713
|
+
const acceptedOnly = findAcceptedExposures(model);
|
|
712
714
|
// Print diagnostics
|
|
713
715
|
const errors = allDiags.filter(d => d.level === 'error');
|
|
714
716
|
const warnings = allDiags.filter(d => d.level === 'warning');
|
|
@@ -728,8 +730,16 @@ export async function cmdValidate(ctx) {
|
|
|
728
730
|
console.log(` ${sev} ${u.asset} → ${u.threat} ${C.dim(fileLink(u.location.file, u.location.line, ctx.root))}`);
|
|
729
731
|
}
|
|
730
732
|
}
|
|
733
|
+
if (acceptedOnly.length > 0) {
|
|
734
|
+
console.log('');
|
|
735
|
+
console.log(C.warn(` ⚡ ${acceptedOnly.length} accepted-but-unmitigated exposure(s) (no control in code):`));
|
|
736
|
+
for (const a of acceptedOnly) {
|
|
737
|
+
const sev = a.severity ? severityBadge(a.severity) : C.dim('unset');
|
|
738
|
+
console.log(` ${sev} ${a.asset} → ${a.threat} ${C.dim(fileLink(a.location.file, a.location.line, ctx.root))}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
731
741
|
console.log('');
|
|
732
|
-
if (errors.length === 0 && unmitigated.length === 0) {
|
|
742
|
+
if (errors.length === 0 && unmitigated.length === 0 && acceptedOnly.length === 0) {
|
|
733
743
|
console.log(C.success(' ✓ All annotations valid, no unmitigated exposures.'));
|
|
734
744
|
}
|
|
735
745
|
else {
|
|
@@ -740,6 +750,8 @@ export async function cmdValidate(ctx) {
|
|
|
740
750
|
parts.push(`${warnings.length} warning(s)`);
|
|
741
751
|
if (unmitigated.length > 0)
|
|
742
752
|
parts.push(`${unmitigated.length} unmitigated`);
|
|
753
|
+
if (acceptedOnly.length > 0)
|
|
754
|
+
parts.push(`${acceptedOnly.length} accepted without mitigation`);
|
|
743
755
|
console.log(` ${parts.join(', ')}`);
|
|
744
756
|
}
|
|
745
757
|
}
|
|
@@ -817,69 +829,110 @@ export async function cmdSarif(args, ctx) {
|
|
|
817
829
|
}
|
|
818
830
|
console.log('');
|
|
819
831
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
832
|
+
const CLI_AGENT_OPTIONS = [
|
|
833
|
+
{ id: 'claude-code', name: 'Claude Code', desc: 'Anthropic\'s coding agent (claude cli)' },
|
|
834
|
+
{ id: 'codex', name: 'Codex CLI', desc: 'OpenAI\'s coding agent (codex cli)' },
|
|
835
|
+
{ id: 'gemini', name: 'Gemini CLI', desc: 'Google\'s coding agent (gemini cli)' },
|
|
836
|
+
];
|
|
837
|
+
const CLI_AGENT_NAMES = {
|
|
838
|
+
'claude-code': 'Claude Code',
|
|
839
|
+
'codex': 'Codex CLI',
|
|
840
|
+
'gemini': 'Gemini CLI',
|
|
841
|
+
};
|
|
842
|
+
/** Provider model catalogs — popular models per provider, ordered by capability */
|
|
843
|
+
const PROVIDER_MODELS = {
|
|
844
|
+
anthropic: [
|
|
845
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', desc: 'Latest, frontier coding & agents' },
|
|
846
|
+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', desc: 'Most intelligent, complex reasoning' },
|
|
847
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', desc: 'Previous gen, strong all-rounder' },
|
|
848
|
+
{ id: 'claude-opus-4-5', name: 'Claude Opus 4.5', desc: 'Previous gen, deep analysis' },
|
|
849
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', desc: 'Fastest, lowest cost' },
|
|
850
|
+
],
|
|
851
|
+
openai: [
|
|
852
|
+
{ id: 'gpt-5.2', name: 'GPT-5.2', desc: 'Latest flagship, smartest & most precise' },
|
|
853
|
+
{ id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro', desc: 'Enhanced GPT-5.2 for complex tasks' },
|
|
854
|
+
{ id: 'gpt-5', name: 'GPT-5', desc: 'Frontier model with reasoning' },
|
|
855
|
+
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', desc: 'Fast and affordable' },
|
|
856
|
+
{ id: 'gpt-5-nano', name: 'GPT-5 Nano', desc: 'Fastest, lowest cost' },
|
|
857
|
+
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex', desc: 'Optimized for agentic coding' },
|
|
858
|
+
{ id: 'o3', name: 'o3', desc: 'Reasoning model, complex analysis' },
|
|
859
|
+
{ id: 'o4-mini', name: 'o4-mini', desc: 'Fast reasoning model' },
|
|
860
|
+
{ id: 'gpt-4.1', name: 'GPT-4.1', desc: 'Previous gen flagship' },
|
|
861
|
+
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini', desc: 'Previous gen, fast' },
|
|
862
|
+
],
|
|
863
|
+
google: [
|
|
864
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', desc: 'Best price-performance, reasoning' },
|
|
865
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', desc: 'Most advanced, deep reasoning & coding' },
|
|
866
|
+
{ id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash-Lite', desc: 'Fastest, most budget-friendly' },
|
|
867
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', desc: 'Preview: frontier-class at low cost' },
|
|
868
|
+
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', desc: 'Preview: state-of-the-art reasoning' },
|
|
869
|
+
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', desc: 'Preview: advanced agentic & coding' },
|
|
870
|
+
],
|
|
871
|
+
deepseek: [
|
|
872
|
+
{ id: 'deepseek-chat', name: 'DeepSeek V3.2', desc: 'General purpose, fast (128K context)' },
|
|
873
|
+
{ id: 'deepseek-reasoner', name: 'DeepSeek R1', desc: 'Thinking mode, best for analysis' },
|
|
874
|
+
],
|
|
875
|
+
openrouter: [
|
|
876
|
+
{ id: 'anthropic/claude-sonnet-4-6', name: 'Claude Sonnet 4.6', desc: 'Anthropic via OpenRouter' },
|
|
877
|
+
{ id: 'anthropic/claude-opus-4-6', name: 'Claude Opus 4.6', desc: 'Anthropic via OpenRouter' },
|
|
878
|
+
{ id: 'openai/gpt-5.2', name: 'GPT-5.2', desc: 'OpenAI via OpenRouter' },
|
|
879
|
+
{ id: 'openai/o3', name: 'o3', desc: 'OpenAI reasoning via OpenRouter' },
|
|
880
|
+
{ id: 'google/gemini-2.5-pro', name: 'Gemini 2.5 Pro', desc: 'Google via OpenRouter' },
|
|
881
|
+
{ id: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash', desc: 'Google via OpenRouter' },
|
|
882
|
+
{ id: 'deepseek/deepseek-r1', name: 'DeepSeek R1', desc: 'DeepSeek via OpenRouter' },
|
|
883
|
+
],
|
|
884
|
+
ollama: [
|
|
885
|
+
{ id: 'llama3.2', name: 'Llama 3.2', desc: 'Meta, good general purpose' },
|
|
886
|
+
{ id: 'qwen2.5-coder:32b', name: 'Qwen 2.5 Coder 32B', desc: 'Best local coding model' },
|
|
887
|
+
{ id: 'deepseek-r1:32b', name: 'DeepSeek R1 32B', desc: 'Local reasoning model' },
|
|
888
|
+
{ id: 'gemma3:27b', name: 'Gemma 3 27B', desc: 'Google, strong local model' },
|
|
889
|
+
{ id: 'mistral', name: 'Mistral 7B', desc: 'Lightweight, fast' },
|
|
890
|
+
],
|
|
891
|
+
};
|
|
892
|
+
/** Helper to display a numbered model selection menu and return the chosen model ID */
|
|
893
|
+
async function pickModel(ctx, provider) {
|
|
894
|
+
const models = PROVIDER_MODELS[provider];
|
|
895
|
+
if (!models || models.length === 0) {
|
|
896
|
+
// Fallback to free-text for unknown providers
|
|
897
|
+
const model = await ask(ctx, ' Model name: ');
|
|
898
|
+
return model || null;
|
|
899
|
+
}
|
|
900
|
+
console.log('');
|
|
901
|
+
console.log(' Select model:');
|
|
902
|
+
for (let i = 0; i < models.length; i++) {
|
|
903
|
+
const m = models[i];
|
|
904
|
+
console.log(` ${C.bold(String(i + 1))} ${m.name.padEnd(24)} ${C.dim(m.desc)}`);
|
|
905
|
+
}
|
|
906
|
+
console.log(` ${C.bold(String(models.length + 1))} ${C.dim('Custom (enter model ID manually)')}`);
|
|
907
|
+
console.log('');
|
|
908
|
+
const choice = await ask(ctx, ` Model [1-${models.length + 1}]: `);
|
|
909
|
+
const idx = parseInt(choice, 10) - 1;
|
|
910
|
+
if (idx < 0 || idx > models.length) {
|
|
911
|
+
console.log(C.warn(' Cancelled.'));
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
if (idx === models.length) {
|
|
915
|
+
// Custom model
|
|
916
|
+
const custom = await ask(ctx, ' Model ID: ');
|
|
917
|
+
return custom || null;
|
|
918
|
+
}
|
|
919
|
+
return models[idx].id;
|
|
873
920
|
}
|
|
874
|
-
// ─── /model ──────────────────────────────────────────────────────────
|
|
875
921
|
export async function cmdModel(ctx) {
|
|
876
922
|
const current = resolveLLMConfig(ctx.root);
|
|
923
|
+
const tuiCfg = loadTuiConfig(ctx.root);
|
|
877
924
|
const source = describeConfigSource(ctx.root);
|
|
878
|
-
|
|
925
|
+
// Show current configuration
|
|
926
|
+
if (tuiCfg?.aiMode === 'cli-agent' && tuiCfg?.cliAgent) {
|
|
927
|
+
const agentName = CLI_AGENT_NAMES[tuiCfg.cliAgent] || tuiCfg.cliAgent;
|
|
928
|
+
console.log(` ${C.dim('Current:')} ${agentName} ${C.dim('(CLI Agent)')}`);
|
|
929
|
+
console.log(` ${C.dim('Source:')} ${source}`);
|
|
930
|
+
console.log('');
|
|
931
|
+
}
|
|
932
|
+
else if (current) {
|
|
879
933
|
console.log(` ${C.dim('Current:')} ${current.provider} / ${current.model}`);
|
|
880
934
|
console.log(` ${C.dim('Source:')} ${source}`);
|
|
881
935
|
console.log('');
|
|
882
|
-
// If config comes from env vars, offer to keep it
|
|
883
936
|
if (source.includes('env var')) {
|
|
884
937
|
const override = await ask(ctx, ' Override with project config? (y/N): ');
|
|
885
938
|
if (override.toLowerCase() !== 'y') {
|
|
@@ -892,88 +945,120 @@ export async function cmdModel(ctx) {
|
|
|
892
945
|
console.log(C.dim(' No AI provider configured.'));
|
|
893
946
|
console.log('');
|
|
894
947
|
}
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
console.log('
|
|
898
|
-
|
|
948
|
+
// Step 1: Choose mode — CLI Agents or API
|
|
949
|
+
console.log(' How would you like to use AI?');
|
|
950
|
+
console.log(` ${C.bold('1')} CLI Agents ${C.dim('(terminal-based coding agents)')}`);
|
|
951
|
+
console.log(` ${C.bold('2')} API ${C.dim('(direct LLM API calls)')}`);
|
|
899
952
|
console.log('');
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
if (
|
|
953
|
+
const modeChoice = await ask(ctx, ' Choice [1-2]: ');
|
|
954
|
+
const modeIdx = parseInt(modeChoice, 10);
|
|
955
|
+
if (modeIdx < 1 || modeIdx > 2) {
|
|
903
956
|
console.log(C.warn(' Cancelled.'));
|
|
904
957
|
return;
|
|
905
958
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
console.log(C.
|
|
959
|
+
if (modeIdx === 1) {
|
|
960
|
+
// ── CLI Agent selection ──
|
|
961
|
+
console.log('');
|
|
962
|
+
console.log(' Select CLI Agent:');
|
|
963
|
+
for (let i = 0; i < CLI_AGENT_OPTIONS.length; i++) {
|
|
964
|
+
const a = CLI_AGENT_OPTIONS[i];
|
|
965
|
+
console.log(` ${C.bold(String(i + 1))} ${a.name.padEnd(16)} ${C.dim(a.desc)}`);
|
|
966
|
+
}
|
|
967
|
+
console.log('');
|
|
968
|
+
const agentChoice = await ask(ctx, ` Agent [1-${CLI_AGENT_OPTIONS.length}]: `);
|
|
969
|
+
const agentIdx = parseInt(agentChoice, 10) - 1;
|
|
970
|
+
if (agentIdx < 0 || agentIdx >= CLI_AGENT_OPTIONS.length) {
|
|
971
|
+
console.log(C.warn(' Cancelled.'));
|
|
913
972
|
return;
|
|
914
973
|
}
|
|
974
|
+
const selectedAgent = CLI_AGENT_OPTIONS[agentIdx];
|
|
975
|
+
saveTuiConfig(ctx.root, {
|
|
976
|
+
aiMode: 'cli-agent',
|
|
977
|
+
cliAgent: selectedAgent.id,
|
|
978
|
+
});
|
|
979
|
+
console.log('');
|
|
980
|
+
console.log(` ${C.success('✓')} Configured: ${C.bold(selectedAgent.name)} ${C.dim('(CLI Agent)')}`);
|
|
981
|
+
console.log(C.dim(' Saved to .guardlink/config.json'));
|
|
982
|
+
console.log(C.dim(` Use /threat-report or /annotate — they will launch ${selectedAgent.name} automatically.`));
|
|
983
|
+
console.log('');
|
|
915
984
|
}
|
|
916
985
|
else {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
};
|
|
927
|
-
const model = await ask(ctx, ` Model [${defaults[provider]}]: `);
|
|
928
|
-
saveTuiConfig(ctx.root, {
|
|
929
|
-
provider,
|
|
930
|
-
model: model || defaults[provider],
|
|
931
|
-
apiKey,
|
|
932
|
-
});
|
|
933
|
-
const displayKey = apiKey.length > 8 ? apiKey.slice(0, 6) + '•'.repeat(8) : '•'.repeat(8);
|
|
934
|
-
console.log('');
|
|
935
|
-
console.log(` ${C.success('✓')} Configured: ${C.bold(model || defaults[provider])} (${provider})`);
|
|
936
|
-
console.log(` Key: ${displayKey}`);
|
|
937
|
-
console.log(C.dim(' Saved to .guardlink/config.json'));
|
|
938
|
-
console.log('');
|
|
939
|
-
}
|
|
940
|
-
// ─── /threat-report ──────────────────────────────────────────────────
|
|
941
|
-
export async function cmdThreatReport(args, ctx) {
|
|
942
|
-
if (!ctx.model) {
|
|
943
|
-
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
const { agent, cleanArgs } = parseAgentFlag(args);
|
|
947
|
-
const framework = cleanArgs.trim().toLowerCase() || '';
|
|
948
|
-
const validFrameworks = ['stride', 'dread', 'pasta', 'attacker', 'rapid', 'general'];
|
|
949
|
-
if (!framework) {
|
|
986
|
+
// ── API provider selection ──
|
|
987
|
+
const providers = [
|
|
988
|
+
{ id: 'anthropic', name: 'Anthropic', desc: 'Claude Sonnet 4.6, Opus 4.6, Haiku 4.5' },
|
|
989
|
+
{ id: 'openai', name: 'OpenAI', desc: 'GPT-5.2, o3, o4-mini, GPT-5.1 Codex' },
|
|
990
|
+
{ id: 'google', name: 'Google', desc: 'Gemini 2.5 Flash/Pro, Gemini 3 Pro' },
|
|
991
|
+
{ id: 'deepseek', name: 'DeepSeek', desc: 'DeepSeek V3.2, R1 reasoning' },
|
|
992
|
+
{ id: 'openrouter', name: 'OpenRouter', desc: 'Multi-provider gateway' },
|
|
993
|
+
{ id: 'ollama', name: 'Ollama', desc: 'Local models (Llama, Qwen, Gemma)' },
|
|
994
|
+
];
|
|
950
995
|
console.log('');
|
|
951
|
-
console.log(
|
|
952
|
-
for (
|
|
953
|
-
|
|
996
|
+
console.log(' Select provider:');
|
|
997
|
+
for (let i = 0; i < providers.length; i++) {
|
|
998
|
+
const p = providers[i];
|
|
999
|
+
console.log(` ${C.bold(String(i + 1))} ${p.name.padEnd(14)} ${C.dim(p.desc)}`);
|
|
1000
|
+
}
|
|
1001
|
+
console.log('');
|
|
1002
|
+
const choice = await ask(ctx, ` Provider [1-${providers.length}]: `);
|
|
1003
|
+
const idx = parseInt(choice, 10) - 1;
|
|
1004
|
+
if (idx < 0 || idx >= providers.length) {
|
|
1005
|
+
console.log(C.warn(' Cancelled.'));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const provider = providers[idx].id;
|
|
1009
|
+
// Model selection — numbered menu
|
|
1010
|
+
const modelId = await pickModel(ctx, provider);
|
|
1011
|
+
if (!modelId)
|
|
1012
|
+
return;
|
|
1013
|
+
// API key
|
|
1014
|
+
let apiKey = '';
|
|
1015
|
+
if (provider !== 'ollama') {
|
|
1016
|
+
console.log('');
|
|
1017
|
+
apiKey = await ask(ctx, ' API Key: ');
|
|
1018
|
+
if (!apiKey) {
|
|
1019
|
+
console.log(C.warn(' Cancelled — no API key provided.'));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
954
1022
|
}
|
|
1023
|
+
else {
|
|
1024
|
+
apiKey = 'ollama-local';
|
|
1025
|
+
}
|
|
1026
|
+
saveTuiConfig(ctx.root, {
|
|
1027
|
+
aiMode: 'api',
|
|
1028
|
+
provider,
|
|
1029
|
+
model: modelId,
|
|
1030
|
+
apiKey,
|
|
1031
|
+
});
|
|
1032
|
+
const displayKey = apiKey.length > 8 ? apiKey.slice(0, 6) + '•'.repeat(8) : '•'.repeat(8);
|
|
1033
|
+
// Find display name for the model
|
|
1034
|
+
const modelEntry = PROVIDER_MODELS[provider]?.find(m => m.id === modelId);
|
|
1035
|
+
const modelDisplay = modelEntry ? `${modelEntry.name} (${modelId})` : modelId;
|
|
955
1036
|
console.log('');
|
|
956
|
-
console.log(C.
|
|
957
|
-
console.log(
|
|
958
|
-
console.log(C.dim('
|
|
1037
|
+
console.log(` ${C.success('✓')} Configured: ${C.bold(modelDisplay)}`);
|
|
1038
|
+
console.log(` Provider: ${providers[idx].name} Key: ${displayKey}`);
|
|
1039
|
+
console.log(C.dim(' Saved to .guardlink/config.json'));
|
|
959
1040
|
console.log('');
|
|
960
|
-
return;
|
|
961
1041
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1042
|
+
}
|
|
1043
|
+
// ─── /threat-report ──────────────────────────────────────────────────
|
|
1044
|
+
/**
|
|
1045
|
+
* Build the full analysis prompt for CLI agents.
|
|
1046
|
+
* Includes system prompt, serialized model, project context, code snippets,
|
|
1047
|
+
* and instructions to read source code.
|
|
1048
|
+
*/
|
|
1049
|
+
function buildAgentAnalysisPrompt(root, model, fw, customPrompt, reportLabel) {
|
|
1050
|
+
const modelJson = serializeModel(model);
|
|
1051
|
+
const projectContext = buildProjectContext(root);
|
|
1052
|
+
const codeSnippets = extractCodeSnippets(root, model);
|
|
1053
|
+
const systemPrompt = FRAMEWORK_PROMPTS[fw];
|
|
1054
|
+
const userMessage = buildUserMessage(modelJson, fw, customPrompt, projectContext || undefined, codeSnippets || undefined);
|
|
1055
|
+
return `You are analyzing a codebase with GuardLink security annotations.
|
|
971
1056
|
You have access to the full source code in the current directory.
|
|
972
1057
|
|
|
973
1058
|
${systemPrompt}
|
|
974
1059
|
|
|
975
1060
|
## Task
|
|
976
|
-
Read the source code and GuardLink annotations, then produce a thorough ${
|
|
1061
|
+
Read the source code and GuardLink annotations, then produce a thorough ${reportLabel}.
|
|
977
1062
|
|
|
978
1063
|
## Threat Model (serialized from annotations)
|
|
979
1064
|
${userMessage}
|
|
@@ -982,51 +1067,128 @@ ${userMessage}
|
|
|
982
1067
|
1. Read the actual source files to understand the code — don't just rely on the serialized model above
|
|
983
1068
|
2. Cross-reference the annotations with the real code to validate findings
|
|
984
1069
|
3. Produce the full report as markdown
|
|
985
|
-
4.
|
|
986
|
-
5.
|
|
987
|
-
|
|
1070
|
+
4. Be specific — reference actual files, functions, and line numbers from the codebase
|
|
1071
|
+
5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages
|
|
1072
|
+
6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."`;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Save inline agent output as a threat report markdown file.
|
|
1076
|
+
*/
|
|
1077
|
+
function saveInlineReport(root, content, fw, agentName, project, annotationCount) {
|
|
1078
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1079
|
+
const reportsDir = resolve(root, '.guardlink', 'threat-reports');
|
|
1080
|
+
if (!existsSync(reportsDir))
|
|
1081
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
1082
|
+
const filename = `${timestamp}-${fw}.md`;
|
|
1083
|
+
const filepath = resolve(reportsDir, filename);
|
|
1084
|
+
const cleanedContent = cleanCliArtifacts(content);
|
|
1085
|
+
const header = `---
|
|
1086
|
+
framework: ${fw}
|
|
1087
|
+
label: ${FRAMEWORK_LABELS[fw]}
|
|
1088
|
+
model: ${agentName}
|
|
1089
|
+
timestamp: ${new Date().toISOString()}
|
|
1090
|
+
project: ${project}
|
|
1091
|
+
annotations: ${annotationCount}
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
# ${FRAMEWORK_LABELS[fw]}
|
|
1095
|
+
|
|
1096
|
+
> Generated by \`guardlink threat-report ${fw}\` on ${new Date().toISOString().slice(0, 10)}
|
|
1097
|
+
> Agent: ${agentName} | Project: ${project} | Annotations: ${annotationCount}
|
|
1098
|
+
|
|
1099
|
+
`;
|
|
1100
|
+
writeFileSync(filepath, header + cleanedContent + '\n');
|
|
1101
|
+
return `.guardlink/threat-reports/${filename}`;
|
|
1102
|
+
}
|
|
1103
|
+
export async function cmdThreatReport(args, ctx) {
|
|
1104
|
+
if (!ctx.model) {
|
|
1105
|
+
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
// Parse any explicit --agent flag override
|
|
1109
|
+
const { agent: flagAgent, cleanArgs } = parseAgentFlag(args);
|
|
1110
|
+
const input = cleanArgs.trim();
|
|
1111
|
+
const validFrameworks = ['stride', 'dread', 'pasta', 'attacker', 'rapid', 'general'];
|
|
1112
|
+
// Show help when no arguments given
|
|
1113
|
+
if (!input) {
|
|
988
1114
|
console.log('');
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1115
|
+
console.log(` ${C.bold('Threat report frameworks:')}`);
|
|
1116
|
+
for (const fw of validFrameworks) {
|
|
1117
|
+
console.log(` ${C.bold('/threat-report ' + fw.padEnd(12))} ${C.dim(FRAMEWORK_LABELS[fw])}`);
|
|
1118
|
+
}
|
|
1119
|
+
console.log('');
|
|
1120
|
+
console.log(` ${C.bold('Custom prompt:')}`);
|
|
1121
|
+
console.log(C.dim(' /threat-report <any text> Uses your text as the analysis prompt'));
|
|
1122
|
+
console.log(C.dim(' Example: /threat-report Create a comprehensive report mixing STRIDE and DREAD'));
|
|
1123
|
+
console.log('');
|
|
1124
|
+
console.log(C.dim(' Uses the AI provider configured via /model (API or CLI agent).'));
|
|
1125
|
+
console.log(C.dim(' Override with: --claude-code --codex --gemini --clipboard'));
|
|
1126
|
+
console.log('');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
// Determine framework vs custom prompt
|
|
1130
|
+
const inputLower = input.toLowerCase();
|
|
1131
|
+
const isStandard = validFrameworks.includes(inputLower);
|
|
1132
|
+
const fw = (isStandard ? inputLower : 'general');
|
|
1133
|
+
const customPrompt = isStandard ? undefined : input;
|
|
1134
|
+
const reportLabel = customPrompt ? 'Custom Threat Analysis' : FRAMEWORK_LABELS[fw];
|
|
1135
|
+
// ── Resolve execution method ──
|
|
1136
|
+
// Priority: explicit --flag > /model config > env-var API
|
|
1137
|
+
const tuiCfg = loadTuiConfig(ctx.root);
|
|
1138
|
+
// Resolve the agent to use (flag override or configured CLI agent)
|
|
1139
|
+
let agent = flagAgent;
|
|
1140
|
+
if (!agent && tuiCfg?.aiMode === 'cli-agent' && tuiCfg?.cliAgent) {
|
|
1141
|
+
agent = AGENTS.find(a => a.id === tuiCfg.cliAgent) || null;
|
|
1142
|
+
}
|
|
1143
|
+
// ── Path 1: CLI Agent (inline, non-interactive) ──
|
|
1144
|
+
if (agent && agent.cmd) {
|
|
1145
|
+
const analysisPrompt = buildAgentAnalysisPrompt(ctx.root, ctx.model, fw, customPrompt, reportLabel);
|
|
1146
|
+
console.log(` ${C.dim('Generating')} ${reportLabel} ${C.dim('via')} ${agent.name} ${C.dim('(inline)...')}`);
|
|
1147
|
+
console.log(C.dim(` Annotations: ${ctx.model.annotations_parsed} | Exposures: ${ctx.model.exposures.length}`));
|
|
1148
|
+
console.log('');
|
|
1149
|
+
const result = await launchAgentInline(agent, analysisPrompt, ctx.root, (text) => process.stdout.write(text), { autoYes: true });
|
|
1150
|
+
if (result.error) {
|
|
1151
|
+
console.log(C.error(`\n ✗ ${result.error}`));
|
|
996
1152
|
console.log('');
|
|
997
|
-
|
|
998
|
-
if (result.error) {
|
|
999
|
-
console.log(C.error(` ✗ ${result.error}`));
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
console.log(`\n ${C.success('✓')} ${agent.name} session ended.`);
|
|
1003
|
-
console.log(` Run ${C.bold('/threat-reports')} to see saved results.`);
|
|
1004
|
-
}
|
|
1153
|
+
return;
|
|
1005
1154
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
console.log(` ${C.success('✓')} ${agent.name} launched with project: ${ctx.projectName}`);
|
|
1013
|
-
console.log(`\n Paste (Cmd+V) the prompt in ${agent.name}.`);
|
|
1014
|
-
}
|
|
1015
|
-
else if (result.error) {
|
|
1016
|
-
console.log(C.error(` ✗ ${result.error}`));
|
|
1017
|
-
}
|
|
1155
|
+
process.stdout.write('\n');
|
|
1156
|
+
// Save the agent's output as a report
|
|
1157
|
+
if (result.content.trim()) {
|
|
1158
|
+
const savedTo = saveInlineReport(ctx.root, result.content, fw, agent.name, ctx.model.project, ctx.model.annotations_parsed);
|
|
1159
|
+
console.log('');
|
|
1160
|
+
console.log(` ${C.success('✓')} Report saved to ${savedTo}`);
|
|
1018
1161
|
}
|
|
1019
1162
|
console.log('');
|
|
1020
1163
|
return;
|
|
1021
1164
|
}
|
|
1022
|
-
// ──
|
|
1165
|
+
// ── Path 2: Clipboard / IDE agent (copy prompt, open app) ──
|
|
1166
|
+
if (agent && !agent.cmd) {
|
|
1167
|
+
const analysisPrompt = buildAgentAnalysisPrompt(ctx.root, ctx.model, fw, customPrompt, reportLabel);
|
|
1168
|
+
const result = launchAgent(agent, analysisPrompt, ctx.root);
|
|
1169
|
+
if (result.clipboardCopied) {
|
|
1170
|
+
console.log(C.success(` ✓ Prompt copied to clipboard (${analysisPrompt.length.toLocaleString()} chars)`));
|
|
1171
|
+
}
|
|
1172
|
+
if (result.launched && agent.app) {
|
|
1173
|
+
console.log(` ${C.success('✓')} ${agent.name} launched with project: ${ctx.projectName}`);
|
|
1174
|
+
console.log(`\n Paste (Cmd+V) the prompt in ${agent.name}.`);
|
|
1175
|
+
}
|
|
1176
|
+
else if (result.error) {
|
|
1177
|
+
console.log(C.error(` ✗ ${result.error}`));
|
|
1178
|
+
}
|
|
1179
|
+
console.log('');
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
// ── Path 3: Direct API call ──
|
|
1023
1183
|
const llmConfig = resolveLLMConfig(ctx.root);
|
|
1024
1184
|
if (!llmConfig) {
|
|
1025
|
-
console.log(C.warn(' No AI provider configured. Run /model first
|
|
1185
|
+
console.log(C.warn(' No AI provider configured. Run /model first.'));
|
|
1026
1186
|
console.log(C.dim(' Or set ANTHROPIC_API_KEY / OPENAI_API_KEY in environment.'));
|
|
1187
|
+
console.log(C.dim(' Or use: /threat-report <prompt> --claude-code'));
|
|
1027
1188
|
return;
|
|
1028
1189
|
}
|
|
1029
|
-
console.log(` ${C.dim('Generating
|
|
1190
|
+
console.log(` ${C.dim('Generating')} ${reportLabel} ${C.dim('with')} ${llmConfig.model}${C.dim('...')}`);
|
|
1191
|
+
console.log(C.dim(` Annotations: ${ctx.model.annotations_parsed} | Exposures: ${ctx.model.exposures.length}`));
|
|
1030
1192
|
console.log('');
|
|
1031
1193
|
try {
|
|
1032
1194
|
const result = await generateThreatReport({
|
|
@@ -1135,8 +1297,10 @@ export async function cmdAnnotate(args, ctx) {
|
|
|
1135
1297
|
}
|
|
1136
1298
|
// ─── Freeform AI Chat ────────────────────────────────────────────────
|
|
1137
1299
|
export async function cmdChat(text, ctx) {
|
|
1300
|
+
const tuiCfg = loadTuiConfig(ctx.root);
|
|
1138
1301
|
const llmConfig = resolveLLMConfig(ctx.root);
|
|
1139
|
-
|
|
1302
|
+
const useAgent = tuiCfg?.aiMode === 'cli-agent' && !!tuiCfg?.cliAgent;
|
|
1303
|
+
if (!useAgent && !llmConfig) {
|
|
1140
1304
|
console.log(C.warn(' No AI provider configured. Run /model first, or set an API key in environment.'));
|
|
1141
1305
|
return;
|
|
1142
1306
|
}
|
|
@@ -1159,17 +1323,37 @@ Keep responses under 500 words unless the user asks for detail.`;
|
|
|
1159
1323
|
};
|
|
1160
1324
|
userMessage = `Threat model context:\n${JSON.stringify(compact, null, 2)}\n\nUser question: ${text}`;
|
|
1161
1325
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1326
|
+
if (useAgent) {
|
|
1327
|
+
const agent = AGENTS.find(a => a.id === tuiCfg.cliAgent);
|
|
1328
|
+
if (!agent) {
|
|
1329
|
+
console.log(C.error(` ✗ Configured agent ${tuiCfg.cliAgent} not found.`));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
console.log('');
|
|
1333
|
+
console.log(C.dim(` Thinking via ${agent.name}...`));
|
|
1334
|
+
console.log('');
|
|
1335
|
+
const prompt = `${systemPrompt}\n\n${userMessage}`;
|
|
1336
|
+
const result = await launchAgentInline(agent, prompt, ctx.root, (chunk) => process.stdout.write(chunk), { autoYes: true });
|
|
1337
|
+
if (result.error) {
|
|
1338
|
+
console.log(C.error(`\n ✗ AI request failed: ${result.error}`));
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
console.log('\n');
|
|
1342
|
+
}
|
|
1169
1343
|
}
|
|
1170
|
-
|
|
1171
|
-
console.log(
|
|
1344
|
+
else {
|
|
1345
|
+
console.log('');
|
|
1346
|
+
console.log(C.dim(` Thinking via ${llmConfig.model}...`));
|
|
1172
1347
|
console.log('');
|
|
1348
|
+
try {
|
|
1349
|
+
const { chatCompletion } = await import('../analyze/llm.js');
|
|
1350
|
+
await chatCompletion(llmConfig, systemPrompt, userMessage, (chunk) => process.stdout.write(chunk));
|
|
1351
|
+
process.stdout.write('\n\n');
|
|
1352
|
+
}
|
|
1353
|
+
catch (err) {
|
|
1354
|
+
console.log(C.error(` ✗ AI request failed: ${err.message}`));
|
|
1355
|
+
console.log('');
|
|
1356
|
+
}
|
|
1173
1357
|
}
|
|
1174
1358
|
}
|
|
1175
1359
|
// ─── /report ─────────────────────────────────────────────────────────
|