skillsio 1.1.0 → 1.1.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.
- package/README.md +46 -7
- package/dist/cli.mjs +235 -140
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ security gate so you can still move fast without running untrusted code.
|
|
|
14
14
|
|
|
15
15
|
## What It Does
|
|
16
16
|
|
|
17
|
-
Every `skillsio add` command runs a local security scan **before** anything is installed. The scanner applies ~
|
|
17
|
+
Every `skillsio add` command runs a local security scan **before** anything is installed. The scanner applies ~81 regex
|
|
18
18
|
rules and a correlation engine derived from the Snyk and ClawHavoc research, organized into 8 threat categories:
|
|
19
19
|
|
|
20
20
|
| Category | What it catches |
|
|
@@ -50,6 +50,23 @@ domain patterns that regex rules can't — letting you eyeball where a skill wan
|
|
|
50
50
|
With `--yes`, URL-only prompts are auto-continued. Skills with high/critical findings always show URLs alongside the
|
|
51
51
|
findings summary.
|
|
52
52
|
|
|
53
|
+
### Third-Party Audits via skills.sh
|
|
54
|
+
|
|
55
|
+
For GitHub-sourced skills, the CLI automatically checks [skills.sh](https://skills.sh) — Vercel's official skill
|
|
56
|
+
directory — which runs independent third-party security audits from three auditors: **Snyk**, **Socket**, and
|
|
57
|
+
**Gen Agent Trust Hub**. Results appear alongside local scan output:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
◆ skills.sh: 3 audits [Snyk ✗] [Socket ✓] [Trust Hub ✗]
|
|
61
|
+
https://skills.sh/inference-sh-3/skills/agent-tools
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Green ✓ = auditor passed, Red ✗ = auditor failed, Dim ~ = no result yet
|
|
65
|
+
- If any auditor returns a **Fail** verdict, severity is escalated to at least **High**, triggering a confirmation
|
|
66
|
+
prompt
|
|
67
|
+
- skills.sh lookup runs in parallel with VT and never blocks installation on error (graceful fallback)
|
|
68
|
+
- Only fires for GitHub-sourced skills that are listed on skills.sh — silent for everything else
|
|
69
|
+
|
|
53
70
|
### Optional: VirusTotal Integration
|
|
54
71
|
|
|
55
72
|
When a [VirusTotal](https://www.virustotal.com/) API key is provided, the CLI also hashes each skill's content
|
|
@@ -120,7 +137,7 @@ npx skillsio add owner/repo --rules ./my-rules.json
|
|
|
120
137
|
npx skillsio add owner/repo --rules ./rules/
|
|
121
138
|
```
|
|
122
139
|
|
|
123
|
-
External rules are applied **in addition to** the built-in ~
|
|
140
|
+
External rules are applied **in addition to** the built-in ~81 rules — they never replace them. Findings from external
|
|
124
141
|
rules follow the same severity-based prompt flow as built-in findings.
|
|
125
142
|
|
|
126
143
|
See [docs/EXTERNAL-RULES.md](docs/EXTERNAL-RULES.md) for the full format reference, more examples, and tips for writing
|
|
@@ -289,8 +306,6 @@ The CLI automatically detects which coding agents you have installed.
|
|
|
289
306
|
| --- | --- |
|
|
290
307
|
| `VT_API_KEY` | VirusTotal API key for optional threat intelligence during security scans |
|
|
291
308
|
| `INSTALL_INTERNAL_SKILLS` | Set to `1` to show and install skills marked as `internal: true` |
|
|
292
|
-
| `DISABLE_TELEMETRY` | Disable anonymous usage telemetry |
|
|
293
|
-
| `DO_NOT_TRACK` | Alternative way to disable telemetry |
|
|
294
309
|
|
|
295
310
|
## Development
|
|
296
311
|
|
|
@@ -305,13 +320,15 @@ pnpm format # Format code with Prettier
|
|
|
305
320
|
|
|
306
321
|
### Scanner Architecture
|
|
307
322
|
|
|
308
|
-
- `src/scanner.ts` — Rules engine. Defines ~
|
|
323
|
+
- `src/scanner.ts` — Rules engine. Defines ~81 regex rules across 8 threat categories, a correlation engine for
|
|
309
324
|
multi-signal detection, and optional deep taint analysis integration. Supports loading external rules from JSON
|
|
310
325
|
files via `--rules`.
|
|
311
|
-
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs
|
|
312
|
-
escalation logic and user confirmation prompts.
|
|
326
|
+
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs VT and skills.sh lookups in parallel,
|
|
327
|
+
handles escalation logic and user confirmation prompts.
|
|
313
328
|
- `src/vt.ts` — VirusTotal API client. SHA-256 hashing, `GET /api/v3/files/{hash}` lookup, verdict mapping, graceful
|
|
314
329
|
error handling.
|
|
330
|
+
- `src/skills-sh.ts` — skills.sh audit client. Fetches and HTML-parses third-party audit results (Snyk, Socket, Gen
|
|
331
|
+
Agent Trust Hub) for GitHub-sourced skills with a 5-second timeout; always resolves gracefully.
|
|
315
332
|
- `src/deep-scan/` — Deep taint analysis engine (enabled via `--deep-scan`). Regex-based tokenizers extract sources,
|
|
316
333
|
sinks, and assignments from Python/JS/TS files; a forward taint tracker propagates data flow; a cross-file analyzer
|
|
317
334
|
detects multi-file attack patterns via import graph analysis. See [docs/deep-scan.md](docs/deep-scan.md).
|
|
@@ -320,6 +337,20 @@ pnpm format # Format code with Prettier
|
|
|
320
337
|
|
|
321
338
|
## Changelog
|
|
322
339
|
|
|
340
|
+
### 1.1.2
|
|
341
|
+
|
|
342
|
+
- **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
|
|
343
|
+
[skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
|
|
344
|
+
- A skills.sh Fail verdict from any auditor escalates severity to at least High, triggering a confirmation prompt
|
|
345
|
+
- Lookups run in parallel with VirusTotal and fail silently on any network or parse error
|
|
346
|
+
|
|
347
|
+
### 1.1.1
|
|
348
|
+
|
|
349
|
+
- Removed anonymous usage telemetry inherited from the original Vercel `skills` CLI
|
|
350
|
+
- The upstream tool sent events to `https://add-skill.vercel.sh/t` on every command (install, remove, find, check, update) — this has been completely stripped out
|
|
351
|
+
- Removed `DISABLE_TELEMETRY` and `DO_NOT_TRACK` environment variables (no longer needed)
|
|
352
|
+
- Added 12 more regex rules to the scanner
|
|
353
|
+
|
|
323
354
|
### 1.1.0
|
|
324
355
|
|
|
325
356
|
- Added `--rules <path>` flag to load external scan rules from JSON files or directories
|
|
@@ -342,6 +373,14 @@ pnpm format # Format code with Prettier
|
|
|
342
373
|
- URL transparency: all external URLs in skill files are shown before installation
|
|
343
374
|
- Scanner rules informed by Snyk and ClawHavoc research
|
|
344
375
|
|
|
376
|
+
## Research
|
|
377
|
+
|
|
378
|
+
The scanner rules are informed by the following research into malicious agent skills:
|
|
379
|
+
|
|
380
|
+
- **Snyk (2025)** — [Analysis of 3,984 published agent skills](https://snyk.io/blog/), finding 76 confirmed malicious skills (13.4% of clawhub.ai had critical issues). Identified core attack taxonomy: data exfiltration, prompt injection, credential theft, and obfuscated payloads.
|
|
381
|
+
- **Koi Security (2025)** — [ClawHavoc: 341 Malicious ClawedBot Skills](https://www.koi.ai/blog/clawhavoc-341-malicious-clawedbot-skills-found-by-the-bot-they-were-targeting). Documented AMOS stealer droppers, password-protected archives, base64 payloads, macOS quarantine bypasses, and reverse shells in the wild.
|
|
382
|
+
- **arxiv 2602.06547v1 (2025)** — [Malicious Agent Skills at Scale](https://arxiv.org/abs/2602.06547v1). Large-scale analysis identifying attack taxonomies (E1-E3 exfiltration, P1-P4 prompt injection, SC1-SC3 supply chain, PE2-PE3 privilege escalation), MCP server abuse, agent hook interception, permission bypass flags, environment-gated sleeper patterns, invisible Unicode instruction smuggling, and the "industrial actor fingerprint" (credential access + remote execution, 97.6% sensitivity).
|
|
383
|
+
|
|
345
384
|
## Acknowledgments
|
|
346
385
|
|
|
347
386
|
This project is a fork of [skills](https://github.com/vercel-labs/skills) by
|
package/dist/cli.mjs
CHANGED
|
@@ -1357,27 +1357,6 @@ async function listInstalledSkills(options = {}) {
|
|
|
1357
1357
|
} catch {}
|
|
1358
1358
|
return Array.from(skillsMap.values());
|
|
1359
1359
|
}
|
|
1360
|
-
const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
1361
|
-
let cliVersion = null;
|
|
1362
|
-
function isCI() {
|
|
1363
|
-
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
1364
|
-
}
|
|
1365
|
-
function isEnabled() {
|
|
1366
|
-
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
1367
|
-
}
|
|
1368
|
-
function setVersion(version) {
|
|
1369
|
-
cliVersion = version;
|
|
1370
|
-
}
|
|
1371
|
-
function track(data) {
|
|
1372
|
-
if (!isEnabled()) return;
|
|
1373
|
-
try {
|
|
1374
|
-
const params = new URLSearchParams();
|
|
1375
|
-
if (cliVersion) params.set("v", cliVersion);
|
|
1376
|
-
if (isCI()) params.set("ci", "1");
|
|
1377
|
-
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
1378
|
-
fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {});
|
|
1379
|
-
} catch {}
|
|
1380
|
-
}
|
|
1381
1360
|
var ProviderRegistryImpl = class {
|
|
1382
1361
|
providers = [];
|
|
1383
1362
|
register(provider) {
|
|
@@ -2569,6 +2548,12 @@ const SCAN_RULES = [
|
|
|
2569
2548
|
description: "Base64 encoding piped to network command",
|
|
2570
2549
|
pattern: /base64\s.*\|\s*(?:curl|wget|fetch|nc|ncat)/i
|
|
2571
2550
|
},
|
|
2551
|
+
{
|
|
2552
|
+
id: "exfil-fs-enumeration",
|
|
2553
|
+
severity: "high",
|
|
2554
|
+
description: "Programmatic scanning of sensitive directories",
|
|
2555
|
+
pattern: /(?:glob\.(?:glob|iglob)|os\.walk|os\.listdir|os\.scandir|pathlib\.Path\s*\([^)]*\)\.(?:glob|iterdir|rglob))\s*\(\s*['"].*(?:\.ssh|\.aws|\.gnupg|\.config|\.env|credential|secret|\.kube|\.docker)/i
|
|
2556
|
+
},
|
|
2572
2557
|
{
|
|
2573
2558
|
id: "injection-ignore-instructions",
|
|
2574
2559
|
severity: "critical",
|
|
@@ -2599,6 +2584,24 @@ const SCAN_RULES = [
|
|
|
2599
2584
|
description: "Known jailbreak phrase (DAN)",
|
|
2600
2585
|
pattern: /\bDAN\b.*(?:do\s+anything\s+now|jailbreak|ignore\s+(?:all\s+)?(?:safety|rules))/i
|
|
2601
2586
|
},
|
|
2587
|
+
{
|
|
2588
|
+
id: "injection-markdown-comment",
|
|
2589
|
+
severity: "high",
|
|
2590
|
+
description: "Hidden instructions in markdown reference-link comment syntax",
|
|
2591
|
+
pattern: /\[\/\/\]:\s*#\s*[\("']/i
|
|
2592
|
+
},
|
|
2593
|
+
{
|
|
2594
|
+
id: "injection-coercive-language",
|
|
2595
|
+
severity: "medium",
|
|
2596
|
+
description: "Authoritative urgency language used in social engineering",
|
|
2597
|
+
pattern: /\b(?:NON[\s-]?NEGOTIABLE|MANDATORY\s+(?:ACTIVATION|COMPLIANCE|EXECUTION)\s+PROTOCOL|SEVERE\s+VIOLATION|CRITICAL\s+COMPLIANCE\s+REQUIRED|IMMEDIATE\s+(?:ACTION|EXECUTION)\s+REQUIRED)\b/
|
|
2598
|
+
},
|
|
2599
|
+
{
|
|
2600
|
+
id: "injection-invisible-unicode",
|
|
2601
|
+
severity: "high",
|
|
2602
|
+
description: "Clusters of zero-width/invisible Unicode characters (instruction smuggling)",
|
|
2603
|
+
pattern: /[\u200B\u200C\u200D\u2060\u2062\u2063\u2064\uFEFF]{3,}|[\u2066\u2067\u2069]{2,}/
|
|
2604
|
+
},
|
|
2602
2605
|
{
|
|
2603
2606
|
id: "fs-rm-rf-root",
|
|
2604
2607
|
severity: "critical",
|
|
@@ -2635,6 +2638,12 @@ const SCAN_RULES = [
|
|
|
2635
2638
|
description: "macOS quarantine bypass (xattr -c/-d)",
|
|
2636
2639
|
pattern: /xattr\s+(?:-[cdr]+\s+|.*com\.apple\.quarantine)/i
|
|
2637
2640
|
},
|
|
2641
|
+
{
|
|
2642
|
+
id: "fs-privilege-escalation",
|
|
2643
|
+
severity: "high",
|
|
2644
|
+
description: "Privilege escalation via sudo or broad chown",
|
|
2645
|
+
pattern: /(?:sudo\s+(?:(?:-[AEHPSkn]\s+)*(?:bash|sh|chmod|chown|rm|cp|mv|tee|cat|curl|wget|python|node|apt|yum|dnf|brew|pip|npm)\b)|chown\s+-[rR]\s+.*\/)/i
|
|
2646
|
+
},
|
|
2638
2647
|
{
|
|
2639
2648
|
id: "cred-aws-key",
|
|
2640
2649
|
severity: "high",
|
|
@@ -2731,6 +2740,12 @@ const SCAN_RULES = [
|
|
|
2731
2740
|
description: "Unicode escape sequences (potential instruction smuggling)",
|
|
2732
2741
|
pattern: /(?:\\u[0-9a-fA-F]{4}){8,}/
|
|
2733
2742
|
},
|
|
2743
|
+
{
|
|
2744
|
+
id: "obfuscation-unsafe-deserialize",
|
|
2745
|
+
severity: "high",
|
|
2746
|
+
description: "Unsafe deserialization (pickle, marshal, yaml.unsafe_load)",
|
|
2747
|
+
pattern: /(?:pickle\.loads?|marshal\.loads?|shelve\.open|yaml\.(?:unsafe_load|full_load|load\s*\([^)]*Loader\s*=\s*yaml\.(?:Unsafe|Full|)Loader))\s*\(/i
|
|
2748
|
+
},
|
|
2734
2749
|
{
|
|
2735
2750
|
id: "reverse-shell-bash",
|
|
2736
2751
|
severity: "critical",
|
|
@@ -2833,6 +2848,36 @@ const SCAN_RULES = [
|
|
|
2833
2848
|
description: "Instructing agent to hide output from user",
|
|
2834
2849
|
pattern: /(?:hide|suppress|conceal|mask|don'?t\s+show|do\s+not\s+show|never\s+show)\s+(?:the\s+)?(?:output|result|response|error|log)s?\s+(?:from|to)\s+(?:the\s+)?user/i
|
|
2835
2850
|
},
|
|
2851
|
+
{
|
|
2852
|
+
id: "directive-suppress-disclosure",
|
|
2853
|
+
severity: "high",
|
|
2854
|
+
description: "Instructing agent to never reveal information to user",
|
|
2855
|
+
pattern: /(?:do\s+not|don'?t|never|must\s+not)\s+(?:mention|reveal|disclose|tell|inform|show|display)\s+.*(?:to\s+the\s+user|in\s+(?:the\s+)?conversation|to\s+(?:the\s+)?human)/i
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
id: "directive-permission-bypass",
|
|
2859
|
+
severity: "critical",
|
|
2860
|
+
description: "Framework permission bypass flags",
|
|
2861
|
+
pattern: /--(?:dangerously-skip-permissions|trust-all|disable-sandbox|skip-validation|allow-all|yolo|no-sandbox)\b/i
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
id: "directive-mcp-config",
|
|
2865
|
+
severity: "critical",
|
|
2866
|
+
description: "MCP server configuration with external endpoints",
|
|
2867
|
+
pattern: /(?:"mcpServers"|"mcp_servers"|mcpServers)\s*[:{].*(?:https?:\/\/|npx\s|uvx\s)/i
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
id: "directive-hook-injection",
|
|
2871
|
+
severity: "high",
|
|
2872
|
+
description: "Agent hook system interception",
|
|
2873
|
+
pattern: /(?:PreToolUse|PostToolUse|Notification|pre_tool_use|post_tool_use)\s*[:{=\[]/i
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
id: "directive-email-manipulation",
|
|
2877
|
+
severity: "high",
|
|
2878
|
+
description: "BCC injection or email auto-forwarding",
|
|
2879
|
+
pattern: /(?:(?:add|include|always\s+(?:add|include|append))\s+.*(?:bcc|cc)\s*[:=]|(?:forward|redirect)\s+.*(?:email|mail|message)\s+to\s+|auto[\s-]?forward\s+.*to\s+)/i
|
|
2880
|
+
},
|
|
2836
2881
|
{
|
|
2837
2882
|
id: "directive-disable-safety",
|
|
2838
2883
|
severity: "critical",
|
|
@@ -2922,6 +2967,12 @@ const SCAN_RULES = [
|
|
|
2922
2967
|
severity: "high",
|
|
2923
2968
|
description: "Temp file write + execute chain",
|
|
2924
2969
|
pattern: /(?:\/tmp\/|%TEMP%|TMPDIR|\$TMPDIR|tempfile|mktemp).*(?:chmod\s+\+x|\.\/|python|node|bash|sh)\b/
|
|
2970
|
+
},
|
|
2971
|
+
{
|
|
2972
|
+
id: "exec-environment-gated",
|
|
2973
|
+
severity: "high",
|
|
2974
|
+
description: "Environment/hostname/time-gated conditional execution (sleeper pattern)",
|
|
2975
|
+
pattern: /(?:if\s+.*(?:hostname|socket\.gethostname|os\.uname|platform\.node|os\.getenv\s*\(\s*['"](?:ENV|ENVIRONMENT|NODE_ENV|DEPLOY|STAGE)|getpass\.getuser|os\.getuid)\s*.*(?:==|!=|in\s+|not\s+in))|(?:datetime|time)\..*(?:hour|weekday|isoweekday)\s*(?:\(\))?\s*(?:>=?|<=?|==|!=|in\s+|not\s+in)\s*(?:\d|[\[(])/i
|
|
2925
2976
|
}
|
|
2926
2977
|
];
|
|
2927
2978
|
const CORRELATION_RULES = [
|
|
@@ -2990,6 +3041,28 @@ const CORRELATION_RULES = [
|
|
|
2990
3041
|
"obfuscation-base64-block"
|
|
2991
3042
|
] }]
|
|
2992
3043
|
},
|
|
3044
|
+
{
|
|
3045
|
+
id: "corr-credential-remote-exec",
|
|
3046
|
+
severity: "critical",
|
|
3047
|
+
description: "Credential access combined with remote script execution (industrial actor pattern)",
|
|
3048
|
+
conditions: [{ anyOf: [
|
|
3049
|
+
"cred-aws-key",
|
|
3050
|
+
"cred-openai-key",
|
|
3051
|
+
"cred-private-key",
|
|
3052
|
+
"cred-github-token",
|
|
3053
|
+
"cred-slack-token",
|
|
3054
|
+
"cred-stripe-key",
|
|
3055
|
+
"cred-anthropic-key",
|
|
3056
|
+
"cred-agent-config-access",
|
|
3057
|
+
"exfil-env-read"
|
|
3058
|
+
] }, { anyOf: [
|
|
3059
|
+
"download-curl-pipe-sh",
|
|
3060
|
+
"download-pipe-python",
|
|
3061
|
+
"download-exec-binary",
|
|
3062
|
+
"download-curl-subshell",
|
|
3063
|
+
"remote-instruction-load"
|
|
3064
|
+
] }]
|
|
3065
|
+
},
|
|
2993
3066
|
{
|
|
2994
3067
|
id: "corr-injection-exec",
|
|
2995
3068
|
severity: "critical",
|
|
@@ -2999,7 +3072,9 @@ const CORRELATION_RULES = [
|
|
|
2999
3072
|
"injection-new-persona",
|
|
3000
3073
|
"injection-hidden-html",
|
|
3001
3074
|
"injection-system-prompt",
|
|
3002
|
-
"injection-do-anything-now"
|
|
3075
|
+
"injection-do-anything-now",
|
|
3076
|
+
"injection-markdown-comment",
|
|
3077
|
+
"injection-invisible-unicode"
|
|
3003
3078
|
] }, { anyOf: [
|
|
3004
3079
|
"exec-python-os-system",
|
|
3005
3080
|
"exec-python-subprocess",
|
|
@@ -3016,7 +3091,8 @@ const CORRELATION_RULES = [
|
|
|
3016
3091
|
conditions: [{ anyOf: [
|
|
3017
3092
|
"directive-silent-exec",
|
|
3018
3093
|
"directive-hide-output",
|
|
3019
|
-
"directive-no-confirm"
|
|
3094
|
+
"directive-no-confirm",
|
|
3095
|
+
"directive-suppress-disclosure"
|
|
3020
3096
|
] }, { anyOf: SCAN_RULES.filter((r) => /^(?:exec-|download-|fs-)/.test(r.id)).map((r) => r.id) }]
|
|
3021
3097
|
}
|
|
3022
3098
|
];
|
|
@@ -3179,7 +3255,7 @@ function extractRemoteSkillFiles(remoteSkill) {
|
|
|
3179
3255
|
function extractWellKnownSkillFiles(skill) {
|
|
3180
3256
|
return skill.files;
|
|
3181
3257
|
}
|
|
3182
|
-
const NOT_FOUND = {
|
|
3258
|
+
const NOT_FOUND$1 = {
|
|
3183
3259
|
found: false,
|
|
3184
3260
|
verdict: "unknown",
|
|
3185
3261
|
maliciousCount: 0,
|
|
@@ -3190,19 +3266,19 @@ async function lookupFileHash(sha256, apiKey) {
|
|
|
3190
3266
|
try {
|
|
3191
3267
|
response = await fetch(`https://www.virustotal.com/api/v3/files/${sha256}`, { headers: { "x-apikey": apiKey } });
|
|
3192
3268
|
} catch {
|
|
3193
|
-
return NOT_FOUND;
|
|
3269
|
+
return NOT_FOUND$1;
|
|
3194
3270
|
}
|
|
3195
|
-
if (response.status === 404) return NOT_FOUND;
|
|
3196
|
-
if (response.status === 429) return NOT_FOUND;
|
|
3197
|
-
if (!response.ok) return NOT_FOUND;
|
|
3271
|
+
if (response.status === 404) return NOT_FOUND$1;
|
|
3272
|
+
if (response.status === 429) return NOT_FOUND$1;
|
|
3273
|
+
if (!response.ok) return NOT_FOUND$1;
|
|
3198
3274
|
let body;
|
|
3199
3275
|
try {
|
|
3200
3276
|
body = await response.json();
|
|
3201
3277
|
} catch {
|
|
3202
|
-
return NOT_FOUND;
|
|
3278
|
+
return NOT_FOUND$1;
|
|
3203
3279
|
}
|
|
3204
3280
|
const attrs = body.data?.attributes;
|
|
3205
|
-
if (!attrs) return NOT_FOUND;
|
|
3281
|
+
if (!attrs) return NOT_FOUND$1;
|
|
3206
3282
|
const stats = attrs.last_analysis_stats;
|
|
3207
3283
|
const maliciousCount = stats?.malicious ?? 0;
|
|
3208
3284
|
const totalEngines = stats ? Object.values(stats).reduce((sum, n) => sum + n, 0) : 0;
|
|
@@ -3228,6 +3304,64 @@ async function lookupFileHash(sha256, apiKey) {
|
|
|
3228
3304
|
async function checkSkillOnVT(skillContent, apiKey) {
|
|
3229
3305
|
return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
|
|
3230
3306
|
}
|
|
3307
|
+
const AUDITORS = [
|
|
3308
|
+
{
|
|
3309
|
+
id: "snyk",
|
|
3310
|
+
displayName: "Snyk"
|
|
3311
|
+
},
|
|
3312
|
+
{
|
|
3313
|
+
id: "socket",
|
|
3314
|
+
displayName: "Socket"
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
id: "agent-trust-hub",
|
|
3318
|
+
displayName: "Gen Agent Trust Hub"
|
|
3319
|
+
}
|
|
3320
|
+
];
|
|
3321
|
+
const NOT_FOUND = {
|
|
3322
|
+
found: false,
|
|
3323
|
+
permalink: "",
|
|
3324
|
+
audits: [],
|
|
3325
|
+
anyFail: false
|
|
3326
|
+
};
|
|
3327
|
+
function parseAudits(html, baseUrl) {
|
|
3328
|
+
return AUDITORS.map(({ id, displayName }) => {
|
|
3329
|
+
const pattern = new RegExp(`\\/security\\/${id}[^]{0,400}?\\b(Pass|Fail)\\b`, "is");
|
|
3330
|
+
const match = html.match(pattern);
|
|
3331
|
+
let status = "unknown";
|
|
3332
|
+
if (match) status = match[1].toLowerCase() === "pass" ? "pass" : "fail";
|
|
3333
|
+
return {
|
|
3334
|
+
auditor: id,
|
|
3335
|
+
displayName,
|
|
3336
|
+
status,
|
|
3337
|
+
permalink: `${baseUrl}/security/${id}`
|
|
3338
|
+
};
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
async function checkSkillOnSkillsSh(source) {
|
|
3342
|
+
const { owner, repo, skillFolder } = source;
|
|
3343
|
+
const permalink = `https://skills.sh/${owner}/${repo}/${skillFolder}`;
|
|
3344
|
+
try {
|
|
3345
|
+
const controller = new AbortController();
|
|
3346
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3347
|
+
let response;
|
|
3348
|
+
try {
|
|
3349
|
+
response = await fetch(permalink, { signal: controller.signal });
|
|
3350
|
+
} finally {
|
|
3351
|
+
clearTimeout(timeout);
|
|
3352
|
+
}
|
|
3353
|
+
if (!response.ok) return NOT_FOUND;
|
|
3354
|
+
const audits = parseAudits(await response.text(), permalink);
|
|
3355
|
+
return {
|
|
3356
|
+
found: true,
|
|
3357
|
+
permalink,
|
|
3358
|
+
audits,
|
|
3359
|
+
anyFail: audits.some((a) => a.status === "fail")
|
|
3360
|
+
};
|
|
3361
|
+
} catch {
|
|
3362
|
+
return NOT_FOUND;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3231
3365
|
const SEVERITY_LABELS = {
|
|
3232
3366
|
critical: import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" CRITICAL "))),
|
|
3233
3367
|
high: import_picocolors.default.red(import_picocolors.default.bold("HIGH")),
|
|
@@ -3256,6 +3390,17 @@ function displayVTVerdict(verdict) {
|
|
|
3256
3390
|
}
|
|
3257
3391
|
if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
|
|
3258
3392
|
}
|
|
3393
|
+
function displaySkillsShResult(result) {
|
|
3394
|
+
if (!result.found || result.audits.length === 0) return;
|
|
3395
|
+
const badges = result.audits.map((a) => {
|
|
3396
|
+
const name = a.auditor === "agent-trust-hub" ? "Trust Hub" : a.displayName;
|
|
3397
|
+
if (a.status === "pass") return `[${import_picocolors.default.green(`${name} ✓`)}]`;
|
|
3398
|
+
if (a.status === "fail") return `[${import_picocolors.default.red(`${name} ✗`)}]`;
|
|
3399
|
+
return `[${import_picocolors.default.dim(`${name} ~`)}]`;
|
|
3400
|
+
}).join(" ");
|
|
3401
|
+
M.message(` ${import_picocolors.default.cyan("◆")} skills.sh: ${result.audits.length} audits ${badges}`);
|
|
3402
|
+
M.message(import_picocolors.default.dim(` ${result.permalink}`));
|
|
3403
|
+
}
|
|
3259
3404
|
async function presentScanResults(results, options) {
|
|
3260
3405
|
const allFindings = results.flatMap((r) => r.findings.map((f) => ({
|
|
3261
3406
|
...f,
|
|
@@ -3263,15 +3408,26 @@ async function presentScanResults(results, options) {
|
|
|
3263
3408
|
})));
|
|
3264
3409
|
const allUrls = [...new Set(results.flatMap((r) => r.urls))];
|
|
3265
3410
|
const vtVerdicts = /* @__PURE__ */ new Map();
|
|
3411
|
+
const skillsShResults = /* @__PURE__ */ new Map();
|
|
3266
3412
|
let vtEscalate = false;
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3413
|
+
let skillsShEscalate = false;
|
|
3414
|
+
await Promise.all([(async () => {
|
|
3415
|
+
if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
|
|
3416
|
+
const verdict = await checkSkillOnVT(content, options.vtKey);
|
|
3417
|
+
vtVerdicts.set(skillName, verdict);
|
|
3418
|
+
if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
|
|
3419
|
+
} catch {}
|
|
3420
|
+
})(), (async () => {
|
|
3421
|
+
if (options.skillsShSources) await Promise.all([...options.skillsShSources.entries()].map(async ([skillName, source]) => {
|
|
3422
|
+
const result = await checkSkillOnSkillsSh(source);
|
|
3423
|
+
skillsShResults.set(skillName, result);
|
|
3424
|
+
if (result.anyFail) skillsShEscalate = true;
|
|
3425
|
+
}));
|
|
3426
|
+
})()]);
|
|
3427
|
+
if (allFindings.length === 0 && !vtEscalate && !skillsShEscalate) {
|
|
3273
3428
|
M.success(import_picocolors.default.green("Security scan passed — no issues found"));
|
|
3274
3429
|
if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3430
|
+
for (const [, result] of skillsShResults) displaySkillsShResult(result);
|
|
3275
3431
|
if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
|
|
3276
3432
|
return true;
|
|
3277
3433
|
}
|
|
@@ -3295,6 +3451,10 @@ async function presentScanResults(results, options) {
|
|
|
3295
3451
|
console.log();
|
|
3296
3452
|
for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3297
3453
|
}
|
|
3454
|
+
if (skillsShResults.size > 0) {
|
|
3455
|
+
console.log();
|
|
3456
|
+
for (const [, result] of skillsShResults) displaySkillsShResult(result);
|
|
3457
|
+
}
|
|
3298
3458
|
if (allUrls.length > 0) {
|
|
3299
3459
|
console.log();
|
|
3300
3460
|
M.info(`External URLs found in skill files (${allUrls.length}):`);
|
|
@@ -3302,6 +3462,7 @@ async function presentScanResults(results, options) {
|
|
|
3302
3462
|
}
|
|
3303
3463
|
console.log();
|
|
3304
3464
|
if (vtEscalate) overallMax = "critical";
|
|
3465
|
+
if (skillsShEscalate && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
|
|
3305
3466
|
if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
|
|
3306
3467
|
M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
|
|
3307
3468
|
return true;
|
|
@@ -3335,16 +3496,7 @@ async function displayUrlsAndPrompt(urls, options) {
|
|
|
3335
3496
|
if (pD(confirmed) || !confirmed) return false;
|
|
3336
3497
|
return true;
|
|
3337
3498
|
}
|
|
3338
|
-
var version$1 = "1.1.0";
|
|
3339
3499
|
const isCancelled = (value) => typeof value === "symbol";
|
|
3340
|
-
async function isSourcePrivate(source) {
|
|
3341
|
-
const ownerRepo = parseOwnerRepo(source);
|
|
3342
|
-
if (!ownerRepo) return false;
|
|
3343
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
|
|
3344
|
-
}
|
|
3345
|
-
function initTelemetry(version) {
|
|
3346
|
-
setVersion(version);
|
|
3347
|
-
}
|
|
3348
3500
|
const _externalRulesCache = /* @__PURE__ */ new Map();
|
|
3349
3501
|
function resolveExternalRules(options) {
|
|
3350
3502
|
if (!options.rules) return void 0;
|
|
@@ -3355,6 +3507,24 @@ function resolveExternalRules(options) {
|
|
|
3355
3507
|
_externalRulesCache.set(key, loaded);
|
|
3356
3508
|
return loaded;
|
|
3357
3509
|
}
|
|
3510
|
+
function buildSkillsShSourcesForRemote(remoteSkill, _url) {
|
|
3511
|
+
const sourceUrl = remoteSkill.sourceUrl;
|
|
3512
|
+
if (!sourceUrl || !sourceUrl.includes("github.com")) return void 0;
|
|
3513
|
+
try {
|
|
3514
|
+
const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
|
|
3515
|
+
const ownerRepo = parseOwnerRepo(parts.slice(0, 2).join("/"));
|
|
3516
|
+
if (!ownerRepo) return void 0;
|
|
3517
|
+
const skillFolder = parts.length > 2 ? parts.at(-1) : remoteSkill.installName;
|
|
3518
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3519
|
+
sources.set(remoteSkill.installName, {
|
|
3520
|
+
...ownerRepo,
|
|
3521
|
+
skillFolder
|
|
3522
|
+
});
|
|
3523
|
+
return sources;
|
|
3524
|
+
} catch {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3358
3528
|
function shortenPath$1(fullPath, cwd) {
|
|
3359
3529
|
const home = homedir();
|
|
3360
3530
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
@@ -3468,7 +3638,6 @@ async function selectAgentsInteractive(options) {
|
|
|
3468
3638
|
} catch {}
|
|
3469
3639
|
return selected;
|
|
3470
3640
|
}
|
|
3471
|
-
setVersion(version$1);
|
|
3472
3641
|
async function handleRemoteSkill(source, url, options, spinner) {
|
|
3473
3642
|
const provider = findProvider(url);
|
|
3474
3643
|
if (!provider) {
|
|
@@ -3615,10 +3784,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
|
|
|
3615
3784
|
});
|
|
3616
3785
|
const vtKey = options.vtKey || process.env.VT_API_KEY;
|
|
3617
3786
|
const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
|
|
3787
|
+
const skillsShSources = buildSkillsShSourcesForRemote(remoteSkill, url);
|
|
3618
3788
|
if (!await presentScanResults([scanResult], {
|
|
3619
3789
|
yes: options.yes,
|
|
3620
3790
|
vtKey,
|
|
3621
|
-
skillContents
|
|
3791
|
+
skillContents,
|
|
3792
|
+
skillsShSources
|
|
3622
3793
|
})) {
|
|
3623
3794
|
xe("Installation cancelled due to security concerns");
|
|
3624
3795
|
process.exit(0);
|
|
@@ -3648,15 +3819,6 @@ async function handleRemoteSkill(source, url, options, spinner) {
|
|
|
3648
3819
|
console.log();
|
|
3649
3820
|
const successful = results.filter((r) => r.success);
|
|
3650
3821
|
const failed = results.filter((r) => !r.success);
|
|
3651
|
-
if (await isSourcePrivate(remoteSkill.sourceIdentifier) !== true) track({
|
|
3652
|
-
event: "install",
|
|
3653
|
-
source: remoteSkill.sourceIdentifier,
|
|
3654
|
-
skills: remoteSkill.installName,
|
|
3655
|
-
agents: targetAgents.join(","),
|
|
3656
|
-
...installGlobally && { global: "1" },
|
|
3657
|
-
skillFiles: JSON.stringify({ [remoteSkill.installName]: url }),
|
|
3658
|
-
sourceType: remoteSkill.providerId
|
|
3659
|
-
});
|
|
3660
3822
|
if (successful.length > 0 && installGlobally) try {
|
|
3661
3823
|
let skillFolderHash = "";
|
|
3662
3824
|
if (remoteSkill.providerId === "github") {
|
|
@@ -3927,19 +4089,8 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3927
4089
|
console.log();
|
|
3928
4090
|
const successful = results.filter((r) => r.success);
|
|
3929
4091
|
const failed = results.filter((r) => !r.success);
|
|
3930
|
-
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
3931
|
-
const skillFiles = {};
|
|
3932
|
-
for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
|
|
3933
|
-
if (await isSourcePrivate(sourceIdentifier) !== true) track({
|
|
3934
|
-
event: "install",
|
|
3935
|
-
source: sourceIdentifier,
|
|
3936
|
-
skills: selectedSkills.map((s) => s.installName).join(","),
|
|
3937
|
-
agents: targetAgents.join(","),
|
|
3938
|
-
...installGlobally && { global: "1" },
|
|
3939
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
3940
|
-
sourceType: "well-known"
|
|
3941
|
-
});
|
|
3942
4092
|
if (successful.length > 0 && installGlobally) {
|
|
4093
|
+
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
3943
4094
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3944
4095
|
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3945
4096
|
await addSkillToLock(skill.installName, {
|
|
@@ -4150,15 +4301,6 @@ async function handleDirectUrlSkillLegacy(source, url, options, spinner) {
|
|
|
4150
4301
|
console.log();
|
|
4151
4302
|
const successful = results.filter((r) => r.success);
|
|
4152
4303
|
const failed = results.filter((r) => !r.success);
|
|
4153
|
-
track({
|
|
4154
|
-
event: "install",
|
|
4155
|
-
source: "mintlify/com",
|
|
4156
|
-
skills: remoteSkill.installName,
|
|
4157
|
-
agents: targetAgents.join(","),
|
|
4158
|
-
...installGlobally && { global: "1" },
|
|
4159
|
-
skillFiles: JSON.stringify({ [remoteSkill.installName]: url }),
|
|
4160
|
-
sourceType: "mintlify"
|
|
4161
|
-
});
|
|
4162
4304
|
if (successful.length > 0 && installGlobally) try {
|
|
4163
4305
|
await addSkillToLock(remoteSkill.installName, {
|
|
4164
4306
|
source: `mintlify/${remoteSkill.installName}`,
|
|
@@ -4456,10 +4598,24 @@ async function runAdd(args, options = {}) {
|
|
|
4456
4598
|
}
|
|
4457
4599
|
}
|
|
4458
4600
|
spinner.stop("Security scan complete");
|
|
4601
|
+
const skillsShSources = /* @__PURE__ */ new Map();
|
|
4602
|
+
if (parsed.type === "github") {
|
|
4603
|
+
const ownerRepoStr = getOwnerRepo(parsed);
|
|
4604
|
+
const ownerRepo = ownerRepoStr ? parseOwnerRepo(ownerRepoStr) : null;
|
|
4605
|
+
if (ownerRepo) for (const skill of selectedSkills) {
|
|
4606
|
+
const displayName = getSkillDisplayName(skill);
|
|
4607
|
+
const skillFolder = skill.path.split("/").at(-1) ?? skill.name;
|
|
4608
|
+
skillsShSources.set(displayName, {
|
|
4609
|
+
...ownerRepo,
|
|
4610
|
+
skillFolder
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4459
4614
|
if (!await presentScanResults(scanResults, {
|
|
4460
4615
|
yes: options.yes,
|
|
4461
4616
|
vtKey,
|
|
4462
|
-
skillContents
|
|
4617
|
+
skillContents,
|
|
4618
|
+
skillsShSources: skillsShSources.size > 0 ? skillsShSources : void 0
|
|
4463
4619
|
})) {
|
|
4464
4620
|
xe("Installation cancelled due to security concerns");
|
|
4465
4621
|
await cleanup(tempDir);
|
|
@@ -4491,6 +4647,7 @@ async function runAdd(args, options = {}) {
|
|
|
4491
4647
|
console.log();
|
|
4492
4648
|
const successful = results.filter((r) => r.success);
|
|
4493
4649
|
const failed = results.filter((r) => !r.success);
|
|
4650
|
+
const normalizedSource = getOwnerRepo(parsed);
|
|
4494
4651
|
const skillFiles = {};
|
|
4495
4652
|
for (const skill of selectedSkills) {
|
|
4496
4653
|
let relativePath;
|
|
@@ -4499,27 +4656,6 @@ async function runAdd(args, options = {}) {
|
|
|
4499
4656
|
else continue;
|
|
4500
4657
|
skillFiles[skill.name] = relativePath;
|
|
4501
4658
|
}
|
|
4502
|
-
const normalizedSource = getOwnerRepo(parsed);
|
|
4503
|
-
if (normalizedSource) {
|
|
4504
|
-
const ownerRepo = parseOwnerRepo(normalizedSource);
|
|
4505
|
-
if (ownerRepo) {
|
|
4506
|
-
if (await isRepoPrivate(ownerRepo.owner, ownerRepo.repo) === false) track({
|
|
4507
|
-
event: "install",
|
|
4508
|
-
source: normalizedSource,
|
|
4509
|
-
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4510
|
-
agents: targetAgents.join(","),
|
|
4511
|
-
...installGlobally && { global: "1" },
|
|
4512
|
-
skillFiles: JSON.stringify(skillFiles)
|
|
4513
|
-
});
|
|
4514
|
-
} else track({
|
|
4515
|
-
event: "install",
|
|
4516
|
-
source: normalizedSource,
|
|
4517
|
-
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4518
|
-
agents: targetAgents.join(","),
|
|
4519
|
-
...installGlobally && { global: "1" },
|
|
4520
|
-
skillFiles: JSON.stringify(skillFiles)
|
|
4521
|
-
});
|
|
4522
|
-
}
|
|
4523
4659
|
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
4524
4660
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
4525
4661
|
for (const skill of selectedSkills) {
|
|
@@ -4848,11 +4984,6 @@ ${DIM$2} 1) npx skills find [query]${RESET$2}
|
|
|
4848
4984
|
${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
|
|
4849
4985
|
if (query) {
|
|
4850
4986
|
const results = await searchSkillsAPI(query);
|
|
4851
|
-
track({
|
|
4852
|
-
event: "find",
|
|
4853
|
-
query,
|
|
4854
|
-
resultCount: String(results.length)
|
|
4855
|
-
});
|
|
4856
4987
|
if (results.length === 0) {
|
|
4857
4988
|
console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
|
|
4858
4989
|
return;
|
|
@@ -4872,12 +5003,6 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
|
|
|
4872
5003
|
console.log();
|
|
4873
5004
|
}
|
|
4874
5005
|
const selected = await runSearchPrompt();
|
|
4875
|
-
track({
|
|
4876
|
-
event: "find",
|
|
4877
|
-
query: "",
|
|
4878
|
-
resultCount: selected ? "1" : "0",
|
|
4879
|
-
interactive: "1"
|
|
4880
|
-
});
|
|
4881
5006
|
if (!selected) {
|
|
4882
5007
|
console.log(`${DIM$2}Search cancelled${RESET$2}`);
|
|
4883
5008
|
console.log();
|
|
@@ -5091,24 +5216,6 @@ async function removeCommand(skillNames, options) {
|
|
|
5091
5216
|
spinner.stop("Removal process complete");
|
|
5092
5217
|
const successful = results.filter((r) => r.success);
|
|
5093
5218
|
const failed = results.filter((r) => !r.success);
|
|
5094
|
-
if (successful.length > 0) {
|
|
5095
|
-
const bySource = /* @__PURE__ */ new Map();
|
|
5096
|
-
for (const r of successful) {
|
|
5097
|
-
const source = r.source || "local";
|
|
5098
|
-
const existing = bySource.get(source) || { skills: [] };
|
|
5099
|
-
existing.skills.push(r.skill);
|
|
5100
|
-
existing.sourceType = r.sourceType;
|
|
5101
|
-
bySource.set(source, existing);
|
|
5102
|
-
}
|
|
5103
|
-
for (const [source, data] of bySource) track({
|
|
5104
|
-
event: "remove",
|
|
5105
|
-
source,
|
|
5106
|
-
skills: data.skills.join(","),
|
|
5107
|
-
agents: targetAgents.join(","),
|
|
5108
|
-
...isGlobal && { global: "1" },
|
|
5109
|
-
sourceType: data.sourceType
|
|
5110
|
-
});
|
|
5111
|
-
}
|
|
5112
5219
|
if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`));
|
|
5113
5220
|
if (failed.length > 0) {
|
|
5114
5221
|
M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`));
|
|
@@ -5152,7 +5259,6 @@ function getVersion() {
|
|
|
5152
5259
|
}
|
|
5153
5260
|
}
|
|
5154
5261
|
const VERSION = getVersion();
|
|
5155
|
-
initTelemetry(VERSION);
|
|
5156
5262
|
const RESET = "\x1B[0m";
|
|
5157
5263
|
const BOLD = "\x1B[1m";
|
|
5158
5264
|
const DIM = "\x1B[38;5;102m";
|
|
@@ -5436,11 +5542,6 @@ async function runCheck(args = []) {
|
|
|
5436
5542
|
console.log();
|
|
5437
5543
|
console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
|
|
5438
5544
|
}
|
|
5439
|
-
track({
|
|
5440
|
-
event: "check",
|
|
5441
|
-
skillCount: String(totalSkills),
|
|
5442
|
-
updatesAvailable: String(updates.length)
|
|
5443
|
-
});
|
|
5444
5545
|
console.log();
|
|
5445
5546
|
}
|
|
5446
5547
|
async function runUpdate() {
|
|
@@ -5516,12 +5617,6 @@ async function runUpdate() {
|
|
|
5516
5617
|
console.log();
|
|
5517
5618
|
if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
5518
5619
|
if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
5519
|
-
track({
|
|
5520
|
-
event: "update",
|
|
5521
|
-
skillCount: String(updates.length),
|
|
5522
|
-
successCount: String(successCount),
|
|
5523
|
-
failCount: String(failCount)
|
|
5524
|
-
});
|
|
5525
5620
|
console.log();
|
|
5526
5621
|
}
|
|
5527
5622
|
async function main() {
|