guardlink 1.0.0 → 1.1.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +14 -0
  3. package/dist/agents/config.d.ts +2 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js +1 -1
  6. package/dist/agents/config.js.map +1 -1
  7. package/dist/agents/prompts.d.ts +2 -2
  8. package/dist/agents/prompts.d.ts.map +1 -1
  9. package/dist/agents/prompts.js +223 -31
  10. package/dist/agents/prompts.js.map +1 -1
  11. package/dist/analyzer/sarif.js +1 -1
  12. package/dist/cli/index.js +2 -56
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/dashboard/data.d.ts.map +1 -1
  15. package/dist/dashboard/data.js +19 -12
  16. package/dist/dashboard/data.js.map +1 -1
  17. package/dist/dashboard/diagrams.d.ts.map +1 -1
  18. package/dist/dashboard/diagrams.js +310 -37
  19. package/dist/dashboard/diagrams.js.map +1 -1
  20. package/dist/dashboard/generate.d.ts.map +1 -1
  21. package/dist/dashboard/generate.js +21 -5
  22. package/dist/dashboard/generate.js.map +1 -1
  23. package/dist/init/picker.d.ts.map +1 -1
  24. package/dist/init/picker.js +2 -2
  25. package/dist/init/picker.js.map +1 -1
  26. package/dist/init/templates.js +1 -1
  27. package/dist/mcp/server.d.ts.map +1 -1
  28. package/dist/mcp/server.js +8 -26
  29. package/dist/mcp/server.js.map +1 -1
  30. package/dist/parser/index.d.ts +1 -0
  31. package/dist/parser/index.d.ts.map +1 -1
  32. package/dist/parser/index.js +1 -0
  33. package/dist/parser/index.js.map +1 -1
  34. package/dist/parser/parse-line.js +3 -3
  35. package/dist/parser/parse-line.js.map +1 -1
  36. package/dist/parser/parse-project.js +1 -1
  37. package/dist/parser/validate.d.ts +19 -0
  38. package/dist/parser/validate.d.ts.map +1 -0
  39. package/dist/parser/validate.js +105 -0
  40. package/dist/parser/validate.js.map +1 -0
  41. package/dist/tui/commands.d.ts +1 -6
  42. package/dist/tui/commands.d.ts.map +1 -1
  43. package/dist/tui/commands.js +96 -221
  44. package/dist/tui/commands.js.map +1 -1
  45. package/dist/tui/config.d.ts +2 -0
  46. package/dist/tui/config.d.ts.map +1 -1
  47. package/dist/tui/config.js.map +1 -1
  48. package/dist/tui/index.d.ts.map +1 -1
  49. package/dist/tui/index.js +20 -24
  50. package/dist/tui/index.js.map +1 -1
  51. package/dist/tui/input.d.ts +2 -2
  52. package/dist/tui/input.js +2 -2
  53. package/dist/types/index.d.ts +1 -1
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/package.json +1 -1
@@ -6,16 +6,16 @@
6
6
  */
7
7
  import { resolve, basename } from 'node:path';
8
8
  import { writeFileSync } from 'node:fs';
9
- import { parseProject } from '../parser/index.js';
9
+ import { parseProject, findDanglingRefs, findUnmitigatedExposures } 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
- import { computeStats, computeSeverity, computeExposures } from '../dashboard/data.js';
13
+ import { computeStats, computeSeverity } from '../dashboard/data.js';
14
14
  import { generateThreatReport, serializeModel, listThreatReports, loadThreatReportsForDashboard, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage } 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';
17
+ import { C, severityBadge, severityText, severityOrder, computeGrade, gradeColored, readCodeContext, bar, fileLink } from './format.js';
18
+ import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js';
19
19
  import { AGENTS, parseAgentFlag, launchAgent, copyToClipboard, buildAnnotatePrompt } from '../agents/index.js';
20
20
  import { describeConfigSource } from '../agents/config.js';
21
21
  // ─── Shared context ──────────────────────────────────────────────────
@@ -61,11 +61,8 @@ 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 [flags]', 'List exposures (--asset, --severity, --file, --threat)'],
68
- ['/show <n>', 'Detail view of exposure #n with code context'],
69
66
  ['/assets', 'Asset tree with threat/control counts'],
70
67
  ['/files', 'Annotated file tree with exposure counts'],
71
68
  ['/view <file>', 'Show all annotations in a file with code context'],
@@ -279,102 +276,6 @@ export function cmdStatus(ctx) {
279
276
  }
280
277
  console.log('');
281
278
  }
