ship-safe 9.2.0 → 9.2.2

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.
@@ -0,0 +1,190 @@
1
+ [
2
+ {
3
+ "timestamp": "2026-04-24T05:18:07.805Z",
4
+ "score": 13,
5
+ "grade": "F",
6
+ "totalFindings": 533,
7
+ "totalDepVulns": 0,
8
+ "categoryScores": {
9
+ "secrets": {
10
+ "deduction": 15,
11
+ "counts": {
12
+ "critical": 5,
13
+ "high": 0,
14
+ "medium": 2,
15
+ "low": 0
16
+ }
17
+ },
18
+ "injection": {
19
+ "deduction": 15,
20
+ "counts": {
21
+ "critical": 24,
22
+ "high": 47,
23
+ "medium": 22,
24
+ "low": 0
25
+ }
26
+ },
27
+ "deps": {
28
+ "deduction": 0,
29
+ "counts": {
30
+ "critical": 0,
31
+ "high": 0,
32
+ "medium": 0,
33
+ "low": 0
34
+ }
35
+ },
36
+ "auth": {
37
+ "deduction": 15,
38
+ "counts": {
39
+ "critical": 5,
40
+ "high": 18,
41
+ "medium": 10,
42
+ "low": 0
43
+ }
44
+ },
45
+ "config": {
46
+ "deduction": 8,
47
+ "counts": {
48
+ "critical": 3,
49
+ "high": 16,
50
+ "medium": 12,
51
+ "low": 2
52
+ }
53
+ },
54
+ "supply-chain": {
55
+ "deduction": 12,
56
+ "counts": {
57
+ "critical": 3,
58
+ "high": 4,
59
+ "medium": 0,
60
+ "low": 0
61
+ }
62
+ },
63
+ "api": {
64
+ "deduction": 10,
65
+ "counts": {
66
+ "critical": 3,
67
+ "high": 21,
68
+ "medium": 5,
69
+ "low": 6
70
+ }
71
+ },
72
+ "llm": {
73
+ "deduction": 12,
74
+ "counts": {
75
+ "critical": 23,
76
+ "high": 178,
77
+ "medium": 124,
78
+ "low": 0
79
+ }
80
+ }
81
+ },
82
+ "suppressions": {
83
+ "total": 94,
84
+ "rules": {
85
+ "suppression": 1,
86
+ "_unspecified": 78,
87
+ "annotations": 2,
88
+ "on": 4,
89
+ "comments": 1,
90
+ "RULE_NAME": 1,
91
+ "comment": 5,
92
+ "as": 2
93
+ }
94
+ }
95
+ },
96
+ {
97
+ "timestamp": "2026-04-25T19:29:18.418Z",
98
+ "score": 13,
99
+ "grade": "F",
100
+ "totalFindings": 541,
101
+ "totalDepVulns": 0,
102
+ "categoryScores": {
103
+ "secrets": {
104
+ "deduction": 15,
105
+ "counts": {
106
+ "critical": 6,
107
+ "high": 0,
108
+ "medium": 2,
109
+ "low": 0
110
+ }
111
+ },
112
+ "injection": {
113
+ "deduction": 15,
114
+ "counts": {
115
+ "critical": 24,
116
+ "high": 47,
117
+ "medium": 22,
118
+ "low": 0
119
+ }
120
+ },
121
+ "deps": {
122
+ "deduction": 0,
123
+ "counts": {
124
+ "critical": 0,
125
+ "high": 0,
126
+ "medium": 0,
127
+ "low": 0
128
+ }
129
+ },
130
+ "auth": {
131
+ "deduction": 15,
132
+ "counts": {
133
+ "critical": 5,
134
+ "high": 18,
135
+ "medium": 10,
136
+ "low": 0
137
+ }
138
+ },
139
+ "config": {
140
+ "deduction": 8,
141
+ "counts": {
142
+ "critical": 3,
143
+ "high": 16,
144
+ "medium": 12,
145
+ "low": 2
146
+ }
147
+ },
148
+ "supply-chain": {
149
+ "deduction": 12,
150
+ "counts": {
151
+ "critical": 3,
152
+ "high": 4,
153
+ "medium": 0,
154
+ "low": 0
155
+ }
156
+ },
157
+ "api": {
158
+ "deduction": 10,
159
+ "counts": {
160
+ "critical": 3,
161
+ "high": 21,
162
+ "medium": 5,
163
+ "low": 6
164
+ }
165
+ },
166
+ "llm": {
167
+ "deduction": 12,
168
+ "counts": {
169
+ "critical": 23,
170
+ "high": 165,
171
+ "medium": 144,
172
+ "low": 0
173
+ }
174
+ }
175
+ },
176
+ "suppressions": {
177
+ "total": 94,
178
+ "rules": {
179
+ "_unspecified": 78,
180
+ "suppression": 1,
181
+ "annotations": 2,
182
+ "on": 4,
183
+ "comments": 1,
184
+ "RULE_NAME": 1,
185
+ "comment": 5,
186
+ "as": 2
187
+ }
188
+ }
189
+ }
190
+ ]
@@ -663,7 +663,9 @@ How it works:
663
663
  // No command + interactive TTY → drop into the REPL.
