@vruum/skills 0.2.0 → 0.4.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.
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # vruum-skills-update-check — periodic version check for the @vruum/skills
3
+ # npm package. Ported from gstack's gstack-update-check, simplified.
4
+ #
5
+ # Output (one line, or nothing):
6
+ # JUST_UPGRADED <old> <new> — marker found from a recent upgrade
7
+ # UPGRADE_AVAILABLE <old> <new> — npm registry reports a newer version
8
+ # (nothing) — up to date, snoozed, or check skipped
9
+ #
10
+ # Env overrides (for testing):
11
+ # VRUUM_STATE_DIR — override ~/.vruum state directory
12
+ # VRUUM_REMOTE_URL — override npm registry URL
13
+ set -euo pipefail
14
+
15
+ STATE_DIR="${VRUUM_STATE_DIR:-$HOME/.vruum}"
16
+ VERSION_FILE="$STATE_DIR/VERSION"
17
+ CACHE_FILE="$STATE_DIR/last-update-check"
18
+ MARKER_FILE="$STATE_DIR/just-upgraded-from"
19
+ SNOOZE_FILE="$STATE_DIR/update-snoozed"
20
+ CONFIG_FILE="$STATE_DIR/config.yaml"
21
+ REMOTE_URL="${VRUUM_REMOTE_URL:-https://registry.npmjs.org/@vruum/skills/latest}"
22
+
23
+ # Force flag busts cache + snooze (used by /vruum-skills-upgrade directly)
24
+ if [ "${1:-}" = "--force" ]; then
25
+ rm -f "$CACHE_FILE" "$SNOOZE_FILE"
26
+ fi
27
+
28
+ # Step 0: if config says updates disabled, bail.
29
+ if [ -f "$CONFIG_FILE" ] && grep -qE '^[[:space:]]*update_check[[:space:]]*:[[:space:]]*false' "$CONFIG_FILE"; then
30
+ exit 0
31
+ fi
32
+
33
+ # Step 1: local version (written by install.js at install time)
34
+ [ -f "$VERSION_FILE" ] || exit 0
35
+ LOCAL="$(tr -d '[:space:]' < "$VERSION_FILE")"
36
+ [ -n "$LOCAL" ] || exit 0
37
+
38
+ # Step 2: just-upgraded marker — emit once, clear snooze, fall through to check
39
+ # remote in case another release landed since the upgrade.
40
+ if [ -f "$MARKER_FILE" ]; then
41
+ OLD="$(tr -d '[:space:]' < "$MARKER_FILE" 2>/dev/null || true)"
42
+ rm -f "$MARKER_FILE" "$SNOOZE_FILE"
43
+ [ -n "$OLD" ] && echo "JUST_UPGRADED $OLD $LOCAL"
44
+ fi
45
+
46
+ # Step 3: snooze helper — returns 0 if snoozed (stay quiet), 1 if not.
47
+ # Snooze file: "<version> <level> <epoch>". Levels: 1=24h, 2=48h, 3+=7d.
48
+ check_snooze() {
49
+ local remote_ver="$1"
50
+ [ -f "$SNOOZE_FILE" ] || return 1
51
+ local sv sl se
52
+ sv="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
53
+ sl="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
54
+ se="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
55
+ [ -n "$sv" ] && [ -n "$sl" ] && [ -n "$se" ] || return 1
56
+ case "$sl" in *[!0-9]*) return 1 ;; esac
57
+ case "$se" in *[!0-9]*) return 1 ;; esac
58
+ # New version drop clears the snooze.
59
+ [ "$sv" = "$remote_ver" ] || return 1
60
+ local dur
61
+ case "$sl" in
62
+ 1) dur=86400 ;;
63
+ 2) dur=172800 ;;
64
+ *) dur=604800 ;;
65
+ esac
66
+ local now expires
67
+ now="$(date +%s)"
68
+ expires=$(( se + dur ))
69
+ [ "$now" -lt "$expires" ]
70
+ }
71
+
72
+ # Step 4: cache — UP_TO_DATE 60 min, UPGRADE_AVAILABLE 12 hours.
73
+ if [ -f "$CACHE_FILE" ]; then
74
+ CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
75
+ case "$CACHED" in
76
+ UP_TO_DATE*) TTL=60 ;;
77
+ UPGRADE_AVAILABLE*) TTL=720 ;;
78
+ *) TTL=0 ;;
79
+ esac
80
+ STALE=$(find "$CACHE_FILE" -mmin +"$TTL" 2>/dev/null || true)
81
+ if [ -z "$STALE" ] && [ "$TTL" -gt 0 ]; then
82
+ case "$CACHED" in
83
+ UP_TO_DATE*)
84
+ CV="$(echo "$CACHED" | awk '{print $2}')"
85
+ [ "$CV" = "$LOCAL" ] && exit 0
86
+ ;;
87
+ UPGRADE_AVAILABLE*)
88
+ CO="$(echo "$CACHED" | awk '{print $2}')"
89
+ if [ "$CO" = "$LOCAL" ]; then
90
+ CN="$(echo "$CACHED" | awk '{print $3}')"
91
+ check_snooze "$CN" && exit 0
92
+ echo "$CACHED"
93
+ exit 0
94
+ fi
95
+ ;;
96
+ esac
97
+ fi
98
+ fi
99
+
100
+ # Step 5: slow path — fetch latest from npm registry.
101
+ mkdir -p "$STATE_DIR"
102
+ RESP="$(curl -fsSL --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
103
+ # Extract "version":"X.Y.Z" without jq.
104
+ REMOTE="$(echo "$RESP" | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
105
+ REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
106
+
107
+ # Reject anything that doesn't look like a semver number.
108
+ if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
109
+ echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
110
+ exit 0
111
+ fi
112
+
113
+ if [ "$LOCAL" = "$REMOTE" ]; then
114
+ echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
115
+ exit 0
116
+ fi
117
+
118
+ echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
119
+ check_snooze "$REMOTE" && exit 0
120
+ echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
package/install.js CHANGED
@@ -27,7 +27,54 @@ const os = require('node:os');
27
27
 