282
- // ─── /exposures ──────────────────────────────────────────────────────
283
- export function cmdExposures(args, ctx) {
284
- if (!ctx.model) {
285
- console.log(C.warn(' No threat model. Run /parse first.'));
286
- return;
287
- }
288
- const rows = computeExposures(ctx.model);
289
- let filtered = rows.filter(r => !r.mitigated && !r.accepted); // open only by default
290
- // Parse flags
291
- const parts = args.split(/\s+/).filter(Boolean);
292
- let showAll = false;
293
- for (let i = 0; i < parts.length; i++) {
294
- const flag = parts[i];
295
- const val = parts[i + 1];
296
- if (flag === '--asset' && val) {
297
- filtered = filtered.filter(r => r.asset.includes(val));
298
- i++;
299
- }
300
- else if (flag === '--severity' && val) {
301
- filtered = filtered.filter(r => r.severity === val.toLowerCase());
302
- i++;
303
- }
304
- else if (flag === '--file' && val) {
305
- filtered = filtered.filter(r => r.file.includes(val));
306
- i++;
307
- }
308
- else if (flag === '--threat' && val) {
309
- filtered = filtered.filter(r => r.threat.includes(val));
310
- i++;
311
- }
312
- else if (flag === '--all') {
313
- filtered = rows;
314
- showAll = true;
315
- }
316
- }
317
- // Sort by severity
318
- filtered.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
319
- // Cache for /show
320
- ctx.lastExposures = filtered.map(r => {
321
- const original = ctx.model.exposures.find(e => e.asset === r.asset && e.threat === r.threat && e.location.file === r.file && e.location.line === r.line);
322
- return original;
323
- }).filter(Boolean);
324
- if (filtered.length === 0) {
325
- console.log(C.green(' No matching exposures found.'));
326
- return;
327
- }
328
- console.log('');
329
- // Determine terminal width for adaptive layout
330
- const termWidth = process.stdout.columns || 100;
331
- // Manual table (we need colored cells which formatTable can't do directly)
332
- 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
- console.log(header);
334
- console.log(C.dim(' ' + '─'.repeat(Math.min(termWidth - 4, 96))));
335
- for (const [i, r] of filtered.entries()) {
336
- const num = String(i + 1).padEnd(4);
337
- const sev = severityTextPad(r.severity, 12);
338
- const asset = trunc(r.asset, 16).padEnd(18);
339
- const threat = trunc(r.threat, 18).padEnd(20);
340
- const linkedFile = fileLinkTrunc(r.file, 28, r.line, ctx.root);
341
- const filePad = ' '.repeat(Math.max(0, 30 - trunc(r.file, 28).length));
342
- const line = ` ${num}${sev}${asset}${threat}${linkedFile}${filePad}${r.line}`;
343
- console.log(line);
344
- }
345
- console.log('');
346
- const countMsg = showAll
347
- ? ` ${filtered.length} exposure(s) total`
348
- : ` ${filtered.length} open exposure(s)`;
349
- console.log(C.dim(countMsg + ' · /show <n> for detail · --asset --severity --threat --file to filter'));
350
- console.log('');
351
- }
352
- // ─── /show ───────────────────────────────────────────────────────────
353
- export function cmdShow(args, ctx) {
354
- const num = parseInt(args.trim(), 10);
355
- if (!num || num < 1 || num > ctx.lastExposures.length) {
356
- console.log(C.warn(` Usage: /show <n> where n is 1-${ctx.lastExposures.length || '?'}. Run /exposures first.`));
357
- return;
358
- }
359
- const exp = ctx.lastExposures[num - 1];
360
- console.log('');
361
- console.log(` ${C.cyan('┌')} ${exp.asset} → ${exp.threat} ${severityBadge(exp.severity)}`);
362
- if (exp.description) {
363
- console.log(` ${C.cyan('│')} ${exp.description}`);
364
- }
365
- if (exp.external_refs.length > 0) {
366
- console.log(` ${C.cyan('│')} ${C.dim(exp.external_refs.join(' · '))}`);
367
- }
368
- console.log(` ${C.cyan('│')} ${C.dim(fileLink(exp.location.file, exp.location.line, ctx.root))}`);
369
- console.log(` ${C.cyan('│')}`);
370
- // Code context
371
- const { lines } = readCodeContext(exp.location.file, exp.location.line, ctx.root);
372
- for (const l of lines) {
373
- console.log(` ${C.cyan('│')} ${l}`);
374
- }
375
- console.log(` ${C.cyan('└')}`);
376
- console.log('');
377
- }
378
279
  // ─── /assets ─────────────────────────────────────────────────────────