664
664
  // Help banner is still available via `--help` and shown when stdin is piped.
665
665
  if (process.argv.length === 2 && process.stdin.isTTY) {
666
- shellCommand('.', {});
666
+ // Await shell before exiting; do NOT fall through to program.parse() or it
667
+ // will print the help banner concurrently with the REPL banner.
668
+ shellCommand('.', {}).then(() => process.exit(0)).catch(() => process.exit(1));
667
669
  } else if (process.argv.length === 2) {
668
670
  console.log(banner);
669
671
  console.log(chalk.yellow('\nQuick start:\n'));
@@ -707,6 +709,6 @@ if (process.argv.length === 2 && process.stdin.isTTY) {
707
709
  console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
708
710
  console.log();
709
711
  process.exit(0);
712
+ } else {
713
+ program.parse();
710
714
  }
711
-
712
- program.parse();
@@ -327,7 +327,7 @@ export async function auditCommand(targetPath = '.', options = {}) {
327
327
  );
328
328
 
329
329
  // ── AI Classification (optional, with LLM cache) ───────────────────────
330
- if (options.ai !== false) {
330
+ if (options.ai !== false && !options.noAi) {
331
331
  const provider = autoDetectProvider(absolutePath, {
332
332
  provider: options.provider,
333
333
  baseUrl: options.baseUrl,
@@ -20,7 +20,9 @@
20
20
 
21
21
  import { createInterface } from 'readline';
22
22
  import { execFileSync, spawnSync } from 'child_process';
23
+ import { readFileSync as fsReadFileSync } from 'fs';
23
24
  import path from 'path';
25
+ import { fileURLToPath } from 'url';
24
26
  import chalk from 'chalk';
25
27
  import ora from 'ora';
26
28
  import { autoDetectProvider } from '../providers/llm-provider.js';
@@ -31,6 +33,84 @@ import * as output from '../utils/output.js';
31
33
 
32
34
  const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
33
35
 
36
+ // Read version from package.json so the banner stays in sync with releases.
37
+ const PKG_VERSION = (() => {
38
+ try {
39
+ const here = path.dirname(fileURLToPath(import.meta.url));
40
+ const pkg = JSON.parse(fsReadFileSync(path.join(here, '..', '..', 'package.json'), 'utf8'));
41
+ return pkg.version;
42
+ } catch { return ''; }
43
+ })();
44
+
45
+ const BANNER_LINES = [
46
+ ' ███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗',
47
+ ' ██╔════╝██║ ██║██║██╔══██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝',
48
+ ' ███████╗███████║██║██████╔╝ ███████╗███████║█████╗ █████╗ ',
49
+ ' ╚════██║██╔══██║██║██╔═══╝ ╚════██║██╔══██║██╔══╝ ██╔══╝ ',
50
+ ' ███████║██║ ██║██║██║ ███████║██║ ██║██║ ███████╗',
51
+ ' ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝',
52
+ ];
53
+
54
+ // Characters drawn from the same box-drawing + block set the banner uses,
55
+ // so random frames look plausibly related rather than pure noise.
56
+ const GLITCH_CHARS = '█▓▒░╔╗╚╝╠╣╦╩╬═║┼┤├┬┴┐└┘┌─│╱╲╳';
57
+
58
+ function glitchFrame(line, reveal) {
59
+ // `reveal` is 0.0..1.0 — characters left of the reveal frontier are real,
60
+ // everything to the right is randomised from the glitch set.
61
+ return line.split('').map((ch, i) => {
62
+ const pos = i / line.length;
63
+ if (pos < reveal || ch === ' ') return ch;
64
+ return GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
65
+ }).join('');
66
+ }
67
+
68
+ function sleep(ms) {
69
+ return new Promise(r => setTimeout(r, ms));
70
+ }
71
+
72
+ async function shipSafeBannerAnimated() {
73
+ const cols = process.stdout.columns;
74
+ const narrow = typeof cols === 'number' && cols > 0 && cols < 70;
75
+ if (narrow) {
76
+ console.log(chalk.cyan.bold(' S H I P S A F E'));
77
+ return;
78
+ }
79
+
80
+ const lines = BANNER_LINES;
81
+ const FRAMES = 8; // glitch passes before the line locks in
82
+ const FRAME_MS = 18; // ms between frames within a line
83
+ const LINE_MS = 28; // ms between lines starting
84
+
85
+ // Print an empty placeholder so we know exactly where each line lives.
86
+ // We'll overwrite them in-place using cursor-up escape codes.
87
+ process.stdout.write('\n');
88
+ for (const line of lines) {
89
+ process.stdout.write(' '.repeat(line.length) + '\n');
90
+ }
91
+ process.stdout.write('\n');
92
+
93
+ for (let li = 0; li < lines.length; li++) {
94
+ const line = lines[li];
95
+ // Move cursor up to this line's row (from the blank line after the banner).
96
+ const stepsUp = lines.length - li + 1;
97
+ process.stdout.write(`\x1B[${stepsUp}A`); // cursor up N
98
+
99
+ // Animate: reveal marches from 0 → 1 across FRAMES frames.
100
+ for (let f = 0; f <= FRAMES; f++) {
101
+ const reveal = f / FRAMES;
102
+ const scrambled = glitchFrame(line, reveal);
103
+ process.stdout.write('\r' + chalk.cyan(scrambled));
104
+ if (f < FRAMES) await sleep(FRAME_MS);
105
+ }
106
+
107
+ // Write the final clean line and move back down to the bottom blank line.
108
+ process.stdout.write('\r' + chalk.cyan(line));
109
+ process.stdout.write(`\x1B[${stepsUp}B`); // cursor down N
110
+ await sleep(LINE_MS);
111
+ }
112
+ }
113
+
34
114
  export async function shellCommand(targetPath = '.', options = {}) {
35
115
  const root = path.resolve(targetPath);
36
116
 
@@ -49,13 +129,18 @@ export async function shellCommand(targetPath = '.', options = {}) {
49
129
  think: options.think || false,
50
130
  });
51
131
 
132
+ // Branded entry: big wordmark, version + provider + cwd, then a CTA hint.
133
+ // Modeled on Claude Code / Gemini CLI / Aider — establish the tool's identity
134
+ // in the first second, then get out of the way.
52
135
  console.log();
53
- output.header('Ship Safe — Interactive Shell');
54
- console.log(chalk.gray(` cwd: ${root}`));
55
- console.log(chalk.gray(` provider: ${state.provider ? chalk.cyan(state.provider.name) : chalk.yellow('none — set DEEPSEEK_API_KEY or similar')}`));
136
+ await shipSafeBannerAnimated();
137
+ // trailing blank line is already written by the animation loop
138
+ const ver = PKG_VERSION ? `v${PKG_VERSION}` : '';
139
+ const prov = state.provider ? chalk.cyan(state.provider.name) : chalk.yellow('no provider');
140
+ const cwd = chalk.gray(prettyCwd(root));
141
+ console.log(` ${chalk.bold(ver)} ${chalk.gray('·')} ${prov} ${chalk.gray('·')} ${cwd}`);
56
142
  console.log();
57
- console.log(chalk.gray(' Type /help for commands, anything else to ask the agent.'));
58
- console.log(chalk.gray(' /quit or Ctrl-D to exit.'));
143
+ console.log(chalk.gray(' /scan to find issues · /agent to fix them · /help for more'));
59
144
  console.log();
60
145
 
61
146
  const rl = createInterface({
@@ -207,13 +292,16 @@ async function handleSlashCommand(line, state, options) {
207
292
  case 'agent':
208
293
  case 'fix': {
209
294
  // Hand off to agent command. Pass through caller options + any inline flags.
295
+ // Forward the active provider key so /provider switches take effect.
210
296
  const opts = { ...options };
297
+ if (state.providerKey) opts.provider = state.providerKey;
211
298
  for (const a of args) {
212
299
  if (a === '--plan-only') opts.planOnly = true;
213
300
  if (a === '--allow-dirty') opts.allowDirty = true;
214
301
  if (a === '--branch') opts.branch = true;
215
302
  if (a === '--pr') opts.pr = true;
216
303
  if (a.startsWith('--severity=')) opts.severity = a.slice('--severity='.length);
304
+ if (a.startsWith('--provider=')) opts.provider = a.slice('--provider='.length);
217
305
  }
218
306
  try {
219
307
  await agentFixCommand(state.root, opts);
@@ -243,7 +331,8 @@ async function handleSlashCommand(line, state, options) {
243
331
  if (!next) {
244
332
  console.log(chalk.yellow(` Could not load provider "${name}" — is the API key set?`));
245
333
  } else {
246
- state.provider = next;
334
+ state.provider = next;
335
+ state.providerKey = name; // keep the string so /agent can forward it
247
336
  console.log(chalk.green(` Provider switched to ${next.name}.`));
248
337
  }
249
338
  return true;
@@ -407,6 +496,12 @@ function sevTag(sev) {
407
496
  }
408
497
  }
409
498
 
499
+ function prettyCwd(p) {
500
+ const home = process.env.HOME || '';
501
+ if (home && p.startsWith(home + '/')) return '~' + p.slice(home.length);
502
+ return p;
503
+ }
504
+
410
505
  function gradeColor(score) {
411
506
  if (score == null) return chalk.gray;
412
507
  if (score >= 80) return chalk.green;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "9.2.0",
3
+ "version": "9.2.2",
4
4
  "description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {