@vruum/skills-operator 0.1.1 → 0.1.3
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 +22 -3
- package/install.js +97 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @vruum/skills-operator
|
|
2
2
|
|
|
3
|
-
OAuth-gated installer for the Vruum operator skill bundle. If you're a Vruum operator (an account that owns 2+ companies in Vruum), this gives you the
|
|
3
|
+
OAuth-gated installer for the Vruum operator skill bundle. If you're a Vruum operator (an account that owns 2+ companies in Vruum), this gives you the 12 operator skills for Claude Code / Codex CLI with one command — no private GitHub access required.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @vruum/skills-operator install
|
|
@@ -12,7 +12,7 @@ npx @vruum/skills-operator install
|
|
|
12
12
|
2. Saves the session to `~/.vruum/auth.json` (mode 0600).
|
|
13
13
|
3. Downloads the latest operator skill tarball from `api.vruum.ai` (server verifies your operator role on every request).
|
|
14
14
|
4. Extracts to `~/.vruum/skills-operator/<sha>/`, updates `~/.vruum/skills-operator/current → <sha>/`.
|
|
15
|
-
5. Symlinks each skill into `~/.claude/skills/` and `~/.
|
|
15
|
+
5. Symlinks each skill into `~/.claude/skills/` and `~/.agents/skills/` via `current/` so rollback is atomic.
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
@@ -47,7 +47,26 @@ Set `telemetry: community` (or `anonymous`) in `~/.vruum/config.yaml` to help su
|
|
|
47
47
|
|
|
48
48
|
## Pairs with
|
|
49
49
|
|
|
50
|
-
The Vruum MCP server at `https://api.vruum.ai/mcp`.
|
|
50
|
+
The Vruum MCP server at `https://api.vruum.ai/mcp`. The installed skills run as local workflows in your AI assistant; the MCP server provides the tools (including the `skill` tool, action=invoke) and data those workflows call.
|
|
51
|
+
|
|
52
|
+
**Register the MCP server in your assistant** (one-time, after install):
|
|
53
|
+
|
|
54
|
+
- **Claude Code** — add to `~/.claude.json`:
|
|
55
|
+
```json
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"vruum-local": { "type": "http", "url": "https://api.vruum.ai/mcp" }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Codex CLI** — add to `~/.codex/config.toml`:
|
|
62
|
+
```toml
|
|
63
|
+
[mcp_servers.vruum-local]
|
|
64
|
+
url = "https://api.vruum.ai/mcp"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Other assistants** — connect to `https://api.vruum.ai/mcp` (HTTP transport, OAuth via standard MCP flow).
|
|
68
|
+
|
|
69
|
+
After registration, restart your assistant. Tools like `mcp__vruum-local__get_outreach_review` should appear in your tool list.
|
|
51
70
|
|
|
52
71
|
## Issues
|
|
53
72
|
|
package/install.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* - 300s listener timeout (enterprise SSO + MFA headroom).
|
|
26
26
|
* - Per-SHA subdirs under ~/.vruum/skills-operator/<sha>/.
|
|
27
27
|
* - `current` symlink sits alongside; skills in ~/.claude/skills/ and
|
|
28
|
-
* ~/.
|
|
28
|
+
* ~/.agents/skills/ point THROUGH current/ so rollback is one rename.
|
|
29
29
|
* - Windows without admin/dev-mode falls back to file copy + explicit message.
|
|
30
30
|
* - Lock file `.install.lock` contains PID; stale PID (`kill -0` fails) is
|
|
31
31
|
* reclaimed; live PID refuses.
|
|
@@ -69,8 +69,11 @@ const CONFIG_FILE = path.join(VRUUM_ROOT, 'config.yaml');
|
|
|
69
69
|
const SESSION_ID = crypto.randomUUID();
|
|
70
70
|
|
|
71
71
|
const HARNESSES = [
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// `detect` = the harness config dir (its presence means the harness is
|
|
73
|
+
// installed); `dir` = where that harness reads skills. Codex reads the
|
|
74
|
+
// agentskills.io standard dir ~/.agents/skills (not ~/.codex/skills).
|
|
75
|
+
{ name: 'Claude Code', detect: path.join(os.homedir(), '.claude'), dir: path.join(os.homedir(), '.claude', 'skills') },
|
|
76
|
+
{ name: 'Codex CLI', detect: path.join(os.homedir(), '.codex'), dir: path.join(os.homedir(), '.agents', 'skills') },
|
|
74
77
|
];
|
|
75
78
|
|
|
76
79
|
// ─── Arg parsing ────────────────────────────────────────────────────────────
|
|
@@ -587,6 +590,72 @@ function linkSkill({ name, srcAbs, targetDir, force }) {
|
|
|
587
590
|
}
|
|
588
591
|
}
|
|
589
592
|
|
|
593
|
+
// A harness symlink is "ours" if its target resolves under
|
|
594
|
+
// ~/.vruum/skills-operator/ (skills are linked THROUGH current/). Boundary-safe:
|
|
595
|
+
// equality OR startsWith(OPERATOR_ROOT + path.sep) — never a substring
|
|
596
|
+
// `.includes`, so siblings like ~/.vruum/skills-operator-backup/… or
|
|
597
|
+
// /tmp/my-skills-operator-notes/… are NOT matched, and the public installer's
|
|
598
|
+
// ~/.vruum/skills/ links are excluded too. Resolved textually so a dangling
|
|
599
|
+
// target (the renamed-skill bug case) is still recognized.
|
|
600
|
+
function isOperatorLink(linkTarget) {
|
|
601
|
+
const resolved = path.resolve(linkTarget);
|
|
602
|
+
return resolved === OPERATOR_ROOT || resolved.startsWith(OPERATOR_ROOT + path.sep);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Remove links for skills no longer in the bundle (renamed/removed), but only
|
|
606
|
+
// symlinks this installer owns — never non-symlinks or foreign/public links.
|
|
607
|
+
function pruneStaleLinks({ targetDir, skillNames }) {
|
|
608
|
+
const results = [];
|
|
609
|
+
let entries;
|
|
610
|
+
try {
|
|
611
|
+
entries = fs.readdirSync(targetDir);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (err.code === 'ENOENT') return results;
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
const current = new Set(skillNames);
|
|
617
|
+
for (const entry of entries) {
|
|
618
|
+
if (current.has(entry)) continue; // current skill — keep
|
|
619
|
+
const dst = path.join(targetDir, entry);
|
|
620
|
+
let linkTarget;
|
|
621
|
+
try {
|
|
622
|
+
const lstat = fs.lstatSync(dst);
|
|
623
|
+
if (!lstat.isSymbolicLink()) continue; // never touch non-symlinks
|
|
624
|
+
linkTarget = fs.readlinkSync(dst);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (err.code === 'ENOENT') continue;
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
if (!isOperatorLink(linkTarget)) continue; // foreign symlink — keep
|
|
630
|
+
fs.unlinkSync(dst);
|
|
631
|
+
results.push({ name: entry, action: 'pruned' });
|
|
632
|
+
}
|
|
633
|
+
return results;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Link each current skill into every detected harness, then prune our stale
|
|
637
|
+
// links. Called from doInstall (both the fresh-install path AND the
|
|
638
|
+
// already-up-to-date early return, so upgrading to this fixed installer
|
|
639
|
+
// reconciles on an unchanged SHA) and from doRollback (so rolling back to a
|
|
640
|
+
// build that lacks a newer skill prunes that now-dangling link).
|
|
641
|
+
function reconcileHarnessLinks({ skillsDir, skillNames, force = false }) {
|
|
642
|
+
for (const { name: harnessName, detect, dir: targetDirForHarness } of HARNESSES) {
|
|
643
|
+
if (!fs.existsSync(detect)) continue;
|
|
644
|
+
fs.mkdirSync(targetDirForHarness, { recursive: true });
|
|
645
|
+
const results = [];
|
|
646
|
+
for (const skillName of skillNames) {
|
|
647
|
+
const srcAbs = path.join(skillsDir, skillName);
|
|
648
|
+
results.push(linkSkill({ name: skillName, srcAbs, targetDir: targetDirForHarness, force }));
|
|
649
|
+
}
|
|
650
|
+
const linked = results.filter((r) => ['linked', 'relinked', 'copied', 'already-linked'].includes(r.action)).length;
|
|
651
|
+
const skipped = results.filter((r) => r.action === 'skipped');
|
|
652
|
+
console.log(`${harnessName}: ${linked}/${skillNames.length} skills ready`);
|
|
653
|
+
for (const s of skipped) console.log(` skipped ${s.name} — ${s.reason}`);
|
|
654
|
+
const pruned = pruneStaleLinks({ targetDir: targetDirForHarness, skillNames });
|
|
655
|
+
for (const p of pruned) console.log(` pruned ${p.name} (stale skill link)`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
590
659
|
function copyUpdateCheckShim() {
|
|
591
660
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
592
661
|
const src = path.join(__dirname, 'bin', 'vruum-skills-operator-update-check');
|
|
@@ -627,6 +696,15 @@ async function doInstall({ force = false } = {}) {
|
|
|
627
696
|
const currentSha = fs.existsSync(VERSION_FILE) ? fs.readFileSync(VERSION_FILE, 'utf8').trim() : null;
|
|
628
697
|
if (currentSha === sha) {
|
|
629
698
|
console.log(`already up to date (sha=${sha})`);
|
|
699
|
+
// Still reconcile harness links: upgrading to this fixed installer must
|
|
700
|
+
// prune stale renamed-skill links even when the SHA hasn't moved.
|
|
701
|
+
const skillsDir = path.join(CURRENT_LINK, 'skills');
|
|
702
|
+
if (fs.existsSync(skillsDir)) {
|
|
703
|
+
const skillNames = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
704
|
+
.filter((d) => d.isDirectory())
|
|
705
|
+
.map((d) => d.name);
|
|
706
|
+
reconcileHarnessLinks({ skillsDir, skillNames, force });
|
|
707
|
+
}
|
|
630
708
|
return;
|
|
631
709
|
}
|
|
632
710
|
|
|
@@ -695,18 +773,7 @@ async function doInstall({ force = false } = {}) {
|
|
|
695
773
|
.filter((d) => d.isDirectory())
|
|
696
774
|
.map((d) => d.name);
|
|
697
775
|
|
|
698
|
-
|
|
699
|
-
if (!fs.existsSync(targetDirForHarness)) continue;
|
|
700
|
-
const results = [];
|
|
701
|
-
for (const skillName of skillNames) {
|
|
702
|
-
const srcAbs = path.join(skillsDir, skillName);
|
|
703
|
-
results.push(linkSkill({ name: skillName, srcAbs, targetDir: targetDirForHarness, force }));
|
|
704
|
-
}
|
|
705
|
-
const linked = results.filter((r) => ['linked', 'relinked', 'copied', 'already-linked'].includes(r.action)).length;
|
|
706
|
-
const skipped = results.filter((r) => r.action === 'skipped');
|
|
707
|
-
console.log(`${harnessName}: ${linked}/${skillNames.length} skills ready`);
|
|
708
|
-
for (const s of skipped) console.log(` skipped ${s.name} — ${s.reason}`);
|
|
709
|
-
}
|
|
776
|
+
reconcileHarnessLinks({ skillsDir, skillNames, force });
|
|
710
777
|
|
|
711
778
|
fs.writeFileSync(VERSION_FILE, sha);
|
|
712
779
|
|
|
@@ -780,6 +847,17 @@ function doRollback({ to }) {
|
|
|
780
847
|
fail(`failed to flip current symlink: ${e.message}`, 'rollback', 'SymlinkError');
|
|
781
848
|
}
|
|
782
849
|
fs.writeFileSync(VERSION_FILE, targetSha);
|
|
850
|
+
|
|
851
|
+
// Reconcile harness links to the rolled-back build: a skill that existed only
|
|
852
|
+
// in the build we rolled away from now dangles, so prune it.
|
|
853
|
+
const skillsDir = path.join(CURRENT_LINK, 'skills');
|
|
854
|
+
if (fs.existsSync(skillsDir)) {
|
|
855
|
+
const skillNames = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
856
|
+
.filter((d) => d.isDirectory())
|
|
857
|
+
.map((d) => d.name);
|
|
858
|
+
reconcileHarnessLinks({ skillsDir, skillNames, force: false });
|
|
859
|
+
}
|
|
860
|
+
|
|
783
861
|
console.log(`rolled back to ${targetSha.slice(0, 12)}`);
|
|
784
862
|
}
|
|
785
863
|
|
|
@@ -798,7 +876,7 @@ function doUninstall() {
|
|
|
798
876
|
const lstat = fs.lstatSync(full);
|
|
799
877
|
if (!lstat.isSymbolicLink()) continue;
|
|
800
878
|
const target = fs.readlinkSync(full);
|
|
801
|
-
if (
|
|
879
|
+
if (isOperatorLink(target)) {
|
|
802
880
|
fs.unlinkSync(full);
|
|
803
881
|
console.log(`unlinked ${full}`);
|
|
804
882
|
}
|
|
@@ -844,5 +922,8 @@ module.exports = {
|
|
|
844
922
|
releaseLock,
|
|
845
923
|
linkSkill,
|
|
846
924
|
pruneOldShas,
|
|
925
|
+
isOperatorLink,
|
|
926
|
+
pruneStaleLinks,
|
|
927
|
+
reconcileHarnessLinks,
|
|
847
928
|
// exposed for tests
|
|
848
929
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills-operator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OAuth-gated installer for Vruum operator skills. Pulls the operator skill bundle from api.vruum.ai and symlinks it into your AI assistant's skill directory.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|