clawskills-mcp 0.5.0 → 0.7.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/README.md +11 -0
- package/dist/index.js +130 -5
- package/dist/index.test.js +48 -3
- package/package.json +3 -3
- package/playbooks/INDEX.md +1 -1
- package/playbooks/hubspot-asana-onboarding.md +14 -0
- package/playbooks/salesforce-hubspot-lead-sync.md +14 -0
- package/playbooks/slack-jira-incident.md +113 -0
- package/playbooks/zendesk-jira-bug-escalation.md +14 -0
- package/skills/ROADMAP.md +19 -14
- package/skills/asana/skill.md +18 -1
- package/skills/dynamics365/skill.md +4 -1
- package/skills/figma/skill.md +23 -3
- package/skills/github/skill.md +81 -40
- package/skills/hubspot/skill.md +9 -3
- package/skills/jira/skill.md +9 -2
- package/skills/linear/skill.md +10 -2
- package/skills/monday/skill.md +9 -5
- package/skills/notion/skill.md +47 -14
- package/skills/salesforce/skill.md +31 -25
- package/skills/servicenow/skill.md +20 -14
- package/skills/slack/skill.md +45 -16
- package/skills/stripe/skill.md +11 -2
- package/skills/zendesk/skill.md +14 -1
package/README.md
CHANGED
|
@@ -49,6 +49,9 @@ docker run --rm -i clawskills-mcp
|
|
|
49
49
|
| `list_playbooks` | List all available cross-tool workflow playbooks |
|
|
50
50
|
| `get_playbook` | Fetch a full playbook by slug |
|
|
51
51
|
| `search_playbooks` | Full-text search across all workflow playbooks |
|
|
52
|
+
| `search_clawskills` | Unified search across both skills and playbooks |
|
|
53
|
+
|
|
54
|
+
For most user queries, start with `search_clawskills`. It biases toward playbooks for workflow-shaped searches like `closed won onboarding`, `zendesk jira escalation`, or `lead sync`.
|
|
52
55
|
|
|
53
56
|
### `list_skills`
|
|
54
57
|
|
|
@@ -92,6 +95,14 @@ No arguments. Returns all available playbooks.
|
|
|
92
95
|
|
|
93
96
|
Returns matching excerpts (±3 lines of context) grouped by playbook, capped at 5 matches per playbook.
|
|
94
97
|
|
|
98
|
+
### `search_clawskills`
|
|
99
|
+
|
|
100
|
+
| Argument | Type | Required | Description |
|
|
101
|
+
|----------|------|----------|-------------|
|
|
102
|
+
| `query` | string | yes | Search string, e.g. `"closed won onboarding"`, `"zendesk jira escalation"`, `"lead sync"` |
|
|
103
|
+
|
|
104
|
+
Returns combined results across skills and playbooks. Workflow-oriented queries rank playbooks ahead of generic skill matches.
|
|
105
|
+
|
|
95
106
|
---
|
|
96
107
|
|
|
97
108
|
## Configuration
|
package/dist/index.js
CHANGED
|
@@ -148,9 +148,55 @@ function findSkill(skills, name) {
|
|
|
148
148
|
function findPlaybook(playbooks, name) {
|
|
149
149
|
return findSkill(playbooks, name);
|
|
150
150
|
}
|
|
151
|
+
function parseFrontmatter(content) {
|
|
152
|
+
if (!content.startsWith("---\n"))
|
|
153
|
+
return {};
|
|
154
|
+
const end = content.indexOf("\n---\n", 4);
|
|
155
|
+
if (end === -1)
|
|
156
|
+
return {};
|
|
157
|
+
const block = content.slice(4, end);
|
|
158
|
+
const metadata = {};
|
|
159
|
+
let currentKey = null;
|
|
160
|
+
for (const rawLine of block.split("\n")) {
|
|
161
|
+
const line = rawLine.trimEnd();
|
|
162
|
+
const keyMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
|
|
163
|
+
if (keyMatch) {
|
|
164
|
+
const [, key, value] = keyMatch;
|
|
165
|
+
if (value === "") {
|
|
166
|
+
metadata[key] = [];
|
|
167
|
+
currentKey = key;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
metadata[key] = value.trim();
|
|
171
|
+
currentKey = null;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const listMatch = line.match(/^\s*-\s+(.*)$/);
|
|
176
|
+
if (listMatch && currentKey) {
|
|
177
|
+
const current = metadata[currentKey];
|
|
178
|
+
if (Array.isArray(current)) {
|
|
179
|
+
current.push(listMatch[1].trim());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return metadata;
|
|
184
|
+
}
|
|
185
|
+
function getPlaybookMetadata(content) {
|
|
186
|
+
const frontmatter = parseFrontmatter(content);
|
|
187
|
+
const systems = Array.isArray(frontmatter.systems) ? frontmatter.systems : [];
|
|
188
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
|
|
189
|
+
return {
|
|
190
|
+
title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
|
|
191
|
+
systems,
|
|
192
|
+
tags,
|
|
193
|
+
triggerType: typeof frontmatter.trigger_type === "string" ? frontmatter.trigger_type : undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
151
196
|
function searchDocuments(documents, query) {
|
|
152
197
|
const results = [];
|
|
153
198
|
const lowerQuery = query.toLowerCase();
|
|
199
|
+
const queryTerms = lowerQuery.split(/[^a-z0-9]+/).filter(Boolean);
|
|
154
200
|
const CONTEXT_LINES = 3;
|
|
155
201
|
const MAX_PER_SKILL = 5;
|
|
156
202
|
for (const [name, content] of documents.entries()) {
|
|
@@ -158,7 +204,10 @@ function searchDocuments(documents, query) {
|
|
|
158
204
|
const excerpts = [];
|
|
159
205
|
const usedRanges = [];
|
|
160
206
|
for (let i = 0; i < lines.length; i++) {
|
|
161
|
-
|
|
207
|
+
const lowerLine = lines[i].toLowerCase();
|
|
208
|
+
const matchesExact = lowerLine.includes(lowerQuery);
|
|
209
|
+
const matchesTerm = queryTerms.some((term) => lowerLine.includes(term));
|
|
210
|
+
if (!matchesExact && !matchesTerm)
|
|
162
211
|
continue;
|
|
163
212
|
const start = Math.max(0, i - CONTEXT_LINES);
|
|
164
213
|
const end = Math.min(lines.length - 1, i + CONTEXT_LINES);
|
|
@@ -184,13 +233,59 @@ function searchSkills(skills, query) {
|
|
|
184
233
|
function searchPlaybooks(playbooks, query) {
|
|
185
234
|
return searchDocuments(playbooks, query);
|
|
186
235
|
}
|
|
236
|
+
function searchClawskills(skills, playbooks, query) {
|
|
237
|
+
const normalizedQuery = query.toLowerCase();
|
|
238
|
+
const queryTerms = normalizedQuery.split(/[^a-z0-9]+/).filter(Boolean);
|
|
239
|
+
const workflowHints = new Set([
|
|
240
|
+
"sync", "onboarding", "closed", "won", "handoff", "escalate",
|
|
241
|
+
"escalation", "workflow", "playbook", "incident", "notify",
|
|
242
|
+
]);
|
|
243
|
+
const workflowShaped = queryTerms.some((term) => workflowHints.has(term));
|
|
244
|
+
const skillResults = searchSkills(skills, query).map((result) => ({
|
|
245
|
+
...result,
|
|
246
|
+
contentType: "skill",
|
|
247
|
+
score: result.excerpts.length * 10 + (workflowShaped ? 0 : 5),
|
|
248
|
+
}));
|
|
249
|
+
const playbookResults = searchPlaybooks(playbooks, query).map((result) => {
|
|
250
|
+
const metadata = getPlaybookMetadata(playbooks.get(result.name));
|
|
251
|
+
const metadataText = [metadata.title, ...metadata.systems, ...metadata.tags, metadata.triggerType]
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.join(" ")
|
|
254
|
+
.toLowerCase();
|
|
255
|
+
const metadataMatches = queryTerms.filter((term) => metadataText.includes(term)).length;
|
|
256
|
+
return {
|
|
257
|
+
...result,
|
|
258
|
+
contentType: "playbook",
|
|
259
|
+
score: result.excerpts.length * 10 + metadataMatches * 5 + (workflowShaped ? 15 : 0),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
return [...playbookResults, ...skillResults]
|
|
263
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
264
|
+
.slice(0, 12);
|
|
265
|
+
}
|
|
187
266
|
// ---------------------------------------------------------------------------
|
|
188
267
|
// Skill summary (first meaningful line)
|
|
189
268
|
// ---------------------------------------------------------------------------
|
|
269
|
+
// Playbooks open with a YAML frontmatter block; skills open with an H1 followed
|
|
270
|
+
// by a blockquote. Without the frontmatter handling below, every playbook
|
|
271
|
+
// summary came out as the literal "---" delimiter.
|
|
190
272
|
function skillSummary(content) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
273
|
+
const lines = content.split("\n");
|
|
274
|
+
let start = 0;
|
|
275
|
+
if (lines[0]?.trim() === "---") {
|
|
276
|
+
const closing = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
277
|
+
if (closing !== -1) {
|
|
278
|
+
for (const line of lines.slice(1, closing)) {
|
|
279
|
+
const title = line.match(/^title:\s*(.+)$/);
|
|
280
|
+
if (title)
|
|
281
|
+
return title[1].trim().replace(/^["']|["']$/g, "");
|
|
282
|
+
}
|
|
283
|
+
start = closing + 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const line of lines.slice(start)) {
|
|
287
|
+
const trimmed = line.trim().replace(/^>\s*/, "");
|
|
288
|
+
if (trimmed && trimmed !== "---" && !trimmed.startsWith("#"))
|
|
194
289
|
return trimmed;
|
|
195
290
|
}
|
|
196
291
|
return "";
|
|
@@ -198,7 +293,7 @@ function skillSummary(content) {
|
|
|
198
293
|
// ---------------------------------------------------------------------------
|
|
199
294
|
// Exports for testing
|
|
200
295
|
// ---------------------------------------------------------------------------
|
|
201
|
-
export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, };
|
|
296
|
+
export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, searchClawskills, getPlaybookMetadata, skillSummary, };
|
|
202
297
|
// ---------------------------------------------------------------------------
|
|
203
298
|
// MCP Server setup
|
|
204
299
|
// ---------------------------------------------------------------------------
|
|
@@ -269,6 +364,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
269
364
|
required: ["query"],
|
|
270
365
|
},
|
|
271
366
|
},
|
|
367
|
+
{
|
|
368
|
+
name: "search_clawskills",
|
|
369
|
+
description: "Search across both skills and playbooks. For workflow-shaped queries, playbooks are ranked ahead of generic skill matches.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
query: { type: "string", description: "Search query, e.g. 'closed won onboarding', 'zendesk jira escalation', 'lead sync'" },
|
|
374
|
+
},
|
|
375
|
+
required: ["query"],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
272
378
|
],
|
|
273
379
|
}));
|
|
274
380
|
// Call tools
|
|
@@ -276,6 +382,7 @@ const GetSkillSchema = z.object({ name: z.string(), section: z.string().optional
|
|
|
276
382
|
const GetPlaybookSchema = z.object({ name: z.string() });
|
|
277
383
|
const SearchSkillsSchema = z.object({ query: z.string() });
|
|
278
384
|
const SearchPlaybooksSchema = z.object({ query: z.string() });
|
|
385
|
+
const SearchClawskillsSchema = z.object({ query: z.string() });
|
|
279
386
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
280
387
|
const { name, arguments: args } = request.params;
|
|
281
388
|
if (name === "list_skills") {
|
|
@@ -373,6 +480,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
373
480
|
.join("\n\n---\n\n");
|
|
374
481
|
return { content: [{ type: "text", text: `Playbook search results for "${query}":\n\n${output}` }] };
|
|
375
482
|
}
|
|
483
|
+
if (name === "search_clawskills") {
|
|
484
|
+
const parsed = SearchClawskillsSchema.safeParse(args);
|
|
485
|
+
if (!parsed.success) {
|
|
486
|
+
return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
|
|
487
|
+
}
|
|
488
|
+
const { query } = parsed.data;
|
|
489
|
+
const results = searchClawskills(skills, playbooks, query);
|
|
490
|
+
if (results.length === 0) {
|
|
491
|
+
return { content: [{ type: "text", text: `No results found for "${query}".` }] };
|
|
492
|
+
}
|
|
493
|
+
const output = results
|
|
494
|
+
.map(({ name: itemName, contentType, excerpts }) => {
|
|
495
|
+
const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
|
|
496
|
+
return `### [${contentType}] ${itemName}\n\n${blocks}`;
|
|
497
|
+
})
|
|
498
|
+
.join("\n\n---\n\n");
|
|
499
|
+
return { content: [{ type: "text", text: `Unified search results for "${query}":\n\n${output}` }] };
|
|
500
|
+
}
|
|
376
501
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
377
502
|
});
|
|
378
503
|
// ---------------------------------------------------------------------------
|
package/dist/index.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, } from "./index.js";
|
|
6
|
+
import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, searchClawskills, getPlaybookMetadata, skillSummary, } from "./index.js";
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const REAL_SKILLS_DIR = path.resolve(__dirname, "../../skills");
|
|
9
9
|
const REAL_PLAYBOOKS_DIR = path.resolve(__dirname, "../../playbooks");
|
|
@@ -56,8 +56,8 @@ let tempPlaybooksDir;
|
|
|
56
56
|
beforeAll(() => {
|
|
57
57
|
tempDir = makeTempSkillsDir({ github: SAMPLE_CONTENT, salesforce: "# Salesforce\nCRM platform.\n## Overview\nSalesforce is a CRM." });
|
|
58
58
|
tempPlaybooksDir = makeTempPlaybooksDir({
|
|
59
|
-
"zendesk-jira": "# Zendesk -> Jira\n\nEscalate bugs from support into engineering.\n\n## Idempotency\n\nUse zendesk:{ticket_id}.",
|
|
60
|
-
"hubspot-asana": "# HubSpot -> Asana\n\nCreate onboarding work from closed won deals.",
|
|
59
|
+
"zendesk-jira": "---\ntitle: Zendesk -> Jira\nsystems:\n - zendesk\n - jira\ntags:\n - support\n - escalation\ntrigger_type: webhook\n---\n# Zendesk -> Jira\n\nEscalate bugs from support into engineering.\n\n## Idempotency\n\nUse zendesk:{ticket_id}.",
|
|
60
|
+
"hubspot-asana": "---\ntitle: HubSpot -> Asana\nsystems:\n - hubspot\n - asana\ntags:\n - onboarding\n - handoff\ntrigger_type: webhook\n---\n# HubSpot -> Asana\n\nCreate onboarding work from closed won deals.",
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
afterAll(() => {
|
|
@@ -229,6 +229,32 @@ describe("searchPlaybooks", () => {
|
|
|
229
229
|
expect(results[0].name).toBe("zendesk-jira");
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
|
+
describe("getPlaybookMetadata", () => {
|
|
233
|
+
it("parses frontmatter metadata from playbooks", () => {
|
|
234
|
+
const playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
235
|
+
const metadata = getPlaybookMetadata(playbooks.get("zendesk-jira"));
|
|
236
|
+
expect(metadata.systems).toContain("zendesk");
|
|
237
|
+
expect(metadata.tags).toContain("escalation");
|
|
238
|
+
expect(metadata.triggerType).toBe("webhook");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe("searchClawskills", () => {
|
|
242
|
+
it("prefers playbooks for workflow-shaped queries", () => {
|
|
243
|
+
const skills = new Map([["hubspot", "HubSpot CRM API.\nUse for contacts and deals."]]);
|
|
244
|
+
const playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
245
|
+
const results = searchClawskills(skills, playbooks, "closed won onboarding");
|
|
246
|
+
expect(results.length).toBeGreaterThan(0);
|
|
247
|
+
expect(results[0].contentType).toBe("playbook");
|
|
248
|
+
expect(results[0].name).toBe("hubspot-asana");
|
|
249
|
+
});
|
|
250
|
+
it("still returns skill hits for non-workflow queries", () => {
|
|
251
|
+
const skills = loadSkills(tempDir);
|
|
252
|
+
const playbooks = loadPlaybooks(tempPlaybooksDir);
|
|
253
|
+
const results = searchClawskills(skills, playbooks, "Personal Access Token");
|
|
254
|
+
expect(results.length).toBeGreaterThan(0);
|
|
255
|
+
expect(results.some((result) => result.contentType === "skill" && result.name === "github")).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
232
258
|
// ---------------------------------------------------------------------------
|
|
233
259
|
// skillSummary
|
|
234
260
|
// ---------------------------------------------------------------------------
|
|
@@ -245,6 +271,25 @@ describe("skillSummary", () => {
|
|
|
245
271
|
const summary = skillSummary("# Title\n## Subtitle\n");
|
|
246
272
|
expect(summary).toBe("");
|
|
247
273
|
});
|
|
274
|
+
it("uses the frontmatter title instead of the --- delimiter", () => {
|
|
275
|
+
const summary = skillSummary("---\ntitle: Slack Incident -> Jira Issue\nsystems:\n - slack\n---\n\n# Heading\n\nBody text.");
|
|
276
|
+
expect(summary).toBe("Slack Incident -> Jira Issue");
|
|
277
|
+
});
|
|
278
|
+
it("falls through past frontmatter that has no title", () => {
|
|
279
|
+
const summary = skillSummary("---\nsystems:\n - slack\n---\n\n# Heading\n\nBody text.");
|
|
280
|
+
expect(summary).toBe("Body text.");
|
|
281
|
+
});
|
|
282
|
+
it("strips the blockquote marker from skill headers", () => {
|
|
283
|
+
const summary = skillSummary("# Jira Skill\n\n> **Last validated:** 2026-08-02\n");
|
|
284
|
+
expect(summary).toBe("**Last validated:** 2026-08-02");
|
|
285
|
+
});
|
|
286
|
+
it("gives every real playbook a summary that is not the --- delimiter", () => {
|
|
287
|
+
for (const [slug, content] of loadPlaybooks(REAL_PLAYBOOKS_DIR)) {
|
|
288
|
+
const summary = skillSummary(content);
|
|
289
|
+
expect(summary, `${slug} has no usable summary`).not.toBe("");
|
|
290
|
+
expect(summary, `${slug} summary is the frontmatter delimiter`).not.toBe("---");
|
|
291
|
+
}
|
|
292
|
+
});
|
|
248
293
|
});
|
|
249
294
|
// ---------------------------------------------------------------------------
|
|
250
295
|
// Real skills validation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawskills-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server exposing ClawSkills API integration skill docs to AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ClawSkills Contributors",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"zod": "^3.23.8"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
54
|
"typescript": "^5.6.0",
|
|
55
55
|
"vitest": "^2.0.0"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
58
|
+
"node": ">=24"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/playbooks/INDEX.md
CHANGED
|
@@ -9,6 +9,7 @@ Workflow playbooks are end-to-end guides for multi-system automations. They comp
|
|
|
9
9
|
| [Zendesk Ticket -> Jira Bug escalation](./zendesk-jira-bug-escalation.md) | Zendesk, Jira | Escalate support-confirmed defects into engineering workflow |
|
|
10
10
|
| [HubSpot Deal Won -> Asana onboarding kickoff](./hubspot-asana-onboarding.md) | HubSpot, Asana | Start implementation or onboarding work when a deal closes |
|
|
11
11
|
| [Salesforce Lead -> HubSpot Contact sync](./salesforce-hubspot-lead-sync.md) | Salesforce, HubSpot | Mirror new/updated leads into marketing automation without duplicates |
|
|
12
|
+
| [Slack Incident -> Jira Issue](./slack-jira-incident.md) | Slack, Jira | Convert in-chat incident escalations into tracked engineering work items |
|
|
12
13
|
|
|
13
14
|
## Playbook template
|
|
14
15
|
|
|
@@ -25,7 +26,6 @@ Each playbook should define:
|
|
|
25
26
|
|
|
26
27
|
## Next candidates
|
|
27
28
|
|
|
28
|
-
- Slack incident -> Jira issue
|
|
29
29
|
- GitHub PR -> Slack notification
|
|
30
30
|
- Zendesk -> Salesforce case sync
|
|
31
31
|
- Stripe payment failed -> HubSpot task
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: HubSpot Deal Won -> Asana Onboarding Kickoff
|
|
3
|
+
systems:
|
|
4
|
+
- hubspot
|
|
5
|
+
- asana
|
|
6
|
+
tags:
|
|
7
|
+
- crm
|
|
8
|
+
- onboarding
|
|
9
|
+
- handoff
|
|
10
|
+
- delivery
|
|
11
|
+
- revops
|
|
12
|
+
trigger_type: webhook
|
|
13
|
+
---
|
|
14
|
+
|
|
1
15
|
# HubSpot Deal Won -> Asana Onboarding Kickoff
|
|
2
16
|
|
|
3
17
|
## Goal
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Salesforce Lead -> HubSpot Contact Sync
|
|
3
|
+
systems:
|
|
4
|
+
- salesforce
|
|
5
|
+
- hubspot
|
|
6
|
+
tags:
|
|
7
|
+
- crm
|
|
8
|
+
- sync
|
|
9
|
+
- marketing
|
|
10
|
+
- lead
|
|
11
|
+
- revops
|
|
12
|
+
trigger_type: scheduled_sync
|
|
13
|
+
---
|
|
14
|
+
|
|
1
15
|
# Salesforce Lead -> HubSpot Contact Sync
|
|
2
16
|
|
|
3
17
|
## Goal
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Slack Incident -> Jira Issue
|
|
3
|
+
systems:
|
|
4
|
+
- slack
|
|
5
|
+
- jira
|
|
6
|
+
tags:
|
|
7
|
+
- incident
|
|
8
|
+
- support
|
|
9
|
+
- engineering
|
|
10
|
+
- escalation
|
|
11
|
+
- on-call
|
|
12
|
+
trigger_type: webhook_event
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Slack Incident -> Jira Issue
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
Capture an incident discussion happening in Slack as a tracked Jira issue, so engineering, on-call, and leadership can follow resolution outside the noisy chat surface. The Slack message remains the conversation record; Jira becomes the accountable work item.
|
|
20
|
+
|
|
21
|
+
## Systems involved
|
|
22
|
+
|
|
23
|
+
- Slack: incident detection, real-time discussion, customer-impact reports
|
|
24
|
+
- Jira: durable tracking, engineering ownership, link to postmortem
|
|
25
|
+
- (Optional) PagerDuty or Opsgenie: paging — usually upstream of this playbook, not driven by it
|
|
26
|
+
|
|
27
|
+
## Trigger
|
|
28
|
+
|
|
29
|
+
Any of:
|
|
30
|
+
- A Slack message receives a designated reaction emoji (e.g. `:jira:` or `:rotating_light:`) — fired via Slack `reaction_added` event
|
|
31
|
+
- A Slack slash command (e.g. `/incident <summary>`) invoked in any channel
|
|
32
|
+
- A message posted in a channel matching `incident-*` with a designated keyword prefix
|
|
33
|
+
|
|
34
|
+
Reaction-based triggers are the most reliable and lowest-friction; slash commands are best when summary capture must be deliberate.
|
|
35
|
+
|
|
36
|
+
## Source of truth
|
|
37
|
+
|
|
38
|
+
- Incident discussion / customer-impact reports: Slack (channel + thread)
|
|
39
|
+
- Engineering execution state, severity, owner, resolution: Jira
|
|
40
|
+
- Linkage: Slack permalink in Jira description; Jira key replied to the Slack thread
|
|
41
|
+
|
|
42
|
+
## Sequence
|
|
43
|
+
|
|
44
|
+
1. Receive the inbound request — either a **Slack Events API event** (`reaction_added` or `message`) delivered as JSON with the event wrapped under an `event` object, OR a **Slash Command HTTP request** (separate endpoint, form-encoded body with `channel_id`, `user_id`, `text`, `trigger_id` at the top level — no `event` wrapper and no `event_id`). Both are signed by Slack; verify the request with `X-Slack-Signature` and `X-Slack-Request-Timestamp` (5-minute window).
|
|
45
|
+
2. **Ack with HTTP 200 immediately after signature verification — on every delivery, not just retries.** Slack Events API enforces a 3-second response budget on first delivery and on retries; missing it causes Slack to redeliver the event and creates avoidable duplicate work. Hand the remaining steps off to an async worker / queue. If a retry header is present (`X-Slack-Retry-Num`), treat it as an expected duplicate signal — still ack 200 and let the dedupe step catch it. (Slash Command requests don't carry retries but still benefit from immediate ack so the user sees a fast response.)
|
|
46
|
+
3. Build a deterministic correlation key: `slack:{team_id}:{channel_id}:{ts}`. The `{ts}` source depends on the trigger:
|
|
47
|
+
- **`reaction_added`:** `event.item.ts` — the timestamp of the message that was reacted to.
|
|
48
|
+
- **`message`:** `event.ts` — the original message timestamp; for replies posted into a thread, prefer `event.thread_ts` so the key anchors to the parent message and all in-thread activity collapses to one issue.
|
|
49
|
+
- **Slash Command:** no inbound `message_ts` exists. Either (a) require the command be invoked from inside a thread and use `thread_ts` from the command payload, or (b — recommended default) the async worker first posts a bot anchor message via `chat.postMessage` and uses that posted message's `ts`. Option (b) requires no user training and gives the thread reply a natural place to land.
|
|
50
|
+
4. Fetch the originating message and thread context via `conversations.history` (with `latest=ts`, `inclusive=true`, `limit=1`) and `conversations.replies` for thread.
|
|
51
|
+
5. Resolve `permalink` for the message via `chat.getPermalink`.
|
|
52
|
+
6. Search Jira for an existing issue carrying that correlation key (label or custom field). If found, skip creation and reuse the Jira key.
|
|
53
|
+
7. If no existing issue, create a Jira issue (`POST /rest/api/3/issue`) with:
|
|
54
|
+
- `summary` derived from the message text (first sentence, truncated to ~120 chars).
|
|
55
|
+
- `description` containing the Slack permalink, the message text, and up to N thread replies for context.
|
|
56
|
+
- `priority`, `issuetype`, `project`, `components` derived from per-channel config or sensible defaults.
|
|
57
|
+
- A label `slack-incident` plus the correlation key as a label or custom-field value.
|
|
58
|
+
8. Reply in the Slack thread via `chat.postMessage` with the Jira issue link, key, and current assignee.
|
|
59
|
+
9. (Optional) Pin the bot reply if the channel is an incident channel.
|
|
60
|
+
10. (Optional) Mirror selected Jira status transitions back to Slack via `chat.postMessage` once a status mapping is finalized.
|
|
61
|
+
|
|
62
|
+
## Field mapping
|
|
63
|
+
|
|
64
|
+
| Slack | Jira |
|
|
65
|
+
|-------|------|
|
|
66
|
+
| Correlation source (varies by trigger): `reaction_added` → `event.item.channel` + `event.item.ts`; `message` → `event.channel` + `event.ts` (use `event.thread_ts` for in-thread replies); Slash Command → command payload `channel_id` + `ts` of the bot's anchor message (or `thread_ts` if invoked inside a thread) | correlation key `slack:{team_id}:{channel_id}:{ts}` stored in label / custom field |
|
|
67
|
+
| message text (first sentence) | `summary` |
|
|
68
|
+
| message text + thread replies + permalink | `description` — Jira Cloud REST v3 requires **ADF (Atlassian Document Format) JSON**; convert Slack text + thread replies into an ADF document (see `skills/jira/skill.md` § ADF) |
|
|
69
|
+
| reactor user / slash command invoker | `reporter` (after Slack-user -> Atlassian-account-id mapping) |
|
|
70
|
+
| channel name or category | `components` or labels |
|
|
71
|
+
| keyword/severity from text | `priority` (P1/P2/P3) |
|
|
72
|
+
| message permalink | external link field or `description` |
|
|
73
|
+
|
|
74
|
+
## Idempotency
|
|
75
|
+
|
|
76
|
+
- **Dedup key:** `slack:{team_id}:{channel_id}:{ts}` (same format as the correlation key in Sequence step 3) stored as a Jira label and/or a custom field for searchability.
|
|
77
|
+
- Slack delivers Events API events at least once. Dedupe at the edge using the **payload field `event_id`** (present in the JSON wrapper, not an HTTP header). Slash command requests have **no `event_id`** — dedupe those by correlation key alone. Always re-dedupe by correlation key immediately before the Jira create call.
|
|
78
|
+
- If multiple users react with the trigger emoji on the same message, only the first reaction creates the issue; subsequent reactions are noops (or append a reactor list to the Jira issue).
|
|
79
|
+
- Slash command invocations must check correlation by `channel_id + thread_ts` first to avoid duplicating during incidents.
|
|
80
|
+
|
|
81
|
+
## Retry and partial failure policy
|
|
82
|
+
|
|
83
|
+
- **Slack rate limits** are per-method tier (Tier 1: 1+/min, Tier 4: 100+/min). On `429`, respect the `Retry-After` header — do not retry sooner.
|
|
84
|
+
- **Jira rate limits** use cost-based throttling. On `429` or `503`, use bounded exponential backoff (base 1s, max 60s, max 5 attempts).
|
|
85
|
+
- Do not blindly retry Jira `400` validation errors — log, surface to operators, and quarantine the event.
|
|
86
|
+
- If Jira create succeeds but the Slack thread reply fails, do not retry Jira create. Reconcile by searching Jira for the correlation key and posting the thread reply on the next event or reconciliation pass.
|
|
87
|
+
- Slack event endpoints must return 200 within 3 seconds. Push work to a queue if create + reply exceeds that budget.
|
|
88
|
+
|
|
89
|
+
## Reconciliation
|
|
90
|
+
|
|
91
|
+
- Hourly or daily job: scan recent messages in channels matching `incident-*` for trigger reactions/keywords and confirm a Jira issue with the matching correlation key exists. Create any missing issues.
|
|
92
|
+
- Reverse pass: list Jira issues with label `slack-incident` updated in the last 24h; verify each has at least one Slack thread reply with the Jira link. Repair missing replies.
|
|
93
|
+
- Drift check: confirm Jira issues marked `Done` have a corresponding Slack thread reply announcing closure (if status mirroring is enabled).
|
|
94
|
+
|
|
95
|
+
## Rollback guidance
|
|
96
|
+
|
|
97
|
+
- If a Jira issue was created in error, transition it to a closed/cancelled state (`Won't Do`, `Cancelled`) rather than deleting it — preserves the dedup key so an accidental re-reaction does not recreate it.
|
|
98
|
+
- Remove the trigger reaction in Slack via `reactions.remove` (cosmetic) and add a `:white_check_mark:` reply explaining the cancellation.
|
|
99
|
+
- Add the correlation key to a deny-list table to prevent re-creation if reactions get added again.
|
|
100
|
+
|
|
101
|
+
## Observability
|
|
102
|
+
|
|
103
|
+
- Time from reaction to Jira issue creation (target: < 5s p95).
|
|
104
|
+
- Daily count of incidents created, by channel and by reactor.
|
|
105
|
+
- Duplicate-detection rate (events that would have created duplicates but were short-circuited).
|
|
106
|
+
- Failed Slack signature verifications (anomalous — investigate).
|
|
107
|
+
- Failed Jira creates, broken down by HTTP status and error code.
|
|
108
|
+
- Lag between Jira closure and Slack-thread closure message.
|
|
109
|
+
|
|
110
|
+
## Related skills
|
|
111
|
+
|
|
112
|
+
- [`skills/slack/skill.md`](../skills/slack/skill.md)
|
|
113
|
+
- [`skills/jira/skill.md`](../skills/jira/skill.md)
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Zendesk Ticket -> Jira Bug Escalation
|
|
3
|
+
systems:
|
|
4
|
+
- zendesk
|
|
5
|
+
- jira
|
|
6
|
+
tags:
|
|
7
|
+
- support
|
|
8
|
+
- engineering
|
|
9
|
+
- escalation
|
|
10
|
+
- incident
|
|
11
|
+
- handoff
|
|
12
|
+
trigger_type: webhook_or_incremental_sync
|
|
13
|
+
---
|
|
14
|
+
|
|
1
15
|
# Zendesk Ticket -> Jira Bug Escalation
|
|
2
16
|
|
|
3
17
|
## Goal
|
package/skills/ROADMAP.md
CHANGED
|
@@ -19,9 +19,9 @@ This skill set enables end-to-end automation and integration across the most com
|
|
|
19
19
|
| Layer | Status |
|
|
20
20
|
|-------|--------|
|
|
21
21
|
| **14 skill docs** — Monday.com, Salesforce, Jira, Dynamics 365, HubSpot, ServiceNow, Zendesk, Asana, GitHub, Figma, Slack, Stripe, Notion, Linear | ✅ Live |
|
|
22
|
-
| **MCP server** — `clawskills-mcp` v0.
|
|
22
|
+
| **MCP server** — `clawskills-mcp` v0.7.0 on npm; `list_skills`, `get_skill`, `search_skills` | ✅ Live |
|
|
23
23
|
| **CI pipeline** — build + test on every push/PR via `ci.yml` | ✅ Live |
|
|
24
|
-
| **Release automation** — `release.yml` workflow_dispatch →
|
|
24
|
+
| **Release automation** — `prepare-release.yml` workflow_dispatch → release PR; `release.yml` on merge → npm publish via OIDC + tag | ✅ Live |
|
|
25
25
|
| **Test suite** — Vitest unit tests + real-skills validation (required sections, ≥5 KB, all 14 tools) | ✅ Live |
|
|
26
26
|
| **Public repo readiness** — MIT license, `.gitignore`, full `package.json` metadata | ✅ Ready |
|
|
27
27
|
|
|
@@ -83,7 +83,7 @@ The repo now needs tighter alignment between the canonical `skills/` directory,
|
|
|
83
83
|
| Channel | Status | Next action |
|
|
84
84
|
|---------|--------|-------------|
|
|
85
85
|
| GitHub (public) | Ready to flip | Make repo public |
|
|
86
|
-
| npm (`clawskills-mcp`) | ✅ Live — v0.
|
|
86
|
+
| npm (`clawskills-mcp`) | ✅ Live — v0.7.0 | Maintain via `release.yml` |
|
|
87
87
|
| MCP registry listings | Not submitted | Submit to glama.ai, mcp.so, smithery.ai after public launch |
|
|
88
88
|
| Docs website | Planned | Mintlify — fastest path to SEO-friendly browsable docs |
|
|
89
89
|
| Lane A vs B | TBD | Revisit after first traction signals from public launch |
|
|
@@ -175,18 +175,19 @@ Each skill/recipe is scored on five dimensions (1–5 each):
|
|
|
175
175
|
- **Freshness pipeline:** Weekly cron checks `Last validated:` dates (see Priority 2 above).
|
|
176
176
|
- **Live smoke tests:** Optional phase B — weekly auth + read tests against sandboxes.
|
|
177
177
|
- **Breaking change monitoring:** Subscribe to each vendor's developer changelog:
|
|
178
|
-
- Monday: https://developer.monday.com/changelog
|
|
179
|
-
- Salesforce: https://
|
|
178
|
+
- Monday: https://developer.monday.com/api-reference/docs/api-versioning (versioning table) + https://developer.monday.com/changelog (entries)
|
|
179
|
+
- Salesforce: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/rest_rns.htm — ⚠️ JS-rendered: the content body is injected client-side, so plain HTTP fetchers (curl, wget, GitHub Actions cron jobs without a JS engine) return only the page shell. Read it in a real browser, or use a headless-browser tool (Playwright/Puppeteer) — see https://github.com/Shanksg/clawskills/issues/12.
|
|
180
|
+
- **Workaround (found 2026-08-02):** https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_versions.htm **is** plain-fetchable and lists the highest published API version with its release name. That covers "did a new version ship?" — the most common freshness question — without a browser. Only the delta review still needs one.
|
|
180
181
|
- Jira: https://developer.atlassian.com/cloud/jira/platform/changelog/
|
|
181
|
-
- Dynamics 365: https://learn.microsoft.com/en-us/dynamics365/
|
|
182
|
+
- Dynamics 365: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/web-api-versions (Web API version differences) + https://learn.microsoft.com/en-us/dynamics365/released-versions/microsoft-dataverse (weekly release notes)
|
|
182
183
|
- HubSpot: https://developers.hubspot.com/changelog
|
|
183
|
-
- ServiceNow: https://
|
|
184
|
-
- Zendesk: https://developer.zendesk.com/api-reference/
|
|
185
|
-
- Asana: https://
|
|
186
|
-
- GitHub: https://github.blog/changelog/
|
|
187
|
-
- Figma: https://
|
|
188
|
-
- Slack: https://api.slack.com/changelog
|
|
189
|
-
- Stripe: https://stripe.com/docs/changelog
|
|
184
|
+
- ServiceNow: https://www.servicenow.com/docs/bundle/australia-release-notes/page/release-notes/family-release-notes.html (current named release — rotate this URL on each new release; https://www.servicenow.com/docs/r/release-notes/available-versions.html lists which family is current)
|
|
185
|
+
- Zendesk: https://developer.zendesk.com/api-reference/changelog/changelog/
|
|
186
|
+
- Asana: https://forum.asana.com/c/forum-en/api/api-change-log/204 — Asana hosts their dated API changelog as a forum category (individual entries are threads under it); the developer-portal page just redirects here
|
|
187
|
+
- GitHub: https://github.blog/changelog/ (entries) + https://docs.github.com/en/rest/about-the-rest-api/breaking-changes (per-version breaking-change list — the fetchable source of truth for REST API versions)
|
|
188
|
+
- Figma: https://developers.figma.com/docs/rest-api/changelog/ (moved from `figma.com/developers/api#changelog`, which now redirects)
|
|
189
|
+
- Slack: https://docs.slack.dev/changelog (moved from `api.slack.com/changelog`, which now redirects)
|
|
190
|
+
- Stripe: https://docs.stripe.com/changelog (moved from `stripe.com/docs/changelog`, which now redirects)
|
|
190
191
|
- Notion: https://developers.notion.com/page/changelog
|
|
191
192
|
- Linear: https://linear.app/changelog
|
|
192
193
|
|
|
@@ -202,7 +203,11 @@ Each skill/recipe is scored on five dimensions (1–5 each):
|
|
|
202
203
|
- PRs to `skill.md` files require: (a) reference to official doc, (b) tested curl/pseudocode example, (c) updated `Last validated:` date.
|
|
203
204
|
- Branch convention: `skill/<toolname>` → PR → merge to main.
|
|
204
205
|
- CI runs `npm test` on every PR — validates skill loads and has all required sections.
|
|
205
|
-
- Releases
|
|
206
|
+
- Releases (two stages — `main` is protected, so nothing can be pushed to it directly):
|
|
207
|
+
1. GitHub Actions → **Prepare Release** → Run workflow → pick `patch / minor / major`. Opens a `release/vX.Y.Z` PR with the version bump; publishes nothing.
|
|
208
|
+
2. Merge that PR. **Release** publishes to npm via OIDC and tags `vX.Y.Z`.
|
|
209
|
+
|
|
210
|
+
> `release.yml` must keep that exact filename — npm's trusted publisher config pins it. Renaming it breaks publishing with a 404.
|
|
206
211
|
|
|
207
212
|
---
|
|
208
213
|
|
package/skills/asana/skill.md
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
# Asana Skill
|
|
2
2
|
|
|
3
|
-
> **Last validated:** 2026-02
|
|
3
|
+
> **Last validated:** 2026-08-02 | **API:** Asana REST API 1.0
|
|
4
4
|
> **REST base URL:** `https://app.asana.com/api/1.0`
|
|
5
5
|
> **Assumed product:** Asana cloud. Enterprise controls, SCIM, and admin APIs may require additional scopes and plan-specific access.
|
|
6
|
+
>
|
|
7
|
+
> **⚠️ Changes since 2026-02-19:**
|
|
8
|
+
> - **Out of Office API (2026-04-27):** new endpoints replace the legacy `vacation_dates` field, which is now **deprecated** on the user object. Migrate any code that reads/writes `vacation_dates`.
|
|
9
|
+
> - **RBAC Role API (2026-02-27):** new CRUD endpoints for managing custom roles on Enterprise plans.
|
|
10
|
+
> - **Timesheet Approval Status API (2026-03-20)** and **Categories for Time Tracking Entries (2026-04-01):** new time-tracking primitives — useful for ops/finance workflows.
|
|
11
|
+
> - **Project portfolio + advanced project search APIs (2026-03-23):** new endpoints for portfolio settings and advanced project filtering.
|
|
12
|
+
> - **Custom Types in Memberships (2026-03-31):** memberships endpoints now support custom-type filters.
|
|
13
|
+
> - **Goals API (2026-04-06):** goals now expose privacy settings, default access levels, and stories.
|
|
14
|
+
> - **Enhanced filtering on the Search API (2026-04-06):** custom task types now usable as filter dimensions.
|
|
15
|
+
>
|
|
16
|
+
> **⚠️ Changes since 2026-05:**
|
|
17
|
+
> - **Granular RBAC permissions (2026-07-02):** finer-grained role-based access control, extending the Role API above.
|
|
18
|
+
> - **AI Studio Usage API (2026-07-02):** new endpoint for tracking AI Studio consumption.
|
|
19
|
+
> - **`sort_by=relevance` on project search (2026-06-04):** relevance ordering now available alongside the existing sort options.
|
|
20
|
+
> - **AI teammates / agent API support announced (2026-05-28)** — not yet GA; no action required, but expect new actor types on tasks and stories.
|
|
21
|
+
>
|
|
22
|
+
> **Source:** https://forum.asana.com/c/forum-en/api/api-change-log/204
|
|
6
23
|
|
|
7
24
|
## What this skill enables
|
|
8
25
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Microsoft Dynamics 365 Skill
|
|
2
2
|
|
|
3
|
-
> **Last validated:** 2026-02
|
|
3
|
+
> **Last validated:** 2026-08-02 | **API:** Dataverse Web API (OData v4) v9.2
|
|
4
4
|
> **Base URL:** `https://{org-name}.crm.dynamics.com/api/data/v9.2/`
|
|
5
5
|
> **Assumed product:** Dynamics 365 Sales. Other apps (Customer Service, Field Service) share the same Web API pattern.
|
|
6
|
+
>
|
|
7
|
+
> **Note:** Re-confirmed on 2026-08-02: v9.0, v9.1, and v9.2 still have identical Web API behavior with no breaking changes. No v9.3 or v10 announced. The 2026 release waves are PowerApps/Copilot features, not Web API surface changes. Microsoft's own guidance is to pin the version that was current when you wrote the code and test before moving up, rather than tracking the newest automatically.
|
|
8
|
+
> **Source:** https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/web-api-versions (page revision 2026-03-27)
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|