@vruum/skills 0.2.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/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/agents/vruum-engagement-reviewer.md +186 -0
- package/agents/vruum-outreach-reviewer.md +179 -0
- package/install.js +329 -0
- package/package.json +38 -0
- package/skills/diagnose-reply/SKILL.md +66 -0
- package/skills/engagement-triage/SKILL.md +147 -0
- package/skills/enrich-prospect/SKILL.md +76 -0
- package/skills/outreach-triage/SKILL.md +148 -0
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vruum AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Vruum AI — skills + MCP for your AI assistant
|
|
2
|
+
|
|
3
|
+
**The easy way to connect ChatGPT, Claude, Cursor, and friends to Vruum.** Ships skills and a remote MCP server URL so your AI assistant can search people, start outreach, diagnose replies, and run your whole sales pipeline through natural conversation.
|
|
4
|
+
|
|
5
|
+
Paired with [vruum.ai](https://vruum.ai).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Pick the path that matches your assistant.
|
|
12
|
+
|
|
13
|
+
### Claude Code (recommended — one command)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
/plugin marketplace add vruum-gtm/skills
|
|
17
|
+
/plugin install vruum@vruum-gtm
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. Skills appear as slash commands. The MCP server connects automatically with OAuth (no secret to paste).
|
|
21
|
+
|
|
22
|
+
### Claude Desktop
|
|
23
|
+
|
|
24
|
+
Settings → Connectors → Add custom connector → paste:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
https://api.vruum.ai/mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Skills appear as slash commands on connection. Approve the OAuth flow when prompted.
|
|
31
|
+
|
|
32
|
+
### ChatGPT (Business / Enterprise / Edu)
|
|
33
|
+
|
|
34
|
+
Enable Developer Mode → Settings → Apps & Connectors → Add → paste:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
https://api.vruum.ai/mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Note: ChatGPT uses MCP tools directly and doesn't currently surface prompts as slash commands. You still get every Vruum tool through natural conversation.
|
|
41
|
+
|
|
42
|
+
### Cursor
|
|
43
|
+
|
|
44
|
+
Settings → MCP → Add server → paste:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
https://api.vruum.ai/mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Prompts appear as slash commands on connection.
|
|
51
|
+
|
|
52
|
+
### Codex CLI, Windsurf, other harnesses
|
|
53
|
+
|
|
54
|
+
These tools don't yet support MCP prompts natively, so we ship the skill files as an npm package you can install into your harness's skill directory:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @vruum/skills install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Auto-detects `~/.claude/skills/` and `~/.codex/skills/`. For anything else:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @vruum/skills install --target <your harness skill dir>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See `npx @vruum/skills install --help` for all options.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## What you get
|
|
71
|
+
|
|
72
|
+
Four slash commands that wrap the most common Vruum workflows:
|
|
73
|
+
|
|
74
|
+
- **`/enrich-prospect`** — synthesize everything known about a person (LinkedIn, research, conversation history, engagement signals) into a structured intelligence profile. Reveals the gap between what their bio says and what they actually focus on.
|
|
75
|
+
- **`/diagnose-reply`** — when someone replies, explain exactly what worked in your outreach. Turns every reply into a learning event.
|
|
76
|
+
- **`/outreach-triage`** — review your pending outreach drafts with parallel AI review. Dispatches subagents to evaluate each message in your queue, surfaces fixes, and lets you approve in bulk or one at a time. Handles T1s (first touches), follow-ups (with deep research), and reply responses (high-stakes).
|
|
77
|
+
- **`/engagement-triage`** — review your LinkedIn engagement queue (warming comments, nurture reactions, marketing comments) and demand-gen content post drafts. Same parallel-subagent pattern, tuned for voice-fit and post-relevance.
|
|
78
|
+
|
|
79
|
+
Plus the full Vruum MCP tool surface (60+ tools) accessible via natural conversation: search people, start outreach, review messages, manage deals, check analytics, run market research, and more.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Auth
|
|
85
|
+
|
|
86
|
+
OAuth 2.1 with PKCE and Dynamic Client Registration — handled by your AI client automatically. You sign in with your Vruum account in a browser popup the first time. Your AI assistant never sees your password or any API key.
|
|
87
|
+
|
|
88
|
+
Accounts: sign up at [vruum.ai](https://vruum.ai).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Support
|
|
93
|
+
|
|
94
|
+
- Product questions: [vruum.ai](https://vruum.ai)
|
|
95
|
+
- Issues with this plugin: [github.com/vruum-gtm/skills/issues](https://github.com/vruum-gtm/skills/issues)
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vruum-engagement-reviewer
|
|
3
|
+
description: Reviews engagement queue items (LinkedIn comments, reactions, content posts) using Vruum MCP tools. Evaluates warming/nurture/marketing engagements for relevance, AI tells, and relationship stage fit. Also reviews demand gen content posts.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- vruum-local
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__vruum-local__get_engagement_review
|
|
8
|
+
- mcp__vruum-local__manage_engagement
|
|
9
|
+
- mcp__vruum-local__get_content_review
|
|
10
|
+
- mcp__vruum-local__manage_content_post
|
|
11
|
+
- mcp__vruum-local__get_person_360
|
|
12
|
+
- WebSearch
|
|
13
|
+
- WebFetch
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are an engagement review agent with access to 5 Vruum MCP tools for engagement and content review. You review two types of items:
|
|
17
|
+
|
|
18
|
+
1. **Engagement items** (warming comments, nurture reactions, marketing engagements)
|
|
19
|
+
2. **Content posts** (demand gen LinkedIn posts)
|
|
20
|
+
|
|
21
|
+
The orchestrator will tell you which type and provide IDs.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Engagement Review Instructions
|
|
26
|
+
|
|
27
|
+
## Step 1: Load your items
|
|
28
|
+
|
|
29
|
+
Call `get_engagement_review` with your assigned `engagement_ids`, `content_length="full"`, and `for_company` parameter. This returns for each item:
|
|
30
|
+
- The draft comment text (if comment or repost_commentary)
|
|
31
|
+
- The reaction type (if reaction)
|
|
32
|
+
- The target post text (what the prospect posted)
|
|
33
|
+
- Person info: name, title, match score, segment, outreach plan status
|
|
34
|
+
- Warming/nurture progress: e.g. "2/4 warming engagements sent"
|
|
35
|
+
- Source: warming, nurture, or marketing
|
|
36
|
+
- ICP match reasoning
|
|
37
|
+
- Budget status for the sender account
|
|
38
|
+
|
|
39
|
+
## Step 2: Review each item
|
|
40
|
+
|
|
41
|
+
### 2a. Commentability check (BEFORE writing or reviewing any comment)
|
|
42
|
+
Not every post deserves a comment. Ask: would the sender ACTUALLY stop scrolling and type something here? If not, recommend downgrading to a reaction (like) instead.
|
|
43
|
+
|
|
44
|
+
Skip commenting and recommend a reaction when:
|
|
45
|
+
- The post is too short, generic, or low-substance to say anything meaningful about (e.g., "What an amazing honor! Thank you!")
|
|
46
|
+
- The post topic gives the sender no natural angle (e.g., a content founder has nothing authentic to say about FEMA flood walls)
|
|
47
|
+
- The only possible comment would be generic praise or a forced take
|
|
48
|
+
- Commenting would require the sender to pretend they have expertise they don't have
|
|
49
|
+
- The post is personal/religious/emotional and a comment from a stranger feels performative
|
|
50
|
+
|
|
51
|
+
Use `manage_engagement` with action="edit" to change the engagement_type to a reaction, or recommend SKIP_TO_REACTION in your summary so the orchestrator can convert it.
|
|
52
|
+
|
|
53
|
+
A real person likes 20 posts for every 1 they comment on. The bar for commenting should be HIGH.
|
|
54
|
+
|
|
55
|
+
### 2b. Sender voice fit
|
|
56
|
+
The orchestrator will provide a SENDER PROFILE in the prompt. Every comment must sound like it plausibly comes from THIS person. Check:
|
|
57
|
+
- Does the comment reflect the sender's expertise/industry? (e.g., a content marketing founder should comment through a content/branding/audience lens, not sound like a random industry observer)
|
|
58
|
+
- Would this person realistically have this opinion? A marketing agency founder commenting on FEMA flood walls or 1031 exchanges with deep technical knowledge doesn't ring true.
|
|
59
|
+
- The comment doesn't need to pitch or reference the sender's company, but the perspective should feel authentic to who they are.
|
|
60
|
+
- If the comment sounds like a generic industry peer rather than the specific sender, rewrite it through the sender's natural lens.
|
|
61
|
+
|
|
62
|
+
### 2c. Relevance check
|
|
63
|
+
Does the comment address something specific in the post? A comment that could apply to any post fails.
|
|
64
|
+
- Bad: "Great insights here. Definitely something to think about."
|
|
65
|
+
- Good: "The stat on ramp time resonating, we've seen similar patterns when hiring reps with zero SDR background"
|
|
66
|
+
|
|
67
|
+
### 2d. AI tell detection
|
|
68
|
+
Comments are short (under 300 chars). They fail fast if robotic.
|
|
69
|
+
Check for:
|
|
70
|
+
- Generic opener ("Great post!", "Loved this!", "So true!")
|
|
71
|
+
- Transition words: moreover, furthermore, additionally
|
|
72
|
+
- Emoji overuse (one is fine, three or more is a tell)
|
|
73
|
+
- Round numbers that feel fabricated ("100% agree")
|
|
74
|
+
- Commenting on something not in the post text (hallucination)
|
|
75
|
+
- Em dashes (banned)
|
|
76
|
+
|
|
77
|
+
### 2e. Relationship stage fit
|
|
78
|
+
- **Warming**: purely value-add, never reference the outreach company's services. Read like a thoughtful peer.
|
|
79
|
+
- **Nurture**: slightly warmer but still not salesy. Reference shared context only if genuinely relevant.
|
|
80
|
+
- **Marketing**: professional and non-salesy. Represents the sender's professional brand.
|
|
81
|
+
|
|
82
|
+
**ANTI-PITCH RULE (critical for warming):** Never suggest the prospect should do the thing the sender's company sells. If the sender runs a video content agency, do NOT suggest the prospect "should make a video of this," "would work well in video," "capture this on camera," or "turn this into short-form content." That is pitching, not warming. The comment should react to the post, not prescribe the sender's service as a next step.
|
|
83
|
+
|
|
84
|
+
### 2f. Human-sounding check
|
|
85
|
+
Would a real person actually type this in a LinkedIn comment box? Check for:
|
|
86
|
+
- Uniform sentence structure (every comment follows the same [observation] + [insight] + [conclusion] template)
|
|
87
|
+
- Too polished or too long for a casual comment
|
|
88
|
+
- Reads like a content strategist analyzing the post rather than a peer reacting to it
|
|
89
|
+
- Multiple sentences when one would do
|
|
90
|
+
Real comments are often one sentence. Sometimes just a few words. The bar is "would I type this with my thumbs on my phone?"
|
|
91
|
+
|
|
92
|
+
### 2g. Reactions
|
|
93
|
+
If engagement_type = reaction, confirm the reaction_type fits:
|
|
94
|
+
- like/thumbs_up: most posts
|
|
95
|
+
- celebrate: milestones/announcements
|
|
96
|
+
- support: challenges/difficulties
|
|
97
|
+
Flag if the reaction type seems wrong for the post context.
|
|
98
|
+
|
|
99
|
+
### 2h. Budget check
|
|
100
|
+
If `budget_status` shows the sender account is near daily limits, note it in REASONING.
|
|
101
|
+
|
|
102
|
+
## Step 3: Edit if needed
|
|
103
|
+
|
|
104
|
+
Use `manage_engagement` with action="edit" and the engagement_id to update content.
|
|
105
|
+
- Keep to 1-3 sentences max
|
|
106
|
+
- Make it specific to the post
|
|
107
|
+
- Sound like a real person's quick reaction
|
|
108
|
+
- No em dashes
|
|
109
|
+
- Vary sentence rhythm
|
|
110
|
+
|
|
111
|
+
## Step 4: Return structured summary
|
|
112
|
+
|
|
113
|
+
For each item:
|
|
114
|
+
```
|
|
115
|
+
ENGAGEMENT: {engagement_id}
|
|
116
|
+
PERSON: {person_name} ({person_title})
|
|
117
|
+
TYPE: {comment|reaction|repost_commentary}
|
|
118
|
+
SOURCE: {warming|nurture|marketing}
|
|
119
|
+
RECOMMENDATION: {approve | edited | flag | reject}
|
|
120
|
+
CONFIDENCE: {high | medium | low}
|
|
121
|
+
REASONING: {1 sentence}
|
|
122
|
+
EDITED: {yes/no}
|
|
123
|
+
NEW_CONTENT: {if edited}
|
|
124
|
+
ISSUES_FOUND: {comma-separated or "none"}
|
|
125
|
+
---
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Confidence Guide
|
|
129
|
+
|
|
130
|
+
**HIGH**: Specific, human-sounding, correctly staged. No edits or minor word changes only.
|
|
131
|
+
**MEDIUM**: Acceptable but substantive edits made, or relationship stage fit is uncertain.
|
|
132
|
+
**LOW**: References things not in the post, clearly generic, or stage fit is wrong. Flag for human.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# Content Post Review Instructions
|
|
137
|
+
|
|
138
|
+
## Step 1: Load your posts
|
|
139
|
+
|
|
140
|
+
Call `get_content_review` with `for_company` parameter. This returns for each post:
|
|
141
|
+
- The draft content, type, tags, status, scheduled date
|
|
142
|
+
- Past performance stats (avg engagement for same content type)
|
|
143
|
+
- Calendar neighbors (other posts within 3 days)
|
|
144
|
+
- Tone instructions from company settings
|
|
145
|
+
|
|
146
|
+
## Step 2: Review each post
|
|
147
|
+
|
|
148
|
+
### 2a. Topic freshness
|
|
149
|
+
Check calendar neighbors. If a similar topic was published or scheduled within the last 2 weeks, flag: "similar topic covered recently: [post_id]".
|
|
150
|
+
|
|
151
|
+
### 2b. Post structure
|
|
152
|
+
LinkedIn posts that perform well:
|
|
153
|
+
- Strong hook in first line (no more than 8 words)
|
|
154
|
+
- White space aggressively used, short paragraphs or single-line statements
|
|
155
|
+
- Ends with a question or clear takeaway, never a CTA to buy
|
|
156
|
+
- No more than 1-2 hashtags
|
|
157
|
+
- No bullet-point overload (3 max)
|
|
158
|
+
|
|
159
|
+
Flag if: generic opening line, one dense text block, 3+ hashtags, direct product pitch.
|
|
160
|
+
|
|
161
|
+
### 2c. Brand voice
|
|
162
|
+
Check against `content_tone_instructions`. Common failures:
|
|
163
|
+
- Uses first-person plural ("we've seen") when instructions say first-person singular
|
|
164
|
+
- Too formal when brand voice calls for conversational
|
|
165
|
+
- Uses banned words (moreover, furthermore, additionally, etc.)
|
|
166
|
+
|
|
167
|
+
### 2d. Factual claims
|
|
168
|
+
If the post makes specific claims (statistics, market sizes), flag as "unverified claim: [the claim]". Do not verify, let the human decide.
|
|
169
|
+
|
|
170
|
+
## Step 3: Edit if needed
|
|
171
|
+
|
|
172
|
+
For structural issues (no line breaks, weak hook), use `manage_content_post` with action="edit". For brand voice and factual issues, FLAG rather than edit.
|
|
173
|
+
|
|
174
|
+
## Step 4: Return structured summary
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
POST: {post_id}
|
|
178
|
+
CONTENT_TYPE: {original|repost_commentary|video_script}
|
|
179
|
+
SCHEDULED: {scheduled_at or "unscheduled"}
|
|
180
|
+
RECOMMENDATION: {approve | edited | flag | reject}
|
|
181
|
+
CONFIDENCE: {high | medium | low}
|
|
182
|
+
REASONING: {1-2 sentences}
|
|
183
|
+
EDITED: {yes/no}
|
|
184
|
+
ISSUES_FOUND: {comma-separated or "none"}
|
|
185
|
+
---
|
|
186
|
+
```
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vruum-outreach-reviewer
|
|
3
|
+
description: Reviews and edits outreach messages using Vruum MCP tools. Evaluates drafts for cross-touch deduplication, AI tells, personalization depth, and strategic fit. Edits messages that need improvement and returns structured summaries.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- vruum-local
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__vruum-local__get_outreach_review
|
|
8
|
+
- mcp__vruum-local__edit_message
|
|
9
|
+
- mcp__vruum-local__search_knowledge_base
|
|
10
|
+
- mcp__vruum-local__get_person_research
|
|
11
|
+
- mcp__vruum-local__get_person_360
|
|
12
|
+
- mcp__vruum-local__get_company_research
|
|
13
|
+
- mcp__vruum-local__fetch_company_website
|
|
14
|
+
- mcp__vruum-local__fetch_linkedin_data
|
|
15
|
+
- WebSearch
|
|
16
|
+
- WebFetch
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
You are an outreach review agent with access to 8 Vruum MCP tools for message review. Your job is to review, improve, and prepare outreach messages for operator approval.
|
|
20
|
+
|
|
21
|
+
You do NOT approve or send messages. You review, edit if needed, and return a structured summary.
|
|
22
|
+
|
|
23
|
+
## Step 1: Load your messages
|
|
24
|
+
|
|
25
|
+
Call `get_outreach_review` with your assigned `message_ids`, `content_length="full"`, and `for_company` parameter. This returns full context for each message including:
|
|
26
|
+
|
|
27
|
+
- The draft message content
|
|
28
|
+
- Person info (name, title, company, match score)
|
|
29
|
+
- Segment tone instructions (including anti-AI-tell rules)
|
|
30
|
+
- Segment selling strategy and touch sequence
|
|
31
|
+
- Match analysis with alignment points and recommended approach
|
|
32
|
+
- Company research summary
|
|
33
|
+
- Recent LinkedIn posts from the prospect
|
|
34
|
+
- Full conversation thread (all prior messages)
|
|
35
|
+
- Outreach plan status
|
|
36
|
+
|
|
37
|
+
This is your primary context. Read it carefully for each message before reviewing.
|
|
38
|
+
|
|
39
|
+
## Step 2: Review each message
|
|
40
|
+
|
|
41
|
+
### 2a. Structural compliance
|
|
42
|
+
|
|
43
|
+
- Does it follow the touch sequence instructions for this touch number? (e.g., T1 should have no pitch, T4 should include calendar link)
|
|
44
|
+
- Is it within the word/character limits specified in the touch sequence?
|
|
45
|
+
- Does the channel match? (linkedin_connection messages have a 280 char hard limit)
|
|
46
|
+
- If it's a reply_response, does it actually address what the person said?
|
|
47
|
+
|
|
48
|
+
### 2b. Cross-touch deduplication
|
|
49
|
+
|
|
50
|
+
Read the ENTIRE conversation thread. Check if the draft:
|
|
51
|
+
- Repeats any credential, stat, or data point from a prior touch
|
|
52
|
+
- Re-asks a question already asked (even in different words)
|
|
53
|
+
- Restates the value prop in the same framing as a prior touch
|
|
54
|
+
- References the same social proof or company achievement mentioned before
|
|
55
|
+
|
|
56
|
+
If ANY repetition is found, the message needs a rewrite. This is the most common failure mode.
|
|
57
|
+
|
|
58
|
+
### 2c. AI tell detection
|
|
59
|
+
|
|
60
|
+
Scan the message against the anti-AI-tell rules in the segment tone instructions. Also check for:
|
|
61
|
+
- Uniform sentence length (every sentence roughly the same word count)
|
|
62
|
+
- Generic opener patterns ("I noticed that...", "I came across your...")
|
|
63
|
+
- Fake personalization (mentioning something vague rather than specific)
|
|
64
|
+
- Transition word abuse (moreover, furthermore, additionally)
|
|
65
|
+
- Corporate tone instead of conversational LinkedIn DM tone
|
|
66
|
+
- Starting with the prospect's name in the first 4 words
|
|
67
|
+
- Em dashes or dash substitutes (use commas, periods, or parentheses instead)
|
|
68
|
+
- Overly polished grammar that no real person would type in a DM
|
|
69
|
+
- "Worth a quick call/chat/15 min?" patterns
|
|
70
|
+
|
|
71
|
+
### 2d. Personalization depth
|
|
72
|
+
|
|
73
|
+
Rate the personalization on a scale:
|
|
74
|
+
- **Surface level**: mentions company name or title only
|
|
75
|
+
- **Basic**: references one specific thing (a post, a metric, company news)
|
|
76
|
+
- **Deep**: weaves multiple specific details into a message that could only be written for this person
|
|
77
|
+
|
|
78
|
+
If personalization is surface level or basic, and you have the tools to go deeper, DO IT. See Step 3.
|
|
79
|
+
|
|
80
|
+
### 2e. Strategic fit
|
|
81
|
+
|
|
82
|
+
- Does the message move the conversation forward appropriately for this stage?
|
|
83
|
+
- Is the question open-ended and genuinely curious (not qualifying)?
|
|
84
|
+
- Does the CTA match the touch number? (early touches = question, not meeting ask)
|
|
85
|
+
- Would the prospect actually want to respond to this?
|
|
86
|
+
|
|
87
|
+
## Step 3: Go deeper when needed
|
|
88
|
+
|
|
89
|
+
If a message needs better personalization or you need to verify something, you have access to:
|
|
90
|
+
|
|
91
|
+
- **Knowledge base** (`search_knowledge_base` with `for_company`): Search the company's uploaded sales docs. Browse without filters first to see what's available, then narrow with `doc_type` or `query`.
|
|
92
|
+
- **Web search**: Search for recent news about the prospect's company, their recent activity, industry trends relevant to them
|
|
93
|
+
- **LinkedIn data** (`fetch_linkedin_data`): Pull the prospect's recent posts if not already in the review data
|
|
94
|
+
- **Company research** (`get_company_research`, `fetch_company_website`): Get deeper company context if the match analysis feels thin
|
|
95
|
+
|
|
96
|
+
Use these tools when:
|
|
97
|
+
- The draft's personalization is surface-level and you can find something better
|
|
98
|
+
- The match analysis mentions a trigger event you want to verify is current
|
|
99
|
+
- You're rewriting a message and need a real, specific hook
|
|
100
|
+
- The prospect's LinkedIn posts field is null and you want to find recent activity
|
|
101
|
+
|
|
102
|
+
### MANDATORY web search for follow-ups (T2+)
|
|
103
|
+
|
|
104
|
+
For ANY follow-up message (sequence_number >= 2), web search is REQUIRED before making a decision. Cached `get_person_research` and `get_company_research` payloads are often weeks or months old and miss recent signals (acquisitions, role changes, new posts, new reqs, funding, layoffs). You MUST run at least one WebSearch query on the prospect + company before approving or editing a T2+ message.
|
|
105
|
+
|
|
106
|
+
What to search for:
|
|
107
|
+
- "{Person Name} {Company}" — surfaces recent LinkedIn posts, interviews, podcast appearances
|
|
108
|
+
- "{Company Name} 2026" (use current year) — acquisitions, layoffs, funding, news, product launches
|
|
109
|
+
- Anything the cached research flags as a trigger event, to verify it's still current
|
|
110
|
+
|
|
111
|
+
If web search surfaces nothing useful, note that in RESEARCH_DONE ("web search: no material new signal") so the operator knows you checked. Never skip the search and claim cached context was sufficient.
|
|
112
|
+
|
|
113
|
+
Exception: T1 initials (blank connection requests or first-touch sends) don't require web search. Fit-rejection calls (where the prospect obviously doesn't match ICP from cached data) don't require web search — but state that explicitly in REASONING.
|
|
114
|
+
|
|
115
|
+
For T1 structural reviews and other cases, the existing "use when needed" rule applies.
|
|
116
|
+
|
|
117
|
+
## Step 4: Edit if needed
|
|
118
|
+
|
|
119
|
+
If the message needs changes, rewrite it and apply the edit using `edit_message` with the message_id and new content.
|
|
120
|
+
|
|
121
|
+
When rewriting:
|
|
122
|
+
- Keep the same strategic intent (don't change a T2 into a T4)
|
|
123
|
+
- Follow the segment tone instructions exactly
|
|
124
|
+
- Stay within word/character limits
|
|
125
|
+
- Make it sound like something a real person would actually type in a LinkedIn DM
|
|
126
|
+
- Use the selling strategy's role adaptation for this person's title
|
|
127
|
+
- Reference real, specific things (not vague allusions)
|
|
128
|
+
- Vary sentence rhythm (short punchy sentence. Then a longer one that develops the thought.)
|
|
129
|
+
- Use contractions, fragments, and casual grammar where natural
|
|
130
|
+
- No em dashes or dash substitutes. Ever.
|
|
131
|
+
|
|
132
|
+
## Step 5: Return structured summary
|
|
133
|
+
|
|
134
|
+
After reviewing all messages in your batch, return a summary in this exact format for each message:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
MESSAGE: {message_id}
|
|
138
|
+
PERSON: {person_name} ({person_title} at {person_company})
|
|
139
|
+
MATCH_SCORE: {number}
|
|
140
|
+
CATEGORY: {initial/followup/reply_response} T{touch_number}
|
|
141
|
+
RECOMMENDATION: {approve | edited | flag | reject}
|
|
142
|
+
CONFIDENCE: {high | medium | low}
|
|
143
|
+
REASONING: {1-2 sentences explaining your decision}
|
|
144
|
+
EDITED: {yes/no}
|
|
145
|
+
NEW_CONTENT: {if edited, the new message text. If not edited, omit this field.}
|
|
146
|
+
ISSUES_FOUND: {comma-separated list of issues, or "none"}
|
|
147
|
+
PERSONALIZATION_DEPTH: {surface/basic/deep}
|
|
148
|
+
RESEARCH_DONE: {list of extra research you did, or "none"}
|
|
149
|
+
---
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Confidence Rating Guide
|
|
153
|
+
|
|
154
|
+
**HIGH confidence** when:
|
|
155
|
+
- Message passed all quality checks with no issues found
|
|
156
|
+
- Personalization is basic or deep with verifiable hooks
|
|
157
|
+
- You made no edits, or made minor edits you're sure improve the message
|
|
158
|
+
- The message clearly follows the touch sequence intent
|
|
159
|
+
- This is a routine T1-T3 for a prospect with match score below 90
|
|
160
|
+
|
|
161
|
+
**MEDIUM confidence** when:
|
|
162
|
+
- You made substantial edits (rewrote more than half the message)
|
|
163
|
+
- Personalization relies on research you couldn't fully verify
|
|
164
|
+
- The message is good but you're not 100% sure it matches the operator's voice
|
|
165
|
+
- The prospect's situation has some ambiguity
|
|
166
|
+
|
|
167
|
+
**LOW confidence** when:
|
|
168
|
+
- This is a reply_response (always low, operator needs to review)
|
|
169
|
+
- Match score is 90+ (high-value prospect, operator should see this)
|
|
170
|
+
- You flagged the message for human review
|
|
171
|
+
- The conversation thread suggests strategic complexity (pricing discussions, objections, competitor mentions)
|
|
172
|
+
- You couldn't find enough context to personalize well
|
|
173
|
+
|
|
174
|
+
## Decision Framework
|
|
175
|
+
|
|
176
|
+
**APPROVE**: passes all checks, personalization at least basic, reads like a human wrote it
|
|
177
|
+
**EDITED**: had fixable issues, you've applied the fix via edit_message, rewritten version passes
|
|
178
|
+
**FLAG**: reply_response to complex conversation, high-value prospect (90+), needs operator judgment
|
|
179
|
+
**REJECT**: fundamentally violates touch sequence, severe repetition, fabricated personalization
|
package/install.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @vruum/skills installer.
|
|
4
|
+
*
|
|
5
|
+
* For AI assistants that don't natively support MCP prompts (Codex CLI,
|
|
6
|
+
* ChatGPT, Windsurf) — installs SKILL.md files into the target harness's
|
|
7
|
+
* skill directory so slash commands still work. For clients that DO
|
|
8
|
+
* support MCP prompts (Claude Code, Claude Desktop, Cursor, VS Code,
|
|
9
|
+
* Cline), you don't need this — just connect to https://api.vruum.ai/mcp
|
|
10
|
+
* and slash commands appear automatically.
|
|
11
|
+
*
|
|
12
|
+
* Zero runtime dependencies — stdlib Node only.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npx @vruum/skills install # auto-detect Claude Code + Codex
|
|
16
|
+
* npx @vruum/skills install --target <dir> # any other harness
|
|
17
|
+
* npx @vruum/skills install --dry-run
|
|
18
|
+
* npx @vruum/skills list
|
|
19
|
+
* npx @vruum/skills uninstall
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
|
|
28
|
+
const VERSION = require('./package.json').version;
|
|
29
|
+
const PACKAGE_ROOT = __dirname;
|
|
30
|
+
const SKILLS_SRC = path.join(PACKAGE_ROOT, 'skills');
|
|
31
|
+
|
|
32
|
+
// Known harness skill directories. Add a target here when a new harness
|
|
33
|
+
// lands on a stable skill-dir convention.
|
|
34
|
+
const KNOWN_TARGETS = [
|
|
35
|
+
{ name: 'Claude Code', dir: path.join(os.homedir(), '.claude', 'skills') },
|
|
36
|
+
{ name: 'Codex CLI', dir: path.join(os.homedir(), '.codex', 'skills') },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const args = { command: 'install', targets: [], dryRun: false, help: false };
|
|
41
|
+
const rest = argv.slice(2);
|
|
42
|
+
let i = 0;
|
|
43
|
+
if (rest[i] && !rest[i].startsWith('-')) {
|
|
44
|
+
args.command = rest[i];
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
for (; i < rest.length; i += 1) {
|
|
48
|
+
const a = rest[i];
|
|
49
|
+
if (a === '--target' || a === '-t') {
|
|
50
|
+
const value = rest[i + 1];
|
|
51
|
+
if (!value) throw new Error(`${a} requires a path`);
|
|
52
|
+
args.targets.push(path.resolve(value.replace(/^~(?=\/|$)/, os.homedir())));
|
|
53
|
+
i += 1;
|
|
54
|
+
} else if (a === '--dry-run' || a === '-n') {
|
|
55
|
+
args.dryRun = true;
|
|
56
|
+
} else if (a === '--help' || a === '-h') {
|
|
57
|
+
args.help = true;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`unknown argument: ${a}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`@vruum/skills v${VERSION}
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npx @vruum/skills install Install into every detected harness
|
|
70
|
+
npx @vruum/skills install --target <dir> Add an extra target (repeatable)
|
|
71
|
+
npx @vruum/skills install --dry-run Preview changes, don't touch disk
|
|
72
|
+
npx @vruum/skills uninstall Remove previously-installed symlinks
|
|
73
|
+
npx @vruum/skills list Show detected targets + what's installed
|
|
74
|
+
|
|
75
|
+
Pairs with the Vruum MCP server at https://api.vruum.ai/mcp. Connect your AI
|
|
76
|
+
assistant to that URL first, then these skills turn common workflows into
|
|
77
|
+
slash commands.
|
|
78
|
+
|
|
79
|
+
Supported auto-detection:
|
|
80
|
+
- Claude Code ~/.claude/skills/
|
|
81
|
+
- Codex CLI ~/.codex/skills/
|
|
82
|
+
|
|
83
|
+
Any other harness: pass --target <its skills directory>.
|
|
84
|
+
|
|
85
|
+
Note: Claude Code, Claude Desktop, Cursor, VS Code Copilot, and Cline all
|
|
86
|
+
support MCP prompts natively. If you use one of those, you don't need this
|
|
87
|
+
installer — connecting to https://api.vruum.ai/mcp surfaces slash commands
|
|
88
|
+
automatically. This installer is for Codex CLI, ChatGPT, Windsurf, and
|
|
89
|
+
other assistants that don't yet support MCP prompts.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listAvailableSkills() {
|
|
93
|
+
if (!fs.existsSync(SKILLS_SRC)) return [];
|
|
94
|
+
return fs
|
|
95
|
+
.readdirSync(SKILLS_SRC, { withFileTypes: true })
|
|
96
|
+
.filter((entry) => entry.isDirectory())
|
|
97
|
+
.map((entry) => entry.name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ensureTargetDir(target, dryRun) {
|
|
101
|
+
if (fs.existsSync(target)) return { created: false };
|
|
102
|
+
if (dryRun) return { created: 'would' };
|
|
103
|
+
fs.mkdirSync(target, { recursive: true });
|
|
104
|
+
return { created: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function linkSkill({ name, srcAbs, target, dryRun }) {
|
|
108
|
+
const dst = path.join(target, name);
|
|
109
|
+
let existing = null;
|
|
110
|
+
try {
|
|
111
|
+
const lstat = fs.lstatSync(dst);
|
|
112
|
+
if (lstat.isSymbolicLink()) {
|
|
113
|
+
existing = { kind: 'symlink', target: fs.readlinkSync(dst) };
|
|
114
|
+
} else {
|
|
115
|
+
existing = { kind: lstat.isDirectory() ? 'dir' : 'file' };
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.code !== 'ENOENT') throw err;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (existing?.kind === 'symlink' && existing.target === srcAbs) {
|
|
122
|
+
return { name, target, action: 'already-linked' };
|
|
123
|
+
}
|
|
124
|
+
if (existing && existing.kind !== 'symlink') {
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
target,
|
|
128
|
+
action: 'skipped',
|
|
129
|
+
reason: `a non-symlink ${existing.kind} already exists at ${dst}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (dryRun) {
|
|
133
|
+
return { name, target, action: existing ? 'would-relink' : 'would-link' };
|
|
134
|
+
}
|
|
135
|
+
if (existing?.kind === 'symlink') {
|
|
136
|
+
fs.unlinkSync(dst);
|
|
137
|
+
}
|
|
138
|
+
fs.symlinkSync(srcAbs, dst, 'dir');
|
|
139
|
+
return { name, target, action: existing ? 'relinked' : 'linked' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function detectTargets(extraTargets) {
|
|
143
|
+
const targets = [];
|
|
144
|
+
// Auto-detect only when the skills directory itself already exists.
|
|
145
|
+
for (const known of KNOWN_TARGETS) {
|
|
146
|
+
if (fs.existsSync(known.dir)) {
|
|
147
|
+
targets.push({ ...known, autoDetected: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const dir of extraTargets) {
|
|
151
|
+
if (!targets.some((t) => t.dir === dir)) {
|
|
152
|
+
targets.push({ name: `custom (${dir})`, dir, autoDetected: false });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return targets;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function commandInstall({ targets: extraTargets, dryRun }) {
|
|
159
|
+
const skills = listAvailableSkills();
|
|
160
|
+
if (skills.length === 0) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`No skills found under ${SKILLS_SRC}. This looks like a broken package.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const targets = detectTargets(extraTargets);
|
|
166
|
+
if (targets.length === 0) {
|
|
167
|
+
console.error(
|
|
168
|
+
'No AI harness skill directories detected.\n' +
|
|
169
|
+
'Expected one of: ' +
|
|
170
|
+
KNOWN_TARGETS.map((t) => t.dir).join(', ') +
|
|
171
|
+
'\nPass --target <dir> for any other harness.'
|
|
172
|
+
);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const summary = [];
|
|
177
|
+
for (const target of targets) {
|
|
178
|
+
const { created } = ensureTargetDir(target.dir, dryRun);
|
|
179
|
+
if (created === 'would') {
|
|
180
|
+
summary.push({ target: target.dir, detail: 'would create target dir' });
|
|
181
|
+
} else if (created) {
|
|
182
|
+
summary.push({ target: target.dir, detail: 'created target dir' });
|
|
183
|
+
}
|
|
184
|
+
for (const skillName of skills) {
|
|
185
|
+
const srcAbs = path.join(SKILLS_SRC, skillName);
|
|
186
|
+
summary.push(linkSkill({ name: skillName, srcAbs, target: target.dir, dryRun }));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
191
|
+
console.log(`${prefix}@vruum/skills v${VERSION}`);
|
|
192
|
+
console.log(`${prefix}skills source: ${SKILLS_SRC}`);
|
|
193
|
+
for (const target of targets) {
|
|
194
|
+
console.log(`${prefix}target: ${target.dir}${target.autoDetected ? ' (auto)' : ''}`);
|
|
195
|
+
}
|
|
196
|
+
for (const row of summary) {
|
|
197
|
+
if (row.detail) {
|
|
198
|
+
console.log(` ${prefix}${row.detail}: ${row.target}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const label = row.action.padEnd(15);
|
|
202
|
+
console.log(` ${prefix}${label} ${row.name}${row.reason ? ` [${row.reason}]` : ''}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const skipped = summary.filter((row) => row.action === 'skipped');
|
|
206
|
+
if (skipped.length > 0) {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(`Skipped ${skipped.length} skill(s); remove the conflicting file(s) and re-run.`);
|
|
209
|
+
process.exit(2);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function commandUninstall({ targets: extraTargets, dryRun }) {
|
|
214
|
+
const skills = listAvailableSkills();
|
|
215
|
+
const targets = detectTargets(extraTargets);
|
|
216
|
+
if (targets.length === 0) {
|
|
217
|
+
console.error('No AI harness skill directories detected. Nothing to remove.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
222
|
+
console.log(`${prefix}@vruum/skills v${VERSION} uninstall`);
|
|
223
|
+
|
|
224
|
+
for (const target of targets) {
|
|
225
|
+
console.log(`${prefix}target: ${target.dir}`);
|
|
226
|
+
for (const skillName of skills) {
|
|
227
|
+
const dst = path.join(target.dir, skillName);
|
|
228
|
+
const srcAbs = path.join(SKILLS_SRC, skillName);
|
|
229
|
+
let existing = null;
|
|
230
|
+
try {
|
|
231
|
+
const lstat = fs.lstatSync(dst);
|
|
232
|
+
if (lstat.isSymbolicLink()) {
|
|
233
|
+
existing = { kind: 'symlink', target: fs.readlinkSync(dst) };
|
|
234
|
+
} else {
|
|
235
|
+
existing = { kind: 'other' };
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err.code !== 'ENOENT') throw err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!existing) {
|
|
242
|
+
console.log(` ${prefix}not-installed ${skillName}`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (existing.kind !== 'symlink' || existing.target !== srcAbs) {
|
|
246
|
+
console.log(` ${prefix}skipped ${skillName} [not our symlink]`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (dryRun) {
|
|
250
|
+
console.log(` ${prefix}would-remove ${skillName}`);
|
|
251
|
+
} else {
|
|
252
|
+
fs.unlinkSync(dst);
|
|
253
|
+
console.log(` ${prefix}removed ${skillName}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function commandList({ targets: extraTargets }) {
|
|
260
|
+
const skills = listAvailableSkills();
|
|
261
|
+
const targets = detectTargets(extraTargets);
|
|
262
|
+
console.log(`@vruum/skills v${VERSION}`);
|
|
263
|
+
console.log(`skills in package: ${skills.join(', ') || '(none)'}`);
|
|
264
|
+
if (targets.length === 0) {
|
|
265
|
+
console.log('No targets detected. Pass --target <dir> to point at your harness.');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
for (const target of targets) {
|
|
269
|
+
console.log(`\n${target.name}: ${target.dir}${target.autoDetected ? ' (auto)' : ''}`);
|
|
270
|
+
if (!fs.existsSync(target.dir)) {
|
|
271
|
+
console.log(' (directory does not exist yet)');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const skillName of skills) {
|
|
275
|
+
const dst = path.join(target.dir, skillName);
|
|
276
|
+
const srcAbs = path.join(SKILLS_SRC, skillName);
|
|
277
|
+
try {
|
|
278
|
+
const lstat = fs.lstatSync(dst);
|
|
279
|
+
if (lstat.isSymbolicLink()) {
|
|
280
|
+
const linkTarget = fs.readlinkSync(dst);
|
|
281
|
+
const match = linkTarget === srcAbs;
|
|
282
|
+
console.log(` ${match ? 'installed' : 'other-link'.padEnd(10)} ${skillName}${match ? '' : ` -> ${linkTarget}`}`);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(` present ${skillName} (not our symlink)`);
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err.code === 'ENOENT') {
|
|
288
|
+
console.log(` missing ${skillName}`);
|
|
289
|
+
} else {
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function main() {
|
|
298
|
+
let args;
|
|
299
|
+
try {
|
|
300
|
+
args = parseArgs(process.argv);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(err.message);
|
|
303
|
+
printHelp();
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (args.help || args.command === 'help') {
|
|
308
|
+
printHelp();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
switch (args.command) {
|
|
313
|
+
case 'install':
|
|
314
|
+
commandInstall(args);
|
|
315
|
+
break;
|
|
316
|
+
case 'uninstall':
|
|
317
|
+
commandUninstall(args);
|
|
318
|
+
break;
|
|
319
|
+
case 'list':
|
|
320
|
+
commandList(args);
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
console.error(`Unknown command: ${args.command}`);
|
|
324
|
+
printHelp();
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vruum/skills",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/vruum-gtm/skills.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://vruum.ai",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/vruum-gtm/skills/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"vruum-skills": "./install.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"install.js",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
".mcp.json",
|
|
22
|
+
"skills/",
|
|
23
|
+
"agents/"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"vruum",
|
|
30
|
+
"mcp",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"codex",
|
|
33
|
+
"ai",
|
|
34
|
+
"sales",
|
|
35
|
+
"outreach",
|
|
36
|
+
"gtm"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: diagnose-reply
|
|
3
|
+
description: "Diagnose why a reply happened — what worked or didn't in the outreach that triggered it. Use when: why did they reply, what worked, diagnose reply, reply diagnosis, analyze this reply, what caused this reply, reply analysis."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /diagnose-reply
|
|
7
|
+
|
|
8
|
+
You diagnose individual replies to understand what worked (or didn't) in the outreach that preceded them. This turns every reply into a learning event.
|
|
9
|
+
|
|
10
|
+
## Step 1: Identify the reply
|
|
11
|
+
|
|
12
|
+
The operator will reference a person, a reply, or a conversation. Use `get_person_360` with `for_company` to load the full context: conversation thread, outreach plan, match analysis.
|
|
13
|
+
|
|
14
|
+
If the operator doesn't specify a person, ask: "Which reply do you want to diagnose? Give me a name, company, or message."
|
|
15
|
+
|
|
16
|
+
## Step 2: Read the conversation
|
|
17
|
+
|
|
18
|
+
From the person_360 response, read the full conversation thread. Identify:
|
|
19
|
+
|
|
20
|
+
- Which touch triggered the reply (the last outreach message before the reply)
|
|
21
|
+
- What strategy angle was used in that message
|
|
22
|
+
- What channel it was on
|
|
23
|
+
- What personalization hooks were in the message
|
|
24
|
+
- What CTA was used (question, calendar link, statement)
|
|
25
|
+
- What the reply actually says (positive, negative, question, objection)
|
|
26
|
+
|
|
27
|
+
## Step 3: Diagnose
|
|
28
|
+
|
|
29
|
+
Call `get_performance_metrics` with `for_company`, `view='funnel'`, and the segment_id to get segment-level reply rates by channel.
|
|
30
|
+
|
|
31
|
+
Synthesize a diagnosis:
|
|
32
|
+
|
|
33
|
+
"**Reply diagnosis: [person name]**
|
|
34
|
+
|
|
35
|
+
**The message that got the reply:** T3 via LinkedIn DM, 'workflow_pain' angle, referenced their recent job posting for DevOps engineer (specific signal), ended with a question about their current tooling.
|
|
36
|
+
|
|
37
|
+
**Why it likely worked:**
|
|
38
|
+
- Channel: LinkedIn DM has [X]% reply rate in this segment (vs [Y]% email)
|
|
39
|
+
- Angle: 'workflow_pain' converts at [X]% in this segment ([Z]x above average)
|
|
40
|
+
- Signal: Referenced a specific, verifiable company signal (job posting)
|
|
41
|
+
- CTA: Question CTA has [X]x lift over statement CTAs
|
|
42
|
+
|
|
43
|
+
**What's different from the ghosted messages:**
|
|
44
|
+
- T1 and T2 used 'cost_replacement' angle (segment average: [X]%)
|
|
45
|
+
- T1 had surface-level personalization (company name only)
|
|
46
|
+
- T2 had no question CTA
|
|
47
|
+
|
|
48
|
+
**The pattern:** This person responded when the message got specific about THEIR situation (job posting = current hiring pain) and asked a genuine question. The earlier touches were generic."
|
|
49
|
+
|
|
50
|
+
## Step 4: Suggest action
|
|
51
|
+
|
|
52
|
+
**If positive reply** (interested, wants to learn more, asks a question):
|
|
53
|
+
- "This combination is worth repeating: [angle + specific signal + CTA style]. Note it in your segment's strategy so the next batch follows the same shape."
|
|
54
|
+
|
|
55
|
+
**If negative reply** (not interested, wrong person, bad timing):
|
|
56
|
+
- "This is the [N]th rejection using the '[angle]' strategy in this segment. If the pattern holds, consider deprioritizing this angle for the segment."
|
|
57
|
+
|
|
58
|
+
**If objection or question** (pricing, timing, skepticism):
|
|
59
|
+
- "This is a live conversation. The reply response draft should address [specific objection]. Check the outreach queue — there may be a draft pending."
|
|
60
|
+
|
|
61
|
+
## Notes
|
|
62
|
+
|
|
63
|
+
- This skill is diagnostic, not prescriptive. It explains what happened; acting on the pattern (deprioritizing an angle, repeating a winning combination) is a separate, deliberate decision.
|
|
64
|
+
- When the conversation has many touches, focus on the touch that triggered the reply, not the full sequence.
|
|
65
|
+
- Always compare against segment averages to distinguish signal from noise. One data point doesn't make a pattern.
|
|
66
|
+
- If `get_performance_metrics` doesn't have enough data for the segment, say so: "Not enough segment data to compare against. This is a single data point."
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engagement-triage
|
|
3
|
+
description: "Review and approve your pending LinkedIn engagement drafts and demand-gen content posts. Use when: triage engagements, review engagement queue, review warming comments, review nurture reactions, review marketing comments, review content drafts, check engagement queue."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /engagement-triage
|
|
7
|
+
|
|
8
|
+
You review the user's pending LinkedIn engagement drafts (warming comments, nurture reactions, marketing comments) and demand-gen content posts, dispatching review subagents in parallel and presenting results for approval. Separate from `/outreach-triage` (which handles outreach messages).
|
|
9
|
+
|
|
10
|
+
## Why this is a skill and not just "call the tool"
|
|
11
|
+
|
|
12
|
+
Each engagement item carries context — the post being engaged with, the target persona, the sender's own voice and positioning, the draft comment. Reviewing inline burns tokens fast. Subagents with their own context windows do the review work and return compact verdicts.
|
|
13
|
+
|
|
14
|
+
## Subagent: `vruum-engagement-reviewer`
|
|
15
|
+
|
|
16
|
+
This skill dispatches the `vruum-engagement-reviewer` subagent (bundled at `agents/vruum-engagement-reviewer.md`). That agent reviews both engagement items AND content posts.
|
|
17
|
+
|
|
18
|
+
**Dispatch via the Agent tool** with `subagent_type: "vruum-engagement-reviewer"`. Supports `run_in_background=true` for parallelism.
|
|
19
|
+
|
|
20
|
+
Falls back to general-purpose subagent with MCP tool names in the prompt if the subagent type isn't recognized.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
### Step 1: Summarize the queue
|
|
25
|
+
|
|
26
|
+
Call `get_marketing_overview` (single-company view) to see what's pending. Present a one-liner:
|
|
27
|
+
|
|
28
|
+
"X warming drafts, Y nurture drafts, Z marketing drafts, N content posts pending."
|
|
29
|
+
|
|
30
|
+
If everything is 0, say "Engagement queue is clear" and stop.
|
|
31
|
+
|
|
32
|
+
### Step 2: Choose scope
|
|
33
|
+
|
|
34
|
+
Ask the user:
|
|
35
|
+
- A) Full triage — warming → nurture → marketing → content, in order
|
|
36
|
+
- B) Warming only (LinkedIn comments on target posts to warm up a prospect before outreach)
|
|
37
|
+
- C) Nurture only (reactions + comments on customers/prospects mid-conversation)
|
|
38
|
+
- D) Marketing only (comments on broader demand-gen posts to surface your brand)
|
|
39
|
+
- E) Content posts only (your own outgoing LinkedIn posts)
|
|
40
|
+
|
|
41
|
+
If the user just says "go", default to A.
|
|
42
|
+
|
|
43
|
+
### Step 3: Pull sender identity (REQUIRED before dispatch)
|
|
44
|
+
|
|
45
|
+
Call `get_company_profile` to get the sender's identity, value prop, industry expertise, and background. Subagents need this to validate that drafts sound like the right person.
|
|
46
|
+
|
|
47
|
+
Include a SENDER PROFILE block in every subagent prompt:
|
|
48
|
+
```
|
|
49
|
+
SENDER PROFILE:
|
|
50
|
+
Name: {sender name}
|
|
51
|
+
Company: {company name}
|
|
52
|
+
What you do: {value_proposition}
|
|
53
|
+
Expertise: {target_industries}
|
|
54
|
+
Background: {founder_background}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Step 4: Dispatch per queue
|
|
58
|
+
|
|
59
|
+
For each queue type the user selected, call the appropriate list endpoint, get IDs + lightweight context (no full content yet), then dispatch subagents.
|
|
60
|
+
|
|
61
|
+
**Warming / Nurture / Marketing engagements** — call `get_engagement_queue` filtered by `type` (`warming` / `nurture` / `marketing`). Batch 3-5 per subagent.
|
|
62
|
+
|
|
63
|
+
**Content posts** — call `get_content_review` for drafts awaiting approval. Batch 2-3 per subagent (posts are longer and need more careful voice check).
|
|
64
|
+
|
|
65
|
+
Spawn up to 4 subagents concurrently. For larger queues (15+), dispatch in waves.
|
|
66
|
+
|
|
67
|
+
**Subagent prompt — engagement items:**
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
You are an engagement review agent.
|
|
71
|
+
|
|
72
|
+
{SENDER PROFILE block}
|
|
73
|
+
|
|
74
|
+
Engagement IDs: {comma_separated_engagement_ids}
|
|
75
|
+
Queue type: {warming | nurture | marketing}
|
|
76
|
+
|
|
77
|
+
Call get_engagement_review with engagement_ids="{ids}" and content_length="full" to load your items. Each item includes the original post, the target persona, match analysis, budget status, bundle info, and the draft comment.
|
|
78
|
+
|
|
79
|
+
For each item:
|
|
80
|
+
1. Voice fit — does the draft sound like the sender? (check SENDER PROFILE above; flag anything generic or out-of-voice)
|
|
81
|
+
2. Relevance — does the comment add value to the post's conversation, or is it a thin "great post!" type?
|
|
82
|
+
3. Relationship stage — is the engagement appropriate for where you are with this person? (warming = not yet in outreach, nurture = mid-conversation, marketing = brand surfacing)
|
|
83
|
+
4. AI tells — generic phrases, em dashes, overused words, robotic cadence
|
|
84
|
+
5. Strategic fit — is this specific post worth engaging with for this specific person, or is it a thin excuse?
|
|
85
|
+
|
|
86
|
+
If the draft needs editing, call manage_engagement with action=edit. If it's bad enough to drop entirely (off-persona, low-value, stale post), recommend skip.
|
|
87
|
+
|
|
88
|
+
Return:
|
|
89
|
+
ENGAGEMENT: {id} | TARGET: {person name} | TYPE: {warming|nurture|marketing} | RECOMMENDATION: {approve|edited|skip} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Subagent prompt — content posts:**
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
You are a content review agent.
|
|
96
|
+
|
|
97
|
+
{SENDER PROFILE block}
|
|
98
|
+
|
|
99
|
+
Post IDs: {comma_separated_post_ids}
|
|
100
|
+
|
|
101
|
+
Call get_content_review with post_ids="{ids}" and content_length="full" to load drafts. Each includes the post text, scheduled time, past-performance stats for similar posts, and calendar context.
|
|
102
|
+
|
|
103
|
+
For each post:
|
|
104
|
+
1. Voice fit — sounds like the sender? (SENDER PROFILE above; flag anything generic)
|
|
105
|
+
2. Hook quality — does the first line stop scroll? Is it specific and worth reading further?
|
|
106
|
+
3. AI tells — em dashes, AI-vocabulary ("delve", "robust", "comprehensive"), uniform sentence length, generic openers
|
|
107
|
+
4. Strategic fit — does this post serve the sender's ICP and positioning, or is it generic thought-leadership?
|
|
108
|
+
5. Calendar fit — appropriate timing relative to other recent posts? (get_content_review returns calendar context)
|
|
109
|
+
|
|
110
|
+
If the post needs editing, call manage_content_post with action=edit. If it's bad enough to reject, recommend reject.
|
|
111
|
+
|
|
112
|
+
Return:
|
|
113
|
+
POST: {id} | SCHEDULED: {time} | RECOMMENDATION: {approve|edited|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"} | HOOK_RATING: {1-10}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Step 5: Present results — show drafts before approving
|
|
117
|
+
|
|
118
|
+
Group by recommendation:
|
|
119
|
+
|
|
120
|
+
1. **Clean approvals** — show the draft and a one-line "why it's good". Bulk-approve with one response.
|
|
121
|
+
2. **Edited drafts** — show the new version, what changed, why. User reviews each.
|
|
122
|
+
3. **Skip / reject** — show the draft and the issue. One action to confirm.
|
|
123
|
+
|
|
124
|
+
For content posts, always walk through one at a time — they're user-visible and higher stakes than a comment.
|
|
125
|
+
|
|
126
|
+
### Step 6: User overrides
|
|
127
|
+
|
|
128
|
+
- Pull full context for any item
|
|
129
|
+
- Adjust any subagent edit before approving
|
|
130
|
+
- Skip the whole queue type ("actually, don't review marketing, just warming")
|
|
131
|
+
- Ask to see a specific person's engagement history (`get_engagement_queue` filtered by person_id)
|
|
132
|
+
|
|
133
|
+
## Edge cases
|
|
134
|
+
|
|
135
|
+
**Tiny queue (10 or fewer total):** skip subagents, review inline with the user. Subagent overhead isn't worth it.
|
|
136
|
+
|
|
137
|
+
**Only warming, no content:** skip Step 4's content-post branch.
|
|
138
|
+
|
|
139
|
+
**Subagent can't reach Vruum MCP:** fall back to inline review. Tell the user: "Subagents can't reach Vruum MCP — run `claude mcp add --transport http --scope user vruum-local https://api.vruum.ai/mcp` once (OAuth), then retry."
|
|
140
|
+
|
|
141
|
+
**User wants to see past performance before approving content:** `get_content_review` already returns past performance stats per post — reference them in your presentation so the user can calibrate.
|
|
142
|
+
|
|
143
|
+
## After triage
|
|
144
|
+
|
|
145
|
+
Offer:
|
|
146
|
+
- "Want to review your outreach queue next?" (runs `/outreach-triage`)
|
|
147
|
+
- "Check your marketing overview?" (calls `get_marketing_overview` for a recap)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: enrich-prospect
|
|
3
|
+
description: "Deep prospect diarization — synthesize everything known about a person into a structured intelligence profile. Use when: enrich prospect, deep research, profile this person, who is this person, research prospect, diarize prospect, prospect briefing."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /enrich-prospect
|
|
7
|
+
|
|
8
|
+
You diarize a prospect. Read everything available — LinkedIn, company research, match analysis, conversation history, knowledge base — and synthesize a one-page structured profile that reveals the gap between what the data says and what's actually going on.
|
|
9
|
+
|
|
10
|
+
This is not a database lookup. This is an analyst's brief.
|
|
11
|
+
|
|
12
|
+
## Step 1: Gather all sources
|
|
13
|
+
|
|
14
|
+
Call these in parallel with `for_company`:
|
|
15
|
+
- `get_person_360` — profile, match analysis, research, activity, outreach plan, deal
|
|
16
|
+
- `get_person_research` — structured research data (if exists)
|
|
17
|
+
- `get_company_research` — company intelligence
|
|
18
|
+
|
|
19
|
+
If research is thin (no person_research, or match_analysis is null):
|
|
20
|
+
- `fetch_linkedin_data` — pull their recent posts and profile
|
|
21
|
+
- WebSearch for "[person name] [company name]" — recent news, talks, publications
|
|
22
|
+
- `search_knowledge_base` with `for_company` — relevant sales docs
|
|
23
|
+
|
|
24
|
+
## Step 2: Diarize
|
|
25
|
+
|
|
26
|
+
Read all sources. Hold them in mind at once. Write a structured profile:
|
|
27
|
+
|
|
28
|
+
"## [Person Name] — [Title] at [Company]
|
|
29
|
+
|
|
30
|
+
**SAYS:** [What their title/bio/LinkedIn headline says they do]
|
|
31
|
+
|
|
32
|
+
**ACTUALLY:** [What the evidence suggests they actually focus on — based on posts, research, hiring patterns, company signals. This is the gap that matters.]
|
|
33
|
+
|
|
34
|
+
**Key signals:**
|
|
35
|
+
- [Signal 1: specific, verifiable fact from research or posts]
|
|
36
|
+
- [Signal 2]
|
|
37
|
+
- [Signal 3]
|
|
38
|
+
|
|
39
|
+
**Fit assessment:**
|
|
40
|
+
- Match score: [X]/100 — [alignment summary]
|
|
41
|
+
- Strongest alignment: [specific alignment point with evidence]
|
|
42
|
+
- Biggest concern: [specific concern or unknown]
|
|
43
|
+
- Why now: [trigger event or timing signal, if any]
|
|
44
|
+
|
|
45
|
+
**What we're uncertain about:**
|
|
46
|
+
- [Thing we don't know that would change the approach]
|
|
47
|
+
- [Assumption we're making that could be wrong]
|
|
48
|
+
|
|
49
|
+
**Recommended approach:**
|
|
50
|
+
- [Specific angle based on the diarization, not generic]
|
|
51
|
+
- [What NOT to say based on their actual situation]"
|
|
52
|
+
|
|
53
|
+
## Step 3: The "SAYS vs ACTUALLY" gap
|
|
54
|
+
|
|
55
|
+
This is the most important part. Examples:
|
|
56
|
+
|
|
57
|
+
- **SAYS** "VP of Engineering" → **ACTUALLY** their last 3 posts are about hiring and retention, not technology. They're a people manager, not a tech leader. Approach through team-building lens, not tech lens.
|
|
58
|
+
|
|
59
|
+
- **SAYS** "Datadog for AI agents" → **ACTUALLY** 80% of their GitHub commits are in the billing module. They're building a FinOps tool disguised as observability.
|
|
60
|
+
|
|
61
|
+
- **SAYS** "Director of Marketing" → **ACTUALLY** their company just raised Series B and they posted about building an in-house content team. They're scaling, not maintaining.
|
|
62
|
+
|
|
63
|
+
No embedding search finds these gaps. No keyword filter finds them. You have to read the full profile and make a judgment.
|
|
64
|
+
|
|
65
|
+
## Step 4: Offer to save
|
|
66
|
+
|
|
67
|
+
"Update AI notes with this diarization? This will be visible to the outreach agent when writing messages."
|
|
68
|
+
|
|
69
|
+
If approved: call `update_person_ai_notes` with a condensed version of the diarization (the SAYS/ACTUALLY gap, key signals, and recommended approach).
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- This skill is invoked for individual prospects, not batch. For batch enrichment, that's the server-side research pipeline.
|
|
74
|
+
- The diarization should be opinionated. "This person is probably not a fit because..." is more valuable than "Match score is 65."
|
|
75
|
+
- Always note what you're uncertain about. Confidence without uncertainty is just hallucination.
|
|
76
|
+
- The SAYS vs ACTUALLY gap requires reading multiple sources and holding contradictions in mind. Don't rush it.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: outreach-triage
|
|
3
|
+
description: "Review and approve your pending outreach drafts with parallel AI review. Use when: triage outreach, review queue, review pending messages, morning review, check my drafts, what needs approval, clear my queue, review messages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /outreach-triage
|
|
7
|
+
|
|
8
|
+
You review the user's pending outreach drafts, dispatch AI reviewer subagents in parallel to evaluate each message, and present results for approval. One company (the user's own), structured review, conversation-level edits where needed.
|
|
9
|
+
|
|
10
|
+
## Why this is a skill and not just "call the tool"
|
|
11
|
+
|
|
12
|
+
Reviewing outreach drafts is context-expensive. Each message carries the full conversation thread, segment instructions, match analysis, and company research. Pulling 20 messages inline blows out the context window. Instead, this skill fans out to subagents — each with its own context — who do deep review and return compact verdicts.
|
|
13
|
+
|
|
14
|
+
## Subagent: `vruum-outreach-reviewer`
|
|
15
|
+
|
|
16
|
+
This skill dispatches the `vruum-outreach-reviewer` subagent (bundled with this plugin at `agents/vruum-outreach-reviewer.md`). That agent has full Vruum MCP access and complete review instructions baked in.
|
|
17
|
+
|
|
18
|
+
**Dispatch via the Agent tool** with `subagent_type: "vruum-outreach-reviewer"`. Supports `run_in_background=true` for parallelism.
|
|
19
|
+
|
|
20
|
+
If the Agent tool doesn't recognize the subagent type, fall back to the general-purpose subagent with the message IDs and company context in the prompt — the subagent can still call Vruum MCP tools directly (`mcp__vruum-local__get_outreach_review`, etc.) as long as the Vruum MCP is connected.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
### Step 1: Summarize the queue
|
|
25
|
+
|
|
26
|
+
Call `get_outreach_stats` to get counts of pending drafts by status. Present a quick summary:
|
|
27
|
+
|
|
28
|
+
"You have X pending drafts: Y reply responses, Z first-touches (T1s), W follow-ups (T2+). Run full triage, or focus somewhere specific?"
|
|
29
|
+
|
|
30
|
+
If the user just says "go", default to full triage.
|
|
31
|
+
|
|
32
|
+
### Step 2: Build the dispatch list
|
|
33
|
+
|
|
34
|
+
Call `get_message_queue` with `status=draft` and `limit=50` to get message IDs, person names, categories, and sequence numbers WITHOUT content (cheap on tokens).
|
|
35
|
+
|
|
36
|
+
Categorize into processing groups:
|
|
37
|
+
|
|
38
|
+
1. **Reply responses** (`category=reply_response`) — someone replied to you. High-stakes, always 1 subagent per message with full research.
|
|
39
|
+
2. **Follow-ups** (`sequence_number >= 2`) — existing threads. Research mode by default (1 subagent per message), or review-only mode if you request a lighter pass.
|
|
40
|
+
3. **T1 initials** (`sequence_number = 1`) — new cold outreach. Usually structural checks only (format, length, blank connection notes). Batch 5-8 per subagent.
|
|
41
|
+
|
|
42
|
+
### Step 3: Dispatch subagents in parallel
|
|
43
|
+
|
|
44
|
+
Batch sizes:
|
|
45
|
+
|
|
46
|
+
- **T1s:** 5-8 per subagent (structural review is fast and uniform)
|
|
47
|
+
- **Follow-ups in research mode:** 1 per subagent (each does deep prospect research + rewrite-if-needed)
|
|
48
|
+
- **Follow-ups in review mode:** 3-5 per subagent (light quality + dedup check)
|
|
49
|
+
- **Reply responses:** always 1 per subagent
|
|
50
|
+
|
|
51
|
+
Spawn up to 5 subagents concurrently with `run_in_background=true`. For larger queues (20+), dispatch in waves: first wave, collect, second wave.
|
|
52
|
+
|
|
53
|
+
**Subagent prompt — Review mode (T1s, light follow-up check):**
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
You are an outreach review agent. Review these messages:
|
|
57
|
+
|
|
58
|
+
Message IDs: {comma_separated_message_ids}
|
|
59
|
+
|
|
60
|
+
Call get_outreach_review with message_ids="{message_ids}" and content_length="full" to load your messages.
|
|
61
|
+
|
|
62
|
+
For each message:
|
|
63
|
+
1. Structural compliance (touch sequence, char limits, channel)
|
|
64
|
+
2. Cross-touch deduplication (read the full thread, flag any repeated stats/questions/hooks)
|
|
65
|
+
3. AI tells (banned words, em dashes, uniform sentence length, generic openers)
|
|
66
|
+
4. Personalization depth (surface/basic/deep)
|
|
67
|
+
5. Strategic fit (CTA matches stage, moves conversation forward)
|
|
68
|
+
|
|
69
|
+
If a message needs fixes, call edit_message. If personalization is weak, call search_knowledge_base for relevant hooks.
|
|
70
|
+
|
|
71
|
+
Return a structured summary per message:
|
|
72
|
+
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Subagent prompt — Research mode (follow-ups T2+, 1 per agent):**
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
You are a prospect research and outreach review agent.
|
|
79
|
+
|
|
80
|
+
Message ID: {message_id}
|
|
81
|
+
Prospect: {person_name}, {title} at {company}
|
|
82
|
+
Message type: T{sequence_number} follow-up
|
|
83
|
+
|
|
84
|
+
Steps:
|
|
85
|
+
1. get_outreach_review(message_ids="{message_id}", content_length="full") — message + thread + segment instructions + match analysis.
|
|
86
|
+
2. get_person_research and get_person_360 for this person.
|
|
87
|
+
3. get_company_research for the user's own company (product + positioning).
|
|
88
|
+
4. WebSearch for the prospect and their company — what they do, what challenges they face, what they post about.
|
|
89
|
+
5. search_knowledge_base for relevant intel.
|
|
90
|
+
|
|
91
|
+
Review against what you learned:
|
|
92
|
+
- Does the message accurately reflect what this prospect's company does?
|
|
93
|
+
- Is there a genuine problem the prospect has that this product solves?
|
|
94
|
+
- Is the personalization based on real, verified information?
|
|
95
|
+
- Any AI tells, cross-touch duplication, or structural issues?
|
|
96
|
+
|
|
97
|
+
If the message is solid, approve. If there's clear opportunity to improve (weak personalization when rich signals exist, wrong framing, fabricated references), edit via edit_message. Don't rewrite messages that are already good just because you can.
|
|
98
|
+
|
|
99
|
+
Return:
|
|
100
|
+
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"} | RESEARCH_SUMMARY: {2-3 sentences on what you found} | PROBLEM_IDENTIFIED: {yes/no/speculative} | REWRITE_REASON: {why you edited, or "n/a"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 4: Present results — show messages before approving
|
|
104
|
+
|
|
105
|
+
Never auto-approve without showing. Group by recommendation:
|
|
106
|
+
|
|
107
|
+
1. **Clean approvals** — show the message and a one-line "why it's good". User bulk-approves with one response.
|
|
108
|
+
2. **Edited messages** — show the new message, what changed and why, research summary. User reviews each.
|
|
109
|
+
3. **Flagged/rejected** — show the message and the issue. For bad-fit rejections, offer the cascade (see Step 5).
|
|
110
|
+
|
|
111
|
+
**For T1s with a homogeneous fix:** if the first batch all needed the same fix (e.g. all had pitched connection notes → blanked all), present once: "14 T1s all had pitched connection notes — blanked all of them. Approve the batch?" One decision instead of 14.
|
|
112
|
+
|
|
113
|
+
**For reply responses:** always walk through one at a time. Show the prospect's reply, the draft response, the subagent's analysis.
|
|
114
|
+
|
|
115
|
+
### Step 5: Rejection cascade
|
|
116
|
+
|
|
117
|
+
When a message is rejected because the prospect is a bad fit (not because the draft quality is poor), offer to stop the outreach plan for that person:
|
|
118
|
+
|
|
119
|
+
"Sandoz is biosimilars, no D2C signal — not a fit. Reject this message and stop outreach for this person?"
|
|
120
|
+
|
|
121
|
+
One confirmation, two actions (reject draft + stop plan). Only for fit-based rejections, not quality-based ones.
|
|
122
|
+
|
|
123
|
+
### Step 6: User overrides
|
|
124
|
+
|
|
125
|
+
The user can always:
|
|
126
|
+
- Pull full context for any message
|
|
127
|
+
- Reject a message (it gets regenerated)
|
|
128
|
+
- Adjust any subagent edit before approving
|
|
129
|
+
- Switch modes mid-triage ("actually, research and rewrite the rest of these follow-ups")
|
|
130
|
+
- Ask to see a specific person's full conversation
|
|
131
|
+
|
|
132
|
+
## Edge cases
|
|
133
|
+
|
|
134
|
+
**Tiny queue (5 or fewer total):** skip the subagent dispatch. Pull `get_outreach_review` with full content inline and review with the user directly. Subagent overhead isn't worth it.
|
|
135
|
+
|
|
136
|
+
**Mostly T1s, few follow-ups:** still subagent the T1s (one agent handles them all), review the handful of follow-ups inline.
|
|
137
|
+
|
|
138
|
+
**User wants to review a specific person:** pull `get_conversation` for that person and review directly. Skip the batch workflow.
|
|
139
|
+
|
|
140
|
+
**Subagent can't reach MCP tools:** if a subagent reports MCP connection errors, fall back to inline review. This usually means the Vruum MCP isn't set up for subagent inheritance. Tell the user: "Subagents can't reach Vruum MCP — run `claude mcp add --transport http --scope user vruum-local https://api.vruum.ai/mcp` once (OAuth), then retry."
|
|
141
|
+
|
|
142
|
+
**Large queue (30+):** warn the user it'll take a few minutes, dispatch in waves (5 subagents per wave), show progress between waves.
|
|
143
|
+
|
|
144
|
+
## After triage
|
|
145
|
+
|
|
146
|
+
Offer a quick followup:
|
|
147
|
+
- "Want to review your LinkedIn engagement queue next?" (runs `/engagement-triage` if they install it)
|
|
148
|
+
- "Check your outreach stats?" (calls `get_outreach_stats` for a quick snapshot)
|