28
28
  const VERSION = require('./package.json').version;
29
29
  const PACKAGE_ROOT = __dirname;
30
- const SKILLS_SRC = path.join(PACKAGE_ROOT, 'skills');
30
+ const PKG_SKILLS = path.join(PACKAGE_ROOT, 'skills');
31
+ const PKG_AGENTS = path.join(PACKAGE_ROOT, 'agents');
32
+
33
+ // Installer-owned stable path. Mirrors gstack's ~/.claude/skills/gstack/
34
+ // approach: package contents get copied here so symlinks target a path that
35
+ // never moves, even when npm/npx wipes the original package directory.
36
+ const VRUUM_ROOT = path.join(os.homedir(), '.vruum');
37
+ const VRUUM_SKILLS = path.join(VRUUM_ROOT, 'skills');
38
+ const VRUUM_AGENTS = path.join(VRUUM_ROOT, 'agents');
39
+ const VRUUM_BIN = path.join(VRUUM_ROOT, 'bin');
40
+ const VRUUM_VERSION_FILE = path.join(VRUUM_ROOT, 'VERSION');
41
+ const PKG_BIN = path.join(PACKAGE_ROOT, 'bin');
42
+
43
+ // Prelude prepended to every SKILL.md as it lands in ~/.vruum/skills/. Runs
44
+ // an update check (1h cache), then gives Claude natural-language instructions
45
+ // for how to react. Mirrors gstack's preamble pattern but injected at install
46
+ // time rather than via a template-gen step.
47
+ const SKILL_PRELUDE = `## Preamble (run first)
48
+
49
+ \`\`\`bash
50
+ _UPD=$(~/.vruum/bin/vruum-skills-update-check 2>/dev/null || true)
51
+ [ -n "$_UPD" ] && echo "$_UPD" || true
52
+ \`\`\`
53
+
54
+ If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.vruum/skills/vruum-skills-upgrade/SKILL.md\` and follow the inline upgrade flow. If \`~/.vruum/config.yaml\` has \`auto_upgrade: true\`, run the upgrade silently; otherwise AskUserQuestion with four options — (A) upgrade now, (B) upgrade with changelog preview, (C) snooze, (D) skip this session. If user picks snooze, bump the snooze level in \`~/.vruum/update-snoozed\` (format: \`<new_version> <level> <unix_epoch>\`; level 1 = 24h, 2 = 48h, 3+ = 7d). Then continue with the skill.
55
+
56
+ If output shows \`JUST_UPGRADED <from> <to>\`: tell the user "Running @vruum/skills v{to} (just updated!)" in one line, then continue.
57
+
58
+ Otherwise proceed silently with the skill below.
59
+
60
+ ---
61
+ `;
62
+
63
+ function injectPrelude(skillDir) {
64
+ const skillFile = path.join(skillDir, 'SKILL.md');
65
+ if (!fs.existsSync(skillFile)) return;
66
+ const raw = fs.readFileSync(skillFile, 'utf8');
67
+ // Find end of YAML frontmatter. If missing, inject at top.
68
+ let insertAt = 0;
69
+ if (raw.startsWith('---\n')) {
70
+ const end = raw.indexOf('\n---\n', 4);
71
+ if (end !== -1) insertAt = end + 5;
72
+ }
73
+ const before = raw.slice(0, insertAt);
74
+ const after = raw.slice(insertAt);
75
+ const patched = before + '\n' + SKILL_PRELUDE + '\n' + after.replace(/^\n+/, '');
76
+ fs.writeFileSync(skillFile, patched);
77
+ }
31
78
 
32
79
  // Known harness skill directories. Add a target here when a new harness
33
80
  // lands on a stable skill-dir convention.
@@ -90,13 +137,56 @@ other assistants that don't yet support MCP prompts.`);
90
137
  }
91
138
 
92
139
  function listAvailableSkills() {
93
- if (!fs.existsSync(SKILLS_SRC)) return [];
140
+ if (!fs.existsSync(PKG_SKILLS)) return [];
94
141
  return fs
95
- .readdirSync(SKILLS_SRC, { withFileTypes: true })
142
+ .readdirSync(PKG_SKILLS, { withFileTypes: true })
96
143
  .filter((entry) => entry.isDirectory())
97
144
  .map((entry) => entry.name);
98
145
  }
99
146
 
147
+ function syncVruumRoot({ dryRun }) {
148
+ if (dryRun) {
149
+ const rows = [
150
+ `would sync ${PKG_SKILLS} -> ${VRUUM_SKILLS} (with auto-update prelude)`,
151
+ ];
152
+ if (fs.existsSync(PKG_AGENTS)) rows.push(`would sync ${PKG_AGENTS} -> ${VRUUM_AGENTS}`);
153
+ if (fs.existsSync(PKG_BIN)) rows.push(`would sync ${PKG_BIN} -> ${VRUUM_BIN}`);
154
+ rows.push(`would write ${VRUUM_VERSION_FILE} = ${VERSION}`);
155
+ return rows;
156
+ }
157
+ fs.mkdirSync(VRUUM_ROOT, { recursive: true });
158
+
159
+ // Skills — copy then inject prelude in-place on the ~/.vruum/ copy.
160
+ fs.rmSync(VRUUM_SKILLS, { recursive: true, force: true });
161
+ fs.cpSync(PKG_SKILLS, VRUUM_SKILLS, { recursive: true });
162
+ for (const skillName of listAvailableSkills()) {
163
+ injectPrelude(path.join(VRUUM_SKILLS, skillName));
164
+ }
165
+ const rows = [`synced ${VRUUM_SKILLS} (prelude injected)`];
166
+
167
+ if (fs.existsSync(PKG_AGENTS)) {
168
+ fs.rmSync(VRUUM_AGENTS, { recursive: true, force: true });
169
+ fs.cpSync(PKG_AGENTS, VRUUM_AGENTS, { recursive: true });
170
+ rows.push(`synced ${VRUUM_AGENTS}`);
171
+ }
172
+
173
+ // Bin scripts — update-check needs to land at a stable path and be exec.
174
+ if (fs.existsSync(PKG_BIN)) {
175
+ fs.rmSync(VRUUM_BIN, { recursive: true, force: true });
176
+ fs.cpSync(PKG_BIN, VRUUM_BIN, { recursive: true });
177
+ for (const entry of fs.readdirSync(VRUUM_BIN)) {
178
+ fs.chmodSync(path.join(VRUUM_BIN, entry), 0o755);
179
+ }
180
+ rows.push(`synced ${VRUUM_BIN}`);
181
+ }
182
+
183
+ // VERSION file — read by vruum-skills-update-check as the local version.
184
+ fs.writeFileSync(VRUUM_VERSION_FILE, VERSION + '\n');
185
+ rows.push(`wrote ${VRUUM_VERSION_FILE} = ${VERSION}`);
186
+
187
+ return rows;
188
+ }
189
+
100
190
  function ensureTargetDir(target, dryRun) {
101
191
  if (fs.existsSync(target)) return { created: false };
102
192
  if (dryRun) return { created: 'would' };
@@ -159,7 +249,7 @@ function commandInstall({ targets: extraTargets, dryRun }) {
159
249
  const skills = listAvailableSkills();
160
250
  if (skills.length === 0) {
161
251
  throw new Error(
162
- `No skills found under ${SKILLS_SRC}. This looks like a broken package.`
252
+ `No skills found under ${PKG_SKILLS}. This looks like a broken package.`
163
253
  );
164
254
  }
165
255
  const targets = detectTargets(extraTargets);
@@ -173,6 +263,8 @@ function commandInstall({ targets: extraTargets, dryRun }) {
173
263
  process.exit(1);
174
264
  }
175
265
 
266
+ const syncRows = syncVruumRoot({ dryRun });
267
+
176
268
  const summary = [];
177
269
  for (const target of targets) {
178
270
  const { created } = ensureTargetDir(target.dir, dryRun);
@@ -182,14 +274,18 @@ function commandInstall({ targets: extraTargets, dryRun }) {
182
274
  summary.push({ target: target.dir, detail: 'created target dir' });
183
275
  }
184
276
  for (const skillName of skills) {
185
- const srcAbs = path.join(SKILLS_SRC, skillName);
277
+ const srcAbs = path.join(VRUUM_SKILLS, skillName);
186
278
  summary.push(linkSkill({ name: skillName, srcAbs, target: target.dir, dryRun }));
187
279
  }
188
280
  }
189
281
 
190
282
  const prefix = dryRun ? '[dry-run] ' : '';
191
283
  console.log(`${prefix}@vruum/skills v${VERSION}`);
192
- console.log(`${prefix}skills source: ${SKILLS_SRC}`);
284
+ console.log(`${prefix}package source: ${PACKAGE_ROOT}`);
285
+ console.log(`${prefix}stable root: ${VRUUM_ROOT}`);
286
+ for (const row of syncRows) {
287
+ console.log(` ${prefix}${row}`);
288
+ }
193
289
  for (const target of targets) {
194
290
  console.log(`${prefix}target: ${target.dir}${target.autoDetected ? ' (auto)' : ''}`);
195
291
  }
@@ -213,19 +309,25 @@ function commandInstall({ targets: extraTargets, dryRun }) {
213
309
  function commandUninstall({ targets: extraTargets, dryRun }) {
214
310
  const skills = listAvailableSkills();
215
311
  const targets = detectTargets(extraTargets);
312
+ const prefix = dryRun ? '[dry-run] ' : '';
313
+ console.log(`${prefix}@vruum/skills v${VERSION} uninstall`);
314
+
216
315
  if (targets.length === 0) {
217
- console.error('No AI harness skill directories detected. Nothing to remove.');
218
- return;
316
+ console.error('No AI harness skill directories detected.');
219
317
  }
220
318
 
221
- const prefix = dryRun ? '[dry-run] ' : '';
222
- console.log(`${prefix}@vruum/skills v${VERSION} uninstall`);
319
+ // A symlink is "ours" if it points into ~/.vruum/skills/. We accept any
320
+ // target there (not just the current skill name) so stale links from
321
+ // renamed skills still get cleaned up.
322
+ const isOurLink = (linkTarget) => {
323
+ const resolved = path.resolve(linkTarget);
324
+ return resolved.startsWith(VRUUM_SKILLS + path.sep) || resolved === VRUUM_SKILLS;
325
+ };
223
326
 
224
327
  for (const target of targets) {
225
328
  console.log(`${prefix}target: ${target.dir}`);
226
329
  for (const skillName of skills) {
227
330
  const dst = path.join(target.dir, skillName);
228
- const srcAbs = path.join(SKILLS_SRC, skillName);
229
331
  let existing = null;
230
332
  try {
231
333
  const lstat = fs.lstatSync(dst);
@@ -242,7 +344,7 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
242
344
  console.log(` ${prefix}not-installed ${skillName}`);
243
345
  continue;
244
346
  }
245
- if (existing.kind !== 'symlink' || existing.target !== srcAbs) {
347
+ if (existing.kind !== 'symlink' || !isOurLink(existing.target)) {
246
348
  console.log(` ${prefix}skipped ${skillName} [not our symlink]`);
247
349
  continue;
248
350
  }
@@ -254,6 +356,33 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
254
356
  }
255
357
  }
256
358
  }
359
+
360
+ // Only clean up our own subdirectories — ~/.vruum/ is a shared state dir
361
+ // (e.g. the .agents/ vruum-update-check keeps config.yaml + snooze state
362
+ // there, and both installers share that config).
363
+ for (const dir of [VRUUM_SKILLS, VRUUM_AGENTS, VRUUM_BIN]) {
364
+ if (!fs.existsSync(dir)) continue;
365
+ if (dryRun) {
366
+ console.log(`${prefix}would remove ${dir}`);
367
+ } else {
368
+ fs.rmSync(dir, { recursive: true, force: true });
369
+ console.log(`${prefix}removed ${dir}`);
370
+ }
371
+ }
372
+ if (fs.existsSync(VRUUM_VERSION_FILE)) {
373
+ if (dryRun) {
374
+ console.log(`${prefix}would remove ${VRUUM_VERSION_FILE}`);
375
+ } else {
376
+ fs.unlinkSync(VRUUM_VERSION_FILE);
377
+ console.log(`${prefix}removed ${VRUUM_VERSION_FILE}`);
378
+ }
379
+ }
380
+ // Best-effort rmdir the root if now empty; ignore ENOTEMPTY.
381
+ if (!dryRun && fs.existsSync(VRUUM_ROOT)) {
382
+ try { fs.rmdirSync(VRUUM_ROOT); } catch (err) {
383
+ if (err.code !== 'ENOTEMPTY' && err.code !== 'EEXIST') throw err;
384
+ }
385
+ }
257
386
  }
258
387
 
259
388
  function commandList({ targets: extraTargets }) {
@@ -273,7 +402,7 @@ function commandList({ targets: extraTargets }) {
273
402
  }
274
403
  for (const skillName of skills) {
275
404
  const dst = path.join(target.dir, skillName);
276
- const srcAbs = path.join(SKILLS_SRC, skillName);
405
+ const srcAbs = path.join(VRUUM_SKILLS, skillName);
277
406
  try {
278
407
  const lstat = fs.lstatSync(dst);
279
408
  if (lstat.isSymbolicLink()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Vruum AI skills for Claude Code, Codex CLI, and any AI assistant with a skill directory. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,7 +20,8 @@
20
20
  "LICENSE",
21
21
  ".mcp.json",
22
22
  "skills/",
23
- "agents/"
23
+ "agents/",
24
+ "bin/"
24
25
  ],
25
26
  "engines": {
26
27
  "node": ">=18"
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: vruum-skills-upgrade
3
+ description: "Upgrade @vruum/skills to the latest npm version and re-sync ~/.vruum/. Use when: upgrade vruum skills, update vruum, pull latest vruum skills, or when the preamble reports UPGRADE_AVAILABLE."
4
+ ---
5
+
6
+ # /vruum-skills-upgrade
7
+
8
+ Upgrade the `@vruum/skills` npm package + re-sync `~/.vruum/` + relink all harness skill dirs.
9
+
10
+ ## Inline upgrade flow (called from preamble)
11
+
12
+ If the calling skill's preamble reported `UPGRADE_AVAILABLE <old> <new>`, follow this flow. It runs inline — when done, the original skill continues.
13
+
14
+ ### Step 1: decide whether to auto-upgrade
15
+
16
+ Read `~/.vruum/config.yaml`. If it contains `auto_upgrade: true`, skip to Step 3 (silent upgrade). Otherwise go to Step 2.
17
+
18
+ ### Step 2: ask the user
19
+
20
+ Use AskUserQuestion with these four options:
21
+
22
+ - **A) Upgrade now** — run the upgrade, return a one-line confirmation, then continue with the original skill.
23
+ - **B) Show changelog first** — fetch `https://raw.githubusercontent.com/vruum-gtm/skills/main/CHANGELOG.md`, show the section for the new version, then re-ask A/C/D.
24
+ - **C) Snooze** — don't upgrade this session. Bump the snooze level:
25
+ ```bash
26
+ # Snooze format: "<version> <level> <epoch>". Level 1=24h, 2=48h, 3+=7d.
27
+ OLD_LEVEL=$(awk '{print $2}' ~/.vruum/update-snoozed 2>/dev/null || echo 0)
28
+ NEW_LEVEL=$((OLD_LEVEL + 1))
29
+ [ $NEW_LEVEL -gt 3 ] && NEW_LEVEL=3
30
+ echo "<new> $NEW_LEVEL $(date +%s)" > ~/.vruum/update-snoozed
31
+ ```
32
+ Replace `<new>` with the version the preamble reported.
33
+ - **D) Skip this session** — do nothing, don't write snooze. Next session will re-prompt.
34
+
35
+ ### Step 3: run the upgrade
36
+
37
+ Detect how `@vruum/skills` was installed (global vs npx) and pick the right command:
38
+
39
+ ```bash
40
+ # If installed globally (vruum-skills is on PATH):
41
+ if command -v vruum-skills >/dev/null 2>&1; then
42
+ OLD=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
43
+ npm install -g @vruum/skills@latest
44
+ vruum-skills install
45
+ NEW=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
46
+ else
47
+ # npx-only install — refresh the npx cache and re-run.
48
+ OLD=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
49
+ npx --yes @vruum/skills@latest install
50
+ NEW=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
51
+ fi
52
+
53
+ # Write the just-upgraded marker so the next preamble greets with "just updated!".
54
+ echo "$OLD" > ~/.vruum/just-upgraded-from
55
+ rm -f ~/.vruum/last-update-check ~/.vruum/update-snoozed
56
+ ```
57
+
58
+ ### Step 4: report
59
+
60
+ One line: `upgraded @vruum/skills $OLD → $NEW`. Then continue with the original skill.
61
+
62
+ ## Standalone mode (user invoked `/vruum-skills-upgrade` directly)
63
+
64
+ Force a fresh check first, then run the flow above starting from Step 2:
65
+
66
+ ```bash
67
+ ~/.vruum/bin/vruum-skills-update-check --force
68
+ ```
69
+
70
+ If the forced check returns nothing, report `already on latest (v$(cat ~/.vruum/VERSION))` and exit.
71
+
72
+ ## When something goes wrong
73
+
74
+ - `npm install` fails with permissions → tell the user to re-run with `sudo` or fix their npm prefix. Don't auto-sudo.
75
+ - `vruum-skills install` reports skipped skills → surface the conflict message verbatim; user needs to remove conflicting files.
76
+ - Network failure on registry lookup → report `upgrade check failed, try again later` and continue with the original skill (don't block work on a transient fetch error).