@skillguard/cli 0.2.1 → 0.5.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Added `skillguard auth` command group:
6
+ - `auth login` (store key),
7
+ - `auth status` (show effective auth source),
8
+ - `auth logout` (remove local config key)
9
+ - Added auth help/man coverage in CLI help pages
10
+
11
+ ## 0.4.0
12
+
13
+ - Added `skillguard workflow` command to generate ready-to-use GitHub Actions CI gate workflow
14
+ - Added `--auto-gh-push` option for workflow command (`git add/commit/push`)
15
+ - Added `skillguard help <command>` and `skillguard man <command>` detailed help pages
16
+ - Added `--workflow` alias for fast workflow generation
17
+ - Updated CLI/web workflow snippets to default to zero-secret gate flow
18
+
19
+ ## 0.3.0
20
+
21
+ - Added `skillguard gate` command for deterministic CI gating (`0` pass, `1` fail)
22
+ - Added `skillguard limits` command with human and `--json` output
23
+ - Added anonymous scan fallback when no API key is configured
24
+ - Added CLI telemetry headers (`X-SkillGuard-Source`, `X-SkillGuard-CI`)
25
+ - Added support for backend anonymous limits endpoint
26
+
3
27
  ## 0.2.0
4
28
 
5
29
  - Added `skillguard scan` without path argument (defaults to recursive scan from current directory)
package/README.md CHANGED
@@ -8,18 +8,38 @@ Security scanner CLI for AI agent `SKILL.md` files.
8
8
  npx @skillguard/cli scan SKILL.md
9
9
  ```
10
10
 
11
+ No API key configured? CLI falls back to anonymous scanning (rate-limited).
12
+
11
13
  ### Scan all skills in current repo
12
14
 
13
15
  ```bash
14
16
  npx @skillguard/cli scan
15
17
  ```
16
18
 
19
+ ### Generate CI workflow (no secrets required)
20
+
21
+ ```bash
22
+ npx @skillguard/cli workflow
23
+ ```
24
+
25
+ ### Command help (man-like)
26
+
27
+ ```bash
28
+ npx @skillguard/cli help scan
29
+ ```
30
+
17
31
  ## Setup
18
32
 
19
33
  ```bash
20
34
  npx @skillguard/cli init
21
35
  ```
22
36
 
37
+ Or via auth command:
38
+
39
+ ```bash
40
+ npx @skillguard/cli auth login
41
+ ```
42
+
23
43
  You can also provide the key via env:
24
44
 
25
45
  ```bash
@@ -46,6 +66,31 @@ npx @skillguard/cli scan ./skills --fail-on warning
46
66
  npx @skillguard/cli scan ./skills --json > skillguard-report.json
47
67
  ```
48
68
 
69
+ ### CI gate (recommended)
70
+
71
+ ```bash
72
+ npx @skillguard/cli gate ./skills
73
+ ```
74
+
75
+ ### Check remaining limits
76
+
77
+ ```bash
78
+ npx @skillguard/cli limits
79
+ ```
80
+
81
+ ### Auth status and logout
82
+
83
+ ```bash
84
+ npx @skillguard/cli auth status
85
+ npx @skillguard/cli auth logout
86
+ ```
87
+
88
+ ### Generate and push workflow in one command
89
+
90
+ ```bash
91
+ npx @skillguard/cli workflow --auto-gh-push
92
+ ```
93
+
49
94
  ### Worst-score CI exit code (recommended)
50
95
 
51
96
  ```bash
@@ -74,6 +119,39 @@ npx @skillguard/cli verify ./SKILL.md
74
119
  - `--skip-node-modules` (default: enabled)
75
120
  - `--scan-all` (disable skip filters, scans everything recursively)
76
121
 
122
+ ### gate
123
+
124
+ - same options as `scan`
125
+ - defaults to CI gate behavior (`--fail-on warning` in threshold mode)
126
+
127
+ ### limits
128
+
129
+ - `--json`
130
+ - `--timeout <ms>` (default: `30000`)
131
+ - `--base-url <url>` (default: `https://skillguard.ai`)
132
+ - `--api-key <key>` (optional)
133
+ - `--no-color`
134
+
135
+ ### auth
136
+
137
+ - `auth login [--api-key <key>] [--base-url <url>]`
138
+ - `auth status [--json] [--api-key <key>] [--base-url <url>]`
139
+ - `auth logout`
140
+
141
+ ### workflow
142
+
143
+ - `--path <file>` (default: `.github/workflows/skillguard.yml`)
144
+ - `--scan-path <path>` (default: `.`)
145
+ - `--fail-on <safe|warning|dangerous>` (default: `warning`)
146
+ - `--print` (print yaml, do not write file)
147
+ - `--force` (overwrite existing workflow file)
148
+ - `--auto-gh-push` (git add/commit/push after write)
149
+
150
+ ### help / man
151
+
152
+ - `help [command]`
153
+ - `man [command]`
154
+
77
155
  ### verify
