@vruum/skills 0.5.2 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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": {
@@ -36,5 +36,5 @@
36
36
  "outreach",
37
37
  "gtm"
38
38
  ],
39
- "contentHash": "e0050f63b3a0e2e8dfd104839324afe1d385046dc74fe0a2347ba37906ddd7b4"
39
+ "contentHash": "b612084d29f288715842a2c9527811f6db59778759c9cb918819f84d10ca5d30"
40
40
  }
@@ -21,19 +21,36 @@ Open with a single question if the seller hasn't already said what they want to
21
21
 
22
22
  Keep it light. You're not locking a brief yet — you just need a seed string to retrieve relevant signal. Do not call any tool until you have at least a rough topic.
23
23
 
24
- ## Step 2: Pull the seller's signal
24
+ ## Step 2: Resolve the author (whose LinkedIn account)
25
25
 
26
- Call `get_seller_signal_bundle` with the seller's rough topic as the `draft_brief` argument. The brief drives a semantic re-rank, so the more concrete the topic, the more relevant the returned evidence.
26
+ A client can have **multiple people**, each with their own connected LinkedIn account. The post must be drafted from the right person's signal and authored from their account. Before pulling any signal, find out who you're authoring as.
27
+
28
+ Call `get_channel_status` and read `channels.linkedin_accounts` — a list of accounts, each with `account_id`, `account_name`, `user_id`, `user_name`, `user_email`, `status`, `connected`, `has_sales_nav`, and `quota`.
29
+
30
+ Then pick the author **`user_id`** to use:
31
+
32
+ - **`linkedin_accounts` is missing, empty, or the `get_channel_status` call fails** → fall back to the legacy default: omit `author_user_id` entirely on the calls below and proceed. Never block content creation on this lookup.
33
+ - **Exactly one usable account** (one entry with a non-null `user_id`) → silently auto-select that account's `user_id`. Do **not** prompt. **Still pass it explicitly** in Step 3 and Step 5 — do not rely on omitting the param. Omitting it grounds the signal on *you* (the caller) and stores *no* author on the draft, and that single account may belong to a teammate, not you.
34
+ - **More than one usable account** → ask the operator **which person to author as**, presenting each option by `user_name` and `user_email`. The selectable unit is the **person (`user_id`)**, because `author_user_id` is the only selector the signal/draft tools accept — if one person has multiple LinkedIn accounts, this skill can't target them individually (say so if asked). Map the chosen person to their `user_id`.
35
+ - **Skip any account whose `user_id` is null/missing** when counting and presenting options (it isn't addressable as an author).
36
+
37
+ Hold the resolved `user_id` (or the decision to omit it) and use it consistently for both the signal pull and the draft.
38
+
39
+ ## Step 3: Pull the seller's signal
40
+
41
+ Call `get_seller_signal_bundle` with the rough topic as the `draft_brief` argument. **If you resolved an author `user_id` in Step 2, pass it as the `author_user_id` query param** so the signal is *that person's*, not the caller's. (Omit `author_user_id` only in the legacy-fallback case from Step 2.) The brief drives a semantic re-rank, so the more concrete the topic, the more relevant the returned evidence.
42
+
43
+ **Trust boundary — handle 403 honestly.** Authoring as another person is permission-guarded server-side (a non-owner can't author as a teammate). If this call (or the draft call in Step 5) returns **403**, STOP: tell the operator plainly that they aren't entitled to author as that person, and ask them to pick a permitted author or have an owner do it. Do **not** retry with `author_user_id` omitted — that would silently fall back to caller/company-scoped generation under a different identity than was asked for.
27
44
 
28
45
  **Ground on `formatted_evidence`.** It is the EVIDENCE-wrapped rendering prepared for drafting — the surface you should read, quote, and reason over. The backend scrubs prospect names, emails, phone numbers, and URLs out of *everything* it returns (both `formatted_evidence` and the raw `bundle` text), so you don't have to police that yourself — there is no un-redacted surface on the response. Still prefer `formatted_evidence`: it is the prepared, sectioned grounding surface, where `bundle` is just the structured raw material behind it.
29
46
 
30
47
  `formatted_evidence` can be `null`. Null-check it before using it:
31
- - If `formatted_evidence` is present → use it as the grounding evidence in Step 3.
48
+ - If `formatted_evidence` is present → use it as the grounding evidence in Step 4.
32
49
  - If `formatted_evidence` is `null` (a `formatted_evidence_skipped_reason` of `bundle_empty` means there was no signal to draw on yet; any other reason means the evidence was withheld and the `bundle` text is redacted to empty too) → say so plainly and fall back to a **profile-only draft**: write from the company voice profile and the seller's stated topic alone, with no evidence grounding.
33
50
 
34
- ## Step 2.5: Check the calendar (optional)
51
+ ## Step 3.5: Check the calendar (optional)
35
52
 
36
- This step is **optional** and **must never gate the loop**. It exists only to make the brief a little smarter, not to add a precondition before drafting. If you skip it, or it returns nothing, or it errors — proceed to Step 3 silently and draft anyway.
53
+ This step is **optional** and **must never gate the loop**. It exists only to make the brief a little smarter, not to add a precondition before drafting. If you skip it, or it returns nothing, or it errors — proceed to Step 4 silently and draft anyway.
37
54
 
38
55
  If a quick read of what's already on the content calendar would help shape the angle, call `get_content_calendar` **once** (the default `days` window is fine — don't loop or page). Treat the returned `posts` (each with `topic_tags`, `status`, and `scheduled_at`/`published_at`) and the `summary` as planning context only, to:
39
56
 
@@ -43,7 +60,7 @@ If a quick read of what's already on the content calendar would help shape the a
43
60
 
44
61
  Surface anything useful to the seller as a light suggestion ("you posted on this theme last week — want a different angle, or a follow-up?"), never as a blocker. If `posts` is empty, the tool is unavailable, or it errors, say nothing about the calendar and just continue. Do not retry it.
45
62
 
46
- ## Step 3: Surface the evidence and settle the brief
63
+ ## Step 4: Surface the evidence and settle the brief
47
64
 
48
65
  If you have `formatted_evidence`, show the seller the relevant points you found, in plain language:
49
66
 
@@ -56,19 +73,23 @@ Which of these do you want to anchor the post on? And what's the angle — a les
56
73
 
57
74
  Let the seller refine. Iterate in conversation until the brief is **settled** — you and the seller agree on the angle, the anchor evidence, and the tone. Do not generate a draft while the brief is still moving.
58
75
 
59
- ## Step 4: Draft on-voice — once
76
+ ## Step 5: Draft on-voice — once
60
77
 
61
- **Only once the brief is settled**, call `generate_linkedin_post`, passing the settled brief as the `topic` argument. The backend writes the draft in the seller's voice and grounds it on their seller signal internallyyou do not pass the evidence yourself.
78
+ **Only once the brief is settled**, call `generate_linkedin_post`, passing the settled brief as the `topic` argument. **If you resolved an author `user_id` in Step 2, pass the same value as `author_user_id`** so the draft is written from that person's voice/signal and the draft row is stamped with their `author_user_id` (this is what later carries the author through schedule/publish). Pass the *same* `user_id` you used for the signal pulldon't let signal and draft disagree. Omit `author_user_id` only in the legacy-fallback case. The same **403** trust-boundary rule from Step 3 applies here: on 403, STOP and ask for a permitted author — never retry with the param omitted.
79
+
80
+ The backend writes the draft in the seller's voice and grounds it on their seller signal internally — you do not pass the evidence yourself.
62
81
 
63
82
  `generate_linkedin_post` creates a new draft row every time it runs. Call it **once** per post. Show the seller the returned draft.
64
83
 
65
- ## Step 5: Iterate by editing — never regenerate
84
+ ## Step 6: Iterate by editing — never regenerate
66
85
 
67
86
  When the seller wants changes (tighten the hook, change the CTA, fix a line), revise the **existing** draft with `manage_content_post` using `action="edit"`, passing the updated `content`.
68
87
 
69
88
  Never call `generate_linkedin_post` again for a revision — that spawns a duplicate draft row and loses the thread. One post = one draft row, edited in place.
70
89
 
71
- ## Step 6: Save draft, schedule, or publish
90
+ `manage_content_post` operates on the existing draft row, which already carries the `author_user_id` you stamped at generation. You do **not** re-pass the author here — schedule/publish inherit it from the row.
91
+
92
+ ## Step 7: Save — draft, schedule, or publish
72
93
 
73
94
  When the seller is happy with the draft, ask how they want to land it. **Default to keeping it as a draft or scheduling it.**
74
95
 
@@ -79,6 +100,15 @@ When the seller is happy with the draft, ask how they want to land it. **Default
79
100
  - Only call `manage_content_post` with `action="publish"` **after** the seller has explicitly confirmed "publish now."
80
101
  - Always offer save-as-draft or schedule as the safer fallback when proposing publish.
81
102
 
103
+ **Author-scoped publish — the backend refuses to fall back to another identity.** Schedule/publish read the author from the draft row. For an author-scoped post (one you authored as a specific person in Step 2, so the draft carries their `author_user_id`), the backend resolves *that person's* LinkedIn account **strictly**: if their account is missing, disconnected, or its `quota` is exhausted at publish time, the publish **fails hard with an `Author account unavailable` error** instead of posting from another connected company account. So an author-scoped post can never silently go out under a *different person's identity* than the one you chose — the server enforces this, including for scheduled posts that publish later at worker execution time (long after this conversation). A legacy / no-author post (you omitted `author_user_id` in Step 2) keeps the old company-wide fallback — there's no specific identity to protect.
104
+
105
+ Because that hard failure lands at publish time — which for a scheduled post can be minutes or hours after you draft it — surface it **early** rather than letting the operator discover a dead, `failed` post later. So before you schedule or publish a post you authored as a specific person, **call `get_channel_status` again — fresh, right now, immediately before the schedule/publish call.** Do **not** trust the Step 2 snapshot: an account can disconnect, change `status`, or exhaust its `quota` during drafting and refinement. Re-read `channels.linkedin_accounts` from this *new* response and find the author's account by the `user_id` you stamped on the draft, then:
106
+
107
+ - If their account is present, `connected`, `status` is healthy, and `quota` is not exhausted **in the fresh response** → proceed with schedule/publish as normal (still behind the explicit "publish now" confirmation above).
108
+ - If their account is **missing, not `connected`, shows a bad `status`, or has an exhausted `quota` in the fresh response** (or the fresh `get_channel_status` call fails / omits `linkedin_accounts`, so you can't confirm the author's account is healthy) → **STOP. Do not schedule or publish.** The backend would reject this author-scoped publish as `Author account unavailable` anyway; tell the operator plainly so they don't end up with a `failed` post. Offer the safe paths: keep it as a draft, reschedule for after that person's account is reconnected / their quota resets, or pick a different permitted author and regenerate. There is no "publish under a different identity" escape hatch for an author-scoped post — the server will not do it; to post from another account the operator must deliberately regenerate the draft under that author (or with no author).
109
+
110
+ For a legacy / no-author post (you omitted `author_user_id` in Step 2) there is no specific author identity to protect, so the standard publish confirmation above is sufficient.
111
+
82
112
  If the seller asks to reschedule a post that is **already scheduled**, be aware that rescheduling an already-scheduled post is not currently supported and may return a "not in draft status" error. Tell the seller this plainly rather than retrying blindly, and offer to keep the existing schedule.
83
113
 
84
114
  ## Notes