@vruum/skills 0.6.43 → 0.6.45
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.45",
|
|
4
4
|
"description": "Vruum AI skills + remote MCP server for B2B GTM teams. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vruum AI",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.45",
|
|
4
4
|
"description": "Vruum AI skills + remote MCP server for B2B GTM teams. Skills for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vruum AI",
|
package/install.js
CHANGED
|
@@ -196,6 +196,45 @@ function syncVruumRoot({ dryRun }) {
|
|
|
196
196
|
return rows;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
function pathApiFor(platform) {
|
|
200
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizePathForComparison(value, platform = process.platform) {
|
|
204
|
+
const pathApi = pathApiFor(platform);
|
|
205
|
+
let normalized = pathApi.resolve(value);
|
|
206
|
+
if (platform === 'win32') {
|
|
207
|
+
// Junction readlink results may use a \\?\ namespace prefix and different
|
|
208
|
+
// casing from os.homedir(). Normalize both forms for identity/ownership.
|
|
209
|
+
normalized = pathApi.toNamespacedPath(normalized).toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
const root = pathApi.parse(normalized).root;
|
|
212
|
+
while (normalized.length > root.length && normalized.endsWith(pathApi.sep)) {
|
|
213
|
+
normalized = normalized.slice(0, -1);
|
|
214
|
+
}
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pathsEqual(left, right, platform = process.platform) {
|
|
219
|
+
return normalizePathForComparison(left, platform)
|
|
220
|
+
=== normalizePathForComparison(right, platform);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function pathIsWithin(candidate, root, platform = process.platform) {
|
|
224
|
+
const pathApi = pathApiFor(platform);
|
|
225
|
+
const normalizedCandidate = normalizePathForComparison(candidate, platform);
|
|
226
|
+
const normalizedRoot = normalizePathForComparison(root, platform);
|
|
227
|
+
return normalizedCandidate === normalizedRoot
|
|
228
|
+
|| normalizedCandidate.startsWith(normalizedRoot + pathApi.sep);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveLinkTarget(dst, linkTarget, platform = process.platform) {
|
|
232
|
+
const pathApi = pathApiFor(platform);
|
|
233
|
+
return pathApi.isAbsolute(linkTarget)
|
|
234
|
+
? pathApi.resolve(linkTarget)
|
|
235
|
+
: pathApi.resolve(pathApi.dirname(dst), linkTarget);
|
|
236
|
+
}
|
|
237
|
+
|
|
199
238
|
// A symlink is "ours" if its target resolves into ~/.vruum/skills/. Resolve
|
|
200
239
|
// textually from the readlink string (relative links against the link's own
|
|
201
240
|
// dir) — never realpathSync, which THROWS on a dangling link, i.e. exactly the
|
|
@@ -204,21 +243,14 @@ function syncVruumRoot({ dryRun }) {
|
|
|
204
243
|
// ~/.vruum/skills-operator, so a bare startsWith would wrongly claim the
|
|
205
244
|
// operator installer's links. Equality covers a link straight at the dir.
|
|
206
245
|
function isOurLink(dst, linkTarget) {
|
|
207
|
-
|
|
208
|
-
? path.resolve(linkTarget)
|
|
209
|
-
: path.resolve(path.dirname(dst), linkTarget);
|
|
210
|
-
return resolved === VRUUM_SKILLS || resolved.startsWith(VRUUM_SKILLS + path.sep);
|
|
246
|
+
return pathIsWithin(resolveLinkTarget(dst, linkTarget), VRUUM_SKILLS);
|
|
211
247
|
}
|
|
212
248
|
|
|
213
249
|
// Operator skills are a strict superset with multi-company scoping. When both
|
|
214
250
|
// installers are present, operator links win for overlapping names regardless
|
|
215
251
|
// of install order. Public-only names still link normally.
|
|
216
252
|
function isOperatorLink(dst, linkTarget) {
|
|
217
|
-
|
|
218
|
-
? path.resolve(linkTarget)
|
|
219
|
-
: path.resolve(path.dirname(dst), linkTarget);
|
|
220
|
-
return resolved === VRUUM_OPERATOR_ROOT
|
|
221
|
-
|| resolved.startsWith(VRUUM_OPERATOR_ROOT + path.sep);
|
|
253
|
+
return pathIsWithin(resolveLinkTarget(dst, linkTarget), VRUUM_OPERATOR_ROOT);
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
function hasSkillFile(dir) {
|
|
@@ -297,14 +329,12 @@ function linkSkill({ name, srcAbs, target, dryRun }) {
|
|
|
297
329
|
if (err.code !== 'ENOENT') throw err;
|
|
298
330
|
}
|
|
299
331
|
|
|
300
|
-
if (existing?.kind === 'symlink' && existing.target === srcAbs) {
|
|
301
|
-
return { name, target, action: 'already-linked' };
|
|
302
|
-
}
|
|
303
332
|
const resolvedExistingTarget = existing?.kind === 'symlink'
|
|
304
|
-
? (
|
|
305
|
-
? path.resolve(existing.target)
|
|
306
|
-
: path.resolve(path.dirname(dst), existing.target))
|
|
333
|
+
? resolveLinkTarget(dst, existing.target)
|
|
307
334
|
: null;
|
|
335
|
+
if (resolvedExistingTarget && pathsEqual(resolvedExistingTarget, srcAbs)) {
|
|
336
|
+
return { name, target, action: 'already-linked' };
|
|
337
|
+
}
|
|
308
338
|
const operatorSkillIsUsable = resolvedExistingTarget
|
|
309
339
|
&& isOperatorLink(dst, existing.target)
|
|
310
340
|
&& hasSkillFile(resolvedExistingTarget);
|
|
@@ -330,7 +360,9 @@ function linkSkill({ name, srcAbs, target, dryRun }) {
|
|
|
330
360
|
if (existing?.kind === 'symlink') {
|
|
331
361
|
fs.unlinkSync(dst);
|
|
332
362
|
}
|
|
333
|
-
|
|
363
|
+
// Windows directory symlinks require Developer Mode or elevation. Directory
|
|
364
|
+
// junctions work for this absolute, same-machine target without either.
|
|
365
|
+
fs.symlinkSync(srcAbs, dst, process.platform === 'win32' ? 'junction' : 'dir');
|
|
334
366
|
return { name, target, action: existing ? 'relinked' : 'linked' };
|
|
335
367
|
}
|
|
336
368
|
|
|
@@ -545,7 +577,7 @@ function commandList({ targets: extraTargets }) {
|
|
|
545
577
|
const lstat = fs.lstatSync(dst);
|
|
546
578
|
if (lstat.isSymbolicLink()) {
|
|
547
579
|
const linkTarget = fs.readlinkSync(dst);
|
|
548
|
-
const match = linkTarget
|
|
580
|
+
const match = pathsEqual(resolveLinkTarget(dst, linkTarget), srcAbs);
|
|
549
581
|
console.log(` ${match ? 'installed' : 'other-link'.padEnd(10)} ${skillName}${match ? '' : ` -> ${linkTarget}`}`);
|
|
550
582
|
} else {
|
|
551
583
|
console.log(` present ${skillName} (not our symlink)`);
|
|
@@ -598,6 +630,8 @@ if (require.main === module) main();
|
|
|
598
630
|
module.exports = {
|
|
599
631
|
parseArgs,
|
|
600
632
|
listAvailableSkills,
|
|
633
|
+
pathsEqual,
|
|
634
|
+
pathIsWithin,
|
|
601
635
|
linkSkill,
|
|
602
636
|
isOurLink,
|
|
603
637
|
isOperatorLink,
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.45",
|
|
4
4
|
"description": "Vruum AI skills for Claude Code, Claude Desktop, Codex CLI, and any AI assistant with a skill directory. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/vruum-gtm/
|
|
9
|
-
"directory": ".agents"
|
|
8
|
+
"url": "git+https://github.com/vruum-gtm/skills.git"
|
|
10
9
|
},
|
|
11
10
|
"homepage": "https://vruum.ai",
|
|
12
11
|
"bugs": {
|
|
@@ -42,5 +41,5 @@
|
|
|
42
41
|
"outreach",
|
|
43
42
|
"gtm"
|
|
44
43
|
],
|
|
45
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "843a119196d95663bc210d5543eb53a5f48a2a2780d425afce7c713577d4b1ac"
|
|
46
45
|
}
|
|
@@ -234,7 +234,9 @@ Apply the requested mode before any persistence:
|
|
|
234
234
|
Per surviving prospect:
|
|
235
235
|
|
|
236
236
|
### a. Save company research (once per company)
|
|
237
|
-
When Phase A produced any newly researched fixed fields, call `research(action="save_company", payload={idempotency_key: <stable run/company save key>, name: <Phase A COMPANY>, website: <Phase A DOMAIN or canonical URL>, company_summary, company_stage, funding_data, growth_metrics, current_priorities: <newline-joined descriptions>, sources_by_field})`. The API field is `name`, not `company_name`; it accepts `website`, not `domain`; and `current_priorities` is one string. Omit reusable fields that were not revalidated so the atomic patch preserves them. Explicit null deliberately clears a field, so do not send null merely because Phase A did not research it. `sources_by_field` keys must equal exactly the supplied non-null research fields. Preserve the identical idempotency key and payload for unknown-commit replay; every bulk item needs its own key.
|
|
237
|
+
When Phase A produced any newly researched fixed fields, call `research(action="save_company", payload={idempotency_key: <stable run/company save key>, name: <Phase A COMPANY>, website: <Phase A DOMAIN or canonical URL>, person_id: <the person's Vruum UUID, when researching an EXISTING person's employer>, company_summary, company_stage, funding_data, growth_metrics, current_priorities: <newline-joined descriptions>, sources_by_field})`. The API field is `name`, not `company_name`; it accepts `website`, not `domain`; and `current_priorities` is one string. Omit reusable fields that were not revalidated so the atomic patch preserves them. Explicit null deliberately clears a field, so do not send null merely because Phase A did not research it. `sources_by_field` keys must equal exactly the supplied non-null research fields. Preserve the identical idempotency key and payload for unknown-commit replay; every bulk item needs its own key.
|
|
238
|
+
|
|
239
|
+
**Person linkage check (VRU-767):** when the research is about a saved person's employer (triage/authoring-time refresh), ALWAYS pass their UUID as `payload.person_id` and read the response's `person_link`. `matched`/`linked`/`repointed` mean the person's future touches will see this research. `mismatch` means no pin was written because the evidence disagreed — the pin sits on a DIFFERENT company (anchored, or a stub whose name carries more identity than the researched one), or an unpinned person's known positions show no role at the researched company. People can hold multiple positions and researching a secondary employer never switches the primary pin. This research will NOT surface on their touches — verify which company they actually work for before authoring from it. `conflict` is a transient race (the pin changed mid-save): replay the identical payload with the same idempotency key once — `person_id` is exempt from the idempotency hash, so adding it to a replay of an earlier save is also the supported repair path. Never assume a bare `success` means the research reached the person.
|
|
238
240
|
|
|
239
241
|
### b. Identity prep (names + company linkage for the atomic save)
|
|
240
242
|
|