78
156
 
79
157
  - `--json`
@@ -92,6 +170,15 @@ npx @skillguard/cli verify ./SKILL.md
92
170
  - `verify`:
93
171
  - `0` signature valid
94
172
  - `1` invalid/tampered/expired signature
173
+ - `limits`:
174
+ - `0` success
175
+ - `gate`:
176
+ - `0` below threshold
177
+ - `1` threshold exceeded
178
+ - `workflow`:
179
+ - `0` success
180
+ - `auth`:
181
+ - `0` success
95
182
  - all commands:
96
183
  - `4` usage/input/config error
97
184
  - `5` network/API/runtime external failure
@@ -99,8 +186,16 @@ npx @skillguard/cli verify ./SKILL.md
99
186
  ## GitHub Actions example
100
187
 
101
188
  ```yaml
102
- name: Skill Security Scan
103
- on: [push, pull_request]
189
+ name: SkillGuard Gate
190
+ on:
191
+ pull_request:
192
+ paths:
193
+ - '**/SKILL.md'
194
+ - '**/skill.md'
195
+ push:
196
+ paths:
197
+ - '**/SKILL.md'
198
+ - '**/skill.md'
104
199
 
105
200
  jobs:
106
201
  scan:
@@ -110,9 +205,7 @@ jobs:
110
205
  - uses: actions/setup-node@v4
111
206
  with:
112
207
  node-version: '20'
113
- - run: npx @skillguard/cli scan ./skills
114
- env:
115
- SKILLGUARD_API_KEY: ${{ secrets.SKILLGUARD_API_KEY }}
208
+ - run: npx @skillguard/cli gate . --fail-on warning
116
209
  ```
117
210
 