379
280
  export function cmdAssets(ctx) {
380
281
  if (!ctx.model) {
@@ -671,33 +572,6 @@ export async function cmdParse(ctx) {
671
572
  console.log(C.error(` ✗ Parse failed: ${err.message}`));
672
573
  }
673
574
  }
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
575
  // ─── /validate ───────────────────────────────────────────────────────
702
576
  export async function cmdValidate(ctx) {
703
577
  console.log(C.dim(' Checking annotations...'));
@@ -817,69 +691,32 @@ export async function cmdSarif(args, ctx) {
817
691
  }
818
692
  console.log('');
819
693
  }
820
- // ─── Helpers: validate ───────────────────────────────────────────────
821
- function findDanglingRefs(model) {
822
- const diagnostics = [];
823
- const definedIds = new Set();
824
- for (const a of model.assets)
825
- if (a.id)
826
- definedIds.add(a.id);
827
- for (const t of model.threats)
828
- if (t.id)
829
- definedIds.add(t.id);
830
- for (const c of model.controls)
831
- if (c.id)
832
- definedIds.add(c.id);
833
- for (const b of model.boundaries)
834
- if (b.id)
835
- definedIds.add(b.id);
836
- const checkRef = (ref, loc) => {
837
- if (ref.startsWith('#')) {
838
- const id = ref.slice(1);
839
- if (!definedIds.has(id)) {
840
- diagnostics.push({
841
- level: 'warning',
842
- message: `Dangling reference: #${id} is never defined`,
843
- file: loc.file,
844
- line: loc.line,
845
- });
846
- }
847
- }
848
- };
849
- for (const m of model.mitigations) {
850
- checkRef(m.threat, m.location);
851
- if (m.control)
852
- checkRef(m.control, m.location);
853
- }
854
- for (const e of model.exposures)
855
- checkRef(e.threat, e.location);
856
- for (const a of model.acceptances)
857
- checkRef(a.threat, a.location);
858
- for (const t of model.transfers)
859
- checkRef(t.threat, t.location);
860
- if (model.validations) {
861
- for (const v of model.validations)
862
- checkRef(v.control, v.location);
863
- }
864
- return diagnostics;
865
- }
866
- function findUnmitigatedExposures(model) {
867
- const mitigated = new Set();
868
- for (const m of model.mitigations)
869
- mitigated.add(`${m.asset}::${m.threat}`);
870
- for (const a of model.acceptances)
871
- mitigated.add(`${a.asset}::${a.threat}`);
872
- return model.exposures.filter(e => !mitigated.has(`${e.asset}::${e.threat}`));
873
- }
874
694
  // ─── /model ──────────────────────────────────────────────────────────
695
+ const CLI_AGENT_OPTIONS = [
696
+ { id: 'claude-code', name: 'Claude Code' },
697
+ { id: 'codex', name: 'Codex CLI' },
698
+ { id: 'gemini', name: 'Gemini CLI' },
699
+ ];
700
+ const CLI_AGENT_NAMES = {
701
+ 'claude-code': 'Claude Code',
702
+ 'codex': 'Codex CLI',
703
+ 'gemini': 'Gemini CLI',
704
+ };
875
705
  export async function cmdModel(ctx) {
876
706
  const current = resolveLLMConfig(ctx.root);
707
+ const tuiCfg = loadTuiConfig(ctx.root);
877
708
  const source = describeConfigSource(ctx.root);
878
- if (current) {
709
+ // Show current configuration
710
+ if (tuiCfg?.aiMode === 'cli-agent' && tuiCfg?.cliAgent) {
711
+ const agentName = CLI_AGENT_NAMES[tuiCfg.cliAgent] || tuiCfg.cliAgent;
712
+ console.log(` ${C.dim('Current:')} ${agentName} ${C.dim('(CLI Agent)')}`);
713
+ console.log(` ${C.dim('Source:')} ${source}`);
714
+ console.log('');
715
+ }
716
+ else if (current) {
879
717
  console.log(` ${C.dim('Current:')} ${current.provider} / ${current.model}`);
880
718
  console.log(` ${C.dim('Source:')} ${source}`);
881
719
  console.log('');
882
- // If config comes from env vars, offer to keep it
883
720
  if (source.includes('env var')) {
884
721
  const override = await ask(ctx, ' Override with project config? (y/N): ');
885
722
  if (override.toLowerCase() !== 'y') {
@@ -892,50 +729,88 @@ export async function cmdModel(ctx) {
892
729
  console.log(C.dim(' No AI provider configured.'));
893
730
  console.log('');
894
731
  }
895
- // Provider selection
896
- const providers = ['anthropic', 'openai', 'openrouter', 'deepseek', 'ollama'];
897
- console.log(' Select provider:');
898
- providers.forEach((p, i) => console.log(` ${C.bold(String(i + 1))} ${p}`));
732
+ // Step 1: Choose mode — CLI Agents or API
733
+ console.log(' How would you like to use AI?');
734
+ console.log(` ${C.bold('1')} CLI Agents ${C.dim('(terminal-based coding agents)')}`);
735
+ console.log(` ${C.bold('2')} API ${C.dim('(direct LLM API calls)')}`);
899
736
  console.log('');
900
- const choice = await ask(ctx, ` Provider [1-${providers.length}]: `);
901
- const idx = parseInt(choice, 10) - 1;
902
- if (idx < 0 || idx >= providers.length) {
737
+ const modeChoice = await ask(ctx, ' Choice [1-2]: ');
738
+ const modeIdx = parseInt(modeChoice, 10);
739
+ if (modeIdx < 1 || modeIdx > 2) {
903
740
  console.log(C.warn(' Cancelled.'));
904
741
  return;
905
742
  }
906
- const provider = providers[idx];
907
- // API key
908
- let apiKey = '';
909
- if (provider !== 'ollama') {
910
- apiKey = await ask(ctx, ' API Key: ');
911
- if (!apiKey) {
912
- console.log(C.warn(' Cancelled no API key provided.'));
743
+ if (modeIdx === 1) {
744
+ // ── CLI Agent selection ──
745
+ console.log('');
746
+ console.log(' Select CLI Agent:');
747
+ CLI_AGENT_OPTIONS.forEach((a, i) => console.log(` ${C.bold(String(i + 1))} ${a.name}`));
748
+ console.log('');
749
+ const agentChoice = await ask(ctx, ` Agent [1-${CLI_AGENT_OPTIONS.length}]: `);
750
+ const agentIdx = parseInt(agentChoice, 10) - 1;
751
+ if (agentIdx < 0 || agentIdx >= CLI_AGENT_OPTIONS.length) {
752
+ console.log(C.warn(' Cancelled.'));
913
753
  return;
914
754
  }
755
+ const selectedAgent = CLI_AGENT_OPTIONS[agentIdx];
756
+ saveTuiConfig(ctx.root, {
757
+ aiMode: 'cli-agent',
758
+ cliAgent: selectedAgent.id,
759
+ });
760
+ console.log('');
761
+ console.log(` ${C.success('✓')} Configured: ${C.bold(selectedAgent.name)} ${C.dim('(CLI Agent)')}`);
762
+ console.log(C.dim(' Saved to .guardlink/config.json'));
763
+ console.log(C.dim(` Use /threat-report or /annotate — they will launch ${selectedAgent.name} automatically.`));
764
+ console.log('');
915
765
  }
916
766
  else {
917
- apiKey = 'ollama-local';
767
+ // ── API provider selection ──
768
+ const providers = ['anthropic', 'openai', 'deepseek', 'openrouter', 'ollama'];
769
+ console.log('');
770
+ console.log(' Select provider:');
771
+ providers.forEach((p, i) => console.log(` ${C.bold(String(i + 1))} ${p}`));
772
+ console.log('');
773
+ const choice = await ask(ctx, ` Provider [1-${providers.length}]: `);
774
+ const idx = parseInt(choice, 10) - 1;
775
+ if (idx < 0 || idx >= providers.length) {
776
+ console.log(C.warn(' Cancelled.'));
777
+ return;
778
+ }
779
+ const provider = providers[idx];
780
+ // API key
781
+ let apiKey = '';
782
+ if (provider !== 'ollama') {
783
+ apiKey = await ask(ctx, ' API Key: ');
784
+ if (!apiKey) {
785
+ console.log(C.warn(' Cancelled — no API key provided.'));
786
+ return;
787
+ }
788
+ }
789
+ else {
790
+ apiKey = 'ollama-local';
791
+ }
792
+ // Model selection
793
+ const defaults = {
794
+ anthropic: 'claude-sonnet-4-5-20250929',
795
+ openai: 'gpt-4o',
796
+ openrouter: 'anthropic/claude-sonnet-4-5-20250929',
797
+ deepseek: 'deepseek-chat',
798
+ ollama: 'llama3.2',
799
+ };
800
+ const model = await ask(ctx, ` Model [${defaults[provider]}]: `);
801
+ saveTuiConfig(ctx.root, {
802
+ aiMode: 'api',
803
+ provider,
804
+ model: model || defaults[provider],
805
+ apiKey,
806
+ });
807
+ const displayKey = apiKey.length > 8 ? apiKey.slice(0, 6) + '•'.repeat(8) : '•'.repeat(8);
808
+ console.log('');
809
+ console.log(` ${C.success('✓')} Configured: ${C.bold(model || defaults[provider])} (${provider})`);
810
+ console.log(` Key: ${displayKey}`);
811
+ console.log(C.dim(' Saved to .guardlink/config.json'));
812
+ console.log('');
918
813
  }
919
- // Model selection
920
- const defaults = {
921
- anthropic: 'claude-sonnet-4-5-20250929',
922
- openai: 'gpt-4o',
923
- openrouter: 'anthropic/claude-sonnet-4-5-20250929',
924
- deepseek: 'deepseek-chat',
925
- ollama: 'llama3.2',
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
814
  }
940
815
  // ─── /threat-report ──────────────────────────────────────────────────
941
816
  export async function cmdThreatReport(args, ctx) {