118
211
  More info: [skillguard.ai](https://skillguard.ai)
package/dist/cli.js CHANGED
@@ -1,21 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
+ import { runAuth } from './commands/auth.js';
3
4
  import { runInit } from './commands/init.js';
5
+ import { runGate } from './commands/gate.js';
6
+ import { runLimits } from './commands/limits.js';
4
7
  import { runScan } from './commands/scan.js';
5
8
  import { runVerify } from './commands/verify.js';
6
- const VERSION = '0.2.0';
9
+ import { runWorkflow } from './commands/workflow.js';
10
+ import { renderGeneralHelp, renderTopicHelp } from './lib/help.js';
11
+ import { CLI_VERSION } from './lib/version.js';
12
+ const VERSION = CLI_VERSION;
7
13
  function printUsage() {
8
- console.log(`SkillGuard CLI ${VERSION}
9
-
10
- Usage:
11
- skillguard init [--api-key <key>] [--base-url <url>]
12
- skillguard scan [path] [--json] [--fail-on <safe|warning|dangerous>] [--timeout <ms>] [--base-url <url>] [--api-key <key>] [--dry-run] [--quiet] [--no-color] [--skip-node-modules] [--scan-all]
13
- skillguard verify <path> [--json] [--timeout <ms>] [--base-url <url>]
14
-
15
- Options:
16
- --help Show help
17
- --version Show version
18
- `);
14
+ console.log(renderGeneralHelp(VERSION));
19
15
  }
20
16
  function toPositiveInteger(raw, fallback) {
21
17
  if (!raw) {
@@ -53,6 +49,11 @@ function parseShared(args) {
53
49
  timeout: { type: 'string' },
54
50
  'base-url': { type: 'string' },
55
51
  'api-key': { type: 'string' },
52
+ path: { type: 'string' },
53
+ 'scan-path': { type: 'string' },
54
+ print: { type: 'boolean' },
55
+ force: { type: 'boolean' },
56
+ 'auto-gh-push': { type: 'boolean' },
56
57
  },
57
58
  strict: false,
58
59
  });
@@ -66,7 +67,12 @@ function isFailOnExplicit(args) {
66
67
  }
67
68
  async function run() {
68
69
  const argv = process.argv.slice(2);
69
- const command = argv[0];
70
+ let command = argv[0];
71
+ let commandArgs = argv.slice(1);
72
+ if (command === '--workflow') {
73
+ command = 'workflow';
74
+ commandArgs = argv.slice(1);
75
+ }
70
76
  if (!command || command === '--help' || command === '-h') {
71
77
  printUsage();
72
78
  return 0;
@@ -75,13 +81,33 @@ async function run() {
75
81
  console.log(VERSION);
76
82
  return 0;
77
83
  }
78
- const { options, positionals } = parseShared(argv.slice(1));
84
+ if (command === 'help' || command === 'man') {
85
+ const topic = argv[1];
86
+ if (!topic) {
87
+ printUsage();
88
+ return 0;
89
+ }
90
+ const helpText = renderTopicHelp(topic);
91
+ if (!helpText) {
92
+ console.error(`Unknown help topic: ${topic}`);
93
+ return 4;
94
+ }
95
+ console.log(helpText);
96
+ return 0;
97
+ }
98
+ const { options, positionals } = parseShared(commandArgs);
79
99
  if (options.version) {
80
100
  console.log(VERSION);
81
101
  return 0;
82
102
  }
83
103
  if (options.help) {
84
- printUsage();
104
+ const topicHelp = renderTopicHelp(command);
105
+ if (topicHelp) {
106
+ console.log(topicHelp);
107
+ }
108
+ else {
109
+ printUsage();
110
+ }
85
111
  return 0;
86
112
  }
87
113
  if (command === 'init') {
@@ -90,6 +116,15 @@ async function run() {
90
116
  baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
91
117
  });
92
118
  }
119
+ if (command === 'auth') {
120
+ const authOptions = {
121
+ subcommand: positionals[0],
122
+ json: options.json === true,
123
+ baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
124
+ apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
125
+ };
126
+ return await runAuth(authOptions);
127
+ }
93
128
  if (command === 'scan') {
94
129
  const pathArg = positionals[0];
95
130
  let parsedTimeout;
@@ -119,6 +154,69 @@ async function run() {
119
154
  };
120
155
  return await runScan(pathArg, scanOptions);
121
156
  }
157
+ if (command === 'gate') {
158
+ const pathArg = positionals[0];
159
+ let parsedTimeout;
160
+ let failOn;
161
+ try {
162
+ parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
163
+ failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
164
+ }
165
+ catch (error) {
166
+ console.error(error.message);
167
+ return 4;
168
+ }
169
+ const gateOptions = {
170
+ json: options.json === true,
171
+ failOn,
172
+ scanAll: options['scan-all'] === true,
173
+ skipNodeModules: options['scan-all'] === true ? false : options['skip-node-modules'] !== false,
174
+ timeoutMs: parsedTimeout,
175
+ baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
176
+ apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
177
+ dryRun: options['dry-run'] === true,
178
+ quiet: options.quiet === true,
179
+ noColor: options['no-color'] === true,
180
+ };
181
+ return await runGate(pathArg, gateOptions);
182
+ }
183
+ if (command === 'limits') {
184
+ let parsedTimeout;
185
+ try {
186
+ parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
187
+ }
188
+ catch (error) {
189
+ console.error(error.message);
190
+ return 4;
191
+ }
192
+ const limitsOptions = {
193
+ json: options.json === true,
194
+ timeoutMs: parsedTimeout,
195
+ baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
196
+ apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
197
+ noColor: options['no-color'] === true,
198
+ };
199
+ return await runLimits(limitsOptions);
200
+ }
201
+ if (command === 'workflow') {
202
+ let failOn;
203
+ try {
204
+ failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
205
+ }
206
+ catch (error) {
207
+ console.error(error.message);
208
+ return 4;
209
+ }
210
+ const workflowOptions = {
211
+ outputPath: typeof options.path === 'string' ? options.path : undefined,
212
+ scanPath: typeof options['scan-path'] === 'string' ? options['scan-path'] : '.',
213
+ failOn,
214
+ print: options.print === true,
215
+ force: options.force === true,
216
+ autoGhPush: options['auto-gh-push'] === true,
217
+ };
218
+ return await runWorkflow(workflowOptions);
219
+ }
122
220
  if (command === 'verify') {
123
221
  const pathArg = positionals[0];
124
222
  if (!pathArg) {
@@ -0,0 +1,7 @@
1
+ export interface AuthOptions {
2
+ subcommand?: string;
3
+ apiKey?: string;
4
+ baseUrl?: string;
5
+ json: boolean;
6
+ }
7
+ export declare function runAuth(options: AuthOptions): Promise<number>;
@@ -0,0 +1,103 @@
1
+ import { clearConfig, DEFAULT_BASE_URL, getConfigPath, normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
2
+ import { runInit } from './init.js';
3
+ function toAuthSubcommand(raw) {
4
+ if (!raw) {
5
+ return null;
6
+ }
7
+ const value = raw.trim().toLowerCase();
8
+ if (value === 'login')
9
+ return 'login';
10
+ if (value === 'status')
11
+ return 'status';
12
+ if (value === 'logout')
13
+ return 'logout';
14
+ return null;
15
+ }
16
+ function renderStatusHuman(input) {
17
+ const lines = [
18
+ `Auth: ${input.authenticated ? 'configured' : 'not configured'}`,
19
+ `Source: ${input.source}`,
20
+ `Base URL: ${input.baseUrl}`,
21
+ `Config file: ${input.configPath}`,
22
+ ];
23
+ if (!input.authenticated) {
24
+ lines.push('No API key found. Use "skillguard auth login" or set SKILLGUARD_API_KEY.');
25
+ }
26
+ if (input.hasEnvKey) {
27
+ lines.push('Env key detected: SKILLGUARD_API_KEY');
28
+ }
29
+ if (input.hasConfigKey) {
30
+ lines.push('Config key detected.');
31
+ }
32
+ return lines.join('\n');
33
+ }
34
+ async function runStatus(options) {
35
+ const configPath = getConfigPath();
36
+ let config = null;
37
+ try {
38
+ config = await readConfig(configPath);
39
+ }
40
+ catch (error) {
41
+ console.error(error.message);
42
+ return 4;
43
+ }
44
+ const resolvedApiKey = resolveApiKey({
45
+ apiKeyFlag: options.apiKey,
46
+ env: process.env,
47
+ config,
48
+ });
49
+ let baseUrl;
50
+ try {
51
+ baseUrl = normalizeBaseUrl(options.baseUrl || config?.apiUrl || DEFAULT_BASE_URL);
52
+ }
53
+ catch (error) {
54
+ console.error(error.message);
55
+ return 4;
56
+ }
57
+ const payload = {
58
+ authenticated: Boolean(resolvedApiKey),
59
+ source: (resolvedApiKey?.source || 'none'),
60
+ baseUrl,
61
+ configPath,
62
+ hasConfigKey: typeof config?.apiKey === 'string' && config.apiKey.trim().length > 0,
63
+ hasEnvKey: typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0,
64
+ };
65
+ if (options.json) {
66
+ console.log(JSON.stringify(payload, null, 2));
67
+ return 0;
68
+ }
69
+ console.log(renderStatusHuman(payload));
70
+ return 0;
71
+ }
72
+ async function runLogout() {
73
+ const configPath = getConfigPath();
74
+ try {
75
+ const removed = await clearConfig(configPath);
76
+ console.log(removed ? `Logged out. Removed ${configPath}` : `No config found at ${configPath}`);
77
+ }
78
+ catch (error) {
79
+ console.error(error.message);
80
+ return 4;
81
+ }
82
+ if (typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0) {
83
+ console.log('Note: SKILLGUARD_API_KEY is set in environment and will still be used by this shell.');
84
+ }
85
+ return 0;
86
+ }
87
+ export async function runAuth(options) {
88
+ const subcommand = toAuthSubcommand(options.subcommand);
89
+ if (!subcommand) {
90
+ console.error('Auth subcommand is required: login | status | logout');
91
+ return 4;
92
+ }
93
+ if (subcommand === 'login') {
94
+ return runInit({
95
+ apiKey: options.apiKey,
96
+ baseUrl: options.baseUrl,
97
+ });
98
+ }
99
+ if (subcommand === 'status') {
100
+ return runStatus(options);
101
+ }
102
+ return runLogout();
103
+ }
@@ -0,0 +1,14 @@
1
+ import type { ScanScore } from '../lib/api.js';
2
+ export interface GateOptions {
3
+ json: boolean;
4
+ failOn?: ScanScore;
5
+ scanAll: boolean;
6
+ skipNodeModules: boolean;
7
+ timeoutMs: number;
8
+ baseUrl?: string;
9
+ apiKey?: string;
10
+ dryRun: boolean;
11
+ quiet: boolean;
12
+ noColor: boolean;
13
+ }
14
+ export declare function runGate(inputPath: string | undefined, options: GateOptions): Promise<number>;
@@ -0,0 +1,17 @@
1
+ import { runScan } from './scan.js';
2
+ export async function runGate(inputPath, options) {
3
+ const scanOptions = {
4
+ json: options.json,
5
+ failOn: options.failOn || 'warning',
6
+ failOnExplicit: true,
7
+ scanAll: options.scanAll,
8
+ skipNodeModules: options.skipNodeModules,
9
+ timeoutMs: options.timeoutMs,
10
+ baseUrl: options.baseUrl,
11
+ apiKey: options.apiKey,
12
+ dryRun: options.dryRun,
13
+ quiet: options.quiet,
14
+ noColor: options.noColor,
15
+ };
16
+ return runScan(inputPath, scanOptions);
17
+ }
@@ -0,0 +1,8 @@
1
+ export interface LimitsOptions {
2
+ json: boolean;
3
+ timeoutMs: number;
4
+ baseUrl?: string;
5
+ apiKey?: string;
6
+ noColor: boolean;
7
+ }
8
+ export declare function runLimits(options: LimitsOptions): Promise<number>;
@@ -0,0 +1,98 @@
1
+ import { fetchLimits } from '../lib/api.js';
2
+ import { normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
3
+ import { CLI_VERSION } from '../lib/version.js';
4
+ function renderHuman(response) {
5
+ const lines = [];
6
+ const mode = typeof response.mode === 'string' ? response.mode : 'unknown';
7
+ lines.push(`Mode: ${mode}`);
8
+ if (typeof response.tokenBalance === 'number') {
9
+ lines.push(`Token balance: ${response.tokenBalance}`);
10
+ }
11
+ if (response.ownerTrial && typeof response.ownerTrial === 'object') {
12
+ const trial = response.ownerTrial;
13
+ lines.push('Owner trial:');
14
+ lines.push(` scans: ${Number(trial.scansUsed || 0)} / ${Number(trial.scansLimit || 0)}`);
15
+ lines.push(` remaining: ${Number(trial.remainingScans || 0)}`);
16
+ lines.push(` active: ${trial.active === true ? 'yes' : 'no'}`);
17
+ if (typeof trial.expiresAt === 'string' && trial.expiresAt) {
18
+ lines.push(` expires: ${trial.expiresAt}`);
19
+ }
20
+ }
21
+ if (response.apiKeyQuota && typeof response.apiKeyQuota === 'object') {
22
+ const quota = response.apiKeyQuota;
23
+ lines.push('API key quota:');
24
+ lines.push(` key type: ${String(quota.keyType || 'standard')}`);
25
+ lines.push(` unlimited: ${quota.unlimited === true ? 'yes' : 'no'}`);
26
+ lines.push(` scans used: ${Number(quota.scansUsed || 0)}`);
27
+ if (typeof quota.maxScans === 'number') {
28
+ lines.push(` max scans: ${quota.maxScans}`);
29
+ }
30
+ if (typeof quota.remainingScans === 'number') {
31
+ lines.push(` remaining: ${quota.remainingScans}`);
32
+ }
33
+ lines.push(` active: ${quota.active === true ? 'yes' : 'no'}`);
34
+ if (typeof quota.expiresAt === 'string' && quota.expiresAt) {
35
+ lines.push(` expires: ${quota.expiresAt}`);
36
+ }
37
+ }
38
+ if (response.anonymousQuota && typeof response.anonymousQuota === 'object') {
39
+ const quota = response.anonymousQuota;
40
+ lines.push('Anonymous quota:');
41
+ lines.push(` scans: ${Number(quota.scansUsed || 0)} / ${Number(quota.maxScans || 0)}`);
42
+ lines.push(` remaining: ${Number(quota.remainingScans || 0)}`);
43
+ if (typeof quota.resetAt === 'string' && quota.resetAt) {
44
+ lines.push(` reset at: ${quota.resetAt}`);
45
+ }
46
+ }
47
+ return lines.join('\n');
48
+ }
49
+ export async function runLimits(options) {
50
+ let baseUrl;
51
+ try {
52
+ baseUrl = normalizeBaseUrl(options.baseUrl);
53
+ }
54
+ catch (error) {
55
+ console.error(error.message);
56
+ return 4;
57
+ }
58
+ const hasApiKeyFlag = typeof options.apiKey === 'string' && options.apiKey.trim().length > 0;
59
+ const hasApiKeyEnv = typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0;
60
+ let config = null;
61
+ if (!hasApiKeyFlag && !hasApiKeyEnv) {
62
+ try {
63
+ config = await readConfig();
64
+ }
65
+ catch (error) {
66
+ console.error(error.message);
67
+ return 4;
68
+ }
69
+ }
70
+ const resolvedApiKey = resolveApiKey({
71
+ apiKeyFlag: options.apiKey,
72
+ env: process.env,
73
+ config,
74
+ });
75
+ let response;
76
+ try {
77
+ response = await fetchLimits({
78
+ baseUrl,
79
+ timeoutMs: options.timeoutMs,
80
+ apiKey: resolvedApiKey?.apiKey,
81
+ });
82
+ }
83
+ catch (error) {
84
+ console.error(error.message);
85
+ return 5;
86
+ }
87
+ if (options.json) {
88
+ console.log(JSON.stringify({
89
+ cli_version: CLI_VERSION,
90
+ fetched_at: new Date().toISOString(),
91
+ ...response,
92
+ }, null, 2));
93
+ }
94
+ else {
95
+ console.log(renderHuman(response));
96
+ }
97
+ return 0;
98
+ }
@@ -1,8 +1,9 @@
1
1
  import { readFile, readdir, realpath } from 'node:fs/promises';
2
2
  import { basename, resolve, sep } from 'node:path';
3
- import { normalizeFindings, scanContent, toScanScore } from '../lib/api.js';
3
+ import { normalizeFindings, scanContent, scanContentAnonymous, toScanScore } from '../lib/api.js';
4
4
  import { normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
5
5
  import { renderSingleScan, renderSummary, shouldUseColor, summarizeScores } from '../lib/format.js';
6
+ import { CLI_VERSION } from '../lib/version.js';
6
7
  const FAIL_LEVELS = {
7
8
  safe: 0,
8
9
  warning: 1,
@@ -164,22 +165,32 @@ export async function runScan(inputPath, options) {
164
165
  env: process.env,
165
166
  config,
166
167
  });
167
- if (!resolvedApiKey) {
168
+ const useAnonymous = !resolvedApiKey;
169
+ if (!useAnonymous && !resolvedApiKey) {
168
170
  console.error("No API key found. Run 'skillguard init' or set SKILLGUARD_API_KEY.");
169
171
  return 4;
170
172
  }
171
173
  const color = shouldUseColor(options.noColor);
172
174
  const results = [];
175
+ if (useAnonymous && !options.quiet && !options.json) {
176
+ console.log('Scanning anonymously (limited). Add API key for higher limits.');
177
+ }
173
178
  for (const filePath of targets) {
174
179
  const content = await readFile(filePath, 'utf8');
175
180
  let apiResponse;
176
181
  try {
177
- apiResponse = await scanContent({
178
- baseUrl,
179
- apiKey: resolvedApiKey.apiKey,
180
- content,
181
- timeoutMs: options.timeoutMs,
182
- });
182
+ apiResponse = useAnonymous
183
+ ? await scanContentAnonymous({
184
+ baseUrl,
185
+ content,
186
+ timeoutMs: options.timeoutMs,
187
+ })
188
+ : await scanContent({
189
+ baseUrl,
190
+ apiKey: resolvedApiKey.apiKey,
191
+ content,
192
+ timeoutMs: options.timeoutMs,
193
+ });
183
194
  }
184
195
  catch (error) {
185
196
  console.error(error.message);
@@ -208,7 +219,7 @@ export async function runScan(inputPath, options) {
208
219
  if (options.json) {
209
220
  const summary = summarizeScores(results);
210
221
  console.log(JSON.stringify({
211
- cli_version: '0.2.0',
222
+ cli_version: CLI_VERSION,
212
223
  scanned_at: new Date().toISOString(),
213
224
  mode,
214
225
  worst_score: worstScore,
@@ -0,0 +1,21 @@
1
+ import type { ScanScore } from '../lib/api.js';
2
+ export interface WorkflowOptions {
3
+ outputPath?: string;
4
+ scanPath: string;
5
+ failOn: ScanScore;
6
+ print: boolean;
7
+ force: boolean;
8
+ autoGhPush: boolean;
9
+ commitMessage?: string;
10
+ }
11
+ interface GitCommandResult {
12
+ stdout: string;
13
+ stderr: string;
14
+ status: number;
15
+ }
16
+ type GitRunner = (args: string[]) => GitCommandResult;
17
+ export declare function runWorkflow(options: WorkflowOptions, runtime?: {
18
+ cwd?: string;
19
+ gitRunner?: GitRunner;
20
+ }): Promise<number>;
21
+ export {};